diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cf0275220..c709badc4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -94,6 +94,10 @@ jobs: path: | benchmarks/results/f2py.json benchmarks/results/prik.json + benchmarks/results/f2py-prik-first.json + benchmarks/results/f2py-f2py-first.json + benchmarks/results/prik-prik-first.json + benchmarks/results/prik-f2py-first.json benchmarks/results/f2py-build.json benchmarks/results/prik-build.json docs/user/performance.md diff --git a/.github/workflows/merge-validation.yml b/.github/workflows/merge-validation.yml index 67de6faee..1e55ee552 100644 --- a/.github/workflows/merge-validation.yml +++ b/.github/workflows/merge-validation.yml @@ -44,7 +44,7 @@ jobs: - name: Ruff format run: python -m ruff format --check . - name: Wrapper-plan generator contracts - run: python tools/check_wrapper_codegen_complexity.py + run: python tools/check_codegen_complexity.py - name: Bandit security scan run: python -m bandit -c pyproject.toml -r prik --severity-level medium --confidence-level medium - name: Vulture dead-code scan @@ -413,7 +413,7 @@ jobs: python tools/run_fortran_toolchain_lane.py \ --compiler gfortran \ --junit-dir "$RUNNER_TEMP" - - name: Run full suite without BLAS or LAPACK + - name: Run full suite without real-library examples env: PYTHONPATH: . HYPOTHESIS_PROFILE: ci @@ -437,7 +437,7 @@ jobs: done native-libraries: - name: BLAS + LAPACK · Ubuntu 24.04 · Python 3.12 + name: BLAS + LAPACK + FFTPACK + MINPACK · Ubuntu 24.04 · Python 3.12 needs: [unit-tests, unit-tests-macos] if: >- ${{ !contains(github.event.pull_request.labels.*.name, 'ignore-real-library-wrappers') }} @@ -520,6 +520,20 @@ jobs: run: | source examples/lapack/build_all.sh python -m pytest -q examples/lapack/tests examples/lapack/ci/full_surface.py + - name: Run FFTPACK 31-procedure full-surface audit + env: + PYTHONPATH: . + HYPOTHESIS_PROFILE: ci + run: | + source examples/fftpack/build_all.sh + python -m pytest -q examples/fftpack/tests + - name: Run MINPACK 22-procedure and parameter-array full-surface audit + env: + PYTHONPATH: . + HYPOTHESIS_PROFILE: ci + run: | + source examples/minpack/build_all.sh + python -m pytest -q examples/minpack/tests documentation-benchmark: name: Documentation performance benchmark · Ubuntu 24.04 ARM64 · Python 3.12 @@ -598,6 +612,10 @@ jobs: path: | benchmarks/results/f2py.json benchmarks/results/prik.json + benchmarks/results/f2py-prik-first.json + benchmarks/results/f2py-f2py-first.json + benchmarks/results/prik-prik-first.json + benchmarks/results/prik-f2py-first.json benchmarks/results/f2py-build.json benchmarks/results/prik-build.json docs/user/performance.md diff --git a/.github/workflows/blas-lapack.yml b/.github/workflows/real-libraries.yml similarity index 85% rename from .github/workflows/blas-lapack.yml rename to .github/workflows/real-libraries.yml index 9849c7eef..e93a51154 100644 --- a/.github/workflows/blas-lapack.yml +++ b/.github/workflows/real-libraries.yml @@ -1,4 +1,4 @@ -name: Native Libraries +name: Real Libraries on: push: @@ -12,7 +12,7 @@ env: jobs: real-library-wrappers: - name: BLAS + LAPACK · Ubuntu 24.04 · Python 3.12 + name: BLAS + LAPACK + FFTPACK + MINPACK · Ubuntu 24.04 · Python 3.12 if: >- ${{ github.event_name != 'pull_request' || @@ -97,3 +97,17 @@ jobs: run: | source examples/lapack/build_all.sh python -m pytest -q examples/lapack/tests examples/lapack/ci/full_surface.py + - name: Run FFTPACK 31-procedure full-surface audit + env: + PYTHONPATH: . + HYPOTHESIS_PROFILE: ci + run: | + source examples/fftpack/build_all.sh + python -m pytest -q examples/fftpack/tests + - name: Run MINPACK 22-procedure and parameter-array full-surface audit + env: + PYTHONPATH: . + HYPOTHESIS_PROFILE: ci + run: | + source examples/minpack/build_all.sh + python -m pytest -q examples/minpack/tests diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index b3baf7d6d..fbc610cc9 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -32,7 +32,7 @@ jobs: - name: Ruff format run: python -m ruff format --check . - name: Wrapper-plan generator contracts - run: python tools/check_wrapper_codegen_complexity.py + run: python tools/check_codegen_complexity.py - name: Bandit security scan run: python -m bandit -c pyproject.toml -r prik --severity-level medium --confidence-level medium - name: Vulture dead-code scan diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4e1c30015..779e6c76f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -135,7 +135,7 @@ jobs: python tools/run_fortran_toolchain_lane.py \ --compiler gfortran \ --junit-dir "$RUNNER_TEMP" - - name: Run full suite without BLAS or LAPACK + - name: Run full suite without real-library examples env: PYTHONPATH: . HYPOTHESIS_PROFILE: ci diff --git a/AGENTS.md b/AGENTS.md index df701f1d0..c9f04b729 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,6 +3,12 @@ The active codebase is entirely Python. Before starting implementation work, update or read the relevant docs first so the intended public behavior, ownership rules, and limitations are explicit; then implement code and tests to match that documented contract. +Update `CHANGELOG.md` under **Unreleased** whenever a change adds or changes +user- or maintainer-visible behavior, public APIs, supported features, +examples, build or CI workflows, benchmark methodology, or documented +limitations. Keep entries concise and outcome-focused; do not add release +notes for internal cleanup that has no visible effect. + Ignore: - *.f90 - *.f95 @@ -15,7 +21,7 @@ Do not spend context window or analysis on those files unless explicitly request When asked to change or move an API, import path, command, feature, or behavior, do not add or keep compatibility layers, aliases, shims, fallback paths, or legacy entrypoints unless explicitly requested. A requested change means the old behavior should be removed. When updating tests, remove obsolete tests that only assert removed/old implementation behavior does not exist. Do not preserve rejection or absence checks for API/features that were intentionally removed unless explicitly requested. -Before wrapper planning begins in `prik/wrapper_codegen/planner.py`, the +Before wrapper planning begins in `prik/codegen/planner.py`, the post-IR policy stage must have completed every semantic decision needed by wrapper generation, including object kind, ownership, transfer, destruction, mutability/writeback, nullability, output projection, release responsibility, @@ -52,8 +58,8 @@ the stage breakdown when they help explain the implementation. Changes limited to wrapper planning, direct bridge/binding lowering, or native compilation should use the focused owners under -`tests/fortran/infrastructure/wrapper_codegen/`, feature-local -`tests/fortran/*/wrapper_codegen/` directories, and +`tests/fortran/infrastructure/codegen/`, feature-local +`tests/fortran/*/codegen/` directories, and `tests/fortran/building_shared_library/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. @@ -72,7 +78,7 @@ pull-request verification: - `python3 -m ruff check .` - `python3 -m ruff format --check .` - `python3 tools/check_static_analysis_versions.py` -- `python3 tools/check_wrapper_codegen_complexity.py` +- `python3 tools/check_codegen_complexity.py` - `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` diff --git a/CHANGELOG.md b/CHANGELOG.md index fd8baf4c6..7e7b9ea97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,52 @@ This file is the canonical record of user-visible PRIK changes. Add changes to release preparation. Versions use [Semantic Versioning](https://semver.org/); release tags add a leading `v` to the package version. +## Unreleased + +### Added + +- Added maintained FFTPACK and MINPACK examples built from the upstream + fortran-lang projects. Their build scripts, user guides, and numerical tests + cover all 31 FFTPACK and 22 MINPACK public procedures. +- Added Python-owned, read-only NumPy snapshots for supported public Fortran + parameter arrays, including MINPACK's `dpmpar` constants. +- Added declaration-expression support for richer arithmetic, comparisons, + conditionals, array inquiries, and local, imported, or standalone + specification functions, including native-dependent result extents. +- Added exact NumPy Boolean-array conversion for compiler-measured 8-, 16-, + 32-, and 64-bit Fortran logical kinds, with canonical writeback. +- Added `WrapperBuildResult.import_module()` to load a generated extension + explicitly without changing `sys.path`. + +### Changed + +- Renamed the developer-facing wrapper generation package from + `prik.wrapper_codegen` to `prik.codegen`; the old import path was removed. +- Expanded public interface resolution so implemented unnamed interfaces and + public generics can be wrapped without exposing private implementation + procedures. +- Expanded the Real Libraries CI lane to build and test BLAS, LAPACK, FFTPACK, + and MINPACK, with cached native BLAS and LAPACK builds where available. +- Made performance comparisons faster and less order-sensitive with balanced + A/B/B/A runtime measurements, merged samples, smaller worker budgets, and + four measured clean builds after warm-up. +- Refreshed the README and website around the canonical + **PRIK — Python Runtime Interop Kit** identity, with a concise FAQ, a fair + PRIK-versus-f2py guide, clearer array guidance, and searchable real-library + examples. +- Hardened preprocessing, compiler-derived type probes, semantic policy + completion, and multi-source build reporting so unsupported contracts fail + earlier with clearer diagnostics. + +### Fixed + +- Preserved authoritative public interface signatures when linked legacy + implementations use different internal storage declarations, including + FFTPACK's `zfftf` complex-array interface. +- Corrected SciPy reference inputs for the LAPACK `dstemr` and `dstebz` tests + and strengthened BLAS and LAPACK routine validation with independent + mathematical expectations. + ## 0.1.1 — 2026-08-03 - Update README and CONTRIBUTING diff --git a/README.md b/README.md index 2343d5dda..1d458ea06 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,21 @@ # PRIK — Python Runtime Interop Kit -**Turn Fortran into natural Python APIs.** +**Generate native Python bindings for Fortran, with editable `.pyi` contracts +and Pythonic APIs.** -PRIK wraps Fortran code into clean, importable native extensions for Python -without requiring you to write low-level binding code. +PRIK generates native Python bindings from Fortran projects, producing +importable extensions and editable `.pyi` contracts for Pythonic APIs. -It is a Fortran-to-Python binding generator that preserves modules, derived -types, arrays, callbacks, and native behavior, and generates an editable -`.pyi` contract so you can shape the resulting Python API. +It preserves modules, derived types, arrays, callbacks, and native behavior so +you can shape the resulting API without writing low-level binding code. **Project status: Alpha (`0.1.x`).** Core Fortran wrapper workflows are implemented and tested across supported compilers, but public APIs may still change before `1.0`. +**PRIK starts with Fortran-to-Python.** Its semantic contract model is designed +to support more native languages over time. + @@ -282,7 +282,7 @@ User-visible `.pyi` syntax is first parsed to Python AST by `prik/parsers/pyi/parser.py`, loaded from text/files by `prik/pipeline/pyi.py`, converted to semantic IR by `prik/semantics/pyi2ir.py`, and printed by -`prik/wrapper_codegen/printers/pyi_printer.py`. The converter and printer operate on +`prik/codegen/printers/pyi_printer.py`. The converter and printer operate on `prik/semantics/models.py`. Important implementation rules: @@ -849,18 +849,18 @@ The main ownership boundaries are: - `prik/pipeline/build.py`: source order, preprocessing/probing, semantic merge, `.pyi` entry-contract loading, native build plan assembly, output placement, direct-versus-Makefile mode, and artifact reporting; -- `prik/wrapper_codegen/planner.py`: projection from completed semantic policy +- `prik/codegen/planner.py`: projection from completed semantic policy into validated typed plans; -- `prik/wrapper_codegen/generator.py`: direct bridge, binding, and source +- `prik/codegen/generator.py`: direct bridge, binding, and source artifact generation; - `prik/compiling/`: compiler commands and shared-library linking; and - `prik/binding_support/`: native binding support copied into each build. Do not move semantic ownership or projection policy into printers. Do not infer @@ -942,7 +942,7 @@ PRIK_C_DOCS_END --> - `prik/semantics/fortran2ir.py` maps Fortran procedures, derived types, module variables, kinds, shapes, storage contracts, visibility, imported references, and compile-time values. -- `prik/wrapper_codegen/printers/pyi_printer.py` emits editable user contracts. +- `prik/codegen/printers/pyi_printer.py` emits editable user contracts. - `prik/parsers/pyi/parser.py` parses edited contracts to Python AST. - `prik/pipeline/pyi.py` converts edited contract text, files, and path sets. - `prik/semantics/pyi2ir.py` converts parsed `.pyi` AST back into semantic IR. @@ -995,7 +995,7 @@ The test ownership is: - loader syntax and error behavior: `tests/fortran/semantic_pyi_format/parsing/`; - printer round-trip shape: `tests/fortran/semantic_pyi_format/pipeline/`; - policy-completion decisions: `tests/fortran/infrastructure/policy/` and feature-local `policy/` directories; -- wrapper-plan diagnostics: `tests/fortran/infrastructure/wrapper_codegen/`. +- wrapper-plan diagnostics: `tests/fortran/infrastructure/codegen/`. - Preprocessing behavior: preprocessing CLI tests and at least one parser path that consumes the recipe. - Wrapper orchestration or codegen behavior: the focused feature-local - `end_to_end/` or `wrapper_codegen/` owner, including an imported runtime + `end_to_end/` or `codegen/` owner, including an imported runtime assertion rather than build success alone. ### Golden Fixture Rules @@ -1238,7 +1238,7 @@ Example target: add a new `Annotated[...]` metadata item or projection helper. `prik/parsers/pyi/parser.py` only when the raw Python AST parsing boundary changes. 3. Add printer tests in `tests/fortran/semantic_pyi_format/pipeline/`. -4. Update `prik/wrapper_codegen/printers/pyi_printer.py`. +4. Update `prik/codegen/printers/pyi_printer.py`. 5. Update semantic models in `prik/semantics/models.py` only if the IR needs a new field or constraint. 6. Update policy completion or wrapper planning if the syntax changes a @@ -1253,7 +1253,7 @@ Focused verification: ```bash PYTHONPATH=. pytest -q tests/fortran/semantic_pyi_format/parsing/ PYTHONPATH=. pytest -q tests/fortran/semantic_pyi_format/pipeline/ -PYTHONPATH=. pytest -q tests/fortran/semantic_ir/semantics/ tests/fortran/infrastructure/wrapper_codegen/ +PYTHONPATH=. pytest -q tests/fortran/semantic_ir/semantics/ tests/fortran/infrastructure/codegen/ ``` ### Add A Stage-Owned Error @@ -1280,13 +1280,13 @@ Focused verification: ```bash PYTHONPATH=. pytest -q tests/fortran/semantic_ir/semantics/ PYTHONPATH=. pytest -q tests/fortran/semantic_ir/semantics/ -PYTHONPATH=. pytest -q tests/fortran/infrastructure/wrapper_codegen/ +PYTHONPATH=. pytest -q tests/fortran/infrastructure/codegen/ ``` @@ -1517,7 +1517,7 @@ Focused tests by concern: - Fortran parser-to-IR conversion: `PYTHONPATH=. pytest -q tests/fortran/semantic_ir/semantics/` - Wrapper-plan support diagnostics: - `PYTHONPATH=. pytest -q tests/fortran/infrastructure/wrapper_codegen/` + `PYTHONPATH=. pytest -q tests/fortran/infrastructure/codegen/` - `.pyi` printer: `PYTHONPATH=. pytest -q tests/fortran/semantic_pyi_format/pipeline/` - `.pyi` loader and edited stub behavior: diff --git a/docs/developer/feature-to-code-map.md b/docs/developer/feature-to-code-map.md index e5e664080..4f427c957 100644 --- a/docs/developer/feature-to-code-map.md +++ b/docs/developer/feature-to-code-map.md @@ -18,21 +18,21 @@ before documentation may call the behavior supported. | Feature or behavior | Public docs | Main implementation files | Focused tests | Support evidence | | --- | --- | --- | --- | --- | | Fortran parse output | `docs/developer/fortran-parser-reference.md` | `prik/parsers/fortran/parser.py`, `models.py`, `lexer.py`, `type_resolver.py` | `tests/fortran/source_parsing/parsing/`, `tests/fortran/source_parsing/parsing/test_fortran_fixture_suite.py` | Parser facts and diagnostics match fixtures | -| Semantic `.pyi` generation | `docs/user/reference/semantic-pyi-format.md` | `prik/wrapper_codegen/printers/pyi_printer.py` | `tests/fortran/semantic_pyi_format/pipeline/`, `tests/fortran/semantic_pyi_format/pipeline/test_modern_example.py` | Printed `.pyi` round-trips or matches fixtures | +| Semantic `.pyi` generation | `docs/user/reference/semantic-pyi-format.md` | `prik/codegen/printers/pyi_printer.py` | `tests/fortran/semantic_pyi_format/pipeline/`, `tests/fortran/semantic_pyi_format/pipeline/test_modern_example.py` | Printed `.pyi` round-trips or matches fixtures | | Semantic `.pyi` conversion and editing | `docs/user/reference/pyi-contracts/index.md`, `docs/user/reference/semantic-pyi-format.md`, `docs/user/examples/recipes/semantic-pyi-contracts.md` | `prik/parsers/pyi/parser.py`, `prik/pipeline/pyi.py`, `prik/semantics/pyi2ir.py`, `models.py` | `tests/fortran/semantic_pyi_format/` | Edited contracts parse to Python AST, then become semantic IR with preserved native facts | -| Semantic and wrapper-planning errors | `docs/user/guide/error-handling.md`, `docs/user/reference/diagnostic-codes.md` | `prik/semantics/fortran2ir.py`, `prik/semantics/policy_completion.py`, `prik/wrapper_codegen/planner.py` | `tests/fortran/infrastructure/policy/`, feature-local `policy/`, and feature-local `wrapper_codegen/` tests | Each owning stage rejects unsupported or incomplete contracts | +| Semantic and wrapper-planning errors | `docs/user/guide/error-handling.md`, `docs/user/reference/diagnostic-codes.md` | `prik/semantics/fortran2ir.py`, `prik/semantics/policy_completion.py`, `prik/codegen/planner.py` | `tests/fortran/infrastructure/policy/`, feature-local `policy/`, and feature-local `codegen/` tests | Each owning stage rejects unsupported or incomplete contracts | | Fortran wrapper orchestration | `docs/user/reference/fortran-wrapper.md`, `docs/user/guide/building-shared-library.md` | `prik/pipeline/build.py` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py`, multi-source wrapper tests | Builds report artifacts and compile/link as documented | -| Completed semantic policy to wrapper artifacts | `docs/user/reference/fortran-wrapper.md` | `prik/semantics/policy_completion.py`, `prik/wrapper_codegen/plan.py`, `planner.py`, `generator.py` | `tests/fortran/infrastructure/policy/`, `tests/fortran/infrastructure/wrapper_codegen/`, and feature-local policy/codegen tests | Runtime policy is explicit, the typed plan is complete, and generated artifacts compile and run | +| Completed semantic policy to wrapper artifacts | `docs/user/reference/fortran-wrapper.md` | `prik/semantics/policy_completion.py`, `prik/codegen/plan.py`, `planner.py`, `generator.py` | `tests/fortran/infrastructure/policy/`, `tests/fortran/infrastructure/codegen/`, and feature-local policy/codegen tests | Runtime policy is explicit, the typed plan is complete, and generated artifacts compile and run | | Native compilation and binding support | `docs/user/reference/fortran-wrapper.md`, `docs/user/guide/building-shared-library.md`, `docs/developer/build-system.md`, `docs/developer/quality-assurance.md` | `prik/compiling/`, `prik/binding_support/` | `tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py`, build-mode tests | Generated sources compile, link, import, and clean up correctly | | Source documentation structure | `docs/developer/source-map.md` | `docs/`, package README files, `tests/shared/docs/test_structure.py` | documentation structure and example tests | Pages have metadata, audience separation, and source coverage checks | @@ -55,7 +55,7 @@ this routing page tied to the source hotspots and package README files. | User workflow | Start in code | Do not mark supported until | | --- | --- | --- | -| Wrapping functions and subroutines | `prik/semantics/fortran2ir.py`, policy completion, `prik/wrapper_codegen/planner.py`, bridge and binding generators | Runtime tests compile, import, call, and verify return and failure behavior | +| Wrapping functions and subroutines | `prik/semantics/fortran2ir.py`, policy completion, `prik/codegen/planner.py`, bridge and binding generators | Runtime tests compile, import, call, and verify return and failure behavior | | Wrapping modules and module variables | parser module facts, semantic module conversion, naming policy, wrapper generators | Python-visible names, accessors, and unsupported module constructs are tested | | Arrays and allocatables | semantic array contracts, ownership policy, typed wrapper plans, bridge/binding array handlers | dtype, shape, rank, contiguity, mutation, returned arrays, and failure paths are tested; ordinary NumPy array actuals validate and extract their buffer directly in the C binding, descriptor handles use the planned runtime-handle path, and strided contracts carry a dense-actual role for zero-copy fast-path selection | | Pointer arguments | semantic metadata, ownership policy, bridge/binding pointer handlers | Owner, lifetime, association, and blocked cases are explicit and tested | diff --git a/docs/developer/fortran-parser-reference.md b/docs/developer/fortran-parser-reference.md index ae62f52c1..e7ec8d7f3 100644 --- a/docs/developer/fortran-parser-reference.md +++ b/docs/developer/fortran-parser-reference.md @@ -691,6 +691,53 @@ dim.upper.name # "ubound" ``` +### 4.4 Declaration-expression ownership + +The declaration parser preserves balanced Fortran 2008/2018 bound text for all +declaration owners: module variables, derived-type fields, dummy arguments, and +procedure results. Nested calls, array constructors, component references, and +colons inside nested syntax do not split an outer dimension or bound. + +Semantic conversion sends every explicit extent through the shared +`prik.utilities.declaration_expressions` layer. That layer retains the native +spelling in `source_shape` and produces the language-neutral public spelling +used by `.pyi`, including +`size(a)` to `a.size`, `size(a, dim)` to `a.shape[dim - 1]`, and `rank(a)` to +`a.ndim`. Post-IR policy then resolves public scalar and array-property +references to wrapper roles. Binding and bridge generators only render the +completed expression for their target language; they do not infer declaration +semantics. + +`lbound(a, dim)` uses the lower bound declared for that dummy axis rather than +Python's index origin. `ubound(a, dim)` combines that bound with the runtime +extent, and the shared expression layer reduces the common +`ubound-lbound+1` form to `a.shape[dim - 1]`. Direct inquiries preserve the +standard zero-extent results: lower bound one and upper bound zero. + +Parsing and preservation are intentionally broader than wrapper execution. +Valid specification expressions whose value exists only in private native +state remain available as source metadata but produce an explicit policy +blocker when no boundary role can supply them. Calls to user specification +functions also remain in the language-neutral expression. Semantic conversion +resolves each call to a local module procedure, through the declaration owner's +`USE` mappings, or to a concrete procedure interface in the same declaration +scope. It records the visible spelling, original native name, native placement, +and resolved declaration. + +A wildcard import is resolved only when file/project parsing has indexed the +named procedure in exactly one imported module; conversion does not guess from +an unavailable module export list. The `.pyi` loader reconstructs the same +identity from module functions, imports, and `@prototype` declarations. A +prototype is one signature model: annotation use makes it a callback signature, +while call use names a standalone procedure entity. Post-IR policy validates +purity, scalar-integer result, argument association, and accessibility, then +selects either a module `use` or a standalone procedure declaration backed by +the generated abstract interface. A pure prototype cannot also be a Python +callback because its generated adapter calls the Python runtime; that mixed use +is blocked before planning. The binding and bridge consume only that +completed action. Fortran 2023 vector bounds and `RANK` clauses are outside the +parser's advertised Fortran 2008/2018 language modes. + ## 5) Running tests Run all tests: @@ -1167,6 +1214,15 @@ consumed by the `.pyi` printer and current Fortran wrapper/runtime stages. Compiler-backed shared-CLI semantic stages resolve compiler-dependent kind expressions, measure numeric and logical intrinsic storage with `storage_size`, attach those facts to semantic types, and reuse memory and persistent caches. +The shared CLI applies project symbol completion even when the input contains +only one source file. That completion follows explicit renamed `use` +associations through project modules and propagates parent/ancestor +host-associated symbols into submodules before compiler-backed stages run. +This includes both a direct intrinsic rename such as `wp => real64` in a +single-file module and a re-exported chain such as `dp => rk => real64`; the +standalone compiler probe therefore receives the intrinsic expression +(`real64`) rather than a project-local alias that is out of scope in the +generated probe program. Character declarations are excluded from storage probing: their semantic type is `String`, while fixed or deferred element length is carried separately from the declaration or runtime descriptor. The generated mapping report describes diff --git a/docs/developer/index.md b/docs/developer/index.md index 15e51093e..ba21a8e9a 100644 --- a/docs/developer/index.md +++ b/docs/developer/index.md @@ -18,6 +18,7 @@ tests, then provides focused contribution workflows. - [Repository structure](repository-structure.md) - [Source map](source-map.md) - [Feature-to-code map](feature-to-code-map.md) +- [Compiler preprocessing reference](compiler-preprocessing.md) - [Fortran parser reference](fortran-parser-reference.md) - [Quality assurance](quality-assurance.md) - [Build system](build-system.md) diff --git a/docs/developer/quality-assurance.md b/docs/developer/quality-assurance.md index 559a398e7..e083347a4 100644 --- a/docs/developer/quality-assurance.md +++ b/docs/developer/quality-assurance.md @@ -97,16 +97,14 @@ python -m coverage report For subprocess coverage investigations, mirror that command shape before deciding a fix. A plain local coverage run can miss subprocess data. -Every Python version excludes the full BLAS/LAPACK real-library wrapper test -while retaining general native-bundle coverage. The `Native Libraries` -component runs the complete BLAS and LAPACK examples on Python 3.12. Each job -step sources `build_all.sh`, which sources the exact `build_prik.sh` and -`build_f2py.sh` sequences displayed in the user documentation, before starting -pytest. Each f2py script reuses the native library from its PRIK script, and -each explicitly selected `ci/full_surface.py` audit reuses the PRIK extension. -The job therefore verifies the copyable commands without repeating native -compilation or wrapper construction in a second process. A -pull request may use the +Every Python version excludes the full real-library wrapper examples while +retaining general native-bundle coverage. The `Real Libraries` component runs +the complete BLAS, LAPACK, FFTPACK, and MINPACK examples on Python 3.12. Each +job step sources the documented `build_all.sh` entrypoint before starting +pytest. BLAS and LAPACK additionally run their CI-only full-surface audits; +FFTPACK and MINPACK run their fail-closed public-inventory tests as part of the +maintained example suites. The job therefore verifies the copyable build and +test commands for all four libraries. A pull request may use the `ignore-real-library-wrappers` label to skip that expensive component without disabling the ordinary Python-version matrix. diff --git a/docs/developer/repository-structure.md b/docs/developer/repository-structure.md index e1537007a..53101c597 100644 --- a/docs/developer/repository-structure.md +++ b/docs/developer/repository-structure.md @@ -24,7 +24,7 @@ artifacts used by tests. Navigate by ownership boundary first, then by file. | `prik/types/` | Cross-layer mappings from resolved semantic types to Python ecosystem types. | | `prik/parsers/` | Public namespace for language and semantic-contract frontends and parser models. | | `prik/semantics/` | Semantic IR, source-to-IR conversion, `.pyi` parsing, and policy completion. | -| `prik/wrapper_codegen/` | Typed wrapper plans, direct native bridge/binding lowering, and source and semantic `.pyi` printers. | +| `prik/codegen/` | Typed wrapper plans, direct native bridge/binding lowering, and source and semantic `.pyi` printers. | | `prik/compiling/` | Native compile objects, compiler command orchestration, native support installation, and linking. | | `prik/binding_support/` | Bundled header-only native support copied into generated wrapper builds. | | `prik/naming/` | Unified public-name and generated-symbol policy. | @@ -54,6 +54,7 @@ through `prik/__init__.py`. | `tests/fortran//` | User-visible Fortran and semantic `.pyi` behavior, with documented features directly below the language root and stages below each feature. | | `tests/fortran/{source_parsing,source_preprocessing,command_line_interface,semantic_ir}/` | Public cross-feature capabilities that begin from source or expose an inspection/reporting surface. | | `tests/fortran/infrastructure/` | Internal cross-feature policy, wrapper-generation, compiler, and runtime frameworks with no honest public-capability owner. | +| `tests/fortran/building_shared_library/end_to_end/real_libraries/` | Opt-in numerical showcase tests that build actual FFTPACK and MINPACK checkouts, call their generated Python routines, and verify known results. | | `tests/c/` | C input-language parsing, preprocessing, probe, semantic, CLI, and fixture evidence. | | `tests/shared/` | Language-neutral product architecture, documentation, naming, tools, type mapping, and utility checks. | | `examples/blas/tests/test_*.py` | User-facing real-library correctness documentation: explicit independent and PRIK/f2py differential validation for every Reference BLAS routine. | diff --git a/docs/developer/source-map.md b/docs/developer/source-map.md index 099b82c1e..19b1a82ea 100644 --- a/docs/developer/source-map.md +++ b/docs/developer/source-map.md @@ -40,29 +40,31 @@ change crosses ownership boundaries. | Change area | Open first | Public docs to update | Focused evidence | | --- | --- | --- | --- | | CLI flags, stage selection, output formatting, diagnostics | `prik/cli.py` | `docs/user/reference/cli-commands.md`, `docs/user/getting-started/beginner-workflow.md` | `tests/fortran/command_line_interface/pipeline/`, `tests/shared/docs/test_examples.py` | -| Compiler preprocessing, include paths, macros, and target flags | `prik/pipeline/preprocessing.py` | `docs/user/examples/recipes/compiler-preprocessing.md`, `docs/developer/fortran-parser-reference.md` | `tests/fortran/source_preprocessing/preprocessing/`, `tests/fortran/source_preprocessing/preprocessing/test_parser_boundaries.py` | +| Compiler preprocessing, include paths, macros, and target flags | `prik/pipeline/preprocessing.py` | `docs/user/examples/recipes/compiler-preprocessing.md`, `docs/developer/compiler-preprocessing.md`, `docs/developer/fortran-parser-reference.md` | `tests/fortran/source_preprocessing/preprocessing/`, `tests/fortran/source_preprocessing/preprocessing/test_parser_boundaries.py` | | Fortran parser facts and diagnostics | `prik/parsers/fortran/parser.py` | `docs/developer/fortran-parser-reference.md`, `docs/user/examples/recipes/inspect-fortran-api.md` | `tests/fortran/source_parsing/parsing/` | -| Semantic `.pyi` parsing, conversion, printing, package generation, and round-trip behavior | `prik/parsers/pyi/parser.py`, `prik/pipeline/pyi.py`, `prik/semantics/pyi2ir.py`, `prik/wrapper_codegen/printers/pyi_printer.py` | `docs/user/reference/semantic-pyi-format.md`, `docs/user/reference/pyi-contracts/index.md`, `docs/user/examples/recipes/semantic-pyi-contracts.md` | `tests/fortran/semantic_pyi_format/`, `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py`, `tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py`, `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py`, `tests/fortran/semantic_pyi_format/pipeline/` | -| Wrapper-planning errors and support claims | `prik/semantics/policy_completion.py`, `prik/wrapper_codegen/planner.py` | `docs/user/reference/diagnostic-codes.md`, `docs/user/language-support/feature-matrix.md` | `tests/fortran/infrastructure/policy/`, feature-local `policy/`, and `tests/fortran/infrastructure/wrapper_codegen/` | +| Semantic `.pyi` parsing, conversion, printing, package generation, and round-trip behavior | `prik/parsers/pyi/parser.py`, `prik/pipeline/pyi.py`, `prik/semantics/pyi2ir.py`, `prik/codegen/printers/pyi_printer.py` | `docs/user/reference/semantic-pyi-format.md`, `docs/user/reference/pyi-contracts/index.md`, `docs/user/examples/recipes/semantic-pyi-contracts.md` | `tests/fortran/semantic_pyi_format/`, `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py`, `tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py`, `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py`, `tests/fortran/semantic_pyi_format/pipeline/` | +| Wrapper-planning errors and support claims | `prik/semantics/policy_completion.py`, `prik/codegen/planner.py` | `docs/user/reference/diagnostic-codes.md`, `docs/user/language-support/feature-matrix.md` | `tests/fortran/infrastructure/policy/`, feature-local `policy/`, and `tests/fortran/infrastructure/codegen/` | | Source-driven Fortran wrapper orchestration | `prik/pipeline/build.py` | `docs/user/reference/fortran-wrapper.md`, `docs/user/guide/building-shared-library.md` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py`, `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py` | | Semantic `.pyi` wrapper orchestration from native artifacts | `prik/pipeline/build.py`, `prik/pipeline/pyi.py`, `prik/semantics/pyi2ir.py` | `docs/user/reference/fortran-wrapper.md`, `docs/user/reference/semantic-pyi-format.md` | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py`, `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py`, `tests/fortran/pyi_contracts/exports_and_modules/`, `tests/fortran/pyi_contracts/functions_and_classes/` | -| Ownership, lifetime, output projection, and unsupported wrapper policy | `prik/semantics/policy_completion.py`, `prik/semantics/ownership.py`, `prik/wrapper_codegen/planner.py` | `docs/user/guide/memory-management.md`, `docs/user/reference/semantic-pyi-format.md`, `docs/user/reference/fortran-wrapper.md` | `tests/fortran/infrastructure/policy/`, feature-local `policy/`, and `tests/fortran/infrastructure/wrapper_codegen/` | -| Immediate callback policy, typed adapters, and trampolines | `prik/semantics/wrapper_policy.py`, `prik/semantics/policy_completion.py`, `prik/wrapper_codegen/plan.py`, `prik/wrapper_codegen/planner.py`, `prik/wrapper_codegen/c/binding.py`, `prik/wrapper_codegen/fortran/bridge.py` | `docs/user/guide/callbacks.md`, `docs/user/reference/semantic-pyi-format.md` | `tests/fortran/callbacks/` | +| Ownership, lifetime, output projection, and unsupported wrapper policy | `prik/semantics/policy_completion.py`, `prik/semantics/ownership.py`, `prik/codegen/planner.py` | `docs/user/guide/memory-management.md`, `docs/user/reference/semantic-pyi-format.md`, `docs/user/reference/fortran-wrapper.md` | `tests/fortran/infrastructure/policy/`, feature-local `policy/`, and `tests/fortran/infrastructure/codegen/` | +| Immediate callback policy, typed adapters, and trampolines | `prik/semantics/wrapper_policy.py`, `prik/semantics/policy_completion.py`, `prik/codegen/plan.py`, `prik/codegen/planner.py`, `prik/codegen/c/binding.py`, `prik/codegen/fortran/bridge.py` | `docs/user/guide/callbacks.md`, `docs/user/reference/semantic-pyi-format.md` | `tests/fortran/callbacks/` | | Native compilation, binding support, and shared-library linking | `prik/pipeline/build.py`, `prik/compiling/compilers.py`, `prik/compiling/native_support.py` | `docs/user/reference/fortran-wrapper.md`, `docs/developer/build-system.md` | `tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py`, `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py` | | Public Python exports | `prik/__init__.py` | `README.md`, `docs/user/reference/python-api.md` | `tests/fortran/source_parsing/parsing/test_public_entrypoints.py` | -| Reference BLAS source ownership, inventory, and numerical validation | `examples/blas/routine_inventory.py`, `examples/blas/tests/test_routine_coverage.py` | `examples/blas/README.md`, `docs/user/examples/blas-wrapper.md` | `examples/blas/tests/test_*.py`, `examples/blas/ci/full_surface.py`, dedicated BLAS/LAPACK workflow | -| Reference LAPACK source ownership, inventory, and numerical validation | `examples/lapack/routine_inventory.py`, `examples/lapack/tests/test_routine_coverage.py` | `examples/lapack/README.md`, `docs/user/examples/lapack-wrapper.md` | `examples/lapack/tests/test_*.py`, `examples/lapack/ci/full_surface.py`, dedicated BLAS/LAPACK workflow | +| Reference BLAS source ownership, inventory, and numerical validation | `examples/blas/routine_inventory.py`, `examples/blas/tests/test_routine_coverage.py` | `examples/blas/README.md`, `docs/user/examples/blas-wrapper.md` | `examples/blas/tests/test_*.py`, `examples/blas/ci/full_surface.py`, dedicated real-libraries workflow | +| Reference LAPACK source ownership, inventory, and numerical validation | `examples/lapack/routine_inventory.py`, `examples/lapack/tests/test_routine_coverage.py` | `examples/lapack/README.md`, `docs/user/examples/lapack-wrapper.md` | `examples/lapack/tests/test_*.py`, `examples/lapack/ci/full_surface.py`, dedicated real-libraries workflow | +| FFTPACK public-module boundary, source ownership, and numerical validation | `examples/fftpack/routine_inventory.py`, `examples/fftpack/tests/test_routine_coverage.py` | `examples/fftpack/README.md`, `docs/user/examples/fftpack-wrapper.md` | `examples/fftpack/tests/test_*.py`, `tests/fortran/building_shared_library/end_to_end/real_libraries/test_fftpack_routines.py`, dedicated real-libraries workflow | +| MINPACK source ownership, parameter constants, and numerical validation | `examples/minpack/routine_inventory.py`, `examples/minpack/tests/test_routine_coverage.py` | `examples/minpack/README.md`, `docs/user/examples/minpack-wrapper.md` | `examples/minpack/tests/test_*.py`, `tests/fortran/building_shared_library/end_to_end/real_libraries/test_minpack_routines.py`, dedicated real-libraries workflow | | Source navigation documentation | `docs/developer/source-map.md`, `docs/developer/feature-to-code-map.md`, package README files | `docs/developer/source-map.md` | `tests/shared/docs/test_structure.py` | ## Package Map @@ -85,7 +87,7 @@ PRIK_C_DOCS_END --> | `prik/parsers/c/` | C lexer, parser, models, preprocessing metadata, and C parser CLI helpers | `parser.py`, `lexer.py`, `models.py`, `preprocessor.py`, `type_resolver.py`, `cli.py` | `tests/c/fixtures/parser/`, `docs/developer/c-parser-reference.md` | | `prik/parsers/pyi/` | Semantic `.pyi` text/file parsing to Python AST. | `parser.py` | `tests/fortran/semantic_pyi_format/parsing/`, `docs/user/reference/semantic-pyi-format.md` | | `prik/semantics/` | Language-neutral semantic IR, source-to-IR conversion, `.pyi` AST conversion, and policy completion | `models.py`, `fortran2ir.py`, `c2ir.py`, `pyi2ir.py`, `policy_completion.py` | `tests/fortran/semantic_ir/semantics/`, `tests/fortran/semantic_pyi_format/semantics/`, `docs/user/reference/semantic-ir.md`, `docs/user/reference/semantic-pyi-format.md` | -| `prik/wrapper_codegen/` | Canonical wrapper planning, C/Fortran generation, source printing, and semantic `.pyi` printing | `plan.py`, `planner.py`, `generator.py`, `printers/` | `tests/fortran/infrastructure/wrapper_codegen/`, feature-local `wrapper_codegen/` and `end_to_end/` tests, `docs/user/reference/fortran-wrapper.md` | +| `prik/codegen/` | Canonical wrapper planning, C/Fortran generation, source printing, and semantic `.pyi` printing | `plan.py`, `planner.py`, `generator.py`, `printers/` | `tests/fortran/infrastructure/codegen/`, feature-local `codegen/` and `end_to_end/` tests, `docs/user/reference/fortran-wrapper.md` | | `prik/naming/` | Unified public-name and generated-symbol policy for Python, C, and Fortran targets | `policy.py` | naming, visibility, and wrapper runtime tests | PRIK_C_DOCS_END --> @@ -112,13 +114,13 @@ update this table, the package README files, and the mechanical checks in | `prik/pipeline/pyi.py` | Semantic `.pyi` text/file/path-set conversion and external-type reconciliation. | | `prik/semantics/pyi2ir.py` | Semantic `.pyi` AST conversion and validation. | | `prik/semantics/policy_completion.py` | Post-IR semantic policy completion before wrapper planning. | -| `prik/wrapper_codegen/plan.py` | Typed, policy-complete wrapper plan records. | -| `prik/wrapper_codegen/planner.py` | Semantic policy to wrapper-plan conversion. | -| `prik/wrapper_codegen/generator.py` | Ordered direct bridge, binding, header, and source generation. | -| `prik/wrapper_codegen/fortran/bridge.py` | Direct Fortran bridge lowering from typed plans. | -| `prik/wrapper_codegen/c/binding.py` | Direct Python-extension binding lowering from typed plans. | -| `prik/wrapper_codegen/printers/source_printers.py` | Native binding, header, and Fortran source printing. | -| `prik/wrapper_codegen/printers/pyi_printer.py` | Semantic `.pyi` printing. | +| `prik/codegen/plan.py` | Typed, policy-complete wrapper plan records. | +| `prik/codegen/planner.py` | Semantic policy to wrapper-plan conversion. | +| `prik/codegen/generator.py` | Ordered direct bridge, binding, header, and source generation. | +| `prik/codegen/fortran/bridge.py` | Direct Fortran bridge lowering from typed plans. | +| `prik/codegen/c/binding.py` | Direct Python-extension binding lowering from typed plans. | +| `prik/codegen/printers/source_printers.py` | Native binding, header, and Fortran source printing. | +| `prik/codegen/printers/pyi_printer.py` | Semantic `.pyi` printing. | | `prik/compiling/objects.py` | Native compile object model. | | `prik/compiling/compilers.py` | Compiler command execution and tool lookup. | | `prik/compiling/native_support.py` | Native binding support installation for generated wrappers. | @@ -145,10 +147,10 @@ prik/cli.py -> prik/probes/fortran_types.py -> prik/semantics/fortran2ir.py -> prik/semantics/policy_completion.py - -> prik/wrapper_codegen/planner.py - -> prik/wrapper_codegen/generator.py - -> prik/wrapper_codegen/fortran/bridge.py - -> prik/wrapper_codegen/c/binding.py + -> prik/codegen/planner.py + -> prik/codegen/generator.py + -> prik/codegen/fortran/bridge.py + -> prik/codegen/c/binding.py -> prik/compiling/compilers.py -> tests/fortran/ ``` @@ -161,8 +163,8 @@ prik/parsers/pyi/parser.py -> prik/pipeline/pyi.py -> prik/semantics/pyi2ir.py -> prik/semantics/policy_completion.py - -> prik/wrapper_codegen/planner.py - -> prik/wrapper_codegen/generator.py + -> prik/codegen/planner.py + -> prik/codegen/generator.py ``` diff --git a/docs/developer/testing-strategy.md b/docs/developer/testing-strategy.md index 45fc72e58..6dc887a38 100644 --- a/docs/developer/testing-strategy.md +++ b/docs/developer/testing-strategy.md @@ -92,6 +92,15 @@ directory and carry `fortran_end_to_end`. Full-library integration nodes carry `examples/lapack/` use both markers and run only in the dedicated BLAS/LAPACK lane. +The opt-in numerical showcases under +`tests/fortran/building_shared_library/end_to_end/real_libraries/` locate a +sibling checkout or an explicitly configured source directory, build the real +library sources, call representative generated routines, and compare against +independently known numerical answers. `PRIK_FFTPACK_SOURCE_DIR` and +`PRIK_MINPACK_SOURCE_DIR` override the default sibling `fftpack/src` and +`minpack/src` locations. The tests skip when the corresponding checkout is +absent. + ## Diagnostics and unsupported behavior Put an unsupported case at its first decisive stage and assert a stable prik diff --git a/docs/index.md b/docs/index.md index 8bb5dcfb9..809d7d048 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- -title: PRIK -description: Turn Fortran functions, modules, arrays, and derived types into natural Python APIs +title: PRIK — Python Runtime Interop Kit +description: PRIK generates native Python bindings from Fortran projects, producing importable extensions and editable .pyi contracts for Pythonic APIs. audience: users prerequisites: none related: user/getting-started/index.md, user/getting-started/installation.md, user/performance.md @@ -8,131 +8,105 @@ status: maintained publication: reviewed --- -# PRIK +# PRIK — Python Runtime Interop Kit -**Python Runtime Interop Kit.** +**Generate native Python bindings for Fortran, with editable `.pyi` contracts +and Pythonic APIs.** -**Turn Fortran into natural Python APIs.** - -Build clean, importable native extensions from supported Fortran without -writing low-level binding code. PRIK preserves modules, derived types, arrays, -and native behavior, and generates an editable `.pyi` contract so you can -shape the Python API. +PRIK generates native Python bindings from Fortran projects, producing +importable extensions and editable `.pyi` contracts for Pythonic APIs. **Project status: Alpha (`0.1.x`).** Core Fortran wrapper workflows are implemented and tested across supported compilers, but public APIs may still change before `1.0`. -The complete example below builds with one command: - -```bash -python3 -m prik points.f90 --out geometry -``` +**PRIK starts with Fortran-to-Python.** Its semantic contract model is designed +to support more native languages over time. --- -## See it in action - -Create `points.f90`: +## From Fortran to Python in one command - -```fortran -module points - implicit none +Install the package in a virtual environment: - type :: point - real(8) :: x = 0.0d0 - real(8) :: y = 0.0d0 - end type point +```bash +python3 -m pip install prik +``` -contains +Create `scale.f90`: - subroutine move(item, dx, dy) - type(point), intent(inout) :: item - real(8), intent(in) :: dx, dy - item%x = item%x + dx - item%y = item%y + dy - end subroutine move + +```fortran +real(8) function scale(value, factor) result(output) + real(8), intent(in) :: value + real(8), intent(in) :: factor + output = value * factor +end function scale +``` - real(8) function norm_squared(item) result(value) - type(point), intent(in) :: item - value = item%x * item%x + item%y * item%y - end function norm_squared +Build an importable extension: -end module points +```bash +python3 -m prik scale.f90 ``` -**Generated Python API:** +Call the generated Python API: ```python import numpy as np -import geometry.points as points -item = points.point(x=np.float64(3.0), y=np.float64(4.0)) -points.move(item, np.float64(1.0), np.float64(-2.0)) +import scale -print(item.x, item.y) # 4.0 2.0 -print(points.norm_squared(item)) # 20.0 +result = scale.scale(np.float64(3.0), np.float64(2.5)) +print(result) # 7.5 ``` -No manual bindings are required. From this source, PRIK creates a Python -namespace, a class with accessible fields, a mutating procedure, and a -function. +No manual binding code is required. PRIK derives the native wrapper and a +readable Python signature from the Fortran source. -Want a different Python API? Edit the generated `.pyi` contract to rename or -hide exports, flatten namespaces, define constructors and methods, or create -overloads. The [contract guide](user/reference/pyi-contracts/index.md) shows -the available edits. +## Why PRIK ---- +- **Natural Python APIs:** Fortran modules become namespaces and derived types + become classes. +- **Editable contracts:** generated `.pyi` files let you rename, hide, flatten, + or reorganize the public API. +- **Explicit native behavior:** NumPy dtypes, array layouts, ownership, and + lifetimes are checked at the boundary. +- **Clear limits:** unsupported contracts fail before wrapper generation with + actionable diagnostics. -## How it works +## Proven on real Fortran libraries -1. Write standard Fortran. -2. Run `prik` on the source. -3. Import the generated native extension. -4. Optionally edit the generated `.pyi` contract to shape the Python API. +The maintained examples wrap and numerically validate +[BLAS](user/examples/blas-wrapper.md), +[LAPACK](user/examples/lapack-wrapper.md), +[FFTPACK](user/examples/fftpack-wrapper.md), and +[MINPACK](user/examples/minpack-wrapper.md). The reproducible +[performance comparison](user/performance.md) measures PRIK and NumPy's f2py +against the same Fortran kernels. -No manual binding code or low-level boilerplate. +## Measured against NumPy's f2py ---- - -## Key Features - -- Fortran modules exposed as Python namespaces and derived types as classes -- NumPy arrays with explicit dtype, shape, and layout checks -- Allocatable and pointer arrays with explicit lifetime operations -- Immediate Python callbacks and overloaded interfaces -- Editable `.pyi` contracts and readable generated docstrings -- Early, clear errors when a boundary cannot be wrapped -- Low wrapper overhead measured against NumPy's - [f2py](user/performance.md) in a reproducible benchmark suite - ---- - -## Measured Performance +The published benchmark compares both tools on the same Fortran sources and +the same machine. The charts show the current published snapshot. Results are +specific to its machine and toolchain, which are documented with the full results. -**Low wrapper overhead, measured against NumPy's f2py.** +**Runtime-call performance** — values above `1.0×` favor PRIK. -The latest published benchmark runs both tools against the same Fortran -kernels through their normal generated interfaces. Results are -machine-dependent; the detailed page records the complete environment and -reproduction method. - -**Runtime-call performance** — values above `1.0×` mean PRIK is faster. - -[![Relative performance of PRIK and f2py across call, vector, and matrix workloads. Values above 1.0 mean PRIK is faster.](user/assets/performance-comparison.svg)](user/performance.md) +[![Relative runtime performance of PRIK and f2py across call, vector, and matrix workloads. Values above 1.0 mean PRIK is faster.](user/assets/performance-comparison.svg)](user/performance.md) { .prik-performance-chart } +The chart shows `f2py time ÷ PRIK time`: values above `1.0×` favor PRIK and +values below `1.0×` favor f2py. + **Clean end-to-end build time** — lower times are better. -[![Clean end-to-end build time for prik and f2py under development and optimized compiler profiles. Lower times are better.](user/assets/build-time-comparison.svg)](user/performance.md#clean-build-time) +[![Clean end-to-end build time for PRIK and f2py under development and optimized compiler profiles. Lower times are better.](user/assets/build-time-comparison.svg)](user/performance.md#clean-build-time) { .prik-performance-chart } -[View the full results and methodology →](user/performance.md) - ---- +[See the benchmark machine, full results, and methodology →](user/performance.md) -**Ready to wrap your Fortran code?** +**Ready to wrap your Fortran project?** -[Getting Started Guide →](user/getting-started/index.md){ .prik-primary-cta } +[Install PRIK →](user/getting-started/installation.md){ .prik-primary-cta } +[Read Getting Started →](user/getting-started/index.md){ .prik-primary-cta } diff --git a/docs/javascripts/faq.js b/docs/javascripts/faq.js new file mode 100644 index 000000000..3339029c7 --- /dev/null +++ b/docs/javascripts/faq.js @@ -0,0 +1,27 @@ +(function () { + "use strict"; + + function openLinkedQuestion() { + if (!window.location.hash) { + return; + } + + const id = decodeURIComponent(window.location.hash.slice(1)); + const target = document.getElementById(id); + if (!target || !target.matches("details.prik-faq-item")) { + return; + } + + target.open = true; + window.requestAnimationFrame(function () { + target.scrollIntoView({ block: "start" }); + }); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", openLinkedQuestion); + } else { + openLinkedQuestion(); + } + window.addEventListener("hashchange", openLinkedQuestion); +})(); diff --git a/docs/maintainer/ci-cd.md b/docs/maintainer/ci-cd.md index ff8e2775a..cd1910d62 100644 --- a/docs/maintainer/ci-cd.md +++ b/docs/maintainer/ci-cd.md @@ -87,11 +87,13 @@ The pre-merge benchmark catches code-induced failures before `main`. The `main` run is still required because it generates the deployment artifact for the merged commit; external runner or service failures can still occur and must be retried or diagnosed honestly. The workflow pins the benchmark -toolchain and alternates whether prik or f2py is measured first to avoid a -systematic ordering advantage. Runtime cases use separate latency, medium, and -bulk sampling budgets and are merged into one pyperf result per tool; this -gives nanosecond-scale calls more independent samples without multiplying the -cost of the largest array workloads. +toolchain. Each runtime group uses an A/B/B/A sequence that splits a reduced +worker budget evenly between PRIK-first and f2py-first measurements, then +merges both passes before publication. Clean-build rounds alternate tool order. +Runtime cases use separate latency, medium, and bulk sampling budgets, giving +nanosecond-scale calls more independent samples without multiplying the cost of +the largest array workloads. The combined budget targets roughly 20 minutes on +the pinned runner while retaining all workloads. Documentation workflow concurrency is isolated by Git ref. Pull-request runs may cancel an older run for the same ref, but a `main` run is never canceled by diff --git a/docs/maintainer/documentation-architecture.md b/docs/maintainer/documentation-architecture.md index 6ef2e7afc..de97b6aa3 100644 --- a/docs/maintainer/documentation-architecture.md +++ b/docs/maintainer/documentation-architecture.md @@ -45,11 +45,14 @@ contains only pages explicitly marked as reviewed. They reserve dedicated right-side space for the copy control, and long lines scroll inside the block instead of widening the page. -`docs/index.md` is the user-first project entrance. Its body introduces prik, -shows the shortest checked source-to-import workflow and its generated function -docstring, and sends the reader into Getting Started. Developer, Maintainer, -and deeper User Guide destinations stay available through site navigation -instead of competing with that first task. +`docs/index.md` is the user-first project entrance. It uses the canonical +`PRIK — Python Runtime Interop Kit` identity and public description, shows the +shortest checked source-to-import workflow, summarizes the product's concrete +advantages, and links to real-library evidence before sending the reader into +Getting Started. Developer, Maintainer, and deeper User Guide destinations stay +available through site navigation instead of competing with that first task. +The FAQ uses natural task questions as concise routes to authoritative guides; +it does not duplicate those guides. ## Audience Lanes @@ -209,6 +212,9 @@ add an architecture-neutral CPU identity to every runtime and build result because pyperf's Linux metadata collector does not report `cpu_model_name` on every ARM64 `/proc/cpuinfo` format. Documentation generation continues to require matching CPU metadata across each prik/f2py result pair. +Runtime results combine equal reduced worker budgets from PRIK-first and +f2py-first passes in the same job. The merged suites drive publication, while +the order-specific suites remain in the uploaded artifact for auditability. ## Continuous Documentation Quality diff --git a/docs/maintainer/internal-architecture/pipeline-map.md b/docs/maintainer/internal-architecture/pipeline-map.md index 73d73b64a..120c3551b 100644 --- a/docs/maintainer/internal-architecture/pipeline-map.md +++ b/docs/maintainer/internal-architecture/pipeline-map.md @@ -44,9 +44,9 @@ PRIK_C_DOCS_END --> | Target probes | `prik/probes/fortran_types.py` | semantic type requirements and compiler flags | resolved kind/storage facts | Fortran type probe tests | | Semantic IR | `prik/semantics/fortran2ir.py` | parser project and target facts | `SemanticModule` objects | semantic Fortran tests | | Semantic policy completion | `prik/semantics/policy_completion.py`, `prik/semantics/ownership.py` | full semantic modules with signatures and `.pyi` overrides | semantic modules annotated with every ownership, transfer, destruction, mutability, storage, accessor, and projection decision needed by wrapper generation | ownership-policy tests | -| Wrapper planning | `prik/wrapper_codegen/planner.py`, `prik/wrapper_codegen/plan.py` | policy-completed semantic modules | typed wrapper plans consuming completed decisions without a separate support-analysis traversal | `tests/fortran/infrastructure/wrapper_codegen/`, wrapper tests | -| Direct bridge and binding lowering | `prik/wrapper_codegen/fortran/bridge.py`, `prik/wrapper_codegen/c/binding.py`, `prik/wrapper_codegen/generator.py` | validated typed wrapper plans | Fortran, C, and header syntax nodes | `tests/fortran/infrastructure/wrapper_codegen/`, wrapper tests | -| Wrapper and semantic-contract printing | `prik/wrapper_codegen/printers/` | wrapper syntax nodes or semantic IR | wrapper source files or semantic `.pyi` text | printer, generated-contract, and wrapper artifact tests | +| Wrapper planning | `prik/codegen/planner.py`, `prik/codegen/plan.py` | policy-completed semantic modules | typed wrapper plans consuming completed decisions without a separate support-analysis traversal | `tests/fortran/infrastructure/codegen/`, wrapper tests | +| Direct bridge and binding lowering | `prik/codegen/fortran/bridge.py`, `prik/codegen/c/binding.py`, `prik/codegen/generator.py` | validated typed wrapper plans | Fortran, C, and header syntax nodes | `tests/fortran/infrastructure/codegen/`, wrapper tests | +| Wrapper and semantic-contract printing | `prik/codegen/printers/` | wrapper syntax nodes or semantic IR | wrapper source files or semantic `.pyi` text | printer, generated-contract, and wrapper artifact tests | | Compile and link | `prik/compiling/`, `prik/pipeline/build.py` | dependency-batched native objects, generated bridge and binding objects, compiler-process limit, and ordered link inputs | shared library | wrapper runtime and build-mode tests | @@ -156,15 +156,15 @@ PRIK_C_DOCS_END --> | --- | --- | --- | | CLI and output routing | `prik/cli.py`, parser CLI helpers | `docs/developer/source-map.md`, `docs/developer/feature-to-code-map.md` | | Source loading and preprocessing | `prik/pipeline/preprocessing.py` | `docs/developer/source-map.md`, parser references | -| Editable semantic contracts | `prik/parsers/pyi/parser.py`, `prik/pipeline/pyi.py`, `prik/semantics/pyi2ir.py`, `prik/wrapper_codegen/printers/pyi_printer.py` | `docs/user/reference/semantic-pyi-format.md` | -| Semantic and wrapper-planning errors | `prik/semantics/fortran2ir.py`, `prik/semantics/policy_completion.py`, `prik/semantics/wrapper_policy.py`, `prik/wrapper_codegen/planner.py` | `docs/user/guide/error-handling.md` | -| Wrapper policy and lowering | `prik/semantics/policy_completion.py`, `prik/semantics/ownership.py`, `prik/wrapper_codegen/planner.py`, `prik/wrapper_codegen/generator.py` | `docs/user/reference/fortran-wrapper.md`, ownership docs | +| Editable semantic contracts | `prik/parsers/pyi/parser.py`, `prik/pipeline/pyi.py`, `prik/semantics/pyi2ir.py`, `prik/codegen/printers/pyi_printer.py` | `docs/user/reference/semantic-pyi-format.md` | +| Semantic and wrapper-planning errors | `prik/semantics/fortran2ir.py`, `prik/semantics/policy_completion.py`, `prik/semantics/wrapper_policy.py`, `prik/codegen/planner.py` | `docs/user/guide/error-handling.md` | +| Wrapper policy and lowering | `prik/semantics/policy_completion.py`, `prik/semantics/ownership.py`, `prik/codegen/planner.py`, `prik/codegen/generator.py` | `docs/user/reference/fortran-wrapper.md`, ownership docs | | Native build | `prik/pipeline/build.py`, `prik/compiling/compilers.py`, `prik/compiling/native_support.py` | compiling package README and build-system docs | ## Semantic `.pyi` Wrapper Pipeline @@ -180,8 +180,8 @@ the Python API. -> prik/semantics/pyi2ir.py -> prik/semantics/native_contract.py -> prik/semantics/policy_completion.py - -> prik/wrapper_codegen/planner.py - -> prik/wrapper_codegen/generator.py + -> prik/codegen/planner.py + -> prik/codegen/generator.py -> compile and link pipeline ``` diff --git a/docs/maintainer/internal-architecture/wrapper-generation-pipeline.md b/docs/maintainer/internal-architecture/wrapper-generation-pipeline.md index 9f749c0eb..5d4bb331f 100644 --- a/docs/maintainer/internal-architecture/wrapper-generation-pipeline.md +++ b/docs/maintainer/internal-architecture/wrapper-generation-pipeline.md @@ -73,7 +73,7 @@ runs both backend preflight checks, lowers recursively to C and Fortran syntax nodes, and asks the source printers to render those nodes. Build integration compiles the rendered sources; it does not own datatype transfer policy. Wrapper C/Fortran source printers and the semantic `.pyi` printer share -`prik/wrapper_codegen/printers/`; no compatibility printer remains under the +`prik/codegen/printers/`; no compatibility printer remains under the legacy codegen package. Wrapper builds have no legacy route or fallback. An unsupported completed plan diff --git a/docs/maintainer/roadmap/documentation-content-checklist.md b/docs/maintainer/roadmap/documentation-content-checklist.md index 1f6fcbf7c..8191c1cbc 100644 --- a/docs/maintainer/roadmap/documentation-content-checklist.md +++ b/docs/maintainer/roadmap/documentation-content-checklist.md @@ -271,6 +271,9 @@ primary placeholder queue. namespace, public state, saved state, visibility, and limitation guide. - [x] `docs/user/getting-started/beginner-workflow.md`: maintained edit, inspect, planning, build, smoke-test, artifact-review, and rebuild loop. +- [x] `docs/user/faq/index.md`: maintained task-oriented answers that route + search questions to checked guides, real-library examples, and the bounded + PRIK/f2py comparison. - [x] `docs/user/reference/semantic-ir.md`: maintained Semantic IR contract. - [x] `docs/user/reference/semantic-pyi-format.md`: maintained semantic `.pyi` contract. @@ -296,8 +299,9 @@ primary placeholder queue. - [x] `docs/user/guide/data-types.md`: maintained Fortran storage, semantic `.pyi`, Python value, and NumPy dtype mapping with compiler-probed limits. - [x] `docs/user/guide/arrays.md`: maintained dtype, rank, shape, layout, - C-order zero-copy and `COPY_F`, stride-aware view, lower-bound, assumed-rank, - zero-size, result, and validation guide. + C-order zero-copy and `COPY_F`, stride-aware view, assumed-rank, zero-size, + result, and validation guide; advanced declaration expressions route to the + contract reference. - [x] `docs/user/guide/strings.md`: maintained immutable value, replacement, mutable storage, fixed-width array, length, and encoding guide. - [x] `docs/user/guide/wrapping-functions.md`: maintained scalar, array-result, diff --git a/docs/maintainer/roadmap/fortran-test-suite-cleanup-checklist.md b/docs/maintainer/roadmap/fortran-test-suite-cleanup-checklist.md index a18645148..425f06fff 100644 --- a/docs/maintainer/roadmap/fortran-test-suite-cleanup-checklist.md +++ b/docs/maintainer/roadmap/fortran-test-suite-cleanup-checklist.md @@ -108,7 +108,7 @@ tests/ calls_and_results/ infrastructure/ policy/ - wrapper_codegen/ + codegen/ c/ README.md parsing/ @@ -136,7 +136,7 @@ tests/fortran/arrays/ parsing/ semantics/ policy/ - wrapper_codegen/ + codegen/ pipeline/ end_to_end/ fixtures/ @@ -177,7 +177,7 @@ behavior and public cross-feature capabilities do not. and end-to-end behavior. - [x] Use the same stage names inside every feature: `parsing`, `probes`, `preprocessing`, `semantics`, `policy`, - `wrapper_codegen`, `compiling`, `pipeline`, `runtime`, and `end_to_end`. + `codegen`, `compiling`, `pipeline`, `runtime`, and `end_to_end`. Create only the stages that own real evidence. - [x] Put pytest modules below a stage directory rather than directly at the feature root, so path shape always answers both “which feature?” and “which diff --git a/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md b/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md index 99e053b5a..216bd669a 100644 --- a/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md +++ b/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md @@ -219,9 +219,9 @@ sit directly under `PATH`. ### Stage 4 — Shared Parity Harness And Standalone Procedures -Standalone external procedure parity now lives in +Standalone-procedure parity now lives in `tests/wrapper/fortran/external_routines/test_external_procedures.py`. Generated -external-only contract bundles keep one compact entry `.pyi`; native sources, +standalone-only contract bundles keep one compact entry `.pyi`; native sources, objects, archives, and libraries remain separate build-plan facts. - [x] Standalone parity tests use the shared `source` / `generated-pyi` @@ -231,22 +231,22 @@ objects, archives, and libraries remain separate build-plan facts. properties such as exact generated contract text, bridge-source inspection, and validation-before-codegen failures. - [x] One fixed-form source containing one standalone procedure generates a - non-empty root fragment with `@external` and rebuilds equivalently. + non-empty root fragment with `@standalone` and rebuilds equivalently. - [x] One free-form source containing one standalone procedure has the same - `@external` generation and runtime parity. + `@standalone` generation and runtime parity. - [x] One source containing several standalone procedures generates external declarations for all of them and exposes each at the extension root. - [x] Several file-level BLAS/LAPACK-style standalone sources can generate one compact entry `.pyi` containing all external declarations while the native build plan links the separated objects in caller order. -- [x] `@external` makes the bridge emit a completed implicit `external` +- [x] `@standalone` makes the bridge emit a completed implicit `external` declaration or a required explicit interface and no module `use`; a module procedure makes the bridge emit the correct `use `. -- [x] `@external` composes with `@bind("native_name")`: the native external is +- [x] `@standalone` composes with `@bind("native_name")`: the native external is called while the wrapper declaration and root export may use different names. - [x] A handwritten external `.pyi` plus native artifacts builds without source and follows the same placement, binding, validation, and export rules. -- [x] Removing `@external` from a generated package-entry declaration or adding +- [x] Removing `@standalone` from a generated package-entry declaration or adding it to a declaration inside a child-namespace module contract fails during validation before wrapper code generation. @@ -380,7 +380,7 @@ evidence lives in - [x] Several contracts imported by one entry resolve from one static archive and one direct shared library while preserving child module namespaces. - [x] Module procedures build with separately supplied `.mod` directories, while - standalone `@external` procedures build without module search inputs. + standalone `@standalone` procedures build without module search inputs. - [x] A mixed bundle containing native modules and standalone external procedures exposes module members below child namespaces and standalone externals at the extension root. @@ -419,7 +419,7 @@ PRIK_C_DOCS_END --> that entrypoint. Planning and lowering consume completed policy metadata instead of recomputing policy from raw datatypes. Evidence: `tests/semantics/policy/`, - `tests/wrapper_codegen/`, + `tests/codegen/`, `tests/semantics/policy/`, and `prik/semantics/README.md`. - [x] `.pyi` parsing and `.pyi` semantic conversion are separate stages: @@ -527,7 +527,7 @@ PRIK_C_DOCS_END --> `tests/wrapper/fortran/edit_pyi_contracts/`, `tests/semantics/policy/`, `tests/fortran/error_handling/semantics/test_status_contract_semantics.py`, - `tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py`, + `tests/fortran/error_handling/codegen/test_status_error_lowering.py`, `tests/fortran/error_handling/end_to_end/test_status_projection.py`, `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py`, `tests/fortran/pyi_contracts/exports_and_modules/`, and @@ -538,8 +538,8 @@ PRIK_C_DOCS_END --> copy-in/copy-out, native-array handles, field access, and cleanup from typed wrapper-plan actions completed before backend entry. The generators do not select semantic behavior from raw datatype, `intent`, alias, or local-memory - checks. Evidence: `prik/wrapper_codegen/fortran/bridge.py`, - `prik/wrapper_codegen/c/binding.py`, `tests/wrapper_codegen/`, and compiled + checks. Evidence: `prik/codegen/fortran/bridge.py`, + `prik/codegen/c/binding.py`, `tests/codegen/`, and compiled feature evidence under `tests/wrapper/fortran/`. - [x] The remaining Stage 8 bridge and binding policy dispatch audit is closed for the current supported surface. Bridge field getters and setters dispatch @@ -551,10 +551,10 @@ PRIK_C_DOCS_END --> bridge and binding code are local emitted-code, ABI, documentation, or object-model mechanics rather than semantic policy selection. Evidence: `prik/semantics/ownership.py`, - `prik/wrapper_codegen/fortran/bridge.py`, - `prik/wrapper_codegen/c/binding.py`, + `prik/codegen/fortran/bridge.py`, + `prik/codegen/c/binding.py`, `tests/semantics/policy/`, - `tests/wrapper_codegen/`, + `tests/codegen/`, `tests/wrapper/fortran/derived_types/test_derived_layout.py`, and `tests/fortran/memory_management/end_to_end/test_explicit_borrowed_owner.py`. PRIK_C_DOCS_END --> @@ -572,7 +572,7 @@ artifact-level evidence. - [x] A module leaf is named `.pyi`; that filename is its native module identity. Procedure kind, native symbol, contained-versus-external status, argument order, ABI types and kinds, rank, intent, and required - native imports are inferred from ordinary declarations plus `@external`, + native imports are inferred from ordinary declarations plus `@standalone`, `@bind`, and `@native_call` only where those facts are not implicit. - [x] Generated `.pyi` retains every native binding fact needed for module procedures, standalone external procedures, type-bound procedures, operators, @@ -629,7 +629,7 @@ Make generated contracts complete and reproducible before composing them. two module leaves plus one root contract instead of concatenating declarations. That root contract is the sole wrapper input. - [x] Standalone fixed-form and free-form procedures emit non-empty `.pyi` - contracts with explicit `@external` placement. + contracts with explicit `@standalone` placement. - [x] The semantic-format feature checks in representative source-owned contract packages under its `pipeline/fixtures/contracts/` tree; runtime parity fixtures live under the consuming feature's `contracts/` tree as they diff --git a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md index c4e138837..cf7a2eb44 100644 --- a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md +++ b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md @@ -691,7 +691,7 @@ already covered by the new generator. | `tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py::test_reduced_entry_generates_only_reachable_module_variable_bindings` | direct wrapper/build route | semantic .pyi generation/parsing; build/compile/link orchestration; scalar inputs/results; module variables/state | `wrapper-plan` | | `tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py::test_scale_runtime_contract[*]` | source/generated-.pyi parity or parametrized route | semantic .pyi generation/parsing; build/compile/link orchestration; scalar inputs/results | `wrapper-plan` | | `tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py::test_source_named_root_discovers_and_builds_module_leaf` | direct wrapper/build route | semantic .pyi generation/parsing; build/compile/link orchestration; scalar inputs/results; module variables/state | `wrapper-plan` | -| `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_documented_homepage_points_example_builds_and_imports` | direct wrapper/build route | build/compile/link orchestration; module namespace and derived-type inputs/results | `wrapper-plan` | +| `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_documented_readme_points_example_builds_and_imports` | direct wrapper/build route | build/compile/link orchestration; module namespace and derived-type inputs/results | `wrapper-plan` | | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_default_module_name_does_not_collide_with_root_function` | direct wrapper/build route | build/compile/link orchestration | `wrapper-plan` | | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_default_places_artifacts_in_invocation_directory` | direct wrapper/build route | build/compile/link orchestration | `wrapper-plan` | | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_dir_separates_abi_artifact_from_cli_alias` | direct wrapper/build route | build/compile/link orchestration | `wrapper-plan` | @@ -738,20 +738,20 @@ already covered by the new generator. | `tests/wrapper/fortran/edit_pyi_contracts/test_policy_dispatch_contracts.py::test_immutable_scalar_string_array_and_derived_policies_return_replacements` | direct wrapper/build route | semantic .pyi generation/parsing; native handles/descriptors; derived types/object lifetimes; optional/presence/writeback | `wrapper-plan` | | `tests/wrapper/fortran/edit_pyi_contracts/test_surface_edit_contracts.py::*` | direct wrapper/build route | semantic .pyi generation/parsing; classes/methods/properties/overloads; naming/visibility/dispatch | `wrapper-plan` | | `tests/wrapper/fortran/edit_pyi_contracts/test_visibility_contracts.py::*` | direct wrapper/build route | semantic .pyi generation/parsing; scalar module visibility and namespace projection | `wrapper-plan` | -| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_compact_blas_like_folder_generates_one_external_entry_and_preserves_separate_objects` | direct wrapper/build route | external symbols/native linkage; ordinary arrays | `wrapper-plan` | +| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_compact_blas_like_folder_generates_one_standalone_entry_and_preserves_separate_objects` | direct wrapper/build route | external symbols/native linkage; ordinary arrays | `wrapper-plan` | | `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_external_bind_renames_python_export_without_changing_native_call` | direct wrapper/build route | scalar external symbol; explicit bridge interface; renamed export | `wrapper-plan` | | `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_classic_external_bridge_uses_implicit_declaration_and_no_module_use` | direct wrapper/build route | scalar external symbol; implicit external declaration | `wrapper-plan` | -| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_fixed_form_standalone_external_runtime_parity[*]` | source/generated-.pyi parity or parametrized route | scalar external symbol; explicit bridge interface | `wrapper-plan` | -| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_free_form_standalone_external_runtime_parity[*]` | source/generated-.pyi parity or parametrized route | scalar external symbol; explicit bridge interface | `wrapper-plan` | -| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_generated_external_contracts_are_non_empty_root_fragments` | direct wrapper/build route | external symbols/native linkage | `wrapper-plan` | +| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_fixed_form_standalone_procedure_runtime_parity[*]` | source/generated-.pyi parity or parametrized route | scalar external symbol; explicit bridge interface | `wrapper-plan` | +| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_free_form_standalone_procedure_runtime_parity[*]` | source/generated-.pyi parity or parametrized route | scalar external symbol; explicit bridge interface | `wrapper-plan` | +| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_generated_standalone_contracts_are_non_empty_root_fragments` | direct wrapper/build route | external symbols/native linkage | `wrapper-plan` | | `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_handwritten_c_order_flat_contract_passes_rank_preserving_bridge_view` | direct wrapper/build route | external symbols/native linkage; ordinary arrays | `wrapper-plan` | | `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_handwritten_fortran_order_flat_contract_flattens_the_final_python_axes` | direct wrapper/build route | external symbols/native linkage; flat arrays; scalar storage | `wrapper-plan` | -| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_external_allocatable_argument_accepts_a_caller_created_handle` | direct wrapper/build route | external symbols/native linkage; native allocatable descriptors | `wrapper-plan` | +| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_standalone_allocatable_argument_accepts_a_caller_created_handle` | direct wrapper/build route | external symbols/native linkage; native allocatable descriptors | `wrapper-plan` | | `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_optional_flat_contracts_preserve_present_and_absent_calls` | direct wrapper/build route | optional/presence; F-order and C-order flat arrays | `wrapper-plan` | | `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_module_procedure_bridge_uses_native_module_scope` | direct wrapper/build route | external symbols/native linkage | `wrapper-plan` | -| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_namespace_imported_module_rejects_external_marker_before_codegen` | non-generating: validation/failure-path assertion | external symbols/native linkage | `not-applicable` | -| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_one_source_with_several_standalone_externals_exports_each_at_root[*]` | source/generated-.pyi parity or parametrized route | scalar external symbols; explicit bridge interfaces | `wrapper-plan` | -| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_package_entry_rejects_non_external_root_declaration_before_codegen` | non-generating: validation/failure-path assertion | external symbols/native linkage | `not-applicable` | +| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_namespace_imported_module_rejects_standalone_marker_before_codegen` | non-generating: validation/failure-path assertion | external symbols/native linkage | `not-applicable` | +| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_one_source_with_several_standalone_procedures_exports_each_at_root[*]` | source/generated-.pyi parity or parametrized route | scalar external symbols; explicit bridge interfaces | `wrapper-plan` | +| `tests/wrapper/fortran/external_routines/test_external_procedures.py::test_package_entry_rejects_non_standalone_root_declaration_before_codegen` | non-generating: validation/failure-path assertion | external symbols/native linkage | `not-applicable` | | `tests/wrapper/fortran/function_calls/test_function_call_generated_pyi_contracts.py::*` | non-generating: generated semantic .pyi fixture parity | semantic .pyi generation/parsing | `not-applicable` | | `tests/wrapper/fortran/function_calls/test_native_call_examples.py::test_native_call_examples_build_from_generated_pyi_and_native_shared_library` | direct wrapper/build route | native-call projections; ordinary arrays; strings; derived types/object lifetimes | `wrapper-plan` | | `tests/wrapper/fortran/function_calls/test_native_call_examples.py::test_native_call_examples_cover_scalar_array_string_and_object_projection[*]` | source/generated-.pyi parity or parametrized route | native-call projections; ordinary arrays; strings; derived types/object lifetimes | `wrapper-plan` | @@ -851,7 +851,7 @@ For each lane: ABI/handoff specs, generator-owned structural checks, directly named backend lowering methods, source-printer support, and support-report coverage. 6. Implement the minimum dependency-closed backend slice in - `prik.wrapper_codegen`: copy small suitable pieces, rewrite oversized legacy + `prik.codegen`: copy small suitable pieces, rewrite oversized legacy classes as minimal equivalents, and add only the intermediate tests required by the contract above. 7. Generate the plan from policy-completed semantic IR, invoke the directly @@ -910,7 +910,7 @@ the product contract. ### Foundation and semantic authority -- [x] Establish the isolated `prik.wrapper_codegen` package boundary and +- [x] Establish the isolated `prik.codegen` package boundary and visitor infrastructure. - [x] Complete the first primitive lane in general wrapper policy before planning, including native-call order, result projection, ownership, and @@ -950,7 +950,7 @@ the product contract. - [x] Run focused wrapper-codegen and pipeline tests; the walkthrough for both supported entry choices where practical; `tests/wrapper` excluding LAPACK; documentation checks; `git diff --check`; the required static-analysis suite; - and `tools/check_wrapper_codegen_complexity.py`. + and `tools/check_codegen_complexity.py`. ## Phase 3 — Scalar Inout, Optional, And Descriptor-Like Scalars @@ -1786,6 +1786,16 @@ byte-order, contiguity, writeability, zero length, and native argument order. spec, validation, named C/Fortran lowering, reduced legacy/direct parity, support widening, and ledger evidence. +Boolean arrays retain an exact one-byte NumPy boundary independently of native +language spelling. Semantic IR records compiler-measured native storage as +`Bool8`, `Bool16`, `Bool32`, or `Bool64`. Post-IR wrapper policy must distinguish +an exact `c_bool` view from an exact-kind representation copy and must record +copy-in and copy-out directions before planning. For copied arrays the bridge +owns the exact-kind temporary; copy-out both converts truth values and writes +canonical zero/one boundary bytes in one traversal. Binding code continues to +validate and forward only `NPY_BOOL` storage and must not infer native logical +kind from a semantic name. + ### Phase 6B — Declared Extents, Flat Storage, And Dense Rank Included: fixed and visible-symbol extent expressions, lower-bound-derived @@ -1796,6 +1806,11 @@ bridge association order follows the completed layout. Any expression that cannot be represented by available roles remains blocked rather than being recomputed in a backend. +Declaration-expression normalization is shared across module variables, +derived fields, dummy arguments, and results. Generated `.pyi` uses Python +array properties (`a.size`, `a.shape[i]`, and `a.ndim`), while the completed +plan carries role-bound expressions that each backend only renders. + Order is an exact-storage selector, not an implicit conversion selector. `ORDER_F` preserves logical axes over Fortran-contiguous storage. `ORDER_C` passes the original C-contiguous address and reverses bridge extents, so native @@ -2136,7 +2151,7 @@ generated shape without dereferencing an invalid address. wrapped forms. - [x] Add focused completed-policy tests for every authoritative action and blocker, plus `array-raw-address-inputs` support classification. -- [x] Add `tests/fortran/raw_addresses/wrapper_codegen/test_raw_array_lowering.py` for plan +- [x] Add `tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py` for plan shape, edits, validation, C nodes, Fortran nodes, native order, and the absence of buffer/descriptor/lifecycle nodes. - [x] Extract `fill_vector_raw` from @@ -2296,7 +2311,7 @@ sources of truth: them from generated source text. The legacy generators are behavioral oracles, not dependencies of -`prik/wrapper_codegen`. Reuse the runtime helpers and completed semantic +`prik/codegen`. Reuse the runtime helpers and completed semantic records directly. Rewrite the smallest equivalent node/lowering methods in the direct generators; do not import legacy binding/bridge generator methods or legacy codegen-model nodes into the wrapper-plan package. @@ -2940,7 +2955,7 @@ new success signal after the Phase 7F correction. Post-correction closure evidence (2026-07-14): 214 focused runtime-handle, policy, planning, lowering, legacy-dispatch, and Phase 7 direct-plan tests; -199 complete `tests/wrapper_codegen` tests; 1,123 documentation tests; 317 +199 complete `tests/codegen` tests; 1,123 documentation tests; 317 wrapper tests outside the shared real-library parameter plus the BLAS-only parameter; and zero locally executed LAPACK tests all passed. The wrapper complexity checker, Ruff lint/format, Bandit, Vulture, whitespace, and the @@ -3124,7 +3139,7 @@ architecture: Capture complete legacy artifacts before each direct slice. Preserve observable runtime behavior while replacing backend inference with completed typed plans. -Do not copy the broad legacy generator control flow into `wrapper_codegen`. +Do not copy the broad legacy generator control flow into `codegen`. The existing wrapper tests decompose as follows: @@ -3354,7 +3369,7 @@ Complete the semantic contract before defining direct plan records. remove the blanket class-owner blocker until the minimal opaque type surface is direct and every remaining Phase 9 dependency is reported separately. - [x] Add normal-print plan tests and direct generator preflight tests under - `tests/wrapper_codegen/test_phase8_derived_types.py`. + `tests/codegen/test_phase8_derived_types.py`. ### Phase 8C — Minimal Opaque Wrapper Storage And Lifecycle @@ -3839,7 +3854,7 @@ they do not expose the public callback semantics deferred to Phase 10. reduced source/generated contract under `tests/wrapper/fortran/derived_types/contracts/fscalar_derived_actual_dummy_matrix_phase8/`, focused policy/plan/artifact tests in - `tests/wrapper_codegen/test_phase8_scalar_derived_actual_dummy_matrix.py`, and + `tests/codegen/test_phase8_scalar_derived_actual_dummy_matrix.py`, and compiled tests in `tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py`. Replace the earlier proposed separate module-allocatable and @@ -3929,7 +3944,7 @@ they do not expose the public callback semantics deferred to Phase 10. - Post-IR origin, identity, handoff, ownership, field, lifecycle, and exact blocker evidence lives in - `tests/wrapper_codegen/test_phase8_derived_types.py`, with supporting parser, + `tests/codegen/test_phase8_derived_types.py`, with supporting parser, printer, source-conversion, ownership, and planning suites named in `tests/wrapper/CHECKLIST_COVERAGE.md`. - Public-field validation is split into named completed-policy, descriptor, @@ -3942,7 +3957,7 @@ they do not expose the public callback semantics deferred to Phase 10. direct-address module object, constant value, field, owner-retention, allocation/cleanup artifact, and exactly-once finalization behavior. - The former isolated scalar-derived descriptor evidence in - `tests/wrapper_codegen/test_phase8_scalar_derived_descriptors.py`, + `tests/codegen/test_phase8_scalar_derived_descriptors.py`, `tests/wrapper/fortran/derived_types/test_scalar_derived_descriptor_plan.py`, and `tests/data/fortran/wrapper/fscalar_derived_descriptors_f90.f90` is superseded by the comprehensive policy/artifact and compiled matrix files @@ -4373,21 +4388,33 @@ boundary. ### Phase 10 Boundary And Explicit Non-Scope Phase 10 composes ordinary call transfers completed in Phases 2-9 but does not -reinterpret them. A callback signature is transport-facing: it describes the -procedure ABI that native Fortran calls, including argument order, -value/reference transport, rank, shape, character length, and result -representation. It deliberately does not repeat native callback `intent`. -Normal wrapper projection and callback adapter projection remain distinct -completed records. - -Named `@prototype` declarations are the single callback-signature authority. -Callback arguments reference a prototype by name; bare prototype arguments use -reference transport and permissive writable Python storage, while `Value(T)` -is the only transport override. Prototypes are semantic-only declarations and -never become Python runtime exports. Post-IR policy selects either an implicit -external adapter declaration or a named explicit declaration from completed -prototype characteristics. Lowering does not reconstruct that decision or -duplicate native `intent`. +reinterpret them. A prototype is interface-facing: it describes the exact +procedure declaration that native Fortran uses, including argument order, +`In`/`Out`/`InOut` direction, value/reference transport, rank, shape, character +length, result representation, and procedure characteristics. Normal wrapper +projection and callback adapter projection remain distinct completed records. + +Named `@prototype` declarations are the single exact native-signature +authority. Annotation use selects a callback signature; call use selects a +directly callable standalone procedure entity. +`In(T)`, `Out(T)`, and `InOut(T)` preserve exact dummy direction, while +`Addr(T)` and `Value(T)` preserve transport independently. `@pure` preserves +the corresponding procedure characteristic. Prototypes are semantic-only +declarations and never become Python runtime exports. + +A pure prototype is not a supported Python callback signature. The callback +adapter calls the Python runtime and therefore cannot satisfy Fortran purity; +post-IR policy must block a prototype used both as a specification function and +as a callback before planning. + +Post-IR policy classifies each use as a callback, a standalone procedure entity, +or a module-procedure call. One shared prototype-signature plan owns the +generated `prik_` abstract-interface symbol and exact characteristics. Lowering +only declares callback adapters or concrete entities with +`procedure(prik_...)`; it does not reconstruct placement, purity, direction, +transport, or declaration mode. Direct prototype calls never fall back to an +implicit external declaration, and `@standalone` is rejected on a prototype as +redundant placement metadata. The supported callback contract is deliberately call-scoped: @@ -4751,9 +4778,9 @@ backend or legacy lowering runs. in one cutover without compatibility flags or per-function fallback. - [x] Do not move modified isolated nodes or printers back into the legacy package during migration. After final cutover, remove the legacy package - pieces proven unused and keep `prik.wrapper_codegen` as the canonical + pieces proven unused and keep `prik.codegen` as the canonical generator rather than performing a second package rename. -- [x] Keep semantic `.pyi` emission under `prik.wrapper_codegen.printers` and +- [x] Keep semantic `.pyi` emission under `prik.codegen.printers` and retire focused tests of the old semantic AST, bridge, binding, and printer implementation before deleting the legacy package. - [x] Remove the temporary legacy route and its route diagnostics after every @@ -4788,13 +4815,13 @@ maintainability reports, and `git diff --check` all passed. the minimal intermediate contract tests required above, and the required static-analysis suite from `AGENTS.md`. - [x] Wrapper-codegen implementation changes pass - `python3 tools/check_wrapper_codegen_complexity.py` with no handler waiver. + `python3 tools/check_codegen_complexity.py` with no handler waiver. - [x] Runtime wrapper tests cover every changed generated behavior. - [x] Every migrated lane completed legacy-oracle comparison before cutover; final tests now exercise only the canonical wrapper-plan route and retain the existing behavior and ABI-relevant call assertions. - [x] Structural dependency tests prove complete generator isolation: no - imports from `prik.wrapper_codegen` to `prik.codegen` or in the reverse + imports from `prik.codegen` to `prik.codegen` or in the reverse direction. - [x] BLAS and LAPACK full-library wrapper tests remained excluded locally and in GitHub Actions throughout Phases 0-11. At the explicit Phase 12 gate, enable @@ -4822,7 +4849,7 @@ The legacy `prik.codegen` package, `prik/semantics/ir2ast.py`, and the obsolete fallback, compatibility import, or rejection-only test preserves that route. Required behavior remains with its current owner: completed semantic policy -tests for semantic decisions, `tests/wrapper_codegen/` for plans and direct +tests for semantic decisions, `tests/codegen/` for plans and direct source generation, and compiled `tests/wrapper/` cases for public Python behavior and native ABI outcomes. Static-analysis baselines cover only source that remains in the repository. diff --git a/docs/old_docs/developper_guide.md b/docs/old_docs/developper_guide.md index 7267a067a..5c6ae2284 100644 --- a/docs/old_docs/developper_guide.md +++ b/docs/old_docs/developper_guide.md @@ -207,7 +207,7 @@ implementation files. | `.pyi` printing | `prik/codegen/printers/pyi_printer.py` | `tests/semantics/test_pyi_printer.py`, `tests/semantics/test_pyi_printer_modern_example.py` | | `.pyi` loading/editing | `prik/pyi_parser/parser.py` | `tests/pyi/test_pyi_to_ir.py`, `tests/pyi/test_pyi_fixture_suite.py` | | Fortran wrapper orchestration | `prik/wrapping.py` | `tests/wrapper/fortran/native_build/test_build_modes.py`, `tests/wrapper/fortran/multi_source/test_multi_source_builds.py` | -| Wrapper planning and owner-local errors | `prik/wrapper_codegen/planner.py` | `tests/wrapper_codegen/` | +| Wrapper planning and owner-local errors | `prik/codegen/planner.py` | `tests/codegen/` | | Semantic IR to codegen AST | `prik/semantics/ir2ast.py` | `tests/semantics/test_ir2ast.py`, `tests/wrapper/` | | Fortran-to-C bridge and CPython binding | `prik/codegen/bridges/fortran_to_c.py`, `prik/codegen/bindings/c_to_python.py` | `tests/wrapper/` subject suites | | Native compilation and binding support | `prik/compiling/`, `prik/binding_support/` | `tests/wrapper/fortran/native_build/test_runtime_abi.py`, `tests/wrapper/fortran/native_build/test_build_modes.py` | @@ -805,7 +805,7 @@ The test ownership is: - loader syntax and error behavior: `tests/pyi/test_pyi_to_ir.py`; - printer round-trip shape: `tests/semantics/test_pyi_printer.py`; -- wrapper-plan support diagnostics: `tests/wrapper_codegen/`. +- wrapper-plan support diagnostics: `tests/codegen/`. When adding projection syntax, first add loader tests that prove the accepted syntax and rejected syntax. Then add policy or wrapper-plan tests only if the @@ -824,7 +824,7 @@ coverage only when the public contract changes. | Parser fixture goldens | Serialized parser contract over curated files | `tests/parser/test_fortran_fixture_suite.py`, `tests/parser/c/test_c_fixture_suite.py` | | Semantic tests | Parser facts converted to wrapper-neutral IR | `tests/semantics/test_fortran2ir.py`, `tests/semantics/test_c2ir.py` | | `.pyi` tests | Editable contract loader/printer behavior | `tests/pyi/test_pyi_to_ir.py`, `tests/semantics/test_pyi_printer.py` | -| Wrapper-plan tests | Completed-policy and unsupported-contract diagnostics | `tests/wrapper_codegen/` | +| Wrapper-plan tests | Completed-policy and unsupported-contract diagnostics | `tests/codegen/` | | CLI tests | User commands, output routing, diagnostics | `tests/parser/test_cli.py`, `tests/parser/test_preprocessing_cli.py` | | Wrapper build tests | Artifact placement, direct/Makefile modes, multi-source ordering | `tests/wrapper/fortran/native_build/test_build_modes.py`, `tests/wrapper/fortran/multi_source/` | | Wrapper runtime tests | Imported extension behavior, ownership, lifetime, and failures | `tests/wrapper/` subject suites indexed by `tests/wrapper/fortran/README.md` | @@ -1024,7 +1024,7 @@ Example target: report a new unsupported C/Fortran semantic contract clearly. Focused verification: ```bash -PYTHONPATH=. pytest -q tests/wrapper_codegen/ +PYTHONPATH=. pytest -q tests/codegen/ ``` ### Add Or Change CLI Behavior @@ -1222,7 +1222,7 @@ Focused tests by concern: - C parser-to-IR conversion: `PYTHONPATH=. pytest -q tests/semantics/test_c2ir.py` - Wrapper-plan support diagnostics: - `PYTHONPATH=. pytest -q tests/wrapper_codegen/` + `PYTHONPATH=. pytest -q tests/codegen/` - `.pyi` printer: `PYTHONPATH=. pytest -q tests/semantics/test_pyi_printer.py` - `.pyi` loader and edited stub behavior: diff --git a/docs/old_docs/pyi_format.md b/docs/old_docs/pyi_format.md index 85cd7eb47..84ee564a8 100644 --- a/docs/old_docs/pyi_format.md +++ b/docs/old_docs/pyi_format.md @@ -69,7 +69,7 @@ reconciles imported external type references across the loaded set. ## Contract Bundles And Native Procedure Placement -> **Roadmap:** `@external`, generated contract bundles, `__init__.pyi` export +> **Roadmap:** `@standalone`, generated contract bundles, `__init__.pyi` export > lowering, `--root-contract`, and wrapper `--out` are the required contract > described here, but are not implemented by the current `.pyi` build subset. @@ -82,7 +82,7 @@ facts. ### Contained Module Procedures One Fortran module maps to one `.pyi` file named for that module. A procedure -declared without `@external` in that module contract is contained in the native +declared without `@standalone` in that module contract is contained in the native Fortran module: ```python @@ -104,15 +104,15 @@ procedure to another module or reinterpret it as standalone. ### Standalone External Procedures A procedure outside every Fortran module is marked explicitly with -`@external`: +`@standalone`: ```python # externals/dgesv.pyi -@external +@standalone def dgesv(a: Float64[:, :], b: Float64[:, :]) -> Int32: ... ``` -`@external` is immutable native-placement metadata. The bridge must generate a +`@standalone` is immutable native-placement metadata. The bridge must generate a matching explicit Fortran interface and call the external procedure without a `use ` statement. The procedure therefore needs no Fortran `.mod` file, but its defining object, archive, or shared library must be supplied to the @@ -122,17 +122,17 @@ Python-visible renaming is separate from placement. `@bind` retains the native Fortran procedure name while the declaration uses a wrapper name: ```python -@external +@standalone @bind("dgesv") def solve(a: Float64[:, :], b: Float64[:, :]) -> Int32: ... ``` Here the bridge calls the external native procedure `dgesv`; the root export contract may expose the wrapper declaration as `solve`. `@bind` does not turn a -module procedure into an external procedure and `@external` does not rename a +module procedure into an external procedure and `@standalone` does not rename a symbol. -Every generated standalone declaration must carry `@external`. Handwritten +Every generated standalone declaration must carry `@standalone`. Handwritten contracts must do the same. Missing or contradictory placement metadata must fail during `.pyi` validation or wrapper planning, before bridge emission or native compilation. @@ -147,13 +147,13 @@ suffix: | One source containing one module | One `.pyi` | | One source containing several modules | One contract directory with `__init__.pyi` and one `.pyi` per module | | Several sources containing modules | One contract directory with `__init__.pyi` and one `.pyi` per module | -| One fixed- or free-form source containing only standalone procedures | One root fragment with `@external` on every procedure | +| One fixed- or free-form source containing only standalone procedures | One root fragment with `@standalone` on every procedure | | Several standalone-procedure sources, such as BLAS/LAPACK | One contract directory with `__init__.pyi` and organized external fragments | | Mixed modules and standalone procedures | One contract directory containing module contracts, external fragments, and `__init__.pyi` | A physical source file containing two modules generates two module `.pyi` files. Conversely, a source file containing several standalone procedures may generate -one external fragment containing several `@external` declarations because those +one external fragment containing several `@standalone` declarations because those procedures all contribute to the extension root rather than a native module namespace. @@ -217,7 +217,7 @@ files while the generated bridge is compiled: ``` Archives do not normally contain `.mod` files, so module directories remain -separate inputs. Standalone `@external` procedures require no `.mod` file +separate inputs. Standalone `@standalone` procedures require no `.mod` file because the bridge emits their implicit external declaration or required explicit interface from the semantic contract. @@ -232,7 +232,7 @@ Required link cases are: | Vendor shared implementation | direct `.so` path or `--native-library NAME` plus search directory | | Mixed implementation | objects, archives, direct shared libraries, and named libraries in one ordered plan | | Module procedures | native artifacts plus every required `.mod` search directory | -| Standalone procedures | native artifacts only; interfaces come from `@external` declarations | +| Standalone procedures | native artifacts only; interfaces come from `@standalone` declarations | Static link order is semantically significant: dependent objects precede the archives or libraries that satisfy them, and dependent libraries precede their diff --git a/docs/old_docs/pyi_wrapper_checklist.md b/docs/old_docs/pyi_wrapper_checklist.md index c913f9a0f..02895d264 100644 --- a/docs/old_docs/pyi_wrapper_checklist.md +++ b/docs/old_docs/pyi_wrapper_checklist.md @@ -212,15 +212,15 @@ different public API or runtime contract. ### 6.2 Standalone native placement - [ ] One fixed-form source containing one standalone procedure generates a - non-empty root fragment with `@external` and rebuilds equivalently. + non-empty root fragment with `@standalone` and rebuilds equivalently. - [ ] One free-form source containing one standalone procedure has the same - `@external` generation and runtime parity. + `@standalone` generation and runtime parity. - [ ] One source containing several standalone procedures generates external declarations for all of them and exposes each at the extension root. -- [ ] `@external` makes the bridge emit an implicit external declaration or a +- [ ] `@standalone` makes the bridge emit an implicit external declaration or a required explicit interface and no module `use`; a module procedure makes the bridge emit the correct `use `. -- [ ] `@external` composes with `@bind("native_name")`: the native external is +- [ ] `@standalone` composes with `@bind("native_name")`: the native external is called while the wrapper declaration and root export may use different names. - [ ] A handwritten external `.pyi` plus native artifacts builds without source and follows the same placement, binding, validation, and export rules. @@ -260,7 +260,7 @@ different public API or runtime contract. - [ ] Mixed object, archive, direct shared-library, and named-library inputs preserve dependency-safe link order and resolve every native symbol. - [ ] Module procedures are tested with separately supplied `.mod` directories; - standalone `@external` procedures are tested without `.mod` inputs. + standalone `@standalone` procedures are tested without `.mod` inputs. - [ ] Static archive dependency order, repeated archives or linker groups for cyclic dependencies, and required transitive libraries have runtime tests. - [ ] Missing symbols, duplicate definitions, incompatible artifacts, missing @@ -272,7 +272,7 @@ different public API or runtime contract. ### 6.6 Invalid structural edits -- [ ] Removing `@external` from a generated external declaration, adding it to a +- [ ] Removing `@standalone` from a generated external declaration, adding it to a module procedure, changing native scope, or moving a declaration between module contracts fails during validation or wrapper planning before codegen. diff --git a/docs/stylesheets/site.css b/docs/stylesheets/site.css index 14b7989ca..70481256c 100644 --- a/docs/stylesheets/site.css +++ b/docs/stylesheets/site.css @@ -150,6 +150,68 @@ outline-offset: 2px; } +.prik-faq-item { + max-width: 56rem; + margin: 0.8rem 0; + border: 1px solid #d6e0e5; + border-radius: 0.45rem; + background: #fff; + box-shadow: 0 1px 3px rgb(0 0 0 / 8%); +} + +.prik-faq-item summary { + position: relative; + padding: 0.9rem 3rem 0.9rem 1rem; + color: #253746; + cursor: pointer; + font-size: 1.05rem; + font-weight: 700; + list-style: none; +} + +.prik-faq-item summary::-webkit-details-marker { + display: none; +} + +.prik-faq-item summary::after { + position: absolute; + top: 50%; + right: 1rem; + color: #176b64; + content: "+"; + font-size: 1.35rem; + line-height: 1; + transform: translateY(-50%); +} + +.prik-faq-item[open] summary { + border-bottom: 1px solid #d6e0e5; + background: #f5fbfa; +} + +.prik-faq-item[open] summary::after { + content: "−"; +} + +.prik-faq-item summary:focus-visible { + outline: 2px solid #176b64; + outline-offset: 2px; +} + +.prik-faq-item:target { + border-color: #176b64; + box-shadow: 0 0 0 2px rgb(23 107 100 / 14%); +} + +.prik-faq-item > :not(summary) { + margin-right: 1rem; + margin-left: 1rem; +} + +.prik-faq-item > :last-child { + margin-bottom: 1rem; +} + .prik-performance-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); diff --git a/docs/user/examples/blas-wrapper.md b/docs/user/examples/blas-wrapper.md index 551a8b28a..15505f278 100644 --- a/docs/user/examples/blas-wrapper.md +++ b/docs/user/examples/blas-wrapper.md @@ -9,35 +9,36 @@ publication: reviewed # Build and Validate the Reference BLAS with PRIK -This example shows how to take the official Reference BLAS sources and produce two importable Python extension modules: +This example builds the official Reference BLAS sources as two importable +Python extension modules: - one generated by **PRIK** - one generated by **NumPy’s f2py** -It then tests both wrappers against explicit mathematical expectations. -The full test suite in the repository applies the same method to all 155 callable routines in the Reference BLAS corpus. +It compares both wrappers with independent mathematical results across all 155 +callable Reference BLAS routines. -### Why this example exists +### What this example shows -- It demonstrates PRIK on a real Fortran library. -- It makes the differences between the two wrappers visible. -- It verifies numerical behaviour with an independent mathematical check (instead of only checking that the two wrappers agree). +- Build PRIK and f2py wrappers against the same compiled BLAS library. +- Call vector and matrix routines with NumPy arrays. +- Compare numerical results and the Python interfaces produced by each tool. You should already be comfortable with NumPy arrays, basic packaging, and building Fortran extensions. --- -## Versions used by the maintained example +## Versions used -| Component | Version / source | -|--------------------|-------------------------------------------------------| -| PRIK | current repository checkout (`0.1.0`) | -| Reference BLAS | snapshot shipped in Netlib LAPACK 3.12.1 | -| Python | 3.12 (dedicated CI job) | -| NumPy / f2py | NumPy 2.5.1 | -| Meson | 1.11.2 | -| Ninja | 1.13.0 | -| Fortran compiler | GNU Fortran 13 in CI (any compatible `gfortran` works locally) | +| Component | Version / source | +| --- | --- | +| PRIK | current repository checkout | +| Reference BLAS | snapshot shipped in Netlib LAPACK 3.12.1 | +| Python | 3.12 in the dedicated CI job | +| NumPy / f2py | NumPy 2.5.1 | +| Meson | 1.11.2 | +| Ninja | 1.13.0 | +| Fortran compiler | GNU Fortran 13 in CI; a compatible `gfortran` works locally | > **Note:** f2py is part of NumPy. > On Python 3.12 it uses the Meson backend, which is why Meson and Ninja are required. @@ -58,8 +59,8 @@ git clone https://github.com/PyNumLab/prik.git cd prik python3 -m venv .venv . .venv/bin/activate -python -m pip install --upgrade pip -python -m pip install -e ".[qa]" \ +python3 -m pip install --upgrade pip +python3 -m pip install -e ".[qa]" \ "numpy==2.5.1" "meson==1.11.2" "ninja==1.13.0" ``` @@ -147,15 +148,13 @@ python -m numpy.f2py -c \ --opt=-O0 ``` -The committed [`blas.pyf`](../../../examples/blas/blas.pyf) is the reviewed -f2py interface. f2py compiles only its wrapper and links it to +The committed [`blas.pyf`](../../../examples/blas/blas.pyf) defines the f2py +interface. f2py compiles only its wrapper and links it to `BLAS_SHARED_LIBRARY`, so both wrappers exercise the same compiled BLAS implementations. -Six rotation routines document scalar writebacks without declaring Fortran -`intent`. Their reviewed `intent(inout)` declarations live directly in -`blas.pyf`, and the tests pass typed 0-D arrays. PRIK needs neither: it returns -unannotated scalar writebacks directly, with ordinary scalar arguments. +A few routines expose scalar writebacks differently through the two wrappers; +the comparison below shows those return-value differences explicitly. Import both modules: @@ -181,61 +180,24 @@ PRIK deliberately follows the native scalar contract: --- -## 4. Run the correctness tests +## 4. Run the complete test suite -The names `prik_blas` and `f2py_blas` in the tests are session-scoped pytest -fixtures from [`conftest.py`](../../../examples/blas/conftest.py). After the -build scripts finish, they import the modules and reuse them for every test -under [`examples/blas/tests/`](../../../examples/blas/tests/). +Build both wrappers and run the 155-routine suite: -The tests import small, explicit helpers from -[`tests/helpers.py`](../../../examples/blas/tests/helpers.py). The two helpers used below -are intentionally narrow: - -- `assert_allclose_for_dtype` chooses a tolerance from the result dtype and the - number of accumulated operations. It delegates the final comparison to - `numpy.testing.assert_allclose`. -- `assert_storage_unchanged` uses exact array equality, including NaNs, to prove - that an input-only array or padding was not modified. - -Their complete implementations are short enough to show here. -Both helper definitions and the test functions below assume: - -```python -import numpy as np +```bash +source examples/blas/build_all.sh +python3 -m pytest -q examples/blas/tests ``` - -```python -def assert_allclose_for_dtype(actual, expected, *, operation_size: int = 1) -> None: - """Compare floating values with dtype- and accumulation-aware tolerances.""" - actual_array = np.asarray(actual) - expected_array = np.asarray(expected) - dtype = np.result_type(actual_array.dtype, expected_array.dtype) - real_dtype = np.empty((), dtype=dtype).real.dtype - epsilon = np.finfo(real_dtype).eps - scale = max(1, operation_size) - magnitude = max(1.0, float(np.max(np.abs(expected_array), initial=0.0))) - np.testing.assert_allclose( - actual_array, - expected_array, - rtol=epsilon * 8 * scale, - atol=epsilon * 8 * scale * magnitude, - ) -``` - - -```python -def assert_storage_unchanged(actual: np.ndarray, original: np.ndarray) -> None: - """Require exact preservation, including NaNs and sentinel padding.""" - np.testing.assert_array_equal(actual, original, strict=True) -``` +The tests cover vector, matrix, packed, banded, symmetric, Hermitian, and +triangular operations. Each routine is called with representative inputs and +checked against an independent mathematical result. --- -## 5. Validate behaviour, not just compilation +## 5. See how results are validated -A solid numerical test checks three relationships: +Each comparison checks three relationships: ```text PRIK result == independent mathematical result @@ -243,12 +205,13 @@ f2py result == independent mathematical result PRIK result == f2py result ``` -It should also verify mutation, input preservation, dtype, shape, increments, leading dimensions, and unused storage when those properties are part of the routine contract. +The suite also checks mutation, input preservation, dtype, shape, increments, +leading dimensions, and unused storage where they are part of a routine's +contract. The independent formula or residual remains the primary numerical +reference. -f2py provides useful differential evidence, but the **independent formula or residual** is the primary oracle. - -The two examples below are taken verbatim from the runnable suite. -A documentation test compares these blocks with the Python AST, so the page and the real tests stay in sync. +The two examples below come directly from the runnable suite and use its small +NumPy comparison helpers. ### DAXPY – in-place vector update @@ -303,28 +266,20 @@ def test_ddot(prik_blas, f2py_blas): --- -## 6. Run the maintained example - -Build both wrappers once, then run any user-facing test selection: - -```bash -cd "$REPOSITORY_ROOT" -source examples/blas/build_all.sh -python -m pytest -q examples/blas/tests -``` +## 6. Run focused examples -Focused commands for quick debugging: +After building the wrappers, run a family or one routine: ```bash -python -m pytest -q examples/blas/tests/test_level1_real.py -python -m pytest -q examples/blas/tests/test_level1_real.py::test_daxpy -python -m pytest -q examples/blas/tests -k dgemm +python3 -m pytest -q examples/blas/tests/test_level1_real.py +python3 -m pytest -q examples/blas/tests/test_level1_real.py::test_daxpy +python3 -m pytest -q examples/blas/tests -k dgemm ``` - Complete Level-1 examples → [`test_level1_real.py`](../../../examples/blas/tests/test_level1_real.py) - Matrix / packed / banded / symmetric / Hermitian / triangular examples → files under [`examples/blas/tests/`](../../../examples/blas/tests/) -- Authoritative classification → [`routine_inventory.py`](../../../examples/blas/routine_inventory.py) -- Coverage guard → [`test_routine_coverage.py`](../../../examples/blas/tests/test_routine_coverage.py) +- Public routine list → [`routine_inventory.py`](../../../examples/blas/routine_inventory.py) +- Routine coverage check → [`test_routine_coverage.py`](../../../examples/blas/tests/test_routine_coverage.py) For the copyable build scripts, test commands, and source provenance, see the [`examples/blas` project README](../../../examples/blas/README.md). @@ -334,16 +289,16 @@ For the copyable build scripts, test commands, and source provenance, see the ## Troubleshooting - Confirm that `gfortran`, `meson` and `ninja` are on your `PATH`. -- On Python 3.12+, do **not** force the old distutils backend of f2py. Use the pinned Meson + Ninja setup shown above. +- On Python 3.12+, do **not** force the old distutils backend of f2py. Use the + pinned Meson and Ninja setup shown above. - Run a single failing test with more detail and keep the build directory: ```bash - python -m pytest -vv -s --basetemp=/tmp/prik-blas-debug examples/blas/tests/test_level1_real.py::test_daxpy + python3 -m pytest -vv -s --basetemp=/tmp/prik-blas-debug \ + examples/blas/tests/test_level1_real.py::test_daxpy ``` - Read the compiler output from `build_all.sh`. -- Keep correctness tests and benchmarking completely separate. - This suite uses small deterministic inputs and makes no performance claims. --- diff --git a/docs/user/examples/fftpack-wrapper.md b/docs/user/examples/fftpack-wrapper.md new file mode 100644 index 000000000..08ceda3c7 --- /dev/null +++ b/docs/user/examples/fftpack-wrapper.md @@ -0,0 +1,255 @@ +--- +title: Build and Validate FFTPACK with PRIK +audience: users, advanced users +prerequisites: arrays, packaging +related: minpack-wrapper.md, ../guide/arrays.md +status: maintained +publication: reviewed +--- + +# Build and Validate FFTPACK with PRIK + +This example takes the checked-in +[fortran-lang/fftpack](https://github.com/fortran-lang/fftpack) sources and +builds an importable Python extension containing all 31 public procedures from +the `fftpack` module. + +The example compares Fourier, cosine, sine, frequency, and spectrum operations +with NumPy, SciPy, or known transform properties. + +### What this example shows + +- Wrap a complete multi-file Fortran library as one Python extension. +- Call both low-level and high-level transforms with NumPy arrays. +- Check transform values, normalization, frequency ordering, dtype, and shape. + +You should already be comfortable with NumPy arrays and building a local +Fortran extension. + +--- + +## Versions used + +| Component | Version / source | +| --- | --- | +| PRIK | current repository checkout | +| FFTPACK | [fortran-lang/fftpack commit `0fffe7c`](https://github.com/fortran-lang/fftpack/tree/0fffe7c05a918363a7cc12ae138a695afd115f36) | +| Python | 3.12 in the dedicated CI job | +| NumPy | 2.5.1 | +| SciPy | 1.18.0 | +| Fortran compiler | GNU Fortran 13 in CI; a compatible `gfortran` works locally | + +The repository owns the checked-in source snapshot under +`examples/fftpack/native/`, so the example does not download code during its +build. + +--- + +## 1. Prepare the repository and toolchain + +Clone PRIK, create a virtual environment, and install the Python tools used by +the dedicated CI job: + +```bash +git clone https://github.com/PyNumLab/prik.git +cd prik +python3 -m venv .venv +. .venv/bin/activate +python3 -m pip install --upgrade pip +python3 -m pip install -e ".[qa]" "numpy==2.5.1" "scipy==1.18.0" +``` + +Install GNU Fortran separately. On Ubuntu: + +```bash +sudo apt-get update +sudo apt-get install --yes gfortran +gfortran --version +``` + +All remaining commands run from the repository root with the virtual +environment active. The complete runnable project lives under +[`examples/fftpack/`](../../../examples/fftpack/). + +--- + +## 2. Build the PRIK wrapper + +FFTPACK uses public module declarations, submodule implementations, and +link-only computational kernels. The build command gives each source the role +it needs: + + +```bash +export EXAMPLE_WORKSPACE="$PWD" +export FFTPACK_BUILD_ROOT="$(mktemp -d)" +export FFTPACK_NATIVE_DIR="$EXAMPLE_WORKSPACE/examples/fftpack/native" + +FFTPACK_PUBLIC_SOURCES=( + "$FFTPACK_NATIVE_DIR/rk.f90" + "$FFTPACK_NATIVE_DIR/fftpack.f90" + "$FFTPACK_NATIVE_DIR"/fftpack_*.f90 +) +FFTPACK_LINK_ONLY_SOURCES=() +for source in "$FFTPACK_NATIVE_DIR"/*.f90; do + case "${source##*/}" in + rk.f90|fftpack.f90|fftpack_*.f90) continue ;; + esac + FFTPACK_LINK_ONLY_SOURCES+=("$source") +done + +mkdir -p "$FFTPACK_BUILD_ROOT/prik/generated" +cd "$FFTPACK_BUILD_ROOT/prik" + +python3 -m prik "${FFTPACK_PUBLIC_SOURCES[@]}" \ + --native-fortran-sources "${FFTPACK_LINK_ONLY_SOURCES[@]}" \ + --out prik_reference_fftpack \ + --out-dir "$FFTPACK_BUILD_ROOT/prik/generated" \ + --compiler "$(command -v gfortran)" \ + --jobs 8 \ + --wrapper-fortran-flags="-O0 -g0" \ + --wrapper-c-flags="-O0 -g0" +``` + +The example uses `-O0` so the tests focus on correct results. Every source is +compiled once: positional files define the Python-facing API, while +`--native-fortran-sources` adds implementation code without exposing it to +Python. + +For normal use, source the convenience entrypoint: + +```bash +source examples/fftpack/build_all.sh +``` + +It builds the extension and exports its directory on `PYTHONPATH` for the +current shell. + +--- + +## 3. Understand how sources define the API + +The source groups have different roles: + +| Source group | Responsibility | +| --- | --- | +| `rk.f90` | Defines the real kind used by the public API. | +| `fftpack.f90` | Declares the public FFTPACK module. | +| `fftpack_*.f90` | Implements its procedures in Fortran submodules. | +| Remaining `.f90` files | Supply linked computational kernels. | + +The public declarations define the Python types. For example, `zfftf` accepts +an ordinary NumPy `complex128` array. + +High-level transform results that are allocatable in Fortran use PRIK's +`AllocatableArray` handle. Read the NumPy view with `to_numpy()` and release +the native allocation with `close()`: + +```python +import numpy as np +import prik_reference_fftpack + +fftpack = prik_reference_fftpack.fftpack +result = fftpack.fft(np.array([1.0, 0.0, 0.0, 0.0], dtype=np.complex128)) +try: + np.testing.assert_allclose(result.to_numpy(), np.ones(4)) +finally: + result.close() +``` + +Fixed-shape frequency and shift results are returned directly as NumPy arrays. + +--- + +## 4. Run the complete test suite + +After the build finishes, run: + +```bash +python3 -m pytest -q examples/fftpack/tests +``` + +The tests cover all 31 public procedures: + +| Family | Procedures | +| --- | ---: | +| Complex work-array transforms | 3 | +| Real work-array transforms | 6 | +| Cosine and sine work-array transforms | 7 | +| High-level Fourier transforms | 4 | +| High-level cosine transforms | 7 | +| Frequency and spectrum ordering | 4 | +| **Total** | **31** | + +Each procedure is called with representative data and checked against NumPy, +SciPy, or a known transform property. + +--- + +## 5. See how results are validated + +The suite compares transform results with independent NumPy or SciPy results +and also checks in-place mutation, dtype, shape, normalization, and frequency +ordering. For example, this `zfftf` test comes directly from the runnable +suite: + + +```python +def test_zfftf(fftpack): + values = np.array([1.0 + 2.0j, -2.0 + 1.0j, 4.0 - 3.0j, 3.0 + 0.5j, -1.0j], dtype=np.complex128) + expected = np.fft.fft(values) + wsave = np.empty(4 * values.size + 15, dtype=np.float64) + fftpack.zffti(np.int32(values.size), wsave) + + fftpack.zfftf(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) +``` + +The call uses the public complex-array signature, mutates the caller's array +in place, and compares the result with NumPy's independently implemented FFT. + +--- + +## 6. Run focused examples + +After building the extension, run a family or one procedure: + +```bash +python3 -m pytest -q examples/fftpack/tests/test_transforms.py +python3 -m pytest -q \ + examples/fftpack/tests/test_transforms.py::test_zfftf +python3 -m pytest -q examples/fftpack/tests -k fftshift +``` + +- Complete numerical examples → + [`test_transforms.py`](../../../examples/fftpack/tests/test_transforms.py) +- Public routine list → + [`routine_inventory.py`](../../../examples/fftpack/routine_inventory.py) +- Routine coverage check → + [`test_routine_coverage.py`](../../../examples/fftpack/tests/test_routine_coverage.py) +- Copyable project instructions → + [`examples/fftpack/README.md`](../../../examples/fftpack/README.md) + +--- + +## Troubleshooting + +- Confirm that `gfortran` is available on `PATH`. +- Use `source examples/fftpack/build_all.sh`; executing it in a child shell + does not preserve the exported `PYTHONPATH`. +- Run one failing procedure with `-vv -s` to retain its compiler and wrapper + diagnostics. + +--- + +## Source provenance + +The `.f90` files under +[`examples/fftpack/native/`](../../../examples/fftpack/native/) match the +upstream `src/` files at +[fortran-lang/fftpack commit `0fffe7c05a918363a7cc12ae138a695afd115f36`](https://github.com/fortran-lang/fftpack/tree/0fffe7c05a918363a7cc12ae138a695afd115f36). + +See the [upstream repository](https://github.com/fortran-lang/fftpack), its +[API documentation](https://fortran-lang.github.io/fftpack/), and its license +before redistributing the bundled native sources. diff --git a/docs/user/examples/index.md b/docs/user/examples/index.md index 8b50b249a..e01ec6400 100644 --- a/docs/user/examples/index.md +++ b/docs/user/examples/index.md @@ -9,16 +9,15 @@ publication: draft # Examples Gallery -The maintained part of this section includes the checked recipes and the -complete BLAS and LAPACK correctness projects. Use them when you need a -copy-paste command, a real-library project, or the current boundary between -inspection and runtime wrapper support. +This section includes checked recipes and four complete real-library examples: +BLAS, LAPACK, FFTPACK, and MINPACK. Each one provides build commands, Python +usage, and numerical checks for its public routines. The larger project examples below are placeholders for future complete runnable projects. Each one must include source, build command, import command, runtime check, limitations, and test evidence before it is marked maintained. -## Choose A Page +## Choose a page | Goal | Page | | --- | --- | @@ -35,7 +34,9 @@ PRIK_C_DOCS_END --> | Use inspection APIs from Python | [Use Python inspection APIs](recipes/use-python-inspection-apis.md) | | Pass compiler and preprocessing options | [Use compiler preprocessing options](recipes/compiler-preprocessing.md) | | Build and validate the complete Reference BLAS | [BLAS wrapper](blas-wrapper.md) | -| Build complete Reference LAPACK and validate the SciPy-backed float64 surface | [LAPACK wrapper](lapack-wrapper.md) | +| Build complete Reference LAPACK and validate 127 float64 routines | [LAPACK wrapper](lapack-wrapper.md) | +| Wrap and validate all 31 FFTPACK procedures with NumPy and SciPy | [FFTPACK wrapper](fftpack-wrapper.md) | +| Wrap all 22 MINPACK procedures and use Python callbacks | [MINPACK wrapper](minpack-wrapper.md) | ## Planned Project Examples diff --git a/docs/user/examples/lapack-wrapper.md b/docs/user/examples/lapack-wrapper.md index 76266a830..b41b5d245 100644 --- a/docs/user/examples/lapack-wrapper.md +++ b/docs/user/examples/lapack-wrapper.md @@ -9,38 +9,35 @@ publication: reviewed # Build and Validate LAPACK with PRIK -This example wraps the complete Reference LAPACK implementation corpus once with PRIK, then validates a reviewed, reproducible correctness surface. -The surface contains the 127 double-precision real routines exposed by `scipy.linalg.lapack` in SciPy 1.18.0 for `dtype=np.float64`. +This example builds the complete Reference LAPACK library and wraps it with +PRIK. It validates the 127 double-precision real routines also available +through `scipy.linalg.lapack` in SciPy 1.18.0. -### Why this example exists +### What this example shows -- PRIK wraps the **complete** LAPACK library (including its BLAS dependencies). - All source-level wrapper and compilation coverage stays intact. -- Raw f2py generates wrappers for the 125 selected routines it can expose - safely and links them to the same complete native artifact as PRIK. -- SciPy supplies the 127 reviewed low-level comparison functions. -- Independent residuals, reconstructions and invariants remain the primary correctness oracle. - -This separation keeps a large real library manageable without weakening the claim: -every LAPACK source compiles once for the complete PRIK wrapper, while every selected float64 routine has one visible, named correctness test. +- Build PRIK and f2py wrappers against the same compiled LAPACK library. +- Call linear-system, factorization, eigenvalue, and singular-value routines + with NumPy arrays. +- Compare results with SciPy and check solutions, residuals, reconstructions, + and other mathematical properties. You should already be comfortable with the BLAS wrapper example, NumPy arrays, and basic packaging. --- -## Versions and source boundary +## Versions used -| Component | Version / source | -|--------------------|-------------------------------------------------------| -| PRIK | current repository checkout (`0.1.0`) | -| Reference LAPACK | Netlib LAPACK 3.12.1 | -| Reference BLAS | BLAS snapshot shipped in LAPACK 3.12.1 | -| Python | 3.12 or newer | -| NumPy / f2py | NumPy 2.5.1 | -| SciPy | exactly 1.18.0 (reviewed inventory) | -| Meson | 1.11.2 | -| Ninja | 1.13.0 | -| Fortran compiler | compatible `gfortran` | +| Component | Version / source | +| --- | --- | +| PRIK | current repository checkout | +| Reference LAPACK | Netlib LAPACK 3.12.1 | +| Reference BLAS | BLAS snapshot shipped in LAPACK 3.12.1 | +| Python | 3.12 or newer | +| NumPy / f2py | NumPy 2.5.1 | +| SciPy | exactly 1.18.0 | +| Meson | 1.11.2 | +| Ninja | 1.13.0 | +| Fortran compiler | compatible `gfortran` | --- @@ -54,8 +51,8 @@ git clone https://github.com/PyNumLab/prik.git cd prik python3 -m venv .venv . .venv/bin/activate -python -m pip install --upgrade pip -python -m pip install -e ".[qa]" \ +python3 -m pip install --upgrade pip +python3 -m pip install -e ".[qa]" \ "numpy==2.5.1" "scipy==1.18.0" \ "meson==1.11.2" "ninja==1.13.0" ``` @@ -114,16 +111,11 @@ metadata needed by the generated wrapper. --- -## 3. Build the reviewed f2py comparison surface +## 3. Build the f2py comparison wrapper The committed [`lapack.pyf`](../../../examples/lapack/lapack.pyf) contains the -reviewed selected routines and `la_constants` module signature. f2py compiles -only its wrapper and links `LAPACK_SHARED_LIBRARY`. - -Nine routines document scalar writebacks without declaring Fortran `intent`. -Their reviewed `intent(inout)` declarations live directly in `lapack.pyf`, and -the tests pass typed 0-D arrays. PRIK needs neither: it returns unannotated -scalar writebacks directly, with ordinary scalar arguments. +125 selected routines and the `la_constants` module signature. f2py compiles +only this wrapper and links `LAPACK_SHARED_LIBRARY`. Run the same direct f2py command exercised by the test suite: @@ -156,12 +148,11 @@ python -m numpy.f2py -c \ compile each wrapper. Both wrappers link the existing shared library instead of recompiling LAPACK. -The committed `lapack.pyf` excludes `dgees` and `dgges` because f2py 2.5.1 -generates incomplete declarations for their selection callbacks. Their tests -exercise PRIK and SciPy, then independently verify the resulting Schur -decompositions. +The comparison excludes `dgees` and `dgges` because f2py 2.5.1 cannot generate +their callback declarations correctly. Those two routines are still checked +through PRIK, SciPy, and their Schur decompositions. -Import the two built modules and SciPy's comparison surface from the repository +Import the two built modules and SciPy's LAPACK module from the repository root: ```python @@ -176,111 +167,42 @@ import prik_reference_lapack_example from scipy.linalg import lapack as scipy_lapack ``` ---- - -## 4. Resolve the SciPy float64 comparison API +### SciPy comparison -SciPy determines eligibility, not the organisation of the test files. -The reviewed inventory was discovered with representative `np.float64` arrays and then frozen for SciPy 1.18.0. - -A typical low-level lookup looks like this: - -```python -import numpy as np -import scipy -from scipy.linalg import lapack - -assert scipy.__version__ == "1.18.0" -matrix = np.array([[3.0, 1.0], [1.0, 2.0]], dtype=np.float64, order="F") -dgesv = lapack.get_lapack_funcs("gesv", (matrix,)) -assert dgesv.typecode == "d" -``` - -The runtime suite does **not** silently select “whatever SciPy exports today”. -It fails clearly if the SciPy version or the expected routine inventory drifts. +The tests use the 127 double-precision real LAPACK routines available in SciPy +1.18.0 for `np.float64` arrays. Pinning that version keeps the comparison API +and expected results reproducible. --- -## 5. Run the correctness tests - -The arguments `prik_lapack`, `f2py_lapack`, and `scipy_lapack` are -session-scoped pytest fixtures from -[`conftest.py`](../../../examples/lapack/conftest.py). After the build scripts -finish, they provide the complete PRIK module, selected f2py comparison module, -and SciPy's pinned low-level LAPACK module to the tests under -[`examples/lapack/tests/`](../../../examples/lapack/tests/). - -The displayed tests use small helpers from -[`tests/helpers.py`](../../../examples/lapack/tests/helpers.py): - -| Helper | Exact responsibility | -| --- | --- | -| `column_major(a)` | Copies a matrix as `np.float64` in Fortran-contiguous column-major order. | -| `active(a, rows, columns)` | Selects the logical matrix and excludes leading-dimension padding. | -| `native_pivots(p)` | Converts SciPy's zero-based general-LU pivots to LAPACK's native one-based values. | -| `assert_allclose_float64(a, b)` | Uses a float64-epsilon tolerance scaled by operation length and expected magnitude. | -| `assert_small_residual(r, ...)` | Checks an infinity-norm backward residual scaled by matrix and solution norms. | -| `assert_storage_unchanged(a, b)` | Requires exact preservation, including NaN sentinels. | - -The numerical and preservation checks are intentionally small and visible: +## 4. Run the complete test suite -```python -import numpy as np -``` +Build both wrappers and run all 127 routine tests: - -```python -def assert_allclose_float64(actual, expected, *, operation_size: int = 1) -> None: - """Compare float64 LAPACK results with an accumulation-aware tolerance.""" - scale = max(1, operation_size) - expected_array = np.asarray(expected) - magnitude = max(1.0, float(np.max(np.abs(expected_array), initial=0.0))) - np.testing.assert_allclose( - actual, - expected, - rtol=np.finfo(np.float64).eps * 32 * scale, - atol=np.finfo(np.float64).eps * 32 * scale * magnitude, - ) -``` - - -```python -def assert_small_residual( - residual, - *, - matrix_norm: float, - solution_norm: float, - operation_size: int, -) -> None: - """Check a backward residual scaled by the represented operation.""" - denominator = max(1.0, matrix_norm * solution_norm) - scaled = np.linalg.norm(np.asarray(residual, dtype=np.float64), ord=np.inf) / denominator - tolerance = np.finfo(np.float64).eps * 128 * max(1, operation_size) - assert scaled <= tolerance, f"scaled residual {scaled} exceeded {tolerance}" -``` - - -```python -def assert_storage_unchanged(actual: np.ndarray, expected: np.ndarray) -> None: - """Compare storage exactly, including NaN sentinels.""" - np.testing.assert_array_equal(actual, expected) +```bash +source examples/lapack/build_all.sh +python3 -m pytest -q examples/lapack/tests ``` -These helpers do not call LAPACK and do not hide any wrapper invocation. The -routine call and the essential residual or reconstruction remain in each test. +The suite covers linear systems, least squares, factorizations, eigenvalue +problems, singular values, and related matrix operations. --- -## 6. Validate mathematical behaviour +## 5. See how results are validated -LAPACK outputs are not always unique. -Eigenvectors and singular vectors may change sign, repeated eigenspaces may use a different orthonormal basis, and pivot ties may choose another valid permutation. +LAPACK outputs are not always unique. Eigenvectors and singular vectors may +change sign, repeated eigenspaces may use a different orthonormal basis, and +pivot ties may choose another valid permutation. Therefore byte-for-byte agreement is not the only oracle. -Tests use explicit solutions, residuals, factor reconstructions, orthogonality, eigen equations and storage invariants. +Tests use explicit solutions, residuals, factor reconstructions, orthogonality, +eigen equations, and storage checks. Small NumPy helpers for matrix layout, +tolerances, and pivot conventions live in +[`tests/helpers.py`](../../../examples/lapack/tests/helpers.py). -The two real tests below keep all three wrapper calls and the independent oracle visible. -The displayed blocks are copied directly from their source functions. +The examples below show the PRIK, f2py, and SciPy calls together with a direct +mathematical check. They come from the runnable suite. ### DGESV – solve a general linear system @@ -321,8 +243,9 @@ def test_dgesv_solves_general_system(prik_lapack, scipy_lapack, f2py_lapack): ) ``` -This test checks the known solution, the independently scaled residual `A @ X - B`, the LU output, native one-based pivots versus SciPy’s convention, and `INFO == 0`. -PRIK preserves the native argument order and returns visible scalar arguments; both PRIK and the f2py comparison module mutate the native output arrays. +This test checks the known solution, the scaled residual `A @ X - B`, the LU +output, native one-based pivots versus SciPy's convention, and `INFO == 0`. +Both PRIK and the f2py comparison module update the output arrays in place. ### DPOTRF – reconstruct a Cholesky factorization @@ -353,36 +276,26 @@ def test_dpotrf_reconstructs_spd_matrix(prik_lapack, scipy_lapack, f2py_lapack): ``` The NaN in the unused upper triangle detects accidental access. -Correctness is established by reconstructing `A = L @ L.T`, not merely by comparing the factor’s bytes. +The reconstruction `A = L @ L.T` confirms that the factor is correct. --- -## 7. Run the maintained example - -Build both wrappers once, then run all 127 named tests: - -```bash -cd "$REPOSITORY_ROOT" -source examples/lapack/build_all.sh -python -m pytest -q examples/lapack/tests -``` +## 6. Run focused examples -Use a family or a single routine while diagnosing a failure: +After building the wrappers, run a family or one routine: ```bash -python -m pytest -q examples/lapack/tests/test_linear_general.py -python -m pytest -q \ +python3 -m pytest -q examples/lapack/tests/test_linear_general.py +python3 -m pytest -q \ examples/lapack/tests/test_linear_general.py::test_dgesv_solves_general_system -python -m pytest -q examples/lapack/tests -k dgesvd +python3 -m pytest -q examples/lapack/tests -k dgesvd ``` - Full DGESV and related general-system tests → [`test_linear_general.py`](../../../examples/lapack/tests/test_linear_general.py) - Cholesky and other positive-definite examples → [`test_linear_positive_definite.py`](../../../examples/lapack/tests/test_linear_positive_definite.py) - Other families live under [`examples/lapack/tests/`](../../../examples/lapack/tests/) -- Authoritative mapping of all 127 routines → [`routine_inventory.py`](../../../examples/lapack/routine_inventory.py) -- Coverage audit → [`test_routine_coverage.py`](../../../examples/lapack/tests/test_routine_coverage.py) - -The command is complete and reproducible with the listed native toolchain. +- Public routine list → [`routine_inventory.py`](../../../examples/lapack/routine_inventory.py) +- Routine coverage check → [`test_routine_coverage.py`](../../../examples/lapack/tests/test_routine_coverage.py) For the copyable build scripts, test commands, and source provenance, see the [`examples/lapack` project README](../../../examples/lapack/README.md). @@ -392,17 +305,19 @@ For the copyable build scripts, test commands, and source provenance, see the ## Troubleshooting - Confirm that `gfortran`, `ar`, `meson` and `ninja` are on `PATH`. -- Keep SciPy at **exactly 1.18.0** for this reviewed inventory. A different version is treated as inventory drift, not silently accepted. -- On Python 3.12 or newer, let f2py use Meson; do not force the removed distutils backend. +- Keep SciPy at **exactly 1.18.0** so its low-level comparison API matches this + example. +- On Python 3.12 or newer, let f2py use Meson; do not force the removed + distutils backend. - Rerun one named test with more detail and keep the build directory: ```bash - python -m pytest -vv -s --basetemp=/tmp/prik-lapack-debug \ + python3 -m pytest -vv -s --basetemp=/tmp/prik-lapack-debug \ examples/lapack/tests/test_linear_general.py::test_dgesv_solves_general_system ``` -- Compare residuals and reconstructions **before** comparing raw factor bytes; several valid LAPACK decompositions are not unique. -- This is a deterministic correctness suite. It collects no timings and makes no performance claims. +- Compare residuals and reconstructions before comparing raw factor bytes; + several valid LAPACK decompositions are not unique. --- diff --git a/docs/user/examples/minpack-wrapper.md b/docs/user/examples/minpack-wrapper.md new file mode 100644 index 000000000..7bf217ef1 --- /dev/null +++ b/docs/user/examples/minpack-wrapper.md @@ -0,0 +1,226 @@ +--- +title: Build and Validate MINPACK with PRIK +audience: users, advanced users +prerequisites: arrays, callbacks, packaging +related: fftpack-wrapper.md, ../guide/arrays.md, ../guide/callbacks.md +status: maintained +publication: reviewed +--- + +# Build and Validate MINPACK with PRIK + +This example takes the checked-in +[fortran-lang/minpack](https://github.com/fortran-lang/minpack) source and +builds an importable Python extension containing all 22 public MINPACK +procedures. + +The example solves known nonlinear and least-squares problems and compares the +results with SciPy and direct linear-algebra checks. + +### What this example shows + +- Wrap a complete numerical solver library as one Python extension. +- Pass NumPy arrays and ordinary Python functions to MINPACK routines. +- Check root-finding, least-squares, Jacobian, and factorization results. + +You should already be comfortable with NumPy arrays, Python callables, and +building a local Fortran extension. + +--- + +## Versions used + +| Component | Version / source | +| --- | --- | +| PRIK | current repository checkout | +| MINPACK | [fortran-lang/minpack commit `c0b5aea`](https://github.com/fortran-lang/minpack/tree/c0b5aea9fcd2b83865af921a7a7e881904f8d3c2) | +| Python | 3.12 in the dedicated CI job | +| NumPy | 2.5.1 | +| SciPy | 1.18.0 | +| Fortran compiler | GNU Fortran 13 in CI; a compatible `gfortran` works locally | + +The repository owns the checked-in source snapshot under +`examples/minpack/native/`, so the example does not download code during its +build. + +--- + +## 1. Prepare the repository and toolchain + +Clone PRIK, create a virtual environment, and install the Python tools used by +the dedicated CI job: + +```bash +git clone https://github.com/PyNumLab/prik.git +cd prik +python3 -m venv .venv +. .venv/bin/activate +python3 -m pip install --upgrade pip +python3 -m pip install -e ".[qa]" "numpy==2.5.1" "scipy==1.18.0" +``` + +Install GNU Fortran separately. On Ubuntu: + +```bash +sudo apt-get update +sudo apt-get install --yes gfortran +gfortran --version +``` + +All remaining commands run from the repository root with the virtual +environment active. The complete runnable project lives under +[`examples/minpack/`](../../../examples/minpack/). + +--- + +## 2. Build the PRIK wrapper + +MINPACK keeps its public declarations and implementations in one source file, +so one command can generate the wrapper and compile the library: + + +```bash +export EXAMPLE_WORKSPACE="$PWD" +export MINPACK_BUILD_ROOT="$(mktemp -d)" + +mkdir -p "$MINPACK_BUILD_ROOT/prik/generated" +cd "$MINPACK_BUILD_ROOT/prik" + +python3 -m prik "$EXAMPLE_WORKSPACE/examples/minpack/native/minpack.f90" \ + --out prik_reference_minpack \ + --out-dir "$MINPACK_BUILD_ROOT/prik/generated" \ + --compiler "$(command -v gfortran)" \ + --jobs 8 \ + --wrapper-fortran-flags="-O0 -g0" \ + --wrapper-c-flags="-O0 -g0" +``` + +The example uses `-O0` so the tests focus on correct results. PRIK compiles the +native source and generated bridge into one extension. + +For normal use, source the convenience entrypoint: + +```bash +source examples/minpack/build_all.sh +``` + +It builds the extension and exports its directory on `PYTHONPATH` for the +current shell. + +--- + +## 3. Use the generated Python API + +MINPACK routines keep their documented argument order, including work arrays +and status values. Pass NumPy arrays with the generated dtype, shape, and +layout. Solver callbacks are ordinary Python functions with the generated +callback signature. + +--- + +## 4. Run the complete test suite + +After the build finishes, run: + +```bash +python3 -m pytest -q examples/minpack/tests +``` + +The tests cover all 22 public procedures: + +| Family | Procedures | +| --- | ---: | +| Diagnostics and finite differences | 4 | +| Hybrid nonlinear solvers | 4 | +| Levenberg-Marquardt solvers | 6 | +| Factorization and update helpers | 8 | +| **Total** | **22** | + +Each procedure is called with representative data and checked against SciPy, a +known solution, or a direct linear-algebra result. + +--- + +## 5. See how results are validated + +For example, `hybrd1` can solve the two-variable equation +`x - [1, -2] = 0`. MINPACK calls the Python function whenever it needs the +current residual: + +```python +import numpy as np +from prik_reference_minpack import minpack_module as minpack + +target = np.array([1.0, -2.0], dtype=np.float64) + + +def residual(_count, x, fvec, _iflag): + fvec[:] = x - target + + +x = np.array([4.0, 4.0], dtype=np.float64) +fvec = np.empty(2, dtype=np.float64) +info = minpack.hybrd1( + residual, + np.int32(2), + x, + fvec, + np.float64(1.0e-12), + np.empty(19, dtype=np.float64), + np.int32(19), +) + +assert info == np.int32(1) +np.testing.assert_allclose(x, target, atol=1.0e-10) +np.testing.assert_allclose(fvec, 0.0, atol=1.0e-10) +``` + +The complete suite applies the same pattern to root-finding and least-squares +solvers, then compares their solutions with SciPy. + +--- + +## 6. Run focused examples + +After building the extension, run a family or one routine: + +```bash +python3 -m pytest -q examples/minpack/tests/test_solvers.py +python3 -m pytest -q \ + examples/minpack/tests/test_solvers.py::test_hybrd1 +``` + +- Callback-driven nonlinear solvers → + [`test_solvers.py`](../../../examples/minpack/tests/test_solvers.py) +- Diagnostics and finite-difference helpers → + [`test_diagnostics.py`](../../../examples/minpack/tests/test_diagnostics.py) +- Factorization and update helpers → + [`test_linear_algebra.py`](../../../examples/minpack/tests/test_linear_algebra.py) +- Public routine list → + [`routine_inventory.py`](../../../examples/minpack/routine_inventory.py) +- Routine coverage check → + [`test_routine_coverage.py`](../../../examples/minpack/tests/test_routine_coverage.py) +- Copyable project instructions → + [`examples/minpack/README.md`](../../../examples/minpack/README.md) + +--- + +## Troubleshooting + +- Confirm that `gfortran` is available on `PATH`. +- Use `source examples/minpack/build_all.sh`; executing it in a child shell + does not preserve the exported `PYTHONPATH`. +- Start with one helper or solver test and add `-vv -s` when diagnosing a + callback or generated-wrapper failure. + +--- + +## Source provenance + +[`examples/minpack/native/minpack.f90`](../../../examples/minpack/native/minpack.f90) +matches the upstream `src/minpack.f90` at +[fortran-lang/minpack commit `c0b5aea9fcd2b83865af921a7a7e881904f8d3c2`](https://github.com/fortran-lang/minpack/tree/c0b5aea9fcd2b83865af921a7a7e881904f8d3c2). + +See the [upstream repository](https://github.com/fortran-lang/minpack), its +[API documentation](https://fortran-lang.github.io/minpack/), and its license +before redistributing the bundled native source. diff --git a/docs/user/examples/recipes/build-and-import-python-api.md b/docs/user/examples/recipes/build-and-import-python-api.md index dd622ab02..986b80934 100644 --- a/docs/user/examples/recipes/build-and-import-python-api.md +++ b/docs/user/examples/recipes/build-and-import-python-api.md @@ -13,11 +13,11 @@ Use this recipe when a Python script needs to build a wrapper and load the generated extension directly. `build_fortran_extension` returns a result object with the module name, shared -library path, generated source paths, and other build artifacts. +library path, generated source paths, and other build artifacts. Call its +`import_module()` method when the script should load the built extension. ```python -from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path from tempfile import TemporaryDirectory @@ -28,9 +28,7 @@ from prik import build_fortran_extension source = Path("tests/fortran/building_shared_library/end_to_end/fixtures/native/fruntime_abi_f90.f90") with TemporaryDirectory() as output_dir: build = build_fortran_extension(source, output_dir=output_dir) - spec = spec_from_file_location(build.module_name, build.shared_library) - module = module_from_spec(spec) - spec.loader.exec_module(module) + module = build.import_module() native_module = module.fruntime_abi_f90 print(build.module_name) @@ -47,7 +45,11 @@ fruntime_abi_f90 ## Notes -- This pattern avoids editing `sys.path`. +- `import_module()` avoids editing `sys.path` and registers the extension under + `build.module_name` in the normal Python module cache. +- The shared-library file must exist. Direct builds can import immediately; + Makefile and source-only results can import after their extension has been + built. - `TemporaryDirectory` keeps documentation and tests from leaving build artifacts in the checkout. - Use the returned artifact paths when debugging generated code. diff --git a/docs/user/examples/recipes/compiler-preprocessing.md b/docs/user/examples/recipes/compiler-preprocessing.md index 34f3744ff..9d19e9bbf 100644 --- a/docs/user/examples/recipes/compiler-preprocessing.md +++ b/docs/user/examples/recipes/compiler-preprocessing.md @@ -2,7 +2,7 @@ title: Use Compiler Preprocessing Options audience: users, developers prerequisites: installation, native project compiler flags -related: ../../../developer/c-parser-reference.md, ../../../developer/fortran-parser-reference.md +related: ../../../developer/compiler-preprocessing.md, ../../../developer/c-parser-reference.md, ../../../developer/fortran-parser-reference.md status: maintained publication: draft --- @@ -46,3 +46,8 @@ PRIK_C_DOCS_END --> facts. - These examples are environment-dependent, so they are not marked as automatic documentation tests. + +## Next + +- Read the [compiler preprocessing reference](../../../developer/compiler-preprocessing.md) + for the pipeline model, adapters, diagnostics, and include-exposure policy. diff --git a/docs/user/faq/index.md b/docs/user/faq/index.md index 597422dbe..ca4b05ccb 100644 --- a/docs/user/faq/index.md +++ b/docs/user/faq/index.md @@ -1,20 +1,95 @@ --- -title: FAQ +title: Frequently Asked Questions +description: Concise answers about calling Fortran from Python, wrapping libraries, NumPy arrays, derived types, and choosing PRIK or f2py audience: users -prerequisites: getting started -related: ../troubleshooting/index.md, ../guide/index.md -status: planned-documentation -publication: draft +prerequisites: none +related: ../getting-started/index.md, ../guide/index.md, ../performance.md +status: maintained +publication: reviewed --- -# FAQ +# Frequently Asked Questions -Reserved page for common user questions, migration questions, and short answers -that link to full guides. +Start with the question closest to your task. Each answer links to the complete, +tested workflow. -## TODO +
+How do I call Fortran from Python? -- TODO: Add questions only when they reflect real user workflows or repeated - support issues. -- TODO: Link each answer to the owning guide, reference, or troubleshooting - page. +Use PRIK to build your Fortran source into an importable Python extension, then +call it with NumPy values that match the generated contract. Follow +[Call Your First Fortran Function from Python](../getting-started/first-wrapped-function.md) +for a complete source-to-result example. + +
+ +
+How do I generate Python bindings for a Fortran module? + +Pass the module source to PRIK. It generates the extension and exposes supported +public procedures and module state through Python. Start with +[Generate Python Bindings for a Fortran Module](../getting-started/first-wrapped-module.md). + +
+ +
+How do I wrap an existing Fortran library for Python? + +Build the public Fortran sources with PRIK and link their native dependencies +into the same extension. The +[shared-library guide](../guide/building-shared-library.md) explains the build +options, while the tested [BLAS](../examples/blas-wrapper.md), +[FFTPACK](../examples/fftpack-wrapper.md), and +[MINPACK](../examples/minpack-wrapper.md) examples show complete libraries. + +
+ +
+How do I expose Fortran derived types as Python classes? + +PRIK maps supported derived types to Python classes with constructors, methods, +fields, and explicit ownership rules. See +[Wrap Fortran Derived Types as Python Classes](../guide/wrapping-derived-types.md). + +
+ +
+How do I pass NumPy arrays to Fortran without unnecessary copies? + +Pass arrays with the dtype, rank, shape, layout, strides, and writeability +required by the generated contract. Compatible arrays can cross the wrapper +without a layout conversion; incompatible inputs are rejected instead of being +silently copied. See [Pass NumPy Arrays to Fortran](../guide/arrays.md). + +
+ +
+Should I use PRIK or f2py? + +Use [NumPy's f2py](https://numpy.org/doc/stable/f2py/) when its established +generated API—or an editable +[`.pyf` signature](https://numpy.org/doc/stable/f2py/signature-file.html)—is +enough for your project. + +Choose PRIK when you want to design the Python API, not just generate a wrapper. +Its editable [semantic `.pyi` contract](../reference/pyi-contracts/index.md) is +a simpler, more Pythonic place to rename or hide exports, flatten modules, +reorder or hide native arguments, and return native outputs as Python results. + +PRIK treats [NumPy arrays](../guide/arrays.md) as complete API contracts: dtype, +rank, shape, memory layout, contiguity, strides, mutation, and copy behavior are +all explicit. This includes +[supported positive-stride views](../guide/arrays.md#strided-views) without +copying. + +PRIK also covers important Fortran features: supported +[derived types](../guide/wrapping-derived-types.md) as Python classes, +[allocatables](../guide/allocatables.md), documented +[pointer forms](../guide/pointers.md), native errors as +[Python exceptions](../guide/error-handling.md), and +[overloaded procedures](../guide/generic-interfaces.md). PRIK is currently +alpha, so check the linked guides for exact limitations. The +[performance results](../performance.md) cover only their measured runtime and +clean-build workloads. + +
diff --git a/docs/user/getting-started/first-wrapped-function.md b/docs/user/getting-started/first-wrapped-function.md index 091e37105..6e45d12cf 100644 --- a/docs/user/getting-started/first-wrapped-function.md +++ b/docs/user/getting-started/first-wrapped-function.md @@ -39,9 +39,9 @@ python3 -m prik generate --pyi scale.f90 The generated semantic `.pyi` contains: ```python -from prik.contracts import Addr, Arg, Float64, external, native_call +from prik.contracts import Addr, Arg, Float64, native_call, standalone -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def scale( value: Float64, @@ -50,7 +50,7 @@ def scale( ``` `Float64` means the function requires `numpy.float64` scalar arguments and -returns the same scalar type. `@external` identifies a procedure outside a +returns the same scalar type. `@standalone` identifies a procedure outside a Fortran module. `@native_call(...)` maps the two Python arguments to the native call and passes each scalar by address. diff --git a/docs/user/guide/arrays.md b/docs/user/guide/arrays.md index 494e96e3d..8dbcd781b 100644 --- a/docs/user/guide/arrays.md +++ b/docs/user/guide/arrays.md @@ -173,8 +173,13 @@ print(vec) # [2. 4. 6. 8.] - Writeability for `intent(out)` or `intent(inout)` arrays - Declared stride pattern for stride-aware contracts -**prik does not silently cast, copy, transpose, or convert layouts.** -A mismatch raises `TypeError` before native code runs. +**prik does not silently cast, copy, transpose, or convert rejected caller +layouts.** A mismatch raises `TypeError` before native code runs. A generated +Boolean contract still accepts only `np.bool_` storage; when its numbered +`Bool8`-`Bool64` element type records a different native logical width, the +completed wrapper plan performs the required Boolean representation copy in +the Fortran bridge. That internal ABI adaptation is not a caller-side dtype or +layout coercion. Contiguous elements have no gaps between them in the required layout. Two arrays can print the same values but use different memory orders. @@ -456,6 +461,12 @@ Use this list when reading or editing a generated `.pyi` contract: leading axes flattened - `T[...]`: assumed-rank, currently rank 1-15 +Generated contracts may describe a shape with visible arguments, such as +`T[rows, columns]`. Most users should keep those generated relationships +unchanged. If you need to edit a complex shape or use a native function to +calculate an extent, see +[Advanced Array Shape Expressions](../reference/pyi-contracts/calls-and-results.md#advanced-array-shape-expressions). + --- ## Next diff --git a/docs/user/guide/callbacks.md b/docs/user/guide/callbacks.md index 6a41af33a..464c1eb34 100644 --- a/docs/user/guide/callbacks.md +++ b/docs/user/guide/callbacks.md @@ -14,8 +14,8 @@ Callbacks let wrapped Fortran call a Python function while an prik call is running. They are useful for objective functions, progress hooks, custom transforms, and small pieces of user-defined numerical logic. -Declare the callback shape once with `@prototype`, then use that prototype name -as the type of the procedure argument that accepts the callback. +Declare the callback shape once with `@prototype`, then use that +prototype name as the type of the procedure argument that accepts the callback. --- @@ -23,17 +23,18 @@ as the type of the procedure argument that accepts the callback. | Native callback argument | Prototype spelling | Python callable receives | | --- | --- | --- | -| Primitive scalar dummy declared with Fortran `value` | `value: Float64` | Independent `np.float64` scalar | -| Primitive scalar reference dummy | `value: Addr(Float64)` | Independent `np.float64` scalar | -| Array reference dummy | `values: Float64[n]` | NumPy array view | -| Fixed-length string reference dummy | `label: String[8]` | Writable rank-zero bytes storage | -| Derived-type reference dummy | `point: point_t` | Generated wrapper object | +| Primitive scalar `value`, `intent(in)` dummy | `value: In(Float64)` | Independent `np.float64` scalar | +| Primitive scalar reference input | `value: In(Addr(Float64))` | Independent `np.float64` scalar | +| Array input/output reference | `values: InOut(Float64[n])` | NumPy array view | +| Fixed-length string output reference | `label: Out(String[8])` | Writable rank-zero bytes storage | +| Derived-type input reference | `point: In(point_t)` | Generated wrapper object | !!! tip "Rule of thumb" - Bare primitive callback arguments are native values: - `value: Float64`, `count: Int32`, and so on. + Primitive callback arguments inside `In(...)`, `Out(...)`, or `InOut(...)` + are native values by default. - Use `Addr(T)` only when a primitive callback dummy is passed by reference. + Use `Addr(T)` inside the direction wrapper when a primitive dummy is passed + by reference. Arrays, strings, and derived-type callback arguments already use native storage or wrapper objects, so they do not need `Addr(...)` for ordinary reference @@ -48,24 +49,24 @@ Two declarations can appear around callbacks, and they control different calls: | Declaration | Controls | | --- | --- | -| `@prototype` | How Fortran calls the callback adapter. | +| `@prototype` | The exact interface through which Fortran calls the callback adapter. | | `@native_call(...)` | How Python arguments are passed into the outer wrapped function. | For example, the wrapped function may need `@native_call([Addr(Arg(1))])` because its `value` argument is passed to Fortran by reference: ```python -from prik.contracts import Addr, Arg, Float64, native_call, prototype +from prik.contracts import Addr, Arg, Float64, In, native_call, prototype @prototype -def scalar_callback(value: Addr(Float64)) -> Float64: ... +def scalar_callback(value: In(Addr(Float64))) -> Float64: ... @native_call([Arg(0), Addr(Arg(1))]) def apply(callback: scalar_callback, value: Float64) -> Float64: ... ``` -The two `Addr(...)` markers belong to different boundaries. The one inside -`@prototype` describes how Fortran calls the callback. The one inside +The two `Addr(...)` markers belong to different boundaries. The one inside the +prototype describes how Fortran calls the callback. The one inside `@native_call(...)` describes how Python calls the wrapped function. At runtime, pass an ordinary Python callable: @@ -132,32 +133,41 @@ print(result) # 7.5 ## Choosing The Prototype Spelling -Prototype declarations describe the **native callback signature**. They are not -Python runtime functions and they are not exported from the generated module. +Prototype declarations describe the **exact native callback interface**. They +are not Python runtime functions and they are not exported from the generated +module. prik lowers each signature to an abstract Fortran interface under a +generated `prik_` name, then declares the callback adapter with +`procedure(prik_...)`. For ordinary scalar and array callback arguments, use the same contract spellings you use elsewhere: ```python -from prik.contracts import Addr, Float64, Int32, prototype +from prik.contracts import Addr, Float64, In, InOut, Int32, prototype @prototype def update_values( - count: Addr(Int32), - scale: Float64, - values: Float64[count] + count: In(Addr(Int32)), + scale: In(Float64), + values: InOut(Float64[count]) ) -> None: ... ``` Here `count` is a primitive reference dummy, while `scale` is a primitive value dummy. Python receives both as NumPy scalar values. +Direction also controls the adapter copy. `In(...)` supplies a read-only array +view or an immutable string value, `Out(...)` exposes writable storage without +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. + For scalar arguments, choose the spelling from the Fortran callback dummy: | Fortran callback dummy | Matching prototype | | --- | --- | -| `real(8), intent(in) :: value` | `value: Addr(Float64)` | -| `real(8), value :: value` | `value: Float64` | +| `real(8), intent(in) :: value` | `value: In(Addr(Float64))` | +| `real(8), value, intent(in) :: value` | `value: In(Float64)` | Both forms call Python with an independent `np.float64` scalar. The difference is the native calling convention prik must match. @@ -196,6 +206,10 @@ The current callback contract does not support: for the no-callback path, or require the callback argument. - Optional arguments inside a `@prototype`. Pass an explicit value, sentinel, or presence flag instead. +- Pure callback prototypes. A Python callback adapter calls the Python runtime, + so it cannot satisfy a pure Fortran procedure contract. In particular, one + pure prototype cannot be used both as a callback annotation and as a called + specification function in an array extent. - Allocatable, pointer, polymorphic, or assumed-type callback arguments and results. Use plain scalars, fixed-shape primitive arrays, fixed-length strings, or supported scalar derived types. diff --git a/docs/user/guide/data-types.md b/docs/user/guide/data-types.md index ff10ea20d..c28ff55da 100644 --- a/docs/user/guide/data-types.md +++ b/docs/user/guide/data-types.md @@ -93,10 +93,24 @@ print(bool(invert(True))) # False | `real(8)` / `real64` | `Float64` | `np.float64` | | `complex(4)` | `Complex64` | `np.complex64` | | `complex(8)` | `Complex128` | `np.complex128` | -| `logical` | `Bool` | `bool` or `np.bool_` | +| `logical` | `Bool8`-`Bool64` | `bool` or `np.bool_` | | `character` | `String` / `String[n]` | `str` or fixed `np.bytes_` | | Derived Type | Generated Class | Instance of that class | +Boolean contract names describe native storage, not different Python dtypes: + +| Semantic Contract | Native Logical Storage Represented | Scalar Input / Result | Array Input / Output Storage | +|-------------------|------------------------------------|-----------------------|------------------------------| +| `Bool` | 8 bits; portable default, equivalent to `Bool8` | `bool` or `np.bool_` | NumPy array with `dtype=np.bool_` | +| `Bool8` | 8 bits | `bool` or `np.bool_` | NumPy array with `dtype=np.bool_` | +| `Bool16` | 16 bits | `bool` or `np.bool_` | NumPy array with `dtype=np.bool_` | +| `Bool32` | 32 bits | `bool` or `np.bool_` | NumPy array with `dtype=np.bool_` | +| `Bool64` | 64 bits | `bool` or `np.bool_` | NumPy array with `dtype=np.bool_` | + +Generated contracts select a numbered name after probing the chosen compiler. +Callers never pass integer arrays for wider logical storage: the wrapper adapts +the one-byte NumPy Boolean representation at the native boundary. + --- ## Runtime Default Constructors diff --git a/docs/user/guide/wrapping-functions.md b/docs/user/guide/wrapping-functions.md index 6b2862d56..f03c26d05 100644 --- a/docs/user/guide/wrapping-functions.md +++ b/docs/user/guide/wrapping-functions.md @@ -43,10 +43,10 @@ For example, rename the generated declaration to `multiply` and add `scale`: ```python -from prik.contracts import Addr, Arg, Float64, bind, external, native_call +from prik.contracts import Addr, Arg, Float64, bind, native_call, standalone @bind("scale") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def multiply( value: Float64, diff --git a/docs/user/language-support/feature-matrix.md b/docs/user/language-support/feature-matrix.md index 51c97870f..7dbb8a03a 100644 --- a/docs/user/language-support/feature-matrix.md +++ b/docs/user/language-support/feature-matrix.md @@ -51,8 +51,8 @@ inspection-only or partial support. | Scalar kind coverage | Supported | [Data types](../guide/data-types.md) | [Fortran type probe](../../developer/source-map.md#hotspot-index) | [Scalar kind tests](../../../tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py) | Wider real, complex, and explicit logical storage is blocked without portable NumPy mapping. | | Caller-ordered multi-source builds, Makefiles, verbose mode, and output placement | Supported | [Building the shared library](../guide/building-shared-library.md) | [Wrapper orchestration](../../developer/source-map.md#common-change-routes) | [Multi-source tests](../../../tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py), [compiler verbose tests](../../../tests/fortran/building_shared_library/compiling/test_compiler_verbose.py) | prik does not discover, reorder, or resolve all external source dependencies. | | Visibility, naming, keyword escaping, and collision policy | Supported | [Visibility and naming](../reference/fortran-wrapper.md#visibility-naming-and-the-python-surface) | [Naming policy](../../developer/source-map.md#hotspot-index) | [Visibility/naming tests](../../../tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_naming.py) | Strict mode rejects names that default mode can normalize. | -| Immediate call-scoped Python callbacks | Supported | [Callbacks](../guide/callbacks.md) | [Callback bridge route](../../developer/source-map.md#common-change-routes) | [Callback plan tests](../../../tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py), [scalar callback tests](../../../tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py), [array callback tests](../../../tests/fortran/callbacks/end_to_end/test_array_callbacks.py), [combined shape tests](../../../tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py) | Direct wrapper-plan generation supports entering-thread callbacks only. Stored, optional, asynchronous, or cross-thread callbacks are unsupported. | -| Runtime error projection, GIL policy, recursion, OpenMP path, and GNU ABI checks | Supported | [Error handling](../guide/error-handling.md) | [Runtime route](../../developer/source-map.md#common-change-routes) | [Status projection runtime](../../../tests/fortran/error_handling/end_to_end/test_status_projection.py), [status and GIL lowering](../../../tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py), [recursion tests](../../../tests/fortran/error_handling/end_to_end/test_runtime_recursion.py), [OpenMP tests](../../../tests/fortran/error_handling/end_to_end/test_openmp_runtime.py), [ABI tests](../../../tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py) | OpenMP and ABI evidence is compiler/platform-specific; callers still own native synchronization. | +| Immediate call-scoped Python callbacks | Supported | [Callbacks](../guide/callbacks.md) | [Callback bridge route](../../developer/source-map.md#common-change-routes) | [Callback plan tests](../../../tests/fortran/callbacks/codegen/test_callback_planning.py), [scalar callback tests](../../../tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py), [array callback tests](../../../tests/fortran/callbacks/end_to_end/test_array_callbacks.py), [combined shape tests](../../../tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py) | Direct wrapper-plan generation supports entering-thread callbacks only. Stored, optional, asynchronous, or cross-thread callbacks are unsupported. | +| Runtime error projection, GIL policy, recursion, OpenMP path, and GNU ABI checks | Supported | [Error handling](../guide/error-handling.md) | [Runtime route](../../developer/source-map.md#common-change-routes) | [Status projection runtime](../../../tests/fortran/error_handling/end_to_end/test_status_projection.py), [status and GIL lowering](../../../tests/fortran/error_handling/codegen/test_status_error_lowering.py), [recursion tests](../../../tests/fortran/error_handling/end_to_end/test_runtime_recursion.py), [OpenMP tests](../../../tests/fortran/error_handling/end_to_end/test_openmp_runtime.py), [ABI tests](../../../tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py) | OpenMP and ABI evidence is compiler/platform-specific; callers still own native synchronization. | | Fortran source wrapper builds | Supported | [Building the shared library](../guide/building-shared-library.md) | [Wrapper orchestration](../../developer/source-map.md#common-change-routes) | [Build modes](../../../tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py), [runtime ABI](../../../tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py) | Implemented for ordered Fortran source inputs. | | Persistent callbacks and procedure pointers | Unsupported | [Callback limitations](../guide/callbacks.md#important-limitations) | [Callback route](../../developer/source-map.md#common-change-routes) | [Callback policy tests](../../../tests/fortran/callbacks/policy/test_callback_policy.py), [scalar callback tests](../../../tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py) | Callbacks are valid only during the wrapped call. | | Advanced multi-source dependency discovery and external-library integration | Unsupported | [Multiple source files](../guide/building-shared-library.md#multiple-source-files) | [Build orchestration](../../developer/source-map.md#common-change-routes) | [Multi-source tests](../../../tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py) | prik does not infer dependency graphs, prebuilt module paths, or external library discovery. | | Blocked array forms | Unsupported | [Unsupported array forms](../guide/arrays.md#unsupported-forms) | [Array policy route](../../developer/source-map.md#common-change-routes) | [Array semantic tests](../../../tests/fortran/arrays/semantics/test_array_semantics.py), [diagnostics](../reference/diagnostic-codes.md) | Assumed type `type(*)`, arrays of derived types, and character arrays not representable as fixed-width bytes need missing runtime contracts. | -| Unsupported polymorphic forms | Unsupported | [Inheritance limits](../reference/fortran-wrapper.md#inheritance-and-polymorphism) | [Class policy route](../../developer/source-map.md#common-change-routes) | [Inheritance tests](../../../tests/fortran/derived_types/wrapper_codegen/test_class_surfaces.py) | Results, mutable dummies, arrays, polymorphic allocatable/pointer scalars, and `class(*)` are blocked. | +| Unsupported polymorphic forms | Unsupported | [Inheritance limits](../reference/fortran-wrapper.md#inheritance-and-polymorphism) | [Class policy route](../../developer/source-map.md#common-change-routes) | [Inheritance tests](../../../tests/fortran/derived_types/codegen/test_class_surfaces.py) | Results, mutable dummies, arrays, polymorphic allocatable/pointer scalars, and `class(*)` are blocked. | | Ambiguous or incomplete constructor overload sets | Unsupported | [Constructor limitations](../reference/fortran-wrapper.md#constructors-initialization-and-finalizers) | [Constructor route](../../developer/source-map.md#common-change-routes) | [Constructor semantic tests](../../../tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py), [class-plan validation tests](../../../tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py) | Candidates must have distinguishable exact runtime signatures and compatible native-owner lifecycles. | | Character arrays and mutable deferred-length character storage | Partially supported | [Strings](../guide/strings.md) | [Character bridge route](../../developer/source-map.md#common-change-routes) | [Character edge tests](../../../tests/fortran/strings/end_to_end/test_character_edge_cases.py) | Character arrays use fixed-width NumPy bytes dtype. Fixed and allocatable deferred element length maps to dtype itemsize; Unicode/object arrays and mutable scalar deferred-length storage are unsupported. | | Wider-than-supported real, complex, and logical storage | Unsupported | [Datatype limits](../guide/data-types.md#unsupported-widths-and-forms) | [Type probing](../../developer/source-map.md#hotspot-index) | [Scalar kind tests](../../../tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py) | prik blocks rather than silently losing precision or Boolean storage semantics. | diff --git a/docs/user/performance.md b/docs/user/performance.md index 4c96a0cbe..b5c7daf4b 100644 --- a/docs/user/performance.md +++ b/docs/user/performance.md @@ -96,12 +96,48 @@ Each value is the mean of 6 clean builds after 1 untimed warm-up. | Optimized (`-O3 -march=native -mtune=native`) · full reference BLAS (155 sources) | **22.3 sec** | 33.1 sec | f2py 1.48× faster | +## Should I use PRIK or f2py? + +These benchmarks answer two narrow questions: runtime-call overhead and clean +build time for the same Fortran sources on the same machine. They do not rank +feature coverage, API design, ecosystem maturity, or suitability for every +project. + +Use [NumPy's f2py](https://numpy.org/doc/stable/f2py/) when its established +generated API—or an editable +[`.pyf` signature](https://numpy.org/doc/stable/f2py/signature-file.html)—is +enough for your project. + +Choose PRIK when you want to design the Python API, not just generate a wrapper. +Its editable [semantic `.pyi` contract](reference/pyi-contracts/index.md) is a +simpler, more Pythonic place to rename or hide exports, flatten modules, reorder +or hide native arguments, and return native outputs as Python results. + +PRIK treats [NumPy arrays](guide/arrays.md) as complete API contracts: dtype, +rank, shape, memory layout, contiguity, strides, mutation, and copy behavior are +all explicit. This includes +[supported positive-stride views](guide/arrays.md#strided-views) without copying. + +PRIK also covers important Fortran features: supported +[derived types](guide/wrapping-derived-types.md) as Python classes, +[allocatables](guide/allocatables.md), documented +[pointer forms](guide/pointers.md), native errors as +[Python exceptions](guide/error-handling.md), and +[overloaded procedures](guide/generic-interfaces.md). PRIK is currently alpha, +so check the linked guides for exact limitations. + ## Fair, Like-for-Like Setup The suite wraps one set of Fortran kernels with the default PRIK and f2py interfaces. It checks both extensions for the same results before measuring them. No benchmark-only wrapper mode is used. +Each runtime group uses an A/B/B/A sequence with equal PRIK-first and f2py-first +process budgets. The two passes are merged before significance, winner counts +and geometric means are calculated, preventing either tool from consistently +benefiting from being measured second. Clean-build rounds alternate tool order +independently. + - Runtime native and generated sources use `-O3 -march=native -mtune=native`. - Clean builds use development (`-O0`) and optimized diff --git a/docs/user/reference/cli-commands.md b/docs/user/reference/cli-commands.md index 785b151d8..484637327 100644 --- a/docs/user/reference/cli-commands.md +++ b/docs/user/reference/cli-commands.md @@ -87,9 +87,9 @@ not mislabel them as preprocessing-only options. It also keeps short examples for a basic source build, an explicitly named extension, and semantic contract generation; `--help-build` labels its basic build, semantic-contract build, and manifest-replay examples separately. Both help levels reuse the canonical -`points.f90` source and naming from the -[homepage example](../../index.md#see-it-in-action), which contains the -complete source, basic build, import flow, and expected result. +`points.f90` and `geometry` naming from the +[derived-type guide](../guide/wrapping-derived-types.md#complete-example), +which contains a complete source, build, import flow, and expected result. The full build help uses the following two forms: @@ -191,7 +191,7 @@ python3 -m prik generate --makefile points.f90 --out-dir build ``` These examples reuse `points.f90` from the -[homepage example](../../index.md#see-it-in-action). +[derived-type guide](../guide/wrapping-derived-types.md#complete-example). These modes are mutually exclusive. Source and Makefile generation still run the preprocessing and semantic-policy stages needed to produce a valid wrapper diff --git a/docs/user/reference/fortran-wrapper.md b/docs/user/reference/fortran-wrapper.md index 48a1e1cbe..c3813d890 100644 --- a/docs/user/reference/fortran-wrapper.md +++ b/docs/user/reference/fortran-wrapper.md @@ -193,7 +193,7 @@ extension root. For example, `solver.f90` containing module `kernels` exposes one child per contained module and compile sources in caller-supplied order. When a folder contains only standalone BLAS/LAPACK-style procedures, `--pyi --out contracts` can generate one compact entry `.pyi` containing all -`@external` declarations while the native sources still compile and link as +`@standalone` declarations while the native sources still compile and link as separate artifacts. | Family | Names | | --- | --- | -| Booleans and generic values | `Bool`, `Any` | +| Booleans and generic values | `Bool`, `Bool8`, `Bool16`, `Bool32`, `Bool64`, `Any` | | Signed integers | `Int`, `Int8`, `Int16`, `Int32`, `Int64` | | Unsigned integers | `UInt8`, `UInt16`, `UInt32`, `UInt64`, `SizeT` | | Reals | `Float32`, `Float64`, `Float128` | | Complex | `Complex64`, `Complex128`, `Complex256` | | Text | `String` | | User types | class names and imported type names | -| Named callable prototypes | `@prototype` function declarations referenced by name | -| Prototype primitive reference | `Addr(T)` inside a `@prototype` declaration | -| Prototype non-primitive value override | `Value(T)` inside a `@prototype` declaration | +| Exact native procedure signatures | `@prototype` function declarations | +| Prototype uses | callback annotations or direct calls in declaration expressions | +| Procedure characteristics | `@pure`, and `In(T)`, `Out(T)`, or `InOut(T)` dummy direction | +| Prototype primitive reference | `Addr(T)` inside an intent wrapper | +| Prototype non-primitive value override | `Value(T)` inside an intent wrapper | + +All Boolean names accept and return Python/NumPy Boolean values. `Bool` is the +portable one-byte boundary contract and is equivalent to `Bool8` at that +boundary. A numbered name additionally records native Boolean storage bits so +language-specific lowering can preserve the native declaration without +exposing a language spelling such as a Fortran kind in the Python API. +```bash +export EXAMPLE_WORKSPACE="$PWD" +export FFTPACK_BUILD_ROOT="$(mktemp -d)" +export FFTPACK_NATIVE_DIR="$EXAMPLE_WORKSPACE/examples/fftpack/native" + +FFTPACK_PUBLIC_SOURCES=( + "$FFTPACK_NATIVE_DIR/rk.f90" + "$FFTPACK_NATIVE_DIR/fftpack.f90" + "$FFTPACK_NATIVE_DIR"/fftpack_*.f90 +) +FFTPACK_LINK_ONLY_SOURCES=() +for source in "$FFTPACK_NATIVE_DIR"/*.f90; do + case "${source##*/}" in + rk.f90|fftpack.f90|fftpack_*.f90) continue ;; + esac + FFTPACK_LINK_ONLY_SOURCES+=("$source") +done + +mkdir -p "$FFTPACK_BUILD_ROOT/prik/generated" +cd "$FFTPACK_BUILD_ROOT/prik" + +python3 -m prik "${FFTPACK_PUBLIC_SOURCES[@]}" \ + --native-fortran-sources "${FFTPACK_LINK_ONLY_SOURCES[@]}" \ + --out prik_reference_fftpack \ + --out-dir "$FFTPACK_BUILD_ROOT/prik/generated" \ + --compiler "$(command -v gfortran)" \ + --jobs 8 \ + --wrapper-fortran-flags="-O0 -g0" \ + --wrapper-c-flags="-O0 -g0" +``` + +## Run focused tests + +After the quick-start build, run one procedure or family: + +```bash +python3 -m pytest -q examples/fftpack/tests/test_transforms.py +python3 -m pytest -q \ + examples/fftpack/tests/test_transforms.py::test_zfftf +python3 -m pytest -q examples/fftpack/tests -k dct +``` + +## What is validated + +The suite covers all 31 public work-array, FFT, DCT, frequency, and shift +procedures. NumPy is the oracle for Fourier transforms, shifts, and frequency +ordering; SciPy is the oracle for cosine and sine families. The tests verify +normalization, in-place mutation, high-level input preservation, dtype, shape, +and allocatable-result cleanup. + +The public routine list stays in sync with the generated exports, and every +public procedure is exercised. + +## Sources and license + +The `.f90` files under [`native/`](native/) match the upstream `src/` files at +[fortran-lang/fftpack commit `0fffe7c05a918363a7cc12ae138a695afd115f36`](https://github.com/fortran-lang/fftpack/tree/0fffe7c05a918363a7cc12ae138a695afd115f36). +See the upstream repository, API documentation, and license before +redistributing the bundled native sources. diff --git a/examples/fftpack/__init__.py b/examples/fftpack/__init__.py new file mode 100644 index 000000000..2b5e19b60 --- /dev/null +++ b/examples/fftpack/__init__.py @@ -0,0 +1 @@ +"""Reference FFTPACK build and numerical-validation example.""" diff --git a/examples/fftpack/build_all.sh b/examples/fftpack/build_all.sh new file mode 100644 index 000000000..9002d284c --- /dev/null +++ b/examples/fftpack/build_all.sh @@ -0,0 +1,3 @@ +source examples/fftpack/build_prik.sh +cd "$EXAMPLE_WORKSPACE" +export PYTHONPATH="$FFTPACK_BUILD_ROOT/prik${PYTHONPATH:+:$PYTHONPATH}" diff --git a/examples/fftpack/build_prik.sh b/examples/fftpack/build_prik.sh new file mode 100644 index 000000000..b2c73669f --- /dev/null +++ b/examples/fftpack/build_prik.sh @@ -0,0 +1,28 @@ +export EXAMPLE_WORKSPACE="$PWD" +export FFTPACK_BUILD_ROOT="$(mktemp -d)" +export FFTPACK_NATIVE_DIR="$EXAMPLE_WORKSPACE/examples/fftpack/native" + +FFTPACK_PUBLIC_SOURCES=( + "$FFTPACK_NATIVE_DIR/rk.f90" + "$FFTPACK_NATIVE_DIR/fftpack.f90" + "$FFTPACK_NATIVE_DIR"/fftpack_*.f90 +) +FFTPACK_LINK_ONLY_SOURCES=() +for source in "$FFTPACK_NATIVE_DIR"/*.f90; do + case "${source##*/}" in + rk.f90|fftpack.f90|fftpack_*.f90) continue ;; + esac + FFTPACK_LINK_ONLY_SOURCES+=("$source") +done + +mkdir -p "$FFTPACK_BUILD_ROOT/prik/generated" +cd "$FFTPACK_BUILD_ROOT/prik" + +python3 -m prik "${FFTPACK_PUBLIC_SOURCES[@]}" \ + --native-fortran-sources "${FFTPACK_LINK_ONLY_SOURCES[@]}" \ + --out prik_reference_fftpack \ + --out-dir "$FFTPACK_BUILD_ROOT/prik/generated" \ + --compiler "$(command -v gfortran)" \ + --jobs 8 \ + --wrapper-fortran-flags="-O0 -g0" \ + --wrapper-c-flags="-O0 -g0" diff --git a/examples/fftpack/conftest.py b/examples/fftpack/conftest.py new file mode 100644 index 000000000..f64511ffc --- /dev/null +++ b/examples/fftpack/conftest.py @@ -0,0 +1,11 @@ +"""Import the FFTPACK extension built by ``build_all.sh``.""" + +import importlib + +import pytest + + +@pytest.fixture(scope="session") +def fftpack(): + """Return the public high-level FFTPACK module namespace.""" + return importlib.import_module("prik_reference_fftpack").fftpack diff --git a/examples/fftpack/native/CMakeLists.txt b/examples/fftpack/native/CMakeLists.txt new file mode 100644 index 000000000..cd88c3c60 --- /dev/null +++ b/examples/fftpack/native/CMakeLists.txt @@ -0,0 +1,210 @@ +#### Pre-process: .fpp -> .f90 via Fypp + +# Create a list of the files to be preprocessed +# set(fppFiles +# stdlib_ascii.fypp +# stdlib_bitsets.fypp +# stdlib_bitsets_64.fypp +# stdlib_bitsets_large.fypp +# stdlib_codata_type.fypp +# stdlib_constants.fypp +# stdlib_error.fypp +# stdlib_hash_32bit.fypp +# stdlib_hash_32bit_fnv.fypp +# stdlib_hash_32bit_nm.fypp +# stdlib_hash_32bit_water.fypp +# stdlib_hash_64bit.fypp +# stdlib_hash_64bit_fnv.fypp +# stdlib_hash_64bit_pengy.fypp +# stdlib_hash_64bit_spookyv2.fypp +# stdlib_intrinsics_dot_product.fypp +# stdlib_intrinsics_sum.fypp +# stdlib_intrinsics.fypp +# stdlib_io.fypp +# stdlib_io_npy.fypp +# stdlib_io_npy_load.fypp +# stdlib_io_npy_save.fypp +# stdlib_kinds.fypp +# stdlib_linalg.fypp +# stdlib_linalg_diag.fypp +# stdlib_linalg_least_squares.fypp +# stdlib_linalg_outer_product.fypp +# stdlib_linalg_kronecker.fypp +# stdlib_linalg_cross_product.fypp +# stdlib_linalg_eigenvalues.fypp +# stdlib_linalg_solve.fypp +# stdlib_linalg_determinant.fypp +# stdlib_linalg_qr.fypp +# stdlib_linalg_inverse.fypp +# stdlib_linalg_pinv.fypp +# stdlib_linalg_norms.fypp +# stdlib_linalg_state.fypp +# stdlib_linalg_svd.fypp +# stdlib_linalg_cholesky.fypp +# stdlib_linalg_schur.fypp +# stdlib_optval.fypp +# stdlib_selection.fypp +# stdlib_sorting.fypp +# stdlib_sorting_ord_sort.fypp +# stdlib_sorting_sort.fypp +# stdlib_sorting_sort_index.fypp +# stdlib_sparse_constants.fypp +# stdlib_sparse_conversion.fypp +# stdlib_sparse_kinds.fypp +# stdlib_sparse_spmv.fypp +# stdlib_specialfunctions_activations.fypp +# stdlib_specialfunctions_gamma.fypp +# stdlib_specialfunctions.fypp +# stdlib_specialmatrices.fypp +# stdlib_specialmatrices_tridiagonal.fypp +# stdlib_stats.fypp +# stdlib_stats_corr.fypp +# stdlib_stats_cov.fypp +# stdlib_stats_mean.fypp +# stdlib_stats_median.fypp +# stdlib_stats_moment.fypp +# stdlib_stats_moment_all.fypp +# stdlib_stats_moment_mask.fypp +# stdlib_stats_moment_scalar.fypp +# stdlib_stats_distribution_uniform.fypp +# stdlib_stats_distribution_normal.fypp +# stdlib_stats_distribution_exponential.fypp +# stdlib_stats_var.fypp +# stdlib_quadrature.fypp +# stdlib_quadrature_trapz.fypp +# stdlib_quadrature_simps.fypp +# stdlib_random.fypp +# stdlib_math.fypp +# stdlib_math_linspace.fypp +# stdlib_math_logspace.fypp +# stdlib_math_arange.fypp +# stdlib_math_is_close.fypp +# stdlib_math_all_close.fypp +# stdlib_math_diff.fypp +# stdlib_math_meshgrid.fypp +# stdlib_str2num.fypp +# stdlib_string_type.fypp +# stdlib_string_type_constructor.fypp +# stdlib_strings_to_string.fypp +# stdlib_strings.fypp +# stdlib_version.fypp +# ) + +# Preprocessed files to contain preprocessor directives -> .F90 +# set(cppFiles +# stdlib_linalg_constants.fypp +# stdlib_linalg_blas.fypp +# stdlib_linalg_lapack.fypp +# ) + +# fypp_f90("${fyppFlags}" "${fppFiles}" outFiles) +# fypp_f90pp("${fyppFlags}" "${cppFiles}" outPreprocFiles) + +set(SRC + cfftb1.f90 + cfftf1.f90 + cffti1.f90 + cosqb1.f90 + cosqf1.f90 + dcosqb.f90 + dcosqf.f90 + dcosqi.f90 + dcost.f90 + dcosti.f90 + dfftb.f90 + dfftf.f90 + dffti.f90 + dsinqb.f90 + dsinqf.f90 + dsinqi.f90 + dsint.f90 + dsinti.f90 + dzfftb.f90 + dzfftf.f90 + dzffti.f90 + ezfft1.f90 + fftpack_dct.f90 + fftpack.f90 + fftpack_fft.f90 + fftpack_fftshift.f90 + fftpack_ifft.f90 + fftpack_ifftshift.f90 + fftpack_irfft.f90 + fftpack_rfft.f90 + fftpack_utils.f90 + passb2.f90 + passb3.f90 + passb4.f90 + passb5.f90 + passb.f90 + passf2.f90 + passf3.f90 + passf4.f90 + passf5.f90 + passf.f90 + radb2.f90 + radb3.f90 + radb4.f90 + radb5.f90 + radbg.f90 + radf2.f90 + radf3.f90 + radf4.f90 + radf5.f90 + radfg.f90 + rfftb1.f90 + rfftf1.f90 + rffti1.f90 + rk.f90 + sint1.f90 + zfftb.f90 + zfftf.f90 + zffti.f90 + ${outFiles} + ${outPreprocFiles} +) + +add_library(${PROJECT_NAME} ${SRC}) + +# # Link to BLAS and LAPACK +# if(BLAS_FOUND AND LAPACK_FOUND) +# target_link_libraries(${PROJECT_NAME} "BLAS::BLAS") +# target_link_libraries(${PROJECT_NAME} "LAPACK::LAPACK") +# endif() + +set_target_properties( + ${PROJECT_NAME} + PROPERTIES + POSITION_INDEPENDENT_CODE ON + WINDOWS_EXPORT_ALL_SYMBOLS ON +) + +if(CMAKE_Fortran_COMPILER_ID STREQUAL GNU AND CMAKE_Fortran_COMPILER_VERSION VERSION_LESS 10.0) + target_compile_options( + ${PROJECT_NAME} + PRIVATE + $<$:-fno-range-check> + ) +endif() + +set(LIB_MOD_DIR ${CMAKE_CURRENT_BINARY_DIR}/mod_files/) +# We need the module directory before we finish the configure stage since the +# build interface might resolve before the module directory is generated by CMake +if(NOT EXISTS "${LIB_MOD_DIR}") + file(MAKE_DIRECTORY "${LIB_MOD_DIR}") +endif() + +set_target_properties(${PROJECT_NAME} PROPERTIES + Fortran_MODULE_DIRECTORY ${LIB_MOD_DIR}) +target_include_directories(${PROJECT_NAME} PUBLIC + $ + $ +) + +install(TARGETS ${PROJECT_NAME} + EXPORT ${PROJECT_NAME}-targets + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" +) +install(DIRECTORY ${LIB_MOD_DIR} DESTINATION "${CMAKE_INSTALL_MODULEDIR}") diff --git a/examples/fftpack/native/cfftb1.f90 b/examples/fftpack/native/cfftb1.f90 new file mode 100644 index 000000000..7c0bf79d7 --- /dev/null +++ b/examples/fftpack/native/cfftb1.f90 @@ -0,0 +1,69 @@ + subroutine cfftb1(n, c, ch, wa, ifac) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n, ifac(*) + real(dp), intent(in) :: wa(*) + real(dp), intent(inout) :: c(*), ch(*) + integer :: i, idl1, ido, idot, ip, iw, ix2, ix3, ix4, & + k1, l1, l2, n2, na, nac, nf + nf = ifac(2) + na = 0 + l1 = 1 + iw = 1 + do k1 = 1, nf + ip = ifac(k1 + 2) + l2 = ip*l1 + ido = n/l2 + idot = ido + ido + idl1 = idot*l1 + if (ip == 4) then + ix2 = iw + idot + ix3 = ix2 + idot + if (na /= 0) then + call passb4(idot, l1, ch, c, wa(iw), wa(ix2), wa(ix3)) + else + call passb4(idot, l1, c, ch, wa(iw), wa(ix2), wa(ix3)) + end if + na = 1 - na + elseif (ip == 2) then + if (na /= 0) then + call passb2(idot, l1, ch, c, wa(iw)) + else + call passb2(idot, l1, c, ch, wa(iw)) + end if + na = 1 - na + elseif (ip == 3) then + ix2 = iw + idot + if (na /= 0) then + call passb3(idot, l1, ch, c, wa(iw), wa(ix2)) + else + call passb3(idot, l1, c, ch, wa(iw), wa(ix2)) + end if + na = 1 - na + elseif (ip /= 5) then + if (na /= 0) then + call passb(nac, idot, ip, l1, idl1, ch, ch, ch, c, c, wa(iw)) + else + call passb(nac, idot, ip, l1, idl1, c, c, c, ch, ch, wa(iw)) + end if + if (nac /= 0) na = 1 - na + else + ix2 = iw + idot + ix3 = ix2 + idot + ix4 = ix3 + idot + if (na /= 0) then + call passb5(idot, l1, ch, c, wa(iw), wa(ix2), wa(ix3), wa(ix4)) + else + call passb5(idot, l1, c, ch, wa(iw), wa(ix2), wa(ix3), wa(ix4)) + end if + na = 1 - na + end if + l1 = l2 + iw = iw + (ip - 1)*idot + end do + if (na == 0) return + n2 = n + n + do concurrent(i=1:n2) + c(i) = ch(i) + end do + end subroutine cfftb1 diff --git a/examples/fftpack/native/cfftf1.f90 b/examples/fftpack/native/cfftf1.f90 new file mode 100644 index 000000000..f692e3af7 --- /dev/null +++ b/examples/fftpack/native/cfftf1.f90 @@ -0,0 +1,69 @@ + subroutine cfftf1(n, c, ch, wa, ifac) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n, ifac(*) + real(dp), intent(inout) :: c(*), ch(*) + real(dp), intent(in) :: wa(*) + integer :: i, idl1, ido, idot, ip, iw, ix2, ix3, ix4, & + k1, l1, l2, n2, na, nac, nf + nf = ifac(2) + na = 0 + l1 = 1 + iw = 1 + do k1 = 1, nf + ip = ifac(k1 + 2) + l2 = ip*l1 + ido = n/l2 + idot = ido + ido + idl1 = idot*l1 + if (ip == 4) then + ix2 = iw + idot + ix3 = ix2 + idot + if (na /= 0) then + call passf4(idot, l1, ch, c, wa(iw), wa(ix2), wa(ix3)) + else + call passf4(idot, l1, c, ch, wa(iw), wa(ix2), wa(ix3)) + end if + na = 1 - na + elseif (ip == 2) then + if (na /= 0) then + call passf2(idot, l1, ch, c, wa(iw)) + else + call passf2(idot, l1, c, ch, wa(iw)) + end if + na = 1 - na + elseif (ip == 3) then + ix2 = iw + idot + if (na /= 0) then + call passf3(idot, l1, ch, c, wa(iw), wa(ix2)) + else + call passf3(idot, l1, c, ch, wa(iw), wa(ix2)) + end if + na = 1 - na + elseif (ip /= 5) then + if (na /= 0) then + call passf(nac, idot, ip, l1, idl1, ch, ch, ch, c, c, wa(iw)) + else + call passf(nac, idot, ip, l1, idl1, c, c, c, ch, ch, wa(iw)) + end if + if (nac /= 0) na = 1 - na + else + ix2 = iw + idot + ix3 = ix2 + idot + ix4 = ix3 + idot + if (na /= 0) then + call passf5(idot, l1, ch, c, wa(iw), wa(ix2), wa(ix3), wa(ix4)) + else + call passf5(idot, l1, c, ch, wa(iw), wa(ix2), wa(ix3), wa(ix4)) + end if + na = 1 - na + end if + l1 = l2 + iw = iw + (ip - 1)*idot + end do + if (na == 0) return + n2 = n + n + do concurrent(i=1:n2) + c(i) = ch(i) + end do + end subroutine cfftf1 diff --git a/examples/fftpack/native/cffti1.f90 b/examples/fftpack/native/cffti1.f90 new file mode 100644 index 000000000..6647e9e26 --- /dev/null +++ b/examples/fftpack/native/cffti1.f90 @@ -0,0 +1,70 @@ + subroutine cffti1(n, wa, ifac) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + integer, intent(out) :: ifac(*) + real(dp), intent(out) :: wa(*) + real(dp) :: arg, argh, argld, fi + integer :: i, i1, ib, ido, idot, ii, ip, ipm, j, k1, & + l1, l2, ld, nf, nl, nq, nr, ntry + integer, dimension(4), parameter :: ntryh = [3, 4, 2, 5] + real(dp), parameter :: tpi = 2.0_dp*acos(-1.0_dp) ! 2 * pi + nl = n + nf = 0 + j = 0 +100 j = j + 1 + if (j <= 4) then + ntry = ntryh(j) + else + ntry = ntry + 2 + end if +200 nq = nl/ntry + nr = nl - ntry*nq + if (nr /= 0) goto 100 + nf = nf + 1 + ifac(nf + 2) = ntry + nl = nq + if (ntry == 2) then + if (nf /= 1) then + do i = 2, nf + ib = nf - i + 2 + ifac(ib + 2) = ifac(ib + 1) + end do + ifac(3) = 2 + end if + end if + if (nl /= 1) goto 200 + ifac(1) = n + ifac(2) = nf + argh = tpi/real(n, kind=dp) + i = 2 + l1 = 1 + do k1 = 1, nf + ip = ifac(k1 + 2) + ld = 0 + l2 = l1*ip + ido = n/l2 + idot = ido + ido + 2 + ipm = ip - 1 + do j = 1, ipm + i1 = i + wa(i - 1) = 1.0_dp + wa(i) = 0.0_dp + ld = ld + l1 + fi = 0.0_dp + argld = real(ld, kind=dp)*argh + do ii = 4, idot, 2 + i = i + 2 + fi = fi + 1.0_dp + arg = fi*argld + wa(i - 1) = cos(arg) + wa(i) = sin(arg) + end do + if (ip > 5) then + wa(i1 - 1) = wa(i - 1) + wa(i1) = wa(i) + end if + end do + l1 = l2 + end do + end subroutine cffti1 diff --git a/examples/fftpack/native/cosqb1.f90 b/examples/fftpack/native/cosqb1.f90 new file mode 100644 index 000000000..3d2b699d8 --- /dev/null +++ b/examples/fftpack/native/cosqb1.f90 @@ -0,0 +1,33 @@ + subroutine cosqb1(n, x, w, xh) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(inout) :: x(*) + real(dp), intent(in) :: w(*) + real(dp), intent(out) :: xh(*) + integer :: i, k, kc, modn, np2, ns2 + real(dp) :: xim1 + ns2 = (n + 1)/2 + np2 = n + 2 + do i = 3, n, 2 + xim1 = x(i - 1) + x(i) + x(i) = x(i) - x(i - 1) + x(i - 1) = xim1 + end do + x(1) = x(1) + x(1) + modn = mod(n, 2) + if (modn == 0) x(n) = x(n) + x(n) + call dfftb(n, x, xh) + do k = 2, ns2 + kc = np2 - k + xh(k) = w(k - 1)*x(kc) + w(kc - 1)*x(k) + xh(kc) = w(k - 1)*x(k) - w(kc - 1)*x(kc) + end do + if (modn == 0) x(ns2 + 1) = w(ns2)*(x(ns2 + 1) + x(ns2 + 1)) + do k = 2, ns2 + kc = np2 - k + x(k) = xh(k) + xh(kc) + x(kc) = xh(k) - xh(kc) + end do + x(1) = x(1) + x(1) + end subroutine cosqb1 diff --git a/examples/fftpack/native/cosqf1.f90 b/examples/fftpack/native/cosqf1.f90 new file mode 100644 index 000000000..21ff74edd --- /dev/null +++ b/examples/fftpack/native/cosqf1.f90 @@ -0,0 +1,31 @@ + subroutine cosqf1(n, x, w, xh) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(inout) :: x(*) + real(dp), intent(in) :: w(*) + real(dp), intent(out) :: xh(*) + integer :: i, k, kc, modn, np2, ns2 + real(dp) :: xim1 + ns2 = (n + 1)/2 + np2 = n + 2 + do k = 2, ns2 + kc = np2 - k + xh(k) = x(k) + x(kc) + xh(kc) = x(k) - x(kc) + end do + modn = mod(n, 2) + if (modn == 0) xh(ns2 + 1) = x(ns2 + 1) + x(ns2 + 1) + do k = 2, ns2 + kc = np2 - k + x(k) = w(k - 1)*xh(kc) + w(kc - 1)*xh(k) + x(kc) = w(k - 1)*xh(k) - w(kc - 1)*xh(kc) + end do + if (modn == 0) x(ns2 + 1) = w(ns2)*xh(ns2 + 1) + call dfftf(n, x, xh) + do i = 3, n, 2 + xim1 = x(i - 1) - x(i) + x(i) = x(i - 1) + x(i) + x(i - 1) = xim1 + end do + end subroutine cosqf1 diff --git a/examples/fftpack/native/dcosqb.f90 b/examples/fftpack/native/dcosqb.f90 new file mode 100644 index 000000000..9b3c34cc4 --- /dev/null +++ b/examples/fftpack/native/dcosqb.f90 @@ -0,0 +1,20 @@ + subroutine dcosqb(n, x, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(in) :: wsave(*) + real(dp), intent(inout) :: x(*) + real(dp) :: x1 + real(dp), parameter :: tsqrt2 = 2.0_dp*sqrt(2.0_dp) + if (n < 2) then + x(1) = 4.0_dp*x(1) + return + elseif (n == 2) then + x1 = 4.0_dp*(x(1) + x(2)) + x(2) = tsqrt2*(x(1) - x(2)) + x(1) = x1 + return + else + call cosqb1(n, x, wsave, wsave(n + 1)) + end if + end subroutine dcosqb diff --git a/examples/fftpack/native/dcosqf.f90 b/examples/fftpack/native/dcosqf.f90 new file mode 100644 index 000000000..e82338762 --- /dev/null +++ b/examples/fftpack/native/dcosqf.f90 @@ -0,0 +1,18 @@ + subroutine dcosqf(n, x, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(in) :: wsave(*) + real(dp), intent(inout) :: x(*) + real(dp) :: tsqx + real(dp), parameter :: sqrt2 = sqrt(2.0_dp) + if (n < 2) then + return + elseif (n == 2) then + tsqx = sqrt2*x(2) + x(2) = x(1) - tsqx + x(1) = x(1) + tsqx + else + call cosqf1(n, x, wsave, wsave(n + 1)) + end if + end subroutine dcosqf diff --git a/examples/fftpack/native/dcosqi.f90 b/examples/fftpack/native/dcosqi.f90 new file mode 100644 index 000000000..184b21459 --- /dev/null +++ b/examples/fftpack/native/dcosqi.f90 @@ -0,0 +1,16 @@ + subroutine dcosqi(n, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(out) :: wsave(*) + real(dp) :: dt, fk + integer :: k + real(dp), parameter :: pih = acos(-1.0_dp)/2.0_dp ! pi / 2 + dt = pih/real(n, kind=dp) + fk = 0.0_dp + do k = 1, n + fk = fk + 1.0_dp + wsave(k) = cos(fk*dt) + end do + call dffti(n, wsave(n + 1)) + end subroutine dcosqi diff --git a/examples/fftpack/native/dcost.f90 b/examples/fftpack/native/dcost.f90 new file mode 100644 index 000000000..4273fbb84 --- /dev/null +++ b/examples/fftpack/native/dcost.f90 @@ -0,0 +1,50 @@ + subroutine dcost(n, x, wsave) + use fftpack_kind, only: rk + implicit none + integer, intent(in) :: n + real(rk), intent(inout) :: wsave(*) + real(rk), intent(inout) :: x(*) + real(rk) :: c1, t1, t2, tx2, x1h, x1p3, & + xi, xim2 + integer :: i, k, kc, modn, nm1, np1, ns2 + nm1 = n - 1 + np1 = n + 1 + ns2 = n/2 + if (n < 2) return + if (n == 2) then + x1h = x(1) + x(2) + x(2) = x(1) - x(2) + x(1) = x1h + return + elseif (n > 3) then + c1 = x(1) - x(n) + x(1) = x(1) + x(n) + do k = 2, ns2 + kc = np1 - k + t1 = x(k) + x(kc) + t2 = x(k) - x(kc) + c1 = c1 + wsave(kc)*t2 + t2 = wsave(k)*t2 + x(k) = t1 - t2 + x(kc) = t1 + t2 + end do + modn = mod(n, 2) + if (modn /= 0) x(ns2 + 1) = x(ns2 + 1) + x(ns2 + 1) + call dfftf(nm1, x, wsave(n + 1)) + xim2 = x(2) + x(2) = c1 + do i = 4, n, 2 + xi = x(i) + x(i) = x(i - 2) - x(i - 1) + x(i - 1) = xim2 + xim2 = xi + end do + if (modn /= 0) x(n) = xim2 + return + end if + x1p3 = x(1) + x(3) + tx2 = x(2) + x(2) + x(2) = x(1) - x(3) + x(1) = x1p3 + tx2 + x(3) = x1p3 - tx2 + end subroutine dcost diff --git a/examples/fftpack/native/dcosti.f90 b/examples/fftpack/native/dcosti.f90 new file mode 100644 index 000000000..ad68e8594 --- /dev/null +++ b/examples/fftpack/native/dcosti.f90 @@ -0,0 +1,22 @@ + subroutine dcosti(n, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(out) :: wsave(*) + real(dp) :: dt, fk + integer :: k, kc, nm1, np1, ns2 + real(dp), parameter :: pi = acos(-1.0_dp) + if (n <= 3) return + nm1 = n - 1 + np1 = n + 1 + ns2 = n/2 + dt = pi/real(nm1, kind=dp) + fk = 0.0_dp + do k = 2, ns2 + kc = np1 - k + fk = fk + 1.0_dp + wsave(k) = 2.0_dp*sin(fk*dt) + wsave(kc) = 2.0_dp*cos(fk*dt) + end do + call dffti(nm1, wsave(n + 1)) + end subroutine dcosti diff --git a/examples/fftpack/native/dfftb.f90 b/examples/fftpack/native/dfftb.f90 new file mode 100644 index 000000000..8620e63ff --- /dev/null +++ b/examples/fftpack/native/dfftb.f90 @@ -0,0 +1,9 @@ + subroutine dfftb(n, r, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(inout) :: r(*) + real(dp), intent(inout) :: wsave(*) + if (n == 1) return + call rfftb1(n, r, wsave, wsave(n + 1), wsave(2*n + 1)) + end subroutine dfftb diff --git a/examples/fftpack/native/dfftf.f90 b/examples/fftpack/native/dfftf.f90 new file mode 100644 index 000000000..39064ef8a --- /dev/null +++ b/examples/fftpack/native/dfftf.f90 @@ -0,0 +1,9 @@ + subroutine dfftf(n, r, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(inout) :: r(*) + real(dp), intent(inout) :: wsave(*) + if (n == 1) return + call rfftf1(n, r, wsave, wsave(n + 1), wsave(2*n + 1)) + end subroutine dfftf diff --git a/examples/fftpack/native/dffti.f90 b/examples/fftpack/native/dffti.f90 new file mode 100644 index 000000000..ec7a341e1 --- /dev/null +++ b/examples/fftpack/native/dffti.f90 @@ -0,0 +1,8 @@ + subroutine dffti(n, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(out) :: wsave(*) + if (n == 1) return + call rffti1(n, wsave(n + 1), wsave(2*n + 1)) + end subroutine dffti diff --git a/examples/fftpack/native/dsinqb.f90 b/examples/fftpack/native/dsinqb.f90 new file mode 100644 index 000000000..dfc85049d --- /dev/null +++ b/examples/fftpack/native/dsinqb.f90 @@ -0,0 +1,25 @@ + subroutine dsinqb(n, x, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(inout) :: x(*) + real(dp), intent(in) :: wsave(*) + integer :: k, kc, ns2 + real(dp) :: xhold + if (n > 1) then + ns2 = n/2 + do k = 2, n, 2 + x(k) = -x(k) + end do + call dcosqb(n, x, wsave) + do k = 1, ns2 + kc = n - k + xhold = x(k) + x(k) = x(kc + 1) + x(kc + 1) = xhold + end do + return + end if + x(1) = 4.0_dp*x(1) + return + end subroutine dsinqb diff --git a/examples/fftpack/native/dsinqf.f90 b/examples/fftpack/native/dsinqf.f90 new file mode 100644 index 000000000..388bb17f4 --- /dev/null +++ b/examples/fftpack/native/dsinqf.f90 @@ -0,0 +1,21 @@ + subroutine dsinqf(n, x, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(inout) :: x(*) + real(dp), intent(in) :: wsave(*) + integer :: k, kc, ns2 + real(dp) :: xhold + if (n == 1) return + ns2 = n/2 + do k = 1, ns2 + kc = n - k + xhold = x(k) + x(k) = x(kc + 1) + x(kc + 1) = xhold + end do + call dcosqf(n, x, wsave) + do k = 2, n, 2 + x(k) = -x(k) + end do + end subroutine dsinqf diff --git a/examples/fftpack/native/dsinqi.f90 b/examples/fftpack/native/dsinqi.f90 new file mode 100644 index 000000000..642b79548 --- /dev/null +++ b/examples/fftpack/native/dsinqi.f90 @@ -0,0 +1,7 @@ + subroutine dsinqi(n, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(out) :: wsave(*) + call dcosqi(n, wsave) + end subroutine dsinqi diff --git a/examples/fftpack/native/dsint.f90 b/examples/fftpack/native/dsint.f90 new file mode 100644 index 000000000..3515772d8 --- /dev/null +++ b/examples/fftpack/native/dsint.f90 @@ -0,0 +1,13 @@ + subroutine dsint(n, x, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(inout) :: x(*) + real(dp), intent(in) :: wsave(*) + integer :: iw1, iw2, iw3, np1 + np1 = n + 1 + iw1 = n/2 + 1 + iw2 = iw1 + np1 + iw3 = iw2 + np1 + call sint1(n, x, wsave, wsave(iw1), wsave(iw2), wsave(iw3)) + end subroutine dsint diff --git a/examples/fftpack/native/dsinti.f90 b/examples/fftpack/native/dsinti.f90 new file mode 100644 index 000000000..a2b2cfbbe --- /dev/null +++ b/examples/fftpack/native/dsinti.f90 @@ -0,0 +1,17 @@ + subroutine dsinti(n, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(out) :: wsave(*) + real(dp) :: dt + integer :: k, np1, ns2 + real(dp), parameter :: pi = acos(-1.0_dp) + if (n <= 1) return + ns2 = n/2 + np1 = n + 1 + dt = pi/real(np1, dp) + do k = 1, ns2 + wsave(k) = 2.0_dp*sin(k*dt) + end do + call dffti(np1, wsave(ns2 + 1)) + end subroutine dsinti diff --git a/examples/fftpack/native/dzfftb.f90 b/examples/fftpack/native/dzfftb.f90 new file mode 100644 index 000000000..1acb8c330 --- /dev/null +++ b/examples/fftpack/native/dzfftb.f90 @@ -0,0 +1,26 @@ + subroutine dzfftb(n, r, azero, a, b, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(out) :: r(*) + real(dp), intent(inout) :: wsave(*) + real(dp), intent(in) :: azero, a(*), b(*) + integer :: i, ns2 + if (n < 2) then + r(1) = azero + return + elseif (n == 2) then + r(1) = azero + a(1) + r(2) = azero - a(1) + return + else + ns2 = (n - 1)/2 + do concurrent(i=1:ns2) + r(2*i) = 0.5_dp*a(i) + r(2*i + 1) = -0.5_dp*b(i) + end do + r(1) = azero + if (mod(n, 2) == 0) r(n) = a(ns2 + 1) + call dfftb(n, r, wsave(n + 1)) + end if + end subroutine dzfftb diff --git a/examples/fftpack/native/dzfftf.f90 b/examples/fftpack/native/dzfftf.f90 new file mode 100644 index 000000000..3ce5d8abb --- /dev/null +++ b/examples/fftpack/native/dzfftf.f90 @@ -0,0 +1,36 @@ + subroutine dzfftf(n, r, azero, a, b, wsave) +! version 3 june 1979 + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(in) :: r(*) + real(dp), intent(out) :: azero, a(*), b(*) + real(dp), intent(inout) :: wsave(*) + real(dp) :: cf, cfm + integer :: i, ns2, ns2m + if (n < 2) then + azero = r(1) + return + elseif (n == 2) then + azero = 0.5_dp*(r(1) + r(2)) + a(1) = 0.5_dp*(r(1) - r(2)) + return + else + do concurrent(i=1:n) + wsave(i) = r(i) + end do + call dfftf(n, wsave, wsave(n + 1)) + cf = 2.0_dp/real(n, dp) + cfm = -cf + azero = 0.5_dp*cf*wsave(1) + ns2 = (n + 1)/2 + ns2m = ns2 - 1 + do concurrent(i=1:ns2m) + a(i) = cf*wsave(2*i) + b(i) = cfm*wsave(2*i + 1) + end do + if (mod(n, 2) == 1) return + a(ns2) = 0.5_dp*cf*wsave(n) + b(ns2) = 0.0_dp + end if + end subroutine dzfftf diff --git a/examples/fftpack/native/dzffti.f90 b/examples/fftpack/native/dzffti.f90 new file mode 100644 index 000000000..468763978 --- /dev/null +++ b/examples/fftpack/native/dzffti.f90 @@ -0,0 +1,8 @@ + subroutine dzffti(n, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(out) :: wsave(*) + if (n == 1) return + call ezfft1(n, wsave(2*n + 1), wsave(3*n + 1)) + end subroutine dzffti diff --git a/examples/fftpack/native/ezfft1.f90 b/examples/fftpack/native/ezfft1.f90 new file mode 100644 index 000000000..38a891529 --- /dev/null +++ b/examples/fftpack/native/ezfft1.f90 @@ -0,0 +1,72 @@ + subroutine ezfft1(n, wa, ifac) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(out) :: wa(*) + integer, intent(out) :: ifac(*) + real(dp) :: arg1, argh, ch1, ch1h, dch1, dsh1, sh1 + integer :: i, ib, ido, ii, ip, ipm, is, j, k1, l1, & + l2, nf, nfm1, nl, nq, nr, ntry + integer, dimension(4), parameter :: ntryh = [4, 2, 3, 5] + real(dp), parameter :: tpi = 2.0_dp*acos(-1.0_dp) ! 2 * pi + nl = n + nf = 0 + j = 0 +100 j = j + 1 + if (j <= 4) then + ntry = ntryh(j) + else + ntry = ntry + 2 + end if +200 nq = nl/ntry + nr = nl - ntry*nq + if (nr /= 0) goto 100 + nf = nf + 1 + ifac(nf + 2) = ntry + nl = nq + if (ntry == 2) then + if (nf /= 1) then + do i = 2, nf + ib = nf - i + 2 + ifac(ib + 2) = ifac(ib + 1) + end do + ifac(3) = 2 + end if + end if + if (nl /= 1) goto 200 + ifac(1) = n + ifac(2) = nf + argh = tpi/real(n, dp) + is = 0 + nfm1 = nf - 1 + l1 = 1 + if (nfm1 == 0) return + do k1 = 1, nfm1 + ip = ifac(k1 + 2) + l2 = l1*ip + ido = n/l2 + ipm = ip - 1 + arg1 = real(l1, dp)*argh + ch1 = 1.0_dp + sh1 = 0.0_dp + dch1 = cos(arg1) + dsh1 = sin(arg1) + do j = 1, ipm + ch1h = dch1*ch1 - dsh1*sh1 + sh1 = dch1*sh1 + dsh1*ch1 + ch1 = ch1h + i = is + 2 + wa(i - 1) = ch1 + wa(i) = sh1 + if (ido >= 5) then + do ii = 5, ido, 2 + i = i + 2 + wa(i - 1) = ch1*wa(i - 3) - sh1*wa(i - 2) + wa(i) = ch1*wa(i - 2) + sh1*wa(i - 3) + end do + end if + is = is + ido + end do + l1 = l2 + end do + end subroutine ezfft1 diff --git a/examples/fftpack/native/fftpack.f90 b/examples/fftpack/native/fftpack.f90 new file mode 100644 index 000000000..ed14799bf --- /dev/null +++ b/examples/fftpack/native/fftpack.f90 @@ -0,0 +1,362 @@ +module fftpack + use fftpack_kind + + implicit none + private + + public :: zffti, zfftf, zfftb + public :: fft, ifft + public :: fftshift, ifftshift + public :: fftfreq, rfftfreq + + public :: dffti, dfftf, dfftb + public :: rfft, irfft + + public :: dzffti, dzfftf, dzfftb + + public :: dcosqi, dcosqf, dcosqb + public :: dcosti, dcost + public :: dct, idct + public :: dct_t1i, dct_t1 + public :: dct_t23i, dct_t2, dct_t3 + + public :: dsinti, dsint + + public :: rk + + interface + + !> Version: experimental + !> + !> Initialize `zfftf` and `zfftb`. + !> ([Specification](../page/specs/fftpack.html#zffti)) + pure subroutine zffti(n, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(out) :: wsave(*) + end subroutine zffti + + !> Version: experimental + !> + !> Forward transform of a complex periodic sequence. + !> ([Specification](../page/specs/fftpack.html#zfftf)) + pure subroutine zfftf(n, c, wsave) + import rk + integer, intent(in) :: n + complex(kind=rk), intent(inout) :: c(*) + real(kind=rk), intent(in) :: wsave(*) + end subroutine zfftf + + !> Version: experimental + !> + !> Unnormalized inverse of `zfftf`. + !> ([Specification](../page/specs/fftpack.html#zfftb)) + pure subroutine zfftb(n, c, wsave) + import rk + integer, intent(in) :: n + complex(kind=rk), intent(inout) :: c(*) + real(kind=rk), intent(in) :: wsave(*) + end subroutine zfftb + + !> Version: experimental + !> + !> Initialize `dfftf` and `dfftb`. + !> ([Specification](../page/specs/fftpack.html#dffti)) + pure subroutine dffti(n, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(out) :: wsave(*) + end subroutine dffti + + !> Version: experimental + !> + !> Forward transform of a real periodic sequence. + !> ([Specification](../page/specs/fftpack.html#dfftf)) + pure subroutine dfftf(n, r, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(inout) :: r(*) + real(kind=rk), intent(in) :: wsave(*) + end subroutine dfftf + + !> Version: experimental + !> + !> Unnormalized inverse of `dfftf`. + !> ([Specification](../page/specs/fftpack.html#dfftb)) + pure subroutine dfftb(n, r, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(inout) :: r(*) + real(kind=rk), intent(in) :: wsave(*) + end subroutine dfftb + + !> Version: experimental + !> + !> Initialize `dzfftf` and `dzfftb`. + !> ([Specification](../page/specs/fftpack.html#dzffti)) + pure subroutine dzffti(n, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(out) :: wsave(*) + end subroutine dzffti + + !> Version: experimental + !> + !> Simplified forward transform of a real periodic sequence. + !> ([Specification](../page/specs/fftpack.html#dzfftf)) + pure subroutine dzfftf(n, r, azero, a, b, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(in) :: r(*) + real(kind=rk), intent(out) :: azero + real(kind=rk), intent(out) :: a(*), b(*) + real(kind=rk), intent(in) :: wsave(*) + end subroutine dzfftf + + !> Version: experimental + !> + !> Unnormalized inverse of `dzfftf`. + !> ([Specification](../page/specs/fftpack.html#dzfftb)) + pure subroutine dzfftb(n, r, azero, a, b, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(out) :: r(*) + real(kind=rk), intent(in) :: azero + real(kind=rk), intent(in) :: a(*), b(*) + real(kind=rk), intent(in) :: wsave(*) + end subroutine dzfftb + + !> Version: experimental + !> + !> Initialize `dcosqf` and `dcosqb`. + !> ([Specification](../page/specs/fftpack.html#initialize-dct-2-3-dcosqi-or-dct_t23i)) + pure subroutine dcosqi(n, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(out) :: wsave(*) + end subroutine dcosqi + + !> Version: experimental + !> + !> Forward transform of quarter wave data. + !> ([Specification](../page/specs/fftpack.html#compute-dct-3-dcosqf-or-dct_t3)) + pure subroutine dcosqf(n, x, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(inout) :: x(*) + real(kind=rk), intent(in) :: wsave(*) + end subroutine dcosqf + + !> Version: experimental + !> + !> Unnormalized inverse of `dcosqf`. + !> ([Specification](../page/specs/fftpack.html#compute-dct-2-dcosqb-or-dct_t2)) + pure subroutine dcosqb(n, x, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(inout) :: x(*) + real(kind=rk), intent(in) :: wsave(*) + end subroutine dcosqb + + !> Version: experimental + !> + !> Initialize `dcost`. + !> ([Specification](../page/specs/fftpack.html#initialize-dct-1-dcosti-or-dct_t1i)) + pure subroutine dcosti(n, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(out) :: wsave(*) + end subroutine dcosti + + !> Version: experimental + !> + !> Discrete fourier cosine transform of an even sequence. + !> ([Specification](../page/specs/fftpack.html#compute-dct-1-dcost-or-dct_t1)) + pure subroutine dcost(n, x, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(inout) :: x(*) + real(kind=rk), intent(in) :: wsave(*) + end subroutine dcost + + + pure subroutine dsinti(n, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(out) :: wsave(*) + end subroutine dsinti + + + pure subroutine dsint(n, x, wsave) + import rk + integer, intent(in) :: n + real(kind=rk), intent(inout) :: x(*) + real(kind=rk), intent(in) :: wsave(*) + end subroutine dsint + + + !> Version: experimental + !> + !> Integer frequency values involved in complex FFT. + !> ([Specifiction](../page/specs/fftpack.html#fftfreq)) + pure module function fftfreq(n) result(out) + integer, intent(in) :: n + integer, dimension(n) :: out + end function fftfreq + + !> Version: experimental + !> + !> Integer frequency values involved in real FFT. + !> ([Specifiction](../page/specs/fftpack.html#rfftfreq)) + pure module function rfftfreq(n) result(out) + integer, intent(in) :: n + integer, dimension(n) :: out + end function rfftfreq + + end interface + + !> Version: experimental + !> + !> Forward transform of a complex periodic sequence. + !> ([Specifiction](../page/specs/fftpack.html#fft)) + interface fft + pure module function fft_rk(x, n) result(result) + complex(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + complex(kind=rk), allocatable :: result(:) + end function fft_rk + end interface fft + + !> Version: experimental + !> + !> Backward transform of a complex periodic sequence. + !> ([Specifiction](../page/specs/fftpack.html#ifft)) + interface ifft + pure module function ifft_rk(x, n) result(result) + complex(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + complex(kind=rk), allocatable :: result(:) + end function ifft_rk + end interface ifft + + !> Version: experimental + !> + !> Forward transform of a real periodic sequence. + !> ([Specifiction](../page/specs/fftpack.html#rfft)) + interface rfft + pure module function rfft_rk(x, n) result(result) + real(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + real(kind=rk), allocatable :: result(:) + end function rfft_rk + end interface rfft + + !> Version: experimental + !> + !> Backward transform of a real periodic sequence. + !> ([Specifiction](../page/specs/fftpack.html#irfft)) + interface irfft + pure module function irfft_rk(x, n) result(result) + real(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + real(kind=rk), allocatable :: result(:) + end function irfft_rk + end interface irfft + + !> Version: experimental + !> + !> Dsicrete cosine transforms. + !> ([Specification](../page/specs/fftpack.html#simplified-dct-of-types-1-2-3-dct)) + interface dct + pure module function dct_rk(x, n, type) result(result) + real(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + integer, intent(in), optional :: type + real(kind=rk), allocatable :: result(:) + end function dct_rk + end interface dct + + !> Version: experimental + !> + !> Inverse discrete cosine transforms. + !> ([Specification](../page/specs/fftpack.html#simplified-inverse-dct-of-types-1-2-3-idct)) + interface idct + pure module function idct_rk(x, n, type) result(result) + real(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + integer, intent(in), optional :: type + real(kind=rk), allocatable :: result(:) + end function idct_rk + end interface idct + + !> Version: experimental + !> + !> Initialize DCT type-1 + !> ([Specification](../page/specs/fftpack.html#initialize-dct-1-dcosti-or-dct_t1i)) + interface dct_t1i + procedure :: dcosti + end interface dct_t1i + + !> Version: experimental + !> + !> Perform DCT type-1 + !> ([Specification](../page/specs/fftpack.html#compute-dct-1-dcost-or-dct_t1)) + interface dct_t1 + procedure :: dcost + end interface dct_t1 + + !> Version: experimental + !> + !> Initialize DCT types 2, 3 + !> ([Specification](../page/specs/fftpack.html#initialize-dct-2-3-dcosqi-or-dct_t23i)) + interface dct_t23i + procedure :: dcosqi + end interface dct_t23i + + !> Version: experimental + !> + !> Perform DCT type-2 + !> ([Specification](../page/specs/fftpack.html#compute-dct-2-dcosqb-or-dct_t2)) + interface dct_t2 + procedure :: dcosqb + end interface dct_t2 + + !> Version: experimental + !> + !> Perform DCT type-3 + !> ([Specification](../page/specs/fftpack.html#compute-dct-3-dcosqf-or-dct_t3)) + interface dct_t3 + procedure :: dcosqf + end interface dct_t3 + + !> Version: experimental + !> + !> Shifts zero-frequency component to center of spectrum. + !> ([Specifiction](../page/specs/fftpack.html#fftshift)) + interface fftshift + pure module function fftshift_crk(x) result(result) + complex(kind=rk), intent(in) :: x(:) + complex(kind=rk), dimension(size(x)) :: result + end function fftshift_crk + pure module function fftshift_rrk(x) result(result) + real(kind=rk), intent(in) :: x(:) + real(kind=rk), dimension(size(x)) :: result + end function fftshift_rrk + end interface fftshift + + !> Version: experimental + !> + !> Shifts zero-frequency component to beginning of spectrum. + !> ([Specifiction](../page/specs/fftpack.html#ifftshift)) + interface ifftshift + pure module function ifftshift_crk(x) result(result) + complex(kind=rk), intent(in) :: x(:) + complex(kind=rk), dimension(size(x)) :: result + end function ifftshift_crk + pure module function ifftshift_rrk(x) result(result) + real(kind=rk), intent(in) :: x(:) + real(kind=rk), dimension(size(x)) :: result + end function ifftshift_rrk + end interface ifftshift + +end module fftpack diff --git a/examples/fftpack/native/fftpack_dct.f90 b/examples/fftpack/native/fftpack_dct.f90 new file mode 100644 index 000000000..e7652157a --- /dev/null +++ b/examples/fftpack/native/fftpack_dct.f90 @@ -0,0 +1,103 @@ +submodule(fftpack) fftpack_dct + +contains + + !> Discrete cosine transforms of types 1, 2, 3. + pure module function dct_rk(x, n, type) result(result) + real(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + integer, intent(in), optional :: type + real(kind=rk), allocatable :: result(:) + + integer :: lenseq, lensav, i + real(kind=rk), allocatable :: wsave(:) + + if (present(n)) then + lenseq = n + if (lenseq <= size(x)) then + result = x(:lenseq) + else if (lenseq > size(x)) then + result = [x, (0.0_rk, i=1, lenseq - size(x))] + end if + else + lenseq = size(x) + result = x + end if + + ! Default to DCT-2 + if (.not.present(type)) then + lensav = 3*lenseq + 15 + allocate (wsave(lensav)) + call dcosqi(lenseq, wsave) + call dcosqb(lenseq, result, wsave) + return + end if + + if (type == 1) then ! DCT-1 + lensav = 3*lenseq + 15 + allocate (wsave(lensav)) + call dcosti(lenseq, wsave) + call dcost(lenseq, result, wsave) + else if (type == 2) then ! DCT-2 + lensav = 3*lenseq + 15 + allocate (wsave(lensav)) + call dcosqi(lenseq, wsave) + call dcosqb(lenseq, result, wsave) + else if (type == 3) then ! DCT-3 + lensav = 3*lenseq + 15 + allocate (wsave(lensav)) + call dcosqi(lenseq, wsave) + call dcosqf(lenseq, result, wsave) + end if + end function dct_rk + + !> Inverse discrete cosine transforms of types 1, 2, 3. + pure module function idct_rk(x, n, type) result(result) + real(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + integer, intent(in), optional :: type + real(kind=rk), allocatable :: result(:) + + integer :: lenseq, lensav, i + real(kind=rk), allocatable :: wsave(:) + + if (present(n)) then + lenseq = n + if (lenseq <= size(x)) then + result = x(:lenseq) + else if (lenseq > size(x)) then + result = [x, (0.0_rk, i=1, lenseq - size(x))] + end if + else + lenseq = size(x) + result = x + end if + + ! Default to t=2; inverse DCT-2 is DCT-3 + if (.not.present(type)) then + lensav = 3*lenseq + 15 + allocate (wsave(lensav)) + call dcosqi(lenseq, wsave) + call dcosqf(lenseq, result, wsave) + return + end if + + if (type == 1) then ! inverse DCT-1 is DCT-1 + lensav = 3*lenseq + 15 + allocate (wsave(lensav)) + call dcosti(lenseq, wsave) + call dcost(lenseq, result, wsave) + else if (type == 2) then ! inverse DCT-2 is DCT-3 + lensav = 3*lenseq + 15 + allocate (wsave(lensav)) + call dcosqi(lenseq, wsave) + call dcosqf(lenseq, result, wsave) + else if (type == 3) then ! inverse DCT-3 is DCT-2 + lensav = 3*lenseq + 15 + allocate (wsave(lensav)) + call dcosqi(lenseq, wsave) + call dcosqb(lenseq, result, wsave) + end if + end function idct_rk + +end submodule fftpack_dct diff --git a/examples/fftpack/native/fftpack_fft.f90 b/examples/fftpack/native/fftpack_fft.f90 new file mode 100644 index 000000000..8f02ee01f --- /dev/null +++ b/examples/fftpack/native/fftpack_fft.f90 @@ -0,0 +1,36 @@ +submodule(fftpack) fftpack_fft + +contains + + !> Forward transform of a complex periodic sequence. + pure module function fft_rk(x, n) result(result) + complex(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + complex(kind=rk), allocatable :: result(:) + + integer :: lenseq, lensav, i + real(kind=rk), allocatable :: wsave(:) + + if (present(n)) then + lenseq = n + if (lenseq <= size(x)) then + result = x(:lenseq) + else if (lenseq > size(x)) then + result = [x, ((0.0_rk, 0.0_rk), i=1, lenseq - size(x))] + end if + else + lenseq = size(x) + result = x + end if + + !> Initialize FFT + lensav = 4*lenseq + 15 + allocate (wsave(lensav)) + call zffti(lenseq, wsave) + + !> Forward transformation + call zfftf(lenseq, result, wsave) + + end function fft_rk + +end submodule fftpack_fft diff --git a/examples/fftpack/native/fftpack_fftshift.f90 b/examples/fftpack/native/fftpack_fftshift.f90 new file mode 100644 index 000000000..69d0f414f --- /dev/null +++ b/examples/fftpack/native/fftpack_fftshift.f90 @@ -0,0 +1,23 @@ +submodule(fftpack) fftpack_fftshift + +contains + + !> Shifts zero-frequency component to center of spectrum for `complex` type. + pure module function fftshift_crk(x) result(result) + complex(kind=rk), intent(in) :: x(:) + complex(kind=rk), dimension(size(x)) :: result + + result = cshift(x, shift=-floor(0.5_rk*size(x))) + + end function fftshift_crk + + !> Shifts zero-frequency component to center of spectrum for `real` type. + pure module function fftshift_rrk(x) result(result) + real(kind=rk), intent(in) :: x(:) + real(kind=rk), dimension(size(x)) :: result + + result = cshift(x, shift=-floor(0.5_rk*size(x))) + + end function fftshift_rrk + +end submodule fftpack_fftshift diff --git a/examples/fftpack/native/fftpack_ifft.f90 b/examples/fftpack/native/fftpack_ifft.f90 new file mode 100644 index 000000000..680e64b42 --- /dev/null +++ b/examples/fftpack/native/fftpack_ifft.f90 @@ -0,0 +1,36 @@ +submodule(fftpack) fftpack_ifft + +contains + + !> Backward transform of a complex periodic sequence. + pure module function ifft_rk(x, n) result(result) + complex(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + complex(kind=rk), allocatable :: result(:) + + integer :: lenseq, lensav, i + real(kind=rk), allocatable :: wsave(:) + + if (present(n)) then + lenseq = n + if (lenseq <= size(x)) then + result = x(:lenseq) + else if (lenseq > size(x)) then + result = [x, ((0.0_rk, 0.0_rk), i=1, lenseq - size(x))] + end if + else + lenseq = size(x) + result = x + end if + + !> Initialize FFT + lensav = 4*lenseq + 15 + allocate (wsave(lensav)) + call zffti(lenseq, wsave) + + !> Backward transformation + call zfftb(lenseq, result, wsave) + + end function ifft_rk + +end submodule fftpack_ifft diff --git a/examples/fftpack/native/fftpack_ifftshift.f90 b/examples/fftpack/native/fftpack_ifftshift.f90 new file mode 100644 index 000000000..b36d2c83f --- /dev/null +++ b/examples/fftpack/native/fftpack_ifftshift.f90 @@ -0,0 +1,23 @@ +submodule(fftpack) fftpack_ifftshift + +contains + + !> Shifts zero-frequency component to beginning of spectrum for `complex` type. + pure module function ifftshift_crk(x) result(result) + complex(kind=rk), intent(in) :: x(:) + complex(kind=rk), dimension(size(x)) :: result + + result = cshift(x, shift=-ceiling(0.5_rk*size(x))) + + end function ifftshift_crk + + !> Shifts zero-frequency component to beginning of spectrum for `real` type. + pure module function ifftshift_rrk(x) result(result) + real(kind=rk), intent(in) :: x(:) + real(kind=rk), dimension(size(x)) :: result + + result = cshift(x, shift=-ceiling(0.5_rk*size(x))) + + end function ifftshift_rrk + +end submodule fftpack_ifftshift diff --git a/examples/fftpack/native/fftpack_irfft.f90 b/examples/fftpack/native/fftpack_irfft.f90 new file mode 100644 index 000000000..13cdb0535 --- /dev/null +++ b/examples/fftpack/native/fftpack_irfft.f90 @@ -0,0 +1,36 @@ +submodule(fftpack) fftpack_irfft + +contains + + !> Backward transform of a real periodic sequence. + pure module function irfft_rk(x, n) result(result) + real(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + real(kind=rk), allocatable :: result(:) + + integer :: lenseq, lensav, i + real(kind=rk), allocatable :: wsave(:) + + if (present(n)) then + lenseq = n + if (lenseq <= size(x)) then + result = x(:lenseq) + else if (lenseq > size(x)) then + result = [x, (0.0_rk, i=1, lenseq - size(x))] + end if + else + lenseq = size(x) + result = x + end if + + !> Initialize FFT + lensav = 2*lenseq + 15 + allocate (wsave(lensav)) + call dffti(lenseq, wsave) + + !> Backward transformation + call dfftb(lenseq, result, wsave) + + end function irfft_rk + +end submodule fftpack_irfft diff --git a/examples/fftpack/native/fftpack_rfft.f90 b/examples/fftpack/native/fftpack_rfft.f90 new file mode 100644 index 000000000..2d107670a --- /dev/null +++ b/examples/fftpack/native/fftpack_rfft.f90 @@ -0,0 +1,36 @@ +submodule(fftpack) fftpack_rfft + +contains + + !> Forward transform of a real periodic sequence. + pure module function rfft_rk(x, n) result(result) + real(kind=rk), intent(in) :: x(:) + integer, intent(in), optional :: n + real(kind=rk), allocatable :: result(:) + + integer :: lenseq, lensav, i + real(kind=rk), allocatable :: wsave(:) + + if (present(n)) then + lenseq = n + if (lenseq <= size(x)) then + result = x(:lenseq) + else if (lenseq > size(x)) then + result = [x, (0.0_rk, i=1, lenseq - size(x))] + end if + else + lenseq = size(x) + result = x + end if + + !> Initialize FFT + lensav = 2*lenseq + 15 + allocate (wsave(lensav)) + call dffti(lenseq, wsave) + + !> Forward transformation + call dfftf(lenseq, result, wsave) + + end function rfft_rk + +end submodule fftpack_rfft diff --git a/examples/fftpack/native/fftpack_utils.f90 b/examples/fftpack/native/fftpack_utils.f90 new file mode 100644 index 000000000..3ea491d1f --- /dev/null +++ b/examples/fftpack/native/fftpack_utils.f90 @@ -0,0 +1,60 @@ +submodule(fftpack) fftpack_utils + +contains + + !> Returns an integer array with the frequency values involved in the + !> performed FFT, ordered in the standard way (zero first, then positive + !> frequencies, then the negative ones). + pure module function fftfreq(n) result(out) + integer, intent(in) :: n + integer, dimension(n) :: out + integer :: i + + out(1) = 0 + if (n == 1) return + + if (mod(n, 2) == 0) then ! n even, smallest n = 2 + do i = 2, n/2 + out(i) = i-1 + end do + out(n/2+1) = -n/2 + do i = n/2+2, n ! only enters if n/2+2 <= n + out(i) = out(i-1) + 1 + end do + else ! n odd, smallest n = 3 + do i = 2, n/2+1 + out(i) = i-1 + end do + out(n/2+2) = -out(n/2+1) + do i = n/2+3, n ! only enters if n/2+3 <= n + out(i) = out(i-1) + 1 + end do + end if + end function fftfreq + + !> Returns an integer array with the frequency values involved in the + !> performed real FFT, ordered in the standard way (zero first, then + !> positive frequencies, then, if applicable, the negative one). + pure module function rfftfreq(n) result(out) + integer, intent(in) :: n + integer, dimension(n) :: out + integer :: i + + out(1) = 0 + if (n == 1) return + + if (mod(n,2) == 0) then ! n even, smallest n = 2 + do i = 2, n-2, 2 + out(i) = out(i-1) + 1 + out(i+1) = out(i) + end do + out(n) = -n/2 + else ! n odd, smallest n = 3 + do i = 2, n-1, 2 + out(i) = out(i-1) + 1 + out(i+1) = out(i) + end do + end if + end function rfftfreq + +end submodule fftpack_utils diff --git a/examples/fftpack/native/passb.f90 b/examples/fftpack/native/passb.f90 new file mode 100644 index 000000000..3a58494cf --- /dev/null +++ b/examples/fftpack/native/passb.f90 @@ -0,0 +1,110 @@ + subroutine passb(nac, ido, ip, l1, idl1, cc, c1, c2, ch, ch2, wa) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(out) :: nac + integer, intent(in) :: ido, ip, l1, idl1 + real(dp), intent(in) :: cc(ido, ip, l1), wa(*) + real(dp), intent(out) :: c1(ido, l1, ip), c2(idl1, ip), ch(ido, l1, ip) + real(dp), intent(inout) :: ch2(idl1, ip) + real(dp) :: wai, war + integer :: i, idij, idj, idl, idlj, idot, idp, & + ik, inc, ipp2, ipph, j, jc, k, l, lc + integer :: nt + idot = ido/2 + nt = ip*idl1 + ipp2 = ip + 2 + ipph = (ip + 1)/2 + idp = ip*ido +! + if (ido < l1) then + do concurrent(k=1:l1, j=2:ipph, i=1:ido) + jc = ipp2 - j + ch(i, k, j) = cc(i, j, k) + cc(i, jc, k) + ch(i, k, jc) = cc(i, j, k) - cc(i, jc, k) + end do + do concurrent(k=1:l1, i=1:ido) + ch(i, k, 1) = cc(i, 1, k) + end do + else + do concurrent(k=1:l1, j=2:ipph, i=1:ido) + jc = ipp2 - j + ch(i, k, j) = cc(i, j, k) + cc(i, jc, k) + ch(i, k, jc) = cc(i, j, k) - cc(i, jc, k) + end do + do concurrent(i=1:ido, k=1:l1) + ch(i, k, 1) = cc(i, 1, k) + end do + end if + idl = 2 - ido + inc = 0 + do l = 2, ipph + lc = ipp2 - l + idl = idl + ido + do concurrent(ik=1:idl1) + c2(ik, l) = ch2(ik, 1) + wa(idl - 1)*ch2(ik, 2) + c2(ik, lc) = wa(idl)*ch2(ik, ip) + end do + idlj = idl + inc = inc + ido + do j = 3, ipph + jc = ipp2 - j + idlj = idlj + inc + if (idlj > idp) idlj = idlj - idp + war = wa(idlj - 1) + wai = wa(idlj) + do concurrent(ik=1:idl1) + c2(ik, l) = c2(ik, l) + war*ch2(ik, j) + c2(ik, lc) = c2(ik, lc) + wai*ch2(ik, jc) + end do + end do + end do + do concurrent(ik=1:idl1, j=2:ipph) + ch2(ik, 1) = ch2(ik, 1) + ch2(ik, j) + end do + do concurrent(j=2:ipph, ik=2:idl1:2) + jc = ipp2 - j + ch2(ik - 1, j) = c2(ik - 1, j) - c2(ik, jc) + ch2(ik - 1, jc) = c2(ik - 1, j) + c2(ik, jc) + ch2(ik, j) = c2(ik, j) + c2(ik - 1, jc) + ch2(ik, jc) = c2(ik, j) - c2(ik - 1, jc) + end do + nac = 1 + if (ido == 2) return + nac = 0 + do concurrent(ik=1:idl1) + c2(ik, 1) = ch2(ik, 1) + end do + do concurrent(j=2:ip, k=1:l1) + c1(1, k, j) = ch(1, k, j) + c1(2, k, j) = ch(2, k, j) + end do + if (idot > l1) then + idj = 2 - ido + do j = 2, ip + idj = idj + ido + do k = 1, l1 + idij = idj + do i = 4, ido, 2 + idij = idij + 2 + c1(i - 1, k, j) = wa(idij - 1)*ch(i - 1, k, j) - wa(idij) & + *ch(i, k, j) + c1(i, k, j) = wa(idij - 1)*ch(i, k, j) + wa(idij) & + *ch(i - 1, k, j) + end do + end do + end do + return + end if + idij = 0 + do j = 2, ip + idij = idij + 2 + do i = 4, ido, 2 + idij = idij + 2 + do concurrent(k=1:l1) + c1(i - 1, k, j) = wa(idij - 1)*ch(i - 1, k, j) - wa(idij)*ch(i, k, j) + c1(i, k, j) = wa(idij - 1)*ch(i, k, j) + wa(idij)*ch(i - 1, k, j) + end do + end do + end do + return + end subroutine passb diff --git a/examples/fftpack/native/passb2.f90 b/examples/fftpack/native/passb2.f90 new file mode 100644 index 000000000..1fd83a87b --- /dev/null +++ b/examples/fftpack/native/passb2.f90 @@ -0,0 +1,26 @@ + subroutine passb2(ido, l1, cc, ch, wa1) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 2, l1), wa1(*) + real(dp), intent(out) :: ch(ido, l1, 2) + real(dp) :: ti2, tr2 + integer :: i, k + if (ido > 2) then + do concurrent(k=1:l1, i=2:ido:2) + ch(i - 1, k, 1) = cc(i - 1, 1, k) + cc(i - 1, 2, k) + tr2 = cc(i - 1, 1, k) - cc(i - 1, 2, k) + ch(i, k, 1) = cc(i, 1, k) + cc(i, 2, k) + ti2 = cc(i, 1, k) - cc(i, 2, k) + ch(i, k, 2) = wa1(i - 1)*ti2 + wa1(i)*tr2 + ch(i - 1, k, 2) = wa1(i - 1)*tr2 - wa1(i)*ti2 + end do + else + do concurrent(k=1:l1) + ch(1, k, 1) = cc(1, 1, k) + cc(1, 2, k) + ch(1, k, 2) = cc(1, 1, k) - cc(1, 2, k) + ch(2, k, 1) = cc(2, 1, k) + cc(2, 2, k) + ch(2, k, 2) = cc(2, 1, k) - cc(2, 2, k) + end do + end if + end subroutine passb2 diff --git a/examples/fftpack/native/passb3.f90 b/examples/fftpack/native/passb3.f90 new file mode 100644 index 000000000..b4fd8c4d1 --- /dev/null +++ b/examples/fftpack/native/passb3.f90 @@ -0,0 +1,47 @@ + subroutine passb3(ido, l1, cc, ch, wa1, wa2) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 3, l1), wa1(*), wa2(*) + real(dp), intent(out) :: ch(ido, l1, 3) + real(dp) :: ci2, ci3, cr2, cr3, di2, di3, & + dr2, dr3, ti2, tr2 + integer :: i, k + real(dp), parameter :: taur = -0.5_dp + real(dp), parameter :: taui = sqrt(3.0_dp)/2.0_dp + if (ido /= 2) then + do concurrent(k=1:l1, i=2:ido:2) + tr2 = cc(i - 1, 2, k) + cc(i - 1, 3, k) + cr2 = cc(i - 1, 1, k) + taur*tr2 + ch(i - 1, k, 1) = cc(i - 1, 1, k) + tr2 + ti2 = cc(i, 2, k) + cc(i, 3, k) + ci2 = cc(i, 1, k) + taur*ti2 + ch(i, k, 1) = cc(i, 1, k) + ti2 + cr3 = taui*(cc(i - 1, 2, k) - cc(i - 1, 3, k)) + ci3 = taui*(cc(i, 2, k) - cc(i, 3, k)) + dr2 = cr2 - ci3 + dr3 = cr2 + ci3 + di2 = ci2 + cr3 + di3 = ci2 - cr3 + ch(i, k, 2) = wa1(i - 1)*di2 + wa1(i)*dr2 + ch(i - 1, k, 2) = wa1(i - 1)*dr2 - wa1(i)*di2 + ch(i, k, 3) = wa2(i - 1)*di3 + wa2(i)*dr3 + ch(i - 1, k, 3) = wa2(i - 1)*dr3 - wa2(i)*di3 + end do + else + do concurrent(k=1:l1) + tr2 = cc(1, 2, k) + cc(1, 3, k) + cr2 = cc(1, 1, k) + taur*tr2 + ch(1, k, 1) = cc(1, 1, k) + tr2 + ti2 = cc(2, 2, k) + cc(2, 3, k) + ci2 = cc(2, 1, k) + taur*ti2 + ch(2, k, 1) = cc(2, 1, k) + ti2 + cr3 = taui*(cc(1, 2, k) - cc(1, 3, k)) + ci3 = taui*(cc(2, 2, k) - cc(2, 3, k)) + ch(1, k, 2) = cr2 - ci3 + ch(1, k, 3) = cr2 + ci3 + ch(2, k, 2) = ci2 + cr3 + ch(2, k, 3) = ci2 - cr3 + end do + end if + end subroutine passb3 diff --git a/examples/fftpack/native/passb4.f90 b/examples/fftpack/native/passb4.f90 new file mode 100644 index 000000000..294715a6c --- /dev/null +++ b/examples/fftpack/native/passb4.f90 @@ -0,0 +1,55 @@ + subroutine passb4(ido, l1, cc, ch, wa1, wa2, wa3) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 4, l1), wa1(*), wa2(*), wa3(*) + real(dp), intent(out) :: ch(ido, l1, 4) + real(dp) :: ci2, ci3, ci4, cr2, cr3, cr4, & + & ti1, ti2, ti3, ti4, tr1, tr2, tr3, tr4 + integer :: i, k + if (ido /= 2) then + do concurrent(k=1:l1, i=2:ido:2) + ti1 = cc(i, 1, k) - cc(i, 3, k) + ti2 = cc(i, 1, k) + cc(i, 3, k) + ti3 = cc(i, 2, k) + cc(i, 4, k) + tr4 = cc(i, 4, k) - cc(i, 2, k) + tr1 = cc(i - 1, 1, k) - cc(i - 1, 3, k) + tr2 = cc(i - 1, 1, k) + cc(i - 1, 3, k) + ti4 = cc(i - 1, 2, k) - cc(i - 1, 4, k) + tr3 = cc(i - 1, 2, k) + cc(i - 1, 4, k) + ch(i - 1, k, 1) = tr2 + tr3 + cr3 = tr2 - tr3 + ch(i, k, 1) = ti2 + ti3 + ci3 = ti2 - ti3 + cr2 = tr1 + tr4 + cr4 = tr1 - tr4 + ci2 = ti1 + ti4 + ci4 = ti1 - ti4 + ch(i - 1, k, 2) = wa1(i - 1)*cr2 - wa1(i)*ci2 + ch(i, k, 2) = wa1(i - 1)*ci2 + wa1(i)*cr2 + ch(i - 1, k, 3) = wa2(i - 1)*cr3 - wa2(i)*ci3 + ch(i, k, 3) = wa2(i - 1)*ci3 + wa2(i)*cr3 + ch(i - 1, k, 4) = wa3(i - 1)*cr4 - wa3(i)*ci4 + ch(i, k, 4) = wa3(i - 1)*ci4 + wa3(i)*cr4 + end do + else + do concurrent(k=1:l1) + ti1 = cc(2, 1, k) - cc(2, 3, k) + ti2 = cc(2, 1, k) + cc(2, 3, k) + tr4 = cc(2, 4, k) - cc(2, 2, k) + ti3 = cc(2, 2, k) + cc(2, 4, k) + tr1 = cc(1, 1, k) - cc(1, 3, k) + tr2 = cc(1, 1, k) + cc(1, 3, k) + ti4 = cc(1, 2, k) - cc(1, 4, k) + tr3 = cc(1, 2, k) + cc(1, 4, k) + ch(1, k, 1) = tr2 + tr3 + ch(1, k, 3) = tr2 - tr3 + ch(2, k, 1) = ti2 + ti3 + ch(2, k, 3) = ti2 - ti3 + ch(1, k, 2) = tr1 + tr4 + ch(1, k, 4) = tr1 - tr4 + ch(2, k, 2) = ti1 + ti4 + ch(2, k, 4) = ti1 - ti4 + end do + end if + end subroutine passb4 diff --git a/examples/fftpack/native/passb5.f90 b/examples/fftpack/native/passb5.f90 new file mode 100644 index 000000000..a39621f5c --- /dev/null +++ b/examples/fftpack/native/passb5.f90 @@ -0,0 +1,85 @@ + subroutine passb5(ido, l1, cc, ch, wa1, wa2, wa3, wa4) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 5, l1), wa1(*), wa2(*), wa3(*), wa4(*) + real(dp), intent(out) :: ch(ido, l1, 5) + real(dp) :: ci2, ci3, ci4, ci5, cr2, cr3, & + cr4, cr5, di2, di3, di4, di5, dr2, dr3, & + dr4, dr5 + real(dp) :: ti2, ti3, ti4, ti5, tr2, tr3, & + tr4, tr5 + integer :: i, k + real(dp), parameter :: pi = acos(-1.0_dp) + real(dp), parameter :: tr11 = cos(2.0_dp*pi/5.0_dp) + real(dp), parameter :: ti11 = sin(2.0_dp*pi/5.0_dp) + real(dp), parameter :: tr12 = cos(4.0_dp*pi/5.0_dp) + real(dp), parameter :: ti12 = sin(4.0_dp*pi/5.0_dp) + if (ido /= 2) then + do concurrent(k=1:l1, i=2:ido:2) + ti5 = cc(i, 2, k) - cc(i, 5, k) + ti2 = cc(i, 2, k) + cc(i, 5, k) + ti4 = cc(i, 3, k) - cc(i, 4, k) + ti3 = cc(i, 3, k) + cc(i, 4, k) + tr5 = cc(i - 1, 2, k) - cc(i - 1, 5, k) + tr2 = cc(i - 1, 2, k) + cc(i - 1, 5, k) + tr4 = cc(i - 1, 3, k) - cc(i - 1, 4, k) + tr3 = cc(i - 1, 3, k) + cc(i - 1, 4, k) + ch(i - 1, k, 1) = cc(i - 1, 1, k) + tr2 + tr3 + ch(i, k, 1) = cc(i, 1, k) + ti2 + ti3 + cr2 = cc(i - 1, 1, k) + tr11*tr2 + tr12*tr3 + ci2 = cc(i, 1, k) + tr11*ti2 + tr12*ti3 + cr3 = cc(i - 1, 1, k) + tr12*tr2 + tr11*tr3 + ci3 = cc(i, 1, k) + tr12*ti2 + tr11*ti3 + cr5 = ti11*tr5 + ti12*tr4 + ci5 = ti11*ti5 + ti12*ti4 + cr4 = ti12*tr5 - ti11*tr4 + ci4 = ti12*ti5 - ti11*ti4 + dr3 = cr3 - ci4 + dr4 = cr3 + ci4 + di3 = ci3 + cr4 + di4 = ci3 - cr4 + dr5 = cr2 + ci5 + dr2 = cr2 - ci5 + di5 = ci2 - cr5 + di2 = ci2 + cr5 + ch(i - 1, k, 2) = wa1(i - 1)*dr2 - wa1(i)*di2 + ch(i, k, 2) = wa1(i - 1)*di2 + wa1(i)*dr2 + ch(i - 1, k, 3) = wa2(i - 1)*dr3 - wa2(i)*di3 + ch(i, k, 3) = wa2(i - 1)*di3 + wa2(i)*dr3 + ch(i - 1, k, 4) = wa3(i - 1)*dr4 - wa3(i)*di4 + ch(i, k, 4) = wa3(i - 1)*di4 + wa3(i)*dr4 + ch(i - 1, k, 5) = wa4(i - 1)*dr5 - wa4(i)*di5 + ch(i, k, 5) = wa4(i - 1)*di5 + wa4(i)*dr5 + end do + else + do concurrent(k=1:l1) + ti5 = cc(2, 2, k) - cc(2, 5, k) + ti2 = cc(2, 2, k) + cc(2, 5, k) + ti4 = cc(2, 3, k) - cc(2, 4, k) + ti3 = cc(2, 3, k) + cc(2, 4, k) + tr5 = cc(1, 2, k) - cc(1, 5, k) + tr2 = cc(1, 2, k) + cc(1, 5, k) + tr4 = cc(1, 3, k) - cc(1, 4, k) + tr3 = cc(1, 3, k) + cc(1, 4, k) + ch(1, k, 1) = cc(1, 1, k) + tr2 + tr3 + ch(2, k, 1) = cc(2, 1, k) + ti2 + ti3 + cr2 = cc(1, 1, k) + tr11*tr2 + tr12*tr3 + ci2 = cc(2, 1, k) + tr11*ti2 + tr12*ti3 + cr3 = cc(1, 1, k) + tr12*tr2 + tr11*tr3 + ci3 = cc(2, 1, k) + tr12*ti2 + tr11*ti3 + cr5 = ti11*tr5 + ti12*tr4 + ci5 = ti11*ti5 + ti12*ti4 + cr4 = ti12*tr5 - ti11*tr4 + ci4 = ti12*ti5 - ti11*ti4 + ch(1, k, 2) = cr2 - ci5 + ch(1, k, 5) = cr2 + ci5 + ch(2, k, 2) = ci2 + cr5 + ch(2, k, 3) = ci3 + cr4 + ch(1, k, 3) = cr3 - ci4 + ch(1, k, 4) = cr3 + ci4 + ch(2, k, 4) = ci3 - cr4 + ch(2, k, 5) = ci2 - cr5 + end do + end if + end subroutine passb5 diff --git a/examples/fftpack/native/passf.f90 b/examples/fftpack/native/passf.f90 new file mode 100644 index 000000000..d78af30f4 --- /dev/null +++ b/examples/fftpack/native/passf.f90 @@ -0,0 +1,109 @@ + subroutine passf(nac, ido, ip, l1, idl1, cc, c1, c2, ch, ch2, wa) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(out) :: nac + integer, intent(in) :: ido, ip, l1, idl1 + real(dp), intent(in) :: cc(ido, ip, l1), wa(*) + real(dp), intent(out) :: c1(ido, l1, ip), c2(idl1, ip), ch(ido, l1, ip) + real(dp), intent(inout) :: ch2(idl1, ip) + real(dp) :: wai, war + integer :: i, idij, idj, idl, idlj, idot, idp, & + ik, inc, ipp2, ipph, j, jc, k, l, lc + integer :: nt + idot = ido/2 + nt = ip*idl1 + ipp2 = ip + 2 + ipph = (ip + 1)/2 + idp = ip*ido +! + if (ido < l1) then + do concurrent(k=1:l1, j=2:ipph, i=1:ido) + jc = ipp2 - j + ch(i, k, j) = cc(i, j, k) + cc(i, jc, k) + ch(i, k, jc) = cc(i, j, k) - cc(i, jc, k) + end do + do concurrent(k=1:l1, i=1:ido) + ch(i, k, 1) = cc(i, 1, k) + end do + else + do concurrent(k=1:l1, j=2:ipph, i=1:ido) + jc = ipp2 - j + ch(i, k, j) = cc(i, j, k) + cc(i, jc, k) + ch(i, k, jc) = cc(i, j, k) - cc(i, jc, k) + end do + do concurrent(i=1:ido, k=1:l1) + ch(i, k, 1) = cc(i, 1, k) + end do + end if + idl = 2 - ido + inc = 0 + do l = 2, ipph + lc = ipp2 - l + idl = idl + ido + do concurrent(ik=1:idl1) + c2(ik, l) = ch2(ik, 1) + wa(idl - 1)*ch2(ik, 2) + c2(ik, lc) = -wa(idl)*ch2(ik, ip) + end do + idlj = idl + inc = inc + ido + do j = 3, ipph + jc = ipp2 - j + idlj = idlj + inc + if (idlj > idp) idlj = idlj - idp + war = wa(idlj - 1) + wai = wa(idlj) + do concurrent(ik=1:idl1) + c2(ik, l) = c2(ik, l) + war*ch2(ik, j) + c2(ik, lc) = c2(ik, lc) - wai*ch2(ik, jc) + end do + end do + end do + do concurrent(ik=1:idl1, j=2:ipph) + ch2(ik, 1) = ch2(ik, 1) + ch2(ik, j) + end do + do concurrent(j=2:ipph, ik=2:idl1:2) + jc = ipp2 - j + ch2(ik - 1, j) = c2(ik - 1, j) - c2(ik, jc) + ch2(ik - 1, jc) = c2(ik - 1, j) + c2(ik, jc) + ch2(ik, j) = c2(ik, j) + c2(ik - 1, jc) + ch2(ik, jc) = c2(ik, j) - c2(ik - 1, jc) + end do + nac = 1 + if (ido == 2) return + nac = 0 + do concurrent(ik=1:idl1) + c2(ik, 1) = ch2(ik, 1) + end do + do concurrent(j=2:ip, k=1:l1) + c1(1, k, j) = ch(1, k, j) + c1(2, k, j) = ch(2, k, j) + end do + if (idot > l1) then + idj = 2 - ido + do j = 2, ip + idj = idj + ido + do k = 1, l1 + idij = idj + do i = 4, ido, 2 + idij = idij + 2 + c1(i - 1, k, j) = wa(idij - 1)*ch(i - 1, k, j) + wa(idij) & + *ch(i, k, j) + c1(i, k, j) = wa(idij - 1)*ch(i, k, j) - wa(idij) & + *ch(i - 1, k, j) + end do + end do + end do + else + idij = 0 + do j = 2, ip + idij = idij + 2 + do i = 4, ido, 2 + idij = idij + 2 + do concurrent(k=1:l1) + c1(i - 1, k, j) = wa(idij - 1)*ch(i - 1, k, j) + wa(idij)*ch(i, k, j) + c1(i, k, j) = wa(idij - 1)*ch(i, k, j) - wa(idij)*ch(i - 1, k, j) + end do + end do + end do + end if + end subroutine passf diff --git a/examples/fftpack/native/passf2.f90 b/examples/fftpack/native/passf2.f90 new file mode 100644 index 000000000..550d954a8 --- /dev/null +++ b/examples/fftpack/native/passf2.f90 @@ -0,0 +1,26 @@ + subroutine passf2(ido, l1, cc, ch, wa1) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 2, l1), wa1(*) + real(dp), intent(out) :: ch(ido, l1, 2) + real(dp) :: ti2, tr2 + integer :: i, k + if (ido > 2) then + do concurrent(k=1:l1, i=2:ido:2) + ch(i - 1, k, 1) = cc(i - 1, 1, k) + cc(i - 1, 2, k) + tr2 = cc(i - 1, 1, k) - cc(i - 1, 2, k) + ch(i, k, 1) = cc(i, 1, k) + cc(i, 2, k) + ti2 = cc(i, 1, k) - cc(i, 2, k) + ch(i, k, 2) = wa1(i - 1)*ti2 - wa1(i)*tr2 + ch(i - 1, k, 2) = wa1(i - 1)*tr2 + wa1(i)*ti2 + end do + else + do concurrent(k=1:l1) + ch(1, k, 1) = cc(1, 1, k) + cc(1, 2, k) + ch(1, k, 2) = cc(1, 1, k) - cc(1, 2, k) + ch(2, k, 1) = cc(2, 1, k) + cc(2, 2, k) + ch(2, k, 2) = cc(2, 1, k) - cc(2, 2, k) + end do + end if + end subroutine passf2 diff --git a/examples/fftpack/native/passf3.f90 b/examples/fftpack/native/passf3.f90 new file mode 100644 index 000000000..e987f36b0 --- /dev/null +++ b/examples/fftpack/native/passf3.f90 @@ -0,0 +1,47 @@ + subroutine passf3(ido, l1, cc, ch, wa1, wa2) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 3, l1), wa1(*), wa2(*) + real(dp), intent(out) :: ch(ido, l1, 3) + real(dp) :: ci2, ci3, cr2, cr3, di2, di3, & + & dr2, dr3, ti2, tr2 + integer :: i, k + real(dp), parameter :: taur = -0.5_dp + real(dp), parameter :: taui = -sqrt(3.0_dp)/2.0_dp + if (ido /= 2) then + do concurrent(k=1:l1, i=2:ido:2) + tr2 = cc(i - 1, 2, k) + cc(i - 1, 3, k) + cr2 = cc(i - 1, 1, k) + taur*tr2 + ch(i - 1, k, 1) = cc(i - 1, 1, k) + tr2 + ti2 = cc(i, 2, k) + cc(i, 3, k) + ci2 = cc(i, 1, k) + taur*ti2 + ch(i, k, 1) = cc(i, 1, k) + ti2 + cr3 = taui*(cc(i - 1, 2, k) - cc(i - 1, 3, k)) + ci3 = taui*(cc(i, 2, k) - cc(i, 3, k)) + dr2 = cr2 - ci3 + dr3 = cr2 + ci3 + di2 = ci2 + cr3 + di3 = ci2 - cr3 + ch(i, k, 2) = wa1(i - 1)*di2 - wa1(i)*dr2 + ch(i - 1, k, 2) = wa1(i - 1)*dr2 + wa1(i)*di2 + ch(i, k, 3) = wa2(i - 1)*di3 - wa2(i)*dr3 + ch(i - 1, k, 3) = wa2(i - 1)*dr3 + wa2(i)*di3 + end do + else + do concurrent(k=1:l1) + tr2 = cc(1, 2, k) + cc(1, 3, k) + cr2 = cc(1, 1, k) + taur*tr2 + ch(1, k, 1) = cc(1, 1, k) + tr2 + ti2 = cc(2, 2, k) + cc(2, 3, k) + ci2 = cc(2, 1, k) + taur*ti2 + ch(2, k, 1) = cc(2, 1, k) + ti2 + cr3 = taui*(cc(1, 2, k) - cc(1, 3, k)) + ci3 = taui*(cc(2, 2, k) - cc(2, 3, k)) + ch(1, k, 2) = cr2 - ci3 + ch(1, k, 3) = cr2 + ci3 + ch(2, k, 2) = ci2 + cr3 + ch(2, k, 3) = ci2 - cr3 + end do + end if + end subroutine passf3 diff --git a/examples/fftpack/native/passf4.f90 b/examples/fftpack/native/passf4.f90 new file mode 100644 index 000000000..483dd1179 --- /dev/null +++ b/examples/fftpack/native/passf4.f90 @@ -0,0 +1,55 @@ + subroutine passf4(ido, l1, cc, ch, wa1, wa2, wa3) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 4, l1), wa1(*), wa2(*), wa3(*) + real(dp), intent(out) :: ch(ido, l1, 4) + real(dp) :: ci2, ci3, ci4, cr2, cr3, cr4, & + & ti1, ti2, ti3, ti4, tr1, tr2, tr3, tr4 + integer :: i, k + if (ido /= 2) then + do concurrent(k=1:l1, i=2:ido:2) + ti1 = cc(i, 1, k) - cc(i, 3, k) + ti2 = cc(i, 1, k) + cc(i, 3, k) + ti3 = cc(i, 2, k) + cc(i, 4, k) + tr4 = cc(i, 2, k) - cc(i, 4, k) + tr1 = cc(i - 1, 1, k) - cc(i - 1, 3, k) + tr2 = cc(i - 1, 1, k) + cc(i - 1, 3, k) + ti4 = cc(i - 1, 4, k) - cc(i - 1, 2, k) + tr3 = cc(i - 1, 2, k) + cc(i - 1, 4, k) + ch(i - 1, k, 1) = tr2 + tr3 + cr3 = tr2 - tr3 + ch(i, k, 1) = ti2 + ti3 + ci3 = ti2 - ti3 + cr2 = tr1 + tr4 + cr4 = tr1 - tr4 + ci2 = ti1 + ti4 + ci4 = ti1 - ti4 + ch(i - 1, k, 2) = wa1(i - 1)*cr2 + wa1(i)*ci2 + ch(i, k, 2) = wa1(i - 1)*ci2 - wa1(i)*cr2 + ch(i - 1, k, 3) = wa2(i - 1)*cr3 + wa2(i)*ci3 + ch(i, k, 3) = wa2(i - 1)*ci3 - wa2(i)*cr3 + ch(i - 1, k, 4) = wa3(i - 1)*cr4 + wa3(i)*ci4 + ch(i, k, 4) = wa3(i - 1)*ci4 - wa3(i)*cr4 + end do + else + do concurrent(k=1:l1) + ti1 = cc(2, 1, k) - cc(2, 3, k) + ti2 = cc(2, 1, k) + cc(2, 3, k) + tr4 = cc(2, 2, k) - cc(2, 4, k) + ti3 = cc(2, 2, k) + cc(2, 4, k) + tr1 = cc(1, 1, k) - cc(1, 3, k) + tr2 = cc(1, 1, k) + cc(1, 3, k) + ti4 = cc(1, 4, k) - cc(1, 2, k) + tr3 = cc(1, 2, k) + cc(1, 4, k) + ch(1, k, 1) = tr2 + tr3 + ch(1, k, 3) = tr2 - tr3 + ch(2, k, 1) = ti2 + ti3 + ch(2, k, 3) = ti2 - ti3 + ch(1, k, 2) = tr1 + tr4 + ch(1, k, 4) = tr1 - tr4 + ch(2, k, 2) = ti1 + ti4 + ch(2, k, 4) = ti1 - ti4 + end do + end if + end subroutine passf4 diff --git a/examples/fftpack/native/passf5.f90 b/examples/fftpack/native/passf5.f90 new file mode 100644 index 000000000..49fc67db5 --- /dev/null +++ b/examples/fftpack/native/passf5.f90 @@ -0,0 +1,85 @@ + subroutine passf5(ido, l1, cc, ch, wa1, wa2, wa3, wa4) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 5, l1), wa1(*), wa2(*), wa3(*), wa4(*) + real(dp), intent(out) :: ch(ido, l1, 5) + real(dp) :: ci2, ci3, ci4, ci5, cr2, cr3, & + cr4, cr5, di2, di3, di4, di5, dr2, dr3, & + dr4, dr5 + real(dp) :: ti2, ti3, ti4, ti5, tr2, tr3, & + tr4, tr5 + integer :: i, k + real(dp), parameter :: pi = acos(-1.0_dp) + real(dp), parameter :: tr11 = cos(2.0_dp*pi/5.0_dp) + real(dp), parameter :: ti11 = -sin(2.0_dp*pi/5.0_dp) + real(dp), parameter :: tr12 = cos(4.0_dp*pi/5.0_dp) + real(dp), parameter :: ti12 = -sin(4.0_dp*pi/5.0_dp) + if (ido /= 2) then + do concurrent(k=1:l1, i=2:ido:2) + ti5 = cc(i, 2, k) - cc(i, 5, k) + ti2 = cc(i, 2, k) + cc(i, 5, k) + ti4 = cc(i, 3, k) - cc(i, 4, k) + ti3 = cc(i, 3, k) + cc(i, 4, k) + tr5 = cc(i - 1, 2, k) - cc(i - 1, 5, k) + tr2 = cc(i - 1, 2, k) + cc(i - 1, 5, k) + tr4 = cc(i - 1, 3, k) - cc(i - 1, 4, k) + tr3 = cc(i - 1, 3, k) + cc(i - 1, 4, k) + ch(i - 1, k, 1) = cc(i - 1, 1, k) + tr2 + tr3 + ch(i, k, 1) = cc(i, 1, k) + ti2 + ti3 + cr2 = cc(i - 1, 1, k) + tr11*tr2 + tr12*tr3 + ci2 = cc(i, 1, k) + tr11*ti2 + tr12*ti3 + cr3 = cc(i - 1, 1, k) + tr12*tr2 + tr11*tr3 + ci3 = cc(i, 1, k) + tr12*ti2 + tr11*ti3 + cr5 = ti11*tr5 + ti12*tr4 + ci5 = ti11*ti5 + ti12*ti4 + cr4 = ti12*tr5 - ti11*tr4 + ci4 = ti12*ti5 - ti11*ti4 + dr3 = cr3 - ci4 + dr4 = cr3 + ci4 + di3 = ci3 + cr4 + di4 = ci3 - cr4 + dr5 = cr2 + ci5 + dr2 = cr2 - ci5 + di5 = ci2 - cr5 + di2 = ci2 + cr5 + ch(i - 1, k, 2) = wa1(i - 1)*dr2 + wa1(i)*di2 + ch(i, k, 2) = wa1(i - 1)*di2 - wa1(i)*dr2 + ch(i - 1, k, 3) = wa2(i - 1)*dr3 + wa2(i)*di3 + ch(i, k, 3) = wa2(i - 1)*di3 - wa2(i)*dr3 + ch(i - 1, k, 4) = wa3(i - 1)*dr4 + wa3(i)*di4 + ch(i, k, 4) = wa3(i - 1)*di4 - wa3(i)*dr4 + ch(i - 1, k, 5) = wa4(i - 1)*dr5 + wa4(i)*di5 + ch(i, k, 5) = wa4(i - 1)*di5 - wa4(i)*dr5 + end do + else + do concurrent(k=1:l1) + ti5 = cc(2, 2, k) - cc(2, 5, k) + ti2 = cc(2, 2, k) + cc(2, 5, k) + ti4 = cc(2, 3, k) - cc(2, 4, k) + ti3 = cc(2, 3, k) + cc(2, 4, k) + tr5 = cc(1, 2, k) - cc(1, 5, k) + tr2 = cc(1, 2, k) + cc(1, 5, k) + tr4 = cc(1, 3, k) - cc(1, 4, k) + tr3 = cc(1, 3, k) + cc(1, 4, k) + ch(1, k, 1) = cc(1, 1, k) + tr2 + tr3 + ch(2, k, 1) = cc(2, 1, k) + ti2 + ti3 + cr2 = cc(1, 1, k) + tr11*tr2 + tr12*tr3 + ci2 = cc(2, 1, k) + tr11*ti2 + tr12*ti3 + cr3 = cc(1, 1, k) + tr12*tr2 + tr11*tr3 + ci3 = cc(2, 1, k) + tr12*ti2 + tr11*ti3 + cr5 = ti11*tr5 + ti12*tr4 + ci5 = ti11*ti5 + ti12*ti4 + cr4 = ti12*tr5 - ti11*tr4 + ci4 = ti12*ti5 - ti11*ti4 + ch(1, k, 2) = cr2 - ci5 + ch(1, k, 5) = cr2 + ci5 + ch(2, k, 2) = ci2 + cr5 + ch(2, k, 3) = ci3 + cr4 + ch(1, k, 3) = cr3 - ci4 + ch(1, k, 4) = cr3 + ci4 + ch(2, k, 4) = ci3 - cr4 + ch(2, k, 5) = ci2 - cr5 + end do + end if + end subroutine passf5 diff --git a/examples/fftpack/native/radb2.f90 b/examples/fftpack/native/radb2.f90 new file mode 100644 index 000000000..7fb4c5546 --- /dev/null +++ b/examples/fftpack/native/radb2.f90 @@ -0,0 +1,31 @@ + subroutine radb2(ido, l1, cc, ch, wa1) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 2, l1), wa1(*) + real(dp), intent(out) :: ch(ido, l1, 2) + real(dp) :: ti2, tr2 + integer :: i, ic, idp2, k + do concurrent(k=1:l1) + ch(1, k, 1) = cc(1, 1, k) + cc(ido, 2, k) + ch(1, k, 2) = cc(1, 1, k) - cc(ido, 2, k) + end do + if (ido < 2) return + if (ido /= 2) then + idp2 = ido + 2 + do concurrent(k=1:l1, i=3:ido:2) + ic = idp2 - i + ch(i - 1, k, 1) = cc(i - 1, 1, k) + cc(ic - 1, 2, k) + tr2 = cc(i - 1, 1, k) - cc(ic - 1, 2, k) + ch(i, k, 1) = cc(i, 1, k) - cc(ic, 2, k) + ti2 = cc(i, 1, k) + cc(ic, 2, k) + ch(i - 1, k, 2) = wa1(i - 2)*tr2 - wa1(i - 1)*ti2 + ch(i, k, 2) = wa1(i - 2)*ti2 + wa1(i - 1)*tr2 + end do + if (mod(ido, 2) == 1) return + end if + do concurrent(k=1:l1) + ch(ido, k, 1) = cc(ido, 1, k) + cc(ido, 1, k) + ch(ido, k, 2) = -(cc(1, 2, k) + cc(1, 2, k)) + end do + end subroutine radb2 diff --git a/examples/fftpack/native/radb3.f90 b/examples/fftpack/native/radb3.f90 new file mode 100644 index 000000000..6b9cba686 --- /dev/null +++ b/examples/fftpack/native/radb3.f90 @@ -0,0 +1,41 @@ + subroutine radb3(ido, l1, cc, ch, wa1, wa2) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 3, l1), wa1(*), wa2(*) + real(dp), intent(out) :: ch(ido, l1, 3) + real(dp) :: ci2, ci3, cr2, cr3, di2, di3, & + dr2, dr3, ti2, tr2 + integer :: i, ic, idp2, k + real(dp), parameter :: taur = -0.5_dp + real(dp), parameter :: taui = sqrt(3.0_dp)/2.0_dp + do concurrent(k=1:l1) + tr2 = cc(ido, 2, k) + cc(ido, 2, k) + cr2 = cc(1, 1, k) + taur*tr2 + ch(1, k, 1) = cc(1, 1, k) + tr2 + ci3 = taui*(cc(1, 3, k) + cc(1, 3, k)) + ch(1, k, 2) = cr2 - ci3 + ch(1, k, 3) = cr2 + ci3 + end do + if (ido == 1) return + idp2 = ido + 2 + do concurrent(k=1:l1, i=3:ido:2) + ic = idp2 - i + tr2 = cc(i - 1, 3, k) + cc(ic - 1, 2, k) + cr2 = cc(i - 1, 1, k) + taur*tr2 + ch(i - 1, k, 1) = cc(i - 1, 1, k) + tr2 + ti2 = cc(i, 3, k) - cc(ic, 2, k) + ci2 = cc(i, 1, k) + taur*ti2 + ch(i, k, 1) = cc(i, 1, k) + ti2 + cr3 = taui*(cc(i - 1, 3, k) - cc(ic - 1, 2, k)) + ci3 = taui*(cc(i, 3, k) + cc(ic, 2, k)) + dr2 = cr2 - ci3 + dr3 = cr2 + ci3 + di2 = ci2 + cr3 + di3 = ci2 - cr3 + ch(i - 1, k, 2) = wa1(i - 2)*dr2 - wa1(i - 1)*di2 + ch(i, k, 2) = wa1(i - 2)*di2 + wa1(i - 1)*dr2 + ch(i - 1, k, 3) = wa2(i - 2)*dr3 - wa2(i - 1)*di3 + ch(i, k, 3) = wa2(i - 2)*di3 + wa2(i - 1)*dr3 + end do + end subroutine radb3 diff --git a/examples/fftpack/native/radb4.f90 b/examples/fftpack/native/radb4.f90 new file mode 100644 index 000000000..4291e7585 --- /dev/null +++ b/examples/fftpack/native/radb4.f90 @@ -0,0 +1,62 @@ + subroutine radb4(ido, l1, cc, ch, wa1, wa2, wa3) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 4, l1), wa1(*), wa2(*), wa3(*) + real(dp), intent(out) :: ch(ido, l1, 4) + real(dp) :: ci2, ci3, ci4, cr2, cr3, cr4, & + ti1, ti2, ti3, ti4, tr1, tr2, tr3, & + tr4 + integer :: i, ic, idp2, k + real(dp), parameter :: sqrt2 = sqrt(2.0_dp) + do concurrent(k=1:l1) + tr1 = cc(1, 1, k) - cc(ido, 4, k) + tr2 = cc(1, 1, k) + cc(ido, 4, k) + tr3 = cc(ido, 2, k) + cc(ido, 2, k) + tr4 = cc(1, 3, k) + cc(1, 3, k) + ch(1, k, 1) = tr2 + tr3 + ch(1, k, 2) = tr1 - tr4 + ch(1, k, 3) = tr2 - tr3 + ch(1, k, 4) = tr1 + tr4 + end do + if (ido < 2) return + if (ido /= 2) then + idp2 = ido + 2 + do concurrent(k=1:l1, i=3:ido:2) + ic = idp2 - i + ti1 = cc(i, 1, k) + cc(ic, 4, k) + ti2 = cc(i, 1, k) - cc(ic, 4, k) + ti3 = cc(i, 3, k) - cc(ic, 2, k) + tr4 = cc(i, 3, k) + cc(ic, 2, k) + tr1 = cc(i - 1, 1, k) - cc(ic - 1, 4, k) + tr2 = cc(i - 1, 1, k) + cc(ic - 1, 4, k) + ti4 = cc(i - 1, 3, k) - cc(ic - 1, 2, k) + tr3 = cc(i - 1, 3, k) + cc(ic - 1, 2, k) + ch(i - 1, k, 1) = tr2 + tr3 + cr3 = tr2 - tr3 + ch(i, k, 1) = ti2 + ti3 + ci3 = ti2 - ti3 + cr2 = tr1 - tr4 + cr4 = tr1 + tr4 + ci2 = ti1 + ti4 + ci4 = ti1 - ti4 + ch(i - 1, k, 2) = wa1(i - 2)*cr2 - wa1(i - 1)*ci2 + ch(i, k, 2) = wa1(i - 2)*ci2 + wa1(i - 1)*cr2 + ch(i - 1, k, 3) = wa2(i - 2)*cr3 - wa2(i - 1)*ci3 + ch(i, k, 3) = wa2(i - 2)*ci3 + wa2(i - 1)*cr3 + ch(i - 1, k, 4) = wa3(i - 2)*cr4 - wa3(i - 1)*ci4 + ch(i, k, 4) = wa3(i - 2)*ci4 + wa3(i - 1)*cr4 + end do + if (mod(ido, 2) == 1) return + end if + do concurrent(k=1:l1) + ti1 = cc(1, 2, k) + cc(1, 4, k) + ti2 = cc(1, 4, k) - cc(1, 2, k) + tr1 = cc(ido, 1, k) - cc(ido, 3, k) + tr2 = cc(ido, 1, k) + cc(ido, 3, k) + ch(ido, k, 1) = tr2 + tr2 + ch(ido, k, 2) = sqrt2*(tr1 - ti1) + ch(ido, k, 3) = ti2 + ti2 + ch(ido, k, 4) = -sqrt2*(tr1 + ti1) + end do + end subroutine radb4 diff --git a/examples/fftpack/native/radb5.f90 b/examples/fftpack/native/radb5.f90 new file mode 100644 index 000000000..c4de1f6bd --- /dev/null +++ b/examples/fftpack/native/radb5.f90 @@ -0,0 +1,72 @@ + subroutine radb5(ido, l1, cc, ch, wa1, wa2, wa3, wa4) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, 5, l1), wa1(*), wa2(*), wa3(*), wa4(*) + real(dp), intent(out) :: ch(ido, l1, 5) + real(dp) :: ci2, ci3, ci4, ci5, cr2, cr3, & + cr4, cr5, di2, di3, di4, di5, dr2, dr3, & + dr4, dr5 + real(dp) :: ti2, ti3, ti4, ti5, tr2, tr3, & + tr4, tr5 + integer :: i, ic, idp2, k + real(dp), parameter :: pi = acos(-1.0_dp) + real(dp), parameter :: tr11 = cos(2.0_dp*pi/5.0_dp) + real(dp), parameter :: ti11 = sin(2.0_dp*pi/5.0_dp) + real(dp), parameter :: tr12 = cos(4.0_dp*pi/5.0_dp) + real(dp), parameter :: ti12 = sin(4.0_dp*pi/5.0_dp) + do concurrent(k=1:l1) + ti5 = cc(1, 3, k) + cc(1, 3, k) + ti4 = cc(1, 5, k) + cc(1, 5, k) + tr2 = cc(ido, 2, k) + cc(ido, 2, k) + tr3 = cc(ido, 4, k) + cc(ido, 4, k) + ch(1, k, 1) = cc(1, 1, k) + tr2 + tr3 + cr2 = cc(1, 1, k) + tr11*tr2 + tr12*tr3 + cr3 = cc(1, 1, k) + tr12*tr2 + tr11*tr3 + ci5 = ti11*ti5 + ti12*ti4 + ci4 = ti12*ti5 - ti11*ti4 + ch(1, k, 2) = cr2 - ci5 + ch(1, k, 3) = cr3 - ci4 + ch(1, k, 4) = cr3 + ci4 + ch(1, k, 5) = cr2 + ci5 + end do + if (ido == 1) return + idp2 = ido + 2 + do concurrent(k=1:l1, i=3:ido:2) + ic = idp2 - i + ti5 = cc(i, 3, k) + cc(ic, 2, k) + ti2 = cc(i, 3, k) - cc(ic, 2, k) + ti4 = cc(i, 5, k) + cc(ic, 4, k) + ti3 = cc(i, 5, k) - cc(ic, 4, k) + tr5 = cc(i - 1, 3, k) - cc(ic - 1, 2, k) + tr2 = cc(i - 1, 3, k) + cc(ic - 1, 2, k) + tr4 = cc(i - 1, 5, k) - cc(ic - 1, 4, k) + tr3 = cc(i - 1, 5, k) + cc(ic - 1, 4, k) + ch(i - 1, k, 1) = cc(i - 1, 1, k) + tr2 + tr3 + ch(i, k, 1) = cc(i, 1, k) + ti2 + ti3 + cr2 = cc(i - 1, 1, k) + tr11*tr2 + tr12*tr3 + ci2 = cc(i, 1, k) + tr11*ti2 + tr12*ti3 + cr3 = cc(i - 1, 1, k) + tr12*tr2 + tr11*tr3 + ci3 = cc(i, 1, k) + tr12*ti2 + tr11*ti3 + cr5 = ti11*tr5 + ti12*tr4 + ci5 = ti11*ti5 + ti12*ti4 + cr4 = ti12*tr5 - ti11*tr4 + ci4 = ti12*ti5 - ti11*ti4 + dr3 = cr3 - ci4 + dr4 = cr3 + ci4 + di3 = ci3 + cr4 + di4 = ci3 - cr4 + dr5 = cr2 + ci5 + dr2 = cr2 - ci5 + di5 = ci2 - cr5 + di2 = ci2 + cr5 + ch(i - 1, k, 2) = wa1(i - 2)*dr2 - wa1(i - 1)*di2 + ch(i, k, 2) = wa1(i - 2)*di2 + wa1(i - 1)*dr2 + ch(i - 1, k, 3) = wa2(i - 2)*dr3 - wa2(i - 1)*di3 + ch(i, k, 3) = wa2(i - 2)*di3 + wa2(i - 1)*dr3 + ch(i - 1, k, 4) = wa3(i - 2)*dr4 - wa3(i - 1)*di4 + ch(i, k, 4) = wa3(i - 2)*di4 + wa3(i - 1)*dr4 + ch(i - 1, k, 5) = wa4(i - 2)*dr5 - wa4(i - 1)*di5 + ch(i, k, 5) = wa4(i - 2)*di5 + wa4(i - 1)*dr5 + end do + end subroutine radb5 diff --git a/examples/fftpack/native/radbg.f90 b/examples/fftpack/native/radbg.f90 new file mode 100644 index 000000000..28f28c751 --- /dev/null +++ b/examples/fftpack/native/radbg.f90 @@ -0,0 +1,147 @@ + subroutine radbg(ido, ip, l1, idl1, cc, c1, c2, ch, ch2, wa) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, ip, l1, idl1 + real(dp), intent(in) :: cc(ido, ip, l1), wa(*) + real(dp), intent(inout) :: c1(ido, l1, ip), ch2(idl1, ip) + real(dp), intent(out) :: c2(idl1, ip), ch(ido, l1, ip) + real(dp) :: ai1, ai2, ar1, ar1h, ar2, ar2h, arg, & + dc2, dcp, ds2, dsp + integer :: i, ic, idij, idp2, ik, ipp2, & + ipph, is, j, j2, jc, k, l, lc, nbd + real(dp), parameter :: tpi = 2*acos(-1.0_dp) ! 2 * pi + arg = tpi/real(ip, dp) + dcp = cos(arg) + dsp = sin(arg) + idp2 = ido + 2 + nbd = (ido - 1)/2 + ipp2 = ip + 2 + ipph = (ip + 1)/2 + if (ido < l1) then + do concurrent(k=1:l1, i=1:ido) + ch(i, k, 1) = cc(i, 1, k) + end do + else + do concurrent(k=1:l1, i=1:ido) + ch(i, k, 1) = cc(i, 1, k) + end do + end if + do concurrent(k=1:l1, j=2:ipph) + jc = ipp2 - j + j2 = j + j + ch(1, k, j) = cc(ido, j2 - 2, k) + cc(ido, j2 - 2, k) + ch(1, k, jc) = cc(1, j2 - 1, k) + cc(1, j2 - 1, k) + end do + if (ido /= 1) then + if (nbd < l1) then + do concurrent(k=1:l1, j=2:ipph, i=3:ido:2) + jc = ipp2 - j + ic = idp2 - i + ch(i - 1, k, j) = cc(i - 1, 2*j - 1, k) + cc(ic - 1, 2*j - 2, k) + ch(i - 1, k, jc) = cc(i - 1, 2*j - 1, k) - cc(ic - 1, 2*j - 2, k) + ch(i, k, j) = cc(i, 2*j - 1, k) - cc(ic, 2*j - 2, k) + ch(i, k, jc) = cc(i, 2*j - 1, k) + cc(ic, 2*j - 2, k) + end do + else + do concurrent(k=1:l1, j=2:ipph, i=3:ido:2) + jc = ipp2 - j + ic = idp2 - i + ch(i - 1, k, j) = cc(i - 1, 2*j - 1, k) + cc(ic - 1, 2*j - 2, k) + ch(i - 1, k, jc) = cc(i - 1, 2*j - 1, k) - cc(ic - 1, 2*j - 2, k) + ch(i, k, j) = cc(i, 2*j - 1, k) - cc(ic, 2*j - 2, k) + ch(i, k, jc) = cc(i, 2*j - 1, k) + cc(ic, 2*j - 2, k) + end do + end if + end if + ar1 = 1.0_dp + ai1 = 0.0_dp + do l = 2, ipph + lc = ipp2 - l + ar1h = dcp*ar1 - dsp*ai1 + ai1 = dcp*ai1 + dsp*ar1 + ar1 = ar1h + do concurrent(ik=1:idl1) + c2(ik, l) = ch2(ik, 1) + ar1*ch2(ik, 2) + c2(ik, lc) = ai1*ch2(ik, ip) + end do + dc2 = ar1 + ds2 = ai1 + ar2 = ar1 + ai2 = ai1 + do j = 3, ipph + jc = ipp2 - j + ar2h = dc2*ar2 - ds2*ai2 + ai2 = dc2*ai2 + ds2*ar2 + ar2 = ar2h + do concurrent(ik=1:idl1) + c2(ik, l) = c2(ik, l) + ar2*ch2(ik, j) + c2(ik, lc) = c2(ik, lc) + ai2*ch2(ik, jc) + end do + end do + end do + do concurrent(ik=1:idl1, j=2:ipph) + ch2(ik, 1) = ch2(ik, 1) + ch2(ik, j) + end do + do concurrent(j=2:ipph, k=1:l1) + jc = ipp2 - j + ch(1, k, j) = c1(1, k, j) - c1(1, k, jc) + ch(1, k, jc) = c1(1, k, j) + c1(1, k, jc) + end do + if (ido /= 1) then + if (nbd < l1) then + do concurrent(j=2:ipph, k=1:l1, i=3:ido:2) + jc = ipp2 - j + ch(i - 1, k, j) = c1(i - 1, k, j) - c1(i, k, jc) + ch(i - 1, k, jc) = c1(i - 1, k, j) + c1(i, k, jc) + ch(i, k, j) = c1(i, k, j) + c1(i - 1, k, jc) + ch(i, k, jc) = c1(i, k, j) - c1(i - 1, k, jc) + end do + else + do concurrent(j=2:ipph, k=1:l1, i=3:ido:2) + jc = ipp2 - j + ch(i - 1, k, j) = c1(i - 1, k, j) - c1(i, k, jc) + ch(i - 1, k, jc) = c1(i - 1, k, j) + c1(i, k, jc) + ch(i, k, j) = c1(i, k, j) + c1(i - 1, k, jc) + ch(i, k, jc) = c1(i, k, j) - c1(i - 1, k, jc) + end do + end if + end if + if (ido == 1) return + do concurrent(ik=1:idl1) + c2(ik, 1) = ch2(ik, 1) + end do + do concurrent(j=2:ip, k=1:l1) + c1(1, k, j) = ch(1, k, j) + end do + if (nbd > l1) then + is = -ido + do j = 2, ip + is = is + ido + do k = 1, l1 + idij = is + do i = 3, ido, 2 + idij = idij + 2 + c1(i - 1, k, j) = wa(idij - 1)*ch(i - 1, k, j) - wa(idij) & + *ch(i, k, j) + c1(i, k, j) = wa(idij - 1)*ch(i, k, j) + wa(idij) & + *ch(i - 1, k, j) + end do + end do + end do + else + is = -ido + do j = 2, ip + is = is + ido + idij = is + do i = 3, ido, 2 + idij = idij + 2 + do k = 1, l1 + c1(i - 1, k, j) = wa(idij - 1)*ch(i - 1, k, j) - wa(idij) & + *ch(i, k, j) + c1(i, k, j) = wa(idij - 1)*ch(i, k, j) + wa(idij) & + *ch(i - 1, k, j) + end do + end do + end do + end if + end subroutine radbg diff --git a/examples/fftpack/native/radf2.f90 b/examples/fftpack/native/radf2.f90 new file mode 100644 index 000000000..10d6343e4 --- /dev/null +++ b/examples/fftpack/native/radf2.f90 @@ -0,0 +1,31 @@ + subroutine radf2(ido, l1, cc, ch, wa1) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, l1, 2), wa1(*) + real(dp), intent(out) :: ch(ido, 2, l1) + real(dp) :: ti2, tr2 + integer :: i, ic, idp2, k + do concurrent(k=1:l1) + ch(1, 1, k) = cc(1, k, 1) + cc(1, k, 2) + ch(ido, 2, k) = cc(1, k, 1) - cc(1, k, 2) + end do + if (ido < 2) return + if (ido /= 2) then + idp2 = ido + 2 + do concurrent(k=1:l1, i=3:ido:2) + ic = idp2 - i + tr2 = wa1(i - 2)*cc(i - 1, k, 2) + wa1(i - 1)*cc(i, k, 2) + ti2 = wa1(i - 2)*cc(i, k, 2) - wa1(i - 1)*cc(i - 1, k, 2) + ch(i, 1, k) = cc(i, k, 1) + ti2 + ch(ic, 2, k) = ti2 - cc(i, k, 1) + ch(i - 1, 1, k) = cc(i - 1, k, 1) + tr2 + ch(ic - 1, 2, k) = cc(i - 1, k, 1) - tr2 + end do + if (mod(ido, 2) == 1) return + end if + do concurrent(k=1:l1) + ch(1, 2, k) = -cc(ido, k, 2) + ch(ido, 1, k) = cc(ido, k, 1) + end do + end subroutine radf2 diff --git a/examples/fftpack/native/radf3.f90 b/examples/fftpack/native/radf3.f90 new file mode 100644 index 000000000..cc27348ef --- /dev/null +++ b/examples/fftpack/native/radf3.f90 @@ -0,0 +1,40 @@ + subroutine radf3(ido, l1, cc, ch, wa1, wa2) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, l1, 3), wa1(*), wa2(*) + real(dp), intent(out) :: ch(ido, 3, l1) + real(dp) :: ci2, cr2, di2, di3, dr2, dr3, & + ti2, ti3, tr2, tr3 + integer :: i, ic, idp2, k + real(dp), parameter :: taur = -0.5_dp + ! note: original comment said this was -sqrt(3)/2 but value was 0.86602540378443864676d0 + real(dp), parameter :: taui = sqrt(3.0_dp)/2.0_dp + do concurrent(k=1:l1) + cr2 = cc(1, k, 2) + cc(1, k, 3) + ch(1, 1, k) = cc(1, k, 1) + cr2 + ch(1, 3, k) = taui*(cc(1, k, 3) - cc(1, k, 2)) + ch(ido, 2, k) = cc(1, k, 1) + taur*cr2 + end do + if (ido == 1) return + idp2 = ido + 2 + do concurrent(k=1:l1, i=3:ido:2) + ic = idp2 - i + dr2 = wa1(i - 2)*cc(i - 1, k, 2) + wa1(i - 1)*cc(i, k, 2) + di2 = wa1(i - 2)*cc(i, k, 2) - wa1(i - 1)*cc(i - 1, k, 2) + dr3 = wa2(i - 2)*cc(i - 1, k, 3) + wa2(i - 1)*cc(i, k, 3) + di3 = wa2(i - 2)*cc(i, k, 3) - wa2(i - 1)*cc(i - 1, k, 3) + cr2 = dr2 + dr3 + ci2 = di2 + di3 + ch(i - 1, 1, k) = cc(i - 1, k, 1) + cr2 + ch(i, 1, k) = cc(i, k, 1) + ci2 + tr2 = cc(i - 1, k, 1) + taur*cr2 + ti2 = cc(i, k, 1) + taur*ci2 + tr3 = taui*(di2 - di3) + ti3 = taui*(dr3 - dr2) + ch(i - 1, 3, k) = tr2 + tr3 + ch(ic - 1, 2, k) = tr2 - tr3 + ch(i, 3, k) = ti2 + ti3 + ch(ic, 2, k) = ti3 - ti2 + end do + end subroutine radf3 diff --git a/examples/fftpack/native/radf4.f90 b/examples/fftpack/native/radf4.f90 new file mode 100644 index 000000000..63fe36109 --- /dev/null +++ b/examples/fftpack/native/radf4.f90 @@ -0,0 +1,58 @@ + subroutine radf4(ido, l1, cc, ch, wa1, wa2, wa3) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, l1, 4), wa1(*), wa2(*), wa3(*) + real(dp), intent(out) :: ch(ido, 4, l1) + real(dp) :: ci2, ci3, ci4, cr2, cr3, cr4, & + ti1, ti2, ti3, ti4, tr1, tr2, tr3, & + tr4 + integer :: i, ic, idp2, k + real(dp), parameter :: hsqt2 = sqrt(2.0_dp)/2.0_dp + do concurrent(k=1:l1) + tr1 = cc(1, k, 2) + cc(1, k, 4) + tr2 = cc(1, k, 1) + cc(1, k, 3) + ch(1, 1, k) = tr1 + tr2 + ch(ido, 4, k) = tr2 - tr1 + ch(ido, 2, k) = cc(1, k, 1) - cc(1, k, 3) + ch(1, 3, k) = cc(1, k, 4) - cc(1, k, 2) + end do + if (ido < 2) return + if (ido /= 2) then + idp2 = ido + 2 + do concurrent(k=1:l1, i=3:ido:2) + ic = idp2 - i + cr2 = wa1(i - 2)*cc(i - 1, k, 2) + wa1(i - 1)*cc(i, k, 2) + ci2 = wa1(i - 2)*cc(i, k, 2) - wa1(i - 1)*cc(i - 1, k, 2) + cr3 = wa2(i - 2)*cc(i - 1, k, 3) + wa2(i - 1)*cc(i, k, 3) + ci3 = wa2(i - 2)*cc(i, k, 3) - wa2(i - 1)*cc(i - 1, k, 3) + cr4 = wa3(i - 2)*cc(i - 1, k, 4) + wa3(i - 1)*cc(i, k, 4) + ci4 = wa3(i - 2)*cc(i, k, 4) - wa3(i - 1)*cc(i - 1, k, 4) + tr1 = cr2 + cr4 + tr4 = cr4 - cr2 + ti1 = ci2 + ci4 + ti4 = ci2 - ci4 + ti2 = cc(i, k, 1) + ci3 + ti3 = cc(i, k, 1) - ci3 + tr2 = cc(i - 1, k, 1) + cr3 + tr3 = cc(i - 1, k, 1) - cr3 + ch(i - 1, 1, k) = tr1 + tr2 + ch(ic - 1, 4, k) = tr2 - tr1 + ch(i, 1, k) = ti1 + ti2 + ch(ic, 4, k) = ti1 - ti2 + ch(i - 1, 3, k) = ti4 + tr3 + ch(ic - 1, 2, k) = tr3 - ti4 + ch(i, 3, k) = tr4 + ti3 + ch(ic, 2, k) = tr4 - ti3 + end do + if (mod(ido, 2) == 1) return + end if + do concurrent(k=1:l1) + ti1 = -hsqt2*(cc(ido, k, 2) + cc(ido, k, 4)) + tr1 = hsqt2*(cc(ido, k, 2) - cc(ido, k, 4)) + ch(ido, 1, k) = tr1 + cc(ido, k, 1) + ch(ido, 3, k) = cc(ido, k, 1) - tr1 + ch(1, 2, k) = ti1 - cc(ido, k, 3) + ch(1, 4, k) = ti1 + cc(ido, k, 3) + end do + end subroutine radf4 diff --git a/examples/fftpack/native/radf5.f90 b/examples/fftpack/native/radf5.f90 new file mode 100644 index 000000000..5ae40d4d1 --- /dev/null +++ b/examples/fftpack/native/radf5.f90 @@ -0,0 +1,68 @@ + subroutine radf5(ido, l1, cc, ch, wa1, wa2, wa3, wa4) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, l1 + real(dp), intent(in) :: cc(ido, l1, 5), wa1(*), wa2(*), wa3(*), wa4(*) + real(dp), intent(out) :: ch(ido, 5, l1) + real(dp) :: ci2, ci3, ci4, ci5, cr2, cr3, & + cr4, cr5, di2, di3, di4, di5, dr2, dr3, & + dr4, dr5 + real(dp) :: ti2, ti3, ti4, ti5, tr2, tr3, & + tr4, tr5 + integer :: i, ic, idp2, k + real(dp), parameter :: pi = acos(-1.0_dp) + real(dp), parameter :: tr11 = cos(2.0_dp*pi/5.0_dp) + real(dp), parameter :: ti11 = sin(2.0_dp*pi/5.0_dp) + real(dp), parameter :: tr12 = cos(4.0_dp*pi/5.0_dp) + real(dp), parameter :: ti12 = sin(4.0_dp*pi/5.0_dp) + do concurrent(k=1:l1) + cr2 = cc(1, k, 5) + cc(1, k, 2) + ci5 = cc(1, k, 5) - cc(1, k, 2) + cr3 = cc(1, k, 4) + cc(1, k, 3) + ci4 = cc(1, k, 4) - cc(1, k, 3) + ch(1, 1, k) = cc(1, k, 1) + cr2 + cr3 + ch(ido, 2, k) = cc(1, k, 1) + tr11*cr2 + tr12*cr3 + ch(1, 3, k) = ti11*ci5 + ti12*ci4 + ch(ido, 4, k) = cc(1, k, 1) + tr12*cr2 + tr11*cr3 + ch(1, 5, k) = ti12*ci5 - ti11*ci4 + end do + if (ido == 1) return + idp2 = ido + 2 + do concurrent(k=1:l1, i=3:ido:2) + ic = idp2 - i + dr2 = wa1(i - 2)*cc(i - 1, k, 2) + wa1(i - 1)*cc(i, k, 2) + di2 = wa1(i - 2)*cc(i, k, 2) - wa1(i - 1)*cc(i - 1, k, 2) + dr3 = wa2(i - 2)*cc(i - 1, k, 3) + wa2(i - 1)*cc(i, k, 3) + di3 = wa2(i - 2)*cc(i, k, 3) - wa2(i - 1)*cc(i - 1, k, 3) + dr4 = wa3(i - 2)*cc(i - 1, k, 4) + wa3(i - 1)*cc(i, k, 4) + di4 = wa3(i - 2)*cc(i, k, 4) - wa3(i - 1)*cc(i - 1, k, 4) + dr5 = wa4(i - 2)*cc(i - 1, k, 5) + wa4(i - 1)*cc(i, k, 5) + di5 = wa4(i - 2)*cc(i, k, 5) - wa4(i - 1)*cc(i - 1, k, 5) + cr2 = dr2 + dr5 + ci5 = dr5 - dr2 + cr5 = di2 - di5 + ci2 = di2 + di5 + cr3 = dr3 + dr4 + ci4 = dr4 - dr3 + cr4 = di3 - di4 + ci3 = di3 + di4 + ch(i - 1, 1, k) = cc(i - 1, k, 1) + cr2 + cr3 + ch(i, 1, k) = cc(i, k, 1) + ci2 + ci3 + tr2 = cc(i - 1, k, 1) + tr11*cr2 + tr12*cr3 + ti2 = cc(i, k, 1) + tr11*ci2 + tr12*ci3 + tr3 = cc(i - 1, k, 1) + tr12*cr2 + tr11*cr3 + ti3 = cc(i, k, 1) + tr12*ci2 + tr11*ci3 + tr5 = ti11*cr5 + ti12*cr4 + ti5 = ti11*ci5 + ti12*ci4 + tr4 = ti12*cr5 - ti11*cr4 + ti4 = ti12*ci5 - ti11*ci4 + ch(i - 1, 3, k) = tr2 + tr5 + ch(ic - 1, 2, k) = tr2 - tr5 + ch(i, 3, k) = ti2 + ti5 + ch(ic, 2, k) = ti5 - ti2 + ch(i - 1, 5, k) = tr3 + tr4 + ch(ic - 1, 4, k) = tr3 - tr4 + ch(i, 5, k) = ti3 + ti4 + ch(ic, 4, k) = ti4 - ti3 + end do + end subroutine radf5 diff --git a/examples/fftpack/native/radfg.f90 b/examples/fftpack/native/radfg.f90 new file mode 100644 index 000000000..2c9091cc1 --- /dev/null +++ b/examples/fftpack/native/radfg.f90 @@ -0,0 +1,156 @@ + subroutine radfg(ido, ip, l1, idl1, cc, c1, c2, ch, ch2, wa) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: ido, ip, l1, idl1 + real(dp), intent(in) :: wa(*) + real(dp), intent(inout) :: cc(ido, ip, l1) + real(dp), intent(inout) :: c1(ido, l1, ip) + real(dp), intent(inout) :: c2(idl1, ip) + real(dp), intent(out) :: ch(ido, l1, ip) + real(dp), intent(inout) :: ch2(idl1, ip) + real(dp) :: ai1, ai2, ar1, ar1h, ar2, ar2h, arg, & + dc2, dcp, ds2, dsp + integer :: i, ic, idij, idp2, ik, ipp2, & + ipph, is, j, j2, jc, k, l, lc, nbd + real(dp), parameter :: tpi = 2.0_dp*acos(-1.0_dp) ! 2 * pi + arg = tpi/real(ip, kind=dp) + dcp = cos(arg) + dsp = sin(arg) + ipph = (ip + 1)/2 + ipp2 = ip + 2 + idp2 = ido + 2 + nbd = (ido - 1)/2 + if (ido == 1) then + do concurrent(ik=1:idl1) + c2(ik, 1) = ch2(ik, 1) + end do + else + do concurrent(ik=1:idl1) + ch2(ik, 1) = c2(ik, 1) + end do + do concurrent(j=2:ip, k=1:l1) + ch(1, k, j) = c1(1, k, j) + end do + if (nbd > l1) then + is = -ido + do j = 2, ip + is = is + ido + do k = 1, l1 + idij = is + do i = 3, ido, 2 + idij = idij + 2 + ch(i - 1, k, j) = wa(idij - 1)*c1(i - 1, k, j) + wa(idij) & + *c1(i, k, j) + ch(i, k, j) = wa(idij - 1)*c1(i, k, j) - wa(idij) & + *c1(i - 1, k, j) + end do + end do + end do + else + is = -ido + do j = 2, ip + is = is + ido + idij = is + do i = 3, ido, 2 + idij = idij + 2 + do k = 1, l1 + ch(i - 1, k, j) = wa(idij - 1)*c1(i - 1, k, j) + wa(idij) & + *c1(i, k, j) + ch(i, k, j) = wa(idij - 1)*c1(i, k, j) - wa(idij) & + *c1(i - 1, k, j) + end do + end do + end do + end if + if (nbd < l1) then + do concurrent(j=2:ipph, k=1:l1, i=3:ido:2) + jc = ipp2 - j + c1(i - 1, k, j) = ch(i - 1, k, j) + ch(i - 1, k, jc) + c1(i - 1, k, jc) = ch(i, k, j) - ch(i, k, jc) + c1(i, k, j) = ch(i, k, j) + ch(i, k, jc) + c1(i, k, jc) = ch(i - 1, k, jc) - ch(i - 1, k, j) + end do + else + do concurrent(j=2:ipph, k=1:l1, i=3:ido:2) + jc = ipp2 - j + c1(i - 1, k, j) = ch(i - 1, k, j) + ch(i - 1, k, jc) + c1(i - 1, k, jc) = ch(i, k, j) - ch(i, k, jc) + c1(i, k, j) = ch(i, k, j) + ch(i, k, jc) + c1(i, k, jc) = ch(i - 1, k, jc) - ch(i - 1, k, j) + end do + end if + end if + do concurrent(j=2:ipph, k=1:l1) + jc = ipp2 - j + c1(1, k, j) = ch(1, k, j) + ch(1, k, jc) + c1(1, k, jc) = ch(1, k, jc) - ch(1, k, j) + end do +! + ar1 = 1.0_dp + ai1 = 0.0_dp + do l = 2, ipph + lc = ipp2 - l + ar1h = dcp*ar1 - dsp*ai1 + ai1 = dcp*ai1 + dsp*ar1 + ar1 = ar1h + do concurrent(ik=1:idl1) + ch2(ik, l) = c2(ik, 1) + ar1*c2(ik, 2) + ch2(ik, lc) = ai1*c2(ik, ip) + end do + dc2 = ar1 + ds2 = ai1 + ar2 = ar1 + ai2 = ai1 + do j = 3, ipph + jc = ipp2 - j + ar2h = dc2*ar2 - ds2*ai2 + ai2 = dc2*ai2 + ds2*ar2 + ar2 = ar2h + do concurrent(ik=1:idl1) + ch2(ik, l) = ch2(ik, l) + ar2*c2(ik, j) + ch2(ik, lc) = ch2(ik, lc) + ai2*c2(ik, jc) + end do + end do + end do + do concurrent(j=2:ipph, ik=1:idl1) + ch2(ik, 1) = ch2(ik, 1) + c2(ik, j) + end do +! + if (ido < l1) then + do concurrent(k=1:l1, i=1:ido) + cc(i, 1, k) = ch(i, k, 1) + end do + else + do concurrent(i=1:ido, k=1:l1) + cc(i, 1, k) = ch(i, k, 1) + end do + end if + do concurrent(j=2:ipph, k=1:l1) + jc = ipp2 - j + j2 = j + j + cc(ido, j2 - 2, k) = ch(1, k, j) + cc(1, j2 - 1, k) = ch(1, k, jc) + end do + if (ido == 1) return + if (nbd < l1) then + do concurrent(j=2:ipph, k=1:l1, i=3:ido:2) + jc = ipp2 - j + j2 = j + j + ic = idp2 - i + cc(i - 1, j2 - 1, k) = ch(i - 1, k, j) + ch(i - 1, k, jc) + cc(ic - 1, j2 - 2, k) = ch(i - 1, k, j) - ch(i - 1, k, jc) + cc(i, j2 - 1, k) = ch(i, k, j) + ch(i, k, jc) + cc(ic, j2 - 2, k) = ch(i, k, jc) - ch(i, k, j) + end do + else + do concurrent(j=2:ipph, k=1:l1, i=3:ido:2) + jc = ipp2 - j + j2 = j + j + ic = idp2 - i + cc(i - 1, j2 - 1, k) = ch(i - 1, k, j) + ch(i - 1, k, jc) + cc(ic - 1, j2 - 2, k) = ch(i - 1, k, j) - ch(i - 1, k, jc) + cc(i, j2 - 1, k) = ch(i, k, j) + ch(i, k, jc) + cc(ic, j2 - 2, k) = ch(i, k, jc) - ch(i, k, j) + end do + end if + end subroutine radfg diff --git a/examples/fftpack/native/rfftb1.f90 b/examples/fftpack/native/rfftb1.f90 new file mode 100644 index 000000000..9f5ac5337 --- /dev/null +++ b/examples/fftpack/native/rfftb1.f90 @@ -0,0 +1,69 @@ + subroutine rfftb1(n, c, ch, wa, ifac) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(inout) :: c(*) + real(dp), intent(in) :: wa(*) + real(dp), intent(inout) :: ch(*) + integer, intent(in) :: ifac(*) + integer :: i, idl1, ido, ip, iw, ix2, ix3, ix4, k1, & + l1, l2, na, nf + nf = ifac(2) + na = 0 + l1 = 1 + iw = 1 + do k1 = 1, nf + ip = ifac(k1 + 2) + l2 = ip*l1 + ido = n/l2 + idl1 = ido*l1 + if (ip == 4) then + ix2 = iw + ido + ix3 = ix2 + ido + if (na /= 0) then + call radb4(ido, l1, ch, c, wa(iw), wa(ix2), wa(ix3)) + else + call radb4(ido, l1, c, ch, wa(iw), wa(ix2), wa(ix3)) + end if + na = 1 - na + elseif (ip == 2) then + if (na /= 0) then + call radb2(ido, l1, ch, c, wa(iw)) + else + call radb2(ido, l1, c, ch, wa(iw)) + end if + na = 1 - na + elseif (ip == 3) then + ix2 = iw + ido + if (na /= 0) then + call radb3(ido, l1, ch, c, wa(iw), wa(ix2)) + else + call radb3(ido, l1, c, ch, wa(iw), wa(ix2)) + end if + na = 1 - na + elseif (ip /= 5) then + if (na /= 0) then + call radbg(ido, ip, l1, idl1, ch, ch, ch, c, c, wa(iw)) + else + call radbg(ido, ip, l1, idl1, c, c, c, ch, ch, wa(iw)) + end if + if (ido == 1) na = 1 - na + else + ix2 = iw + ido + ix3 = ix2 + ido + ix4 = ix3 + ido + if (na /= 0) then + call radb5(ido, l1, ch, c, wa(iw), wa(ix2), wa(ix3), wa(ix4)) + else + call radb5(ido, l1, c, ch, wa(iw), wa(ix2), wa(ix3), wa(ix4)) + end if + na = 1 - na + end if + l1 = l2 + iw = iw + (ip - 1)*ido + end do + if (na == 0) return + do concurrent(i=1:n) + c(i) = ch(i) + end do + end subroutine rfftb1 diff --git a/examples/fftpack/native/rfftf1.f90 b/examples/fftpack/native/rfftf1.f90 new file mode 100644 index 000000000..107e55982 --- /dev/null +++ b/examples/fftpack/native/rfftf1.f90 @@ -0,0 +1,69 @@ + subroutine rfftf1(n, c, ch, wa, ifac) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(inout) :: c(*) + real(dp), intent(in) :: wa(*) + real(dp), intent(inout) :: ch(*) + integer, intent(in) :: ifac(*) + integer :: i, idl1, ido, ip, iw, ix2, ix3, ix4, k1, & + kh, l1, l2, na, nf + nf = ifac(2) + na = 1 + l2 = n + iw = n + do k1 = 1, nf + kh = nf - k1 + ip = ifac(kh + 3) + l1 = l2/ip + ido = n/l2 + idl1 = ido*l1 + iw = iw - (ip - 1)*ido + na = 1 - na + if (ip == 4) then + ix2 = iw + ido + ix3 = ix2 + ido + if (na /= 0) then + call radf4(ido, l1, ch, c, wa(iw), wa(ix2), wa(ix3)) + else + call radf4(ido, l1, c, ch, wa(iw), wa(ix2), wa(ix3)) + end if + elseif (ip /= 2) then + if (ip == 3) then + ix2 = iw + ido + if (na /= 0) then + call radf3(ido, l1, ch, c, wa(iw), wa(ix2)) + else + call radf3(ido, l1, c, ch, wa(iw), wa(ix2)) + end if + elseif (ip /= 5) then + if (ido == 1) na = 1 - na + if (na /= 0) then + call radfg(ido, ip, l1, idl1, ch, ch, ch, c, c, wa(iw)) + na = 0 + else + call radfg(ido, ip, l1, idl1, c, c, c, ch, ch, wa(iw)) + na = 1 + end if + else + ix2 = iw + ido + ix3 = ix2 + ido + ix4 = ix3 + ido + if (na /= 0) then + call radf5(ido, l1, ch, c, wa(iw), wa(ix2), wa(ix3), wa(ix4)) + else + call radf5(ido, l1, c, ch, wa(iw), wa(ix2), wa(ix3), wa(ix4)) + end if + end if + elseif (na /= 0) then + call radf2(ido, l1, ch, c, wa(iw)) + else + call radf2(ido, l1, c, ch, wa(iw)) + end if + l2 = l1 + end do + if (na == 1) return + do concurrent(i=1:n) + c(i) = ch(i) + end do + end subroutine rfftf1 diff --git a/examples/fftpack/native/rffti1.f90 b/examples/fftpack/native/rffti1.f90 new file mode 100644 index 000000000..5ae8f4188 --- /dev/null +++ b/examples/fftpack/native/rffti1.f90 @@ -0,0 +1,66 @@ + subroutine rffti1(n, wa, ifac) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(out) :: wa(*) + integer, intent(out) :: ifac(*) + real(dp) :: arg, argh, argld, fi + integer :: i, ib, ido, ii, ip, ipm, is, j, k1, l1, & + l2, ld, nf, nfm1, nl, nq, nr, ntry + integer, dimension(4), parameter :: ntryh = [4, 2, 3, 5] + real(dp), parameter :: tpi = 2.0_dp*acos(-1.0_dp) ! 2 * pi + nl = n + nf = 0 + j = 0 +100 j = j + 1 + if (j <= 4) then + ntry = ntryh(j) + else + ntry = ntry + 2 + end if +200 nq = nl/ntry + nr = nl - ntry*nq + if (nr /= 0) goto 100 + nf = nf + 1 + ifac(nf + 2) = ntry + nl = nq + if (ntry == 2) then + if (nf /= 1) then + do i = 2, nf + ib = nf - i + 2 + ifac(ib + 2) = ifac(ib + 1) + end do + ifac(3) = 2 + end if + end if + if (nl /= 1) goto 200 + ifac(1) = n + ifac(2) = nf + argh = tpi/real(n, dp) + is = 0 + nfm1 = nf - 1 + l1 = 1 + if (nfm1 == 0) return + do k1 = 1, nfm1 + ip = ifac(k1 + 2) + ld = 0 + l2 = l1*ip + ido = n/l2 + ipm = ip - 1 + do j = 1, ipm + ld = ld + l1 + i = is + argld = real(ld, dp)*argh + fi = 0.0_dp + do ii = 3, ido, 2 + i = i + 2 + fi = fi + 1.0_dp + arg = fi*argld + wa(i - 1) = cos(arg) + wa(i) = sin(arg) + end do + is = is + ido + end do + l1 = l2 + end do + end subroutine rffti1 diff --git a/examples/fftpack/native/rk.f90 b/examples/fftpack/native/rk.f90 new file mode 100644 index 000000000..3ab114da8 --- /dev/null +++ b/examples/fftpack/native/rk.f90 @@ -0,0 +1,4 @@ +module fftpack_kind + use, intrinsic :: iso_fortran_env, only: rk => real64 + implicit none(type, external) +end module fftpack_kind diff --git a/examples/fftpack/native/sint1.f90 b/examples/fftpack/native/sint1.f90 new file mode 100644 index 000000000..dddecd11c --- /dev/null +++ b/examples/fftpack/native/sint1.f90 @@ -0,0 +1,46 @@ + subroutine sint1(n, war, was, xh, x, ifac) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n, ifac(*) + real(dp), intent(in) :: was(*) + real(dp), intent(inout) :: war(*), x(*) + real(dp), intent(out) :: xh(*) + integer :: i, k, kc, modn, np1, ns2 + real(dp) :: t1, t2, xhold + real(dp), parameter :: sqrt3 = sqrt(3.0_dp) + do i = 1, n + xh(i) = war(i) + war(i) = x(i) + end do + if (n < 2) then + xh(1) = xh(1) + xh(1) + elseif (n == 2) then + xhold = sqrt3*(xh(1) + xh(2)) + xh(2) = sqrt3*(xh(1) - xh(2)) + xh(1) = xhold + else + np1 = n + 1 + ns2 = n/2 + x(1) = 0.0_dp + do k = 1, ns2 + kc = np1 - k + t1 = xh(k) - xh(kc) + t2 = was(k)*(xh(k) + xh(kc)) + x(k + 1) = t1 + t2 + x(kc + 1) = t2 - t1 + end do + modn = mod(n, 2) + if (modn /= 0) x(ns2 + 2) = 4.0_dp*xh(ns2 + 1) + call rfftf1(np1, x, xh, war, ifac) + xh(1) = 0.5_dp*x(1) + do i = 3, n, 2 + xh(i - 1) = -x(i) + xh(i) = xh(i - 2) + x(i - 1) + end do + if (modn == 0) xh(n) = -x(n + 1) + end if + do i = 1, n + x(i) = war(i) + war(i) = xh(i) + end do + end subroutine sint1 diff --git a/examples/fftpack/native/zfftb.f90 b/examples/fftpack/native/zfftb.f90 new file mode 100644 index 000000000..aae8cd5e9 --- /dev/null +++ b/examples/fftpack/native/zfftb.f90 @@ -0,0 +1,12 @@ + subroutine zfftb(n, c, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(inout) :: c(*) + real(dp), intent(inout) :: wsave(*) + integer :: iw1, iw2 + if (n == 1) return + iw1 = n + n + 1 + iw2 = iw1 + n + n + call cfftb1(n, c, wsave, wsave(iw1), wsave(iw2)) + end subroutine zfftb diff --git a/examples/fftpack/native/zfftf.f90 b/examples/fftpack/native/zfftf.f90 new file mode 100644 index 000000000..675b57e87 --- /dev/null +++ b/examples/fftpack/native/zfftf.f90 @@ -0,0 +1,12 @@ + subroutine zfftf(n, c, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(inout) :: c(*) + real(dp), intent(inout) :: wsave(*) + integer :: iw1, iw2 + if (n == 1) return + iw1 = n + n + 1 + iw2 = iw1 + n + n + call cfftf1(n, c, wsave, wsave(iw1), wsave(iw2)) + end subroutine zfftf diff --git a/examples/fftpack/native/zffti.f90 b/examples/fftpack/native/zffti.f90 new file mode 100644 index 000000000..d6da6d8ef --- /dev/null +++ b/examples/fftpack/native/zffti.f90 @@ -0,0 +1,11 @@ + subroutine zffti(n, wsave) + use fftpack_kind, only: dp => rk + implicit none + integer, intent(in) :: n + real(dp), intent(out) :: wsave(*) + integer :: iw1, iw2 + if (n == 1) return + iw1 = n + n + 1 + iw2 = iw1 + n + n + call cffti1(n, wsave(iw1), wsave(iw2)) + end subroutine zffti diff --git a/examples/fftpack/routine_inventory.py b/examples/fftpack/routine_inventory.py new file mode 100644 index 000000000..b545cb572 --- /dev/null +++ b/examples/fftpack/routine_inventory.py @@ -0,0 +1,25 @@ +"""Public FFTPACK module surface and its explicit test mapping.""" + +from __future__ import annotations + +ROUTINE_GROUPS: dict[str, tuple[str, ...]] = { + "Complex work-array transforms": ("zffti", "zfftf", "zfftb"), + "Real work-array transforms": ("dffti", "dfftf", "dfftb", "dzffti", "dzfftf", "dzfftb"), + "Cosine and sine work-array transforms": ( + "dcosqi", + "dcosqf", + "dcosqb", + "dcosti", + "dcost", + "dsinti", + "dsint", + ), + "High-level Fourier transforms": ("fft", "ifft", "rfft", "irfft"), + "High-level cosine transforms": ("dct", "idct", "dct_t1i", "dct_t1", "dct_t23i", "dct_t2", "dct_t3"), + "Frequency and spectrum ordering": ("fftfreq", "rfftfreq", "fftshift", "ifftshift"), +} + +ALL_ROUTINES = tuple(routine for group in ROUTINE_GROUPS.values() for routine in group) +PRIK_TESTED_ROUTINES = frozenset(ALL_ROUTINES) +UNSUPPORTED_ROUTINES: dict[str, str] = {} +EXPLICIT_TEST_NAMES = {routine: f"test_{routine}" for routine in ALL_ROUTINES} diff --git a/examples/fftpack/tests/__init__.py b/examples/fftpack/tests/__init__.py new file mode 100644 index 000000000..adc89fadd --- /dev/null +++ b/examples/fftpack/tests/__init__.py @@ -0,0 +1 @@ +"""Numerical validation suite for the PRIK FFTPACK example.""" diff --git a/examples/fftpack/tests/helpers.py b/examples/fftpack/tests/helpers.py new file mode 100644 index 000000000..a5e39dfbc --- /dev/null +++ b/examples/fftpack/tests/helpers.py @@ -0,0 +1,29 @@ +"""Reference conversions for FFTPACK's documented unnormalized conventions.""" + +from __future__ import annotations + +import numpy as np + + +def numpy_rfft_packing(values: np.ndarray) -> np.ndarray: + """Return NumPy's real FFT in FFTPACK's one-dimensional packed layout.""" + spectrum = np.fft.rfft(values) + packed = np.empty(values.size, dtype=np.float64) + packed[0] = spectrum[0].real + if values.size % 2 == 0: + packed[-1] = spectrum[-1].real + stop = spectrum.size - 1 + else: + stop = spectrum.size + for index in range(1, stop): + packed[2 * index - 1] = spectrum[index].real + packed[2 * index] = spectrum[index].imag + return packed + + +def take_owned_array(handle) -> np.ndarray: + """Copy one PRIK allocatable result and release its native allocation.""" + try: + return handle.to_numpy().copy() + finally: + handle.close() diff --git a/examples/fftpack/tests/test_routine_coverage.py b/examples/fftpack/tests/test_routine_coverage.py new file mode 100644 index 000000000..5d24f9394 --- /dev/null +++ b/examples/fftpack/tests/test_routine_coverage.py @@ -0,0 +1,51 @@ +"""Fail closed when the reviewed FFTPACK transform surface or tests drift.""" + +from __future__ import annotations + +import ast +from pathlib import Path + +import pytest + +from ..routine_inventory import ( + ALL_ROUTINES, + EXPLICIT_TEST_NAMES, + PRIK_TESTED_ROUTINES, + ROUTINE_GROUPS, + UNSUPPORTED_ROUTINES, +) + + +pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] +TEST_FILE = Path(__file__).with_name("test_transforms.py") + + +def _test_functions() -> dict[str, ast.FunctionDef]: + """Return the explicitly named public-routine tests in this suite.""" + tree = ast.parse(TEST_FILE.read_text(encoding="utf-8"), filename=str(TEST_FILE)) + return { + node.name: node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name.startswith("test_") + } + + +def test_every_public_fftpack_routine_has_one_visible_numerical_test(): + functions = _test_functions() + assert len(ALL_ROUTINES) == len(set(ALL_ROUTINES)) + assert set(ALL_ROUTINES) == PRIK_TESTED_ROUTINES + assert UNSUPPORTED_ROUTINES == {} + + source_text = TEST_FILE.read_text(encoding="utf-8") + for routine, test_name in EXPLICIT_TEST_NAMES.items(): + source = ast.get_source_segment(source_text, functions[test_name]) + assert source is not None + assert routine in source, f"{test_name} does not visibly exercise {routine}" + assert "fftpack" in source, f"{test_name} does not visibly invoke FFTPACK" + + +def test_inventory_groups_cover_each_generated_public_routine_once(fftpack): + grouped = tuple(routine for group in ROUTINE_GROUPS.values() for routine in group) + exported = {name for name in dir(fftpack) if not name.startswith("_")} + + assert grouped == ALL_ROUTINES + assert len(grouped) == len(set(grouped)) + assert exported == set(ALL_ROUTINES) diff --git a/examples/fftpack/tests/test_transforms.py b/examples/fftpack/tests/test_transforms.py new file mode 100644 index 000000000..6a99e7e00 --- /dev/null +++ b/examples/fftpack/tests/test_transforms.py @@ -0,0 +1,313 @@ +"""Every public FFTPACK procedure checked against NumPy or SciPy.""" + +from __future__ import annotations + +import numpy as np +import pytest +from scipy import fft as scipy_fft + +from .helpers import numpy_rfft_packing, take_owned_array + + +pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] + + +def test_zffti(fftpack): + n = np.int32(5) + wsave = np.full(4 * n + 15, np.nan, dtype=np.float64) + + fftpack.zffti(n, wsave) + + assert np.any(np.isfinite(wsave)) + + +def test_zfftf(fftpack): + values = np.array([1.0 + 2.0j, -2.0 + 1.0j, 4.0 - 3.0j, 3.0 + 0.5j, -1.0j], dtype=np.complex128) + expected = np.fft.fft(values) + wsave = np.empty(4 * values.size + 15, dtype=np.float64) + fftpack.zffti(np.int32(values.size), wsave) + + fftpack.zfftf(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + + +def test_zfftb(fftpack): + values = np.array([1.0 + 2.0j, -2.0 + 1.0j, 4.0 - 3.0j, 3.0 + 0.5j, -1.0j], dtype=np.complex128) + expected = values * values.size + wsave = np.empty(4 * values.size + 15, dtype=np.float64) + fftpack.zffti(np.int32(values.size), wsave) + fftpack.zfftf(np.int32(values.size), values, wsave) + + fftpack.zfftb(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + + +def test_dffti(fftpack): + n = np.int32(5) + wsave = np.full(2 * n + 15, np.nan, dtype=np.float64) + + fftpack.dffti(n, wsave) + + assert np.any(np.isfinite(wsave)) + + +def test_dfftf(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + expected = numpy_rfft_packing(values) + wsave = np.empty(2 * values.size + 15, dtype=np.float64) + fftpack.dffti(np.int32(values.size), wsave) + + fftpack.dfftf(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + + +def test_dfftb(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + expected = values * values.size + wsave = np.empty(2 * values.size + 15, dtype=np.float64) + fftpack.dffti(np.int32(values.size), wsave) + fftpack.dfftf(np.int32(values.size), values, wsave) + + fftpack.dfftb(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + + +def test_dzffti(fftpack): + n = np.int32(5) + wsave = np.full(3 * n + 15, np.nan, dtype=np.float64) + + fftpack.dzffti(n, wsave) + + assert np.any(np.isfinite(wsave)) + + +def test_dzfftf(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + spectrum = np.fft.rfft(values) + coefficients_a = np.empty((values.size + 1) // 2, dtype=np.float64) + coefficients_b = np.empty_like(coefficients_a) + wsave = np.empty(3 * values.size + 15, dtype=np.float64) + fftpack.dzffti(np.int32(values.size), wsave) + + azero = fftpack.dzfftf(np.int32(values.size), values, coefficients_a, coefficients_b, wsave) + + np.testing.assert_allclose(azero, spectrum[0].real / values.size, rtol=0.0, atol=1.0e-12) + np.testing.assert_allclose(coefficients_a[:-1], 2.0 * spectrum[1:].real / values.size, rtol=0.0, atol=1.0e-12) + np.testing.assert_allclose(coefficients_b[:-1], -2.0 * spectrum[1:].imag / values.size, rtol=0.0, atol=1.0e-12) + + +def test_dzfftb(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + coefficients_a = np.empty((values.size + 1) // 2, dtype=np.float64) + coefficients_b = np.empty_like(coefficients_a) + wsave = np.empty(3 * values.size + 15, dtype=np.float64) + fftpack.dzffti(np.int32(values.size), wsave) + azero = fftpack.dzfftf(np.int32(values.size), values, coefficients_a, coefficients_b, wsave) + result = np.empty_like(values) + + fftpack.dzfftb(np.int32(values.size), result, np.float64(azero), coefficients_a, coefficients_b, wsave) + + np.testing.assert_allclose(result, values, rtol=0.0, atol=1.0e-12) + + +def test_dcosqi(fftpack): + n = np.int32(5) + wsave = np.full(3 * n + 15, np.nan, dtype=np.float64) + + fftpack.dcosqi(n, wsave) + + assert np.any(np.isfinite(wsave)) + + +def test_dcosqf(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + expected = scipy_fft.dct(values, type=3, norm=None) + wsave = np.empty(3 * values.size + 15, dtype=np.float64) + fftpack.dcosqi(np.int32(values.size), wsave) + + fftpack.dcosqf(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + + +def test_dcosqb(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + expected = 2.0 * scipy_fft.dct(values, type=2, norm=None) + wsave = np.empty(3 * values.size + 15, dtype=np.float64) + fftpack.dcosqi(np.int32(values.size), wsave) + + fftpack.dcosqb(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + + +def test_dcosti(fftpack): + n = np.int32(5) + wsave = np.full(3 * n + 15, np.nan, dtype=np.float64) + + fftpack.dcosti(n, wsave) + + assert np.any(np.isfinite(wsave)) + + +def test_dcost(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + expected = scipy_fft.dct(values, type=1, norm=None) + wsave = np.empty(3 * values.size + 15, dtype=np.float64) + fftpack.dcosti(np.int32(values.size), wsave) + + fftpack.dcost(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + + +def test_dsinti(fftpack): + n = np.int32(5) + wsave = np.full(2 * n + 15, np.nan, dtype=np.float64) + + fftpack.dsinti(n, wsave) + + assert np.any(np.isfinite(wsave)) + + +def test_dsint(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + expected = scipy_fft.dst(values, type=1, norm=None) + wsave = np.empty(2 * values.size + 15, dtype=np.float64) + fftpack.dsinti(np.int32(values.size), wsave) + + fftpack.dsint(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + + +def test_fft(fftpack): + values = np.array([1.0 + 2.0j, 2.0 - 1.0j, -1.0 + 1.0j, 3.0], dtype=np.complex128) + original = values.copy() + + result = take_owned_array(fftpack.fft(values)) + + np.testing.assert_allclose(result, np.fft.fft(values), rtol=0.0, atol=1.0e-12) + np.testing.assert_array_equal(values, original) + + +def test_ifft(fftpack): + spectrum = np.array([5.0 + 2.0j, 4.0 - 1.0j, -5.0 + 2.0j, -3.0j], dtype=np.complex128) + original = spectrum.copy() + + result = take_owned_array(fftpack.ifft(spectrum)) + + np.testing.assert_allclose(result, np.fft.ifft(spectrum) * spectrum.size, rtol=0.0, atol=1.0e-12) + np.testing.assert_array_equal(spectrum, original) + + +def test_rfft(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + original = values.copy() + + result = take_owned_array(fftpack.rfft(values)) + + np.testing.assert_allclose(result, numpy_rfft_packing(values), rtol=0.0, atol=1.0e-12) + np.testing.assert_array_equal(values, original) + + +def test_irfft(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + packed = take_owned_array(fftpack.rfft(values)) + + result = take_owned_array(fftpack.irfft(packed)) + + np.testing.assert_allclose(result, values * values.size, rtol=0.0, atol=1.0e-12) + + +def test_dct(fftpack): + values = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64) + + for transform_type, scale in ((1, 1.0), (2, 2.0), (3, 1.0)): + result = take_owned_array(fftpack.dct(values, type=np.int32(transform_type))) + reference = scipy_fft.dct(values, type=transform_type, norm=None) * scale + np.testing.assert_allclose(result, reference, rtol=0.0, atol=1.0e-12) + + +def test_idct(fftpack): + values = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64) + + for transform_type, scale in ((1, 2 * (values.size - 1)), (2, 4 * values.size), (3, 4 * values.size)): + transformed = take_owned_array(fftpack.dct(values, type=np.int32(transform_type))) + result = take_owned_array(fftpack.idct(transformed, type=np.int32(transform_type))) + np.testing.assert_allclose(result, values * scale, rtol=0.0, atol=1.0e-12) + + +def test_dct_t1i(fftpack): + n = np.int32(5) + wsave = np.full(3 * n + 15, np.nan, dtype=np.float64) + + fftpack.dct_t1i(n, wsave) + + assert np.any(np.isfinite(wsave)) + + +def test_dct_t1(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + expected = scipy_fft.dct(values, type=1, norm=None) + wsave = np.empty(3 * values.size + 15, dtype=np.float64) + fftpack.dct_t1i(np.int32(values.size), wsave) + + fftpack.dct_t1(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + + +def test_dct_t23i(fftpack): + n = np.int32(5) + wsave = np.full(3 * n + 15, np.nan, dtype=np.float64) + + fftpack.dct_t23i(n, wsave) + + assert np.any(np.isfinite(wsave)) + + +def test_dct_t2(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + expected = 2.0 * scipy_fft.dct(values, type=2, norm=None) + wsave = np.empty(3 * values.size + 15, dtype=np.float64) + fftpack.dct_t23i(np.int32(values.size), wsave) + + fftpack.dct_t2(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + + +def test_dct_t3(fftpack): + values = np.array([1.0, -2.0, 4.0, 3.0, -1.0], dtype=np.float64) + expected = scipy_fft.dct(values, type=3, norm=None) + wsave = np.empty(3 * values.size + 15, dtype=np.float64) + fftpack.dct_t23i(np.int32(values.size), wsave) + + fftpack.dct_t3(np.int32(values.size), values, wsave) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + + +def test_fftfreq(fftpack): + for size, expected in ((4, [0, 1, -2, -1]), (5, [0, 1, 2, -2, -1])): + np.testing.assert_array_equal(fftpack.fftfreq(np.int32(size)), expected) + + +def test_rfftfreq(fftpack): + for size, expected in ((4, [0, 1, 1, -2]), (5, [0, 1, 1, 2, 2])): + np.testing.assert_array_equal(fftpack.rfftfreq(np.int32(size)), expected) + + +def test_fftshift(fftpack): + for values in (np.arange(5, dtype=np.float64), np.arange(6, dtype=np.float64) + 1.0j): + np.testing.assert_array_equal(fftpack.fftshift(values), np.fft.fftshift(values)) + + +def test_ifftshift(fftpack): + for values in (np.arange(5, dtype=np.float64), np.arange(6, dtype=np.float64) + 1.0j): + np.testing.assert_array_equal(fftpack.ifftshift(values), np.fft.ifftshift(values)) diff --git a/examples/lapack/tests/helpers.py b/examples/lapack/tests/helpers.py index 9ff770b52..6c25b4c11 100644 --- a/examples/lapack/tests/helpers.py +++ b/examples/lapack/tests/helpers.py @@ -53,13 +53,6 @@ def native_pivots(scipy_pivots: np.ndarray) -> np.ndarray: return np.asarray(scipy_pivots, dtype=np.int32) + np.int32(1) -def gfortran_logical_mask(values) -> np.ndarray: - """Represent a default-GFortran LOGICAL vector through PRIK's bool buffer ABI.""" - logical_bytes = np.zeros(len(values) * np.dtype(np.int32).itemsize, dtype=np.bool_) - logical_bytes[:: np.dtype(np.int32).itemsize] = np.asarray(values, dtype=np.bool_) - return logical_bytes - - def pivot_matrix(pivots: np.ndarray, size: int, *, one_based: bool) -> np.ndarray: """Build the row permutation represented by sequential LAPACK pivots.""" permutation = np.eye(size, dtype=np.float64) diff --git a/examples/lapack/tests/test_eigen_generalized.py b/examples/lapack/tests/test_eigen_generalized.py index 714e99517..ca6a4746a 100644 --- a/examples/lapack/tests/test_eigen_generalized.py +++ b/examples/lapack/tests/test_eigen_generalized.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from .helpers import assert_allclose_float64, assert_orthogonal, gfortran_logical_mask +from .helpers import assert_allclose_float64, assert_orthogonal pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] @@ -339,7 +339,7 @@ def test_dtgsen_reorders_selected_generalized_eigenvalue(prik_lapack, scipy_lapa a, b = _generalized_problem() identity = np.eye(2, dtype=np.float64, order="F") selection = np.array([False, True], dtype=np.bool_) - prik_selection = gfortran_logical_mask(selection) + prik_selection = selection.copy() prik_a, f2py_a = a.copy(order="F"), a.copy(order="F") prik_b, f2py_b = b.copy(order="F"), b.copy(order="F") prik_q, f2py_q = identity.copy(order="F"), identity.copy(order="F") diff --git a/examples/lapack/tests/test_eigen_nonsymmetric.py b/examples/lapack/tests/test_eigen_nonsymmetric.py index c7467474d..54cd8539d 100644 --- a/examples/lapack/tests/test_eigen_nonsymmetric.py +++ b/examples/lapack/tests/test_eigen_nonsymmetric.py @@ -5,14 +5,26 @@ import numpy as np import pytest -from .helpers import assert_allclose_float64, assert_orthogonal, gfortran_logical_mask +from .helpers import assert_allclose_float64, assert_orthogonal pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] +def _hessenberg_q_from_reflectors(factor: np.ndarray, tau: np.ndarray) -> np.ndarray: + """Form the orthogonal similarity transform stored by DGEHRD.""" + size = factor.shape[0] + q = np.eye(size, dtype=np.float64) + for index in range(size - 1): + vector = np.zeros(size, dtype=np.float64) + vector[index + 1] = 1.0 + vector[index + 2 :] = factor[index + 2 :, index] + q = q @ (np.eye(size, dtype=np.float64) - tau[index] * np.outer(vector, vector)) + return q + + def test_dgebal_preserves_eigenvalues_while_balancing(prik_lapack, scipy_lapack, f2py_lapack): - matrix = np.diag(np.array([2.0, 5.0], dtype=np.float64)).copy(order="F") + matrix = np.array([[1.0, 1.0e6], [1.0e-6, 2.0]], dtype=np.float64, order="F") prik_a, f2py_a = matrix.copy(order="F"), matrix.copy(order="F") prik_scale, f2py_scale = np.empty(2), np.empty(2) @@ -27,8 +39,11 @@ def test_dgebal_preserves_eigenvalues_while_balancing(prik_lapack, scipy_lapack, assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 assert prik_scalars[2:4] == (scipy_lo + 1, scipy_hi + 1) - assert_allclose_float64(np.sort(np.diag(prik_a)), [2.0, 5.0]) - assert_allclose_float64(np.sort(np.diag(f2py_a)), [2.0, 5.0]) + expected_eigenvalues = np.sort(np.linalg.eigvals(matrix).real) + assert_allclose_float64(np.sort(np.linalg.eigvals(prik_a).real), expected_eigenvalues, operation_size=2) + assert_allclose_float64(np.sort(np.linalg.eigvals(f2py_a).real), expected_eigenvalues, operation_size=2) + assert not np.array_equal(prik_a, matrix) + assert not np.array_equal(f2py_a, matrix) assert_allclose_float64(prik_a, scipy_a) assert_allclose_float64(f2py_a, scipy_a) assert_allclose_float64(prik_scale, scipy_scale) @@ -36,7 +51,7 @@ def test_dgebal_preserves_eigenvalues_while_balancing(prik_lapack, scipy_lapack, def test_dgees_computes_real_schur_decomposition(prik_lapack, scipy_lapack): - matrix = np.array([[1.0, 2.0], [0.0, 3.0]], dtype=np.float64, order="F") + matrix = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float64, order="F") prik_t = matrix.copy(order="F") prik_wr, prik_wi = np.empty(2), np.empty(2) prik_vs = np.empty((2, 2), order="F") @@ -64,13 +79,14 @@ def test_dgees_computes_real_schur_decomposition(prik_lapack, scipy_lapack): assert prik_scalars[-1] == scipy_info == 0 assert prik_scalars[3] == scipy_sdim == 0 + expected_eigenvalues = np.sort(np.linalg.eigvals(matrix).real) for t, vs, wr, wi in ( (prik_t, prik_vs, prik_wr, prik_wi), (scipy_t, scipy_vs, scipy_wr, scipy_wi), ): assert_orthogonal(vs) assert_allclose_float64(vs @ t @ vs.T, matrix, operation_size=2) - assert_allclose_float64(np.sort(wr), [1.0, 3.0]) + assert_allclose_float64(np.sort(wr), expected_eigenvalues, operation_size=2) assert_allclose_float64(wi, [0.0, 0.0]) @@ -112,20 +128,23 @@ def test_dgeev_returns_right_eigenvectors(prik_lapack, scipy_lapack, f2py_lapack def test_dgehrd_reduces_matrix_to_upper_hessenberg(prik_lapack, scipy_lapack, f2py_lapack): - matrix = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float64, order="F") + matrix = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]], dtype=np.float64, order="F") prik_a, f2py_a = matrix.copy(order="F"), matrix.copy(order="F") - prik_tau, f2py_tau = np.empty(1), np.empty(1) + prik_tau, f2py_tau = np.empty(2), np.empty(2) prik_scalars = prik_lapack.dgehrd( - np.int32(2), np.int32(1), np.int32(2), prik_a, np.int32(2), prik_tau, np.empty(64), np.int32(64), np.int32(0) + np.int32(3), np.int32(1), np.int32(3), prik_a, np.int32(3), prik_tau, np.empty(64), np.int32(64), np.int32(0) ) - f2py_result = f2py_lapack.dgehrd(2, 1, 2, f2py_a, f2py_tau, np.empty(64), 64, 0) - scipy_a, scipy_tau, scipy_info = scipy_lapack.dgehrd(matrix.copy(order="F"), lo=0, hi=1, lwork=64) + f2py_result = f2py_lapack.dgehrd(3, 1, 3, f2py_a, f2py_tau, np.empty(64), 64, 0) + scipy_a, scipy_tau, scipy_info = scipy_lapack.dgehrd(matrix.copy(order="F"), lo=0, hi=2, lwork=64) assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert_allclose_float64(np.triu(prik_a, -1), matrix) - assert_allclose_float64(np.triu(f2py_a, -1), matrix) + for factor, tau in ((prik_a, prik_tau), (f2py_a, f2py_tau), (scipy_a, scipy_tau)): + q = _hessenberg_q_from_reflectors(factor, tau) + hessenberg = np.triu(factor, -1) + assert_orthogonal(q) + assert_allclose_float64(q.T @ matrix @ q, hessenberg, operation_size=3) assert_allclose_float64(prik_a, scipy_a) assert_allclose_float64(f2py_a, scipy_a) assert_allclose_float64(prik_tau, scipy_tau) @@ -133,22 +152,23 @@ def test_dgehrd_reduces_matrix_to_upper_hessenberg(prik_lapack, scipy_lapack, f2 def test_dorghr_forms_hessenberg_similarity_transform(prik_lapack, scipy_lapack, f2py_lapack): - matrix = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float64, order="F") - factor, tau, factor_info = scipy_lapack.dgehrd(matrix.copy(order="F"), lo=0, hi=1, lwork=64) + matrix = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 10.0]], dtype=np.float64, order="F") + factor, tau, factor_info = scipy_lapack.dgehrd(matrix.copy(order="F"), lo=0, hi=2, lwork=64) assert factor_info == 0 + hessenberg = np.triu(factor, -1) prik_q, f2py_q = factor.copy(order="F"), factor.copy(order="F") prik_scalars = prik_lapack.dorghr( - np.int32(2), np.int32(1), np.int32(2), prik_q, np.int32(2), tau, np.empty(64), np.int32(64), np.int32(0) + np.int32(3), np.int32(1), np.int32(3), prik_q, np.int32(3), tau, np.empty(64), np.int32(64), np.int32(0) ) - f2py_result = f2py_lapack.dorghr(2, 1, 2, f2py_q, tau, np.empty(64), 64, 0) - scipy_q, scipy_info = scipy_lapack.dorghr(factor.copy(order="F"), tau, lo=0, hi=1, lwork=64) + f2py_result = f2py_lapack.dorghr(3, 1, 3, f2py_q, tau, np.empty(64), 64, 0) + scipy_q, scipy_info = scipy_lapack.dorghr(factor.copy(order="F"), tau, lo=0, hi=2, lwork=64) assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 for q in (prik_q, f2py_q, scipy_q): assert_orthogonal(q) - assert_allclose_float64(q.T @ matrix @ q, matrix, operation_size=2) + assert_allclose_float64(q.T @ matrix @ q, hessenberg, operation_size=3) def test_dtrexc_reorders_real_schur_blocks(prik_lapack, scipy_lapack, f2py_lapack): @@ -179,7 +199,7 @@ def test_dtrsen_reorders_selected_schur_eigenvalue(prik_lapack, scipy_lapack, f2 prik_wr, prik_wi = np.empty(2), np.empty(2) f2py_wr, f2py_wi = np.empty(2), np.empty(2) selection = np.array([False, True], dtype=np.bool_) - prik_selection = gfortran_logical_mask(selection) + prik_selection = selection.copy() prik_scalars = prik_lapack.dtrsen( "N", diff --git a/examples/lapack/tests/test_eigen_symmetric.py b/examples/lapack/tests/test_eigen_symmetric.py index 1fde5d207..a98f47123 100644 --- a/examples/lapack/tests/test_eigen_symmetric.py +++ b/examples/lapack/tests/test_eigen_symmetric.py @@ -11,10 +11,10 @@ pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] -def _diagonal_problem(): - matrix = np.diag([2.0, 3.0]).astype(np.float64, order="F") - diagonal = np.array([2.0, 3.0], dtype=np.float64) - offdiag = np.array([0.0], dtype=np.float64) +def _symmetric_problem(): + matrix = np.array([[2.5, 0.5], [0.5, 2.5]], dtype=np.float64, order="F") + diagonal = np.array([2.5, 2.5], dtype=np.float64) + offdiag = np.array([0.5], dtype=np.float64) return matrix, diagonal, offdiag @@ -25,7 +25,7 @@ def _assert_eigensystem(matrix, values, vectors): def test_dpteqr_diagonalizes_positive_definite_tridiagonal(prik_lapack, scipy_lapack, f2py_lapack): - matrix, diagonal, offdiag = _diagonal_problem() + matrix, diagonal, offdiag = _symmetric_problem() prik_d, f2py_d = diagonal.copy(), diagonal.copy() prik_e, f2py_e = offdiag.copy(), offdiag.copy() prik_z, f2py_z = np.eye(2, dtype=np.float64, order="F"), np.eye(2, dtype=np.float64, order="F") @@ -43,7 +43,7 @@ def test_dpteqr_diagonalizes_positive_definite_tridiagonal(prik_lapack, scipy_la def test_dsbev_diagonalizes_symmetric_band_matrix(prik_lapack, scipy_lapack, f2py_lapack): - matrix, _diagonal, _offdiag = _diagonal_problem() + matrix, _diagonal, _offdiag = _symmetric_problem() band = symmetric_band_storage(matrix, 1, lower=False) prik_ab, f2py_ab = band.copy(order="F"), band.copy(order="F") prik_w, f2py_w = np.empty(2), np.empty(2) @@ -63,7 +63,7 @@ def test_dsbev_diagonalizes_symmetric_band_matrix(prik_lapack, scipy_lapack, f2p def test_dsbevd_diagonalizes_band_matrix_by_divide_and_conquer(prik_lapack, scipy_lapack, f2py_lapack): - matrix, _diagonal, _offdiag = _diagonal_problem() + matrix, _diagonal, _offdiag = _symmetric_problem() band = symmetric_band_storage(matrix, 1, lower=False) prik_ab, f2py_ab = band.copy(order="F"), band.copy(order="F") prik_w, f2py_w = np.empty(2), np.empty(2) @@ -98,7 +98,7 @@ def test_dsbevd_diagonalizes_band_matrix_by_divide_and_conquer(prik_lapack, scip def test_dsbevx_selects_all_symmetric_band_eigenpairs(prik_lapack, scipy_lapack, f2py_lapack): - matrix, _diagonal, _offdiag = _diagonal_problem() + matrix, _diagonal, _offdiag = _symmetric_problem() band = symmetric_band_storage(matrix, 1, lower=False) prik_ab, f2py_ab = band.copy(order="F"), band.copy(order="F") prik_w, f2py_w = np.empty(2), np.empty(2) @@ -165,7 +165,7 @@ def test_dsbevx_selects_all_symmetric_band_eigenpairs(prik_lapack, scipy_lapack, def test_dstebz_bisects_tridiagonal_eigenvalues(prik_lapack, scipy_lapack, f2py_lapack): - _matrix, diagonal, offdiag = _diagonal_problem() + _matrix, diagonal, offdiag = _symmetric_problem() prik_w, f2py_w = np.empty(2), np.empty(2) prik_iblock, f2py_iblock = np.empty(2, dtype=np.int32), np.empty(2, dtype=np.int32) prik_isplit, f2py_isplit = np.empty(2, dtype=np.int32), np.empty(2, dtype=np.int32) @@ -216,18 +216,21 @@ def test_dstebz_bisects_tridiagonal_eigenvalues(prik_lapack, scipy_lapack, f2py_ assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[7] == scipy_m == 2 + prik_m = int(prik_scalars[6]) + prik_nsplit = int(prik_scalars[7]) + assert prik_m == scipy_m == 2 + assert prik_nsplit == 1 assert_allclose_float64(prik_w[:2], [2.0, 3.0]) assert_allclose_float64(f2py_w[:2], [2.0, 3.0]) assert_allclose_float64(scipy_w[:2], [2.0, 3.0]) np.testing.assert_array_equal(prik_iblock, scipy_iblock) np.testing.assert_array_equal(f2py_iblock, scipy_iblock) - np.testing.assert_array_equal(prik_isplit, scipy_isplit) - np.testing.assert_array_equal(f2py_isplit, scipy_isplit) + np.testing.assert_array_equal(prik_isplit[:prik_nsplit], scipy_isplit[:prik_nsplit]) + np.testing.assert_array_equal(f2py_isplit[:prik_nsplit], scipy_isplit[:prik_nsplit]) def test_dstein_computes_tridiagonal_eigenvectors(prik_lapack, scipy_lapack, f2py_lapack): - matrix, diagonal, offdiag = _diagonal_problem() + matrix, diagonal, offdiag = _symmetric_problem() m, values, iblock, isplit, split_info = scipy_lapack.dstebz(diagonal, offdiag, 0, 0.0, 0.0, 1, 2, 0.0, b"E") assert split_info == 0 and m == 2 prik_z, f2py_z = np.empty((2, 2), order="F"), np.empty((2, 2), order="F") @@ -274,7 +277,7 @@ def test_dstein_computes_tridiagonal_eigenvectors(prik_lapack, scipy_lapack, f2p def test_dstemr_computes_robust_tridiagonal_eigenpairs(prik_lapack, scipy_lapack, f2py_lapack): - matrix, diagonal, offdiag = _diagonal_problem() + matrix, diagonal, offdiag = _symmetric_problem() prik_d, f2py_d = diagonal.copy(), diagonal.copy() prik_e, f2py_e = np.array([offdiag[0], 0.0]), np.array([offdiag[0], 0.0]) prik_w, f2py_w = np.empty(2), np.empty(2) @@ -327,7 +330,16 @@ def test_dstemr_computes_robust_tridiagonal_eigenpairs(prik_lapack, scipy_lapack 0, ) scipy_m, scipy_w, scipy_z, scipy_info = scipy_lapack.dstemr( - diagonal, np.array([0.0, 0.0]), 0, 0.0, 0.0, 1, 2, compute_v=1, lwork=128, liwork=64 + diagonal, + np.array([offdiag[0], 0.0]), + 0, + 0.0, + 0.0, + 1, + 2, + compute_v=1, + lwork=128, + liwork=64, ) assert f2py_result is None @@ -339,7 +351,7 @@ def test_dstemr_computes_robust_tridiagonal_eigenpairs(prik_lapack, scipy_lapack def test_dsterf_computes_tridiagonal_eigenvalues(prik_lapack, scipy_lapack, f2py_lapack): - _matrix, diagonal, offdiag = _diagonal_problem() + _matrix, diagonal, offdiag = _symmetric_problem() prik_d, f2py_d = diagonal.copy(), diagonal.copy() prik_e, f2py_e = offdiag.copy(), offdiag.copy() @@ -355,7 +367,7 @@ def test_dsterf_computes_tridiagonal_eigenvalues(prik_lapack, scipy_lapack, f2py def test_dstev_computes_tridiagonal_eigenpairs(prik_lapack, scipy_lapack, f2py_lapack): - matrix, diagonal, offdiag = _diagonal_problem() + matrix, diagonal, offdiag = _symmetric_problem() prik_d, f2py_d = diagonal.copy(), diagonal.copy() prik_e, f2py_e = offdiag.copy(), offdiag.copy() prik_z, f2py_z = np.empty((2, 2), order="F"), np.empty((2, 2), order="F") @@ -372,7 +384,7 @@ def test_dstev_computes_tridiagonal_eigenpairs(prik_lapack, scipy_lapack, f2py_l def test_dstevd_computes_divide_and_conquer_tridiagonal_eigenpairs(prik_lapack, scipy_lapack, f2py_lapack): - matrix, diagonal, offdiag = _diagonal_problem() + matrix, diagonal, offdiag = _symmetric_problem() prik_d, f2py_d = diagonal.copy(), diagonal.copy() prik_e, f2py_e = offdiag.copy(), offdiag.copy() prik_z, f2py_z = np.empty((2, 2), order="F"), np.empty((2, 2), order="F") @@ -429,7 +441,7 @@ def test_dsyev_returns_orthonormal_eigenvectors(prik_lapack, scipy_lapack, f2py_ def test_dsyevd_computes_divide_and_conquer_symmetric_eigenpairs(prik_lapack, scipy_lapack, f2py_lapack): - matrix, _diagonal, _offdiag = _diagonal_problem() + matrix, _diagonal, _offdiag = _symmetric_problem() prik_a, f2py_a = matrix.copy(order="F"), matrix.copy(order="F") prik_w, f2py_w = np.empty(2), np.empty(2) @@ -461,7 +473,7 @@ def test_dsyevd_computes_divide_and_conquer_symmetric_eigenpairs(prik_lapack, sc def test_dsyevr_selects_symmetric_eigenpairs_by_index(prik_lapack, scipy_lapack, f2py_lapack): - matrix, _diagonal, _offdiag = _diagonal_problem() + matrix, _diagonal, _offdiag = _symmetric_problem() prik_a, f2py_a = matrix.copy(order="F"), matrix.copy(order="F") prik_w, f2py_w = np.empty(2), np.empty(2) prik_z, f2py_z = np.empty((2, 2), order="F"), np.empty((2, 2), order="F") @@ -524,7 +536,7 @@ def test_dsyevr_selects_symmetric_eigenpairs_by_index(prik_lapack, scipy_lapack, def test_dsyevx_selects_symmetric_eigenpairs_by_value(prik_lapack, scipy_lapack, f2py_lapack): - matrix, _diagonal, _offdiag = _diagonal_problem() + matrix, _diagonal, _offdiag = _symmetric_problem() prik_a, f2py_a = matrix.copy(order="F"), matrix.copy(order="F") prik_w, f2py_w = np.empty(2), np.empty(2) prik_z, f2py_z = np.empty((2, 2), order="F"), np.empty((2, 2), order="F") @@ -587,26 +599,29 @@ def test_dsyevx_selects_symmetric_eigenpairs_by_value(prik_lapack, scipy_lapack, def test_dsytrd_reduces_symmetric_matrix_to_tridiagonal(prik_lapack, scipy_lapack, f2py_lapack): - matrix, diagonal, offdiag = _diagonal_problem() + matrix = np.array( + [[4.0, 1.0, 2.0], [1.0, 3.0, -1.0], [2.0, -1.0, 5.0]], + dtype=np.float64, + order="F", + ) prik_a, f2py_a = matrix.copy(order="F"), matrix.copy(order="F") - prik_d, f2py_d = np.empty(2), np.empty(2) - prik_e, f2py_e = np.empty(1), np.empty(1) - prik_tau, f2py_tau = np.empty(1), np.empty(1) + prik_d, f2py_d = np.empty(3), np.empty(3) + prik_e, f2py_e = np.empty(2), np.empty(2) + prik_tau, f2py_tau = np.empty(2), np.empty(2) prik_scalars = prik_lapack.dsytrd( - "U", np.int32(2), prik_a, np.int32(2), prik_d, prik_e, prik_tau, np.empty(64), np.int32(64), np.int32(0) + "U", np.int32(3), prik_a, np.int32(3), prik_d, prik_e, prik_tau, np.empty(64), np.int32(64), np.int32(0) ) - f2py_result = f2py_lapack.dsytrd(b"U", 2, f2py_a, f2py_d, f2py_e, f2py_tau, np.empty(64), 64, 0) + f2py_result = f2py_lapack.dsytrd(b"U", 3, f2py_a, f2py_d, f2py_e, f2py_tau, np.empty(64), 64, 0) scipy_a, scipy_d, scipy_e, scipy_tau, scipy_info = scipy_lapack.dsytrd(matrix.copy(order="F"), lower=0, lwork=64) assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert_allclose_float64(prik_d, diagonal) - assert_allclose_float64(f2py_d, diagonal) - assert_allclose_float64(scipy_d, diagonal) - assert_allclose_float64(prik_e, offdiag) - assert_allclose_float64(f2py_e, offdiag) - assert_allclose_float64(scipy_e, offdiag) + expected_eigenvalues = np.linalg.eigvalsh(matrix) + for diagonal, offdiagonal in ((prik_d, prik_e), (f2py_d, f2py_e), (scipy_d, scipy_e)): + tridiagonal = np.diag(diagonal) + np.diag(offdiagonal, 1) + np.diag(offdiagonal, -1) + assert_allclose_float64(np.linalg.eigvalsh(tridiagonal), expected_eigenvalues, operation_size=3) + assert np.any(np.abs(offdiagonal) > np.finfo(np.float64).eps) assert_allclose_float64(prik_a, scipy_a) assert_allclose_float64(f2py_a, scipy_a) assert_allclose_float64(prik_tau, scipy_tau) diff --git a/examples/lapack/tests/test_linear_banded_tridiagonal.py b/examples/lapack/tests/test_linear_banded_tridiagonal.py index ef2b8d6a9..efc78dbe4 100644 --- a/examples/lapack/tests/test_linear_banded_tridiagonal.py +++ b/examples/lapack/tests/test_linear_banded_tridiagonal.py @@ -10,6 +10,7 @@ general_band_storage, native_pivots, symmetric_band_storage, + tridiagonal_matrix, ) @@ -34,6 +35,11 @@ def _general_tridiagonal_factorization(): return lower, diagonal, upper, rhs, expected +def _upper_cholesky_from_band(factor: np.ndarray) -> np.ndarray: + """Unpack a one-superdiagonal upper Cholesky factor.""" + return np.array([[factor[1, 0], factor[0, 1]], [0.0, factor[1, 1]]], dtype=np.float64) + + def test_dgbcon_estimates_general_band_condition(prik_lapack, scipy_lapack, f2py_lapack): factor = np.array([[4.0]], dtype=np.float64, order="F") native_ipiv = np.array([1], dtype=np.int32) @@ -106,6 +112,7 @@ def test_dgbsv_solves_general_band_system(prik_lapack, scipy_lapack, f2py_lapack assert_allclose_float64(prik_ab, scipy_lu) assert_allclose_float64(f2py_ab, scipy_lu) np.testing.assert_array_equal(prik_piv, native_pivots(scipy_piv)) + np.testing.assert_array_equal(f2py_piv, native_pivots(scipy_piv)) def test_dgbtrf_factorizes_general_band_matrix(prik_lapack, scipy_lapack, f2py_lapack): @@ -282,8 +289,8 @@ def test_dgtsvx_solves_and_bounds_tridiagonal_error(prik_lapack, scipy_lapack, f np.empty(2, dtype=np.int32), 0, ) - _dlf, _df, _duf, _du2, _piv, scipy_x, scipy_rcond, scipy_ferr, scipy_berr, scipy_info = scipy_lapack.dgtsvx( - lower, diagonal, upper, rhs.copy(order="F") + scipy_dlf, scipy_df, scipy_duf, scipy_du2, scipy_piv, scipy_x, scipy_rcond, scipy_ferr, scipy_berr, scipy_info = ( + scipy_lapack.dgtsvx(lower, diagonal, upper, rhs.copy(order="F")) ) assert f2py_result is None @@ -294,6 +301,21 @@ def test_dgtsvx_solves_and_bounds_tridiagonal_error(prik_lapack, scipy_lapack, f assert_allclose_float64(prik_scalars[-2], scipy_rcond, operation_size=2) assert_allclose_float64(prik_ferr, scipy_ferr) assert_allclose_float64(prik_berr, scipy_berr) + assert_allclose_float64(f2py_ferr, scipy_ferr) + assert_allclose_float64(f2py_berr, scipy_berr) + for actual, expected_factor in ( + (prik_dlf, scipy_dlf), + (f2py_dlf, scipy_dlf), + (prik_df, scipy_df), + (f2py_df, scipy_df), + (prik_duf, scipy_duf), + (f2py_duf, scipy_duf), + (prik_du2, scipy_du2), + (f2py_du2, scipy_du2), + ): + assert_allclose_float64(actual, expected_factor) + np.testing.assert_array_equal(prik_piv, scipy_piv) + np.testing.assert_array_equal(f2py_piv, scipy_piv) def test_dgttrf_factorizes_general_tridiagonal_matrix(prik_lapack, scipy_lapack, f2py_lapack): @@ -358,6 +380,9 @@ def test_dpbsv_solves_positive_definite_band_system(prik_lapack, scipy_lapack, f assert prik_scalars[-1] == scipy_info == 0 assert_allclose_float64(prik_ab, scipy_factor, operation_size=2) assert_allclose_float64(f2py_ab, scipy_factor, operation_size=2) + for factor in (prik_ab, f2py_ab, scipy_factor): + upper = _upper_cholesky_from_band(factor) + assert_allclose_float64(upper.T @ upper, matrix, operation_size=2) assert_allclose_float64(prik_b, [[1.0], [2.0]], operation_size=2) assert_allclose_float64(f2py_b, [[1.0], [2.0]], operation_size=2) assert_allclose_float64(scipy_x, [[1.0], [2.0]], operation_size=2) @@ -376,6 +401,9 @@ def test_dpbtrf_factorizes_positive_definite_band_matrix(prik_lapack, scipy_lapa assert prik_scalars[-1] == scipy_info == 0 assert_allclose_float64(prik_ab, scipy_factor, operation_size=2) assert_allclose_float64(f2py_ab, scipy_factor, operation_size=2) + for factor in (prik_ab, f2py_ab, scipy_factor): + upper = _upper_cholesky_from_band(factor) + assert_allclose_float64(upper.T @ upper, matrix, operation_size=2) def test_dpbtrs_solves_from_positive_definite_band_factor(prik_lapack, scipy_lapack, f2py_lapack): @@ -471,10 +499,17 @@ def test_dptsvx_solves_and_bounds_spd_tridiagonal_error(prik_lapack, scipy_lapac assert_allclose_float64(prik_scalars[-2], scipy_rcond, operation_size=2) assert_allclose_float64(prik_ferr, scipy_ferr) assert_allclose_float64(prik_berr, scipy_berr) + assert_allclose_float64(f2py_ferr, scipy_ferr) + assert_allclose_float64(f2py_berr, scipy_berr) + assert_allclose_float64(prik_df, _df) + assert_allclose_float64(f2py_df, _df) + assert_allclose_float64(prik_ef, _ef) + assert_allclose_float64(f2py_ef, _ef) def test_dpttrf_factorizes_spd_tridiagonal_matrix(prik_lapack, scipy_lapack, f2py_lapack): _lower, diagonal, offdiag, _rhs, _expected = _general_tridiagonal() + matrix = tridiagonal_matrix(offdiag, diagonal, offdiag) prik_d, f2py_d = diagonal.copy(), diagonal.copy() prik_e, f2py_e = offdiag.copy(), offdiag.copy() @@ -488,6 +523,10 @@ def test_dpttrf_factorizes_spd_tridiagonal_matrix(prik_lapack, scipy_lapack, f2p assert_allclose_float64(prik_e, scipy_e) assert_allclose_float64(f2py_d, scipy_d) assert_allclose_float64(f2py_e, scipy_e) + for factor_d, factor_e in ((prik_d, prik_e), (f2py_d, f2py_e), (scipy_d, scipy_e)): + lower = np.eye(2, dtype=np.float64) + lower[1, 0] = factor_e[0] + assert_allclose_float64(lower @ np.diag(factor_d) @ lower.T, matrix, operation_size=2) def test_dpttrs_solves_from_spd_tridiagonal_factor(prik_lapack, scipy_lapack, f2py_lapack): diff --git a/examples/lapack/tests/test_linear_general.py b/examples/lapack/tests/test_linear_general.py index 097cd7d60..26ad18ac8 100644 --- a/examples/lapack/tests/test_linear_general.py +++ b/examples/lapack/tests/test_linear_general.py @@ -199,6 +199,8 @@ def test_dgesvx_solves_and_reports_error_bounds(prik_lapack, scipy_lapack, f2py_ assert_allclose_float64(prik_scalars[-2], scipy_rcond) assert_allclose_float64(prik_ferr, scipy_ferr) assert_allclose_float64(prik_berr, scipy_berr) + assert_allclose_float64(f2py_ferr, scipy_ferr) + assert_allclose_float64(f2py_berr, scipy_berr) def test_dgetc2_factorizes_with_complete_pivoting(prik_lapack, scipy_lapack, f2py_lapack): @@ -218,6 +220,8 @@ def test_dgetc2_factorizes_with_complete_pivoting(prik_lapack, scipy_lapack, f2p assert_allclose_float64(scipy_lu, [[4.0]]) np.testing.assert_array_equal(prik_ipiv, native_pivots(scipy_ipiv)) np.testing.assert_array_equal(prik_jpiv, native_pivots(scipy_jpiv)) + np.testing.assert_array_equal(f2py_ipiv, native_pivots(scipy_ipiv)) + np.testing.assert_array_equal(f2py_jpiv, native_pivots(scipy_jpiv)) def test_dgetri_inverts_lu_factorization(prik_lapack, scipy_lapack, f2py_lapack): diff --git a/examples/lapack/tests/test_linear_positive_definite.py b/examples/lapack/tests/test_linear_positive_definite.py index 9fadc9cea..248c7015f 100644 --- a/examples/lapack/tests/test_linear_positive_definite.py +++ b/examples/lapack/tests/test_linear_positive_definite.py @@ -181,6 +181,8 @@ def test_dposvx_solves_and_bounds_spd_error(prik_lapack, scipy_lapack, f2py_lapa assert_allclose_float64(prik_scalars[-2], scipy_rcond) assert_allclose_float64(prik_ferr, scipy_ferr) assert_allclose_float64(prik_berr, scipy_berr) + assert_allclose_float64(f2py_ferr, scipy_ferr) + assert_allclose_float64(f2py_berr, scipy_berr) def test_dpotrf_reconstructs_spd_matrix(prik_lapack, scipy_lapack, f2py_lapack): diff --git a/examples/lapack/tests/test_linear_symmetric_indefinite.py b/examples/lapack/tests/test_linear_symmetric_indefinite.py index e8c730e2b..103c0b769 100644 --- a/examples/lapack/tests/test_linear_symmetric_indefinite.py +++ b/examples/lapack/tests/test_linear_symmetric_indefinite.py @@ -117,7 +117,7 @@ def test_dsysv_solves_symmetric_indefinite_system(prik_lapack, scipy_lapack, f2p np.int32(0), ) f2py_result = f2py_lapack.dsysv(b"U", 1, 1, f2py_a, f2py_piv, f2py_b, np.empty(8), 8, 0) - scipy_factor, _scipy_piv, scipy_x, scipy_info = scipy_lapack.dsysv( + scipy_factor, scipy_piv, scipy_x, scipy_info = scipy_lapack.dsysv( matrix.copy(order="F"), rhs.copy(order="F"), lwork=8 ) @@ -129,6 +129,8 @@ def test_dsysv_solves_symmetric_indefinite_system(prik_lapack, scipy_lapack, f2p assert_allclose_float64(prik_a, scipy_factor) assert_allclose_float64(f2py_a, scipy_factor) assert_allclose_float64(matrix @ prik_b, rhs) + np.testing.assert_array_equal(prik_piv, scipy_piv) + np.testing.assert_array_equal(f2py_piv, scipy_piv) def test_dsysvx_solves_and_bounds_symmetric_error(prik_lapack, scipy_lapack, f2py_lapack): @@ -180,7 +182,7 @@ def test_dsysvx_solves_and_bounds_symmetric_error(prik_lapack, scipy_lapack, f2p np.empty(1, dtype=np.int32), 0, ) - _a, scipy_factor, _piv, _b, scipy_x, scipy_rcond, scipy_ferr, scipy_berr, scipy_info = scipy_lapack.dsysvx( + _a, scipy_factor, scipy_piv, _b, scipy_x, scipy_rcond, scipy_ferr, scipy_berr, scipy_info = scipy_lapack.dsysvx( matrix.copy(order="F"), rhs.copy(order="F"), lwork=3 ) @@ -194,6 +196,10 @@ def test_dsysvx_solves_and_bounds_symmetric_error(prik_lapack, scipy_lapack, f2p assert_allclose_float64(prik_scalars[-3], scipy_rcond) assert_allclose_float64(prik_ferr, scipy_ferr) assert_allclose_float64(prik_berr, scipy_berr) + assert_allclose_float64(f2py_ferr, scipy_ferr) + assert_allclose_float64(f2py_berr, scipy_berr) + np.testing.assert_array_equal(prik_piv, scipy_piv) + np.testing.assert_array_equal(f2py_piv, scipy_piv) def test_dsytf2_factorizes_symmetric_indefinite_matrix(prik_lapack, scipy_lapack, f2py_lapack): diff --git a/examples/lapack/tests/test_svd.py b/examples/lapack/tests/test_svd.py index b73603fbb..eb9baf8a4 100644 --- a/examples/lapack/tests/test_svd.py +++ b/examples/lapack/tests/test_svd.py @@ -12,7 +12,8 @@ def test_dgejsv_reconstructs_matrix_with_jacobi_svd(prik_lapack, scipy_lapack, f2py_lapack): - matrix = np.diag([3.0, 2.0]).astype(np.float64, order="F") + matrix = np.array([[3.0, 1.0], [0.0, 2.0]], dtype=np.float64, order="F") + expected_values = np.linalg.svd(matrix, compute_uv=False) prik_a, f2py_a = matrix.copy(order="F"), matrix.copy(order="F") prik_s, f2py_s = np.empty(2), np.empty(2) prik_u, f2py_u = np.empty((2, 2), order="F"), np.empty((2, 2), order="F") @@ -62,7 +63,7 @@ def test_dgejsv_reconstructs_matrix_with_jacobi_svd(prik_lapack, scipy_lapack, f assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 for values, u, v in ((prik_s, prik_u, prik_v), (f2py_s, f2py_u, f2py_v), (scipy_s, scipy_u, scipy_v)): - assert_allclose_float64(values, [3.0, 2.0], operation_size=2) + assert_allclose_float64(values, expected_values, operation_size=2) assert_orthogonal(u) assert_orthogonal(v) assert_allclose_float64(u @ np.diag(values) @ v.T, matrix, operation_size=2) @@ -153,10 +154,13 @@ def test_dgesvd_reconstructs_matrix(prik_lapack, scipy_lapack, f2py_lapack): def test_dorcsd_decomposes_partitioned_orthogonal_matrix(prik_lapack, scipy_lapack, f2py_lapack): - x11 = np.array([[1.0]], dtype=np.float64, order="F") - x12 = np.array([[0.0]], dtype=np.float64, order="F") - x21 = np.array([[0.0]], dtype=np.float64, order="F") - x22 = np.array([[1.0]], dtype=np.float64, order="F") + angle = 0.4 + cosine_value = np.cos(angle) + sine_value = np.sin(angle) + x11 = np.array([[cosine_value]], dtype=np.float64, order="F") + x12 = np.array([[-sine_value]], dtype=np.float64, order="F") + x21 = np.array([[sine_value]], dtype=np.float64, order="F") + x22 = np.array([[cosine_value]], dtype=np.float64, order="F") prik_blocks = [block.copy(order="F") for block in (x11, x12, x21, x22)] f2py_blocks = [block.copy(order="F") for block in (x11, x12, x21, x22)] prik_theta, f2py_theta = np.empty(1), np.empty(1) @@ -231,9 +235,14 @@ def test_dorcsd_decomposes_partitioned_orthogonal_matrix(prik_lapack, scipy_lapa (scipy_theta, scipy_u1, scipy_u2, scipy_v1t, scipy_v2t), ): cosine = np.array([[np.cos(theta[0])]]) - assert_allclose_float64(theta, [0.0]) - assert_allclose_float64(u1 @ cosine @ v1t, x11) - assert_allclose_float64(u2 @ cosine @ v2t, x22) + sine = np.array([[np.sin(theta[0])]]) + assert_allclose_float64(theta, [angle]) + for factor in (u1, u2, v1t, v2t): + assert_allclose_float64(factor.T @ factor, np.eye(1)) + assert_allclose_float64(np.abs(u1 @ cosine @ v1t), np.abs(x11)) + assert_allclose_float64(np.abs(u1 @ sine @ v2t), np.abs(x12)) + assert_allclose_float64(np.abs(u2 @ sine @ v1t), np.abs(x21)) + assert_allclose_float64(np.abs(u2 @ cosine @ v2t), np.abs(x22)) def test_dlasd4_solves_rank_one_secular_equation(prik_lapack, scipy_lapack, f2py_lapack): diff --git a/examples/minpack/README.md b/examples/minpack/README.md new file mode 100644 index 000000000..278a58667 --- /dev/null +++ b/examples/minpack/README.md @@ -0,0 +1,92 @@ +# Wrap MINPACK with PRIK + +Build the bundled modern +[fortran-lang/minpack](https://github.com/fortran-lang/minpack) source with +PRIK and validate all 22 public procedures against SciPy, direct +linear-algebra identities, and deterministic nonlinear problems. + +No f2py comparison wrapper is required, and the inventory has no unsupported +or skipped procedures. + +## Requirements + +Install GNU Fortran. On Ubuntu: + +```console +sudo apt-get update +sudo apt-get install --yes gfortran +``` + +Install the pinned numerical tools: + +```console +python3 -m pip install "numpy==2.5.1" "scipy==1.18.0" pytest +``` + +Run the remaining commands from the PRIK repository root. + +## Quick start + +Build the extension and run the complete test suite: + +```bash +source examples/minpack/build_all.sh +python3 -m pytest -q examples/minpack/tests +``` + +Use `source` so the build directory exported by `build_all.sh` remains on +`PYTHONPATH` for pytest. + +## How the build works + +PRIK reads and compiles the checked-in `minpack.f90` public module together +with its generated bridge. Each source is compiled once and no alternative +wrapper is created. + +### Build the PRIK wrapper + + +```bash +export EXAMPLE_WORKSPACE="$PWD" +export MINPACK_BUILD_ROOT="$(mktemp -d)" + +mkdir -p "$MINPACK_BUILD_ROOT/prik/generated" +cd "$MINPACK_BUILD_ROOT/prik" + +python3 -m prik "$EXAMPLE_WORKSPACE/examples/minpack/native/minpack.f90" \ + --out prik_reference_minpack \ + --out-dir "$MINPACK_BUILD_ROOT/prik/generated" \ + --compiler "$(command -v gfortran)" \ + --jobs 8 \ + --wrapper-fortran-flags="-O0 -g0" \ + --wrapper-c-flags="-O0 -g0" +``` + +## Run focused tests + +After the quick-start build, run one family or routine: + +```bash +python3 -m pytest -q examples/minpack/tests/test_solvers.py +python3 -m pytest -q \ + examples/minpack/tests/test_solvers.py::test_hybrd1 +``` + +## What is validated + +The suite covers all 22 public diagnostics, finite-difference helpers, hybrid +solvers, Levenberg-Marquardt solvers, factorizations, and rank-one updates. +Solver tests check residuals and statuses against known systems and SciPy; +helper tests use nontrivial algebraic invariants. Tests also verify callback +counts, caller-array writebacks, and Fortran-order matrices. + +The public routine list stays in sync with the generated exports, and every +public procedure is exercised. + +## Sources and license + +[`native/minpack.f90`](native/minpack.f90) matches upstream `src/minpack.f90` +at +[fortran-lang/minpack commit `c0b5aea9fcd2b83865af921a7a7e881904f8d3c2`](https://github.com/fortran-lang/minpack/tree/c0b5aea9fcd2b83865af921a7a7e881904f8d3c2). +See the upstream repository, API documentation, and license before +redistributing the bundled native source. diff --git a/examples/minpack/__init__.py b/examples/minpack/__init__.py new file mode 100644 index 000000000..d2b3e10dd --- /dev/null +++ b/examples/minpack/__init__.py @@ -0,0 +1 @@ +"""Reference MINPACK build and numerical-validation example.""" diff --git a/examples/minpack/build_all.sh b/examples/minpack/build_all.sh new file mode 100644 index 000000000..f649089e0 --- /dev/null +++ b/examples/minpack/build_all.sh @@ -0,0 +1,3 @@ +source examples/minpack/build_prik.sh +cd "$EXAMPLE_WORKSPACE" +export PYTHONPATH="$MINPACK_BUILD_ROOT/prik${PYTHONPATH:+:$PYTHONPATH}" diff --git a/examples/minpack/build_prik.sh b/examples/minpack/build_prik.sh new file mode 100644 index 000000000..76a0026c8 --- /dev/null +++ b/examples/minpack/build_prik.sh @@ -0,0 +1,13 @@ +export EXAMPLE_WORKSPACE="$PWD" +export MINPACK_BUILD_ROOT="$(mktemp -d)" + +mkdir -p "$MINPACK_BUILD_ROOT/prik/generated" +cd "$MINPACK_BUILD_ROOT/prik" + +python3 -m prik "$EXAMPLE_WORKSPACE/examples/minpack/native/minpack.f90" \ + --out prik_reference_minpack \ + --out-dir "$MINPACK_BUILD_ROOT/prik/generated" \ + --compiler "$(command -v gfortran)" \ + --jobs 8 \ + --wrapper-fortran-flags="-O0 -g0" \ + --wrapper-c-flags="-O0 -g0" diff --git a/examples/minpack/conftest.py b/examples/minpack/conftest.py new file mode 100644 index 000000000..63852ec90 --- /dev/null +++ b/examples/minpack/conftest.py @@ -0,0 +1,11 @@ +"""Import the MINPACK extension built by ``build_all.sh``.""" + +import importlib + +import pytest + + +@pytest.fixture(scope="session") +def minpack(): + """Return the public MINPACK module namespace.""" + return importlib.import_module("prik_reference_minpack").minpack_module diff --git a/examples/minpack/native/minpack.f90 b/examples/minpack/native/minpack.f90 new file mode 100644 index 000000000..709445be5 --- /dev/null +++ b/examples/minpack/native/minpack.f90 @@ -0,0 +1,3832 @@ +!***************************************************************************************** +!> +! Modernized Minpack +! +!### Authors +! * argonne national laboratory. minpack project. march 1980. +! burton s. garbow, kenneth e. hillstrom, jorge j. more. +! * Jacob Williams, Sept 2021, updated to modern standards. + +module minpack_module + + use iso_fortran_env, only: wp => real64 + + implicit none + + real(wp), dimension(3), parameter :: dpmpar = [epsilon(1.0_wp), & + tiny(1.0_wp), & + huge(1.0_wp)] !! machine constants + + real(wp), parameter, private :: epsmch = dpmpar(1) !! the machine precision + real(wp), parameter, private :: one = 1.0_wp + real(wp), parameter, private :: zero = 0.0_wp + + abstract interface + subroutine func(n, x, fvec, iflag) + !! user-supplied subroutine for [[hybrd]], [[hybrd1]], and [[fdjac1]] + import :: wp + implicit none + integer, intent(in) :: n !! the number of variables. + real(wp), intent(in) :: x(n) !! independent variable vector + real(wp), intent(out) :: fvec(n) !! value of function at `x` + integer, intent(inout) :: iflag !! set to <0 to terminate execution + end subroutine func + + subroutine func2(m, n, x, fvec, iflag) + !! user-supplied subroutine for [[fdjac2]], [[lmdif]], and [[lmdif1]] + import :: wp + implicit none + integer, intent(in) :: m !! the number of functions. + integer, intent(in) :: n !! the number of variables. + real(wp), intent(in) :: x(n) !! independent variable vector + real(wp), intent(out) :: fvec(m) !! value of function at `x` + integer, intent(inout) :: iflag !! the value of iflag should not be changed unless + !! the user wants to terminate execution of lmdif. + !! in this case set iflag to a negative integer. + end subroutine func2 + + subroutine fcn_hybrj(n, x, fvec, fjac, ldfjac, iflag) + !! user-supplied subroutine for [[hybrj]] and [[hybrj1]] + import :: wp + implicit none + integer, intent(in) :: n !! the number of variables. + real(wp), dimension(n), intent(in) :: x !! independent variable vector + integer, intent(in) :: ldfjac !! leading dimension of the array fjac. + real(wp), dimension(n), intent(inout) :: fvec !! value of function at `x` + real(wp), dimension(ldfjac, n), intent(inout) :: fjac !! jacobian matrix at `x` + integer, intent(inout) :: iflag !! if iflag = 1 calculate the functions at x and + !! return this vector in fvec. do not alter fjac. + !! if iflag = 2 calculate the jacobian at x and + !! return this matrix in fjac. do not alter fvec. + !! + !! the value of iflag should not be changed by fcn unless + !! the user wants to terminate execution of hybrj. + !! in this case set iflag to a negative integer. + end subroutine fcn_hybrj + + subroutine fcn_lmder(m, n, x, fvec, fjac, ldfjac, iflag) + !! user-supplied subroutine for [[lmder]] and [[lmder1]] + import :: wp + implicit none + integer, intent(in) :: m !! the number of functions. + integer, intent(in) :: n !! the number of variables. + integer, intent(in) :: ldfjac !! leading dimension of the array fjac. + integer, intent(inout) :: iflag !! if iflag = 1 calculate the functions at x and + !! return this vector in fvec. do not alter fjac. + !! if iflag = 2 calculate the jacobian at x and + !! return this matrix in fjac. do not alter fvec. + !! + !! the value of iflag should not be changed by fcn unless + !! the user wants to terminate execution of lmder. + !! in this case set iflag to a negative integer. + real(wp), intent(in) :: x(n) !! independent variable vector + real(wp), intent(inout) :: fvec(m) !! value of function at `x` + real(wp), intent(inout) :: fjac(ldfjac, n) !! jacobian matrix at `x` + end subroutine fcn_lmder + + subroutine fcn_lmstr(m, n, x, fvec, fjrow, iflag) + import :: wp + implicit none + integer, intent(in) :: m !! the number of functions. + integer, intent(in) :: n !! the number of variables. + integer, intent(inout) :: iflag !! if iflag = 1 calculate the functions at x and + !! return this vector in fvec. + !! if iflag = i calculate the (i-1)-st row of the + !! jacobian at x and return this vector in fjrow. + !! + !! the value of iflag should not be changed by fcn unless + !! the user wants to terminate execution of lmstr. + !! in this case set iflag to a negative integer. + real(wp), intent(in) :: x(n) !! independent variable vector + real(wp), intent(inout) :: fvec(m) !! value of function at `x` + real(wp), intent(inout) :: fjrow(n) !! jacobian row + end subroutine fcn_lmstr + + end interface + +contains +!***************************************************************************************** + +!***************************************************************************************** +!> +! this subroutine checks the gradients of m nonlinear functions +! in n variables, evaluated at a point x, for consistency with +! the functions themselves. +! +! the subroutine does not perform reliably if cancellation or +! rounding errors cause a severe loss of significance in the +! evaluation of a function. therefore, none of the components +! of x should be unusually small (in particular, zero) or any +! other value which may cause loss of significance. + + subroutine chkder(m, n, x, Fvec, Fjac, Ldfjac, Xp, Fvecp, Mode, Err) + + implicit none + + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of functions. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of variables. + integer, intent(in) :: Ldfjac !! a positive integer input parameter not less than m + !! which specifies the leading dimension of the array fjac. + integer, intent(in) :: Mode !! an integer input variable set to 1 on the first call + !! and 2 on the second. other values of mode are equivalent + !! to mode = 1. + !! + !! the user must call chkder twice, + !! first with mode = 1 and then with mode = 2. + !! + !! * mode = 1. **on input**, x must contain the point of evaluation. + !! **on output**, xp is set to a neighboring point. + !! + !! * mode = 2. **on input**, fvec must contain the functions and the + !! rows of fjac must contain the gradients + !! of the respective functions each evaluated + !! at x, and fvecp must contain the functions + !! evaluated at xp. + !! **on output**, err contains measures of correctness of + !! the respective gradients. + real(wp), intent(in) :: x(n) !! input array + real(wp), intent(in) :: Fvec(m) !! an array of length m. on input when mode = 2, + !! fvec must contain the functions evaluated at x. + real(wp), intent(in) :: Fjac(Ldfjac, n) !! an m by n array. on input when mode = 2, + !! the rows of fjac must contain the gradients of + !! the respective functions evaluated at x. + real(wp), intent(out) :: Xp(n) !! an array of length n. on output when mode = 1, + !! xp is set to a neighboring point of x. + real(wp), intent(in) :: Fvecp(m) !! an array of length m. on input when mode = 2, + !! fvecp must contain the functions evaluated at xp. + real(wp), intent(out) :: Err(m) !! an array of length m. on output when mode = 2, + !! err contains measures of correctness of the respective + !! gradients. if there is no severe loss of significance, + !! then if err(i) is 1.0 the i-th gradient is correct, + !! while if err(i) is 0.0 the i-th gradient is incorrect. + !! for values of err between 0.0 and 1.0, the categorization + !! is less certain. in general, a value of err(i) greater + !! than 0.5 indicates that the i-th gradient is probably + !! correct, while a value of err(i) less than 0.5 indicates + !! that the i-th gradient is probably incorrect. + + integer :: i, j + real(wp) :: temp + + real(wp), parameter :: eps = sqrt(epsmch) + real(wp), parameter :: factor = 100.0_wp + real(wp), parameter :: epsf = factor*epsmch + real(wp), parameter :: epslog = log10(eps) + + select case (Mode) + case (2) + Err = zero + do j = 1, n + temp = abs(x(j)) + if (temp == zero) temp = one + do i = 1, m + Err(i) = Err(i) + temp*Fjac(i, j) + end do + end do + do i = 1, m + temp = one + if (Fvec(i) /= zero .and. Fvecp(i) /= zero .and. abs(Fvecp(i) - Fvec(i)) >= epsf*abs(Fvec(i))) & + temp = eps*abs((Fvecp(i) - Fvec(i))/eps - Err(i))/(abs(Fvec(i)) + abs(Fvecp(i))) + Err(i) = one + if (temp > epsmch .and. temp < eps) Err(i) = (log10(temp) - epslog)/epslog + if (temp >= eps) Err(i) = zero + end do + case (1) + do j = 1, n + temp = eps*abs(x(j)) + if (temp == zero) temp = eps + Xp(j) = x(j) + temp + end do + case default + error stop 'invalid mode in chkder' + end select + + end subroutine chkder +!***************************************************************************************** + +!***************************************************************************************** +!> +! given an m by n matrix a, an n by n nonsingular diagonal +! matrix d, an m-vector b, and a positive number delta, the +! problem is to determine the convex combination x of the +! gauss-newton and scaled gradient directions that minimizes +! (a*x - b) in the least squares sense, subject to the +! restriction that the euclidean norm of d*x be at most delta. +! +! this subroutine completes the solution of the problem +! if it is provided with the necessary information from the +! qr factorization of a. that is, if a = q*r, where q has +! orthogonal columns and r is an upper triangular matrix, +! then dogleg expects the full upper triangle of r and +! the first n components of (q transpose)*b. + + subroutine dogleg(n, r, Lr, Diag, Qtb, Delta, x, Wa1, Wa2) + + implicit none + + integer, intent(in) :: n !! a positive integer input variable set to the order of r. + integer, intent(in) :: Lr !! a positive integer input variable not less than (n*(n+1))/2. + real(wp), intent(in) :: Delta !! a positive input variable which specifies an upper + !! bound on the euclidean norm of d*x. + real(wp), intent(in) :: r(Lr) !! an input array of length lr which must contain the upper + !! triangular matrix r stored by rows. + real(wp), intent(in) :: Diag(n) !! an input array of length n which must contain the + !! diagonal elements of the matrix d. + real(wp), intent(in) :: Qtb(n) !! an input array of length n which must contain the first + !! n elements of the vector (q transpose)*b. + real(wp), intent(out) :: x(n) !! an output array of length n which contains the desired + !! convex combination of the gauss-newton direction and the + !! scaled gradient direction. + real(wp), intent(inout) :: Wa1(n) !! work arrays of length n + real(wp), intent(inout) :: Wa2(n) !! work arrays of length n + + integer :: i, j, jj, jp1, k, l + real(wp) :: alpha, bnorm, gnorm, qnorm, sgnorm, sum, temp + + ! first, calculate the gauss-newton direction. + + jj = (n*(n + 1))/2 + 1 + do k = 1, n + j = n - k + 1 + jp1 = j + 1 + jj = jj - k + l = jj + 1 + sum = zero + if (n >= jp1) then + do i = jp1, n + sum = sum + r(l)*x(i) + l = l + 1 + end do + end if + temp = r(jj) + if (temp == zero) then + l = j + do i = 1, j + temp = max(temp, abs(r(l))) + l = l + n - i + end do + temp = epsmch*temp + if (temp == zero) temp = epsmch + end if + x(j) = (Qtb(j) - sum)/temp + end do + + ! test whether the gauss-newton direction is acceptable. + + do j = 1, n + Wa1(j) = zero + Wa2(j) = Diag(j)*x(j) + end do + qnorm = enorm(n, Wa2) + if (qnorm > Delta) then + + ! the gauss-newton direction is not acceptable. + ! next, calculate the scaled gradient direction. + + l = 1 + do j = 1, n + temp = Qtb(j) + do i = j, n + Wa1(i) = Wa1(i) + r(l)*temp + l = l + 1 + end do + Wa1(j) = Wa1(j)/Diag(j) + end do + + ! calculate the norm of the scaled gradient and test for + ! the special case in which the scaled gradient is zero. + + gnorm = enorm(n, Wa1) + sgnorm = zero + alpha = Delta/qnorm + if (gnorm /= zero) then + + ! calculate the point along the scaled gradient + ! at which the quadratic is minimized. + + do j = 1, n + Wa1(j) = (Wa1(j)/gnorm)/Diag(j) + end do + l = 1 + do j = 1, n + sum = zero + do i = j, n + sum = sum + r(l)*Wa1(i) + l = l + 1 + end do + Wa2(j) = sum + end do + temp = enorm(n, Wa2) + sgnorm = (gnorm/temp)/temp + + ! test whether the scaled gradient direction is acceptable. + + alpha = zero + if (sgnorm < Delta) then + + ! the scaled gradient direction is not acceptable. + ! finally, calculate the point along the dogleg + ! at which the quadratic is minimized. + + bnorm = enorm(n, Qtb) + temp = (bnorm/gnorm)*(bnorm/qnorm)*(sgnorm/Delta) + temp = temp - (Delta/qnorm)*(sgnorm/Delta)**2 + & + sqrt((temp - (Delta/qnorm))**2 + & + (one - (Delta/qnorm)**2)*(one - (sgnorm/Delta)**2)) + alpha = ((Delta/qnorm)*(one - (sgnorm/Delta)**2))/temp + end if + end if + + ! form appropriate convex combination of the gauss-newton + ! direction and the scaled gradient direction. + + temp = (one - alpha)*min(sgnorm, Delta) + do j = 1, n + x(j) = temp*Wa1(j) + alpha*x(j) + end do + end if + + end subroutine dogleg +!***************************************************************************************** + +!***************************************************************************************** +!> +! given an n-vector x, this function calculates the +! euclidean norm of x. +! +! the euclidean norm is computed by accumulating the sum of +! squares in three different sums. the sums of squares for the +! small and large components are scaled so that no overflows +! occur. non-destructive underflows are permitted. underflows +! and overflows do not occur in the computation of the unscaled +! sum of squares for the intermediate components. +! the definitions of small, intermediate and large components +! depend on two constants, rdwarf and rgiant. the main +! restrictions on these constants are that rdwarf**2 not +! underflow and rgiant**2 not overflow. the constants +! given here are suitable for every known computer. + + pure real(wp) function enorm(n, x) + + implicit none + + integer, intent(in) :: n !! a positive integer input variable. + real(wp), intent(in) :: x(n) !! an input array of length n. + + integer :: i + real(wp) :: agiant, s1, s2, s3, xabs, x1max, x3max + + real(wp), parameter :: rdwarf = 3.834e-20_wp + real(wp), parameter :: rgiant = 1.304e19_wp + + s1 = zero + s2 = zero + s3 = zero + x1max = zero + x3max = zero + agiant = rgiant/real(n, wp) + do i = 1, n + xabs = abs(x(i)) + if (xabs > rdwarf .and. xabs < agiant) then + ! sum for intermediate components. + s2 = s2 + xabs**2 + elseif (xabs <= rdwarf) then + ! sum for small components. + if (xabs <= x3max) then + if (xabs /= zero) s3 = s3 + (xabs/x3max)**2 + else + s3 = one + s3*(x3max/xabs)**2 + x3max = xabs + end if + ! sum for large components. + elseif (xabs <= x1max) then + s1 = s1 + (xabs/x1max)**2 + else + s1 = one + s1*(x1max/xabs)**2 + x1max = xabs + end if + end do + + ! calculation of norm. + + if (s1 /= zero) then + enorm = x1max*sqrt(s1 + (s2/x1max)/x1max) + elseif (s2 == zero) then + enorm = x3max*sqrt(s3) + else + if (s2 >= x3max) enorm = sqrt(s2*(one + (x3max/s2)*(x3max*s3))) + if (s2 < x3max) enorm = sqrt(x3max*((s2/x3max) + (x3max*s3))) + end if + + end function enorm +!***************************************************************************************** + +!***************************************************************************************** +!> +! this subroutine computes a forward-difference approximation +! to the n by n jacobian matrix associated with a specified +! problem of n functions in n variables. if the jacobian has +! a banded form, then function evaluations are saved by only +! approximating the nonzero terms. + + subroutine fdjac1(fcn, n, x, Fvec, Fjac, Ldfjac, Iflag, Ml, Mu, Epsfcn, Wa1, Wa2) + + implicit none + + procedure(func) :: fcn !! the user-supplied subroutine which + !! calculates the functions. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of functions and variables. + integer, intent(in) :: Ldfjac !! a positive integer input variable not less than n + !! which specifies the leading dimension of the array fjac. + integer, intent(inout) :: Iflag !! an integer variable which can be used to terminate + !! the execution of fdjac1. see description of [[func]]. + integer, intent(in) :: Ml !! a nonnegative integer input variable which specifies + !! the number of subdiagonals within the band of the + !! jacobian matrix. if the jacobian is not banded, set + !! ml to at least n - 1. + integer, intent(in) :: Mu !! a nonnegative integer input variable which specifies + !! the number of superdiagonals within the band of the + !! jacobian matrix. if the jacobian is not banded, set + !! mu to at least n - 1. + real(wp), intent(in) :: Epsfcn !! an input variable used in determining a suitable + !! step length for the forward-difference approximation. this + !! approximation assumes that the relative errors in the + !! functions are of the order of epsfcn. if epsfcn is less + !! than the machine precision, it is assumed that the relative + !! errors in the functions are of the order of the machine + !! precision. + real(wp), intent(inout) :: x(n) !! an input array of length n. + real(wp), intent(in) :: Fvec(n) !! an input array of length n which must contain the + !! functions evaluated at x. + real(wp), intent(out) :: Fjac(Ldfjac, n) !! an output n by n array which contains the + !! approximation to the jacobian matrix evaluated at x. + real(wp), intent(inout) :: Wa1(n) !! work array of length n. + real(wp), intent(inout) :: Wa2(n) !! work array of length n. if ml + mu + 1 is at + !! least n, then the jacobian is considered dense, and wa2 is + !! not referenced. + + integer :: i, j, k, msum + real(wp) :: eps, h, temp + + eps = sqrt(max(Epsfcn, epsmch)) + msum = Ml + Mu + 1 + if (msum < n) then + ! computation of banded approximate jacobian. + do k = 1, msum + do j = k, n, msum + Wa2(j) = x(j) + h = eps*abs(Wa2(j)) + if (h == zero) h = eps + x(j) = Wa2(j) + h + end do + call fcn(n, x, Wa1, Iflag) + if (Iflag < 0) return + do j = k, n, msum + x(j) = Wa2(j) + h = eps*abs(Wa2(j)) + if (h == zero) h = eps + do i = 1, n + Fjac(i, j) = zero + if (i >= j - Mu .and. i <= j + Ml) Fjac(i, j) = (Wa1(i) - Fvec(i))/h + end do + end do + end do + else + ! computation of dense approximate jacobian. + do j = 1, n + temp = x(j) + h = eps*abs(temp) + if (h == zero) h = eps + x(j) = temp + h + call fcn(n, x, Wa1, Iflag) + if (Iflag < 0) return + x(j) = temp + do i = 1, n + Fjac(i, j) = (Wa1(i) - Fvec(i))/h + end do + end do + end if + + end subroutine fdjac1 +!***************************************************************************************** + +!***************************************************************************************** +!> +! this subroutine computes a forward-difference approximation +! to the m by n jacobian matrix associated with a specified +! problem of m functions in n variables. + + subroutine fdjac2(fcn, m, n, x, Fvec, Fjac, Ldfjac, Iflag, Epsfcn, Wa) + + implicit none + + procedure(func2) :: fcn !! the user-supplied subroutine which + !! calculates the functions. + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of functions. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of variables. n must not exceed m. + integer, intent(in) :: Ldfjac !! a positive integer input variable not less than m + !! which specifies the leading dimension of the array fjac. + integer, intent(inout) :: Iflag !! an integer variable which can be used to terminate + !! the execution of fdjac2. see description of [[func2]]. + real(wp), intent(in) :: Epsfcn !! an input variable used in determining a suitable + !! step length for the forward-difference approximation. this + !! approximation assumes that the relative errors in the + !! functions are of the order of epsfcn. if epsfcn is less + !! than the machine precision, it is assumed that the relative + !! errors in the functions are of the order of the machine + !! precision. + real(wp), intent(inout) :: x(n) !! an input array of length n. + real(wp), intent(in) :: Fvec(m) !! an input array of length m which must contain the + !! functions evaluated at x. + real(wp), intent(out) :: Fjac(Ldfjac, n) !! an output m by n array which contains the + !! approximation to the jacobian matrix evaluated at x. + real(wp), intent(inout) :: Wa(m) !! a work array of length m. + + integer :: i, j + real(wp) :: eps, h, temp + + eps = sqrt(max(Epsfcn, epsmch)) + do j = 1, n + temp = x(j) + h = eps*abs(temp) + if (h == zero) h = eps + x(j) = temp + h + call fcn(m, n, x, Wa, Iflag) + if (Iflag < 0) return + x(j) = temp + do i = 1, m + Fjac(i, j) = (Wa(i) - Fvec(i))/h + end do + end do + + end subroutine fdjac2 +!***************************************************************************************** + +!***************************************************************************************** +!> +! the purpose of hybrd is to find a zero of a system of +! n nonlinear functions in n variables by a modification +! of the powell hybrid method. the user must provide a +! subroutine which calculates the functions. the jacobian is +! then calculated by a forward-difference approximation. + + subroutine hybrd(fcn, n, x, Fvec, Xtol, Maxfev, Ml, Mu, Epsfcn, Diag, Mode, & + Factor, Nprint, Info, Nfev, Fjac, Ldfjac, r, Lr, Qtf, Wa1, & + Wa2, Wa3, Wa4) + + implicit none + + procedure(func) :: fcn !! user-supplied subroutine which calculates the functions + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of functions and variables. + integer, intent(in) :: maxfev !! a positive integer input variable. termination + !! occurs when the number of calls to `fcn` is at least `maxfev` + !! by the end of an iteration. + integer, intent(in) :: ml !! a nonnegative integer input variable which specifies + !! the number of subdiagonals within the band of the + !! jacobian matrix. if the jacobian is not banded, set + !! `ml` to at least `n - 1`. + integer, intent(in) :: mu !! a nonnegative integer input variable which specifies + !! the number of superdiagonals within the band of the + !! jacobian matrix. if the jacobian is not banded, set + !! `mu` to at least` n - 1`. + integer, intent(in) :: mode !! if `mode = 1`, the + !! variables will be scaled internally. if `mode = 2`, + !! the scaling is specified by the input `diag`. other + !! values of `mode` are equivalent to `mode = 1`. + integer, intent(in) :: nprint !! an integer input variable that enables controlled + !! printing of iterates if it is positive. in this case, + !! `fcn` is called with `iflag = 0` at the beginning of the first + !! iteration and every `nprint` iterations thereafter and + !! immediately prior to return, with `x` and `fvec` available + !! for printing. if `nprint` is not positive, no special calls + !! of `fcn` with `iflag = 0` are made. + integer, intent(out) :: info !! an integer output variable. if the user has + !! terminated execution, `info` is set to the (negative) + !! value of `iflag`. see description of `fcn`. otherwise, + !! `info` is set as follows: + !! + !! * ***info = 0*** improper input parameters. + !! * ***info = 1*** relative error between two consecutive iterates + !! is at most `xtol`. + !! * ***info = 2*** number of calls to `fcn` has reached or exceeded + !! `maxfev`. + !! * ***info = 3*** `xtol` is too small. no further improvement in + !! the approximate solution `x` is possible. + !! * ***info = 4*** iteration is not making good progress, as + !! measured by the improvement from the last + !! five jacobian evaluations. + !! * ***info = 5*** iteration is not making good progress, as + !! measured by the improvement from the last + !! ten iterations. + integer, intent(out) :: nfev !! output variable set to the number of calls to `fcn`. + integer, intent(in):: ldfjac !! a positive integer input variable not less than `n` + !! which specifies the leading dimension of the array `fjac`. + integer, intent(in) :: lr !! a positive integer input variable not less than `(n*(n+1))/2`. + real(wp), intent(in) :: xtol !! a nonnegative input variable. termination + !! occurs when the relative error between two consecutive + !! iterates is at most `xtol`. + real(wp), intent(in) :: epsfcn !! an input variable used in determining a suitable + !! step length for the forward-difference approximation. this + !! approximation assumes that the relative errors in the + !! functions are of the order of `epsfcn`. if `epsfcn` is less + !! than the machine precision, it is assumed that the relative + !! errors in the functions are of the order of the machine + !! precision. + real(wp), intent(in) :: factor !! a positive input variable used in determining the + !! initial step bound. this bound is set to the product of + !! `factor` and the euclidean norm of `diag*x` if nonzero, or else + !! to `factor` itself. in most cases factor should lie in the + !! interval (.1,100.). 100. is a generally recommended value. + real(wp), intent(inout) :: x(n) !! array of length n. on input `x` must contain + !! an initial estimate of the solution vector. on output `x` + !! contains the final estimate of the solution vector. + real(wp), intent(out) :: fvec(n) !! an output array of length `n` which contains + !! the functions evaluated at the output `x`. + real(wp), intent(inout) :: diag(n) !! an array of length `n`. if `mode = 1` (see + !! below), `diag` is internally set. if `mode = 2`, `diag` + !! must contain positive entries that serve as + !! multiplicative scale factors for the variables. + real(wp), intent(out) :: fjac(ldfjac, n) !! array which contains the + !! orthogonal matrix `q` produced by the QR factorization + !! of the final approximate jacobian. + real(wp), intent(out) :: r(lr) !! an output array which contains the + !! upper triangular matrix produced by the QR factorization + !! of the final approximate jacobian, stored rowwise. + real(wp), intent(out) :: qtf(n) !! an output array of length `n` which contains + !! the vector `(q transpose)*fvec`. + real(wp), intent(inout) :: wa1(n) !! work array + real(wp), intent(inout) :: wa2(n) !! work array + real(wp), intent(inout) :: wa3(n) !! work array + real(wp), intent(inout) :: wa4(n) !! work array + + integer :: i, iflag, iter, j, jm1, l, msum, ncfail, ncsuc, nslow1, nslow2 + integer :: iwa(1) + logical :: jeval, sing + real(wp) :: actred, delta, fnorm, fnorm1, pnorm, prered, ratio, sum, temp, xnorm + + real(wp), parameter :: p1 = 1.0e-1_wp + real(wp), parameter :: p5 = 5.0e-1_wp + real(wp), parameter :: p001 = 1.0e-3_wp + real(wp), parameter :: p0001 = 1.0e-4_wp + + Info = 0 + iflag = 0 + Nfev = 0 + + main : block + + ! check the input parameters for errors. + + if (n <= 0 .or. Xtol < zero .or. Maxfev <= 0 .or. Ml < 0 .or. Mu < 0 .or. & + Factor <= zero .or. Ldfjac < n .or. Lr < (n*(n + 1))/2) exit main + if (Mode == 2) then + do j = 1, n + if (Diag(j) <= zero) exit main + end do + end if + + ! evaluate the function at the starting point + ! and calculate its norm. + + iflag = 1 + call fcn(n, x, Fvec, iflag) + Nfev = 1 + if (iflag < 0) exit main + fnorm = enorm(n, Fvec) + + ! determine the number of calls to fcn needed to compute + ! the jacobian matrix. + + msum = min(Ml + Mu + 1, n) + + ! initialize iteration counter and monitors. + + iter = 1 + ncsuc = 0 + ncfail = 0 + nslow1 = 0 + nslow2 = 0 + + ! beginning of the outer loop. + outer : do + + jeval = .true. + + ! calculate the jacobian matrix. + + iflag = 2 + call fdjac1(fcn, n, x, Fvec, Fjac, Ldfjac, iflag, Ml, Mu, Epsfcn, Wa1, Wa2) + Nfev = Nfev + msum + if (iflag < 0) exit main + + ! compute the qr factorization of the jacobian. + + call qrfac(n, n, Fjac, Ldfjac, .false., iwa, 1, Wa1, Wa2, Wa3) + + ! on the first iteration and if mode is 1, scale according + ! to the norms of the columns of the initial jacobian. + + if (iter == 1) then + if (Mode /= 2) then + do j = 1, n + Diag(j) = Wa2(j) + if (Wa2(j) == zero) Diag(j) = one + end do + end if + ! on the first iteration, calculate the norm of the scaled x + ! and initialize the step bound delta. + do j = 1, n + Wa3(j) = Diag(j)*x(j) + end do + xnorm = enorm(n, Wa3) + delta = Factor*xnorm + if (delta == zero) delta = Factor + end if + + ! form (q transpose)*fvec and store in qtf. + + do i = 1, n + Qtf(i) = Fvec(i) + end do + do j = 1, n + if (Fjac(j, j) /= zero) then + sum = zero + do i = j, n + sum = sum + Fjac(i, j)*Qtf(i) + end do + temp = -sum/Fjac(j, j) + do i = j, n + Qtf(i) = Qtf(i) + Fjac(i, j)*temp + end do + end if + end do + + ! copy the triangular factor of the qr factorization into r. + + sing = .false. + do j = 1, n + l = j + jm1 = j - 1 + if (jm1 >= 1) then + do i = 1, jm1 + r(l) = Fjac(i, j) + l = l + n - i + end do + end if + r(l) = Wa1(j) + if (Wa1(j) == zero) sing = .true. + end do + + ! accumulate the orthogonal factor in fjac. + + call qform(n, n, Fjac, Ldfjac, Wa1) + + ! rescale if necessary. + + if (Mode /= 2) then + do j = 1, n + Diag(j) = max(Diag(j), Wa2(j)) + end do + end if + + ! beginning of the inner loop. + inner : do + + ! if requested, call fcn to enable printing of iterates. + + if (Nprint > 0) then + iflag = 0 + if (mod(iter - 1, Nprint) == 0) call fcn(n, x, Fvec, iflag) + if (iflag < 0) exit main + end if + + ! determine the direction p. + + call dogleg(n, r, Lr, Diag, Qtf, delta, Wa1, Wa2, Wa3) + + ! store the direction p and x + p. calculate the norm of p. + + do j = 1, n + Wa1(j) = -Wa1(j) + Wa2(j) = x(j) + Wa1(j) + Wa3(j) = Diag(j)*Wa1(j) + end do + pnorm = enorm(n, Wa3) + + ! on the first iteration, adjust the initial step bound. + + if (iter == 1) delta = min(delta, pnorm) + + ! evaluate the function at x + p and calculate its norm. + + iflag = 1 + call fcn(n, Wa2, Wa4, iflag) + Nfev = Nfev + 1 + if (iflag < 0) exit main + + fnorm1 = enorm(n, Wa4) + + ! compute the scaled actual reduction. + + actred = -one + if (fnorm1 < fnorm) actred = one - (fnorm1/fnorm)**2 + + ! compute the scaled predicted reduction. + + l = 1 + do i = 1, n + sum = zero + do j = i, n + sum = sum + r(l)*Wa1(j) + l = l + 1 + end do + Wa3(i) = Qtf(i) + sum + end do + temp = enorm(n, Wa3) + prered = zero + if (temp < fnorm) prered = one - (temp/fnorm)**2 + + ! compute the ratio of the actual to the predicted + ! reduction. + + ratio = zero + if (prered > zero) ratio = actred/prered + + ! update the step bound. + + if (ratio >= p1) then + ncfail = 0 + ncsuc = ncsuc + 1 + if (ratio >= p5 .or. ncsuc > 1) delta = max(delta, pnorm/p5) + if (abs(ratio - one) <= p1) delta = pnorm/p5 + else + ncsuc = 0 + ncfail = ncfail + 1 + delta = p5*delta + end if + + ! test for successful iteration. + + if (ratio >= p0001) then + ! successful iteration. update x, fvec, and their norms. + do j = 1, n + x(j) = Wa2(j) + Wa2(j) = Diag(j)*x(j) + Fvec(j) = Wa4(j) + end do + xnorm = enorm(n, Wa2) + fnorm = fnorm1 + iter = iter + 1 + end if + + ! determine the progress of the iteration. + + nslow1 = nslow1 + 1 + if (actred >= p001) nslow1 = 0 + if (jeval) nslow2 = nslow2 + 1 + if (actred >= p1) nslow2 = 0 + + ! test for convergence. + + if (delta <= Xtol*xnorm .or. fnorm == zero) Info = 1 + if (Info /= 0) exit main + + ! tests for termination and stringent tolerances. + + if (Nfev >= Maxfev) Info = 2 + if (p1*max(p1*delta, pnorm) <= epsmch*xnorm) Info = 3 + if (nslow2 == 5) Info = 4 + if (nslow1 == 10) Info = 5 + if (Info /= 0) exit main + + ! criterion for recalculating jacobian approximation + ! by forward differences. + + if (ncfail == 2) cycle outer + + ! calculate the rank one modification to the jacobian + ! and update qtf if necessary. + + do j = 1, n + sum = zero + do i = 1, n + sum = sum + Fjac(i, j)*Wa4(i) + end do + Wa2(j) = (sum - Wa3(j))/pnorm + Wa1(j) = Diag(j)*((Diag(j)*Wa1(j))/pnorm) + if (ratio >= p0001) Qtf(j) = sum + end do + + ! compute the qr factorization of the updated jacobian. + + call r1updt(n, n, r, Lr, Wa1, Wa2, Wa3, sing) + call r1mpyq(n, n, Fjac, Ldfjac, Wa2, Wa3) + call r1mpyq(1, n, Qtf, 1, Wa2, Wa3) + + jeval = .false. + + end do inner ! end of the inner loop. + + end do outer ! end of the outer loop. + + end block main + + ! termination, either normal or user imposed. + + if (iflag < 0) Info = iflag + iflag = 0 + if (Nprint > 0) call fcn(n, x, Fvec, iflag) + + end subroutine hybrd +!***************************************************************************************** + +!***************************************************************************************** +!> +! the purpose of hybrd1 is to find a zero of a system of +! n nonlinear functions in n variables by a modification +! of the powell hybrid method. this is done by using the +! more general nonlinear equation solver hybrd. the user +! must provide a subroutine which calculates the functions. +! the jacobian is then calculated by a forward-difference +! approximation. + + subroutine hybrd1(fcn, n, x, Fvec, Tol, Info, Wa, Lwa) + + implicit none + + procedure(func) :: fcn !! user-supplied subroutine which calculates the functions + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of functions and variables. + integer, intent(out) :: info !! an integer output variable. if the user has + !! terminated execution, info is set to the (negative) + !! value of `iflag`. see description of `fcn`. otherwise, + !! `info` is set as follows: + !! + !! * ***info = 0*** improper input parameters. + !! * ***info = 1*** algorithm estimates that the relative error + !! between `x` and the solution is at most `tol`. + !! * ***info = 2*** number of calls to `fcn` has reached or exceeded + !! `200*(n+1)`. + !! * ***info = 3*** `tol` is too small. no further improvement in + !! the approximate solution `x` is possible. + !! * ***info = 4*** iteration is not making good progress. + real(wp), intent(in) :: tol !! a nonnegative input variable. termination occurs + !! when the algorithm estimates that the relative error + !! between `x` and the solution is at most `tol`. + real(wp), dimension(n), intent(inout) :: x !! an array of length `n`. on input `x` must contain + !! an initial estimate of the solution vector. on output `x` + !! contains the final estimate of the solution vector. + real(wp), dimension(n), intent(out) :: fvec !! an output array of length `n` which contains + !! the functions evaluated at the output `x`. + integer, intent(in) :: Lwa !! a positive integer input variable not less than + !! (n*(3*n+13))/2. + real(wp), intent(inout) :: Wa(Lwa) !! a work array of length lwa. + + integer :: index, j, lr, maxfev, ml, mode, mu, nfev, nprint + real(wp) :: epsfcn, xtol + + reaL(wp), parameter :: factor = 100.0_wp + + Info = 0 + + ! check the input parameters for errors. + + if (n > 0 .and. Tol >= zero .and. Lwa >= (n*(3*n + 13))/2) then + ! call hybrd. + maxfev = 200*(n + 1) + xtol = Tol + ml = n - 1 + mu = n - 1 + epsfcn = zero + mode = 2 + do j = 1, n + Wa(j) = one + end do + nprint = 0 + lr = (n*(n + 1))/2 + index = 6*n + lr + call hybrd(fcn, n, x, Fvec, xtol, maxfev, ml, mu, epsfcn, Wa(1), mode, & + factor, nprint, Info, nfev, Wa(index + 1), n, Wa(6*n + 1), lr, & + Wa(n + 1), Wa(2*n + 1), Wa(3*n + 1), Wa(4*n + 1), Wa(5*n + 1)) + if (Info == 5) Info = 4 + end if + + end subroutine hybrd1 +!***************************************************************************************** + +!***************************************************************************************** +!> +! the purpose of hybrj is to find a zero of a system of +! n nonlinear functions in n variables by a modification +! of the powell hybrid method. the user must provide a +! subroutine which calculates the functions and the jacobian. + + subroutine hybrj(fcn, n, x, Fvec, Fjac, Ldfjac, Xtol, Maxfev, Diag, Mode, & + Factor, Nprint, Info, Nfev, Njev, r, Lr, Qtf, Wa1, Wa2, & + Wa3, Wa4) + + implicit none + + procedure(fcn_hybrj) :: fcn !! the user-supplied subroutine which + !! calculates the functions and the jacobian + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of functions and variables. + integer, intent(in) :: Ldfjac !! a positive integer input variable not less than n + !! which specifies the leading dimension of the array fjac. + integer, intent(in) :: Maxfev !! a positive integer input variable. termination + !! occurs when the number of calls to fcn with iflag = 1 + !! has reached maxfev. + integer, intent(in) :: Mode !! an integer input variable. if mode = 1, the + !! variables will be scaled internally. if mode = 2, + !! the scaling is specified by the input diag. other + !! values of mode are equivalent to mode = 1. + integer, intent(in) :: Nprint !! an integer input variable that enables controlled + !! printing of iterates if it is positive. in this case, + !! fcn is called with iflag = 0 at the beginning of the first + !! iteration and every nprint iterations thereafter and + !! immediately prior to return, with x and fvec available + !! for printing. fvec and fjac should not be altered. + !! if nprint is not positive, no special calls of fcn + !! with iflag = 0 are made. + integer, intent(out) :: Info !! an integer output variable. if the user has + !! terminated execution, info is set to the (negative) + !! value of iflag. see description of fcn. otherwise, + !! info is set as follows: + !! + !! * ***info = 0*** improper input parameters. + !! * ***info = 1*** relative error between two consecutive iterates + !! is at most xtol. + !! * ***info = 2*** number of calls to fcn with iflag = 1 has + !! reached maxfev. + !! * ***info = 3*** xtol is too small. no further improvement in + !! the approximate solution x is possible. + !! * ***info = 4*** iteration is not making good progress, as + !! measured by the improvement from the last + !! five jacobian evaluations. + !! * ***info = 5*** iteration is not making good progress, as + !! measured by the improvement from the last + !! ten iterations. + integer, intent(out) :: Nfev !! an integer output variable set to the number of + !! calls to fcn with iflag = 1. + integer, intent(out) :: Njev !! an integer output variable set to the number of + !! calls to fcn with iflag = 2. + integer, intent(in) :: Lr !! a positive integer input variable not less than + !! (n*(n+1))/2. + real(wp), intent(in) :: Xtol !! a nonnegative input variable. termination + !! occurs when the relative error between two consecutive + !! iterates is at most xtol. + real(wp), intent(in) :: Factor !! a positive input variable used in determining the + !! initial step bound. this bound is set to the product of + !! factor and the euclidean norm of diag*x if nonzero, or else + !! to factor itself. in most cases factor should lie in the + !! interval (.1,100.). 100. is a generally recommended value. + real(wp), intent(inout) :: x(n) !! an array of length n. on input x must contain + !! an initial estimate of the solution vector. on output x + !! contains the final estimate of the solution vector. + real(wp), intent(out) :: Fvec(n) !! an output array of length n which contains + !! the functions evaluated at the output x. + real(wp), intent(out) :: Fjac(Ldfjac, n) !! an output n by n array which contains the + !! orthogonal matrix q produced by the qr factorization + !! of the final approximate jacobian. + real(wp), intent(inout) :: Diag(n) !! an array of length n. if mode = 1 (see + !! below), diag is internally set. if mode = 2, diag + !! must contain positive entries that serve as + !! multiplicative scale factors for the variables. + real(wp), intent(out) :: r(Lr) !! an output array of length lr which contains the + !! upper triangular matrix produced by the qr factorization + !! of the final approximate jacobian, stored rowwise. + real(wp), intent(out) :: Qtf(n) !! an output array of length n which contains + !! the vector (q transpose)*fvec. + real(wp), intent(inout) :: Wa1(n) !! work array of length n. + real(wp), intent(inout) :: Wa2(n) !! work array of length n. + real(wp), intent(inout) :: Wa3(n) !! work array of length n. + real(wp), intent(inout) :: Wa4(n) !! work array of length n. + + integer :: i, iflag, iter, j, jm1, l, ncfail, ncsuc, nslow1, nslow2 + integer :: iwa(1) + logical :: jeval, sing + real(wp) :: actred, delta, fnorm, fnorm1, pnorm, prered, ratio, sum, temp, xnorm + + real(wp), parameter :: p1 = 1.0e-1_wp + real(wp), parameter :: p5 = 5.0e-1_wp + real(wp), parameter :: p001 = 1.0e-3_wp + real(wp), parameter :: p0001 = 1.0e-4_wp + + Info = 0 + iflag = 0 + Nfev = 0 + Njev = 0 + + main : block + + ! check the input parameters for errors. + + if (n <= 0 .or. Ldfjac < n .or. Xtol < zero .or. Maxfev <= 0 .or. & + Factor <= zero .or. Lr < (n*(n + 1))/2) exit main + if (Mode == 2) then + do j = 1, n + if (Diag(j) <= zero) exit main + end do + end if + + ! evaluate the function at the starting point + ! and calculate its norm. + + iflag = 1 + call fcn(n, x, Fvec, Fjac, Ldfjac, iflag) + Nfev = 1 + if (iflag < 0) exit main + fnorm = enorm(n, Fvec) + + ! initialize iteration counter and monitors. + + iter = 1 + ncsuc = 0 + ncfail = 0 + nslow1 = 0 + nslow2 = 0 + + ! beginning of the outer loop. + outer : do + + jeval = .true. + + ! calculate the jacobian matrix. + + iflag = 2 + call fcn(n, x, Fvec, Fjac, Ldfjac, iflag) + Njev = Njev + 1 + if (iflag < 0) exit main + + ! compute the qr factorization of the jacobian. + + call qrfac(n, n, Fjac, Ldfjac, .false., iwa, 1, Wa1, Wa2, Wa3) + + ! on the first iteration and if mode is 1, scale according + ! to the norms of the columns of the initial jacobian. + + if (iter == 1) then + if (Mode /= 2) then + do j = 1, n + Diag(j) = Wa2(j) + if (Wa2(j) == zero) Diag(j) = one + end do + end if + + ! on the first iteration, calculate the norm of the scaled x + ! and initialize the step bound delta. + + do j = 1, n + Wa3(j) = Diag(j)*x(j) + end do + xnorm = enorm(n, Wa3) + delta = Factor*xnorm + if (delta == zero) delta = Factor + end if + + ! form (q transpose)*fvec and store in qtf. + + do i = 1, n + Qtf(i) = Fvec(i) + end do + do j = 1, n + if (Fjac(j, j) /= zero) then + sum = zero + do i = j, n + sum = sum + Fjac(i, j)*Qtf(i) + end do + temp = -sum/Fjac(j, j) + do i = j, n + Qtf(i) = Qtf(i) + Fjac(i, j)*temp + end do + end if + end do + + ! copy the triangular factor of the qr factorization into r. + + sing = .false. + do j = 1, n + l = j + jm1 = j - 1 + if (jm1 >= 1) then + do i = 1, jm1 + r(l) = Fjac(i, j) + l = l + n - i + end do + end if + r(l) = Wa1(j) + if (Wa1(j) == zero) sing = .true. + end do + + ! accumulate the orthogonal factor in fjac. + + call qform(n, n, Fjac, Ldfjac, Wa1) + + ! rescale if necessary. + + if (Mode /= 2) then + do j = 1, n + Diag(j) = max(Diag(j), Wa2(j)) + end do + end if + + ! beginning of the inner loop. + inner : do + + ! if requested, call fcn to enable printing of iterates. + + if (Nprint > 0) then + iflag = 0 + if (mod(iter - 1, Nprint) == 0) & + call fcn(n, x, Fvec, Fjac, Ldfjac, iflag) + if (iflag < 0) exit main + end if + + ! determine the direction p. + + call dogleg(n, r, Lr, Diag, Qtf, delta, Wa1, Wa2, Wa3) + + ! store the direction p and x + p. calculate the norm of p. + + do j = 1, n + Wa1(j) = -Wa1(j) + Wa2(j) = x(j) + Wa1(j) + Wa3(j) = Diag(j)*Wa1(j) + end do + pnorm = enorm(n, Wa3) + + ! on the first iteration, adjust the initial step bound. + + if (iter == 1) delta = min(delta, pnorm) + + ! evaluate the function at x + p and calculate its norm. + + iflag = 1 + call fcn(n, Wa2, Wa4, Fjac, Ldfjac, iflag) + Nfev = Nfev + 1 + if (iflag < 0) exit main + + fnorm1 = enorm(n, Wa4) + + ! compute the scaled actual reduction. + + actred = -one + if (fnorm1 < fnorm) actred = one - (fnorm1/fnorm)**2 + + ! compute the scaled predicted reduction. + + l = 1 + do i = 1, n + sum = zero + do j = i, n + sum = sum + r(l)*Wa1(j) + l = l + 1 + end do + Wa3(i) = Qtf(i) + sum + end do + temp = enorm(n, Wa3) + prered = zero + if (temp < fnorm) prered = one - (temp/fnorm)**2 + + ! compute the ratio of the actual to the predicted + ! reduction. + + ratio = zero + if (prered > zero) ratio = actred/prered + + ! update the step bound. + + if (ratio >= p1) then + ncfail = 0 + ncsuc = ncsuc + 1 + if (ratio >= p5 .or. ncsuc > 1) delta = max(delta, pnorm/p5) + if (abs(ratio - one) <= p1) delta = pnorm/p5 + else + ncsuc = 0 + ncfail = ncfail + 1 + delta = p5*delta + end if + + ! test for successful iteration. + + if (ratio >= p0001) then + + ! successful iteration. update x, fvec, and their norms. + + do j = 1, n + x(j) = Wa2(j) + Wa2(j) = Diag(j)*x(j) + Fvec(j) = Wa4(j) + end do + xnorm = enorm(n, Wa2) + fnorm = fnorm1 + iter = iter + 1 + end if + + ! determine the progress of the iteration. + + nslow1 = nslow1 + 1 + if (actred >= p001) nslow1 = 0 + if (jeval) nslow2 = nslow2 + 1 + if (actred >= p1) nslow2 = 0 + + ! test for convergence. + + if (delta <= Xtol*xnorm .or. fnorm == zero) Info = 1 + if (Info /= 0) exit main + + ! tests for termination and stringent tolerances. + + if (Nfev >= Maxfev) Info = 2 + if (p1*max(p1*delta, pnorm) <= epsmch*xnorm) Info = 3 + if (nslow2 == 5) Info = 4 + if (nslow1 == 10) Info = 5 + if (Info /= 0) exit main + + ! criterion for recalculating jacobian. + + if (ncfail == 2) cycle outer + + ! calculate the rank one modification to the jacobian + ! and update qtf if necessary. + + do j = 1, n + sum = zero + do i = 1, n + sum = sum + Fjac(i, j)*Wa4(i) + end do + Wa2(j) = (sum - Wa3(j))/pnorm + Wa1(j) = Diag(j)*((Diag(j)*Wa1(j))/pnorm) + if (ratio >= p0001) Qtf(j) = sum + end do + + ! compute the qr factorization of the updated jacobian. + + call r1updt(n, n, r, Lr, Wa1, Wa2, Wa3, sing) + call r1mpyq(n, n, Fjac, Ldfjac, Wa2, Wa3) + call r1mpyq(1, n, Qtf, 1, Wa2, Wa3) + + jeval = .false. + + end do inner ! end of the inner loop. + + end do outer ! end of the outer loop. + + end block main + + ! termination, either normal or user imposed. + + if (iflag < 0) Info = iflag + iflag = 0 + if (Nprint > 0) call fcn(n, x, Fvec, Fjac, Ldfjac, iflag) + + end subroutine hybrj +!***************************************************************************************** + +!***************************************************************************************** +!> +! the purpose of hybrj1 is to find a zero of a system of +! n nonlinear functions in n variables by a modification +! of the powell hybrid method. this is done by using the +! more general nonlinear equation solver hybrj. the user +! must provide a subroutine which calculates the functions +! and the jacobian. + + subroutine hybrj1(fcn, n, x, Fvec, Fjac, Ldfjac, Tol, Info, Wa, Lwa) + + implicit none + + procedure(fcn_hybrj) :: fcn !! the user-supplied subroutine which + !! calculates the functions and the jacobian + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of functions and variables. + integer, intent(in) :: Ldfjac !! a positive integer input variable not less than n + !! which specifies the leading dimension of the array fjac. + integer, intent(out) :: Info !! an integer output variable. if the user has + !! terminated execution, info is set to the (negative) + !! value of iflag. see description of fcn. otherwise, + !! info is set as follows: + !! + !! * ***info = 0*** improper input parameters. + !! * ***info = 1*** algorithm estimates that the relative error + !! between x and the solution is at most tol. + !! * ***info = 2*** number of calls to fcn with iflag = 1 has + !! reached 100*(n+1). + !! * ***info = 3*** tol is too small. no further improvement in + !! the approximate solution x is possible. + !! * ***info = 4*** iteration is not making good progress. + integer, intent(in) :: Lwa !! a positive integer input variable not less than + !! (n*(n+13))/2. + real(wp), intent(in) :: Tol !! a nonnegative input variable. termination occurs + !! when the algorithm estimates that the relative error + !! between x and the solution is at most tol. + real(wp), intent(inout) :: x(n) !! an array of length n. on input x must contain + !! an initial estimate of the solution vector. on output x + !! contains the final estimate of the solution vector. + real(wp), intent(out) :: Fvec(n) !! an output array of length n which contains + !! the functions evaluated at the output x. + real(wp), intent(out) :: Fjac(Ldfjac, n) !! an output n by n array which contains the + !! orthogonal matrix q produced by the qr factorization + !! of the final approximate jacobian. + real(wp), intent(inout) :: Wa(Lwa) !! a work array of length lwa. + + integer :: j, lr, maxfev, mode, nfev, njev, nprint + real(wp) :: xtol + + real(wp), parameter :: factor = 100.0_wp + + Info = 0 + + ! check the input parameters for errors. + + if (n > 0 .and. Ldfjac >= n .and. Tol >= zero .and. Lwa >= (n*(n + 13))/2) then + ! call hybrj. + maxfev = 100*(n + 1) + xtol = Tol + mode = 2 + do j = 1, n + Wa(j) = one + end do + nprint = 0 + lr = (n*(n + 1))/2 + call hybrj(fcn, n, x, Fvec, Fjac, Ldfjac, xtol, maxfev, Wa(1), mode, & + factor, nprint, Info, nfev, njev, Wa(6*n + 1), lr, Wa(n + 1), & + Wa(2*n + 1), Wa(3*n + 1), Wa(4*n + 1), Wa(5*n + 1)) + if (Info == 5) Info = 4 + end if + + end subroutine hybrj1 +!***************************************************************************************** + +!***************************************************************************************** +!> +! the purpose of lmder is to minimize the sum of the squares of +! m nonlinear functions in n variables by a modification of +! the levenberg-marquardt algorithm. the user must provide a +! subroutine which calculates the functions and the jacobian. + + subroutine lmder(fcn, m, n, x, Fvec, Fjac, Ldfjac, Ftol, Xtol, Gtol, Maxfev, & + Diag, Mode, Factor, Nprint, Info, Nfev, Njev, Ipvt, Qtf, & + Wa1, Wa2, Wa3, Wa4) + + implicit none + + procedure(fcn_lmder) :: fcn !! the user-supplied subroutine which + !! calculates the functions and the jacobian + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of functions. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of variables. n must not exceed m. + integer, intent(in) :: Ldfjac !! a positive integer input variable not less than m + !! which specifies the leading dimension of the array fjac. + integer, intent(in) :: Maxfev !! a positive integer input variable. termination + !! occurs when the number of calls to fcn with iflag = 1 + !! has reached maxfev. + integer, intent(in) :: Mode !! an integer input variable. if mode = 1, the + !! variables will be scaled internally. if mode = 2, + !! the scaling is specified by the input diag. other + !! values of mode are equivalent to mode = 1. + integer, intent(in) :: Nprint !! an integer input variable that enables controlled + !! printing of iterates if it is positive. in this case, + !! fcn is called with iflag = 0 at the beginning of the first + !! iteration and every nprint iterations thereafter and + !! immediately prior to return, with x, fvec, and fjac + !! available for printing. fvec and fjac should not be + !! altered. if nprint is not positive, no special calls + !! of fcn with iflag = 0 are made. + integer, intent(out) :: Info !! an integer output variable. if the user has + !! terminated execution, info is set to the (negative) + !! value of iflag. see description of fcn. otherwise, + !! info is set as follows: + !! + !! * ***info = 0*** improper input parameters. + !! * ***info = 1*** both actual and predicted relative reductions + !! in the sum of squares are at most ftol. + !! * ***info = 2*** relative error between two consecutive iterates + !! is at most xtol. + !! * ***info = 3*** conditions for info = 1 and info = 2 both hold. + !! * ***info = 4*** the cosine of the angle between fvec and any + !! column of the jacobian is at most gtol in + !! absolute value. + !! * ***info = 5*** number of calls to fcn with iflag = 1 has + !! reached maxfev. + !! * ***info = 6*** ftol is too small. no further reduction in + !! the sum of squares is possible. + !! * ***info = 7*** xtol is too small. no further improvement in + !! the approximate solution x is possible. + !! * ***info = 8*** gtol is too small. fvec is orthogonal to the + !! columns of the jacobian to machine precision. + integer, intent(out) :: Nfev !! an integer output variable set to the number of + !! calls to fcn with iflag = 1. + integer, intent(out) :: Njev !! an integer output variable set to the number of + !! calls to fcn with iflag = 2. + integer, intent(out) :: Ipvt(n) !! an integer output array of length n. ipvt + !! defines a permutation matrix p such that jac*p = q*r, + !! where jac is the final calculated jacobian, q is + !! orthogonal (not stored), and r is upper triangular + !! with diagonal elements of nonincreasing magnitude. + !! column j of p is column ipvt(j) of the identity matrix. + real(wp), intent(in) :: Ftol !! a nonnegative input variable. termination + !! occurs when both the actual and predicted relative + !! reductions in the sum of squares are at most ftol. + !! therefore, ftol measures the relative error desired + !! in the sum of squares. + real(wp), intent(in) :: Xtol !! a nonnegative input variable. termination + !! occurs when the relative error between two consecutive + !! iterates is at most xtol. therefore, xtol measures the + !! relative error desired in the approximate solution. + real(wp), intent(in) :: Gtol !! a nonnegative input variable. termination + !! occurs when the cosine of the angle between fvec and + !! any column of the jacobian is at most gtol in absolute + !! value. therefore, gtol measures the orthogonality + !! desired between the function vector and the columns + !! of the jacobian. + real(wp), intent(in) :: Factor !! a positive input variable used in determining the + !! initial step bound. this bound is set to the product of + !! factor and the euclidean norm of diag*x if nonzero, or else + !! to factor itself. in most cases factor should lie in the + !! interval (.1,100.).100. is a generally recommended value. + real(wp), intent(inout) :: x(n) !! an array of length n. on input x must contain + !! an initial estimate of the solution vector. on output x + !! contains the final estimate of the solution vector. + real(wp), intent(out) :: Fvec(m) !! an output array of length m which contains + !! the functions evaluated at the output x. + real(wp), intent(out) :: Fjac(Ldfjac, n) !! an output m by n array. the upper n by n submatrix + !! of fjac contains an upper triangular matrix r with + !! diagonal elements of nonincreasing magnitude such that + !!``` + !! t t t + !! p *(jac *jac)*p = r *r, + !!``` + !! where p is a permutation matrix and jac is the final + !! calculated jacobian. column j of p is column ipvt(j) + !! (see below) of the identity matrix. the lower trapezoidal + !! part of fjac contains information generated during + !! the computation of r. + real(wp), intent(inout) :: Diag(n) !! an array of length n. if mode = 1 (see + !! below), diag is internally set. if mode = 2, diag + !! must contain positive entries that serve as + !! multiplicative scale factors for the variables. + real(wp), intent(out) :: Qtf(n) !! an output array of length n which contains + !! the first n elements of the vector (q transpose)*fvec. + real(wp), intent(inout) :: Wa1(n) !! work array of length n. + real(wp), intent(inout) :: Wa2(n) !! work array of length n. + real(wp), intent(inout) :: Wa3(n) !! work array of length n. + real(wp), intent(inout) :: Wa4(m) !! work array of length m. + + integer :: i, iflag, iter, j, l + real(wp) :: actred, delta, dirder, fnorm, fnorm1, gnorm, par, & + pnorm, prered, ratio, sum, temp, temp1, temp2, xnorm + + real(wp), parameter :: p1 = 1.0e-1_wp + real(wp), parameter :: p5 = 5.0e-1_wp + real(wp), parameter :: p25 = 2.5e-1_wp + real(wp), parameter :: p75 = 7.5e-1_wp + real(wp), parameter :: p0001 = 1.0e-4_wp + + Info = 0 + iflag = 0 + Nfev = 0 + Njev = 0 + + main : block + + ! check the input parameters for errors. + + if (n > 0 .and. m >= n .and. Ldfjac >= m .and. Ftol >= zero .and. & + Xtol >= zero .and. Gtol >= zero .and. Maxfev > 0 .and. & + Factor > zero) then + if (Mode == 2) then + do j = 1, n + if (Diag(j) <= zero) exit main + end do + end if + else + exit main + end if + + ! evaluate the function at the starting point + ! and calculate its norm. + + iflag = 1 + call fcn(m, n, x, Fvec, Fjac, Ldfjac, iflag) + Nfev = 1 + if (iflag < 0) exit main + fnorm = enorm(m, Fvec) + + ! initialize levenberg-marquardt parameter and iteration counter. + + par = zero + iter = 1 + + ! beginning of the outer loop. + + outer : do + + ! calculate the jacobian matrix. + + iflag = 2 + call fcn(m, n, x, Fvec, Fjac, Ldfjac, iflag) + Njev = Njev + 1 + if (iflag < 0) exit main + + ! if requested, call fcn to enable printing of iterates. + + if (Nprint > 0) then + iflag = 0 + if (mod(iter - 1, Nprint) == 0) & + call fcn(m, n, x, Fvec, Fjac, Ldfjac, iflag) + if (iflag < 0) exit main + end if + + ! compute the qr factorization of the jacobian. + + call qrfac(m, n, Fjac, Ldfjac, .true., Ipvt, n, Wa1, Wa2, Wa3) + + ! on the first iteration and if mode is 1, scale according + ! to the norms of the columns of the initial jacobian. + + if (iter == 1) then + if (Mode /= 2) then + do j = 1, n + Diag(j) = Wa2(j) + if (Wa2(j) == zero) Diag(j) = one + end do + end if + + ! on the first iteration, calculate the norm of the scaled x + ! and initialize the step bound delta. + + do j = 1, n + Wa3(j) = Diag(j)*x(j) + end do + xnorm = enorm(n, Wa3) + delta = Factor*xnorm + if (delta == zero) delta = Factor + end if + + ! form (q transpose)*fvec and store the first n components in + ! qtf. + + do i = 1, m + Wa4(i) = Fvec(i) + end do + do j = 1, n + if (Fjac(j, j) /= zero) then + sum = zero + do i = j, m + sum = sum + Fjac(i, j)*Wa4(i) + end do + temp = -sum/Fjac(j, j) + do i = j, m + Wa4(i) = Wa4(i) + Fjac(i, j)*temp + end do + end if + Fjac(j, j) = Wa1(j) + Qtf(j) = Wa4(j) + end do + + ! compute the norm of the scaled gradient. + + gnorm = zero + if (fnorm /= zero) then + do j = 1, n + l = Ipvt(j) + if (Wa2(l) /= zero) then + sum = zero + do i = 1, j + sum = sum + Fjac(i, j)*(Qtf(i)/fnorm) + end do + gnorm = max(gnorm, abs(sum/Wa2(l))) + end if + end do + end if + + ! test for convergence of the gradient norm. + + if (gnorm <= Gtol) Info = 4 + if (Info /= 0) exit main + + ! rescale if necessary. + + if (Mode /= 2) then + do j = 1, n + Diag(j) = max(Diag(j), Wa2(j)) + end do + end if + + ! beginning of the inner loop. + inner : do + + ! determine the levenberg-marquardt parameter. + + call lmpar(n, Fjac, Ldfjac, Ipvt, Diag, Qtf, delta, par, Wa1, Wa2, Wa3, Wa4) + + ! store the direction p and x + p. calculate the norm of p. + + do j = 1, n + Wa1(j) = -Wa1(j) + Wa2(j) = x(j) + Wa1(j) + Wa3(j) = Diag(j)*Wa1(j) + end do + pnorm = enorm(n, Wa3) + + ! on the first iteration, adjust the initial step bound. + + if (iter == 1) delta = min(delta, pnorm) + + ! evaluate the function at x + p and calculate its norm. + + iflag = 1 + call fcn(m, n, Wa2, Wa4, Fjac, Ldfjac, iflag) + Nfev = Nfev + 1 + if (iflag < 0) exit main + fnorm1 = enorm(m, Wa4) + + ! compute the scaled actual reduction. + + actred = -one + if (p1*fnorm1 < fnorm) actred = one - (fnorm1/fnorm)**2 + + ! compute the scaled predicted reduction and + ! the scaled directional derivative. + + do j = 1, n + Wa3(j) = zero + l = Ipvt(j) + temp = Wa1(l) + do i = 1, j + Wa3(i) = Wa3(i) + Fjac(i, j)*temp + end do + end do + temp1 = enorm(n, Wa3)/fnorm + temp2 = (sqrt(par)*pnorm)/fnorm + prered = temp1**2 + temp2**2/p5 + dirder = -(temp1**2 + temp2**2) + + ! compute the ratio of the actual to the predicted + ! reduction. + + ratio = zero + if (prered /= zero) ratio = actred/prered + + ! update the step bound. + + if (ratio <= p25) then + if (actred >= zero) temp = p5 + if (actred < zero) temp = p5*dirder/(dirder + p5*actred) + if (p1*fnorm1 >= fnorm .or. temp < p1) temp = p1 + delta = temp*min(delta, pnorm/p1) + par = par/temp + elseif (par == zero .or. ratio >= p75) then + delta = pnorm/p5 + par = p5*par + end if + + ! test for successful iteration. + + if (ratio >= p0001) then + ! successful iteration. update x, fvec, and their norms. + do j = 1, n + x(j) = Wa2(j) + Wa2(j) = Diag(j)*x(j) + end do + do i = 1, m + Fvec(i) = Wa4(i) + end do + xnorm = enorm(n, Wa2) + fnorm = fnorm1 + iter = iter + 1 + end if + + ! tests for convergence. + if (abs(actred) <= Ftol .and. prered <= Ftol .and. p5*ratio <= one) Info = 1 + if (delta <= Xtol*xnorm) Info = 2 + if (abs(actred) <= Ftol .and. prered <= Ftol .and. p5*ratio <= one .and. Info == 2) Info = 3 + if (Info /= 0) exit main + + ! tests for termination and stringent tolerances. + if (Nfev >= Maxfev) Info = 5 + if (abs(actred) <= epsmch .and. prered <= epsmch .and. p5*ratio <= one) Info = 6 + if (delta <= epsmch*xnorm) Info = 7 + if (gnorm <= epsmch) Info = 8 + if (Info /= 0) exit main + + if (ratio >= p0001) exit inner + + end do inner ! end of the inner loop. repeat if iteration unsuccessful. + + end do outer ! end of the outer loop + + end block main + + ! termination, either normal or user imposed. + + if (iflag < 0) Info = iflag + iflag = 0 + if (Nprint > 0) call fcn(m, n, x, Fvec, Fjac, Ldfjac, iflag) + + end subroutine lmder +!***************************************************************************************** + +!***************************************************************************************** +!> +! the purpose of lmder1 is to minimize the sum of the squares of +! m nonlinear functions in n variables by a modification of the +! levenberg-marquardt algorithm. this is done by using the more +! general least-squares solver lmder. the user must provide a +! subroutine which calculates the functions and the jacobian. + + subroutine lmder1(fcn, m, n, x, Fvec, Fjac, Ldfjac, Tol, Info, Ipvt, Wa, Lwa) + implicit none + + procedure(fcn_lmder) :: fcn !! user-supplied subroutine which + !! calculates the functions and the jacobian. + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of functions. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of variables. n must not exceed m. + integer, intent(in) :: Ldfjac !! a positive integer input variable not less than m + !! which specifies the leading dimension of the array fjac. + integer, intent(out) :: Info !! an integer output variable. if the user has + !! terminated execution, info is set to the (negative) + !! value of iflag. see description of fcn. otherwise, + !! info is set as follows. + !! + !! * ***info = 0*** improper input parameters. + !! * ***info = 1*** algorithm estimates that the relative error + !! in the sum of squares is at most tol. + !! * ***info = 2*** algorithm estimates that the relative error + !! between x and the solution is at most tol. + !! * ***info = 3*** conditions for info = 1 and info = 2 both hold. + !! * ***info = 4*** fvec is orthogonal to the columns of the + !! jacobian to machine precision. + !! * ***info = 5*** number of calls to fcn with iflag = 1 has + !! reached 100*(n+1). + !! * ***info = 6*** tol is too small. no further reduction in + !! the sum of squares is possible. + !! * ***info = 7*** tol is too small. no further improvement in + !! the approximate solution x is possible. + integer, intent(in) :: Lwa !! a positive integer input variable not less than 5*n+m. + integer, intent(out) :: Ipvt(n) !! an integer output array of length n. ipvt + !! defines a permutation matrix p such that jac*p = q*r, + !! where jac is the final calculated jacobian, q is + !! orthogonal (not stored), and r is upper triangular + !! with diagonal elements of nonincreasing magnitude. + !! column j of p is column ipvt(j) of the identity matrix. + real(wp), intent(in) :: Tol !! a nonnegative input variable. termination occurs + !! when the algorithm estimates either that the relative + !! error in the sum of squares is at most tol or that + !! the relative error between x and the solution is at + !! most tol. + real(wp), intent(inout) :: x(n) !! an array of length n. on input x must contain + !! an initial estimate of the solution vector. on output x + !! contains the final estimate of the solution vector. + real(wp), intent(out) :: Fvec(m) !! an output array of length m which contains + !! the functions evaluated at the output x. + real(wp), intent(out) :: Fjac(Ldfjac, n) !! an output m by n array. the upper n by n submatrix + !! of fjac contains an upper triangular matrix r with + !! diagonal elements of nonincreasing magnitude such that + !!``` + !! t t t + !! p *(jac *jac)*p = r *r, + !!``` + !! where p is a permutation matrix and jac is the final + !! calculated jacobian. column j of p is column ipvt(j) + !! (see below) of the identity matrix. the lower trapezoidal + !! part of fjac contains information generated during + !! the computation of r. + real(wp), intent(inout) :: Wa(Lwa) !! a work array of length lwa. + + integer :: maxfev, mode, nfev, njev, nprint + real(wp) :: ftol, gtol, xtol + + real(wp), parameter :: factor = 100.0_wp + + Info = 0 + + ! check the input parameters for errors. + + if (n > 0 .and. m >= n .and. Ldfjac >= m .and. Tol >= zero .and. & + Lwa >= 5*n + m) then + ! call lmder. + maxfev = 100*(n + 1) + ftol = Tol + xtol = Tol + gtol = zero + mode = 1 + nprint = 0 + call lmder(fcn, m, n, x, Fvec, Fjac, Ldfjac, ftol, xtol, gtol, maxfev, & + & Wa(1), mode, factor, nprint, Info, nfev, njev, Ipvt, Wa(n + 1)& + & , Wa(2*n + 1), Wa(3*n + 1), Wa(4*n + 1), Wa(5*n + 1)) + if (Info == 8) Info = 4 + end if + + end subroutine lmder1 +!***************************************************************************************** + +!***************************************************************************************** +!> +! the purpose of lmdif is to minimize the sum of the squares of +! m nonlinear functions in n variables by a modification of +! the levenberg-marquardt algorithm. the user must provide a +! subroutine which calculates the functions. the jacobian is +! then calculated by a forward-difference approximation. + + subroutine lmdif(fcn, m, n, x, Fvec, Ftol, Xtol, Gtol, Maxfev, Epsfcn, Diag, & + Mode, Factor, Nprint, Info, Nfev, Fjac, Ldfjac, Ipvt, & + Qtf, Wa1, Wa2, Wa3, Wa4) + implicit none + + procedure(func2) :: fcn !! the user-supplied subroutine which + !! calculates the functions. + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of functions. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of variables. n must not exceed m. + integer, intent(in) :: Maxfev !! a positive integer input variable. termination + !! occurs when the number of calls to fcn is at least + !! maxfev by the end of an iteration. + integer, intent(in) :: Mode !! an integer input variable. if mode = 1, the + !! variables will be scaled internally. if mode = 2, + !! the scaling is specified by the input diag. other + !! values of mode are equivalent to mode = 1. + integer, intent(in) :: Nprint !! an integer input variable that enables controlled + !! printing of iterates if it is positive. in this case, + !! fcn is called with iflag = 0 at the beginning of the first + !! iteration and every nprint iterations thereafter and + !! immediately prior to return, with x and fvec available + !! for printing. if nprint is not positive, no special calls + !! of fcn with iflag = 0 are made. + integer, intent(out) :: Info !! an integer output variable. if the user has + !! terminated execution, info is set to the (negative) + !! value of iflag. see description of fcn. otherwise, + !! info is set as follows: + !! + !! * ***info = 0*** improper input parameters. + !! * ***info = 1*** both actual and predicted relative reductions + !! in the sum of squares are at most ftol. + !! * ***info = 2*** relative error between two consecutive iterates + !! is at most xtol. + !! * ***info = 3*** conditions for info = 1 and info = 2 both hold. + !! * ***info = 4*** the cosine of the angle between fvec and any + !! column of the jacobian is at most gtol in + !! absolute value. + !! * ***info = 5*** number of calls to fcn has reached or + !! exceeded maxfev. + !! * ***info = 6*** ftol is too small. no further reduction in + !! the sum of squares is possible. + !! * ***info = 7*** xtol is too small. no further improvement in + !! the approximate solution x is possible. + !! * ***info = 8*** gtol is too small. fvec is orthogonal to the + !! columns of the jacobian to machine precision. + integer, intent(out) :: Nfev !! an integer output variable set to the number of + !! calls to fcn. + integer, intent(in) :: Ldfjac !! a positive integer input variable not less than m + !! which specifies the leading dimension of the array fjac. + integer, intent(out) :: Ipvt(n) !! an integer output array of length n. ipvt + !! defines a permutation matrix p such that jac*p = q*r, + !! where jac is the final calculated jacobian, q is + !! orthogonal (not stored), and r is upper triangular + !! with diagonal elements of nonincreasing magnitude. + !! column j of p is column ipvt(j) of the identity matrix. + real(wp), intent(in) :: Ftol !! a nonnegative input variable. termination + !! occurs when both the actual and predicted relative + !! reductions in the sum of squares are at most ftol. + !! therefore, ftol measures the relative error desired + !! in the sum of squares. + real(wp), intent(in) :: Xtol !! a nonnegative input variable. termination + !! occurs when the relative error between two consecutive + !! iterates is at most xtol. therefore, xtol measures the + !! relative error desired in the approximate solution. + real(wp), intent(in) :: Gtol !! a nonnegative input variable. termination + !! occurs when the cosine of the angle between fvec and + !! any column of the jacobian is at most gtol in absolute + !! value. therefore, gtol measures the orthogonality + !! desired between the function vector and the columns + !! of the jacobian. + real(wp), intent(in) :: Epsfcn !! an input variable used in determining a suitable + !! step length for the forward-difference approximation. this + !! approximation assumes that the relative errors in the + !! functions are of the order of epsfcn. if epsfcn is less + !! than the machine precision, it is assumed that the relative + !! errors in the functions are of the order of the machine + !! precision. + real(wp), intent(in) :: Factor !! a positive input variable used in determining the + !! initial step bound. this bound is set to the product of + !! factor and the euclidean norm of diag*x if nonzero, or else + !! to factor itself. in most cases factor should lie in the + !! interval (.1,100.). 100. is a generally recommended value. + real(wp), intent(inout) :: x(n) !! an array of length n. on input x must contain + !! an initial estimate of the solution vector. on output x + !! contains the final estimate of the solution vector. + real(wp), intent(out) :: Fvec(m) !! an output array of length m which contains + !! the functions evaluated at the output x. + real(wp), intent(inout) :: Diag(n) !! an array of length n. if mode = 1 (see + !! below), diag is internally set. if mode = 2, diag + !! must contain positive entries that serve as + !! multiplicative scale factors for the variables. + real(wp), intent(out) :: Fjac(Ldfjac, n) !! an output m by n array. the upper n by n submatrix + !! of fjac contains an upper triangular matrix r with + !! diagonal elements of nonincreasing magnitude such that + !!``` + !! t t t + !! p *(jac *jac)*p = r *r, + !!``` + !! where p is a permutation matrix and jac is the final + !! calculated jacobian. column j of p is column ipvt(j) + !! (see below) of the identity matrix. the lower trapezoidal + !! part of fjac contains information generated during + !! the computation of r. + real(wp), intent(out) :: Qtf(n) !! an output array of length n which contains + !! the first n elements of the vector (q transpose)*fvec. + real(wp), intent(inout) :: Wa1(n) !! work array of length n. + real(wp), intent(inout) :: Wa2(n) !! work array of length n. + real(wp), intent(inout) :: Wa3(n) !! work array of length n. + real(wp), intent(inout) :: Wa4(m) !! work array of length n. + + integer :: i, iflag, iter, j, l + real(wp) :: actred, delta, dirder, fnorm, & + fnorm1, gnorm, par, pnorm, prered, & + ratio, sum, temp, temp1, temp2, xnorm + + real(wp), parameter :: p1 = 1.0e-1_wp + real(wp), parameter :: p5 = 5.0e-1_wp + real(wp), parameter :: p25 = 2.5e-1_wp + real(wp), parameter :: p75 = 7.5e-1_wp + real(wp), parameter :: p0001 = 1.0e-4_wp + + Info = 0 + iflag = 0 + Nfev = 0 + + main : block + + ! check the input parameters for errors. + + if (n > 0 .and. m >= n .and. Ldfjac >= m .and. Ftol >= zero .and. & + Xtol >= zero .and. Gtol >= zero .and. Maxfev > 0 .and. & + Factor > zero) then + if (Mode == 2) then + do j = 1, n + if (Diag(j) <= zero) exit main + end do + end if + else + exit main + end if + + ! evaluate the function at the starting point + ! and calculate its norm. + + iflag = 1 + call fcn(m, n, x, Fvec, iflag) + Nfev = 1 + if (iflag < 0) exit main + + fnorm = enorm(m, Fvec) + + ! initialize levenberg-marquardt parameter and iteration counter. + + par = zero + iter = 1 + + ! beginning of the outer loop. + + outer : do + + ! calculate the jacobian matrix. + + iflag = 2 + call fdjac2(fcn, m, n, x, Fvec, Fjac, Ldfjac, iflag, Epsfcn, Wa4) + Nfev = Nfev + n + if (iflag < 0) exit main + + ! if requested, call fcn to enable printing of iterates. + + if (Nprint > 0) then + iflag = 0 + if (mod(iter - 1, Nprint) == 0) & + call fcn(m, n, x, Fvec, iflag) + if (iflag < 0) exit main + end if + + ! compute the qr factorization of the jacobian. + + call qrfac(m, n, Fjac, Ldfjac, .true., Ipvt, n, Wa1, Wa2, Wa3) + + ! on the first iteration and if mode is 1, scale according + ! to the norms of the columns of the initial jacobian. + + if (iter == 1) then + if (Mode /= 2) then + do j = 1, n + Diag(j) = Wa2(j) + if (Wa2(j) == zero) Diag(j) = one + end do + end if + + ! on the first iteration, calculate the norm of the scaled x + ! and initialize the step bound delta. + + do j = 1, n + Wa3(j) = Diag(j)*x(j) + end do + xnorm = enorm(n, Wa3) + delta = Factor*xnorm + if (delta == zero) delta = Factor + end if + + ! form (q transpose)*fvec and store the first n components in + ! qtf. + + do i = 1, m + Wa4(i) = Fvec(i) + end do + do j = 1, n + if (Fjac(j, j) /= zero) then + sum = zero + do i = j, m + sum = sum + Fjac(i, j)*Wa4(i) + end do + temp = -sum/Fjac(j, j) + do i = j, m + Wa4(i) = Wa4(i) + Fjac(i, j)*temp + end do + end if + Fjac(j, j) = Wa1(j) + Qtf(j) = Wa4(j) + end do + + ! compute the norm of the scaled gradient. + + gnorm = zero + if (fnorm /= zero) then + do j = 1, n + l = Ipvt(j) + if (Wa2(l) /= zero) then + sum = zero + do i = 1, j + sum = sum + Fjac(i, j)*(Qtf(i)/fnorm) + end do + gnorm = max(gnorm, abs(sum/Wa2(l))) + end if + end do + end if + + ! test for convergence of the gradient norm. + + if (gnorm <= Gtol) Info = 4 + if (Info /= 0) exit main + + ! rescale if necessary. + + if (Mode /= 2) then + do j = 1, n + Diag(j) = max(Diag(j), Wa2(j)) + end do + end if + + ! beginning of the inner loop. + + inner : do + + ! determine the levenberg-marquardt parameter. + + call lmpar(n, Fjac, Ldfjac, Ipvt, Diag, Qtf, delta, par, Wa1, & + Wa2, Wa3, Wa4) + + ! store the direction p and x + p. calculate the norm of p. + + do j = 1, n + Wa1(j) = -Wa1(j) + Wa2(j) = x(j) + Wa1(j) + Wa3(j) = Diag(j)*Wa1(j) + end do + pnorm = enorm(n, Wa3) + + ! on the first iteration, adjust the initial step bound. + + if (iter == 1) delta = min(delta, pnorm) + + ! evaluate the function at x + p and calculate its norm. + + iflag = 1 + call fcn(m, n, Wa2, Wa4, iflag) + Nfev = Nfev + 1 + if (iflag < 0) exit main + + fnorm1 = enorm(m, Wa4) + + ! compute the scaled actual reduction. + + actred = -one + if (p1*fnorm1 < fnorm) actred = one - (fnorm1/fnorm)**2 + + ! compute the scaled predicted reduction and + ! the scaled directional derivative. + + do j = 1, n + Wa3(j) = zero + l = Ipvt(j) + temp = Wa1(l) + do i = 1, j + Wa3(i) = Wa3(i) + Fjac(i, j)*temp + end do + end do + temp1 = enorm(n, Wa3)/fnorm + temp2 = (sqrt(par)*pnorm)/fnorm + prered = temp1**2 + temp2**2/p5 + dirder = -(temp1**2 + temp2**2) + + ! compute the ratio of the actual to the predicted + ! reduction. + + ratio = zero + if (prered /= zero) ratio = actred/prered + + ! update the step bound. + + if (ratio <= p25) then + if (actred >= zero) temp = p5 + if (actred < zero) & + temp = p5*dirder/(dirder + p5*actred) + if (p1*fnorm1 >= fnorm .or. temp < p1) temp = p1 + delta = temp*min(delta, pnorm/p1) + par = par/temp + elseif (par == zero .or. ratio >= p75) then + delta = pnorm/p5 + par = p5*par + end if + + ! test for successful iteration. + + if (ratio >= p0001) then + + ! successful iteration. update x, fvec, and their norms. + + do j = 1, n + x(j) = Wa2(j) + Wa2(j) = Diag(j)*x(j) + end do + do i = 1, m + Fvec(i) = Wa4(i) + end do + xnorm = enorm(n, Wa2) + fnorm = fnorm1 + iter = iter + 1 + end if + + ! tests for convergence. + + if (abs(actred) <= Ftol .and. prered <= Ftol .and. & + p5*ratio <= one) Info = 1 + if (delta <= Xtol*xnorm) Info = 2 + if (abs(actred) <= Ftol .and. prered <= Ftol .and. & + p5*ratio <= one .and. Info == 2) Info = 3 + if (Info /= 0) exit main + + ! tests for termination and stringent tolerances. + + if (Nfev >= Maxfev) Info = 5 + if (abs(actred) <= epsmch .and. & + prered <= epsmch .and. p5*ratio <= one) & + Info = 6 + if (delta <= epsmch*xnorm) Info = 7 + if (gnorm <= epsmch) Info = 8 + if (Info /= 0) exit main + + if (ratio >= p0001) exit inner + + end do inner ! end of the inner loop. repeat if iteration unsuccessful. + + end do outer ! end of the outer loop. + + end block main + + ! termination, either normal or user imposed. + + if (iflag < 0) Info = iflag + iflag = 0 + if (Nprint > 0) call fcn(m, n, x, Fvec, iflag) + + end subroutine lmdif +!***************************************************************************************** + +!***************************************************************************************** +!> +! the purpose of lmdif1 is to minimize the sum of the squares of +! m nonlinear functions in n variables by a modification of the +! levenberg-marquardt algorithm. this is done by using the more +! general least-squares solver lmdif. the user must provide a +! subroutine which calculates the functions. the jacobian is +! then calculated by a forward-difference approximation. + + subroutine lmdif1(fcn, m, n, x, Fvec, Tol, Info, Iwa, Wa, Lwa) + implicit none + + procedure(func2) :: fcn !! the user-supplied subroutine which + !! calculates the functions. + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of functions. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of variables. n must not exceed m. + integer, intent(out) :: Info !! an integer output variable. if the user has + !! terminated execution, info is set to the (negative) + !! value of iflag. see description of fcn. otherwise, + !! info is set as follows: + !! + !! * ***info = 0*** improper input parameters. + !! * ***info = 1*** algorithm estimates that the relative error + !! in the sum of squares is at most tol. + !! * ***info = 2*** algorithm estimates that the relative error + !! between x and the solution is at most tol. + !! * ***info = 3*** conditions for info = 1 and info = 2 both hold. + !! * ***info = 4*** fvec is orthogonal to the columns of the + !! jacobian to machine precision. + !! * ***info = 5*** number of calls to fcn has reached or + !! exceeded 200*(n+1). + !! * ***info = 6*** tol is too small. no further reduction in + !! the sum of squares is possible. + !! * ***info = 7*** tol is too small. no further improvement in + !! the approximate solution x is possible. + integer, intent(in) :: Lwa !! a positive integer input variable not less than + !! m*n+5*n+m. + integer, intent(inout) :: Iwa(n) !! an integer work array of length n. + real(wp), intent(in) :: Tol !! a nonnegative input variable. termination occurs + !! when the algorithm estimates either that the relative + !! error in the sum of squares is at most tol or that + !! the relative error between x and the solution is at + !! most tol. + real(wp), intent(inout) :: x(n) !! an array of length n. on input x must contain + !! an initial estimate of the solution vector. on output x + !! contains the final estimate of the solution vector. + real(wp), intent(out) :: Fvec(m) !! an output array of length m which contains + !! the functions evaluated at the output x. + real(wp), intent(inout) :: Wa(Lwa) !! a work array of length lwa. + + integer :: maxfev, mode, mp5n, nfev, nprint + real(wp) :: epsfcn, ftol, gtol, xtol + + real(wp), parameter :: factor = 1.0e2_wp + + Info = 0 + + ! check the input parameters for errors. + + if (n > 0 .and. m >= n .and. Tol >= zero .and. Lwa >= m*n + 5*n + m) then + + ! call lmdif. + + maxfev = 200*(n + 1) + ftol = Tol + xtol = Tol + gtol = zero + epsfcn = zero + mode = 1 + nprint = 0 + mp5n = m + 5*n + call lmdif(fcn, m, n, x, Fvec, ftol, xtol, gtol, maxfev, epsfcn, Wa(1), & + mode, factor, nprint, Info, nfev, Wa(mp5n + 1), m, Iwa, & + Wa(n + 1), Wa(2*n + 1), Wa(3*n + 1), Wa(4*n + 1), Wa(5*n + 1)) + if (Info == 8) Info = 4 + end if + + end subroutine lmdif1 +!***************************************************************************************** + +!***************************************************************************************** +!> +! given an m by n matrix a, an n by n nonsingular diagonal +! matrix d, an m-vector b, and a positive number delta, +! the problem is to determine a value for the parameter +! par such that if x solves the system +!``` +! a*x = b , sqrt(par)*d*x = 0 , +!``` +! in the least squares sense, and dxnorm is the euclidean +! norm of d*x, then either par is zero and +!``` +! (dxnorm-delta) <= 0.1*delta , +!``` +! or par is positive and +!``` +! abs(dxnorm-delta) <= 0.1*delta . +!``` +! this subroutine completes the solution of the problem +! if it is provided with the necessary information from the +! qr factorization, with column pivoting, of a. that is, if +! a*p = q*r, where p is a permutation matrix, q has orthogonal +! columns, and r is an upper triangular matrix with diagonal +! elements of nonincreasing magnitude, then lmpar expects +! the full upper triangle of r, the permutation matrix p, +! and the first n components of (q transpose)*b. on output +! lmpar also provides an upper triangular matrix s such that +!``` +! t t t +! p *(a *a + par*d*d)*p = s *s . +!``` +! s is employed within lmpar and may be of separate interest. +! +! only a few iterations are generally needed for convergence +! of the algorithm. if, however, the limit of 10 iterations +! is reached, then the output par will contain the best +! value obtained so far. + + subroutine lmpar(n, r, Ldr, Ipvt, Diag, Qtb, Delta, Par, x, Sdiag, Wa1, Wa2) + implicit none + + integer, intent(in) :: n !! a positive integer input variable set to the order of r. + integer, intent(in) :: Ldr !! a positive integer input variable not less than n + !! which specifies the leading dimension of the array r. + integer, intent(in) :: Ipvt(n) !! an integer input array of length n which defines the + !! permutation matrix p such that a*p = q*r. column j of p + !! is column ipvt(j) of the identity matrix. + real(wp) :: Delta !! a positive input variable which specifies an upper + !! bound on the euclidean norm of d*x. + real(wp), intent(inout) :: Par !! a nonnegative variable. on input par contains an + !! initial estimate of the levenberg-marquardt parameter. + !! on output par contains the final estimate. + real(wp), intent(inout) :: r(Ldr, n) !! an n by n array. on input the full upper triangle + !! must contain the full upper triangle of the matrix r. + !! on output the full upper triangle is unaltered, and the + !! strict lower triangle contains the strict upper triangle + !! (transposed) of the upper triangular matrix s. + real(wp), intent(in) :: Diag(n) !! an input array of length n which must contain the + !! diagonal elements of the matrix d. + real(wp), intent(in) :: Qtb(n) !! an input array of length n which must contain the first + !! n elements of the vector (q transpose)*b. + real(wp), intent(out) :: x(n) !! an output array of length n which contains the least + !! squares solution of the system a*x = b, sqrt(par)*d*x = 0, + !! for the output par. + real(wp), intent(out) :: Sdiag(n) !! an output array of length n which contains the + !! diagonal elements of the upper triangular matrix s. + real(wp), intent(inout) :: Wa1(n) !! work array of length n. + real(wp), intent(inout) :: Wa2(n) !! work array of length n. + + integer :: i, iter, j, jm1, jp1, k, l, nsing + real(wp) :: dxnorm, fp, gnorm, parc, parl, paru, sum, temp + + real(wp), parameter :: p1 = 1.0e-1_wp + real(wp), parameter :: p001 = 1.0e-3_wp + real(wp), parameter :: dwarf = dpmpar(2) !! the smallest positive magnitude + + ! compute and store in x the gauss-newton direction. if the + ! jacobian is rank-deficient, obtain a least squares solution. + + nsing = n + do j = 1, n + Wa1(j) = Qtb(j) + if (r(j, j) == zero .and. nsing == n) nsing = j - 1 + if (nsing < n) Wa1(j) = zero + end do + if (nsing >= 1) then + do k = 1, nsing + j = nsing - k + 1 + Wa1(j) = Wa1(j)/r(j, j) + temp = Wa1(j) + jm1 = j - 1 + if (jm1 >= 1) then + do i = 1, jm1 + Wa1(i) = Wa1(i) - r(i, j)*temp + end do + end if + end do + end if + do j = 1, n + l = Ipvt(j) + x(l) = Wa1(j) + end do + + ! initialize the iteration counter. + ! evaluate the function at the origin, and test + ! for acceptance of the gauss-newton direction. + + iter = 0 + do j = 1, n + Wa2(j) = Diag(j)*x(j) + end do + dxnorm = enorm(n, Wa2) + fp = dxnorm - Delta + if (fp <= p1*Delta) then + ! termination. + if (iter == 0) Par = zero + else + + ! if the jacobian is not rank deficient, the newton + ! step provides a lower bound, parl, for the zero of + ! the function. otherwise set this bound to zero. + + parl = zero + if (nsing >= n) then + do j = 1, n + l = Ipvt(j) + Wa1(j) = Diag(l)*(Wa2(l)/dxnorm) + end do + do j = 1, n + sum = zero + jm1 = j - 1 + if (jm1 >= 1) then + do i = 1, jm1 + sum = sum + r(i, j)*Wa1(i) + end do + end if + Wa1(j) = (Wa1(j) - sum)/r(j, j) + end do + temp = enorm(n, Wa1) + parl = ((fp/Delta)/temp)/temp + end if + + ! calculate an upper bound, paru, for the zero of the function. + + do j = 1, n + sum = zero + do i = 1, j + sum = sum + r(i, j)*Qtb(i) + end do + l = Ipvt(j) + Wa1(j) = sum/Diag(l) + end do + gnorm = enorm(n, Wa1) + paru = gnorm/Delta + if (paru == zero) paru = dwarf/min(Delta, p1) + + ! if the input par lies outside of the interval (parl,paru), + ! set par to the closer endpoint. + + Par = max(Par, parl) + Par = min(Par, paru) + if (Par == zero) Par = gnorm/dxnorm + + ! beginning of an iteration. + do + + iter = iter + 1 + + ! evaluate the function at the current value of par. + + if (Par == zero) Par = max(dwarf, p001*paru) + temp = sqrt(Par) + do j = 1, n + Wa1(j) = temp*Diag(j) + end do + call qrsolv(n, r, Ldr, Ipvt, Wa1, Qtb, x, Sdiag, Wa2) + do j = 1, n + Wa2(j) = Diag(j)*x(j) + end do + dxnorm = enorm(n, Wa2) + temp = fp + fp = dxnorm - Delta + + ! if the function is small enough, accept the current value + ! of par. also test for the exceptional cases where parl + ! is zero or the number of iterations has reached 10. + + if (abs(fp) <= p1*Delta .or. parl == zero .and. fp <= temp .and. & + temp < zero .or. iter == 10) then + if (iter == 0) Par = zero + exit + else + + ! compute the newton correction. + + do j = 1, n + l = Ipvt(j) + Wa1(j) = Diag(l)*(Wa2(l)/dxnorm) + end do + do j = 1, n + Wa1(j) = Wa1(j)/Sdiag(j) + temp = Wa1(j) + jp1 = j + 1 + if (n >= jp1) then + do i = jp1, n + Wa1(i) = Wa1(i) - r(i, j)*temp + end do + end if + end do + temp = enorm(n, Wa1) + parc = ((fp/Delta)/temp)/temp + + ! depending on the sign of the function, update parl or paru. + + if (fp > zero) parl = max(parl, Par) + if (fp < zero) paru = min(paru, Par) + + ! compute an improved estimate for par. + + Par = max(parl, Par + parc) + + end if + + end do ! end of an iteration. + + end if + + end subroutine lmpar +!***************************************************************************************** + +!***************************************************************************************** +!> +! the purpose of lmstr is to minimize the sum of the squares of +! m nonlinear functions in n variables by a modification of +! the levenberg-marquardt algorithm which uses minimal storage. +! the user must provide a subroutine which calculates the +! functions and the rows of the jacobian. + + subroutine lmstr(fcn, m, n, x, Fvec, Fjac, Ldfjac, Ftol, Xtol, Gtol, Maxfev, & + Diag, Mode, Factor, Nprint, Info, Nfev, Njev, Ipvt, Qtf, & + Wa1, Wa2, Wa3, Wa4) + implicit none + + procedure(fcn_lmstr) :: fcn !! user-supplied subroutine which + !! calculates the functions and the rows of the jacobian. + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of functions. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of variables. n must not exceed m. + integer, intent(in) :: Ldfjac !! a positive integer input variable not less than n + !! which specifies the leading dimension of the array fjac. + integer, intent(in) :: Maxfev !! a positive integer input variable. termination + !! occurs when the number of calls to fcn with iflag = 1 + !! has reached maxfev. + integer, intent(in) :: Mode !! an integer input variable. if mode = 1, the + !! variables will be scaled internally. if mode = 2, + !! the scaling is specified by the input diag. other + !! values of mode are equivalent to mode = 1. + integer, intent(in) :: Nprint !! an integer input variable that enables controlled + !! printing of iterates if it is positive. in this case, + !! fcn is called with iflag = 0 at the beginning of the first + !! iteration and every nprint iterations thereafter and + !! immediately prior to return, with x and fvec available + !! for printing. if nprint is not positive, no special calls + !! of fcn with iflag = 0 are made. + integer, intent(out) :: Info !! an integer output variable. if the user has + !! terminated execution, info is set to the (negative) + !! value of iflag. see description of fcn. otherwise, + !! info is set as follows: + !! + !! * ***info = 0*** improper input parameters. + !! * ***info = 1*** both actual and predicted relative reductions + !! in the sum of squares are at most ftol. + !! * ***info = 2*** relative error between two consecutive iterates + !! is at most xtol. + !! * ***info = 3*** conditions for info = 1 and info = 2 both hold. + !! * ***info = 4*** the cosine of the angle between fvec and any + !! column of the jacobian is at most gtol in + !! absolute value. + !! * ***info = 5*** number of calls to fcn with iflag = 1 has + !! reached maxfev. + !! * ***info = 6*** ftol is too small. no further reduction in + !! the sum of squares is possible. + !! * ***info = 7*** xtol is too small. no further improvement in + !! the approximate solution x is possible. + !! * ***info = 8*** gtol is too small. fvec is orthogonal to the + !! columns of the jacobian to machine precision. + integer, intent(out) :: Nfev !! an integer output variable set to the number of + !! calls to fcn with iflag = 1. + integer, intent(out) :: Njev !! an integer output variable set to the number of + !! calls to fcn with iflag = 2. + integer, intent(out) :: Ipvt(n) !! an integer output array of length n. ipvt + !! defines a permutation matrix p such that jac*p = q*r, + !! where jac is the final calculated jacobian, q is + !! orthogonal (not stored), and r is upper triangular. + !! column j of p is column ipvt(j) of the identity matrix. + real(wp), intent(in) :: Ftol !! a nonnegative input variable. termination + !! occurs when both the actual and predicted relative + !! reductions in the sum of squares are at most ftol. + !! therefore, ftol measures the relative error desired + !! in the sum of squares. + real(wp), intent(in) :: Xtol !! a nonnegative input variable. termination + !! occurs when the relative error between two consecutive + !! iterates is at most xtol. therefore, xtol measures the + !! relative error desired in the approximate solution. + real(wp), intent(in) :: Gtol !! a nonnegative input variable. termination + !! occurs when the cosine of the angle between fvec and + !! any column of the jacobian is at most gtol in absolute + !! value. therefore, gtol measures the orthogonality + !! desired between the function vector and the columns + !! of the jacobian. + real(wp), intent(in) :: Factor !! a positive input variable used in determining the + !! initial step bound. this bound is set to the product of + !! factor and the euclidean norm of diag*x if nonzero, or else + !! to factor itself. in most cases factor should lie in the + !! interval (.1,100.). 100. is a generally recommended value. + real(wp), intent(inout) :: x(n) !! an array of length n. on input x must contain + !! an initial estimate of the solution vector. on output x + !! contains the final estimate of the solution vector. + real(wp), intent(out) :: Fvec(m) !! an output array of length m which contains + !! the functions evaluated at the output x. + real(wp), intent(out) :: Fjac(Ldfjac, n) !! an output n by n array. the upper triangle of fjac + !! contains an upper triangular matrix r such that + !!``` + !! t t t + !! p *(jac *jac)*p = r *r, + !!``` + !! where p is a permutation matrix and jac is the final + !! calculated jacobian. column j of p is column ipvt(j) + !! (see below) of the identity matrix. the lower triangular + !! part of fjac contains information generated during + !! the computation of r. + real(wp), intent(inout) :: Diag(n) !! an array of length n. if mode = 1 (see + !! below), diag is internally set. if mode = 2, diag + !! must contain positive entries that serve as + !! multiplicative scale factors for the variables. + real(wp), intent(out) :: Qtf(n) !! an output array of length n which contains + !! the first n elements of the vector (q transpose)*fvec. + real(wp), intent(inout) :: Wa1(n) !! work array of length n. + real(wp), intent(inout) :: Wa2(n) !! work array of length n. + real(wp), intent(inout) :: Wa3(n) !! work array of length n. + real(wp), intent(inout) :: Wa4(m) !! work array of length m. + + integer :: i, iflag, iter, j, l + real(wp) :: actred, delta, dirder, fnorm, & + fnorm1, gnorm, par, pnorm, prered, & + ratio, sum, temp, temp1, temp2, xnorm + logical :: sing + + real(wp), parameter :: p1 = 1.0e-1_wp + real(wp), parameter :: p5 = 5.0e-1_wp + real(wp), parameter :: p25 = 2.5e-1_wp + real(wp), parameter :: p75 = 7.5e-1_wp + real(wp), parameter :: p0001 = 1.0e-4_wp + + Info = 0 + iflag = 0 + Nfev = 0 + Njev = 0 + + main : block + + ! check the input parameters for errors. + + if (n <= 0 .or. m < n .or. Ldfjac < n .or. Ftol < zero .or. & + Xtol < zero .or. Gtol < zero .or. Maxfev <= 0 .or. Factor <= zero) & + exit main + if (Mode == 2) then + do j = 1, n + if (Diag(j) <= zero) exit main + end do + end if + + ! evaluate the function at the starting point + ! and calculate its norm. + + iflag = 1 + call fcn(m, n, x, Fvec, Wa3, iflag) + Nfev = 1 + if (iflag < 0) exit main + fnorm = enorm(m, Fvec) + + ! initialize levenberg-marquardt parameter and iteration counter. + + par = zero + iter = 1 + + ! beginning of the outer loop. + + outer : do + + ! if requested, call fcn to enable printing of iterates. + + if (Nprint > 0) then + iflag = 0 + if (mod(iter - 1, Nprint) == 0) call fcn(m, n, x, Fvec, Wa3, iflag) + if (iflag < 0) exit main + end if + + ! compute the qr factorization of the jacobian matrix + ! calculated one row at a time, while simultaneously + ! forming (q transpose)*fvec and storing the first + ! n components in qtf. + + do j = 1, n + Qtf(j) = zero + do i = 1, n + Fjac(i, j) = zero + end do + end do + iflag = 2 + do i = 1, m + call fcn(m, n, x, Fvec, Wa3, iflag) + if (iflag < 0) exit main + temp = Fvec(i) + call rwupdt(n, Fjac, Ldfjac, Wa3, Qtf, temp, Wa1, Wa2) + iflag = iflag + 1 + end do + Njev = Njev + 1 + + ! if the jacobian is rank deficient, call qrfac to + ! reorder its columns and update the components of qtf. + + sing = .false. + do j = 1, n + if (Fjac(j, j) == zero) sing = .true. + Ipvt(j) = j + Wa2(j) = enorm(j, Fjac(1, j)) + end do + if (sing) then + call qrfac(n, n, Fjac, Ldfjac, .true., Ipvt, n, Wa1, Wa2, Wa3) + do j = 1, n + if (Fjac(j, j) /= zero) then + sum = zero + do i = j, n + sum = sum + Fjac(i, j)*Qtf(i) + end do + temp = -sum/Fjac(j, j) + do i = j, n + Qtf(i) = Qtf(i) + Fjac(i, j)*temp + end do + end if + Fjac(j, j) = Wa1(j) + end do + end if + + ! on the first iteration and if mode is 1, scale according + ! to the norms of the columns of the initial jacobian. + + if (iter == 1) then + if (Mode /= 2) then + do j = 1, n + Diag(j) = Wa2(j) + if (Wa2(j) == zero) Diag(j) = one + end do + end if + + ! on the first iteration, calculate the norm of the scaled x + ! and initialize the step bound delta. + + do j = 1, n + Wa3(j) = Diag(j)*x(j) + end do + xnorm = enorm(n, Wa3) + delta = Factor*xnorm + if (delta == zero) delta = Factor + end if + + ! compute the norm of the scaled gradient. + + gnorm = zero + if (fnorm /= zero) then + do j = 1, n + l = Ipvt(j) + if (Wa2(l) /= zero) then + sum = zero + do i = 1, j + sum = sum + Fjac(i, j)*(Qtf(i)/fnorm) + end do + gnorm = max(gnorm, abs(sum/Wa2(l))) + end if + end do + end if + + ! test for convergence of the gradient norm. + + if (gnorm <= Gtol) Info = 4 + if (Info /= 0) exit main + + ! rescale if necessary. + + if (Mode /= 2) then + do j = 1, n + Diag(j) = max(Diag(j), Wa2(j)) + end do + end if + + ! beginning of the inner loop. + + inner : do + + ! determine the levenberg-marquardt parameter. + + call lmpar(n, Fjac, Ldfjac, Ipvt, Diag, Qtf, delta, par, Wa1, Wa2, Wa3, Wa4) + + ! store the direction p and x + p. calculate the norm of p. + + do j = 1, n + Wa1(j) = -Wa1(j) + Wa2(j) = x(j) + Wa1(j) + Wa3(j) = Diag(j)*Wa1(j) + end do + pnorm = enorm(n, Wa3) + + ! on the first iteration, adjust the initial step bound. + + if (iter == 1) delta = min(delta, pnorm) + + ! evaluate the function at x + p and calculate its norm. + + iflag = 1 + call fcn(m, n, Wa2, Wa4, Wa3, iflag) + Nfev = Nfev + 1 + if (iflag < 0) exit main + + fnorm1 = enorm(m, Wa4) + + ! compute the scaled actual reduction. + + actred = -one + if (p1*fnorm1 < fnorm) actred = one - (fnorm1/fnorm)**2 + + ! compute the scaled predicted reduction and + ! the scaled directional derivative. + + do j = 1, n + Wa3(j) = zero + l = Ipvt(j) + temp = Wa1(l) + do i = 1, j + Wa3(i) = Wa3(i) + Fjac(i, j)*temp + end do + end do + temp1 = enorm(n, Wa3)/fnorm + temp2 = (sqrt(par)*pnorm)/fnorm + prered = temp1**2 + temp2**2/p5 + dirder = -(temp1**2 + temp2**2) + + ! compute the ratio of the actual to the predicted + ! reduction. + + ratio = zero + if (prered /= zero) ratio = actred/prered + + ! update the step bound. + + if (ratio <= p25) then + if (actred >= zero) temp = p5 + if (actred < zero) temp = p5*dirder/(dirder + p5*actred) + if (p1*fnorm1 >= fnorm .or. temp < p1) temp = p1 + delta = temp*min(delta, pnorm/p1) + par = par/temp + elseif (par == zero .or. ratio >= p75) then + delta = pnorm/p5 + par = p5*par + end if + + ! test for successful iteration. + + if (ratio >= p0001) then + + ! successful iteration. update x, fvec, and their norms. + + do j = 1, n + x(j) = Wa2(j) + Wa2(j) = Diag(j)*x(j) + end do + do i = 1, m + Fvec(i) = Wa4(i) + end do + xnorm = enorm(n, Wa2) + fnorm = fnorm1 + iter = iter + 1 + end if + + ! tests for convergence. + + if (abs(actred) <= Ftol .and. prered <= Ftol .and. & + p5*ratio <= one) Info = 1 + if (delta <= Xtol*xnorm) Info = 2 + if (abs(actred) <= Ftol .and. prered <= Ftol .and. & + p5*ratio <= one .and. Info == 2) Info = 3 + if (Info /= 0) exit main + + ! tests for termination and stringent tolerances. + + if (Nfev >= Maxfev) Info = 5 + if (abs(actred) <= epsmch .and. prered <= epsmch .and. & + p5*ratio <= one) Info = 6 + if (delta <= epsmch*xnorm) Info = 7 + if (gnorm <= epsmch) Info = 8 + if (Info /= 0) exit main + + if (ratio >= p0001) exit inner + + end do inner ! end of the inner loop. repeat if iteration unsuccessful. + + end do outer ! end of the outer loop. + + end block main + + ! termination, either normal or user imposed. + + if (iflag < 0) Info = iflag + iflag = 0 + if (Nprint > 0) call fcn(m, n, x, Fvec, Wa3, iflag) + + end subroutine lmstr +!***************************************************************************************** + +!***************************************************************************************** +!> +! the purpose of lmstr1 is to minimize the sum of the squares of +! m nonlinear functions in n variables by a modification of +! the levenberg-marquardt algorithm which uses minimal storage. +! this is done by using the more general least-squares solver +! lmstr. the user must provide a subroutine which calculates +! the functions and the rows of the jacobian. + + subroutine lmstr1(fcn, m, n, x, Fvec, Fjac, Ldfjac, Tol, Info, Ipvt, Wa, Lwa) + implicit none + + procedure(fcn_lmstr) :: fcn !! user-supplied subroutine which + !! calculates the functions and the rows of the jacobian. + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of functions. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of variables. n must not exceed m. + integer, intent(in) :: Ldfjac !! a positive integer input variable not less than n + !! which specifies the leading dimension of the array fjac. + integer, intent(out) :: Info !! an integer output variable. if the user has + !! terminated execution, info is set to the (negative) + !! value of iflag. see description of fcn. otherwise, + !! info is set as follows: + !! + !! * ***info = 0*** improper input parameters. + !! * ***info = 1*** algorithm estimates that the relative error + !! in the sum of squares is at most tol. + !! * ***info = 2*** algorithm estimates that the relative error + !! between x and the solution is at most tol. + !! * ***info = 3*** conditions for info = 1 and info = 2 both hold. + !! * ***info = 4*** fvec is orthogonal to the columns of the + !! jacobian to machine precision. + !! * ***info = 5*** number of calls to fcn with iflag = 1 has + !! reached 100*(n+1). + !! * ***info = 6*** tol is too small. no further reduction in + !! the sum of squares is possible. + !! * ***info = 7*** tol is too small. no further improvement in + !! the approximate solution x is possible. + integer, intent(in) :: Lwa !! a positive integer input variable not less than 5*n+m. + integer, intent(out) :: Ipvt(n) !! an integer output array of length n. ipvt + !! defines a permutation matrix p such that jac*p = q*r, + !! where jac is the final calculated jacobian, q is + !! orthogonal (not stored), and r is upper triangular. + !! column j of p is column ipvt(j) of the identity matrix. + real(wp), intent(in) :: Tol !! a nonnegative input variable. termination occurs + !! when the algorithm estimates either that the relative + !! error in the sum of squares is at most tol or that + !! the relative error between x and the solution is at + !! most tol. + real(wp), intent(inout) :: x(n) !! an array of length n. on input x must contain + !! an initial estimate of the solution vector. on output x + !! contains the final estimate of the solution vector. + real(wp), intent(out) :: Fvec(m) !! an output array of length m which contains + !! the functions evaluated at the output x. + real(wp), intent(out) :: Fjac(Ldfjac, n) !! an output n by n array. the upper triangle of fjac + !! contains an upper triangular matrix r such that + !!``` + !! t t t + !! p *(jac *jac)*p = r *r, + !!``` + !! where p is a permutation matrix and jac is the final + !! calculated jacobian. column j of p is column ipvt(j) + !! (see below) of the identity matrix. the lower triangular + !! part of fjac contains information generated during + !! the computation of r. + real(wp), intent(inout) :: Wa(Lwa) !! a work array of length lwa. + + integer :: maxfev, mode, nfev, njev, nprint + real(wp) :: ftol, gtol, xtol + + real(wp), parameter :: factor = 1.0e2_wp + + Info = 0 + + ! check the input parameters for errors. + + if (n > 0 .and. m >= n .and. Ldfjac >= n .and. Tol >= zero .and. & + Lwa >= 5*n + m) then + + ! call lmstr. + + maxfev = 100*(n + 1) + ftol = Tol + xtol = Tol + gtol = zero + mode = 1 + nprint = 0 + call lmstr(fcn, m, n, x, Fvec, Fjac, Ldfjac, ftol, xtol, gtol, maxfev, & + Wa(1), mode, factor, nprint, Info, nfev, njev, Ipvt, Wa(n + 1), & + Wa(2*n + 1), Wa(3*n + 1), Wa(4*n + 1), Wa(5*n + 1)) + if (Info == 8) Info = 4 + end if + + end subroutine lmstr1 +!***************************************************************************************** + +!***************************************************************************************** +!> +! this subroutine proceeds from the computed qr factorization of +! an m by n matrix a to accumulate the m by m orthogonal matrix +! q from its factored form. + + subroutine qform(m, n, q, Ldq, Wa) + implicit none + + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of rows of a and the order of q. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of columns of a. + integer, intent(in) :: Ldq !! a positive integer input variable not less than m + !! which specifies the leading dimension of the array q. + real(wp), intent(inout) :: q(Ldq, m) !! an m by m array. on input the full lower trapezoid in + !! the first min(m,n) columns of q contains the factored form. + !! on output q has been accumulated into a square matrix. + real(wp), intent(inout) :: Wa(m) !! a work array of length m. + + integer :: i, j, jm1, k, l, minmn, np1 + real(wp) :: sum, temp + + ! zero out upper triangle of q in the first min(m,n) columns. + + minmn = min(m, n) + if (minmn >= 2) then + do j = 2, minmn + jm1 = j - 1 + do i = 1, jm1 + q(i, j) = zero + end do + end do + end if + + ! initialize remaining columns to those of the identity matrix. + + np1 = n + 1 + if (m >= np1) then + do j = np1, m + do i = 1, m + q(i, j) = zero + end do + q(j, j) = one + end do + end if + + ! accumulate q from its factored form. + + do l = 1, minmn + k = minmn - l + 1 + do i = k, m + Wa(i) = q(i, k) + q(i, k) = zero + end do + q(k, k) = one + if (Wa(k) /= zero) then + do j = k, m + sum = zero + do i = k, m + sum = sum + q(i, j)*Wa(i) + end do + temp = sum/Wa(k) + do i = k, m + q(i, j) = q(i, j) - temp*Wa(i) + end do + end do + end if + end do + + end subroutine qform +!***************************************************************************************** + +!***************************************************************************************** +!> +! this subroutine uses householder transformations with column +! pivoting (optional) to compute a qr factorization of the +! m by n matrix a. that is, qrfac determines an orthogonal +! matrix q, a permutation matrix p, and an upper trapezoidal +! matrix r with diagonal elements of nonincreasing magnitude, +! such that a*p = q*r. the householder transformation for +! column k, k = 1,2,...,min(m,n), is of the form +!``` +! t +! i - (1/u(k))*u*u +!``` +! where u has zeros in the first k-1 positions. the form of +! this transformation and the method of pivoting first +! appeared in the corresponding linpack subroutine. + + subroutine qrfac(m, n, a, Lda, Pivot, Ipvt, Lipvt, Rdiag, Acnorm, Wa) + implicit none + + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of rows of a. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of columns of a. + integer, intent(in) :: Lda !! a positive integer input variable not less than m + !! which specifies the leading dimension of the array a. + integer, intent(in) :: Lipvt !! a positive integer input variable. if pivot is false, + !! then lipvt may be as small as 1. if pivot is true, then + !! lipvt must be at least n. + integer, intent(out) :: Ipvt(Lipvt) !! an integer output array of length lipvt. ipvt + !! defines the permutation matrix p such that a*p = q*r. + !! column j of p is column ipvt(j) of the identity matrix. + !! if pivot is false, ipvt is not referenced. + logical, intent(in) :: Pivot !! a logical input variable. if pivot is set true, + !! then column pivoting is enforced. if pivot is set false, + !! then no column pivoting is done. + real(wp), intent(inout) :: a(Lda, n) !! an m by n array. on input a contains the matrix for + !! which the qr factorization is to be computed. on output + !! the strict upper trapezoidal part of a contains the strict + !! upper trapezoidal part of r, and the lower trapezoidal + !! part of a contains a factored form of q (the non-trivial + !! elements of the u vectors described above). + real(wp), intent(out) :: Rdiag(n) !! an output array of length n which contains the + !! diagonal elements of r. + real(wp), intent(out) :: Acnorm(n) !! an output array of length n which contains the + !! norms of the corresponding columns of the input matrix a. + !! if this information is not needed, then acnorm can coincide + !! with rdiag. + real(wp), intent(inout) :: Wa(n) !! a work array of length n. if pivot is false, then wa + !! can coincide with rdiag. + + integer :: i, j, jp1, k, kmax, minmn + real(wp) :: ajnorm, sum, temp + + real(wp), parameter :: p05 = 5.0e-2_wp + + ! compute the initial column norms and initialize several arrays. + + do j = 1, n + Acnorm(j) = enorm(m, a(1, j)) + Rdiag(j) = Acnorm(j) + Wa(j) = Rdiag(j) + if (Pivot) Ipvt(j) = j + end do + + ! reduce a to r with householder transformations. + + minmn = min(m, n) + do j = 1, minmn + if (Pivot) then + + ! bring the column of largest norm into the pivot position. + + kmax = j + do k = j, n + if (Rdiag(k) > Rdiag(kmax)) kmax = k + end do + if (kmax /= j) then + do i = 1, m + temp = a(i, j) + a(i, j) = a(i, kmax) + a(i, kmax) = temp + end do + Rdiag(kmax) = Rdiag(j) + Wa(kmax) = Wa(j) + k = Ipvt(j) + Ipvt(j) = Ipvt(kmax) + Ipvt(kmax) = k + end if + end if + + ! compute the householder transformation to reduce the + ! j-th column of a to a multiple of the j-th unit vector. + + ajnorm = enorm(m - j + 1, a(j, j)) + if (ajnorm /= zero) then + if (a(j, j) < zero) ajnorm = -ajnorm + do i = j, m + a(i, j) = a(i, j)/ajnorm + end do + a(j, j) = a(j, j) + one + + ! apply the transformation to the remaining columns + ! and update the norms. + + jp1 = j + 1 + if (n >= jp1) then + do k = jp1, n + sum = zero + do i = j, m + sum = sum + a(i, j)*a(i, k) + end do + temp = sum/a(j, j) + do i = j, m + a(i, k) = a(i, k) - temp*a(i, j) + end do + if (.not. (.not. Pivot .or. Rdiag(k) == zero)) then + temp = a(j, k)/Rdiag(k) + Rdiag(k) = Rdiag(k)*sqrt(max(zero, one - temp**2)) + if (p05*(Rdiag(k)/Wa(k))**2 <= epsmch) then + Rdiag(k) = enorm(m - j, a(jp1, k)) + Wa(k) = Rdiag(k) + end if + end if + end do + end if + end if + Rdiag(j) = -ajnorm + end do + + end subroutine qrfac +!***************************************************************************************** + +!***************************************************************************************** +!> +! given an m by n matrix a, an n by n diagonal matrix d, +! and an m-vector b, the problem is to determine an x which +! solves the system +!``` +! a*x = b , d*x = 0 , +!``` +! in the least squares sense. +! +! this subroutine completes the solution of the problem +! if it is provided with the necessary information from the +! qr factorization, with column pivoting, of a. that is, if +! a*p = q*r, where p is a permutation matrix, q has orthogonal +! columns, and r is an upper triangular matrix with diagonal +! elements of nonincreasing magnitude, then qrsolv expects +! the full upper triangle of r, the permutation matrix p, +! and the first n components of (q transpose)*b. the system +! a*x = b, d*x = 0, is then equivalent to +!``` +! t t +! r*z = q *b , p *d*p*z = 0 , +!``` +! where x = p*z. if this system does not have full rank, +! then a least squares solution is obtained. on output qrsolv +! also provides an upper triangular matrix s such that +!``` +! t t t +! p *(a *a + d*d)*p = s *s . +!``` +! s is computed within qrsolv and may be of separate interest. + + subroutine qrsolv(n, r, Ldr, Ipvt, Diag, Qtb, x, Sdiag, Wa) + implicit none + + integer, intent(in) :: n !! a positive integer input variable set to the order of r. + integer, intent(in) :: Ldr !! a positive integer input variable not less than n + !! which specifies the leading dimension of the array r. + integer, intent(in) :: Ipvt(n) !! an integer input array of length n which defines the + !! permutation matrix p such that a*p = q*r. column j of p + !! is column ipvt(j) of the identity matrix. + real(wp), intent(inout) :: r(Ldr, n) !! an n by n array. on input the full upper triangle + !! must contain the full upper triangle of the matrix r. + !! on output the full upper triangle is unaltered, and the + !! strict lower triangle contains the strict upper triangle + !! (transposed) of the upper triangular matrix s. + real(wp), intent(in) :: Diag(n) !! an input array of length n which must contain the + !! diagonal elements of the matrix d. + real(wp), intent(in) :: Qtb(n) !! an input array of length n which must contain the first + !! n elements of the vector (q transpose)*b. + real(wp), intent(out) :: x(n) !! an output array of length n which contains the least + !! squares solution of the system a*x = b, d*x = 0. + real(wp), intent(out) :: Sdiag(n) !! an output array of length n which contains the + !! diagonal elements of the upper triangular matrix s. + real(wp), intent(inout) :: Wa(n) !! a work array of length n. + + integer :: i, j, jp1, k, kp1, l, nsing + real(wp) :: cos, cotan, qtbpj, sin, sum, tan, temp + + real(wp), parameter :: p5 = 5.0e-1_wp + real(wp), parameter :: p25 = 2.5e-1_wp + + ! copy r and (q transpose)*b to preserve input and initialize s. + ! in particular, save the diagonal elements of r in x. + + do j = 1, n + do i = j, n + r(i, j) = r(j, i) + end do + x(j) = r(j, j) + Wa(j) = Qtb(j) + end do + + ! eliminate the diagonal matrix d using a givens rotation. + + do j = 1, n + + ! prepare the row of d to be eliminated, locating the + ! diagonal element using p from the qr factorization. + + l = Ipvt(j) + if (Diag(l) /= zero) then + do k = j, n + Sdiag(k) = zero + end do + Sdiag(j) = Diag(l) + + ! the transformations to eliminate the row of d + ! modify only a single element of (q transpose)*b + ! beyond the first n, which is initially zero. + + qtbpj = zero + do k = j, n + + ! determine a givens rotation which eliminates the + ! appropriate element in the current row of d. + + if (Sdiag(k) /= zero) then + if (abs(r(k, k)) >= abs(Sdiag(k))) then + tan = Sdiag(k)/r(k, k) + cos = p5/sqrt(p25 + p25*tan**2) + sin = cos*tan + else + cotan = r(k, k)/Sdiag(k) + sin = p5/sqrt(p25 + p25*cotan**2) + cos = sin*cotan + end if + + ! compute the modified diagonal element of r and + ! the modified element of ((q transpose)*b,0). + + r(k, k) = cos*r(k, k) + sin*Sdiag(k) + temp = cos*Wa(k) + sin*qtbpj + qtbpj = -sin*Wa(k) + cos*qtbpj + Wa(k) = temp + + ! accumulate the tranformation in the row of s. + + kp1 = k + 1 + if (n >= kp1) then + do i = kp1, n + temp = cos*r(i, k) + sin*Sdiag(i) + Sdiag(i) = -sin*r(i, k) + cos*Sdiag(i) + r(i, k) = temp + end do + end if + end if + end do + end if + + ! store the diagonal element of s and restore + ! the corresponding diagonal element of r. + + Sdiag(j) = r(j, j) + r(j, j) = x(j) + end do + + ! solve the triangular system for z. if the system is + ! singular, then obtain a least squares solution. + + nsing = n + do j = 1, n + if (Sdiag(j) == zero .and. nsing == n) nsing = j - 1 + if (nsing < n) Wa(j) = zero + end do + if (nsing >= 1) then + do k = 1, nsing + j = nsing - k + 1 + sum = zero + jp1 = j + 1 + if (nsing >= jp1) then + do i = jp1, nsing + sum = sum + r(i, j)*Wa(i) + end do + end if + Wa(j) = (Wa(j) - sum)/Sdiag(j) + end do + end if + + ! permute the components of z back to components of x. + + do j = 1, n + l = Ipvt(j) + x(l) = Wa(j) + end do + + end subroutine qrsolv +!***************************************************************************************** + +!***************************************************************************************** +!> +! given an m by n matrix a, this subroutine computes a*q where +! q is the product of 2*(n - 1) transformations +!``` +! gv(n-1)*...*gv(1)*gw(1)*...*gw(n-1) +!``` +! and gv(i), gw(i) are givens rotations in the (i,n) plane which +! eliminate elements in the i-th and n-th planes, respectively. +! q itself is not given, rather the information to recover the +! gv, gw rotations is supplied. + + subroutine r1mpyq(m, n, a, Lda, v, w) + implicit none + + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of rows of a. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of columns of a. + integer, intent(in) :: Lda !! a positive integer input variable not less than m + !! which specifies the leading dimension of the array a. + real(wp), intent(inout) :: a(Lda, n) !! an m by n array. on input a must contain the matrix + !! to be postmultiplied by the orthogonal matrix q + !! described above. on output a*q has replaced a. + real(wp), intent(in) :: v(n) !! an input array of length n. v(i) must contain the + !! information necessary to recover the givens rotation gv(i) + !! described above. + real(wp), intent(in) :: w(n) !! an input array of length n. w(i) must contain the + !! information necessary to recover the givens rotation gw(i) + !! described above. + + integer :: i, j, nmj, nm1 + real(wp) :: cos, sin, temp + + ! apply the first set of givens rotations to a. + + nm1 = n - 1 + if (nm1 >= 1) then + do nmj = 1, nm1 + j = n - nmj + if (abs(v(j)) > one) then + cos = one/v(j) + sin = sqrt(one - cos**2) + else + sin = v(j) + cos = sqrt(one - sin**2) + end if + do i = 1, m + temp = cos*a(i, j) - sin*a(i, n) + a(i, n) = sin*a(i, j) + cos*a(i, n) + a(i, j) = temp + end do + end do + + ! apply the second set of givens rotations to a. + + do j = 1, nm1 + if (abs(w(j)) > one) cos = one/w(j) + if (abs(w(j)) > one) sin = sqrt(one - cos**2) + if (abs(w(j)) <= one) sin = w(j) + if (abs(w(j)) <= one) cos = sqrt(one - sin**2) + do i = 1, m + temp = cos*a(i, j) + sin*a(i, n) + a(i, n) = -sin*a(i, j) + cos*a(i, n) + a(i, j) = temp + end do + end do + end if + + end subroutine r1mpyq +!***************************************************************************************** + +!***************************************************************************************** +!> +! given an m by n lower trapezoidal matrix s, an m-vector u, +! and an n-vector v, the problem is to determine an +! orthogonal matrix q such that +!``` +! t +! (s + u*v )*q +!``` +! is again lower trapezoidal. +! +! this subroutine determines q as the product of 2*(n - 1) +! transformations +!``` +! gv(n-1)*...*gv(1)*gw(1)*...*gw(n-1) +!``` +! where gv(i), gw(i) are givens rotations in the (i,n) plane +! which eliminate elements in the i-th and n-th planes, +! respectively. q itself is not accumulated, rather the +! information to recover the gv, gw rotations is returned. + + subroutine r1updt(m, n, s, Ls, u, v, w, Sing) + implicit none + + integer, intent(in) :: m !! a positive integer input variable set to the number + !! of rows of s. + integer, intent(in) :: n !! a positive integer input variable set to the number + !! of columns of s. n must not exceed m. + integer, intent(in) :: Ls !! a positive integer input variable not less than + !! (n*(2*m-n+1))/2. + logical, intent(out) :: Sing !! a logical output variable. sing is set true if any + !! of the diagonal elements of the output s are zero. otherwise + !! sing is set false. + real(wp), intent(inout) :: s(Ls) !! an array of length ls. on input s must contain the lower + !! trapezoidal matrix s stored by columns. on output s contains + !! the lower trapezoidal matrix produced as described above. + real(wp), intent(in) :: u(m) !! an input array of length m which must contain the + !! vector u. + real(wp), intent(inout) :: v(n) !! an array of length n. on input v must contain the vector + !! v. on output v(i) contains the information necessary to + !! recover the givens rotation gv(i) described above. + real(wp), intent(out) :: w(m) !! an output array of length m. w(i) contains information + !! necessary to recover the givens rotation gw(i) described + !! above. + + integer :: i, j, jj, l, nmj, nm1 + real(wp) :: cos, cotan, sin, tan, tau, temp + + real(wp), parameter :: p5 = 5.0e-1_wp + real(wp), parameter :: p25 = 2.5e-1_wp + real(wp), parameter :: giant = dpmpar(3) !! the largest magnitude. + + ! initialize the diagonal element pointer. + + jj = (n*(2*m - n + 1))/2 - (m - n) + + ! move the nontrivial part of the last column of s into w. + + l = jj + do i = n, m + w(i) = s(l) + l = l + 1 + end do + + ! rotate the vector v into a multiple of the n-th unit vector + ! in such a way that a spike is introduced into w. + + nm1 = n - 1 + if (nm1 >= 1) then + do nmj = 1, nm1 + j = n - nmj + jj = jj - (m - j + 1) + w(j) = zero + if (v(j) /= zero) then + + ! determine a givens rotation which eliminates the + ! j-th element of v. + + if (abs(v(n)) >= abs(v(j))) then + tan = v(j)/v(n) + cos = p5/sqrt(p25 + p25*tan**2) + sin = cos*tan + tau = sin + else + cotan = v(n)/v(j) + sin = p5/sqrt(p25 + p25*cotan**2) + cos = sin*cotan + tau = one + if (abs(cos)*giant > one) tau = one/cos + end if + + ! apply the transformation to v and store the information + ! necessary to recover the givens rotation. + + v(n) = sin*v(j) + cos*v(n) + v(j) = tau + + ! apply the transformation to s and extend the spike in w. + + l = jj + do i = j, m + temp = cos*s(l) - sin*w(i) + w(i) = sin*s(l) + cos*w(i) + s(l) = temp + l = l + 1 + end do + end if + end do + end if + + ! add the spike from the rank 1 update to w. + + do i = 1, m + w(i) = w(i) + v(n)*u(i) + end do + + ! eliminate the spike. + + Sing = .false. + if (nm1 >= 1) then + do j = 1, nm1 + if (w(j) /= zero) then + + ! determine a givens rotation which eliminates the + ! j-th element of the spike. + + if (abs(s(jj)) >= abs(w(j))) then + tan = w(j)/s(jj) + cos = p5/sqrt(p25 + p25*tan**2) + sin = cos*tan + tau = sin + else + cotan = s(jj)/w(j) + sin = p5/sqrt(p25 + p25*cotan**2) + cos = sin*cotan + tau = one + if (abs(cos)*giant > one) tau = one/cos + end if + + ! apply the transformation to s and reduce the spike in w. + + l = jj + do i = j, m + temp = cos*s(l) + sin*w(i) + w(i) = -sin*s(l) + cos*w(i) + s(l) = temp + l = l + 1 + end do + + ! store the information necessary to recover the + ! givens rotation. + + w(j) = tau + end if + + ! test for zero diagonal elements in the output s. + + if (s(jj) == zero) Sing = .true. + jj = jj + (m - j + 1) + end do + end if + + ! move w back into the last column of the output s. + + l = jj + do i = n, m + s(l) = w(i) + l = l + 1 + end do + if (s(jj) == zero) Sing = .true. + + end subroutine r1updt +!***************************************************************************************** + +!***************************************************************************************** +!> +! given an n by n upper triangular matrix r, this subroutine +! computes the qr decomposition of the matrix formed when a row +! is added to r. if the row is specified by the vector w, then +! rwupdt determines an orthogonal matrix q such that when the +! n+1 by n matrix composed of r augmented by w is premultiplied +! by (q transpose), the resulting matrix is upper trapezoidal. +! the matrix (q transpose) is the product of n transformations +!``` +! g(n)*g(n-1)* ... *g(1) +!``` +! where g(i) is a givens rotation in the (i,n+1) plane which +! eliminates elements in the (n+1)-st plane. rwupdt also +! computes the product (q transpose)*c where c is the +! (n+1)-vector (b,alpha). q itself is not accumulated, rather +! the information to recover the g rotations is supplied. + + subroutine rwupdt(n, r, Ldr, w, b, Alpha, Cos, Sin) + implicit none + + integer, intent(in) :: n !! a positive integer input variable set to the order of r. + integer, intent(in) :: Ldr !! a positive integer input variable not less than n + !! which specifies the leading dimension of the array r. + real(wp), intent(inout) :: Alpha !! a variable. on input alpha must contain the + !! (n+1)-st element of the vector c. on output alpha contains + !! the (n+1)-st element of the vector (q transpose)*c. + real(wp), intent(inout) :: r(Ldr, n) !! an n by n array. on input the upper triangular part of + !! r must contain the matrix to be updated. on output r + !! contains the updated triangular matrix. + real(wp), intent(in) :: w(n) !! an input array of length n which must contain the row + !! vector to be added to r. + real(wp), intent(inout) :: b(n) !! an array of length n. on input b must contain the + !! first n elements of the vector c. on output b contains + !! the first n elements of the vector (q transpose)*c. + real(wp), intent(out) :: Cos(n) !! an output array of length n which contains the + !! cosines of the transforming givens rotations. + real(wp), intent(out) :: Sin(n) !! an output array of length n which contains the + !! sines of the transforming givens rotations. + + integer :: i, j, jm1 + real(wp) :: cotan, rowj, tan, temp + + real(wp), parameter :: p5 = 5.0e-1_wp + real(wp), parameter :: p25 = 2.5e-1_wp + + do j = 1, n + rowj = w(j) + jm1 = j - 1 + + ! apply the previous transformations to + ! r(i,j), i=1,2,...,j-1, and to w(j). + + if (jm1 >= 1) then + do i = 1, jm1 + temp = Cos(i)*r(i, j) + Sin(i)*rowj + rowj = -Sin(i)*r(i, j) + Cos(i)*rowj + r(i, j) = temp + end do + end if + + ! determine a givens rotation which eliminates w(j). + + Cos(j) = one + Sin(j) = zero + if (rowj /= zero) then + if (abs(r(j, j)) >= abs(rowj)) then + tan = rowj/r(j, j) + Cos(j) = p5/sqrt(p25 + p25*tan**2) + Sin(j) = Cos(j)*tan + else + cotan = r(j, j)/rowj + Sin(j) = p5/sqrt(p25 + p25*cotan**2) + Cos(j) = Sin(j)*cotan + end if + + ! apply the current transformation to r(j,j), b(j), and alpha. + + r(j, j) = Cos(j)*r(j, j) + Sin(j)*rowj + temp = Cos(j)*b(j) + Sin(j)*Alpha + Alpha = -Sin(j)*b(j) + Cos(j)*Alpha + b(j) = temp + end if + end do + + end subroutine rwupdt +!***************************************************************************************** + +!***************************************************************************************** +end module minpack_module +!***************************************************************************************** diff --git a/examples/minpack/routine_inventory.py b/examples/minpack/routine_inventory.py new file mode 100644 index 000000000..d8dd32052 --- /dev/null +++ b/examples/minpack/routine_inventory.py @@ -0,0 +1,15 @@ +"""Reviewed public MINPACK surface and its explicit test mapping.""" + +from __future__ import annotations + +ROUTINE_GROUPS: dict[str, tuple[str, ...]] = { + "Diagnostics and finite differences": ("chkder", "enorm", "fdjac1", "fdjac2"), + "Hybrid nonlinear solvers": ("hybrd", "hybrd1", "hybrj", "hybrj1"), + "Levenberg-Marquardt solvers": ("lmder", "lmder1", "lmdif", "lmdif1", "lmstr", "lmstr1"), + "Factorization and update helpers": ("dogleg", "lmpar", "qform", "qrfac", "qrsolv", "r1mpyq", "r1updt", "rwupdt"), +} + +ALL_ROUTINES = tuple(routine for group in ROUTINE_GROUPS.values() for routine in group) +PRIK_TESTED_ROUTINES = frozenset(ALL_ROUTINES) +UNSUPPORTED_ROUTINES: dict[str, str] = {} +EXPLICIT_TEST_NAMES = {routine: f"test_{routine}" for routine in ALL_ROUTINES} diff --git a/examples/minpack/tests/__init__.py b/examples/minpack/tests/__init__.py new file mode 100644 index 000000000..7db0f2a93 --- /dev/null +++ b/examples/minpack/tests/__init__.py @@ -0,0 +1 @@ +"""Numerical validation suite for the PRIK MINPACK example.""" diff --git a/examples/minpack/tests/helpers.py b/examples/minpack/tests/helpers.py new file mode 100644 index 000000000..aa3863e00 --- /dev/null +++ b/examples/minpack/tests/helpers.py @@ -0,0 +1,85 @@ +"""Small deterministic problems shared by the MINPACK routine tests.""" + +from __future__ import annotations + +import numpy as np +from scipy import optimize + + +INT = np.int32 +FLOAT = np.float64 +TARGET = np.array([1.0, -2.0], dtype=np.float64) + + +def vector(values=(4.0, 4.0)) -> np.ndarray: + """Return one writable float64 vector for a two-variable test problem.""" + return np.array(values, dtype=np.float64) + + +def matrix() -> np.ndarray: + """Return one writable Fortran-order 2-by-2 float64 matrix.""" + return np.empty((2, 2), dtype=np.float64, order="F") + + +def residual_callback(_count, x, fvec, _iflag) -> None: + """Write the residual of the linear root problem ``x - TARGET``.""" + fvec[:] = x - TARGET + + +def squared_residual_callback(_count, x, fvec, _iflag) -> None: + """Write the elementwise nonlinear residual ``x**2 - 1``.""" + fvec[:] = x**2 - 1.0 + + +def squared_least_squares_callback(_m, _n, x, fvec, _iflag) -> None: + """Write the elementwise nonlinear residual for ``fdjac2``.""" + fvec[:] = x**2 - 1.0 + + +def jacobian_callback(_count, x, fvec, fjac, _ldfjac, iflag) -> None: + """Write residuals or the exact identity Jacobian as MINPACK requests.""" + if iflag == 1: + fvec[:] = x - TARGET + elif iflag == 2: + fjac[:, :] = np.eye(2, dtype=np.float64) + + +def least_squares_callback(_m, _n, x, fvec, _iflag) -> None: + """Write residuals for the two-equation least-squares problem.""" + fvec[:] = x - TARGET + + +def least_squares_jacobian_callback(_m, _n, x, fvec, fjac, _ldfjac, iflag) -> None: + """Write residuals or an exact Jacobian for LMDER-style callbacks.""" + if iflag == 1: + fvec[:] = x - TARGET + elif iflag == 2: + fjac[:, :] = np.eye(2, dtype=np.float64) + + +def least_squares_row_callback(_m, _n, x, fvec, fjrow, iflag) -> None: + """Write residuals or the one requested identity-Jacobian row.""" + if iflag == 1: + fvec[:] = x - TARGET + else: + fjrow[:] = 0.0 + fjrow[int(iflag) - 2] = 1.0 + + +def scipy_root() -> np.ndarray: + """Solve the same root problem independently through SciPy.""" + result = optimize.root(lambda x: x - TARGET, vector()) + assert result.success + return result.x + + +def scipy_least_squares() -> np.ndarray: + """Solve the same least-squares problem independently through SciPy.""" + result = optimize.least_squares(lambda x: x - TARGET, vector()) + assert result.success + return result.x + + +def assert_solution(x: np.ndarray, reference: np.ndarray) -> None: + """Check a MINPACK iterate against the independently solved target.""" + np.testing.assert_allclose(x, reference, rtol=0.0, atol=1.0e-10) diff --git a/examples/minpack/tests/test_diagnostics.py b/examples/minpack/tests/test_diagnostics.py new file mode 100644 index 000000000..c588f9e3e --- /dev/null +++ b/examples/minpack/tests/test_diagnostics.py @@ -0,0 +1,90 @@ +"""Diagnostic norms, gradients, immutable constants, and finite differences.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from .helpers import FLOAT, INT, matrix, squared_least_squares_callback, squared_residual_callback, vector + + +pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] + + +def test_dpmpar_is_an_immutable_float64_snapshot(minpack): + values = minpack.dpmpar + + np.testing.assert_array_equal( + values, + np.array([np.finfo(np.float64).eps, np.finfo(np.float64).tiny, np.finfo(np.float64).max]), + ) + assert values.flags.writeable is False + with pytest.raises(ValueError, match="read-only"): + values[0] = 1.0 + + +def test_enorm(minpack): + values = np.array([3.0, 4.0, 12.0], dtype=np.float64) + + assert minpack.enorm(INT(values.size), values) == pytest.approx(np.linalg.norm(values)) + + +def test_chkder(minpack): + x = np.array([1.5, -2.0], dtype=np.float64) + fvec = x.copy() + fjac = np.eye(2, dtype=np.float64, order="F") + xp = np.empty(2, dtype=np.float64) + fvecp = np.empty(2, dtype=np.float64) + err = np.empty(2, dtype=np.float64) + + minpack.chkder(INT(2), INT(2), x, fvec, fjac, INT(2), xp, fvecp, INT(1), err) + fvecp[:] = xp + minpack.chkder(INT(2), INT(2), x, fvec, fjac, INT(2), xp, fvecp, INT(2), err) + + np.testing.assert_allclose(err, np.ones(2), rtol=0.0, atol=1.0e-12) + + +def test_fdjac1(minpack): + x = vector((1.0, 2.0)) + fvec = x**2 - 1.0 + fjac = matrix() + + result = minpack.fdjac1( + squared_residual_callback, + INT(2), + x, + fvec, + fjac, + INT(2), + INT(0), + INT(1), + INT(1), + FLOAT(0.0), + np.empty(2), + np.empty(2), + ) + + assert result == INT(0) + np.testing.assert_allclose(fjac, np.diag([2.0, 4.0]), rtol=1.0e-7, atol=1.0e-7) + + +def test_fdjac2(minpack): + x = vector((1.0, 2.0)) + fvec = x**2 - 1.0 + fjac = matrix() + + result = minpack.fdjac2( + squared_least_squares_callback, + INT(2), + INT(2), + x, + fvec, + fjac, + INT(2), + INT(0), + FLOAT(0.0), + np.empty(2), + ) + + assert result == INT(0) + np.testing.assert_allclose(fjac, np.diag([2.0, 4.0]), rtol=1.0e-7, atol=1.0e-7) diff --git a/examples/minpack/tests/test_linear_algebra.py b/examples/minpack/tests/test_linear_algebra.py new file mode 100644 index 000000000..e247d64d8 --- /dev/null +++ b/examples/minpack/tests/test_linear_algebra.py @@ -0,0 +1,173 @@ +"""Factorization and update helpers validated with linear-algebra invariants.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from .helpers import FLOAT, INT + + +pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] +TWO = INT(2) + + +def _apply_r1_rotations(values: np.ndarray, v: np.ndarray, w: np.ndarray) -> np.ndarray: + """Apply MINPACK's encoded right-side Givens rotations in NumPy.""" + result = values.copy() + last = result.shape[1] - 1 + for column in range(last - 1, -1, -1): + encoded = v[column] + if abs(encoded) > 1.0: + cosine = 1.0 / encoded + sine = np.sqrt(1.0 - cosine**2) + else: + sine = encoded + cosine = np.sqrt(1.0 - sine**2) + current, final = result[:, column].copy(), result[:, last].copy() + result[:, column] = cosine * current - sine * final + result[:, last] = sine * current + cosine * final + for column in range(last): + encoded = w[column] + if abs(encoded) > 1.0: + cosine = 1.0 / encoded + sine = np.sqrt(1.0 - cosine**2) + else: + sine = encoded + cosine = np.sqrt(1.0 - sine**2) + current, final = result[:, column].copy(), result[:, last].copy() + result[:, column] = cosine * current + sine * final + result[:, last] = -sine * current + cosine * final + return result + + +def test_dogleg(minpack): + r = np.array([1.0, 0.0, 1.0], dtype=np.float64) + diagonal = np.ones(2, dtype=np.float64) + qtb = np.array([3.0, 4.0], dtype=np.float64) + x = np.empty(2, dtype=np.float64) + + minpack.dogleg(TWO, r, INT(3), diagonal, qtb, FLOAT(1.0), x, np.empty(2), np.empty(2)) + + np.testing.assert_allclose(x, np.array([0.6, 0.8]), rtol=0.0, atol=1.0e-12) + assert np.linalg.norm(x) == pytest.approx(1.0) + + +def test_lmpar(minpack): + r = np.eye(2, dtype=np.float64, order="F") + x = np.empty(2, dtype=np.float64) + sdiag = np.empty(2, dtype=np.float64) + + delta, par = minpack.lmpar( + TWO, + r, + TWO, + np.array([1, 2], dtype=np.int32), + np.ones(2), + np.array([3.0, 4.0]), + FLOAT(1.0), + FLOAT(0.0), + x, + sdiag, + np.empty(2), + np.empty(2), + ) + + assert (delta, par) == (FLOAT(1.0), FLOAT(4.0)) + np.testing.assert_allclose(x, np.array([0.6, 0.8]), rtol=0.0, atol=1.0e-12) + np.testing.assert_allclose(sdiag, np.sqrt(5.0), rtol=0.0, atol=1.0e-12) + + +def test_qrfac(minpack): + values = np.asfortranarray([[3.0, 0.0], [4.0, 5.0]], dtype=np.float64) + pivots = np.zeros(2, dtype=np.int32) + diagonal = np.zeros(2, dtype=np.float64) + norms = np.zeros(2, dtype=np.float64) + workspace = np.zeros(2, dtype=np.float64) + + minpack.qrfac(TWO, TWO, values, TWO, True, pivots, TWO, diagonal, norms, workspace) + + np.testing.assert_array_equal(pivots, np.array([1, 2], dtype=np.int32)) + np.testing.assert_allclose(norms, np.array([5.0, 5.0])) + np.testing.assert_allclose(diagonal, np.array([-5.0, -3.0]), atol=1.0e-12) + + +def test_qform(minpack): + values = np.asfortranarray([[3.0, 0.0], [4.0, 5.0]], dtype=np.float64) + workspace = np.zeros(2, dtype=np.float64) + minpack.qrfac( + TWO, + TWO, + values, + TWO, + True, + np.zeros(2, dtype=np.int32), + TWO, + np.zeros(2), + np.zeros(2), + np.zeros(2), + ) + + minpack.qform(TWO, TWO, values, TWO, workspace) + + np.testing.assert_allclose(values.T @ values, np.eye(2), rtol=0.0, atol=1.0e-12) + + +def test_qrsolv(minpack): + x = np.empty(2, dtype=np.float64) + sdiag = np.empty(2, dtype=np.float64) + + minpack.qrsolv( + TWO, + np.eye(2, dtype=np.float64, order="F"), + TWO, + np.array([1, 2], dtype=np.int32), + np.ones(2), + np.array([3.0, 4.0]), + x, + sdiag, + np.empty(2), + ) + + np.testing.assert_allclose(x, np.array([1.5, 2.0]), rtol=0.0, atol=1.0e-12) + np.testing.assert_allclose(sdiag, np.sqrt(2.0), rtol=0.0, atol=1.0e-12) + + +def test_r1mpyq(minpack): + values = np.asfortranarray([[1.0, 2.0], [3.0, 4.0]], dtype=np.float64) + original = values.copy(order="F") + v = np.array([0.6, 0.0], dtype=np.float64) + w = np.array([-0.8, 0.0], dtype=np.float64) + expected = _apply_r1_rotations(original, v, w) + + minpack.r1mpyq(TWO, TWO, values, TWO, v, w) + + np.testing.assert_allclose(values, expected, rtol=0.0, atol=1.0e-12) + np.testing.assert_allclose(np.linalg.norm(values, axis=1), np.linalg.norm(original, axis=1)) + + +def test_r1updt(minpack): + packed_lower = np.array([2.0, 1.0, 3.0], dtype=np.float64) + original = np.array([[2.0, 0.0], [1.0, 3.0]], dtype=np.float64) + u = np.array([0.5, -1.0], dtype=np.float64) + v = np.array([0.25, 0.75], dtype=np.float64) + original_v = v.copy() + work = np.empty(2, dtype=np.float64) + + singular = minpack.r1updt(TWO, TWO, packed_lower, INT(3), u, v, work) + + assert singular is False + expected = _apply_r1_rotations(original + np.outer(u, original_v), v, work) + updated_lower = np.array([[packed_lower[0], 0.0], [packed_lower[1], packed_lower[2]]]) + np.testing.assert_allclose(expected, updated_lower, rtol=0.0, atol=1.0e-12) + + +def test_rwupdt(minpack): + r = np.zeros((2, 2), dtype=np.float64, order="F") + b = np.array([3.0, 4.0], dtype=np.float64) + original_norm = np.linalg.norm(np.array([*b, 5.0])) + + alpha = minpack.rwupdt(TWO, r, TWO, np.array([1.0, 2.0]), b, FLOAT(5.0), np.empty(2), np.empty(2)) + + assert np.linalg.norm(np.array([*b, alpha])) == pytest.approx(original_norm) + np.testing.assert_allclose(r, np.array([[1.0, 2.0], [0.0, 0.0]]), rtol=0.0, atol=1.0e-12) diff --git a/examples/minpack/tests/test_routine_coverage.py b/examples/minpack/tests/test_routine_coverage.py new file mode 100644 index 000000000..4d8f203ea --- /dev/null +++ b/examples/minpack/tests/test_routine_coverage.py @@ -0,0 +1,57 @@ +"""Fail closed when the reviewed MINPACK surface or explicit tests drift.""" + +from __future__ import annotations + +import ast +from pathlib import Path + +import pytest + +from ..routine_inventory import ( + ALL_ROUTINES, + EXPLICIT_TEST_NAMES, + PRIK_TESTED_ROUTINES, + ROUTINE_GROUPS, + UNSUPPORTED_ROUTINES, +) + + +pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] +TEST_ROOT = Path(__file__).parent +TEST_MODULES = ("test_diagnostics.py", "test_linear_algebra.py", "test_solvers.py") + + +def _test_functions() -> dict[str, tuple[Path, ast.FunctionDef]]: + """Collect uniquely named top-level routine tests from this reviewed suite.""" + functions: dict[str, tuple[Path, ast.FunctionDef]] = {} + for filename in TEST_MODULES: + path = TEST_ROOT / filename + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name.startswith("test_"): + assert node.name not in functions, f"duplicate explicit test name: {node.name}" + functions[node.name] = (path, node) + return functions + + +def test_every_public_minpack_routine_has_one_visible_numerical_test(): + functions = _test_functions() + assert len(ALL_ROUTINES) == len(set(ALL_ROUTINES)) + assert set(ALL_ROUTINES) == PRIK_TESTED_ROUTINES + assert UNSUPPORTED_ROUTINES == {} + + for routine, test_name in EXPLICIT_TEST_NAMES.items(): + path, node = functions[test_name] + source = ast.get_source_segment(path.read_text(encoding="utf-8"), node) + assert source is not None + assert routine in source, f"{test_name} does not visibly exercise {routine}" + assert "minpack" in source, f"{test_name} does not visibly invoke MINPACK" + + +def test_inventory_groups_cover_the_generated_public_surface_once(minpack): + grouped = tuple(routine for group in ROUTINE_GROUPS.values() for routine in group) + exported = {name for name in dir(minpack) if not name.startswith("_")} + + assert grouped == ALL_ROUTINES + assert len(grouped) == len(set(grouped)) + assert exported == {*ALL_ROUTINES, "dpmpar"} diff --git a/examples/minpack/tests/test_solvers.py b/examples/minpack/tests/test_solvers.py new file mode 100644 index 000000000..9f78d9741 --- /dev/null +++ b/examples/minpack/tests/test_solvers.py @@ -0,0 +1,265 @@ +"""Callback-driven MINPACK solvers checked against SciPy's solution.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from .helpers import ( + FLOAT, + INT, + assert_solution, + jacobian_callback, + least_squares_callback, + least_squares_jacobian_callback, + least_squares_row_callback, + matrix, + residual_callback, + scipy_least_squares, + scipy_root, + vector, +) + + +pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] +ROOT_TOLERANCE = FLOAT(1.0e-12) +MAX_EVALUATIONS = INT(100) +FACTOR = FLOAT(100.0) +ZERO = FLOAT(0.0) +ONE = INT(1) +TWO = INT(2) + + +def test_hybrd(minpack): + x, fvec, fjac, r, qtf = vector(), vector((0.0, 0.0)), matrix(), np.empty(3), np.empty(2) + info, calls = minpack.hybrd( + residual_callback, + TWO, + x, + fvec, + ROOT_TOLERANCE, + MAX_EVALUATIONS, + ONE, + ONE, + ZERO, + np.ones(2), + TWO, + FACTOR, + INT(0), + fjac, + TWO, + r, + INT(3), + qtf, + vector((0.0, 0.0)), + vector((0.0, 0.0)), + vector((0.0, 0.0)), + vector((0.0, 0.0)), + ) + + assert info == INT(1) + assert calls > 0 + assert_solution(x, scipy_root()) + np.testing.assert_allclose(fvec, 0.0, atol=1.0e-10) + + +def test_hybrd1(minpack): + x, fvec = vector(), vector((0.0, 0.0)) + info = minpack.hybrd1(residual_callback, TWO, x, fvec, ROOT_TOLERANCE, np.empty(19), INT(19)) + + assert info == INT(1) + assert_solution(x, scipy_root()) + np.testing.assert_allclose(fvec, 0.0, atol=1.0e-10) + + +def test_hybrj(minpack): + x, fvec, fjac, r, qtf = vector(), vector((0.0, 0.0)), matrix(), np.empty(3), np.empty(2) + info, function_calls, jacobian_calls = minpack.hybrj( + jacobian_callback, + TWO, + x, + fvec, + fjac, + TWO, + ROOT_TOLERANCE, + MAX_EVALUATIONS, + np.ones(2), + TWO, + FACTOR, + INT(0), + r, + INT(3), + qtf, + vector((0.0, 0.0)), + vector((0.0, 0.0)), + vector((0.0, 0.0)), + vector((0.0, 0.0)), + ) + + assert (info, function_calls, jacobian_calls) == (INT(1), INT(2), INT(1)) + assert_solution(x, scipy_root()) + np.testing.assert_allclose(fvec, 0.0, atol=1.0e-10) + + +def test_hybrj1(minpack): + x, fvec, fjac = vector(), vector((0.0, 0.0)), matrix() + info = minpack.hybrj1(jacobian_callback, TWO, x, fvec, fjac, TWO, ROOT_TOLERANCE, np.empty(15), INT(15)) + + assert info == INT(1) + assert_solution(x, scipy_root()) + np.testing.assert_allclose(fvec, 0.0, atol=1.0e-10) + + +def test_lmder(minpack): + x, fvec, fjac, ipvt, qtf = vector(), vector((0.0, 0.0)), matrix(), np.empty(2, dtype=np.int32), np.empty(2) + info, function_calls, jacobian_calls = minpack.lmder( + least_squares_jacobian_callback, + TWO, + TWO, + x, + fvec, + fjac, + TWO, + ROOT_TOLERANCE, + ROOT_TOLERANCE, + ZERO, + MAX_EVALUATIONS, + np.ones(2), + TWO, + FACTOR, + INT(0), + ipvt, + qtf, + vector((0.0, 0.0)), + vector((0.0, 0.0)), + vector((0.0, 0.0)), + vector((0.0, 0.0)), + ) + + assert (info, function_calls, jacobian_calls) == (INT(4), INT(2), INT(2)) + assert_solution(x, scipy_least_squares()) + np.testing.assert_allclose(fvec, 0.0, atol=1.0e-10) + + +def test_lmder1(minpack): + x, fvec, fjac, ipvt = vector(), vector((0.0, 0.0)), matrix(), np.empty(2, dtype=np.int32) + info = minpack.lmder1( + least_squares_jacobian_callback, + TWO, + TWO, + x, + fvec, + fjac, + TWO, + ROOT_TOLERANCE, + ipvt, + np.empty(12), + INT(12), + ) + + assert info == INT(4) + assert_solution(x, scipy_least_squares()) + np.testing.assert_allclose(fvec, 0.0, atol=1.0e-10) + + +def test_lmdif(minpack): + x, fvec, fjac, ipvt, qtf = vector(), vector((0.0, 0.0)), matrix(), np.empty(2, dtype=np.int32), np.empty(2) + info, function_calls = minpack.lmdif( + least_squares_callback, + TWO, + TWO, + x, + fvec, + ROOT_TOLERANCE, + ROOT_TOLERANCE, + ZERO, + MAX_EVALUATIONS, + ZERO, + np.ones(2), + TWO, + FACTOR, + INT(0), + fjac, + TWO, + ipvt, + qtf, + vector((0.0, 0.0)), + vector((0.0, 0.0)), + vector((0.0, 0.0)), + vector((0.0, 0.0)), + ) + + assert (info, function_calls) == (INT(4), INT(6)) + assert_solution(x, scipy_least_squares()) + np.testing.assert_allclose(fvec, 0.0, atol=1.0e-10) + + +def test_lmdif1(minpack): + x, fvec, iwa = vector(), vector((0.0, 0.0)), np.empty(2, dtype=np.int32) + info = minpack.lmdif1( + least_squares_callback, + TWO, + TWO, + x, + fvec, + ROOT_TOLERANCE, + iwa, + np.empty(16), + INT(16), + ) + + assert info == INT(4) + assert_solution(x, scipy_least_squares()) + np.testing.assert_allclose(fvec, 0.0, atol=1.0e-10) + + +def test_lmstr(minpack): + x, fvec, fjac, ipvt, qtf = vector(), vector((0.0, 0.0)), matrix(), np.empty(2, dtype=np.int32), np.empty(2) + info, function_calls, jacobian_calls = minpack.lmstr( + least_squares_row_callback, + TWO, + TWO, + x, + fvec, + fjac, + TWO, + ROOT_TOLERANCE, + ROOT_TOLERANCE, + ZERO, + MAX_EVALUATIONS, + np.ones(2), + TWO, + FACTOR, + INT(0), + ipvt, + qtf, + vector((0.0, 0.0)), + vector((0.0, 0.0)), + vector((0.0, 0.0)), + vector((0.0, 0.0)), + ) + + assert (info, function_calls, jacobian_calls) == (INT(4), INT(2), INT(2)) + assert_solution(x, scipy_least_squares()) + np.testing.assert_allclose(fvec, 0.0, atol=1.0e-10) + + +def test_lmstr1(minpack): + x, fvec, fjac, ipvt = vector(), vector((0.0, 0.0)), matrix(), np.empty(2, dtype=np.int32) + info = minpack.lmstr1( + least_squares_row_callback, + TWO, + TWO, + x, + fvec, + fjac, + TWO, + ROOT_TOLERANCE, + ipvt, + np.empty(12), + INT(12), + ) + + assert info == INT(4) + assert_solution(x, scipy_least_squares()) + np.testing.assert_allclose(fvec, 0.0, atol=1.0e-10) diff --git a/mkdocs.yml b/mkdocs.yml index acd1c3339..0fce22ae1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,5 @@ -site_name: PRIK +site_name: PRIK — Python Runtime Interop Kit +site_description: PRIK generates native Python bindings from Fortran projects, producing importable extensions and editable .pyi contracts for Pythonic APIs. site_url: https://pynumlab.github.io/prik/ repo_url: https://github.com/PyNumLab/prik repo_name: GitHub @@ -17,6 +18,7 @@ extra_css: - stylesheets/code-copy.css extra_javascript: - javascripts/code-copy.js + - javascripts/faq.js plugins: - search hooks: @@ -24,6 +26,7 @@ hooks: markdown_extensions: - admonition - attr_list + - md_in_html - toc: permalink: true exclude_docs: | @@ -70,6 +73,8 @@ nav: - Overview: user/examples/index.md - BLAS Wrapper: user/examples/blas-wrapper.md - LAPACK Wrapper: user/examples/lapack-wrapper.md + - FFTPACK Wrapper: user/examples/fftpack-wrapper.md + - MINPACK Wrapper: user/examples/minpack-wrapper.md - CFD Mini Example: user/examples/cfd-mini-example.md - MPI Example: user/examples/mpi-example.md - Object-Oriented Fortran: user/examples/object-oriented-fortran.md @@ -117,6 +122,7 @@ nav: - Source Map: developer/source-map.md - Feature To Code Map: developer/feature-to-code-map.md - Repository Structure: developer/repository-structure.md + - Compiler Preprocessing Reference: developer/compiler-preprocessing.md # PRIK_C_DOCS: - C Parser Reference: developer/c-parser-reference.md - Fortran Parser Reference: developer/fortran-parser-reference.md - Quality Assurance: developer/quality-assurance.md diff --git a/prik/README.md b/prik/README.md index 6e96b7643..4229f4de6 100644 --- a/prik/README.md +++ b/prik/README.md @@ -15,10 +15,10 @@ jumping directly into generated-code internals. | `runtime/` | Python runtime objects used by generated extensions. | | `types/` | Semantic-to-Python ecosystem type mappings. | | `parsers/` | Parser namespace containing the `c`, `fortran`, and semantic `.pyi` frontends. | -| `semantics/` | Language-neutral semantic IR, policy completion, and `.pyi` conversion. | -| `wrapper_codegen/` | Canonical wrapper plans, direct native bridge/binding generation, and source printers. | +| `semantics/` | Language-neutral semantic IR, declaration-expression provenance, policy completion, and `.pyi` conversion. | +| `codegen/` | Canonical wrapper plans, direct native bridge/binding generation, and source printers. | | `compiling/` | Native compiler objects, wrapper compilation, native support installation, and linking. | -| `utilities/` | Small domain-neutral helpers, including class visitor dispatch. | +| `utilities/` | Shared parsing, normalization, rendering, evaluation, and visitor helpers. | The package root contains the public entrypoint modules plus the shared `stage_values.py` record support. Supported library symbols are flattened @@ -27,6 +27,13 @@ through `prik.__init__`; internal modules import their canonical owner. is part of semantic `.pyi` syntax. Parser-specific imports use the public `prik.parsers.c`, `prik.parsers.fortran`, and `prik.parsers.pyi` namespaces. +Array declaration expressions cross three source packages in a fixed order: +`utilities/declaration_expressions.py` parses and normalizes expression text, +`semantics/` records native callable provenance and completes support policy, +and `codegen/` consumes only the completed result while rendering generated +artifacts. Follow that order when changing an expression feature; source +printers, bridges, and bindings must not infer missing expression semantics. + ## Source Navigation Docs - `docs/developer/source-map.md` diff --git a/prik/__init__.py b/prik/__init__.py index 97e2142ed..99894f3b1 100644 --- a/prik/__init__.py +++ b/prik/__init__.py @@ -40,7 +40,7 @@ ) from prik.semantics.pyi2ir import convert_pyi_to_ir from prik.pipeline.pyi import pyi_file_to_semantic_module, pyi_paths_to_semantic_modules, pyi_text_to_semantic_module -from prik.wrapper_codegen.printers import emit_module_stubs, opaque_dependency_modules +from prik.codegen.printers import emit_module_stubs, opaque_dependency_modules from prik.runtime.handles import AllocatableArray, NativeArrayHandleBase, PointerArray __version__ = _distribution_version("prik") diff --git a/prik/cli.py b/prik/cli.py index f5c6ecac7..09d80d0c5 100644 --- a/prik/cli.py +++ b/prik/cli.py @@ -496,36 +496,24 @@ def _parse_fortran_source_files( code, _preprocessing_recipe = _fortran_source_for_path(path, preprocessing) parsed_files.append((path, parser.parse_file(code, filename=str(path)))) - if len(parsed_files) > 1: - _resolve_fortran_project_parameters(parser, [parsed for _path, parsed in parsed_files]) + _resolve_fortran_project_parameters(parser, [parsed for _path, parsed in parsed_files]) return parsed_files def _resolve_fortran_project_parameters(parser: FortranParser, parsed_files) -> None: """Apply project-wide parameter facts without enforcing global symbols.""" - module_params: dict[str, dict[str, str]] = {} - for parsed_file in parsed_files: - if parsed_file.source is not None: - module_params.update(parser._collect_module_parameters(parsed_file.source, parsed_file.filename)) + module_params = parser._helper_project_module_symbols(parsed_files) seen_procedures: set[int] = set() for parsed_file in parsed_files: - for proc in parsed_file.procedures: + for proc in parser._helper_project_file_procedures(parsed_file): if id(proc) not in seen_procedures: parser._resolve_signature_kinds(proc, module_params, resolve_shapes=False) seen_procedures.add(id(proc)) for module in parsed_file.modules: parser._resolve_module_variable_kinds(module, module_params) - for proc in module.procedures: - if id(proc) not in seen_procedures: - parser._resolve_signature_kinds(proc, module_params, resolve_shapes=False) - seen_procedures.add(id(proc)) for submodule in parsed_file.submodules: parser._resolve_module_variable_kinds(submodule, module_params) - for proc in submodule.procedures: - if id(proc) not in seen_procedures: - parser._resolve_signature_kinds(proc, module_params, resolve_shapes=False) - seen_procedures.add(id(proc)) for program in parsed_file.programs: parser._resolve_module_variable_kinds(program, module_params) for block_data in parsed_file.block_data_units: @@ -610,7 +598,7 @@ def _convert_fortran_semantic_sources( def _semantic_payload_for_converted_files(converted_files) -> dict[str, dict]: - from prik.wrapper_codegen.printers import emit_module_stubs + from prik.codegen.printers import emit_module_stubs out: dict[str, dict] = {} available_modules = [module for _p, modules in converted_files for module in modules] @@ -637,7 +625,7 @@ def _is_fortran_semantic_file(modules) -> bool: def _fortran_contract_payload(path: Path, modules, available_modules) -> dict[str, object]: - from prik.wrapper_codegen.printers import emit_module_stubs + from prik.codegen.printers import emit_module_stubs native_modules = [module for module in modules if module.origin.source_kind == "module"] external_modules = [module for module in modules if module.origin.source_kind != "module"] diff --git a/prik/wrapper_codegen/__init__.py b/prik/codegen/__init__.py similarity index 100% rename from prik/wrapper_codegen/__init__.py rename to prik/codegen/__init__.py diff --git a/prik/wrapper_codegen/c/__init__.py b/prik/codegen/c/__init__.py similarity index 100% rename from prik/wrapper_codegen/c/__init__.py rename to prik/codegen/c/__init__.py diff --git a/prik/wrapper_codegen/c/binding.py b/prik/codegen/c/binding.py similarity index 91% rename from prik/wrapper_codegen/c/binding.py rename to prik/codegen/c/binding.py index 450bb80ff..0fefd394e 100644 --- a/prik/wrapper_codegen/c/binding.py +++ b/prik/codegen/c/binding.py @@ -1,4 +1,11 @@ -"""Direct recursive C binding generation from shared wrapper plans.""" +"""Lower validated wrapper plans into CPython C binding syntax nodes. + +Use :class:`CBindingGenerator` after post-IR policy completion and wrapper +planning. Its visitor entrypoint consumes a validated `ModulePlan` and +returns a `CModule` plus `CHeader` for the source printers. This stage +only projects completed binding actions: it does not infer ownership, +conversion, or lifecycle policy from datatypes or local state. +""" from __future__ import annotations @@ -6,6 +13,7 @@ import math import re +from prik.utilities.declaration_expressions import declaration_extent_uses_power, render_declaration_extent from prik.semantics.ownership import ( CodegenAction, ObjectKind, @@ -42,7 +50,8 @@ WritebackPhase, overload_builtin_scalar_family, ) -from prik.wrapper_codegen.nodes import ( +from prik.types.numpy import is_boolean_semantic_type_name +from prik.codegen.nodes import ( CAllowThreadsBegin, CAllowThreadsEnd, CBreak, @@ -68,8 +77,8 @@ CStructDefinition, CodeExpression, ) -from prik.wrapper_codegen.naming import NativeSymbolNames -from prik.wrapper_codegen.plan import ( +from prik.codegen.naming import NativeSymbolNames +from prik.codegen.plan import ( ArrayHandoffPlan, ArgumentTransferPlan, CallbackHandoffPlan, @@ -93,12 +102,18 @@ NativeCallSlotPlan, ResultPlan, ) -from prik.wrapper_codegen.primitive_scalar_types import PrimitiveScalarTypeRegistry -from prik.wrapper_codegen.visitor import ClassVisitor +from prik.codegen.primitive_scalar_types import PrimitiveScalarTypeRegistry +from prik.codegen.visitor import ClassVisitor @dataclass class _CArgumentNames: + """Binding-private C local names for one planned Python argument. + + The context builder creates this immutable record once per argument so all + extraction, conversion, call, and writeback helpers use identical names. + """ + object_name: str value_name: str length_name: str @@ -115,6 +130,13 @@ class _CArgumentNames: @dataclass class _CFunctionContext: + """Per-function names and role substitutions shared across C lowering. + + The record is derived from a completed function plan and is read-only while + declarations, conversion nodes, bridge calls, and result assembly are + emitted. + """ + arguments: dict[str, _CArgumentNames] native_outputs: dict[str, str] result_name: str | None @@ -124,13 +146,25 @@ class _CFunctionContext: class CBindingGenerator(ClassVisitor): - """Recursively lower binding plan views directly into C syntax nodes.""" + """Build the CPython C half of a wrapper from validated binding-plan views. + + Use :meth:`require_supported` followed by :meth:`visit` for a single + C module/header pair, or :meth:`binding_modules` when a plan qualifies + for independent wrapper shards. The returned nodes are normally consumed + by the C source printer. Completed semantic policy remains outside this + class; unsupported plan actions fail instead of being reinterpreted here. + """ _SHARD_MIN_FUNCTIONS = 128 _SHARD_TARGET_FUNCTIONS = 32 def require_supported(self, plan: ModulePlan) -> None: - """Preflight C scalar types after shared plan validation.""" + """Preflight primitive spellings needed by an already-validated plan. + + Call this before direct binding lowering. It checks only capability + of the C scalar registry; cross-view consistency and policy selection + are completed and validated before this backend stage. + """ for derived in self._derived_types(plan): self._require_derived_type_supported(derived) for function in self._functions(plan): @@ -189,17 +223,28 @@ def _require_argument_supported(self, argument: ArgumentTransferPlan) -> None: self._require_backend_type_supported(argument.semantic_type_name, argument.datatype_family) def _visit_ModulePlan(self, plan: ModulePlan) -> tuple[CModule, CHeader]: - """Return a complete C module and header from one shared plan.""" + """Build the matching C implementation module and public header. + + Both artifacts project the same validated plan. They are returned as + nodes so the next printer stage can render them independently. + """ return self.binding_module(plan), self.binding_header(plan) def binding_module(self, plan: ModulePlan) -> CModule: - """Lower the binding implementation from one completed wrapper plan.""" + """Build one complete C implementation module from a validated plan. + + This records class Python names for later property-source generation, + then assembles module support, runtime helpers, wrappers, and module + initialization in emitted dependency order. + """ + # Stage 1: cache names that the generated class-property surface shares. self._class_python_names = { surface.type_identity: surface.python_names[0] for namespace in plan.namespaces for surface in namespace.classes if surface.python_names } + # Stage 2: select support and assemble generated functions in dependency order. functions = tuple(function for namespace in plan.namespaces for function in self.visit(namespace)) needs_native_support = self.requires_native_support(plan) needs_free = self._module_needs_allocator(plan) @@ -210,6 +255,7 @@ def binding_module(self, plan: ModulePlan) -> CModule: declarations=self._module_declarations(plan), functions=( *self._module_allocator_functions(needs_free), + *self._extent_expression_support_functions(plan), *self._callback_runtime_functions(plan), *self._derived_call_runtime_functions(plan), *self._derived_origin_functions(plan), @@ -224,7 +270,12 @@ def binding_module(self, plan: ModulePlan) -> CModule: ) def binding_modules(self, plan: ModulePlan) -> tuple[CModule, ...]: - """Lower one or more independently compilable binding units.""" + """Build one implementation module or independently compilable wrapper shards. + + Use this public entrypoint when compilation can benefit from sharding. + Plans with coupled runtime support intentionally return one module so + helper state and declarations remain shared. + """ module = self.binding_module(plan) function_groups = self._binding_function_shards(plan) if not function_groups: @@ -329,9 +380,85 @@ def _module_has_shard_runtime_state(self, plan: ModulePlan) -> bool: self._module_needs_allocator(plan), self._module_uses_callbacks(plan), self._module_uses_derived_calls(plan), + self._module_uses_extent_power(plan), + ) + ) + + def _extent_expression_support_functions(self, plan: ModulePlan) -> tuple[CFunction, ...]: + """Emit the integer-power helper only when a completed extent uses it.""" + if not self._module_uses_extent_power(plan): + return () + return ( + CFunction( + "prik_extent_power", + "npy_intp", + parameters=(CParameter("base", "npy_intp"), CParameter("exponent", "npy_intp")), + storage="static", + body=( + CIf( + CodeExpression("exponent < 0"), + body=( + CIf(CodeExpression("base == 1"), body=(CReturn(CodeExpression("1")),)), + CIf( + CodeExpression("base == -1"), + body=(CReturn(CodeExpression("(exponent % 2) ? -1 : 1")),), + ), + CReturn(CodeExpression("0")), + ), + ), + CDeclaration("value", "npy_intp", CodeExpression("1")), + CFor( + "", + CodeExpression("exponent > 0"), + CodeExpression("exponent /= 2"), + body=( + CIf( + CodeExpression("exponent % 2 != 0"), + body=(CExpressionStatement(CodeExpression("value *= base")),), + ), + CIf( + CodeExpression("exponent > 1"), + body=(CExpressionStatement(CodeExpression("base *= base")),), + ), + ), + ), + CReturn(CodeExpression("value")), + ), + ), + ) + + def _module_uses_extent_power(self, plan: ModulePlan) -> bool: + """Return whether any executable plan-owned array extent uses ``**``.""" + return ( + any(self._array_uses_extent_power(variable.array) for variable in self._variables(plan)) + or any( + self._array_uses_extent_power(field.array) + for derived in self._derived_types(plan) + for field in derived.fields + ) + or any(self._function_uses_extent_power(function) for function in self._functions(plan)) + ) + + def _function_uses_extent_power(self, function: FunctionPlan) -> bool: + """Scan one function's direct and callback transfer arrays for ``**``.""" + direct_owners = (*function.arguments, *function.results, *function.native_call_slots) + if any(self._array_uses_extent_power(owner.array) for owner in direct_owners): + return True + callbacks = (argument.callback for argument in function.arguments if argument.callback is not None) + return any( + self._array_uses_extent_power(transfer.array) + for callback in callbacks + for transfer in ( + *callback.arguments, + *((callback.result.transfer,) if callback.result.transfer is not None else ()), ) ) + @staticmethod + def _array_uses_extent_power(array: ArrayHandoffPlan | None) -> bool: + """Return whether one optional completed array shape contains power.""" + return bool(array) and any(declaration_extent_uses_power(expression) for expression in array.shape) + @staticmethod def _functions_use_native_array_handles(functions: tuple[FunctionPlan, ...]) -> bool: """Return whether persistent descriptor helpers couple the wrappers.""" @@ -344,7 +471,12 @@ def _functions_use_native_array_handles(functions: tuple[FunctionPlan, ...]) -> return any((arguments_use_handles, results_use_handles)) def binding_header(self, plan: ModulePlan) -> CHeader: - """Lower the binding header from one completed wrapper plan.""" + """Build the C header that declares wrappers from the validated plan. + + The header mirrors whether wrapper functions are externally linked for + sharding. It is paired with :meth:`binding_module` or + :meth:`binding_modules` output. + """ external_wrappers = bool(self._binding_function_shards(plan)) return CHeader( guard=f"{plan.binding.owner_path.upper()}_WRAPPER_H", @@ -503,11 +635,17 @@ def _module_uses_memory_copy(self, plan: ModulePlan) -> bool: return ( self._module_uses_string_values(plan) or self._module_uses_array_result_copy(plan) + or any( + variable.binding.getter_action is ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE + for namespace in plan.namespaces + for variable in namespace.variables + ) or self._module_uses_derived_string_copy(plan) or self._module_uses_non_direct_derived_calls(plan) ) def _module_uses_array_result_copy(self, plan: ModulePlan) -> bool: + """Return module uses array result copy from the supplied completed binding records; this helper preserves the selected binding behavior.""" return any( result.object_kind is ObjectKind.NUMPY_ARRAY for function in self._functions(plan) @@ -515,6 +653,7 @@ def _module_uses_array_result_copy(self, plan: ModulePlan) -> bool: ) def _module_uses_derived_string_copy(self, plan: ModulePlan) -> bool: + """Return module uses derived string copy from the supplied completed binding records; this helper preserves the selected binding behavior.""" return any( field.access is DerivedFieldAccessMechanism.FIXED_STRING_COPY for derived in self._derived_types(plan) @@ -522,6 +661,7 @@ def _module_uses_derived_string_copy(self, plan: ModulePlan) -> bool: ) def _module_uses_non_direct_derived_calls(self, plan: ModulePlan) -> bool: + """Return module uses non direct derived calls from the supplied completed binding records; this helper preserves the selected binding behavior.""" return any( any(case.actual_storage is not DerivedObjectStorage.DIRECT for case in argument.derived_call.cases) for function in self._functions(plan) @@ -536,6 +676,7 @@ def _module_uses_derived_calls(self, plan: ModulePlan) -> bool: ) def _module_uses_derived_alias_validation(self, plan: ModulePlan) -> bool: + """Return module uses derived alias validation from the supplied completed binding records; this helper preserves the selected binding behavior.""" return any( sum(argument.derived_call is not None for argument in function.arguments) >= 2 for function in self._functions(plan) @@ -683,6 +824,7 @@ def _callback_abort_if_null( name: str, message: str, ) -> CIf: + """Build callback abort if null from the supplied local lowering values; emitted nodes only project completed binding actions.""" return CIf( CodeExpression(f"{name} == NULL"), body=(CExpressionStatement(CodeExpression(f'{callback.abort_symbol}("{message}")')),), @@ -743,6 +885,7 @@ def _callback_array_nodes( position: int, target: str, ) -> tuple[CDeclaration | CExpressionStatement, ...]: + """Build callback array nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" scalar = PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name) rank = transfer.rank dimensions = f"callback_dims_{position}" @@ -787,6 +930,7 @@ def _callback_string_nodes( transfer: CallbackTransferPlan, target: str, ) -> tuple[CDeclaration, ...]: + """Build callback string nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" base = self._callback_parameter_base_name(transfer) if transfer.adapter_action is CallbackTransferAction.COPY_IN: expression = f"PyUnicode_FromStringAndSize((const char *){base}_data, (Py_ssize_t){base}_length)" @@ -803,6 +947,7 @@ def _callback_derived_nodes( position: int, target: str, ) -> tuple: + """Build callback derived nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" symbol = transfer.derived_backend_symbol if symbol is None: raise ValueError(f"Callback derived argument {transfer.owner_path!r} has no backend symbol") @@ -857,6 +1002,7 @@ def _callback_result_nodes( @staticmethod def _callback_void_result_nodes(callback: CallbackHandoffPlan, gil: str) -> tuple: + """Build callback void result nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" return ( CIf( CodeExpression("callback_result != Py_None"), @@ -878,6 +1024,7 @@ def _callback_scalar_result_nodes( transfer: CallbackTransferPlan, gil: str, ) -> tuple: + """Build callback scalar result nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" scalar = PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name) return ( CDeclaration( @@ -902,6 +1049,7 @@ def _callback_array_result_nodes( context: str, gil: str, ) -> tuple: + """Build callback array result nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" scalar = PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name) shape = transfer.array.shape if transfer.array is not None else () invalid = [ @@ -942,16 +1090,15 @@ def _callback_extent_value_expression( extent: str, ) -> str: """Spell one completed callback extent source in the flattened C ABI.""" - source = next((item for item in callback.arguments if item.name == extent), None) - if source is None: - return extent - base = self._callback_parameter_base_name(source) - if source.abi is CallbackABIKind.VALUE: - return base - if source.abi is CallbackABIKind.REFERENCE: - scalar = PrimitiveScalarTypeRegistry.type_for(source.semantic_type_name) - return f"*(({scalar.c_spelling} *){base}_data)" - raise ValueError(f"Callback extent {extent!r} in {callback.owner_path!r} is not a scalar value or reference") + substitutions = {} + for source in callback.arguments: + base = self._callback_parameter_base_name(source) + if source.abi is CallbackABIKind.VALUE: + substitutions[source.name] = base + elif source.abi is CallbackABIKind.REFERENCE: + scalar = PrimitiveScalarTypeRegistry.type_for(source.semantic_type_name) + substitutions[source.name] = f"*(({scalar.c_spelling} *){base}_data)" + return render_declaration_extent(extent, substitutions, target="c") def _callback_derived_result_nodes( self, @@ -960,6 +1107,7 @@ def _callback_derived_result_nodes( context: str, gil: str, ) -> tuple: + """Build callback derived result nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" symbol = transfer.derived_backend_symbol if symbol is None: raise ValueError(f"Callback derived result {transfer.owner_path!r} has no backend symbol") @@ -1017,6 +1165,7 @@ def _callback_c_parameters( self, transfers: tuple[CallbackTransferPlan, ...], ) -> tuple[CParameter, ...]: + """Build callback c parameters from the supplied local lowering values; emitted nodes only project completed binding actions.""" return tuple( parameter for transfer in transfers for parameter in self._callback_c_transfer_parameters(transfer) ) @@ -1025,6 +1174,7 @@ def _callback_c_transfer_parameters( self, transfer: CallbackTransferPlan, ) -> tuple[CParameter, ...]: + """Build callback c transfer parameters from the supplied local lowering values; emitted nodes only project completed binding actions.""" base = self._callback_parameter_base_name(transfer) if transfer.abi is CallbackABIKind.VALUE: scalar = PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name) @@ -1039,6 +1189,7 @@ def _callback_c_transfer_parameters( return (CParameter(f"{base}_data", "void *"),) def _callback_c_return_type(self, callback: CallbackHandoffPlan) -> str: + """Return the binding-local callback c return type derived from the supplied local lowering values; this helper preserves completed policy.""" transfer = callback.result.transfer if callback.result.action is CallbackResultAction.RETURN_VOID: return "void" @@ -1048,6 +1199,7 @@ def _callback_c_return_type(self, callback: CallbackHandoffPlan) -> str: @staticmethod def _callback_parameter_base_name(transfer: CallbackTransferPlan) -> str: + """Return the binding-local callback parameter base name derived from the supplied local lowering values; this helper preserves completed policy.""" return re.sub(r"\W", "_", transfer.name).casefold() # Shared scalar-derived runtime dispatch. @@ -1121,6 +1273,7 @@ def _derived_call_case_declaration(self, argument: ArgumentTransferPlan) -> CDec ) def _derived_call_case_initializer(self, argument: ArgumentTransferPlan, case) -> str: + """Return derived call case initializer from the supplied completed binding records; this helper preserves the selected binding behavior.""" uses_ops = case.actual_storage in { DerivedObjectStorage.MODULE_PROXY, DerivedObjectStorage.MODULE_ALLOCATABLE, @@ -1144,6 +1297,7 @@ def _derived_case_capsule_name( argument: ArgumentTransferPlan, storage: DerivedObjectStorage, ) -> str | None: + """Return the binding-local derived case capsule name derived from the supplied completed binding records; this helper preserves completed policy.""" if argument.derived is None: raise ValueError(f"Derived argument {argument.owner_path!r} has no handoff") if storage in {DerivedObjectStorage.DIRECT, DerivedObjectStorage.MODULE_TARGET}: @@ -1156,6 +1310,7 @@ def _derived_case_capsule_name( @staticmethod def _derived_call_case_table_name(argument: ArgumentTransferPlan) -> str: + """Return the binding-local derived call case table name derived from the supplied completed binding records; this helper preserves completed policy.""" symbol = re.sub(r"\W", "_", argument.owner_path).casefold() return f"prik_derived_cases_{symbol}" @@ -1219,6 +1374,7 @@ def _derived_alias_validator_function(self) -> CFunction: ) def _derived_argument_extractor_function(self) -> CFunction: + """Build derived argument extractor function from the supplied local lowering values; emitted nodes only project completed binding actions.""" return CFunction( "prik_extract_derived_argument", "int", @@ -1371,6 +1527,7 @@ def _derived_argument_origin_extraction_nodes(self) -> tuple[CIf, ...]: ) def _derived_argument_ops_extraction_nodes(self) -> tuple: + """Build derived argument ops extraction nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" return ( CDeclaration( "operation_map", @@ -1438,6 +1595,7 @@ def _derived_argument_ops_extraction_nodes(self) -> tuple: ) def _derived_argument_capsule_extraction_nodes(self) -> tuple: + """Build derived argument capsule extraction nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" return ( CDeclaration( "capsule_name", @@ -1483,6 +1641,7 @@ def _derived_argument_capsule_extraction_nodes(self) -> tuple: # Typed module-origin operation tables. def _derived_origin_variables(self, plan: ModulePlan) -> tuple[ModuleVariablePlan, ...]: + """Return derived origin variables from the supplied completed binding records; this helper preserves the selected binding behavior.""" return tuple(variable for variable in self._variables(plan) if variable.derived is not None) def _derived_origin_declarations(self, plan: ModulePlan) -> tuple: @@ -1531,6 +1690,7 @@ def _derived_origin_declarations(self, plan: ModulePlan) -> tuple: return tuple(declarations) def _derived_origin_bridge_prototypes(self, variable: ModuleVariablePlan) -> tuple[CFunctionPrototype, ...]: + """Return the binding-local derived origin bridge prototypes derived from the supplied completed binding records; this helper preserves completed policy.""" prototypes = [] if self._derived_origin_supports(variable, "present"): prototypes.append(CFunctionPrototype(self._derived_origin_bridge_name(variable, "present"), "bool")) @@ -1566,6 +1726,7 @@ def _derived_origin_bridge_prototypes(self, variable: ModuleVariablePlan) -> tup return tuple(prototypes) def _derived_origin_wrapper_prototypes(self, variable: ModuleVariablePlan) -> tuple[CFunctionPrototype, ...]: + """Return the binding-local derived origin wrapper prototypes derived from the supplied completed binding records; this helper preserves completed policy.""" return tuple( CFunctionPrototype( self._derived_origin_wrapper_name(variable, operation), @@ -1579,6 +1740,7 @@ def _derived_origin_wrapper_prototypes(self, variable: ModuleVariablePlan) -> tu @staticmethod def _derived_origin_operation_parameters(operation: str) -> tuple[CParameter, ...]: + """Build derived origin operation parameters from the supplied local lowering values; emitted nodes only project completed binding actions.""" if operation == "scoped": return ( CParameter("consumer", "prik_derived_consumer_fn"), @@ -1591,6 +1753,7 @@ def _derived_origin_operation_parameters(operation: str) -> tuple[CParameter, .. return () def _derived_origin_functions(self, plan: ModulePlan) -> tuple[CFunction, ...]: + """Build derived origin functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" return tuple( function for variable in self._derived_origin_variables(plan) @@ -1605,6 +1768,7 @@ def _derived_origin_functions(self, plan: ModulePlan) -> tuple[CFunction, ...]: ) def _derived_origin_operation_function(self, variable: ModuleVariablePlan, operation: str) -> CFunction: + """Build derived origin operation function from the supplied completed binding records; emitted nodes only project completed binding actions.""" builders = { "present": self._derived_origin_present_function, "address": self._derived_origin_address_function, @@ -1615,6 +1779,7 @@ def _derived_origin_operation_function(self, variable: ModuleVariablePlan, opera return builders[operation](variable) def _derived_origin_present_function(self, variable: ModuleVariablePlan) -> CFunction: + """Build derived origin present function from the supplied completed binding records; emitted nodes only project completed binding actions.""" return CFunction( self._derived_origin_wrapper_name(variable, "present"), "int", @@ -1623,6 +1788,7 @@ def _derived_origin_present_function(self, variable: ModuleVariablePlan) -> CFun ) def _derived_origin_address_function(self, variable: ModuleVariablePlan) -> CFunction: + """Build derived origin address function from the supplied completed binding records; emitted nodes only project completed binding actions.""" return CFunction( self._derived_origin_wrapper_name(variable, "address"), "void *", @@ -1631,6 +1797,7 @@ def _derived_origin_address_function(self, variable: ModuleVariablePlan) -> CFun ) def _derived_origin_scoped_function(self, variable: ModuleVariablePlan) -> CFunction: + """Build derived origin scoped function from the supplied completed binding records; emitted nodes only project completed binding actions.""" active = self._derived_origin_active_name(variable) poisoned = self._derived_origin_poisoned_name(variable) fault = "prik_derived_fault" @@ -1660,6 +1827,7 @@ def _derived_origin_scoped_function(self, variable: ModuleVariablePlan) -> CFunc ) def _derived_origin_checkout_function(self, variable: ModuleVariablePlan) -> CFunction: + """Build derived origin checkout function from the supplied completed binding records; emitted nodes only project completed binding actions.""" active = self._derived_origin_active_name(variable) poisoned = self._derived_origin_poisoned_name(variable) fault = "prik_derived_fault" @@ -1691,6 +1859,7 @@ def _derived_origin_checkout_function(self, variable: ModuleVariablePlan) -> CFu ) def _derived_origin_restore_function(self, variable: ModuleVariablePlan) -> CFunction: + """Build derived origin restore function from the supplied completed binding records; emitted nodes only project completed binding actions.""" active = self._derived_origin_active_name(variable) poisoned = self._derived_origin_poisoned_name(variable) fault = "prik_derived_fault" @@ -1733,6 +1902,7 @@ def _derived_origin_fault_return( phase: str, name: str, ) -> CIf: + """Return derived origin fault return from the supplied completed binding records; this helper preserves the selected binding behavior.""" selector = self._c_string_literal(f"{operation}:{phase}:{variable.symbol_name}") return CIf( CodeExpression(f"{name} != NULL && strcmp({name}, {selector}) == 0"), @@ -1746,6 +1916,7 @@ def _derived_origin_fault_status( phase: str, name: str, ) -> CIf: + """Return the binding-local derived origin fault status derived from the supplied completed binding records; this helper preserves completed policy.""" selector = self._c_string_literal(f"{operation}:{phase}:{variable.symbol_name}") return CIf( CodeExpression(f"status == 0 && {name} != NULL && strcmp({name}, {selector}) == 0"), @@ -1753,6 +1924,7 @@ def _derived_origin_fault_status( ) def _derived_origin_capsule_method(self, variable: ModuleVariablePlan) -> CFunction: + """Return derived origin capsule method from the supplied completed binding records; this helper preserves the selected binding behavior.""" return CFunction( self._derived_origin_capsule_method_name(variable), "PyObject *", @@ -1770,6 +1942,7 @@ def _derived_origin_capsule_method(self, variable: ModuleVariablePlan) -> CFunct @staticmethod def _derived_origin_supports(variable: ModuleVariablePlan, operation: str) -> bool: + """Return derived origin supports from the supplied completed binding records; this helper preserves the selected binding behavior.""" storage = variable.derived.handoff.storage support = { DerivedObjectStorage.MODULE_PROXY: {"scoped"}, @@ -1786,28 +1959,36 @@ def _derived_origin_supports(variable: ModuleVariablePlan, operation: str) -> bo return operation in support.get(storage, set()) def _derived_origin_needs_guard(self, variable: ModuleVariablePlan) -> bool: + """Return derived origin needs guard from the supplied completed binding records; this helper preserves the selected binding behavior.""" return any(self._derived_origin_supports(variable, operation) for operation in ("scoped", "checkout")) @staticmethod def _derived_origin_symbol(variable: ModuleVariablePlan) -> str: + """Return the binding-local derived origin symbol derived from the supplied completed binding records; this helper preserves completed policy.""" return NativeSymbolNames.compact(variable.owner_path, variable.symbol_name) def _derived_origin_bridge_name(self, variable: ModuleVariablePlan, operation: str) -> str: + """Return the binding-local derived origin bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_prik_origin_{self._derived_origin_symbol(variable)}_{operation}" def _derived_origin_wrapper_name(self, variable: ModuleVariablePlan, operation: str) -> str: + """Return the binding-local derived origin wrapper name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_origin_{self._derived_origin_symbol(variable)}_{operation}" def _derived_origin_table_name(self, variable: ModuleVariablePlan) -> str: + """Return the binding-local derived origin table name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_origin_{self._derived_origin_symbol(variable)}_ops" def _derived_origin_active_name(self, variable: ModuleVariablePlan) -> str: + """Return the binding-local derived origin active name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_origin_{self._derived_origin_symbol(variable)}_active" def _derived_origin_poisoned_name(self, variable: ModuleVariablePlan) -> str: + """Return the binding-local derived origin poisoned name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_origin_{self._derived_origin_symbol(variable)}_poisoned" def _derived_origin_capsule_method_name(self, variable: ModuleVariablePlan) -> str: + """Return the binding-local derived origin capsule method name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"_prik_origin_{self._derived_origin_symbol(variable)}_native_ops" def _module_declarations( @@ -1903,6 +2084,7 @@ def _constructible_class_identities(plan: ModulePlan) -> set[tuple[str, str]]: } def _owned_derived_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Return owned derived result identities from the supplied completed binding records; this helper preserves the selected binding behavior.""" return { result.derived.type_identity for function in self._functions(plan) @@ -1914,6 +2096,7 @@ def _owned_derived_result_identities(self, plan: ModulePlan) -> set[tuple[str, s } def _owned_derived_module_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Return owned derived module identities from the supplied completed binding records; this helper preserves the selected binding behavior.""" return { variable.derived.handoff.type_identity for variable in self._variables(plan) @@ -1927,6 +2110,7 @@ def _allocatable_holder_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) def _allocatable_holder_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Build allocatable holder result identities from the supplied completed binding records; emitted nodes only project completed binding actions.""" return { result.derived.type_identity for function in self._functions(plan) @@ -1935,6 +2119,7 @@ def _allocatable_holder_result_identities(self, plan: ModulePlan) -> set[tuple[s } def _allocatable_holder_argument_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Build allocatable holder argument identities from the supplied completed binding records; emitted nodes only project completed binding actions.""" return { argument.derived.type_identity for function in self._functions(plan) @@ -1950,11 +2135,13 @@ def _allocatable_holder_argument_identities(self, plan: ModulePlan) -> set[tuple } def _pointer_holder_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: + """Return the binding-local pointer holder types derived from the supplied completed binding records; this helper preserves completed policy.""" identities = self._pointer_holder_result_identities(plan) identities.update(self._pointer_holder_argument_identities(plan)) return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) def _pointer_holder_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Return pointer holder result identities from the supplied completed binding records; this helper preserves the selected binding behavior.""" return { result.derived.type_identity for function in self._functions(plan) @@ -1963,6 +2150,7 @@ def _pointer_holder_result_identities(self, plan: ModulePlan) -> set[tuple[str, } def _pointer_holder_argument_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Return pointer holder argument identities from the supplied completed binding records; this helper preserves the selected binding behavior.""" return { argument.derived.type_identity for function in self._functions(plan) @@ -1979,6 +2167,7 @@ def _pointer_holder_argument_identities(self, plan: ModulePlan) -> set[tuple[str @staticmethod def _uses_allocatable_holder(argument: ArgumentTransferPlan) -> bool: + """Return whether allocatable holder is required by the supplied completed binding records; this helper does not choose policy.""" call = argument.derived_call return bool( call is not None @@ -2006,6 +2195,7 @@ def _allocatable_holder_destroy_bridge_prototype(self, derived: DerivedTypePlan) ) def _pointer_holder_destroy_bridge_prototype(self, derived: DerivedTypePlan) -> CFunctionPrototype: + """Return the binding-local pointer holder destroy bridge prototype derived from the supplied local lowering values; this helper preserves completed policy.""" return CFunctionPrototype( self._pointer_holder_destroy_bridge_name(derived.backend_symbol), "void", @@ -2120,6 +2310,7 @@ def _class_constructor_function( ) def _pointer_holder_capsule_destructor(self, derived: DerivedTypePlan) -> CFunction: + """Return pointer holder capsule destructor from the supplied local lowering values; this helper preserves the selected binding behavior.""" type_symbol = derived.backend_symbol return CFunction( self._pointer_holder_capsule_destructor_name(type_symbol), @@ -2213,6 +2404,7 @@ def _derived_field_bridge_prototypes(self, plan: ModulePlan) -> tuple[CFunctionP ) def _direct_field_bridge_prototype_entries(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: + """Return the binding-local direct field bridge prototype entries derived from the supplied completed binding records; this helper preserves completed policy.""" return tuple( prototype for derived in self._derived_types(plan) @@ -2221,6 +2413,7 @@ def _direct_field_bridge_prototype_entries(self, plan: ModulePlan) -> tuple[CFun ) def _module_member_bridge_prototype_entries(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: + """Return the binding-local module member bridge prototype entries derived from the supplied completed binding records; this helper preserves completed policy.""" return tuple( prototype for variable in self._derived_member_proxy_variables(plan) @@ -2232,6 +2425,7 @@ def _allocatable_holder_field_bridge_prototype_entries( self, plan: ModulePlan, ) -> tuple[CFunctionPrototype, ...]: + """Return the binding-local allocatable holder field bridge prototype entries derived from the supplied completed binding records; this helper preserves completed policy.""" return tuple( prototype for derived in self._allocatable_holder_types(plan) @@ -2240,6 +2434,7 @@ def _allocatable_holder_field_bridge_prototype_entries( ) def _pointer_holder_field_bridge_prototype_entries(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: + """Return the binding-local pointer holder field bridge prototype entries derived from the supplied completed binding records; this helper preserves completed policy.""" return tuple( prototype for derived in self._pointer_holder_types(plan) @@ -2278,6 +2473,7 @@ def _pointer_holder_field_bridge_prototypes( derived: DerivedTypePlan, field: DerivedFieldPlan, ) -> tuple[CFunctionPrototype, ...]: + """Return the binding-local pointer holder field bridge prototypes derived from the supplied completed binding records; this helper preserves completed policy.""" if field.access is not DerivedFieldAccessMechanism.SCALAR_VALUE: raise ValueError(f"Unsupported pointer-holder field for {field.owner_path!r}: {field.access.value}") value_type = self._derived_field_c_type(field) @@ -2491,6 +2687,7 @@ def _derived_handle_shape_prototype( owner: tuple[CParameter, ...], rank: int, ) -> CFunctionPrototype: + """Return the binding-local derived handle shape prototype derived from the supplied local lowering values; this helper preserves completed policy.""" parameters = (*owner, *(CParameter(f"extent_{axis}", "int64_t *") for axis in range(rank))) return CFunctionPrototype(name, "void", parameters) @@ -2499,6 +2696,7 @@ def _derived_handle_descriptor_prototype( name: str, owner: tuple[CParameter, ...], ) -> CFunctionPrototype: + """Return the binding-local derived handle descriptor prototype derived from the supplied local lowering values; this helper preserves completed policy.""" parameters = ( *owner, CParameter("callback", "void", function_parameters=("CFI_cdesc_t *", "void *")), @@ -2512,6 +2710,7 @@ def _derived_handle_extent_prototype( owner: tuple[CParameter, ...], rank: int, ) -> CFunctionPrototype: + """Return the binding-local derived handle extent prototype derived from the supplied local lowering values; this helper preserves completed policy.""" parameters = (*owner, *(CParameter(f"extent_{axis}", "int64_t") for axis in range(rank))) return CFunctionPrototype(name, "void", parameters) @@ -2561,6 +2760,7 @@ def _derived_field_functions(self, plan: ModulePlan) -> tuple[CFunction, ...]: ) def _direct_field_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: + """Build direct field functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" return tuple( function for derived in self._derived_types(plan) @@ -2569,6 +2769,7 @@ def _direct_field_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ) def _module_member_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: + """Build module member functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" return tuple( function for variable in self._derived_member_proxy_variables(plan) @@ -2577,6 +2778,7 @@ def _module_member_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction ) def _allocatable_holder_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: + """Build allocatable holder functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" derived_types = self._allocatable_holder_types(plan) fields = tuple( function @@ -2588,6 +2790,7 @@ def _allocatable_holder_functions_for_plan(self, plan: ModulePlan) -> tuple[CFun return (*presence, *fields) def _pointer_holder_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: + """Build pointer holder functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" derived_types = self._pointer_holder_types(plan) fields = tuple( function @@ -2599,6 +2802,7 @@ def _pointer_holder_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunctio return (*presence, *fields) def _module_proxy_guard_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: + """Build module proxy guard functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" variables = self._derived_member_proxy_variables(plan) return tuple( self._module_derived_presence_method(variable) @@ -2700,6 +2904,7 @@ def _allocatable_holder_owner_nodes(self, type_name: str, *, setter: bool) -> tu ) def _pointer_holder_presence_method(self, derived: DerivedTypePlan) -> CFunction: + """Return pointer holder presence method from the supplied local lowering values; this helper preserves the selected binding behavior.""" return self._derived_private_method( self._pointer_holder_presence_method_name(derived.backend_symbol), ( @@ -2726,6 +2931,7 @@ def _pointer_holder_field_functions( derived: DerivedTypePlan, field: DerivedFieldPlan, ) -> tuple[CFunction, ...]: + """Build pointer holder field functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" if field.access is not DerivedFieldAccessMechanism.SCALAR_VALUE: raise ValueError(f"Unsupported pointer-holder field for {field.owner_path!r}: {field.access.value}") scalar = PrimitiveScalarTypeRegistry.type_for(field.semantic_type_name) @@ -2760,6 +2966,7 @@ def _pointer_holder_field_functions( return getter, setter def _pointer_holder_owner_nodes(self, type_name: str, *, setter: bool) -> tuple: + """Build pointer holder owner nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" declarations: tuple = (CDeclaration("owner_obj", "PyObject *"),) if setter: declarations = (*declarations, CDeclaration("value_obj", "PyObject *")) @@ -2845,15 +3052,18 @@ def _module_member_functions( raise ValueError(f"Unsupported module member lowering for {member.field.owner_path!r}") from exc def _direct_string_field_functions(self, derived, field) -> tuple[CFunction, ...]: + """Build direct string field functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" return self._optional_field_functions( self._direct_string_field_getter(derived, field), self._direct_string_field_setter(derived, field), ) def _direct_handle_field_functions(self, derived, field) -> tuple[CFunction, ...]: + """Build direct handle field functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" return (self._direct_native_handle_field_getter(derived, field),) def _direct_array_field_functions(self, derived, field) -> tuple[CFunction, ...]: + """Build direct array field functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" callback = self._ordinary_array_field_descriptor_callback( field, self._derived_field_descriptor_callback_name(derived, field), @@ -2865,27 +3075,32 @@ def _direct_array_field_functions(self, derived, field) -> tuple[CFunction, ...] ) def _direct_scalar_field_functions(self, derived, field) -> tuple[CFunction, ...]: + """Build direct scalar field functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" return self._optional_field_functions( self._direct_scalar_field_getter(derived, field), self._direct_scalar_field_setter(derived, field), ) def _direct_nested_field_functions(self, derived, field) -> tuple[CFunction, ...]: + """Build direct nested field functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" return self._optional_field_functions( self._direct_nested_field_getter(derived, field), self._direct_nested_field_setter(derived, field), ) def _module_string_member_functions(self, variable, member) -> tuple[CFunction, ...]: + """Build module string member functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" return self._optional_field_functions( self._module_string_member_getter(variable, member), self._module_string_member_setter(variable, member), ) def _module_handle_member_functions(self, variable, member) -> tuple[CFunction, ...]: + """Build module handle member functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" return (self._module_native_handle_member_getter(variable, member),) def _module_array_member_functions(self, variable, member) -> tuple[CFunction, ...]: + """Build module array member functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" callback = self._ordinary_array_field_descriptor_callback( member.field, self._module_member_descriptor_callback_name(variable, member), @@ -2897,12 +3112,14 @@ def _module_array_member_functions(self, variable, member) -> tuple[CFunction, . ) def _module_scalar_member_functions(self, variable, member) -> tuple[CFunction, ...]: + """Build module scalar member functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" return self._optional_field_functions( self._module_scalar_member_getter(variable, member), self._module_scalar_member_setter(variable, member), ) def _module_nested_member_functions(self, variable, member) -> tuple[CFunction, ...]: + """Build module nested member functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" return self._optional_field_functions( self._module_nested_member_getter(variable, member), self._module_nested_member_setter(variable, member), @@ -2910,10 +3127,12 @@ def _module_nested_member_functions(self, variable, member) -> tuple[CFunction, @staticmethod def _optional_field_functions(getter: CFunction, setter: CFunction | None) -> tuple[CFunction, ...]: + """Build optional field functions from the supplied local lowering values; emitted nodes only project completed binding actions.""" return (getter, *CBindingGenerator._present_field_function(setter)) @staticmethod def _present_field_function(function: CFunction | None) -> tuple[CFunction, ...]: + """Build present field function from the supplied completed binding records; emitted nodes only project completed binding actions.""" return () if function is None else (function,) def _direct_ordinary_array_field_getter( @@ -3078,6 +3297,7 @@ def _module_string_member_setter( @staticmethod def _fixed_string_field_length(field: DerivedFieldPlan) -> int: + """Return the binding-local fixed string field length derived from the supplied completed binding records; this helper preserves completed policy.""" length = field.character_length if length is None or length <= 0: raise ValueError(f"Fixed string field {field.owner_path!r} has no positive length") @@ -3214,6 +3434,7 @@ def _field_handle_operation_name( field: DerivedFieldPlan, operation: NativeArrayOperation, ) -> str: + """Return the binding-local field handle operation name derived from the supplied completed binding records; this helper preserves completed policy.""" if isinstance(owner, DerivedTypePlan): return self._derived_handle_operation_name(owner, field, operation) variable, member = owner @@ -3327,7 +3548,8 @@ def _ordinary_array_field_input_nodes( f"!PyArray_IS_F_CONTIGUOUS((PyArrayObject *){object_name})", ] conditions.extend( - f"PyArray_DIM((PyArrayObject *){object_name}, {axis}) != (npy_intp)({extent})" + f"PyArray_DIM((PyArrayObject *){object_name}, {axis}) != " + f"(npy_intp)({render_declaration_extent(extent, {}, target='c')})" for axis, extent in enumerate(array.shape) ) return ( @@ -3347,6 +3569,7 @@ def _ordinary_array_field_input_nodes( ) def _direct_scalar_field_getter(self, derived: DerivedTypePlan, field: DerivedFieldPlan) -> CFunction: + """Return direct scalar field getter from the supplied completed binding records; this helper preserves the selected binding behavior.""" scalar = PrimitiveScalarTypeRegistry.type_for(field.semantic_type_name) body = ( *self._derived_owner_address_nodes(derived), @@ -3364,6 +3587,7 @@ def _direct_scalar_field_setter( derived: DerivedTypePlan, field: DerivedFieldPlan, ) -> CFunction | None: + """Return direct scalar field setter from the supplied completed binding records; this helper preserves the selected binding behavior.""" if field.setter_action is not SetterAction.WRITE_THROUGH: return None scalar = PrimitiveScalarTypeRegistry.type_for(field.semantic_type_name) @@ -3383,6 +3607,7 @@ def _module_scalar_member_getter( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> CFunction: + """Return module scalar member getter from the supplied completed binding records; this helper preserves the selected binding behavior.""" scalar = PrimitiveScalarTypeRegistry.type_for(member.field.semantic_type_name) body = ( CDeclaration("owner_obj", "PyObject *"), @@ -3401,6 +3626,7 @@ def _module_scalar_member_setter( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> CFunction | None: + """Return module scalar member setter from the supplied completed binding records; this helper preserves the selected binding behavior.""" field = member.field if field.setter_action is not SetterAction.WRITE_THROUGH: return None @@ -3419,6 +3645,7 @@ def _module_scalar_member_setter( return self._derived_private_method(self._module_member_method_name(variable, member, "set"), body) def _direct_nested_field_getter(self, derived: DerivedTypePlan, field: DerivedFieldPlan) -> CFunction: + """Return direct nested field getter from the supplied completed binding records; this helper preserves the selected binding behavior.""" if field.derived is None: raise ValueError(f"Nested field {field.owner_path!r} has no derived handoff") child_type = field.derived.type_name @@ -3454,6 +3681,7 @@ def _direct_nested_field_setter( derived: DerivedTypePlan, field: DerivedFieldPlan, ) -> CFunction | None: + """Return direct nested field setter from the supplied completed binding records; this helper preserves the selected binding behavior.""" if field.setter_action is not SetterAction.WRITE_THROUGH or field.derived is None: return None body = ( @@ -3474,6 +3702,7 @@ def _module_nested_member_getter( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> CFunction: + """Return module nested member getter from the supplied completed binding records; this helper preserves the selected binding behavior.""" field = member.field if field.derived is None: raise ValueError(f"Nested module member {field.owner_path!r} has no derived handoff") @@ -3495,6 +3724,7 @@ def _module_nested_member_setter( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> CFunction | None: + """Return module nested member setter from the supplied completed binding records; this helper preserves the selected binding behavior.""" field = member.field if field.setter_action is not SetterAction.WRITE_THROUGH or field.derived is None: return None @@ -3677,6 +3907,7 @@ def _scalar_field_unpack_statement( ) def _derived_field_c_type(self, field: DerivedFieldPlan) -> str: + """Return the binding-local derived field c type derived from the supplied completed binding records; this helper preserves completed policy.""" if field.object_kind is ObjectKind.DERIVED_TYPE: return "void *" return PrimitiveScalarTypeRegistry.type_for(field.semantic_type_name).c_spelling @@ -3908,6 +4139,7 @@ def _field_handle_owner_nodes(self, owner) -> tuple: @staticmethod def _field_handle_owner_arguments(owner) -> str: + """Return field handle owner arguments from the supplied local lowering values; this helper preserves the selected binding behavior.""" return "owner_address" if isinstance(owner, DerivedTypePlan) else "" def _field_handle_bridge_name( @@ -3916,24 +4148,28 @@ def _field_handle_bridge_name( field: DerivedFieldPlan, operation: NativeArrayOperation, ) -> str: + """Return the binding-local field handle bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" if isinstance(owner, DerivedTypePlan): return self._derived_handle_bridge_name(owner, field, operation) variable, member = owner return self._module_member_handle_bridge_name(variable, member, operation) def _field_handle_descriptor_callback(self, owner, field: DerivedFieldPlan) -> str: + """Build field handle descriptor callback from the supplied completed binding records; emitted nodes only project completed binding actions.""" if isinstance(owner, DerivedTypePlan): return self._derived_handle_descriptor_callback_name(owner, field) variable, member = owner return self._module_member_handle_descriptor_callback_name(variable, member) def _field_handle_actual_callback(self, owner, field: DerivedFieldPlan) -> str: + """Build field handle actual callback from the supplied completed binding records; emitted nodes only project completed binding actions.""" if isinstance(owner, DerivedTypePlan): return self._derived_handle_actual_callback_name(owner, field) variable, member = owner return self._module_member_handle_actual_callback_name(variable, member) def _field_handle_shape_nodes(self, field: DerivedFieldPlan, bridge: str, owner_args: str) -> tuple: + """Build field handle shape nodes from the supplied completed binding records; emitted nodes only project completed binding actions.""" handle = field.native_array_handle if handle is None or handle.array.rank is None: raise ValueError(f"Native handle field {field.owner_path!r} has no shape rank") @@ -3959,6 +4195,7 @@ def _field_handle_shape_nodes(self, field: DerivedFieldPlan, bridge: str, owner_ @staticmethod def _field_handle_descriptor_nodes(bridge: str, owner_args: str, callback: str) -> tuple: + """Build field handle descriptor nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" arguments = ", ".join((*((owner_args,) if owner_args else ()), callback, "&descriptor_record")) return ( CDeclaration("descriptor_record", "PyObject *", CodeExpression("NULL")), @@ -3968,6 +4205,7 @@ def _field_handle_descriptor_nodes(bridge: str, owner_args: str, callback: str) @staticmethod def _field_handle_actual_nodes(bridge: str, owner_args: str, callback: str) -> tuple: + """Build field handle actual nodes from the supplied local lowering values; emitted nodes only project completed binding actions.""" arguments = ", ".join((*((owner_args,) if owner_args else ()), callback, "&base_addr")) return ( CDeclaration("base_addr", "void *", CodeExpression("NULL")), @@ -3976,6 +4214,7 @@ def _field_handle_actual_nodes(bridge: str, owner_args: str, callback: str) -> t ) def _field_handle_shape_mutation_nodes(self, field: DerivedFieldPlan, bridge: str, owner_args: str) -> tuple: + """Build field handle shape mutation nodes from the supplied completed binding records; emitted nodes only project completed binding actions.""" handle = field.native_array_handle if handle is None or handle.array.rank is None: raise ValueError(f"Native handle field {field.owner_path!r} has no mutation rank") @@ -4449,10 +4688,12 @@ def _module_allocatable_descriptor_callbacks( return descriptor_callback, array_actual_callback def _module_descriptor_callback_name(self, variable: ModuleVariablePlan) -> str: + """Return the binding-local module descriptor callback name derived from the supplied completed binding records; this helper preserves completed policy.""" owner = re.sub(r"\W", "_", variable.owner_path).casefold() return f"prik_module_{owner}_descriptor_callback" def _module_array_actual_callback_name(self, variable: ModuleVariablePlan) -> str: + """Return the binding-local module array actual callback name derived from the supplied completed binding records; this helper preserves completed policy.""" owner = re.sub(r"\W", "_", variable.owner_path).casefold() return f"prik_module_{owner}_array_actual_callback" @@ -4624,6 +4865,7 @@ def _module_native_array_operation_name( variable: ModuleVariablePlan, operation: NativeArrayOperation, ) -> str: + """Return the binding-local module native array operation name derived from the supplied completed binding records; this helper preserves completed policy.""" owner = re.sub(r"\W", "_", variable.owner_path).casefold() return f"prik_module_{owner}_{operation.value}" @@ -4632,6 +4874,7 @@ def _module_native_array_operation_def_name( variable: ModuleVariablePlan, operation: NativeArrayOperation, ) -> str: + """Return the binding-local module native array operation def name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"{self._module_native_array_operation_name(variable, operation)}_def" def _module_native_array_bridge_operation_name( @@ -4639,13 +4882,16 @@ def _module_native_array_bridge_operation_name( variable: ModuleVariablePlan, operation: NativeArrayOperation, ) -> str: + """Return the binding-local module native array bridge operation name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_{variable.symbol_name}_{operation.value}" def _module_native_array_cache_name(self, variable: ModuleVariablePlan) -> str: + """Return the binding-local module native array cache name derived from the supplied completed binding records; this helper preserves completed policy.""" owner = re.sub(r"\W", "_", variable.owner_path).casefold() return f"prik_module_{owner}_handle" def _module_native_array_owner_name(self, variable: ModuleVariablePlan) -> str: + """Return the binding-local module native array owner name derived from the supplied completed binding records; this helper preserves completed policy.""" owner = re.sub(r"\W", "_", variable.owner_path).casefold() return f"prik_module_{owner}_owner" @@ -5441,6 +5687,7 @@ def _default_native_array_binder_name(self, argument: ArgumentTransferPlan) -> s return f"prik_bind_default_{owner}" def _default_native_array_binder_def_name(self, argument: ArgumentTransferPlan) -> str: + """Return the binding-local default native array binder def name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"{self._default_native_array_binder_name(argument)}_def" @staticmethod @@ -5464,6 +5711,8 @@ def _lower_module_getter(self, plan: ModuleVariablePlan) -> tuple[CFunction, ... return self._lower_module_getter_constant_value(plan) case ModuleGetterAction.NATIVE_CONSTANT_VALUE: return self._lower_module_getter_native_constant_value(plan) + case ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE: + return self._lower_module_getter_constant_value(plan) case ModuleGetterAction.DIRECT_VALUE: return self._lower_module_getter_direct_value(plan) case ModuleGetterAction.NULLABLE_SNAPSHOT: @@ -5925,10 +6174,16 @@ def _module_setter_unpack_statement(self, plan, scalar_type) -> CExpressionState ) def _visit_FunctionPlan(self, plan: FunctionPlan) -> CFunction: - """Recursively assemble one complete CPython binding function.""" + """Build one CPython wrapper through context, conversion, and call stages. + + The returned function preserves the completed conversion order and + lifecycle actions; this orchestration does not select those policies. + """ + # Stage 1: allocate stable local names and separate declarations from executable nodes. context = self._function_context(plan) argument_declarations, argument_body = self._declarations_first(self._function_argument_nodes(plan, context)) alias_declarations, alias_body = self._declarations_first(self._derived_alias_preflight_nodes(plan, context)) + # Stage 2: assemble bridge call, completed output projection, and cleanup. output_nodes = self._output_nodes(plan, context) return CFunction( name=self._binding_function_name(plan), @@ -5940,6 +6195,7 @@ def _visit_FunctionPlan(self, plan: FunctionPlan) -> CFunction: *argument_declarations, *alias_declarations, *self._callback_context_declarations(plan), + *self._declaration_extent_result_declarations(plan), *self._direct_result_declaration(plan, context), *self._native_output_declarations(plan, context), self._parse_statement(plan, context), @@ -5951,12 +6207,14 @@ def _visit_FunctionPlan(self, plan: FunctionPlan) -> CFunction: ) def _function_argument_nodes(self, plan: FunctionPlan, context: _CFunctionContext) -> tuple: + """Build function argument nodes from the supplied completed binding records; emitted nodes only project completed binding actions.""" return tuple( node for argument in self._binding_conversion_order(plan) for node in self.visit(argument, context=context) ) @staticmethod def _declarations_first(nodes: tuple) -> tuple[tuple, tuple]: + """Build declarations first from the supplied local lowering values; emitted nodes only project completed binding actions.""" declarations = tuple(node for node in nodes if isinstance(node, CDeclaration)) body = tuple(node for node in nodes if not isinstance(node, CDeclaration)) return declarations, body @@ -6139,6 +6397,7 @@ def _callback_context_pop_nodes( @staticmethod def _callback_context_name(argument: ArgumentTransferPlan) -> str: + """Return the binding-local callback context name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"{argument.binding.python_name.casefold()}_callback_context" def _lower_planned_derived_call_argument( @@ -6364,14 +6623,17 @@ def _polymorphic_argument_nodes( @staticmethod def _polymorphic_type_name_name(names: _CArgumentNames) -> str: + """Return the binding-local polymorphic type name name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"{names.polymorphic_name}_type_name" @staticmethod def _polymorphic_type_symbol_name(names: _CArgumentNames) -> str: + """Return the binding-local polymorphic type symbol name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"{names.polymorphic_name}_type_symbol" @staticmethod def _polymorphic_capsule_name_name(names: _CArgumentNames) -> str: + """Return the binding-local polymorphic capsule name name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"{names.polymorphic_name}_capsule_name" def _derived_none_argument_nodes(self, plan: ArgumentTransferPlan, access_name: str) -> tuple: @@ -6398,18 +6660,22 @@ def _derived_none_argument_nodes(self, plan: ArgumentTransferPlan, access_name: @staticmethod def _derived_access_name(names: _CArgumentNames) -> str: + """Return the binding-local derived access name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"{names.value_name}_derived_access" @staticmethod def _derived_ops_name(names: _CArgumentNames) -> str: + """Return the binding-local derived ops name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"{names.value_name}_derived_ops" @staticmethod def _derived_identity_name(names: _CArgumentNames) -> str: + """Return the binding-local derived identity name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"{names.value_name}_derived_identity" @staticmethod def _derived_status_name(names: _CArgumentNames) -> str: + """Return the binding-local derived status name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"{names.value_name}_derived_status" # Scalar argument lowering. @@ -6723,7 +6989,11 @@ def _native_array_actual_shape_nodes( prefix = names.value_name nodes = [] for axis, expression in enumerate(actual.shape): - if expression in {":", "::Strided", "Flat"} or (actual.flatten_storage and axis == actual.flat_axis): + if ( + expression in {":", "::Strided", "Flat"} + or (actual.flatten_storage and axis == actual.flat_axis) + or array.extent_evaluation[axis] == "bridge" + ): nodes.append(CExpressionStatement(CodeExpression("Py_INCREF(Py_None)"))) item = "Py_None" else: @@ -6839,6 +7109,8 @@ def _array_shape_checks( for axis, expression in enumerate(handoff.shape): if expression in runtime_markers: continue + if handoff.extent_evaluation[axis] == "bridge": + continue expected = self._array_extent_expression(handoff, axis, expression, context) actual_axis = self._array_actual_axis_expression(handoff, array, axis) checks.append( @@ -6871,15 +7143,19 @@ def _array_extent_expression( context: _CFunctionContext, ) -> str: """Lower one validated extent expression through its planned role references.""" - lowered = expression - for role in handoff.extent_reference_roles[axis]: + substitutions = {} + references = zip( + handoff.extent_reference_tokens[axis], + handoff.extent_reference_roles[axis], + strict=True, + ) + for token, role in references: try: value_name = context.role_values[role] except KeyError: raise ValueError(f"Array extent role {role!r} has no binding value") from None - reference_name = role.rsplit(".", 1)[-1].split(":", 1)[0] - lowered = re.sub(rf"\b{re.escape(reference_name)}\b", value_name, lowered) - return lowered + substitutions[token] = value_name + return render_declaration_extent(expression, substitutions, target="c") def _array_extraction_nodes( self, @@ -8016,6 +8292,12 @@ def _lower_result_owned_native_array_handle( CDeclaration(f"{prefix}_owner", "PyObject *", CodeExpression("NULL")), CDeclaration(f"{prefix}_operation", "PyObject *", CodeExpression("NULL")), CDeclaration(python_name, "PyObject *", CodeExpression("NULL")), + *self._owned_pointer_result_normalization_nodes( + plan, + descriptor_name, + cleanup, + failure_cleanup, + ), CExpressionStatement(CodeExpression(f"{prefix}_ops = PyDict_New()")), CIf( CodeExpression(f"{prefix}_ops == NULL"), @@ -8097,6 +8379,58 @@ def _lower_result_owned_native_array_handle( ) return tuple(nodes) + def _owned_pointer_result_normalization_nodes( + self, + plan: ResultPlan, + descriptor_name: str, + cleanup: tuple[CExpressionStatement, ...], + failure_cleanup: tuple[str, ...], + ) -> tuple[CIf, ...]: + """Re-establish empty numeric pointer storage before publishing it. + + Some Fortran runtimes clear descriptor metadata when assigning an + unassociated pointer result. The binding consumes the completed + descriptor kind, rank, and dtype to restore a valid empty CFI record; + associated results and runtime-width string descriptors are unchanged. + """ + handle = plan.native_array_handle + if ( + handle is None + or handle.descriptor_kind is not NativeArrayDescriptorKind.POINTER + or plan.datatype_family is DatatypeFamily.STRING + ): + return () + cfi_type = self._native_array_cfi_type(plan) + status_name = f"{descriptor_name}_owner_status" + return ( + CIf( + CodeExpression(f"{descriptor_name}->base_addr == NULL"), + body=( + CExpressionStatement( + CodeExpression( + f"{status_name} = CFI_establish({descriptor_name}, NULL, CFI_attribute_pointer, " + f"{cfi_type}, {self._native_array_expected_element_size(plan)}, " + f"{handle.array.rank}, NULL)" + ) + ), + CIf( + CodeExpression(f"{status_name} != CFI_SUCCESS"), + body=( + *cleanup, + *self._decref_names(failure_cleanup), + CExpressionStatement( + CodeExpression( + 'PyErr_SetString(PyExc_RuntimeError, "failed to normalize unassociated pointer ' + 'result descriptor")' + ) + ), + CReturn(CodeExpression("NULL")), + ), + ), + ), + ), + ) + def _owned_native_array_ops_dictionary_nodes( self, result: ResultPlan, @@ -8156,7 +8490,7 @@ def _lower_result_array_copy( if handoff is None or handoff.rank is None or python_name is None: raise ValueError(f"Array result {plan.owner_path!r} has no fixed binding shape") dimensions = tuple( - self._array_extent_expression(handoff, axis, expression, context) + self._result_extent_expression(plan, handoff, axis, expression, context) for axis, expression in enumerate(handoff.shape) ) dimension_declarations: tuple[CDeclaration, ...] @@ -8212,6 +8546,19 @@ def _lower_result_array_copy( ), ) + def _result_extent_expression( + self, + result: ResultPlan, + handoff: ArrayHandoffPlan, + axis: int, + expression: str, + context: _CFunctionContext, + ) -> str: + """Use the bridge result for native axes and local roles for all others.""" + if handoff.extent_evaluation[axis] == "bridge": + return self._declaration_extent_result_name(result, axis) + return self._array_extent_expression(handoff, axis, expression, context) + def _array_result_creation_expression( self, plan: ResultPlan, @@ -8566,6 +8913,7 @@ def _lower_result_direct_value( context: _CFunctionContext, failure_cleanup: tuple[str, ...], ) -> tuple[CExpressionStatement | CDeclaration | CIf, ...]: + """Lower result direct value from the supplied completed binding records without inferring semantic policy.""" return self._lower_result_value(plan, context, failure_cleanup) def _lower_result_value( @@ -8778,6 +9126,7 @@ def _one_derived_call_error_nodes( argument: ArgumentTransferPlan, context: _CFunctionContext, ) -> tuple[CIf, ...]: + """Build one derived call error nodes from the supplied completed binding records; emitted nodes only project completed binding actions.""" status = self._derived_status_name(context.arguments[argument.owner_path]) name = self._c_string_literal(argument.binding.python_name) return ( @@ -8974,6 +9323,7 @@ def _derived_native_storage_cleanup_nodes( ) def _derived_result_destroy_bridge_name(self, result: ResultPlan) -> str: + """Return the binding-local derived result destroy bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" if result.derived.storage is DerivedObjectStorage.ALLOCATABLE_HOLDER: return self._allocatable_holder_destroy_bridge_name(result.derived.backend_symbol) if result.derived.storage is DerivedObjectStorage.POINTER_HOLDER: @@ -9282,6 +9632,7 @@ def _descriptor_output_present_name(names: _CArgumentNames) -> str: @staticmethod def _holder_allocation_status_name(names: _CArgumentNames) -> str: + """Return the binding-local holder allocation status name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"{names.value_name}_holder_allocation_status" def _argument_for_role(self, plan: FunctionPlan, role: str) -> ArgumentTransferPlan: @@ -9292,6 +9643,7 @@ def _argument_for_role(self, plan: FunctionPlan, role: str) -> ArgumentTransferP raise ValueError(f"{plan.owner_path!r} has no argument for lifecycle role {role!r}") def _function_context(self, plan: FunctionPlan) -> _CFunctionContext: + """Build function context from the supplied completed binding records; emitted nodes only project completed binding actions.""" arguments = self._argument_contexts(plan) native_outputs = self._native_output_names(plan) output_owners = self._output_owners(plan) @@ -9365,13 +9717,22 @@ def _argument_role_values( arguments: dict[str, _CArgumentNames], ) -> dict[str, str]: """Map completed handoff roles to their binding value locals.""" - return { + values = { argument.binding.handoff_role: self._argument_role_value( argument, arguments[argument.owner_path].value_name, ) for argument in plan.arguments } + values.update( + { + role: arguments[argument.owner_path].extent_names[axis] + for argument in plan.arguments + if argument.array is not None + for axis, role in enumerate(argument.array.extent_roles) + } + ) + return values @staticmethod def _argument_role_value(argument: ArgumentTransferPlan, value_name: str) -> str: @@ -9406,6 +9767,7 @@ def _argument_context_names(self, argument: ArgumentTransferPlan) -> _CArgumentN ) def _keyword_declaration(self, plan: FunctionPlan) -> CDeclaration: + """Build keyword declaration from the supplied completed binding records; emitted nodes only project completed binding actions.""" keywords = ", ".join( f'"{argument.binding.python_name}"' for argument in sorted(plan.arguments, key=lambda item: item.python_position) @@ -9414,6 +9776,7 @@ def _keyword_declaration(self, plan: FunctionPlan) -> CDeclaration: return CDeclaration("kwlist[]", "static char *", CodeExpression(f"{{{entries}}}")) def _parse_statement(self, plan: FunctionPlan, context: _CFunctionContext) -> CExpressionStatement: + """Return parse statement from the supplied completed binding records; this helper preserves the selected binding behavior.""" arguments = sorted(plan.arguments, key=lambda item: item.python_position) required_modes = {OptionalMode.REQUIRED, OptionalMode.REQUIRED_DESCRIPTOR} required = [item for item in arguments if item.binding.optional_mode in required_modes] @@ -9430,6 +9793,7 @@ def _direct_result_declaration( plan: FunctionPlan, context: _CFunctionContext, ) -> tuple[CDeclaration, ...]: + """Build direct result declaration from the supplied completed binding records; emitted nodes only project completed binding actions.""" result = self._direct_result(plan) if result is None or context.result_name is None: return () @@ -9471,6 +9835,20 @@ def _direct_result_declaration( scalar_type = PrimitiveScalarTypeRegistry.type_for(result.semantic_type_name) return (CDeclaration(context.result_name, scalar_type.c_spelling),) + def _declaration_extent_result_declarations(self, plan: FunctionPlan) -> tuple[CDeclaration, ...]: + """Declare storage populated by native-dependent main-bridge extent outputs.""" + return tuple( + CDeclaration( + self._declaration_extent_result_name(result, axis), + "int64_t", + CodeExpression("0"), + ) + for result in plan.results + if result.array is not None + for axis, evaluation in enumerate(result.array.extent_evaluation) + if evaluation == "bridge" + ) + def _native_output_declarations( self, plan: FunctionPlan, @@ -9627,10 +10005,15 @@ def _native_array_capsule_new_expression( """Create one versioned capsule around established descriptor storage.""" handle = plan.native_array_handle cfi_type = self._native_array_cfi_type(plan) + element_size = ( + f"{descriptor}->elem_len" + if plan.datatype_family is DatatypeFamily.STRING + else self._native_array_expected_element_size(plan) + ) return ( "prik_native_array_handle_capsule_new(" f"{self._native_array_handle_kind_constant(handle)}, {handle.array.rank}, {cfi_type}, " - f"{descriptor}->elem_len, sizeof(CFI_CDESC_T({handle.array.rank})), {descriptor}, " + f"{element_size}, sizeof(CFI_CDESC_T({handle.array.rank})), {descriptor}, " f"{self._native_array_capsule_release_name(plan)})" ) @@ -9809,13 +10192,25 @@ def _owned_result_descriptor_name(self, result: ResultPlan, context: _CFunctionC return native_name def _bridge_call(self, plan: FunctionPlan, context: _CFunctionContext) -> str: + """Return bridge call from the supplied completed binding records; this helper preserves the selected binding behavior.""" arguments = [ *self._bridge_visible_argument_values(plan, context), *self._bridge_hidden_result_values(plan, context), *self._bridge_direct_result_values(plan, context), + *self._declaration_extent_result_values(plan), ] return f"{self._bridge_function_name(plan)}({', '.join(arguments)})" + def _declaration_extent_result_values(self, plan: FunctionPlan) -> tuple[str, ...]: + """Pass native-dependent result extent output storage to the main bridge.""" + return tuple( + f"&{self._declaration_extent_result_name(result, axis)}" + for result in plan.results + if result.array is not None + for axis, evaluation in enumerate(result.array.extent_evaluation) + if evaluation == "bridge" + ) + def _bridge_visible_argument_values( self, plan: FunctionPlan, @@ -9960,6 +10355,7 @@ def _selected_array_axis_names(self, names: tuple[str, ...], roles: tuple[str, . return names if roles else () def _bridge_prototype(self, plan: FunctionPlan) -> CFunctionPrototype: + """Return the binding-local bridge prototype derived from the supplied completed binding records; this helper preserves completed policy.""" argument_parameters = tuple( parameter for argument in sorted(plan.arguments, key=lambda item: item.bridge.abi_position) @@ -9972,12 +10368,28 @@ def _bridge_prototype(self, plan: FunctionPlan) -> CFunctionPrototype: ) direct_result = self._direct_result(plan) direct_parameters = self._direct_bridge_result_parameters(direct_result) + declaration_extent_parameters = self._declaration_extent_result_parameters(plan) return CFunctionPrototype( self._bridge_function_name(plan), self._bridge_return_type(plan), - (*argument_parameters, *result_parameters, *direct_parameters), + (*argument_parameters, *result_parameters, *direct_parameters, *declaration_extent_parameters), ) + def _declaration_extent_result_parameters(self, plan: FunctionPlan) -> tuple[CParameter, ...]: + """Declare native-dependent result extent outputs in the C prototype.""" + return tuple( + CParameter(self._declaration_extent_result_name(result, axis), "int64_t *") + for result in plan.results + if result.array is not None + for axis, evaluation in enumerate(result.array.extent_evaluation) + if evaluation == "bridge" + ) + + @staticmethod + def _declaration_extent_result_name(result: ResultPlan, axis: int) -> str: + """Return the shared main-bridge ABI name for one evaluated result axis.""" + return f"prik_decl_extent_{result.result_position}_{axis}" + def _owned_native_array_bridge_prototypes(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: """Declare typed Fortran operations over binding-owned result descriptors.""" return tuple( @@ -10227,6 +10639,7 @@ def _module_variable_bridge_prototypes( handler = { ModuleGetterAction.NATIVE_ARRAY_HANDLE: self._module_native_array_bridge_prototypes, ModuleGetterAction.BORROWED_ARRAY_VIEW: self._module_borrowed_array_bridge_prototypes, + ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE: self._module_borrowed_array_bridge_prototypes, ModuleGetterAction.DERIVED_OBJECT: self._module_derived_bridge_prototypes, }.get(plan.binding.getter_action) if handler is not None: @@ -10403,6 +10816,7 @@ def _module_variable_helper_prototypes( if plan.binding.getter_action in { ModuleGetterAction.CONSTANT_VALUE, ModuleGetterAction.NATIVE_CONSTANT_VALUE, + ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE, }: return () prototypes = [CFunctionPrototype(self._module_getter_name(plan), "PyObject *", storage="static")] @@ -10436,7 +10850,11 @@ def _module_property_support( ) for variable in namespace.variables if variable.binding.getter_action - not in {ModuleGetterAction.CONSTANT_VALUE, ModuleGetterAction.NATIVE_CONSTANT_VALUE} + not in { + ModuleGetterAction.CONSTANT_VALUE, + ModuleGetterAction.NATIVE_CONSTANT_VALUE, + ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE, + } for python_name in variable.binding.python_names ) if not entries: @@ -10449,6 +10867,7 @@ def _module_property_support( ) def _binding_prototype(self, plan: FunctionPlan, *, external: bool = False) -> CFunctionPrototype: + """Return the binding-local binding prototype derived from the supplied completed binding records; this helper preserves completed policy.""" return CFunctionPrototype( self._binding_function_name(plan), "PyObject *", @@ -10473,52 +10892,65 @@ def _derived_destroy_bridge_name(type_name: str) -> str: @staticmethod def _allocatable_holder_capsule_name(type_name: str) -> str: + """Return the binding-local allocatable holder capsule name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"prik.derived.{type_name}.allocatable_holder" @staticmethod def _pointer_holder_capsule_name(type_name: str) -> str: + """Return the binding-local pointer holder capsule name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"prik.derived.{type_name}.pointer_holder" @staticmethod def _pointer_holder_capsule_destructor_name(type_name: str) -> str: + """Return the binding-local pointer holder capsule destructor name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"prik_destroy_{type_name.casefold()}_pointer_holder_capsule" @staticmethod def _pointer_holder_destroy_bridge_name(type_name: str) -> str: + """Return the binding-local pointer holder destroy bridge name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"bind_c_prik_destroy_{type_name.casefold()}_pointer_holder" @staticmethod def _pointer_holder_presence_bridge_name(type_name: str) -> str: + """Return the binding-local pointer holder presence bridge name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"bind_c_prik_{type_name.casefold()}_pointer_holder_present" @staticmethod def _allocatable_holder_capsule_destructor_name(type_name: str) -> str: + """Return the binding-local allocatable holder capsule destructor name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"prik_destroy_{type_name.casefold()}_allocatable_holder_capsule" @staticmethod def _allocatable_holder_destroy_bridge_name(type_name: str) -> str: + """Return the binding-local allocatable holder destroy bridge name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"bind_c_prik_destroy_{type_name.casefold()}_allocatable_holder" @staticmethod def _allocatable_holder_presence_bridge_name(type_name: str) -> str: + """Return the binding-local allocatable holder presence bridge name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"bind_c_prik_{type_name.casefold()}_allocatable_holder_present" @staticmethod def _allocatable_holder_presence_method_name(type_name: str) -> str: + """Return the binding-local allocatable holder presence method name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"_prik_{type_name.casefold()}_allocatable_holder_require_present" @staticmethod def _pointer_holder_presence_method_name(type_name: str) -> str: + """Return the binding-local pointer holder presence method name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"_prik_{type_name.casefold()}_pointer_holder_require_present" @staticmethod def _derived_field_symbol(derived: DerivedTypePlan, field: DerivedFieldPlan) -> str: + """Return the binding-local derived field symbol derived from the supplied completed binding records; this helper preserves completed policy.""" return f"{derived.backend_symbol}_{field.name}".casefold() def _derived_field_method_name(self, derived: DerivedTypePlan, field: DerivedFieldPlan, action: str) -> str: + """Return the binding-local derived field method name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"_prik_field_{self._derived_field_symbol(derived, field)}_{action}" def _derived_field_bridge_name(self, derived: DerivedTypePlan, field: DerivedFieldPlan, action: str) -> str: + """Return the binding-local derived field bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_prik_field_{self._derived_field_symbol(derived, field)}_{action}" def _allocatable_holder_field_bridge_name( @@ -10527,6 +10959,7 @@ def _allocatable_holder_field_bridge_name( field: DerivedFieldPlan, action: str, ) -> str: + """Return the binding-local allocatable holder field bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_prik_allocatable_holder_field_{self._derived_field_symbol(derived, field)}_{action}" def _allocatable_holder_field_method_name( @@ -10535,6 +10968,7 @@ def _allocatable_holder_field_method_name( field: DerivedFieldPlan, action: str, ) -> str: + """Return the binding-local allocatable holder field method name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"_prik_allocatable_holder_field_{self._derived_field_symbol(derived, field)}_{action}" def _pointer_holder_field_bridge_name( @@ -10543,6 +10977,7 @@ def _pointer_holder_field_bridge_name( field: DerivedFieldPlan, action: str, ) -> str: + """Return the binding-local pointer holder field bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_prik_pointer_holder_field_{self._derived_field_symbol(derived, field)}_{action}" def _pointer_holder_field_method_name( @@ -10551,14 +10986,17 @@ def _pointer_holder_field_method_name( field: DerivedFieldPlan, action: str, ) -> str: + """Return the binding-local pointer holder field method name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"_prik_pointer_holder_field_{self._derived_field_symbol(derived, field)}_{action}" @staticmethod def _allocatable_holder_ops_name(type_name: str) -> str: + """Return the binding-local allocatable holder ops name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"_prik_ops_{type_name.casefold()}_allocatable_holder" @staticmethod def _pointer_holder_ops_name(type_name: str) -> str: + """Return the binding-local pointer holder ops name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"_prik_ops_{type_name.casefold()}_pointer_holder" def _derived_field_descriptor_callback_name( @@ -10566,6 +11004,7 @@ def _derived_field_descriptor_callback_name( derived: DerivedTypePlan, field: DerivedFieldPlan, ) -> str: + """Return the binding-local derived field descriptor callback name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_field_{self._derived_field_symbol(derived, field)}_descriptor" def _derived_handle_operation_name( @@ -10574,6 +11013,7 @@ def _derived_handle_operation_name( field: DerivedFieldPlan, operation: NativeArrayOperation, ) -> str: + """Return the binding-local derived handle operation name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_field_handle_{self._derived_field_symbol(derived, field)}_{operation.value}" def _derived_handle_bridge_name( @@ -10582,6 +11022,7 @@ def _derived_handle_bridge_name( field: DerivedFieldPlan, operation: NativeArrayOperation, ) -> str: + """Return the binding-local derived handle bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_prik_field_handle_{self._derived_field_symbol(derived, field)}_{operation.value}" def _derived_handle_descriptor_callback_name( @@ -10589,6 +11030,7 @@ def _derived_handle_descriptor_callback_name( derived: DerivedTypePlan, field: DerivedFieldPlan, ) -> str: + """Return the binding-local derived handle descriptor callback name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_field_handle_{self._derived_field_symbol(derived, field)}_descriptor_callback" def _derived_handle_actual_callback_name( @@ -10596,10 +11038,12 @@ def _derived_handle_actual_callback_name( derived: DerivedTypePlan, field: DerivedFieldPlan, ) -> str: + """Return the binding-local derived handle actual callback name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_field_handle_{self._derived_field_symbol(derived, field)}_actual_callback" @staticmethod def _module_member_symbol(variable: ModuleVariablePlan, member: DerivedMemberPathPlan) -> str: + """Return the binding-local module member symbol derived from the supplied completed binding records; this helper preserves completed policy.""" return "_".join((variable.symbol_name, *member.path)).casefold() def _module_member_method_name( @@ -10608,6 +11052,7 @@ def _module_member_method_name( member: DerivedMemberPathPlan, action: str, ) -> str: + """Return the binding-local module member method name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"_prik_module_field_{self._module_member_symbol(variable, member)}_{action}" def _module_member_bridge_name( @@ -10616,6 +11061,7 @@ def _module_member_bridge_name( member: DerivedMemberPathPlan, action: str, ) -> str: + """Return the binding-local module member bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_prik_module_field_{self._module_member_symbol(variable, member)}_{action}" def _module_member_descriptor_callback_name( @@ -10623,6 +11069,7 @@ def _module_member_descriptor_callback_name( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> str: + """Return the binding-local module member descriptor callback name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_module_field_{self._module_member_symbol(variable, member)}_descriptor" def _module_member_handle_operation_name( @@ -10631,6 +11078,7 @@ def _module_member_handle_operation_name( member: DerivedMemberPathPlan, operation: NativeArrayOperation, ) -> str: + """Return the binding-local module member handle operation name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_module_field_handle_{self._module_member_symbol(variable, member)}_{operation.value}" def _module_member_handle_bridge_name( @@ -10639,6 +11087,7 @@ def _module_member_handle_bridge_name( member: DerivedMemberPathPlan, operation: NativeArrayOperation, ) -> str: + """Return the binding-local module member handle bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_prik_module_field_handle_{self._module_member_symbol(variable, member)}_{operation.value}" def _module_member_handle_descriptor_callback_name( @@ -10646,6 +11095,7 @@ def _module_member_handle_descriptor_callback_name( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> str: + """Return the binding-local module member handle descriptor callback name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_module_field_handle_{self._module_member_symbol(variable, member)}_descriptor_callback" def _module_member_handle_actual_callback_name( @@ -10653,10 +11103,12 @@ def _module_member_handle_actual_callback_name( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> str: + """Return the binding-local module member handle actual callback name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"prik_module_field_handle_{self._module_member_symbol(variable, member)}_actual_callback" @staticmethod def _module_member_ops_name(variable: ModuleVariablePlan, prefix: tuple[str, ...]) -> str: + """Return the binding-local module member ops name derived from the supplied completed binding records; this helper preserves completed policy.""" suffix = "_".join((variable.symbol_name, *prefix)).casefold() return f"_prik_ops_{suffix}" @@ -10685,10 +11137,12 @@ def _derived_module_owner_declarations(self, plan: ModulePlan) -> tuple[CDeclara @staticmethod def _derived_module_owner_name(variable: ModuleVariablePlan) -> str: + """Return the binding-local derived module owner name derived from the supplied completed binding records; this helper preserves completed policy.""" owner = re.sub(r"\W", "_", variable.owner_path).casefold() return f"prik_module_{owner}_derived_owner" def _method_table(self, module: ModulePlan, namespace: NamespacePlan) -> CMethodDefTable: + """Build method table from the supplied completed binding records; emitted nodes only project completed binding actions.""" return CMethodDefTable( f"{module.binding.owner_path}_{self._namespace_symbol(namespace)}_methods", ( @@ -10727,6 +11181,7 @@ def _derived_private_method_entries(self, namespace: NamespacePlan) -> tuple[CMe return tuple(CMethodDefEntry(name, name, "METH_VARARGS", "") for name in names) def _direct_field_method_names(self, namespace: NamespacePlan) -> tuple[str, ...]: + """Return the binding-local direct field method names derived from the supplied completed binding records; this helper preserves completed policy.""" return tuple( self._derived_field_method_name(derived, field, action) for derived in namespace.derived_types @@ -10735,6 +11190,7 @@ def _direct_field_method_names(self, namespace: NamespacePlan) -> tuple[str, ... ) def _module_member_method_names(self, namespace: NamespacePlan) -> tuple[str, ...]: + """Return the binding-local module member method names derived from the supplied completed binding records; this helper preserves completed policy.""" return tuple( self._module_member_method_name(variable, member, action) for variable in namespace.variables @@ -10744,12 +11200,14 @@ def _module_member_method_names(self, namespace: NamespacePlan) -> tuple[str, .. ) def _namespace_allocatable_holder_identities(self, namespace: NamespacePlan) -> frozenset[tuple[str, str]]: + """Return the binding-local namespace allocatable holder identities derived from the supplied completed binding records; this helper preserves completed policy.""" identities = self._namespace_allocatable_holder_result_identities(namespace) identities.update(self._namespace_allocatable_holder_argument_identities(namespace)) return frozenset(identities) @staticmethod def _namespace_allocatable_holder_result_identities(namespace: NamespacePlan) -> set[tuple[str, str]]: + """Return the binding-local namespace allocatable holder result identities derived from the supplied completed binding records; this helper preserves completed policy.""" return { result.derived.type_identity for function in namespace.functions @@ -10761,6 +11219,7 @@ def _namespace_allocatable_holder_argument_identities( self, namespace: NamespacePlan, ) -> set[tuple[str, str]]: + """Return the binding-local namespace allocatable holder argument identities derived from the supplied completed binding records; this helper preserves completed policy.""" return { argument.derived.type_identity for function in namespace.functions @@ -10772,6 +11231,7 @@ def _namespace_allocatable_holder_argument_identities( } def _allocatable_holder_method_names(self, namespace: NamespacePlan) -> tuple[str, ...]: + """Return the binding-local allocatable holder method names derived from the supplied completed binding records; this helper preserves completed policy.""" holder_identities = self._namespace_allocatable_holder_identities(namespace) fields = tuple( self._allocatable_holder_field_method_name(derived, field, action) @@ -10788,12 +11248,14 @@ def _allocatable_holder_method_names(self, namespace: NamespacePlan) -> tuple[st return (*fields, *guards) def _namespace_pointer_holder_identities(self, namespace: NamespacePlan) -> frozenset[tuple[str, str]]: + """Return the binding-local namespace pointer holder identities derived from the supplied completed binding records; this helper preserves completed policy.""" identities = self._namespace_pointer_holder_result_identities(namespace) identities.update(self._namespace_pointer_holder_argument_identities(namespace)) return frozenset(identities) @staticmethod def _namespace_pointer_holder_result_identities(namespace: NamespacePlan) -> set[tuple[str, str]]: + """Return the binding-local namespace pointer holder result identities derived from the supplied completed binding records; this helper preserves completed policy.""" return { result.derived.type_identity for function in namespace.functions @@ -10803,6 +11265,7 @@ def _namespace_pointer_holder_result_identities(namespace: NamespacePlan) -> set @staticmethod def _namespace_pointer_holder_argument_identities(namespace: NamespacePlan) -> set[tuple[str, str]]: + """Return the binding-local namespace pointer holder argument identities derived from the supplied completed binding records; this helper preserves completed policy.""" return { argument.derived.type_identity for function in namespace.functions @@ -10818,6 +11281,7 @@ def _namespace_pointer_holder_argument_identities(namespace: NamespacePlan) -> s } def _pointer_holder_method_names(self, namespace: NamespacePlan) -> tuple[str, ...]: + """Return the binding-local pointer holder method names derived from the supplied completed binding records; this helper preserves completed policy.""" holder_identities = self._namespace_pointer_holder_identities(namespace) fields = tuple( self._pointer_holder_field_method_name(derived, field, action) @@ -10834,6 +11298,7 @@ def _pointer_holder_method_names(self, namespace: NamespacePlan) -> tuple[str, . return (*fields, *guards) def _module_proxy_guard_method_names(self, namespace: NamespacePlan) -> tuple[str, ...]: + """Return the binding-local module proxy guard method names derived from the supplied completed binding records; this helper preserves completed policy.""" presence = tuple( self._module_derived_presence_method_name(variable) for variable in namespace.variables @@ -10848,9 +11313,11 @@ def _module_proxy_guard_method_names(self, namespace: NamespacePlan) -> tuple[st @staticmethod def _field_method_actions(field: DerivedFieldPlan) -> tuple[str, ...]: + """Return field method actions from the supplied completed binding records; this helper preserves the selected binding behavior.""" return ("get", *(("set",) if field.setter_action is SetterAction.WRITE_THROUGH else ())) def _module_def(self, module: ModulePlan, namespace: NamespacePlan) -> CModuleDef: + """Return module def from the supplied completed binding records; this helper preserves the selected binding behavior.""" owner = module.binding.owner_path symbol = self._namespace_symbol(namespace) python_name = self._namespace_module_name(module, namespace) @@ -10862,6 +11329,7 @@ def _module_def(self, module: ModulePlan, namespace: NamespacePlan) -> CModuleDe ) def _module_init(self, plan: ModulePlan, needs_native_support: bool) -> CFunction: + """Return module init from the supplied completed binding records; this helper preserves the selected binding behavior.""" module_name = plan.binding.owner_path root_namespace = self._namespace(plan, ()) child_namespaces = self._ordered_child_namespaces(plan) @@ -11393,6 +11861,7 @@ def _callable_public_arguments(function: FunctionPlan) -> tuple[ArgumentTransfer @staticmethod def _python_parameter_suffix(arguments: tuple[ArgumentTransferPlan, ...]) -> str: + """Return the binding-local python parameter suffix derived from the supplied local lowering values; this helper preserves completed policy.""" if not arguments: return "" rendered = ", ".join( @@ -11412,6 +11881,7 @@ def _optional_keyword_collection_lines( *, indent: str, ) -> tuple[str, ...]: + """Build optional keyword collection lines from the supplied local lowering values; emitted nodes only project completed binding actions.""" lines = [] for argument in arguments: name = argument.binding.python_name @@ -11478,8 +11948,9 @@ def _numpy_array_overload_predicate( @staticmethod def _numpy_scalar_type_name(semantic_type_name: str) -> str: """Map a completed semantic scalar to its NumPy runtime spelling.""" + if is_boolean_semantic_type_name(semantic_type_name): + return "bool_" numpy_types = { - "Bool": "bool_", "Int8": "int8", "Int16": "int16", "Int32": "int32", @@ -11504,16 +11975,19 @@ def _class_base_name( surface: ClassSurfacePlan | None, class_names: dict[tuple[str, str], str], ) -> str | None: + """Return the binding-local class base name derived from the supplied local lowering values; this helper preserves completed policy.""" if surface is None or not surface.base_identities: return None return class_names[surface.base_identities[0]] @staticmethod def _class_create_bridge_name(surface: ClassSurfacePlan) -> str: + """Return the binding-local class create bridge name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"bind_c_prik_create_{surface.type_identity[1].casefold()}" @staticmethod def _class_create_method_name(surface: ClassSurfacePlan) -> str: + """Return the binding-local class create method name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"_prik_create_{surface.type_identity[1].casefold()}" @staticmethod @@ -11522,6 +11996,7 @@ def _class_wrap_helper_name( *, fallback: str | None = None, ) -> str: + """Return the binding-local class wrap helper name derived from the supplied local lowering values; this helper preserves completed policy.""" name = surface.python_names[0] if surface is not None else fallback if name is None: raise ValueError("Class wrapper helper requires a Python type name") @@ -11529,6 +12004,7 @@ def _class_wrap_helper_name( @staticmethod def _derived_property_python_lines(field: DerivedFieldPlan) -> tuple[str, ...]: + """Build derived property python lines from the supplied completed binding records; emitted nodes only project completed binding actions.""" lines = [ " @property", f" def {field.name}(self):", @@ -11560,6 +12036,7 @@ def _derived_property_python_lines(field: DerivedFieldPlan) -> tuple[str, ...]: return tuple(lines) def _direct_type_ops_literal(self, derived: DerivedTypePlan) -> str: + """Return the binding-local direct type ops literal derived from the supplied local lowering values; this helper preserves completed policy.""" entries = [] for field in derived.fields: entries.append(f"'{field.name}_get': {self._derived_field_method_name(derived, field, 'get')}") @@ -11568,6 +12045,7 @@ def _direct_type_ops_literal(self, derived: DerivedTypePlan) -> str: return "{" + ", ".join(entries) + "}" def _allocatable_holder_ops_python_source(self, derived: DerivedTypePlan) -> str: + """Build allocatable holder ops python source from the supplied local lowering values; emitted nodes only project completed binding actions.""" entries = [f"'_present': {self._allocatable_holder_presence_method_name(derived.backend_symbol)}"] for field in derived.fields: entries.append(f"'{field.name}_get': {self._allocatable_holder_field_method_name(derived, field, 'get')}") @@ -11578,6 +12056,7 @@ def _allocatable_holder_ops_python_source(self, derived: DerivedTypePlan) -> str return f"{self._allocatable_holder_ops_name(derived.backend_symbol)} = {{{', '.join(entries)}}}" def _pointer_holder_ops_python_source(self, derived: DerivedTypePlan) -> str: + """Return pointer holder ops python source from the supplied local lowering values; this helper preserves the selected binding behavior.""" entries = [f"'_present': {self._pointer_holder_presence_method_name(derived.backend_symbol)}"] for field in derived.fields: entries.append(f"'{field.name}_get': {self._pointer_holder_field_method_name(derived, field, 'get')}") @@ -11587,6 +12066,7 @@ def _pointer_holder_ops_python_source(self, derived: DerivedTypePlan) -> str: @staticmethod def _direct_type_ops_name(derived: DerivedTypePlan) -> str: + """Return the binding-local direct type ops name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"_prik_ops_{derived.type_name.casefold()}" def _module_proxy_ops_python_source(self, variable: ModuleVariablePlan) -> str: @@ -11612,6 +12092,7 @@ def _module_proxy_ops_literal( prefix: tuple[str, ...], members: list[DerivedMemberPathPlan], ) -> str: + """Return module proxy ops literal from the supplied completed binding records; this helper preserves the selected binding behavior.""" entries = [] if not prefix: entries.append(f"'_native_ops': {self._derived_origin_capsule_method_name(variable)}()") @@ -11696,6 +12177,7 @@ def _module_constant_nodes( if variable.binding.getter_action not in { ModuleGetterAction.CONSTANT_VALUE, ModuleGetterAction.NATIVE_CONSTANT_VALUE, + ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE, }: continue for python_name in variable.binding.python_names: @@ -11723,8 +12205,10 @@ def _module_constant_declarations( variable: ModuleVariablePlan, value_name: str, object_name: str, - ) -> tuple[CDeclaration, ...]: + ) -> tuple[CDeclaration | CExpressionStatement, ...]: """Materialize one planned binding or native constant value.""" + if variable.binding.getter_action is ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE: + return self._module_constant_array_declarations(variable, value_name, object_name) if variable.datatype_family is DatatypeFamily.STRING: literal = self._c_string_literal(str(variable.binding.constant_value)) return ( @@ -11749,6 +12233,55 @@ def _module_constant_declarations( ), ) + def _module_constant_array_declarations( + self, + variable: ModuleVariablePlan, + value_name: str, + object_name: str, + ) -> tuple[CDeclaration | CExpressionStatement]: + """Materialize one compiler-evaluated parameter array as a read-only NumPy snapshot.""" + array = variable.array + if array is None or array.rank is None or array.rank <= 0: + raise ValueError(f"Module parameter array {variable.owner_path!r} has no fixed array plan") + scalar_type = PrimitiveScalarTypeRegistry.type_for(variable.semantic_type_name) + extent_names = tuple(f"{value_name}_extent_{axis}" for axis in range(array.rank)) + dimensions = f"{value_name}_dimensions" + return ( + *(CDeclaration(extent, "int64_t", CodeExpression("0")) for extent in extent_names), + CDeclaration( + value_name, + "void *", + CodeExpression( + f"{self._module_bridge_getter_name(variable)}({', '.join(f'&{extent}' for extent in extent_names)})" + ), + ), + CExpressionStatement( + CodeExpression(f"if ({value_name} == NULL) {{ PyErr_NoMemory(); Py_DECREF(mod); return NULL; }}") + ), + CDeclaration( + f"{dimensions}[{array.rank}]", + "npy_intp", + CodeExpression("{" + ", ".join(f"(npy_intp){extent}" for extent in extent_names) + "}"), + ), + CDeclaration( + object_name, + "PyObject *", + CodeExpression( + f"(PyObject *)PyArray_EMPTY({array.rank}, {dimensions}, {scalar_type.numpy_type_macro}, 1)" + ), + ), + CExpressionStatement(CodeExpression(f"if ({object_name} == NULL) {{ Py_DECREF(mod); return NULL; }}")), + CExpressionStatement( + CodeExpression( + f"memcpy(PyArray_DATA((PyArrayObject *){object_name}), {value_name}, " + f"PyArray_NBYTES((PyArrayObject *){object_name}))" + ) + ), + CExpressionStatement( + CodeExpression(f"PyArray_CLEARFLAGS((PyArrayObject *){object_name}, NPY_ARRAY_WRITEABLE)") + ), + ) + def _module_literal(self, plan: ModuleVariablePlan, value: object) -> str: """Dispatch one completed datatype family to its C literal spelling.""" family = plan.datatype_family @@ -11765,19 +12298,24 @@ def _module_literal(self, plan: ModuleVariablePlan, value: object) -> str: # Scalar module-literal lowering. def _lower_module_literal_bool(self, value: object) -> str: + """Lower module literal bool from the supplied local lowering values without inferring semantic policy.""" return "true" if value else "false" def _lower_module_literal_integer(self, value: object) -> str: + """Lower module literal integer from the supplied local lowering values without inferring semantic policy.""" return str(value) def _lower_module_literal_real(self, value: object) -> str: + """Lower module literal real from the supplied local lowering values without inferring semantic policy.""" return repr(value) def _lower_module_literal_complex(self, value: object) -> str: + """Lower module literal complex from the supplied local lowering values without inferring semantic policy.""" number = complex(value) return f"({number.real!r} + {number.imag!r} * I)" def _binding_parameters(self) -> tuple[CParameter, ...]: + """Build binding parameters from the supplied local lowering values; emitted nodes only project completed binding actions.""" return ( CParameter("self", "PyObject *"), CParameter("args", "PyObject *"), @@ -11785,21 +12323,27 @@ def _binding_parameters(self) -> tuple[CParameter, ...]: ) def _binding_function_name(self, plan: FunctionPlan) -> str: + """Return the binding-local binding function name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"wrap_{plan.symbol_name}" def _bridge_function_name(self, plan: FunctionPlan) -> str: + """Return the binding-local bridge function name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_{plan.symbol_name}" def _module_getter_name(self, plan: ModuleVariablePlan) -> str: + """Return the binding-local module getter name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"module_get_{plan.symbol_name}" def _module_setter_name(self, plan: ModuleVariablePlan) -> str: + """Return the binding-local module setter name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"module_set_{plan.symbol_name}" def _module_bridge_getter_name(self, plan: ModuleVariablePlan) -> str: + """Return the binding-local module bridge getter name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_get_{plan.symbol_name}" def _module_bridge_setter_name(self, plan: ModuleVariablePlan) -> str: + """Return the binding-local module bridge setter name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_set_{plan.symbol_name}" @staticmethod @@ -11818,29 +12362,79 @@ def _nullable_derived_module_proxy(plan: ModuleVariablePlan) -> bool: @staticmethod def _module_derived_presence_bridge_name(plan: ModuleVariablePlan) -> str: + """Return the binding-local module derived presence bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"bind_c_prik_module_{plan.symbol_name.casefold()}_present" @staticmethod def _module_derived_presence_method_name(plan: ModuleVariablePlan) -> str: + """Return the binding-local module derived presence method name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"_prik_module_{plan.symbol_name.casefold()}_require_present" def _functions(self, plan: ModulePlan) -> tuple[FunctionPlan, ...]: + """Build functions from the supplied completed binding records; emitted nodes only project completed binding actions.""" return tuple(function for namespace in plan.namespaces for function in namespace.functions) def _variables(self, plan: ModulePlan) -> tuple[ModuleVariablePlan, ...]: + """Return variables from the supplied completed binding records; this helper preserves the selected binding behavior.""" return tuple(variable for namespace in plan.namespaces for variable in namespace.variables) def _namespace(self, plan: ModulePlan, python_path: tuple[str, ...]) -> NamespacePlan: + """Return the binding-local namespace derived from the supplied completed binding records; this helper preserves completed policy.""" for namespace in plan.namespaces: if namespace.python_path == python_path: return namespace raise ValueError(f"{plan.owner_path!r} has no namespace {python_path!r}") def _namespace_symbol(self, plan: NamespacePlan) -> str: + """Return the binding-local namespace symbol derived from the supplied completed binding records; this helper preserves completed policy.""" return "_".join(plan.python_path).casefold() if plan.python_path else "root" def _namespace_object_name(self, plan: NamespacePlan) -> str: + """Return the binding-local namespace object name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"namespace_{self._namespace_symbol(plan)}" if plan.python_path else "mod" def _namespace_module_name(self, module: ModulePlan, namespace: NamespacePlan) -> str: + """Return the binding-local namespace module name derived from the supplied completed binding records; this helper preserves completed policy.""" return ".".join((module.binding.owner_path, *namespace.python_path)) + + +if __name__ == "__main__": + from prik.codegen.planner import WrapperPlanner + from prik.semantics.models import SemanticArgument, SemanticFunction, SemanticModule, SemanticType + from prik.semantics.policy_completion import complete_semantic_policies + + module = SemanticModule( + name="binding_demo", + functions=[ + SemanticFunction( + name="double_value", + native_name="DOUBLE_VALUE", + arguments=[SemanticArgument("value", SemanticType("Float64"))], + return_type=SemanticType("Float64"), + ) + ], + ) + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + function_plan = plan.namespaces[0].functions[0] + binding = CBindingGenerator() + binding.require_supported(plan) + c_module, c_header = binding.visit(plan) + wrapper = next(function for function in c_module.functions if function.name == "wrap_double_value") + + print(f"Native procedure: {function_plan.bridge.native_name}") + print( + "Native call slots:", + ", ".join(f"{slot.source_kind}:{slot.native_name}" for slot in function_plan.native_call_slots), + ) + print(f"C module: {c_module.name}") + print(f"Header guard: {c_header.guard}") + print("Header prototypes:", ", ".join(prototype.name for prototype in c_header.prototypes)) + print(f"Binding wrapper: {wrapper.name}") + print(f"Return type: {wrapper.return_type}") + print("Parameters:") + for parameter in wrapper.parameters: + print(f" {parameter.name}: {parameter.type_name}") + print("Body nodes:") + for statement in wrapper.body: + print(f" {statement!r}") diff --git a/prik/wrapper_codegen/checks.py b/prik/codegen/checks.py similarity index 97% rename from prik/wrapper_codegen/checks.py rename to prik/codegen/checks.py index 8d396a6b9..464fa6367 100644 --- a/prik/wrapper_codegen/checks.py +++ b/prik/codegen/checks.py @@ -11,8 +11,8 @@ __all__ = ( "WrapperCodegenCheckConfig", "WrapperCodegenViolation", - "check_wrapper_codegen_package", - "check_wrapper_codegen_paths", + "check_codegen_package", + "check_codegen_paths", ) @@ -56,7 +56,7 @@ class WrapperCodegenCheckConfig: @dataclass(frozen=True) class WrapperCodegenViolation: - """One static-contract violation in ``prik.wrapper_codegen``.""" + """One static-contract violation in ``prik.codegen``.""" path: Path lineno: int @@ -71,17 +71,17 @@ def label(self) -> str: DEFAULT_CHECK_CONFIG = WrapperCodegenCheckConfig() -def check_wrapper_codegen_package( +def check_codegen_package( package_root: Path | None = None, *, config: WrapperCodegenCheckConfig | None = None, ) -> tuple[WrapperCodegenViolation, ...]: """Check every Python module in the isolated wrapper-codegen package.""" root = package_root or Path(__file__).resolve().parent - return check_wrapper_codegen_paths(sorted(root.rglob("*.py")), config=config) + return check_codegen_paths(sorted(root.rglob("*.py")), config=config) -def check_wrapper_codegen_paths( +def check_codegen_paths( paths: list[Path], *, config: WrapperCodegenCheckConfig | None = None, diff --git a/prik/wrapper_codegen/docstrings.py b/prik/codegen/docstrings.py similarity index 60% rename from prik/wrapper_codegen/docstrings.py rename to prik/codegen/docstrings.py index cbaf2ebfb..638d10bc9 100644 --- a/prik/wrapper_codegen/docstrings.py +++ b/prik/codegen/docstrings.py @@ -1,4 +1,11 @@ -"""Python-facing documentation rendered from completed wrapper-plan records.""" +"""Render Python-facing documentation from completed wrapper-plan records. + +``WrapperDocstringBuilder`` turns completed plan facts into compact NumPy-style +docstrings for generated modules, functions, classes, descriptors, overloads, +and constructors. It is a presentation-only projection: ownership, optional +behavior, shape, and lifecycle decisions are read from the plan and never +re-derived from native declarations or backend output. +""" from __future__ import annotations @@ -9,7 +16,7 @@ NativeArrayDescriptorKind, OptionalMode, ) -from prik.wrapper_codegen.plan import ( +from prik.codegen.plan import ( ArgumentTransferPlan, ArrayHandoffPlan, BindingStatusErrorPlan, @@ -24,10 +31,11 @@ OverloadPlan, ResultPlan, ) +from prik.types.numpy import BOOLEAN_SEMANTIC_TYPE_NAMES _SCALAR_TYPES = { - "Bool": "bool", + **dict.fromkeys(BOOLEAN_SEMANTIC_TYPE_NAMES, "bool"), "Int8": "int8", "Int16": "int16", "Int32": "int32", @@ -43,9 +51,17 @@ class WrapperDocstringBuilder: - """Build compact NumPy-style documentation without backend inference.""" + """Build compact, public NumPy-style documentation from completed plans. + + Use the public entrypoints while ``WrapperPlanner`` constructs namespace, + callable, class, and attribute plan records. The builder preserves the + completed plan's public visibility, ordering, ownership, and transfer + facts, returning plain strings that generators later attach to public + Python surfaces. Its sections cover summaries, callable documentation, + constructors and attributes, then shared formatting helpers. + """ - # Namespace and class summaries. + # Public entrypoints: namespace and class summaries. def namespace( self, module_name: str, @@ -55,7 +71,14 @@ def namespace( classes, overloads: tuple[OverloadPlan, ...], ) -> str: - """Index every public owner in one generated Python namespace.""" + """Render one namespace summary from its completed public plan records. + + ``module_name`` and ``path`` select the display name; functions, + variables, classes, and overloads supply the summary entries. Private + generated functions are omitted, while ordering within each supplied + collection is retained. The resulting string is stored on the + namespace plan and later attached to the generated module. + """ display_name = path[-1] if path else module_name lines = [display_name] callable_lines = ( @@ -80,7 +103,13 @@ def class_surface( methods: tuple[ClassMethodPlan, ...], overloads: tuple[OverloadPlan, ...], ) -> str: - """Summarize one opaque class and its complete public descriptors.""" + """Render the public summary for one opaque native-wrapper class. + + The supplied constructor, fields, methods, and overloads are already + completed plan records. Only public methods appear in the summary; + their pre-rendered first lines preserve the planner's method order. + The returned text is normally attached to the generated Python class. + """ lines = [python_name, "", f"Opaque wrapper for native type {native_type_name}."] self._append_section(lines, "Constructor", (self._first_line(constructor.docstring),)) self._append_section(lines, "Fields", tuple(self._first_line(field.docstring) for field in fields)) @@ -94,7 +123,7 @@ def class_surface( ) return "\n".join(lines) - # Callback documentation. + # Public entrypoints: callable, overload, and constructor documentation. def function( self, python_name: str, @@ -104,10 +133,20 @@ def function( status_error: BindingStatusErrorPlan | None = None, excluded_native_position: int | None = None, ) -> str: - """Describe one Python callable from its completed transfers.""" + """Render a callable signature, parameter, return, and exception sections. + + ``arguments`` and ``results`` are completed transfer records. An + optional excluded native position removes a passed-object receiver from + a method's public signature. The returned string is attached to a + function plan or method surface; it does not validate or alter the + transfer records. + """ + # Select public arguments and outputs before rendering their shared summary. visible = self._visible_arguments(arguments, excluded_native_position) outputs = self._documented_outputs(arguments, results) lines = [self._callable_signature(python_name, visible, outputs)] + + # Append sections in the stable public order used by generated callables. self._append_section( lines, "Parameters", @@ -122,7 +161,13 @@ def function( return "\n".join(lines) def method(self, method: ClassMethodPlan) -> str: - """Describe one public method while omitting its passed-object slot.""" + """Render one class method, omitting its passed-object transfer. + + ``method`` carries both its public method metadata and underlying + function plan. Instance methods add the established in-place update + note when their completed receiver is mutable; static methods preserve + the base callable documentation unchanged. + """ docstring = self.function( method.python_name, method.function.arguments, @@ -138,7 +183,13 @@ def method(self, method: ClassMethodPlan) -> str: return docstring def overload(self, overload: OverloadPlan) -> str: - """List accepted public signatures without exposing private candidates.""" + """Render an overload dispatcher without exposing its private candidates. + + Candidate functions provide exact signatures, while their paired + passed-object flags determine whether a receiver is omitted. The + returned text documents public dispatch behavior and its TypeError + contract without changing candidate selection policy. + """ signatures = tuple( self._candidate_signature(overload.python_name, candidate, passed) for candidate, passed in zip( @@ -163,9 +214,17 @@ def constructor( constructor: ConstructorPlan, fields: tuple[DerivedFieldPlan, ...], ) -> str: - """Describe the selected construction route for one generated class.""" + """Render the completed construction route for one generated class. + + ``constructor.kind`` selects the documented route: absent, default + fields, one bound procedure, or an overload set. The builder adds the + shared return and TypeError sections after a supported route, returning + text that the class emitter attaches to ``__new__`` or ``__init__``. + """ if constructor.kind is ClassConstructorKind.ABSENT: return self._absent_constructor(python_name, constructor) + + # Select the route already completed by policy; this is documentation dispatch only. handlers = { ClassConstructorKind.DEFAULT_FIELDS: self._default_constructor, ClassConstructorKind.BOUND_PROCEDURE: self._bound_constructor, @@ -175,6 +234,8 @@ def constructor( if handler is None: # pragma: no cover - policy validation owns the enum envelope raise ValueError(f"Unsupported constructor kind: {constructor.kind.value}") lines = handler(python_name, constructor, fields) + + # Every supported route shares the same public result and argument-error contract. self._append_section(lines, "Returns", (python_name, " New wrapper-owned native instance.")) self._append_section( lines, @@ -185,7 +246,13 @@ def constructor( @staticmethod def _absent_constructor(python_name: str, constructor: ConstructorPlan) -> str: - """Describe an explicitly nonconstructible wrapper class.""" + """Render the rejection-only documentation for an absent constructor. + + ``python_name`` supplies the displayed call while the completed + constructor may supply a custom rejection message. The returned text + contains only the stable TypeError contract and does not modify class + construction behavior. + """ return "\n".join( ( f"{python_name}(*args, **kwargs)", @@ -203,7 +270,13 @@ def _default_constructor( constructor: ConstructorPlan, fields: tuple[DerivedFieldPlan, ...], ) -> list[str]: - """Document the keyword-only editable-field constructor.""" + """Render the editable-field portion of a default constructor. + + Constructor field metadata selects the subset and order of supplied + field plans. Missing plan fields remain absent as established by the + completed constructor route; the returned mutable line list receives + common return/raise sections from the caller. + """ by_name = {field.name: field for field in fields} parameters = tuple(by_name[item.name] for item in constructor.fields if item.name in by_name) lines = [self._keyword_field_signature(python_name, constructor, parameters)] @@ -220,7 +293,13 @@ def _bound_constructor( constructor: ConstructorPlan, _fields: tuple[DerivedFieldPlan, ...], ) -> list[str]: - """Document a constructor backed by one completed native call.""" + """Render the signature and parameters for one bound-procedure constructor. + + The completed target function supplies public arguments and an optional + passed-object position. A missing target is inconsistent plan input + and raises ``ValueError``; otherwise the caller appends shared sections + to the returned line list. + """ target = constructor.target if target is None: raise ValueError(f"Bound constructor {python_name!r} has no target plan") @@ -240,7 +319,13 @@ def _overloaded_constructor( constructor: ConstructorPlan, _fields: tuple[DerivedFieldPlan, ...], ) -> list[str]: - """Document exact signatures accepted by an overloaded constructor.""" + """Render public signatures for an overload-set constructor. + + Candidate/receiver pairs remain zipped in completed order and use the + class name as their public result type. A missing overload record is + invalid plan input and raises ``ValueError``; common sections remain + the responsibility of the caller. + """ overload = constructor.overload if overload is None: raise ValueError(f"Overloaded constructor {python_name!r} has no overload plan") @@ -256,9 +341,15 @@ def _overloaded_constructor( self._append_section(lines, "Supported Signatures", signatures) return lines - # Attribute documentation. + # Public entrypoints: module attributes and class fields. def module_variable(self, variable: ModuleVariablePlan) -> str: - """Describe a module attribute where CPython cannot attach a descriptor docstring.""" + """Render a module-attribute summary for its owning namespace docstring. + + CPython cannot attach a separate descriptor docstring to these module + attributes, so the returned lines are included in the namespace + documentation. Getter, setter, array-handle, and derived-object text + comes directly from the completed variable plan. + """ name = variable.binding.python_names[0] nullable = variable.binding.getter_action is ModuleGetterAction.NULLABLE_SNAPSHOT lines = [f"{name} : {self._type(variable, nullable=nullable, signature=False)}"] @@ -266,6 +357,7 @@ def module_variable(self, variable: ModuleVariablePlan) -> str: if variable.binding.getter_action in { ModuleGetterAction.CONSTANT_VALUE, ModuleGetterAction.NATIVE_CONSTANT_VALUE, + ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE, }: lines.append(" Read-only constant.") elif variable.binding.getter_action is ModuleGetterAction.BORROWED_ARRAY_VIEW: @@ -279,7 +371,12 @@ def module_variable(self, variable: ModuleVariablePlan) -> str: return "\n".join(lines) def field(self, field: DerivedFieldPlan) -> str: - """Describe one property and its native lifetime or assignment behavior.""" + """Render one generated class-property docstring from its field plan. + + Array/handle lifetime and setter wording reflect the completed field + access policy. The string is later installed on the generated Python + property; this builder does not alter native assignment or retention. + """ lines = [f"{field.name} : {self._type(field, nullable=False, signature=False)}"] lines.extend(self._array_lines(field.array)) if field.native_array_handle is not None: @@ -295,17 +392,27 @@ def field(self, field: DerivedFieldPlan) -> str: lines.append(" Read-only attribute.") return "\n".join(lines) - # Shared signature and section helpers. + # Shared callable signatures, public sections, and output descriptions. @staticmethod def _append_section(lines: list[str], heading: str, body: tuple[str, ...]) -> None: - """Append one nonempty NumPy-style section.""" + """Append one nonempty NumPy-style section to an in-progress line list. + + ``lines`` is mutated only when ``body`` contains public content. The + helper preserves the established blank-line, heading, underline, and + body ordering; empty sections intentionally leave the list unchanged. + """ if not body: return lines.extend(("", heading, "-" * len(heading), *body)) @staticmethod def _first_line(docstring: str) -> str: - """Return the stable summary line of a rendered docstring.""" + """Return the first summary line from rendered text or an empty string. + + Namespace and class summaries use this to embed a callable's compact + public signature. It neither trims nor changes the supplied docstring + beyond selecting its first split line. + """ return docstring.splitlines()[0] if docstring else "" def _callable_signature( @@ -314,6 +421,12 @@ def _callable_signature( arguments: tuple[ArgumentTransferPlan, ...], outputs: tuple[ArgumentTransferPlan | ResultPlan, ...], ) -> str: + """Build the first-line callable signature from visible inputs and outputs. + + ``arguments`` are already filtered for public visibility and ``outputs`` + are ordered public result producers. The helper delegates type wording + to the shared signature/result formatters and has no plan side effects. + """ return self._signature(name, arguments, self._result_summary(outputs)) def _signature( @@ -322,11 +435,23 @@ def _signature( arguments: tuple[ArgumentTransferPlan, ...], result_type: str, ) -> str: + """Render one untyped public signature with its prepared result text. + + Parameter order is the supplied tuple order, which callers derive from + completed Python positions. ``result_type`` is already rendered so + this helper only joins the final stable first line. + """ parameters = ", ".join(self._signature_parameter(argument) for argument in arguments) return f"{name}({parameters}) -> {result_type}" @staticmethod def _signature_parameter(argument: ArgumentTransferPlan) -> str: + """Render one public parameter name and optional default marker. + + Required values keep their Python name unchanged. Any completed + optional mode renders the established ``= ...`` marker without + inspecting native defaults or changing optionality. + """ name = argument.binding.python_name if argument.binding.optional_mode not in {OptionalMode.REQUIRED, OptionalMode.REQUIRED_DESCRIPTOR}: return f"{name}=..." @@ -340,6 +465,12 @@ def _candidate_signature( *, result_type: str | None = None, ) -> str: + """Render one typed overload candidate without exposing private names. + + ``passed_object`` determines whether the completed receiver position is + removed. The candidate's public outputs produce the result summary + unless a constructor supplies its explicit ``result_type``. + """ passed = candidate.class_call.passed_object_position if passed_object and candidate.class_call else None arguments = self._visible_arguments(candidate.arguments, passed) outputs = self._documented_outputs(candidate.arguments, candidate.results) @@ -348,7 +479,12 @@ def _candidate_signature( @staticmethod def _overload_candidates(overload: OverloadPlan): - """Pair candidates with their completed passed-object flags.""" + """Yield overload candidates paired with completed receiver flags. + + The strict zip preserves candidate order and exposes a length mismatch + as the normal ``ValueError``. Callers consume this iterator for + documentation-only receiver notes without changing dispatch policy. + """ return zip(overload.candidates, overload.candidate_passed_objects, strict=True) @staticmethod @@ -356,18 +492,33 @@ def _argument_at_native_position( arguments: tuple[ArgumentTransferPlan, ...], native_position: int, ) -> ArgumentTransferPlan: - """Return the validated receiver selected by class-call policy.""" + """Return the argument at one completed native receiver position. + + The caller supplies a function's transfer tuple and its selected native + position. The first matching transfer is returned; a missing receiver + raises the normal ``StopIteration`` because the plan is inconsistent. + """ return next(argument for argument in arguments if argument.native_position == native_position) def _candidate_mutates_receiver(self, candidate: FunctionPlan, passed_object: bool) -> bool: - """Report receiver mutation for one completed overload candidate.""" + """Return whether one overload candidate mutates its passed-object receiver. + + Static candidates and candidates without class-call metadata are false. + Instance candidates reuse the completed receiver transfer and return + its stored mutability fact without deriving behavior from method names. + """ if not passed_object or candidate.class_call is None: return False receiver = self._argument_at_native_position(candidate.arguments, candidate.class_call.passed_object_position) return receiver.mutates_native def _overload_notes(self, overload: OverloadPlan) -> tuple[str, ...]: - """Describe only receiver behavior shared by class-owned candidates.""" + """Render shared class-overload notes from completed receiver metadata. + + Module overloads return no notes. Class-owned overloads describe their + native-instance dispatch and add the established update note when any + candidate mutates its receiver; candidate order and state stay intact. + """ if not any(overload.candidate_passed_objects): return () notes = ["Dispatches to a native operation on the wrapped instance."] @@ -379,7 +530,12 @@ def _overload_notes(self, overload: OverloadPlan) -> tuple[str, ...]: return tuple(notes) def _typed_signature_parameter(self, argument: ArgumentTransferPlan) -> str: - """Render enough public type information to distinguish overloads.""" + """Render one typed overload parameter and optional default marker. + + The type and nullability are read from the completed transfer. This + richer spelling differentiates public overload candidates while using + the same optional marker as ordinary callable signatures. + """ parameter = f"{argument.binding.python_name}: {self._type(argument, nullable=argument.binding.nullable, signature=True)}" if argument.binding.optional_mode not in {OptionalMode.REQUIRED, OptionalMode.REQUIRED_DESCRIPTOR}: return f"{parameter} = ..." @@ -390,6 +546,12 @@ def _visible_arguments( arguments: tuple[ArgumentTransferPlan, ...], excluded_native_position: int | None, ) -> tuple[ArgumentTransferPlan, ...]: + """Return public arguments in completed Python-position order. + + The passed-object position is excluded only when supplied. Arguments + marked non-public are always omitted, preserving method and callback + signatures without mutating their underlying function plan. + """ return tuple( argument for argument in sorted(arguments, key=lambda item: item.python_position) @@ -401,6 +563,13 @@ def _documented_outputs( arguments: tuple[ArgumentTransferPlan, ...], results: tuple[ResultPlan, ...], ) -> tuple[ArgumentTransferPlan | ResultPlan, ...]: + """Merge projected arguments and declared results by public result position. + + Projected arguments are collected first; declared results at the same + position take precedence, matching the established public projection. + The returned tuple is sorted by position and leaves both input tuples + unchanged. + """ by_position = { argument.result_position: argument for argument in arguments @@ -410,6 +579,12 @@ def _documented_outputs( return tuple(by_position[position] for position in sorted(by_position)) def _result_summary(self, outputs: tuple[ArgumentTransferPlan | ResultPlan, ...]) -> str: + """Render the compact result type used by a callable's first line. + + Nullable argument outputs use their completed optional mode; result + outputs use their stored nullability. Zero, one, and multiple outputs + render as ``None``, one type, or a typed tuple respectively. + """ types = tuple( self._type( output, @@ -428,8 +603,14 @@ def _result_summary(self, outputs: tuple[ArgumentTransferPlan | ResultPlan, ...] return types[0] return f"tuple[{', '.join(types)}]" - # Parameter and result details. + # Parameter, result, ownership, and exception details. def _argument_lines(self, argument: ArgumentTransferPlan) -> tuple[str, ...]: + """Render the complete public parameter block for one transfer. + + The lines combine type, array shape, optionality, mutation, ownership, + and descriptor facts in a fixed order. All wording derives from the + supplied completed transfer and returns a new tuple without mutation. + """ 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)}"] @@ -447,6 +628,13 @@ def _output_lines( output: ArgumentTransferPlan | ResultPlan, arguments: tuple[ArgumentTransferPlan, ...], ) -> tuple[str, ...]: + """Render the complete public return block for one output producer. + + Projected arguments keep their Python name and optionality; declared + results resolve a stable public result name. Array/handle, ownership, + copy-return, and nullable notes are appended in the established order + without changing the owning plan records. + """ if isinstance(output, ArgumentTransferPlan): name = output.binding.python_name nullable = output.binding.optional_mode not in {OptionalMode.REQUIRED, OptionalMode.REQUIRED_DESCRIPTOR} @@ -472,6 +660,12 @@ def _output_lines( @staticmethod def _optional_lines(argument: ArgumentTransferPlan) -> tuple[str, ...]: + """Describe a transfer's completed optional or nullable input contract. + + Descriptor modes distinguish absent native dummies from present empty + descriptors. Other optional modes use the public omission wording; + required nullable values retain their distinct ``None`` wording. + """ mode = argument.binding.optional_mode if mode is OptionalMode.DESCRIPTOR: return ( @@ -488,6 +682,12 @@ def _optional_lines(argument: ArgumentTransferPlan) -> tuple[str, ...]: @staticmethod def _mutation_lines(argument: ArgumentTransferPlan) -> tuple[str, ...]: + """Describe completed native mutation and copy-return projection behavior. + + Non-mutating arguments produce no note. Copy returns, projected + updates, and in-place storage each retain their established wording; + the helper does not infer mutability from datatype or intent. + """ if not argument.mutates_native: return () if argument.transfer_mode is TransferMode.COPY_RETURN: @@ -505,6 +705,12 @@ def _raise_lines( outputs: tuple[ArgumentTransferPlan | ResultPlan, ...], status_error: BindingStatusErrorPlan | None, ) -> tuple[str, ...]: + """Collect public exceptions implied by completed callable transfers. + + Type errors are always documented; arrays, derived objects, and status + envelopes contribute their respective contract errors. Duplicate + exception names are grouped by the shared formatter in insertion order. + """ exceptions = [("TypeError", "If an argument has an incompatible Python type or dtype.")] if any(item.array is not None or item.native_array_handle is not None for item in (*arguments, *outputs)): exceptions.append(("ValueError", "If rank, shape, layout, or descriptor state violates the contract.")) @@ -521,7 +727,12 @@ def _raise_lines( @staticmethod def _merged_exception_lines(exceptions: list[tuple[str, str]]) -> tuple[str, ...]: - """Group descriptions under one heading per public exception type.""" + """Group exception descriptions by type while preserving first-seen order. + + ``exceptions`` is an ordered list of public error descriptions. The + returned alternating heading/detail lines merge repeated exception + names without sorting or mutating the input collection. + """ grouped: dict[str, list[str]] = {} for exception, description in exceptions: grouped.setdefault(exception, []).append(description) @@ -531,14 +742,26 @@ def _merged_exception_lines(exceptions: list[tuple[str, str]]) -> tuple[str, ... for line in (exception, *(f" {item}" for item in descriptions)) ) - # Type, array, ownership, and constructor helpers. + # Type, array, ownership, and constructor-formatting helpers. def _type(self, transfer, *, nullable: bool, signature: bool) -> str: + """Render one completed transfer's public type, optionally with ``None``. + + ``signature`` selects annotation versus prose spelling for nullability. + The base type is delegated to the completed family/handle/array facts; + this helper performs presentation only. + """ type_name = self._base_type(transfer) if not nullable: return type_name return f"{type_name} | None" if signature else f"{type_name} or None" def _base_type(self, transfer) -> str: + """Map one completed transfer family and storage facet to public type text. + + Callback and derived types keep their named plan identities. Scalar, + descriptor-handle, and ordinary-array cases use the shared scalar map + and stored facets, never a native declaration or backend spelling. + """ if getattr(transfer, "datatype_family", None) is DatatypeFamily.CALLBACK: return self._callback_type(transfer.callback) if getattr(transfer, "datatype_family", None) is DatatypeFamily.DERIVED: @@ -558,12 +781,24 @@ def _base_type(self, transfer) -> str: return scalar def _callback_type(self, callback: CallbackHandoffPlan | None) -> str: + """Return the named completed prototype for one callback transfer. + + Callback documentation requires the handoff plan attached during + policy completion. A missing handoff is an inconsistent caller input + and raises ``ValueError`` rather than inventing a callable signature. + """ if callback is None: raise ValueError("Callback documentation requires a completed handoff plan") - return callback.prototype_name + return callback.prototype.name @staticmethod def _callback_transfer_type(transfer: CallbackTransferPlan) -> str: + """Render a callback prototype argument or result from completed ABI facts. + + 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. + """ 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) @@ -573,11 +808,18 @@ def _callback_transfer_type(transfer: CallbackTransferPlan) -> str: @staticmethod def _array_lines(array: ArrayHandoffPlan | None) -> tuple[str, ...]: + """Render rank, resolved display shape, and layout notes for one array facet. + + ``None`` produces no lines. Unknown extents are intentionally omitted + from shape text while rank and supported layout facts remain visible; + the supplied plan is not normalized or validated here. + """ if array is None: return () lines = [WrapperDocstringBuilder._array_rank_line(array)] - if array.shape and all(str(extent) not in _UNKNOWN_EXTENTS for extent in array.shape): - lines.append(f" Shape: ({', '.join(map(str, array.shape))})") + 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))})") if (array.rank is None or array.rank > 1) and array.order in {"ORDER_C", "ORDER_F"}: layout = "C-contiguous" if array.order == "ORDER_C" else "F-contiguous" lines.append(f" Layout: {layout}") @@ -585,6 +827,12 @@ def _array_lines(array: ArrayHandoffPlan | None) -> tuple[str, ...]: @staticmethod def _array_rank_line(array: ArrayHandoffPlan) -> str: + """Render the rank sentence for ordinary or flattened Python storage. + + Flattened storage preserves the special native-rank and flat-axis + wording. Assumed rank and concrete rank use their distinct stable + forms, with no inspection of native declaration expressions. + """ if array.flatten_python_storage: native_rank = 1 if array.rank is None else array.rank if native_rank == 1: @@ -597,6 +845,12 @@ def _array_rank_line(array: ArrayHandoffPlan) -> str: @staticmethod def _ownership_lines(owner: OwnershipOwner) -> tuple[str, ...]: + """Render the public ownership label for one completed owner enum. + + Every supported ``OwnershipOwner`` maps to one stable prose label. An + unsupported value raises the normal mapping error rather than silently + choosing a different ownership description. + """ label = { OwnershipOwner.CALLER: "Caller-owned", OwnershipOwner.NATIVE: "Native-owned", @@ -609,6 +863,12 @@ def _ownership_lines(owner: OwnershipOwner) -> tuple[str, ...]: @staticmethod def _result_name(result: ResultPlan, arguments: tuple[ArgumentTransferPlan, ...]) -> str: + """Choose the stable public name for one declared result record. + + A matching projected argument wins, then a native-slot Python name, + then the positional ``result`` fallback. The lookup only presents the + already-planned public projection and does not modify result ordering. + """ projected = next( ( argument.binding.python_name @@ -624,6 +884,13 @@ def _result_name(result: ResultPlan, arguments: tuple[ArgumentTransferPlan, ...] return "result" if result.result_position == 0 else f"result_{result.result_position}" def _module_variable_summary_lines(self, variable: ModuleVariablePlan) -> tuple[str, ...]: + """Expand one module-variable docstring for every exported Python alias. + + The first line supplies the rendered type while the remaining details + are reused verbatim for each alias, preserving attribute documentation + without rebuilding getter/setter policy. A nonstandard first line is + returned unchanged as one summary line. + """ first, *details = variable.docstring.splitlines() _name, separator, type_name = first.partition(" : ") if not separator: @@ -636,6 +903,12 @@ def _keyword_field_signature( constructor: ConstructorPlan, fields: tuple[DerivedFieldPlan, ...], ) -> str: + """Render a keyword-only default-field constructor signature. + + Field order follows the supplied completed field tuple, and each + constructor default uses the matching prepared field metadata. With no + fields the helper returns the stable empty constructor form. + """ defaults = {field.name: field.default_value for field in constructor.fields} parameters = ", ".join( f"{field.name}={defaults[field.name] if defaults[field.name] is not None else '...'}" for field in fields @@ -643,4 +916,38 @@ def _keyword_field_signature( return f"{python_name}(*, {parameters}) -> {python_name}" if parameters else f"{python_name}() -> {python_name}" def _constructor_field_lines(self, field: DerivedFieldPlan) -> tuple[str, ...]: + """Render one editable constructor field as a public parameter line. + + The field's already completed type is rendered without nullability + decoration, matching the default-field constructor contract. No + setter or ownership wording is added in this compact parameter view. + """ return (f"{field.name} : {self._type(field, nullable=False, signature=False)}",) + + +if __name__ == "__main__": + from prik.codegen.planner import WrapperPlanner + from prik.semantics.models import SemanticArgument, SemanticFunction, SemanticModule, SemanticType + from prik.semantics.policy_completion import complete_semantic_policies + + module = SemanticModule( + name="docstring_demo", + functions=[ + SemanticFunction( + name="double_value", + native_name="DOUBLE_VALUE", + arguments=[SemanticArgument("value", SemanticType("Float64"))], + return_type=SemanticType("Float64"), + ) + ], + ) + complete_semantic_policies(module) + function = WrapperPlanner().build(module).namespaces[0].functions[0] + docstring = WrapperDocstringBuilder().function( + function.binding.python_name, + function.arguments, + function.results, + status_error=function.binding.status_error, + ) + + print(docstring) diff --git a/prik/wrapper_codegen/fortran/__init__.py b/prik/codegen/fortran/__init__.py similarity index 100% rename from prik/wrapper_codegen/fortran/__init__.py rename to prik/codegen/fortran/__init__.py diff --git a/prik/wrapper_codegen/fortran/bridge.py b/prik/codegen/fortran/bridge.py similarity index 87% rename from prik/wrapper_codegen/fortran/bridge.py rename to prik/codegen/fortran/bridge.py index 396ca31bd..73f0027ac 100644 --- a/prik/wrapper_codegen/fortran/bridge.py +++ b/prik/codegen/fortran/bridge.py @@ -1,9 +1,17 @@ -"""Direct recursive Fortran bridge generation from shared wrapper plans.""" +"""Lower validated wrapper plans into Fortran bridge syntax nodes. + +Use :class:`FortranBridgeGenerator` after post-IR policy completion and wrapper +planning. Its :meth:`visit` method consumes a validated `ModulePlan` and +returns a `FortranModule` for the source printer. This stage does not infer +ownership, argument, or result policy: it dispatches only the completed bridge +actions already projected into the plan. +""" from __future__ import annotations import re +from prik.utilities.declaration_expressions import render_declaration_extent from prik.semantics.ownership import ( AssignmentMode, CodegenAction, @@ -15,6 +23,7 @@ from prik.semantics.metadata import SCALAR_STORAGE_CATEGORY from prik.semantics.wrapper_policy import ( ArgumentHandoffMode, + ArrayLogicalABI, ArrayWritebackABI, BridgeDataAction, CallbackABIKind, @@ -29,6 +38,7 @@ DerivedDummyCategory, DerivedObjectStorage, DerivedRelease, + DeclarationCallableAction, DirectResultABI, ExternalDeclarationMode, ModuleGetterAction, @@ -41,8 +51,10 @@ NativeDescriptorHandoffABI, NativeInvocationKind, OptionalMode, + ScalarLogicalABI, ) -from prik.wrapper_codegen.nodes import ( +from prik.types.numpy import is_boolean_semantic_type_name +from prik.codegen.nodes import ( CodeExpression, FortranAllocate, FortranAssignment, @@ -62,14 +74,15 @@ FortranTypeDefinition, FortranUse, ) -from prik.wrapper_codegen.naming import NativeSymbolNames -from prik.wrapper_codegen.plan import ( +from prik.codegen.naming import NativeSymbolNames +from prik.codegen.plan import ( ArrayHandoffPlan, ArgumentTransferPlan, CallbackHandoffPlan, CallbackTransferPlan, ClassSurfacePlan, DatatypeFamily, + DeclarationCallablePlan, DerivedFieldPlan, DerivedMemberPathPlan, DerivedTypePlan, @@ -79,21 +92,45 @@ NamespacePlan, NativeArrayHandlePlan, NativeCallSlotPlan, + ProcedurePrototypeArgumentPlan, + ProcedurePrototypePlan, + ProcedurePrototypeResultPlan, ResultPlan, ) -from prik.wrapper_codegen.primitive_scalar_types import PrimitiveScalarTypeRegistry -from prik.wrapper_codegen.visitor import ClassVisitor +from prik.codegen.primitive_scalar_types import PrimitiveScalarTypeRegistry +from prik.codegen.visitor import ClassVisitor class FortranBridgeGenerator(ClassVisitor): - """Recursively lower bridge plan views directly into Fortran nodes.""" + """Build the Fortran half of a wrapper from validated bridge-plan views. + + Instantiate this visitor when direct generation needs backend syntax nodes, + rather than rendered source. Call :meth:`require_supported` for the + backend-local type preflight and then :meth:`visit` with a `ModulePlan`. + The result is a `FortranModule` consumed by `FortranSourcePrinter`. + Completed policy stays outside this class; unmatched lowering actions fail + defensively instead of being reinterpreted here. + """ def __init__(self, *, method_prefix: str | None = None): + """Initialize the visitor and clear the per-module scoped-type cache. + + The optional prefix is forwarded unchanged to :class:`ClassVisitor`. + Scoped identities are temporary visitor state and are restored after + each module visit. + """ super().__init__(method_prefix=method_prefix) self._active_scoped_type_identities: frozenset[tuple[str, str]] = frozenset() def require_supported(self, plan: ModulePlan) -> None: - """Preflight Fortran scalar types after shared plan validation.""" + """Preflight primitive spellings required by an already-validated plan. + + Call this before :meth:`visit` when using the bridge generator + directly. It resolves only the primitive types the Fortran backend + must emit; plan consistency and semantic-policy decisions remain owned + by earlier stages. Unsupported registry entries propagate their normal + lookup error. + """ for derived in self._derived_types(plan): self._require_derived_type_supported(derived) for function in self._functions(plan): @@ -145,10 +182,16 @@ def _require_backend_type_supported( PrimitiveScalarTypeRegistry.type_for(semantic_type_name) def _visit_ModulePlan(self, plan: ModulePlan) -> FortranModule: - """Return one complete Fortran bridge module.""" + """Build one complete bridge module from one validated module plan. + + The temporary scoped-origin identity cache is installed only while this + visit runs and is restored even when a lowering helper fails. + """ + # Scoped origins are module-wide facts needed by derived-call lowering. previous_scoped = self._active_scoped_type_identities self._active_scoped_type_identities = self._scoped_origin_type_identities(plan) try: + # Assemble imports, declarations, and procedures from plan projections. return FortranModule( name=f"bind_c_{plan.bridge.owner_path}_wrapper", uses=( @@ -158,11 +201,13 @@ def _visit_ModulePlan(self, plan: ModulePlan) -> FortranModule: type_definitions=self._derived_holder_definitions(plan), interfaces=( *self._derived_call_interfaces(plan), + *self._prototype_interfaces(plan), *self._external_interfaces(plan), *self._module_descriptor_callback_interfaces(plan), *self._derived_array_callback_interfaces(plan), *self._allocator_interfaces(plan), ), + declarations=self._prototype_entity_declarations(plan), procedures=( *(procedure for namespace in plan.namespaces for procedure in self.visit(namespace)), # Typed derived-field access remains separate from class orchestration. @@ -186,15 +231,15 @@ def _visit_ModulePlan(self, plan: ModulePlan) -> FortranModule: for procedure in self._derived_origin_procedures(variable) ), ), - external_procedures=self._callback_external_adapter_procedures(plan), + standalone_procedures=self._callback_standalone_adapter_procedures(plan), ) finally: self._active_scoped_type_identities = previous_scoped - def _callback_external_adapter_procedures(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: + def _callback_standalone_adapter_procedures(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: """Return separately linked callback adapters in stable site order.""" return tuple( - self._callback_external_adapter_procedure(callback, plan) for callback in self._callback_sites(plan) + self._callback_standalone_adapter_procedure(callback, plan) for callback in self._callback_sites(plan) ) def _derived_holder_definitions(self, plan: ModulePlan) -> tuple[FortranTypeDefinition, ...]: @@ -243,7 +288,12 @@ def _visit_NamespacePlan(self, plan: NamespacePlan) -> tuple[FortranFunction, .. ) def _visit_FunctionPlan(self, plan: FunctionPlan) -> FortranFunction: - """Recursively assemble one complete bridge procedure.""" + """Build one bridge procedure through ABI, call, and cleanup stages. + + All declarations and nodes come from completed function-plan actions; + this orchestration only preserves their required execution order. + """ + # Stage 1: determine the bridge ABI and result representation. result_name, result_type = self._lower_result(plan) owned_direct_result = self._owned_direct_result(plan) parameters = tuple( @@ -256,13 +306,17 @@ def _visit_FunctionPlan(self, plan: FunctionPlan) -> FortranFunction: *self._native_output_parameters(plan), *self._owned_direct_result_parameters(owned_direct_result), *self._scalar_descriptor_direct_result_parameters(plan), + *self._declaration_extent_result_parameters(plan), ) bridge_name = self._bridge_function_name(plan) is_subroutine = plan.bridge.native_is_subroutine or owned_direct_result is not None + # Stage 2: assemble the native invocation and its ordered finalizers. function_body, optional_procedures = self._function_body(plan, result_name) native_body = ( *self._derived_pointer_call_initializers(plan), *function_body, + *self._logical_scalar_argument_finalizers(plan), + *self._logical_array_argument_finalizers(plan), *self._array_writeback_finalizers(plan), *self._derived_pointer_call_finalizers(plan), *self._required_descriptor_finalizers(plan), @@ -271,6 +325,7 @@ def _visit_FunctionPlan(self, plan: FunctionPlan) -> FortranFunction: *self._direct_result_finalizers(plan), *self._native_output_finalizers(plan), ) + # Stage 3: wrap native execution in derived-result and carrier lifecycles. call_body = self._derived_result_execution(plan, result_name, native_body) derived_body, internal_procedures = self._derived_call_execution(plan, call_body) return FortranFunction( @@ -283,6 +338,7 @@ def _visit_FunctionPlan(self, plan: FunctionPlan) -> FortranFunction: *self._callback_external_declarations(plan), *self._native_external_declarations(plan), *self._optional_declarations(plan), + *self._logical_scalar_argument_declarations(plan), *self._opaque_address_declarations(plan), *self._array_declarations(plan), *self._raw_array_address_declarations(plan), @@ -296,11 +352,15 @@ def _visit_FunctionPlan(self, plan: FunctionPlan) -> FortranFunction: body=( *self._descriptor_initializers(plan), *self._required_descriptor_initializers(plan), + *self._logical_scalar_argument_initializers(plan), *self._opaque_address_initializers(plan), *self._array_initializers(plan), + *self._logical_array_argument_initializers(plan), *self._raw_array_address_initializers(plan), *self._string_value_initializers(plan), *self._string_address_initializers(plan), + *self._declaration_extent_result_assignments(plan), + *self._direct_array_result_initializers(plan), *derived_body, ), is_subroutine=is_subroutine, @@ -311,8 +371,44 @@ def _visit_FunctionPlan(self, plan: FunctionPlan) -> FortranFunction: ), ) + def _declaration_extent_result_parameters(self, plan: FunctionPlan) -> tuple[FortranParameter, ...]: + """Expose bridge-evaluated result extents through the main bridge ABI.""" + return tuple( + FortranParameter( + self._declaration_extent_result_name(result, axis), + "integer(c_int64_t)", + ("intent(out)",), + ) + for result in plan.results + if result.array is not None + for axis, evaluation in enumerate(result.array.extent_evaluation) + if evaluation == "bridge" + ) + + def _declaration_extent_result_assignments(self, plan: FunctionPlan) -> tuple[FortranAssignment, ...]: + """Evaluate native-dependent result axes inside the Fortran bridge.""" + assignments = [] + for result in plan.results: + if result.array is None or "bridge" not in result.array.extent_evaluation: + continue + shape = self._array_shape_from_roles(result.array, plan) + assignments.extend( + FortranAssignment( + self._declaration_extent_result_name(result, axis), + CodeExpression(f"int({shape[axis]}, c_int64_t)"), + ) + for axis, evaluation in enumerate(result.array.extent_evaluation) + if evaluation == "bridge" + ) + return tuple(assignments) + + @staticmethod + def _declaration_extent_result_name(result: ResultPlan, axis: int) -> str: + """Return the shared main-bridge ABI name for one evaluated result axis.""" + return f"prik_decl_extent_{result.result_position}_{axis}" + # Immediate callback adapters. - def _callback_external_adapter_procedure( + def _callback_standalone_adapter_procedure( self, callback: CallbackHandoffPlan, plan: ModulePlan, @@ -326,7 +422,7 @@ def _callback_external_adapter_procedure( parameters=tuple(self._callback_native_parameter(transfer) for transfer in callback.arguments), result_name=None if is_subroutine else "callback_result", result_type=None if is_subroutine else self._callback_native_result_type(result), - uses=self._callback_external_adapter_uses(callback, plan), + uses=self._callback_standalone_adapter_uses(callback, plan), implicit_none=True, interfaces=( FortranInterface( @@ -369,6 +465,8 @@ def _callback_native_parameter(self, transfer: CallbackTransferPlan) -> FortranP attributes = [] if transfer.passed_by_value: attributes.append("value") + if transfer.intent is not None: + attributes.append(f"intent({transfer.intent})") if transfer.abi is not CallbackABIKind.VALUE and transfer.adapter_action in { CallbackTransferAction.BORROW_READ_ONLY, CallbackTransferAction.BORROW_WRITABLE, @@ -382,7 +480,7 @@ def _callback_native_parameter(self, transfer: CallbackTransferPlan) -> FortranP tuple(attributes), ) - def _callback_external_adapter_uses( + def _callback_standalone_adapter_uses( self, callback: CallbackHandoffPlan, plan: ModulePlan, @@ -420,36 +518,18 @@ def _callback_external_adapter_uses( ) def _callback_external_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, ...]: - """Declare external callback adapters from completed callback policy.""" - declarations = [] - for callback in ( - argument.callback - for argument in sorted(plan.arguments, key=lambda item: item.native_position) - if argument.callback is not None - ): - if callback.declaration_mode is ExternalDeclarationMode.EXPLICIT_INTERFACE: - declarations.append( - FortranDeclaration( - callback.adapter_symbol, - f"procedure({self._callback_imported_prototype_symbol(callback)})", - ) - ) - continue - if callback.declaration_mode is not ExternalDeclarationMode.IMPLICIT_EXTERNAL: - raise ValueError( - f"Callback {callback.owner_path!r} has unsupported declaration mode {callback.declaration_mode!r}" - ) - if callback.result.action is CallbackResultAction.RETURN_VOID: - declarations.append(FortranDeclaration(callback.adapter_symbol, "external")) - continue - declarations.append( - FortranDeclaration( - callback.adapter_symbol, - self._callback_native_result_type(callback.result.transfer), - ("external",), - ) + """Declare every external callback adapter from its shared prototype.""" + return tuple( + FortranDeclaration( + callback.adapter_symbol, + f"procedure({callback.prototype.interface_symbol})", ) - return tuple(declarations) + for callback in ( + argument.callback + for argument in sorted(plan.arguments, key=lambda item: item.native_position) + if argument.callback is not None + ) + ) def _callback_transfer_declarations( self, @@ -636,14 +716,19 @@ def _callback_native_type(self, transfer: CallbackTransferPlan) -> str: @staticmethod 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.""" 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 ", ".join(transfer.array.shape) + return ", ".join( + render_declaration_extent(expression, {}, target="fortran") for expression in transfer.array.shape + ) def _callback_address_source(self, transfer: CallbackTransferPlan) -> str: + """Return the C-address expression that backs one callback transfer.""" if transfer.adapter_action in { CallbackTransferAction.COPY_IN, CallbackTransferAction.COPY_OUT, @@ -653,10 +738,12 @@ def _callback_address_source(self, transfer: CallbackTransferPlan) -> str: return self._callback_parameter_base_name(transfer) def _callback_storage_name(self, transfer: CallbackTransferPlan) -> str: + """Return the local storage name used while adapting one callback transfer.""" return f"{self._callback_parameter_base_name(transfer)}_callback_storage" # Scalar-derived carrier preparation, invocation, and restoration. def _derived_arguments(self, plan: FunctionPlan) -> tuple[ArgumentTransferPlan, ...]: + """Return function arguments that carry completed scalar-derived call actions, preserving native-call order.""" return tuple( sorted( (argument for argument in plan.arguments if argument.derived_call is not None), @@ -734,6 +821,7 @@ def _derived_call_execution( return tuple(body), internal def _derived_call_preparation_nodes(self, arguments: tuple[ArgumentTransferPlan, ...]) -> tuple: + """Build carrier initialization, preparation, and transaction-acquisition nodes for derived arguments. Acquisition remains in argument order.""" return ( *(node for argument in arguments for node in self._derived_argument_initializers(argument)), FortranAssignment("prik_derived_ready", CodeExpression(".true.")), @@ -745,6 +833,7 @@ def _scoped_derived_arguments( self, arguments: tuple[ArgumentTransferPlan, ...], ) -> tuple[ArgumentTransferPlan, ...]: + """Select derived arguments that need scoped-origin invocation and whose producer exists in the active module.""" return tuple( argument for argument in arguments @@ -754,6 +843,7 @@ def _scoped_derived_arguments( @staticmethod def _derived_argument_uses_access(argument: ArgumentTransferPlan, access: DerivedActualAccess) -> bool: + """Return whether any compatible derived-call case uses the requested actual-access mechanism.""" return any( case.access is access for case in argument.derived_call.cases @@ -766,6 +856,7 @@ def _derived_call_invocation( scoped: tuple[ArgumentTransferPlan, ...], call_body: tuple, ) -> tuple[FortranIf, tuple[FortranFunction, ...]]: + """Build the guarded native invocation, using nested internal procedures only when scoped origins are required.""" ready = self._derived_ready_condition(arguments) if scoped: return ( @@ -778,6 +869,7 @@ def _derived_call_invocation( return FortranIf(CodeExpression(ready), body=call_body), () def _derived_argument_initializers(self, argument: ArgumentTransferPlan) -> tuple: + """Initialize bridge-local state for one derived carrier before its completed action is dispatched.""" name = argument.bridge.native_name.lower() nodes = [ FortranAssignment(f"bound_{name}_status", CodeExpression("0_c_int")), @@ -827,6 +919,7 @@ def _derived_argument_preparation(self, argument: ArgumentTransferPlan) -> Fortr ) def _derived_direct_preparation(self, argument: ArgumentTransferPlan) -> tuple: + """Build preparation for the completed direct scalar-derived handoff case.""" if argument.polymorphic is not None: return self._polymorphic_direct_preparation(argument) name = argument.bridge.native_name.lower() @@ -874,6 +967,7 @@ def _polymorphic_direct_preparation(self, argument: ArgumentTransferPlan) -> tup ) def _derived_scoped_preparation(self, argument: ArgumentTransferPlan) -> tuple: + """Build preparation for the completed scoped-address scalar-derived handoff case.""" name = argument.bridge.native_name.lower() return ( FortranIf( @@ -889,6 +983,7 @@ def _derived_scoped_preparation(self, argument: ArgumentTransferPlan) -> tuple: ) def _derived_allocatable_holder_preparation(self, argument: ArgumentTransferPlan) -> tuple: + """Build preparation for a completed allocatable-holder scalar-derived handoff.""" name = argument.bridge.native_name.lower() holder = f"{name}_allocatable_holder" return ( @@ -909,6 +1004,7 @@ def _derived_allocatable_holder_preparation(self, argument: ArgumentTransferPlan ) def _derived_allocatable_payload_preparation(self, argument: ArgumentTransferPlan) -> tuple: + """Build preparation for an allocatable-holder payload after its carrier has been acquired.""" if argument.derived_call.dummy_category in { DerivedDummyCategory.ALLOCATABLE, DerivedDummyCategory.ALLOCATABLE_TARGET, @@ -924,6 +1020,7 @@ def _derived_allocatable_payload_preparation(self, argument: ArgumentTransferPla ) def _derived_pointer_holder_preparation(self, argument: ArgumentTransferPlan) -> tuple: + """Build preparation for a completed pointer-holder scalar-derived handoff.""" name = argument.bridge.native_name.lower() holder = f"{name}_pointer_holder" return ( @@ -945,6 +1042,7 @@ def _derived_pointer_holder_preparation(self, argument: ArgumentTransferPlan) -> ) def _derived_pointer_payload_preparation(self, argument: ArgumentTransferPlan) -> tuple: + """Build preparation for a pointer-holder payload after its carrier has been acquired.""" if argument.derived_call.dummy_category is DerivedDummyCategory.POINTER: return () name = argument.bridge.native_name.lower() @@ -957,12 +1055,15 @@ def _derived_pointer_payload_preparation(self, argument: ArgumentTransferPlan) - ) def _derived_allocatable_transaction_preparation(self, argument: ArgumentTransferPlan) -> tuple: + """Build the transaction setup selected for an allocatable derived carrier.""" return self._derived_transaction_operation_preparation(argument) def _derived_pointer_transaction_preparation(self, argument: ArgumentTransferPlan) -> tuple: + """Build the transaction setup selected for a pointer derived carrier.""" return self._derived_transaction_operation_preparation(argument) def _derived_transaction_operation_preparation(self, argument: ArgumentTransferPlan) -> tuple: + """Dispatch one derived transaction operation only by its completed action.""" name = argument.bridge.native_name.lower() return ( FortranIf( @@ -986,6 +1087,7 @@ def _derived_transaction_acquisition( arguments: tuple[ArgumentTransferPlan, ...], index: int, ) -> FortranIf: + """Build acquisition nodes for one derived argument at the current native-call position.""" argument = arguments[index] name = argument.bridge.native_name.lower() acquisition = FortranSelectCase( @@ -1019,6 +1121,7 @@ def _one_derived_transaction_acquisition( *, allocatable: bool, ) -> tuple: + """Build the guarded acquisition for one derived argument and preserve earlier failure state.""" name = argument.bridge.native_name.lower() holder = f"{name}_{'allocatable' if allocatable else 'pointer'}_holder" return ( @@ -1039,6 +1142,7 @@ def _one_derived_transaction_acquisition( ) def _derived_transaction_restoration(self, argument: ArgumentTransferPlan) -> FortranIf: + """Build the restoration selected for one derived carrier after the native call.""" name = argument.bridge.native_name.lower() return FortranIf( CodeExpression(f"{name}_acquired"), @@ -1060,6 +1164,7 @@ def _derived_scoped_internal_procedures( scoped: tuple[ArgumentTransferPlan, ...], call_body: tuple, ) -> tuple[FortranFunction, ...]: + """Build nested procedures that serialize scoped-origin consumers around the native invocation.""" procedures = [] for index, argument in enumerate(scoped): name = argument.bridge.native_name.lower() @@ -1150,6 +1255,7 @@ def _derived_scoped_step_body( return body def _derived_pointer_call_initializers(self, plan: FunctionPlan) -> tuple: + """Initialize temporary pointer-call associations before invoking the native procedure.""" return tuple( node for argument in self._derived_arguments(plan) @@ -1158,6 +1264,7 @@ def _derived_pointer_call_initializers(self, plan: FunctionPlan) -> tuple: ) def _one_derived_pointer_call_initializer(self, argument: ArgumentTransferPlan) -> tuple: + """Build initialization for one derived pointer actual selected by its completed call action.""" name = argument.bridge.native_name.lower() holder = f"{name}_pointer_holder%value" associate_holder = FortranIf( @@ -1179,6 +1286,7 @@ def _one_derived_pointer_call_initializer(self, argument: ArgumentTransferPlan) ) def _derived_pointer_call_finalizers(self, plan: FunctionPlan) -> tuple: + """Build final pointer-call cleanup in native argument order after invocation.""" return tuple( node for argument in self._derived_arguments(plan) @@ -1187,6 +1295,7 @@ def _derived_pointer_call_finalizers(self, plan: FunctionPlan) -> tuple: ) def _one_derived_pointer_call_finalizer(self, argument: ArgumentTransferPlan) -> tuple: + """Build finalization for one derived pointer actual selected by its completed call action.""" name = argument.bridge.native_name.lower() return ( FortranIf( @@ -1207,6 +1316,7 @@ def _one_derived_pointer_call_finalizer(self, argument: ArgumentTransferPlan) -> ) def _derived_argument_output_and_cleanup(self, argument: ArgumentTransferPlan) -> tuple: + """Build output projection and cleanup for one derived argument after restoration.""" name = argument.bridge.native_name.lower() nodes = [] if argument.bridge.descriptor_output_role is not None: @@ -1227,6 +1337,7 @@ def _derived_argument_output_and_cleanup(self, argument: ArgumentTransferPlan) - return tuple(nodes) def _derived_argument_output_finalizer(self, argument: ArgumentTransferPlan) -> FortranIf: + """Build the completed output action for one derived carrier.""" name = argument.bridge.native_name.lower() return FortranIf( CodeExpression(f"bound_{name}_access == 3_c_int"), @@ -1247,6 +1358,7 @@ def _derived_argument_output_finalizer(self, argument: ArgumentTransferPlan) -> @staticmethod def _derived_holder_output_nodes(name: str, *, allocatable: bool) -> tuple: + """Build C-address output nodes for a derived holder that survived the native call.""" holder = f"{name}_{'allocatable' if allocatable else 'pointer'}_holder" inquiry = "allocated" if allocatable else "associated" return ( @@ -1260,18 +1372,22 @@ def _derived_holder_output_nodes(name: str, *, allocatable: bool) -> tuple: @staticmethod def _derived_ready_condition(arguments: tuple[ArgumentTransferPlan, ...]) -> str: + """Return the combined success condition that guards the derived native invocation.""" return "prik_derived_ready" if arguments else ".true." @staticmethod def _derived_status_parameter(argument: ArgumentTransferPlan) -> str: + """Return the bridge status parameter shared by derived transaction helpers.""" return f"bound_{argument.bridge.native_name.lower()}_status" @staticmethod def _derived_step_name(index: int) -> str: + """Return a deterministic nested procedure name for one scoped-origin invocation step.""" return f"prik_derived_step_{index}" @staticmethod def _derived_consumer_name(index: int) -> str: + """Return the deterministic consumer-procedure name for one scoped derived argument.""" return f"prik_derived_consumer_{index}" def _lower_result( @@ -1626,6 +1742,8 @@ def _lower_module_getter(self, plan: ModuleVariablePlan) -> tuple[FortranFunctio return self._lower_module_getter_constant_value(plan) case ModuleGetterAction.NATIVE_CONSTANT_VALUE: return self._lower_module_getter_direct_value(plan) + case ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE: + return self._lower_module_getter_constant_array_value(plan) case ModuleGetterAction.DIRECT_VALUE: return self._lower_module_getter_direct_value(plan) case ModuleGetterAction.NULLABLE_SNAPSHOT: @@ -1690,6 +1808,7 @@ def _lower_module_derived_presence(self, plan: ModuleVariablePlan) -> tuple[Fort # Runtime-selected scalar-derived module origins. def _derived_origin_variables(self, plan: ModulePlan) -> tuple[ModuleVariablePlan, ...]: + """Return module variables that provide a completed scalar-derived origin, preserving module order.""" return tuple(variable for variable in self._variables(plan) if variable.derived is not None) def _scoped_origin_type_identities(self, plan: ModulePlan) -> frozenset[tuple[str, str]]: @@ -1720,6 +1839,7 @@ def _derived_origin_procedures(self, variable: ModuleVariablePlan) -> tuple[Fort ) def _derived_origin_presence_procedure(self, variable: ModuleVariablePlan) -> FortranFunction: + """Build the native-presence inquiry for one scalar-derived module origin.""" storage = variable.derived.handoff.storage inquiry = "associated" if storage is DerivedObjectStorage.MODULE_POINTER else "allocated" name = self._derived_origin_bridge_name(variable, "present") @@ -1732,6 +1852,7 @@ def _derived_origin_presence_procedure(self, variable: ModuleVariablePlan) -> Fo ) def _derived_origin_address_procedure(self, variable: ModuleVariablePlan) -> FortranFunction: + """Build the address-export bridge procedure for one scalar-derived module origin.""" storage = variable.derived.handoff.storage native = self._native_variable_name(variable) name = self._derived_origin_bridge_name(variable, "address") @@ -1754,6 +1875,7 @@ def _derived_origin_address_procedure(self, variable: ModuleVariablePlan) -> For ) def _derived_origin_scoped_procedure(self, variable: ModuleVariablePlan) -> FortranFunction: + """Build the callback-based scoped-origin procedure for one scalar-derived module variable.""" name = self._derived_origin_bridge_name(variable, "scoped") native = self._native_variable_name(variable) storage = variable.derived.handoff.storage @@ -1809,6 +1931,7 @@ def _derived_origin_scoped_procedure(self, variable: ModuleVariablePlan) -> Fort ) def _derived_origin_checkout_procedure(self, variable: ModuleVariablePlan) -> FortranFunction: + """Dispatch checkout generation from the origin's completed native storage category.""" storage = variable.derived.handoff.storage return ( self._derived_origin_allocatable_checkout(variable) @@ -1817,6 +1940,7 @@ def _derived_origin_checkout_procedure(self, variable: ModuleVariablePlan) -> Fo ) def _derived_origin_allocatable_checkout(self, variable: ModuleVariablePlan) -> FortranFunction: + """Build checkout that moves a module allocatable into a bridge-owned holder.""" name = self._derived_origin_bridge_name(variable, "checkout") holder_type = self._allocatable_holder_type_name(variable.derived.handoff.backend_symbol) return FortranFunction( @@ -1851,6 +1975,7 @@ def _derived_origin_allocatable_checkout(self, variable: ModuleVariablePlan) -> ) def _derived_origin_pointer_checkout(self, variable: ModuleVariablePlan) -> FortranFunction: + """Build checkout that transfers a module pointer association into a bridge-owned holder.""" name = self._derived_origin_bridge_name(variable, "checkout") holder_type = self._pointer_holder_type_name(variable.derived.handoff.backend_symbol) native = self._native_variable_name(variable) @@ -1885,6 +2010,7 @@ def _derived_origin_pointer_checkout(self, variable: ModuleVariablePlan) -> Fort ) def _derived_origin_restore_procedure(self, variable: ModuleVariablePlan) -> FortranFunction: + """Dispatch restore generation from the origin's completed native storage category.""" storage = variable.derived.handoff.storage return ( self._derived_origin_allocatable_restore(variable) @@ -1893,6 +2019,7 @@ def _derived_origin_restore_procedure(self, variable: ModuleVariablePlan) -> For ) def _derived_origin_allocatable_restore(self, variable: ModuleVariablePlan) -> FortranFunction: + """Build restore that moves an allocatable holder payload back to its module variable.""" name = self._derived_origin_bridge_name(variable, "restore") holder_type = self._allocatable_holder_type_name(variable.derived.handoff.backend_symbol) return FortranFunction( @@ -1923,6 +2050,7 @@ def _derived_origin_allocatable_restore(self, variable: ModuleVariablePlan) -> F ) def _derived_origin_pointer_restore(self, variable: ModuleVariablePlan) -> FortranFunction: + """Build restore that re-associates a pointer holder payload with its module variable.""" name = self._derived_origin_bridge_name(variable, "restore") holder_type = self._pointer_holder_type_name(variable.derived.handoff.backend_symbol) native = self._native_variable_name(variable) @@ -1954,6 +2082,7 @@ def _derived_origin_pointer_restore(self, variable: ModuleVariablePlan) -> Fortr @staticmethod def _derived_origin_supports(variable: ModuleVariablePlan, operation: str) -> bool: + """Return whether the completed module-origin handoff declares the requested bridge operation.""" storage = variable.derived.handoff.storage support = { DerivedObjectStorage.MODULE_PROXY: {"scoped"}, @@ -1971,9 +2100,11 @@ def _derived_origin_supports(variable: ModuleVariablePlan, operation: str) -> bo @staticmethod def _derived_origin_symbol(variable: ModuleVariablePlan) -> str: + """Return the collision-safe symbol fragment for one scalar-derived module origin.""" return NativeSymbolNames.compact(variable.owner_path, variable.symbol_name) def _derived_origin_bridge_name(self, variable: ModuleVariablePlan, operation: str) -> str: + """Return the exported bridge symbol for one derived-origin operation.""" return f"bind_c_prik_origin_{self._derived_origin_symbol(variable)}_{operation}" def _lower_module_getter_derived_value_copy( @@ -2318,6 +2449,7 @@ def _module_native_array_nullify_operation(self, plan: ModuleVariablePlan) -> Fo ) def _module_native_array_presence_expression(self, plan: ModuleVariablePlan) -> str: + """Return the presence inquiry selected by a module native-array handle's descriptor kind.""" handle = plan.native_array_handle if handle is None: raise ValueError(f"Module handle {plan.owner_path!r} has no descriptor kind") @@ -2348,6 +2480,70 @@ def _lower_module_getter_direct_value(self, plan: ModuleVariablePlan) -> tuple[F ), ) + def _lower_module_getter_constant_array_value(self, plan: ModuleVariablePlan) -> tuple[FortranFunction, ...]: + """Copy one compiler-owned parameter array into persistent bridge storage. + + The binding copies this temporary native buffer into its one + Python-owned read-only NumPy allocation during module initialization. + A Fortran parameter itself has no addressable storage to expose. + """ + array = plan.array + if array is None or array.rank is None or array.rank <= 0: + raise ValueError(f"Module parameter array {plan.owner_path!r} has no fixed array plan") + scalar_type = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) + name = self._module_bridge_getter_name(plan) + native = self._native_variable_name(plan) + snapshot = "parameter_snapshot" + extents = tuple(f"extent_{axis}" for axis in range(array.rank)) + return ( + FortranFunction( + name=name, + parameters=tuple( + FortranParameter(extent, "integer(c_int64_t)", ("intent(out)",)) for extent in extents + ), + result_name="result", + result_type="type(c_ptr)", + bind_name=name, + uses=(FortranUse("iso_c_binding", ("c_int", "c_int64_t", "c_loc", "c_null_ptr", "c_ptr")),), + declarations=( + FortranDeclaration( + snapshot, + scalar_type.fortran_spelling, + ("allocatable", "target", "save", self._array_dimension_attribute(array.rank)), + ), + FortranDeclaration("allocation_status", "integer(c_int)"), + ), + body=( + FortranAssignment("result", CodeExpression("c_null_ptr")), + FortranAssignment("allocation_status", CodeExpression("0_c_int")), + FortranIf( + CodeExpression(f".not. allocated({snapshot})"), + body=( + FortranAllocate( + snapshot, + tuple(CodeExpression(f"size({native}, {axis + 1})") for axis in range(array.rank)), + status="allocation_status", + ), + ), + ), + FortranIf( + CodeExpression("allocation_status == 0_c_int"), + body=( + FortranAssignment(snapshot, CodeExpression(native)), + *( + FortranAssignment( + extent, + CodeExpression(f"int(size({native}, {axis + 1}), c_int64_t)"), + ) + for axis, extent in enumerate(extents) + ), + FortranAssignment("result", CodeExpression(f"c_loc({snapshot})")), + ), + ), + ), + ), + ) + def _lower_module_getter_borrowed_array_view( self, plan: ModuleVariablePlan, @@ -2516,6 +2712,7 @@ def _lower_derived_argument( @staticmethod def _is_character_buffer_argument(plan: ArgumentTransferPlan) -> bool: + """Return whether an argument uses the completed character-buffer handoff mode.""" return ( plan.object_kind is ObjectKind.STRING and plan.bridge.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER ) @@ -2723,6 +2920,7 @@ def _lower_argument_descriptor(self, plan: ArgumentTransferPlan) -> tuple[Fortra ) def _parameter(self, plan: ArgumentTransferPlan, attributes: tuple[str, ...]) -> FortranParameter: + """Return one bridge ABI parameter from its completed transfer plan.""" scalar_type = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) return FortranParameter(plan.bridge.native_name.lower(), scalar_type.fortran_spelling, attributes) @@ -2940,10 +3138,12 @@ def _derived_native_parameter( @staticmethod def _derived_optional_parameter_name(argument: ArgumentTransferPlan) -> str: + """Return the local optional-presence parameter name for one derived argument.""" return f"prik_optional_{argument.bridge.native_name.lower()}" @staticmethod def _derived_optional_step_name(index: int) -> str: + """Return the deterministic nested-procedure name for one optional derived dispatch case.""" return f"prik_derived_optional_step_{index}" def _optional_call_tree( @@ -2976,6 +3176,7 @@ def _native_invocation( result_name: str | None, replacements: dict[str, str], ) -> FortranAssignment | FortranCall | FortranPointerAssignment: + """Build the native procedure call from the ordered completed call-slot plan.""" if plan.bridge.native_invocation is NativeInvocationKind.DEFINED_OPERATOR: return self._defined_operator_invocation(plan, present, result_name, replacements) if plan.bridge.native_invocation is NativeInvocationKind.DEFINED_ASSIGNMENT: @@ -3089,6 +3290,7 @@ def _native_invocation_target( @staticmethod def _is_pointer_derived_holder_result(result: ResultPlan | None) -> bool: + """Return whether a result uses the completed pointer-holder derived storage mode.""" return bool( result is not None and result.object_kind is ObjectKind.DERIVED_TYPE @@ -3104,6 +3306,7 @@ def _native_arguments( *, excluded_position: int | None = None, ) -> tuple[CodeExpression, ...]: + """Return native call expressions in planned ABI order without recomputing argument policy.""" expressions = dict(self._visible_native_argument_entries(plan, present, replacements)) expressions.update( (slot.native_position, CodeExpression(self._literal_expression(slot.literal_value))) @@ -3202,6 +3405,7 @@ def _hidden_native_result_entries( return tuple(entries) def _native_argument_expression(self, plan: ArgumentTransferPlan) -> str: + """Return the native actual expression selected by one completed call slot.""" name = plan.bridge.native_name.lower() if plan.callback is not None: return plan.callback.adapter_symbol @@ -3220,9 +3424,12 @@ def _native_argument_expression(self, plan: ArgumentTransferPlan) -> str: return name if plan.bridge.optional_mode in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR}: return f"{name}_descriptor" + if plan.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: + return f"{name}_native" return name def _presence_condition(self, plan: ArgumentTransferPlan) -> str: + """Return the local C-pointer association condition for one nullable bridge argument.""" name = plan.bridge.native_name.lower() if plan.derived_call is not None: return f"bound_{name}_access /= 0_c_int" @@ -3307,6 +3514,19 @@ def _prepare_present_representation_copy( plan: ArgumentTransferPlan, ) -> tuple[FortranCall | FortranAssignment | FortranIf, ...]: """Copy only when completed policy requires a different native representation.""" + if plan.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY: + nodes: list[FortranCall | FortranAssignment | FortranIf] = list(self._array_pointer_initializer_nodes(plan)) + if plan.array_copy_in: + nodes.append( + FortranAssignment( + self._logical_array_native_name(plan), + CodeExpression(self._array_boundary_argument_expression(plan)), + ) + ) + return tuple(nodes) + if plan.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: + name = plan.bridge.native_name.lower() + return (FortranAssignment(f"{name}_native", CodeExpression(name)),) if plan.bridge.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER: return self._string_value_initializer_nodes(plan) if self._is_derived_value_copy(plan): @@ -3382,6 +3602,57 @@ def _optional_argument_declarations( declarations.append(FortranDeclaration(f"{name}_output", scalar_type.fortran_spelling, ("pointer",))) return tuple(declarations) + def _logical_scalar_argument_declarations( + self, + plan: FunctionPlan, + ) -> tuple[FortranDeclaration, ...]: + """Declare exact-kind native locals selected by scalar logical policy.""" + declarations = [] + for argument in plan.arguments: + if argument.scalar_logical_abi is not ScalarLogicalABI.NATIVE_KIND_COPY: + continue + if not argument.scalar_native_type: + raise ValueError(f"Logical argument {argument.owner_path!r} has no native type spelling") + declarations.append( + FortranDeclaration( + f"{argument.bridge.native_name.lower()}_native", + argument.scalar_native_type, + ) + ) + return tuple(declarations) + + def _logical_scalar_argument_initializers( + self, + plan: FunctionPlan, + ) -> tuple[FortranAssignment, ...]: + """Copy required C Boolean values into their exact native kinds.""" + return tuple( + FortranAssignment( + f"{argument.bridge.native_name.lower()}_native", + CodeExpression(argument.bridge.native_name.lower()), + ) + for argument in plan.arguments + if argument.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY + and argument.bridge.optional_mode is OptionalMode.REQUIRED + ) + + def _logical_scalar_argument_finalizers( + self, + plan: FunctionPlan, + ) -> tuple[FortranAssignment | FortranIf, ...]: + """Copy mutable exact-kind logical values back to C Boolean storage.""" + nodes = [] + for argument in plan.arguments: + if argument.scalar_logical_abi is not ScalarLogicalABI.NATIVE_KIND_COPY or not argument.mutates_native: + continue + name = argument.bridge.native_name.lower() + assignment = FortranAssignment(name, CodeExpression(f"{name}_native")) + if argument.bridge.optional_mode is OptionalMode.REQUIRED: + nodes.append(assignment) + else: + nodes.append(FortranIf(CodeExpression(self._presence_condition(argument)), body=(assignment,))) + return tuple(nodes) + def _opaque_address_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, ...]: """Return typed pointer locals for required opaque scalar addresses.""" return tuple( @@ -3474,7 +3745,7 @@ def _is_derived_value_copy(argument: ArgumentTransferPlan) -> bool: # Ordinary-array bridge storage. def _array_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, ...]: - """Declare typed pointer views for ordinary array buffers.""" + """Declare boundary views and policy-selected native logical storage.""" declarations = [] for argument in plan.arguments: if argument.bridge.handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER: @@ -3504,6 +3775,16 @@ def _array_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, . ("pointer", self._array_dimension_attribute(array.rank)), ) ) + if argument.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY: + if not argument.array_native_type: + raise ValueError(f"Logical array {argument.owner_path!r} has no native type spelling") + declarations.append( + FortranDeclaration( + self._logical_array_native_name(argument), + argument.array_native_type, + (self._logical_array_dimension_attribute(argument),), + ) + ) if argument.array_writeback_abi is ArrayWritebackABI.LOGICAL_LOW_BIT_INT8: declarations.append( FortranDeclaration( @@ -3514,6 +3795,64 @@ def _array_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, . ) return tuple(declarations) + def _logical_array_argument_initializers( + self, + plan: FunctionPlan, + ) -> tuple[FortranAssignment, ...]: + """Copy required one-byte Boolean inputs into exact-kind native arrays.""" + return tuple( + FortranAssignment( + self._logical_array_native_name(argument), + CodeExpression(self._array_boundary_argument_expression(argument)), + ) + for argument in plan.arguments + if argument.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY + and argument.array_copy_in + and argument.bridge.optional_mode is OptionalMode.REQUIRED + ) + + def _logical_array_argument_finalizers( + self, + plan: FunctionPlan, + ) -> tuple[FortranAssignment | FortranIf, ...]: + """Copy exact-kind logical outputs into canonical one-byte storage. + + ``merge`` converts truth values while assigning them to the original + ``logical(c_bool)`` view, so copy-out and canonicalization share one + array traversal. Optional buffers are written only when present. + """ + finalizers = [] + for argument in plan.arguments: + if argument.array_logical_abi is not ArrayLogicalABI.NATIVE_KIND_COPY or not argument.array_copy_out: + continue + target = self._array_boundary_argument_expression(argument) + native = self._logical_array_native_name(argument) + assignment = FortranAssignment( + target, + CodeExpression(f"merge(.true._c_bool, .false._c_bool, {native})"), + ) + if argument.bridge.optional_mode is OptionalMode.REQUIRED: + finalizers.append(assignment) + else: + finalizers.append(FortranIf(CodeExpression(self._presence_condition(argument)), body=(assignment,))) + return tuple(finalizers) + + @staticmethod + def _logical_array_native_name(argument: ArgumentTransferPlan) -> str: + """Return the bridge-local exact-kind array name for ``argument``.""" + return f"{argument.bridge.native_name.lower()}_native" + + def _logical_array_dimension_attribute(self, argument: ArgumentTransferPlan) -> str: + """Render automatic-array extents in the completed native orientation.""" + array = argument.array + if array is None or array.rank is None: + raise ValueError(f"Logical array {argument.owner_path!r} requires a concrete rank") + name = argument.bridge.native_name.lower() + extents = [f"{name}_extent_{axis}" for axis in range(array.rank)] + if array.native_order == "ORDER_C": + extents.reverse() + return f"dimension({', '.join(extents)})" + def _array_writeback_finalizers( self, plan: FunctionPlan, @@ -3561,6 +3900,7 @@ def _logical_array_writeback_for_rank( argument: ArgumentTransferPlan, rank: int, ) -> tuple[FortranCall | FortranAssignment, ...]: + """Return logical-array writeback nodes for one rank using the completed ABI conversion action.""" name = argument.bridge.native_name.lower() byte_pointer = self._logical_array_byte_pointer_name(argument) byte_count = " * ".join(f"{name}_extent_{axis}" for axis in range(rank)) @@ -3581,6 +3921,7 @@ def _logical_array_writeback_for_rank( @staticmethod def _logical_array_byte_pointer_name(argument: ArgumentTransferPlan) -> str: + """Return the bridge-local byte-pointer name for one logical-array rank conversion.""" return f"{argument.bridge.native_name.lower()}_logical_bytes" def _array_initializers(self, plan: FunctionPlan) -> tuple[FortranCall | FortranIf, ...]: @@ -3623,7 +3964,7 @@ def _raw_array_pointer_initializer( array = argument.array if array is None or array.rank is None: raise ValueError(f"Raw array address {argument.owner_path!r} requires a concrete rank") - shape = list(self._array_shape_from_roles(array, plan.arguments)) + shape = list(self._array_shape_from_roles(array, plan)) if array.native_order == "ORDER_C": shape.reverse() name = argument.bridge.native_name.lower() @@ -3731,7 +4072,13 @@ def _array_pointer_name(self, argument: ArgumentTransferPlan) -> str: return f"{name}_base" if argument.array is not None and argument.array.contiguous is False else name def _array_native_argument_expression(self, argument: ArgumentTransferPlan) -> str: - """Pass a dense pointer or the explicitly planned positive-stride slice.""" + """Pass exact-kind logical storage or the planned boundary array view.""" + if argument.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY: + return self._logical_array_native_name(argument) + return self._array_boundary_argument_expression(argument) + + def _array_boundary_argument_expression(self, argument: ArgumentTransferPlan) -> str: + """Return the dense pointer or planned positive-stride boundary view.""" array = argument.array if array is None: raise ValueError(f"Array argument {argument.owner_path!r} has no handoff spec") @@ -3941,6 +4288,7 @@ def _lower_argument_string_copyback( ) def _descriptor_initializers(self, plan: FunctionPlan) -> tuple[FortranPointerAssignment, ...]: + """Initialize pointer descriptors required by ordinary nullable descriptor arguments before call preparation.""" return tuple( FortranPointerAssignment( f"{argument.bridge.native_name.lower()}_descriptor", @@ -4341,11 +4689,40 @@ def _direct_array_result_declarations( FortranDeclaration("result_copy", element_type, ("pointer",)), ) copy_type = "character(kind=c_char)" if result.datatype_family is DatatypeFamily.STRING else element_type + if "bridge" in result.array.extent_evaluation: + return ( + FortranDeclaration( + "result_value", + element_type, + ("allocatable", self._array_dimension_attribute(result.array.rank)), + ), + FortranDeclaration("result_copy", copy_type, ("pointer", "dimension(:)")), + ) return ( FortranDeclaration("result_value", element_type, (f"dimension({', '.join(shape)})",)), FortranDeclaration("result_copy", copy_type, ("pointer", "dimension(:)")), ) + def _direct_array_result_initializers( + self, + plan: FunctionPlan, + ) -> tuple[FortranAllocate, ...]: + """Allocate a native-dependent result from its already evaluated ABI extents.""" + result = self._direct_result(plan) + if ( + result is None + or result.object_kind is not ObjectKind.NUMPY_ARRAY + or result.array is None + or "bridge" not in result.array.extent_evaluation + or self._is_scalar_storage_array(result.array) + ): + return () + shape = list(self._array_result_shape(plan, result)) + for axis, evaluation in enumerate(result.array.extent_evaluation): + if evaluation == "bridge": + shape[axis] = self._declaration_extent_result_name(result, axis) + return (FortranAllocate(f"result_value({', '.join(shape)})"),) + def _direct_result_finalizers( self, plan: FunctionPlan, @@ -4501,6 +4878,7 @@ def _owned_direct_array_result_collector(self, result: ResultPlan) -> FortranFun def _derived_direct_result_finalizers( result: ResultPlan, ) -> tuple[FortranAssignment | FortranCall | FortranIf, ...]: + """Build direct-derived result transfer nodes from the completed holder storage category.""" if result.derived.storage is DerivedObjectStorage.ALLOCATABLE_HOLDER: return ( FortranCall( @@ -4526,6 +4904,15 @@ def _representation_copy_output_declarations( slot: NativeCallSlotPlan, ) -> tuple[FortranDeclaration, ...]: """Declare storage only for one justified representation-copy output.""" + if slot.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: + if not slot.scalar_native_type: + raise ValueError(f"Logical output {slot.owner_path!r} has no native type spelling") + return ( + FortranDeclaration( + f"{slot.native_name.lower()}_value", + slot.scalar_native_type, + ), + ) if slot.object_kind is ObjectKind.DERIVED_TYPE: if slot.derived is None: raise ValueError(f"Derived output {slot.owner_path!r} has no handoff plan") @@ -4770,6 +5157,9 @@ def _lower_native_output_representation_copy( slot: NativeCallSlotPlan, ) -> tuple[FortranAssignment | FortranIf, ...]: """Copy one native output only through the explicit policy permission.""" + if slot.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: + name = slot.native_name.lower() + return (FortranAssignment(name, CodeExpression(f"{name}_value")),) if slot.object_kind is ObjectKind.NUMPY_ARRAY: if slot.array is None: raise ValueError(f"Array output {slot.owner_path!r} has no shape plan") @@ -4827,7 +5217,11 @@ def _fixed_array_copy_nodes( return ( FortranAssignment( target_name, - CodeExpression(f"c_malloc(max(1_c_size_t, c_sizeof({value_name})))"), + CodeExpression( + "c_malloc(max(1_c_size_t, " + f"size({value_name}, kind=c_size_t) * " + f"storage_size({value_name}, kind=c_size_t) / 8_c_size_t))" + ), ), FortranIf( CodeExpression(f"c_associated({target_name})"), @@ -4972,6 +5366,8 @@ def _fixed_string_copy_nodes( def _native_output_value_name(self, slot: NativeCallSlotPlan) -> str: """Return the native-call expression selected for one output slot.""" name = slot.native_name.lower() + if slot.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: + return f"{name}_value" if slot.scalar_descriptor is not None: return f"{name}_value" if self._is_owned_native_array_slot(slot): @@ -4989,16 +5385,19 @@ def _native_output_value_name(self, slot: NativeCallSlotPlan) -> str: ) def _string_output_length(self, slot: NativeCallSlotPlan) -> int: + """Return a validated fixed length for a hidden native string output; reject absent or non-positive lengths.""" if slot.character_length is None or slot.character_length <= 0: raise ValueError(f"String output {slot.owner_path!r} is missing a fixed character length") return slot.character_length def _string_result_length(self, result: ResultPlan) -> int: + """Return a validated fixed length for a direct native string result; reject absent or non-positive lengths.""" if result.character_length is None or result.character_length <= 0: raise ValueError(f"String result {result.owner_path!r} is missing a fixed character length") return result.character_length def _native_direct_result_name(self, plan: FunctionPlan, result_name: str | None) -> str | None: + """Return the native call target selected for the procedure's completed direct result ABI.""" result = self._direct_result(plan) if result is None: return result_name @@ -5042,6 +5441,7 @@ def _native_scalar_direct_result_name(result: ResultPlan, result_name: str | Non raise ValueError(f"Scalar result {result.owner_path!r} has no completed direct-result ABI") def _bridge_result_type(self, plan: FunctionPlan, result: ResultPlan | None = None) -> str: + """Return the C-interoperable bridge result spelling selected by the completed direct-result plan.""" result = result or self._direct_result(plan) if result is None: raise ValueError(f"{plan.owner_path!r} native function has no result plan") @@ -5077,6 +5477,7 @@ def _uses_owned_direct_array_result_collector(self, plan: FunctionPlan) -> bool: @staticmethod def _owned_direct_array_result_collector_name() -> str: + """Return the fixed internal helper name for collecting maybe-unallocated owned array results.""" return "prik_collect_allocatable_array_result" @staticmethod @@ -5106,6 +5507,7 @@ def _native_module_uses(self, plan: ModulePlan) -> tuple[FortranUse, ...]: modules: dict[str, list[str]] = {} self._add_derived_module_uses(plan, modules) self._add_function_module_uses(plan, modules) + self._add_declaration_callable_module_uses(plan, modules) self._add_variable_module_uses(plan, modules) return tuple(FortranUse(module, tuple(dict.fromkeys(names))) for module, names in modules.items()) @@ -5119,7 +5521,6 @@ def _add_derived_module_uses(self, plan: ModulePlan, modules: dict[str, list[str def _add_function_module_uses(self, plan: ModulePlan, modules: dict[str, list[str]]) -> None: """Import module procedures, excluding direct type-bound invocation.""" for function in self._functions(plan): - self._add_callback_prototype_uses(function, modules) if ( function.bridge.native_module is not None and function.bridge.native_invocation is not NativeInvocationKind.PROCEDURE @@ -5133,21 +5534,20 @@ def _add_function_module_uses(self, plan: ModulePlan, modules: dict[str, list[st f"{self._native_function_name(function)} => {function.bridge.native_name}" ) - def _add_callback_prototype_uses( + def _add_declaration_callable_module_uses( self, - function: FunctionPlan, + plan: ModulePlan, modules: dict[str, list[str]], ) -> None: - """Import named prototypes selected by completed callback policy.""" - for argument in function.arguments: - callback = argument.callback - if ( - callback is not None - and callback.declaration_mode is ExternalDeclarationMode.EXPLICIT_INTERFACE - and callback.prototype_module is not None - ): - modules.setdefault(callback.prototype_module, []).append( - f"{self._callback_imported_prototype_symbol(callback)} => {callback.prototype_name}" + """Import module specification functions under their planned bridge names.""" + for function in self._functions(plan): + for declaration in function.declaration_callables: + if declaration.action is not DeclarationCallableAction.MODULE_IMPORT: + continue + if declaration.native_scope is None: + raise ValueError(f"Module declaration callable {declaration.owner_path!r} has no module") + modules.setdefault(declaration.native_scope, []).append( + f"{declaration.backend_symbol} => {declaration.native_name}" ) def _add_variable_module_uses(self, plan: ModulePlan, modules: dict[str, list[str]]) -> None: @@ -5184,6 +5584,7 @@ def _constructible_class_identities(plan: ModulePlan) -> set[tuple[str, str]]: } def _owned_derived_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Return derived identities required by direct-result ownership helpers.""" return { result.derived.type_identity for function in self._functions(plan) @@ -5195,6 +5596,7 @@ def _owned_derived_result_identities(self, plan: ModulePlan) -> set[tuple[str, s } def _owned_derived_module_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Return derived identities required by owned module-origin helpers.""" return { variable.derived.handoff.type_identity for variable in self._variables(plan) @@ -5208,6 +5610,7 @@ def _allocatable_holder_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) def _allocatable_holder_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Return identities whose direct results need allocatable holder definitions.""" return { result.derived.type_identity for function in self._functions(plan) @@ -5216,6 +5619,7 @@ def _allocatable_holder_result_identities(self, plan: ModulePlan) -> set[tuple[s } def _allocatable_holder_argument_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Return identities whose arguments need allocatable holder definitions.""" return { argument.derived.type_identity for function in self._functions(plan) @@ -5236,6 +5640,7 @@ def _pointer_holder_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...] return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) def _pointer_holder_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Return identities whose direct results need pointer holder definitions.""" return { result.derived.type_identity for function in self._functions(plan) @@ -5244,6 +5649,7 @@ def _pointer_holder_result_identities(self, plan: ModulePlan) -> set[tuple[str, } def _pointer_holder_argument_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: + """Return identities whose arguments need pointer holder definitions.""" return { argument.derived.type_identity for function in self._functions(plan) @@ -5259,10 +5665,12 @@ def _pointer_holder_argument_identities(self, plan: ModulePlan) -> set[tuple[str @staticmethod def _uses_allocatable_holder(argument: ArgumentTransferPlan) -> bool: + """Return whether the module plan requires the allocatable holder for one native derived identity.""" return FortranBridgeGenerator._uses_holder(argument, DerivedActualAccess.ALLOCATABLE_HOLDER) @staticmethod def _uses_pointer_holder(argument: ArgumentTransferPlan) -> bool: + """Return whether the module plan requires the pointer holder for one native derived identity.""" return FortranBridgeGenerator._uses_holder(argument, DerivedActualAccess.POINTER_HOLDER) @staticmethod @@ -5285,6 +5693,7 @@ def _derived_field_procedures(self, plan: ModulePlan) -> tuple[FortranFunction, ) def _direct_field_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: + """Return direct derived-field procedure builders in stable action order.""" return tuple( procedure for derived in self._derived_types(plan) @@ -5293,6 +5702,7 @@ def _direct_field_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFunc ) def _module_member_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: + """Return module-member procedure builders in stable action order.""" return tuple( procedure for variable in self._derived_member_proxy_variables(plan) @@ -5301,6 +5711,7 @@ def _module_member_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFun ) def _allocatable_holder_field_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: + """Return allocatable-holder field procedure builders in stable action order.""" return tuple( procedure for derived in self._allocatable_holder_field_types(plan) @@ -5309,6 +5720,7 @@ def _allocatable_holder_field_procedure_entries(self, plan: ModulePlan) -> tuple ) def _pointer_holder_field_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: + """Return pointer-holder field procedure builders in stable action order.""" return tuple( procedure for derived in self._pointer_holder_field_types(plan) @@ -5587,6 +5999,7 @@ def _module_string_member_setter( @staticmethod def _fixed_string_field_length(field: DerivedFieldPlan) -> int: + """Return a validated fixed length for a derived string field; reject missing lengths.""" length = field.character_length if length is None or length <= 0: raise ValueError(f"Fixed string field {field.owner_path!r} has no positive length") @@ -5657,6 +6070,7 @@ def _native_handle_field_procedure( raise ValueError(f"Unsupported field handle operation {operation!r} for {field.owner_path!r}") def _native_handle_field_state_procedure(self, owner, field, operation) -> FortranFunction: + """Build the state inquiry for one native-array-handle field.""" expression = self._native_handle_field_expression(owner, field) presence = self._native_handle_field_presence(field, expression) if operation is NativeArrayOperation.ALLOCATED: @@ -5680,6 +6094,7 @@ def _native_handle_field_state_procedure(self, owner, field, operation) -> Fortr ) def _native_handle_field_length_procedure(self, owner, field) -> FortranFunction: + """Build the element-count inquiry for one native-array-handle field.""" expression = self._native_handle_field_expression(owner, field) presence = self._native_handle_field_presence(field, expression) name = self._native_handle_field_bridge_name(owner, field, NativeArrayOperation.ELEMENT_LENGTH) @@ -5701,6 +6116,7 @@ def _native_handle_field_length_procedure(self, owner, field) -> FortranFunction ) def _native_handle_field_shape_procedure(self, owner, field) -> FortranFunction: + """Build the per-axis shape inquiry for one native-array-handle field.""" handle = field.native_array_handle if handle is None or handle.array.rank is None: raise ValueError(f"Native handle field {field.owner_path!r} has no shape rank") @@ -5731,6 +6147,7 @@ def _native_handle_field_shape_procedure(self, owner, field) -> FortranFunction: ) def _native_handle_field_descriptor_procedure(self, owner, field) -> FortranFunction: + """Build the descriptor-export procedure for one native-array-handle field.""" name = self._native_handle_field_bridge_name(owner, field, NativeArrayOperation.DESCRIPTOR) interface = self._native_handle_field_callback_interface_name(owner, field) return FortranFunction( @@ -5763,6 +6180,7 @@ def _native_handle_field_descriptor_procedure(self, owner, field) -> FortranFunc ) def _native_handle_field_resize_procedure(self, owner, field, operation) -> FortranFunction: + """Build the resize procedure selected for one native-array-handle field.""" handle = field.native_array_handle if handle is None or handle.array.rank is None: raise ValueError(f"Native handle field {field.owner_path!r} has no mutation rank") @@ -5822,6 +6240,7 @@ def _native_handle_field_associate_procedure(self, owner, field) -> FortranFunct ) def _native_handle_field_deallocate_procedure(self, owner, field) -> FortranFunction: + """Build the deallocation procedure selected for one native-array-handle field.""" expression = self._native_handle_field_expression(owner, field) name = self._native_handle_field_bridge_name(owner, field, NativeArrayOperation.DEALLOCATE) return FortranFunction( @@ -5840,6 +6259,7 @@ def _native_handle_field_deallocate_procedure(self, owner, field) -> FortranFunc ) def _native_handle_field_nullify_procedure(self, owner, field) -> FortranFunction: + """Build the pointer-nullification procedure selected for one native-array-handle field.""" expression = self._native_handle_field_expression(owner, field) name = self._native_handle_field_bridge_name(owner, field, NativeArrayOperation.NULLIFY) return FortranFunction( @@ -5856,21 +6276,25 @@ def _native_handle_field_nullify_procedure(self, owner, field) -> FortranFunctio @staticmethod def _native_handle_field_owner_parameters(owner) -> tuple[FortranParameter, ...]: + """Return owner-address ABI parameters for a native-array-handle field procedure.""" if isinstance(owner, DerivedTypePlan): return (FortranParameter("owner_address", "type(c_ptr)", ("value",)),) return () def _native_handle_field_owner_declarations(self, owner) -> tuple[FortranDeclaration, ...]: + """Return local owner declarations required by a native-array-handle field procedure.""" if isinstance(owner, DerivedTypePlan): return (self._derived_owner_declaration(owner),) return () def _native_handle_field_owner_body(self, owner) -> tuple[FortranCall, ...]: + """Return the owner association nodes shared by native-array-handle field procedures.""" if isinstance(owner, DerivedTypePlan): return (self._derived_owner_association(),) return () def _native_handle_field_expression(self, owner, field: DerivedFieldPlan) -> str: + """Return the native field expression after the owner association has been established.""" if isinstance(owner, DerivedTypePlan): return f"owner%{field.native_name}" variable, member = owner @@ -5878,6 +6302,7 @@ def _native_handle_field_expression(self, owner, field: DerivedFieldPlan) -> str @staticmethod def _native_handle_field_presence(field: DerivedFieldPlan, expression: str) -> str: + """Return the allocation or association inquiry selected for one native-array-handle field.""" handle = field.native_array_handle if handle is None: raise ValueError(f"Native handle field {field.owner_path!r} has no descriptor kind") @@ -5885,12 +6310,14 @@ def _native_handle_field_presence(field: DerivedFieldPlan, expression: str) -> s return f"{intrinsic}({expression})" def _native_handle_field_bridge_name(self, owner, field, operation) -> str: + """Return the exported bridge name for a native-array-handle field operation.""" if isinstance(owner, DerivedTypePlan): return self._derived_handle_bridge_name(owner, field, operation) variable, member = owner return self._module_member_handle_bridge_name(variable, member, operation) def _native_handle_field_callback_interface_name(self, owner, field) -> str: + """Return the callback-interface name used by one native-array-handle field.""" if isinstance(owner, DerivedTypePlan): return self._derived_handle_callback_interface_name(owner, field) variable, member = owner @@ -6029,16 +6456,18 @@ def _ordinary_array_field_association(self, field: DerivedFieldPlan) -> FortranC array = field.array if array is None or array.rank is None or len(array.shape) != array.rank: raise ValueError(f"Ordinary array field {field.owner_path!r} has no fixed shape") + shape = [render_declaration_extent(expression, {}, target="fortran") for expression in array.shape] return FortranCall( "c_f_pointer", ( CodeExpression("value_address"), CodeExpression("value"), - CodeExpression(f"[{', '.join(array.shape)}]"), + CodeExpression(f"[{', '.join(shape)}]"), ), ) def _direct_scalar_field_getter(self, derived: DerivedTypePlan, field: DerivedFieldPlan) -> FortranFunction: + """Build the direct-owner scalar field getter for one completed readable field.""" scalar = PrimitiveScalarTypeRegistry.type_for(field.semantic_type_name) name = self._derived_field_bridge_name(derived, field, "get") return FortranFunction( @@ -6059,6 +6488,7 @@ def _direct_scalar_field_setter( derived: DerivedTypePlan, field: DerivedFieldPlan, ) -> FortranFunction | None: + """Build the direct-owner scalar field setter only for a completed write-through field.""" if field.setter_action is not SetterAction.WRITE_THROUGH: return None scalar = PrimitiveScalarTypeRegistry.type_for(field.semantic_type_name) @@ -6079,6 +6509,7 @@ def _direct_scalar_field_setter( ) def _direct_nested_field_getter(self, derived: DerivedTypePlan, field: DerivedFieldPlan) -> FortranFunction: + """Build the direct-owner nested derived field getter for one completed field path.""" if field.derived is None: raise ValueError(f"Nested field {field.owner_path!r} has no handoff") name = self._derived_field_bridge_name(derived, field, "get") @@ -6100,6 +6531,7 @@ def _direct_nested_field_setter( derived: DerivedTypePlan, field: DerivedFieldPlan, ) -> FortranFunction | None: + """Build the direct-owner nested derived field setter only for a completed write-through path.""" if field.setter_action is not SetterAction.WRITE_THROUGH or field.derived is None: return None name = self._derived_field_bridge_name(derived, field, "set") @@ -6131,6 +6563,7 @@ def _module_scalar_member_getter( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> FortranFunction: + """Build the module-origin scalar member getter for one completed member path.""" scalar = PrimitiveScalarTypeRegistry.type_for(member.field.semantic_type_name) name = self._module_member_bridge_name(variable, member, "get") return FortranFunction( @@ -6146,6 +6579,7 @@ def _module_scalar_member_setter( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> FortranFunction | None: + """Build the module-origin scalar member setter only for a completed write-through path.""" field = member.field if field.setter_action is not SetterAction.WRITE_THROUGH: return None @@ -6164,6 +6598,7 @@ def _module_nested_member_setter( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> FortranFunction | None: + """Build the module-origin nested derived setter only for a completed write-through path.""" field = member.field if field.setter_action is not SetterAction.WRITE_THROUGH or field.derived is None: return None @@ -6187,6 +6622,7 @@ def _module_nested_member_setter( ) def _derived_owner_declaration(self, derived: DerivedTypePlan) -> FortranDeclaration: + """Return the local native-derived owner declaration used by direct field procedures.""" return FortranDeclaration( "owner", f"type({self._derived_native_alias(derived.backend_symbol)})", @@ -6195,16 +6631,20 @@ def _derived_owner_declaration(self, derived: DerivedTypePlan) -> FortranDeclara @staticmethod def _derived_owner_association() -> FortranCall: + """Return the shared C-address-to-owner association call for direct field procedures.""" return FortranCall("c_f_pointer", (CodeExpression("owner_address"), CodeExpression("owner"))) def _module_member_expression(self, variable: ModuleVariablePlan, member: DerivedMemberPathPlan) -> str: + """Return the native member-access expression for a completed module member path.""" return "%".join((self._native_variable_name(variable), *member.native_path)) @staticmethod def _derived_field_symbol(derived: DerivedTypePlan, field: DerivedFieldPlan) -> str: + """Return the normalized symbol fragment shared by all procedures for one derived field.""" return f"{derived.backend_symbol}_{field.name}".casefold() def _derived_field_bridge_name(self, derived: DerivedTypePlan, field: DerivedFieldPlan, action: str) -> str: + """Return the exported bridge symbol for one direct derived-field action.""" return f"bind_c_prik_field_{self._derived_field_symbol(derived, field)}_{action}" def _allocatable_holder_field_bridge_name( @@ -6213,6 +6653,7 @@ def _allocatable_holder_field_bridge_name( field: DerivedFieldPlan, action: str, ) -> str: + """Return the exported bridge symbol for one allocatable-holder field action.""" return f"bind_c_prik_allocatable_holder_field_{self._derived_field_symbol(derived, field)}_{action}" def _pointer_holder_field_bridge_name( @@ -6221,6 +6662,7 @@ def _pointer_holder_field_bridge_name( field: DerivedFieldPlan, action: str, ) -> str: + """Return the exported bridge symbol for one pointer-holder field action.""" return f"bind_c_prik_pointer_holder_field_{self._derived_field_symbol(derived, field)}_{action}" def _derived_field_callback_interface_name( @@ -6228,6 +6670,7 @@ def _derived_field_callback_interface_name( derived: DerivedTypePlan, field: DerivedFieldPlan, ) -> str: + """Return the consumer-interface name associated with one direct derived field.""" return f"prik_field_{self._derived_field_symbol(derived, field)}_consumer" def _derived_handle_bridge_name( @@ -6236,6 +6679,7 @@ def _derived_handle_bridge_name( field: DerivedFieldPlan, operation: NativeArrayOperation, ) -> str: + """Return the exported bridge symbol for one direct native-array-handle field operation.""" return f"bind_c_prik_field_handle_{self._derived_field_symbol(derived, field)}_{operation.value}" def _derived_handle_callback_interface_name( @@ -6243,10 +6687,12 @@ def _derived_handle_callback_interface_name( derived: DerivedTypePlan, field: DerivedFieldPlan, ) -> str: + """Return the consumer-interface name associated with one direct native-array-handle field.""" return f"prik_field_handle_{self._derived_field_symbol(derived, field)}_consumer" @staticmethod def _module_member_symbol(variable: ModuleVariablePlan, member: DerivedMemberPathPlan) -> str: + """Return the normalized symbol fragment shared by procedures for one module member path.""" return "_".join((variable.symbol_name, *member.path)).casefold() def _module_member_bridge_name( @@ -6255,6 +6701,7 @@ def _module_member_bridge_name( member: DerivedMemberPathPlan, action: str, ) -> str: + """Return the exported bridge symbol for one module-member action.""" return f"bind_c_prik_module_field_{self._module_member_symbol(variable, member)}_{action}" def _module_member_callback_interface_name( @@ -6262,6 +6709,7 @@ def _module_member_callback_interface_name( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> str: + """Return the consumer-interface name associated with one module member.""" return f"prik_module_field_{self._module_member_symbol(variable, member)}_consumer" def _module_member_handle_bridge_name( @@ -6270,6 +6718,7 @@ def _module_member_handle_bridge_name( member: DerivedMemberPathPlan, operation: NativeArrayOperation, ) -> str: + """Return the exported bridge symbol for one module native-array-handle operation.""" return f"bind_c_prik_module_field_handle_{self._module_member_symbol(variable, member)}_{operation.value}" def _module_member_handle_callback_interface_name( @@ -6277,9 +6726,11 @@ def _module_member_handle_callback_interface_name( variable: ModuleVariablePlan, member: DerivedMemberPathPlan, ) -> str: + """Return the consumer-interface name for one module native-array-handle member.""" return f"prik_module_field_handle_{self._module_member_symbol(variable, member)}_consumer" def _derived_member_proxy_variables(self, plan: ModulePlan) -> tuple[ModuleVariablePlan, ...]: + """Return derived module variables whose completed access mechanism is member proxying.""" return tuple( variable for variable in self._variables(plan) @@ -6352,6 +6803,7 @@ def _class_constructor_procedure( @staticmethod def _class_create_bridge_name(surface: ClassSurfacePlan) -> str: + """Return the exported bridge symbol for a class constructor allocation procedure.""" return f"bind_c_prik_create_{surface.type_identity[1].casefold()}" def _allocatable_holder_destroy_procedure(self, derived: DerivedTypePlan) -> FortranFunction: @@ -6425,47 +6877,217 @@ def _pointer_holder_presence_procedure(self, derived: DerivedTypePlan) -> Fortra @staticmethod def _derived_native_alias(type_name: str) -> str: + """Return the imported native alias used to disambiguate one derived type in bridge source.""" return f"prik_type_{type_name.casefold()}" @staticmethod def _derived_destroy_bridge_name(type_name: str) -> str: + """Return the exported bridge symbol for destroying one wrapper-owned native derived object.""" return f"bind_c_prik_destroy_{type_name.casefold()}" @staticmethod def _allocatable_holder_type_name(type_name: str) -> str: + """Return the internal Fortran type name for an allocatable derived holder.""" return f"prik_{type_name.casefold()}_allocatable_holder" @staticmethod def _pointer_holder_type_name(type_name: str) -> str: + """Return the internal Fortran type name for a pointer derived holder.""" return f"prik_{type_name.casefold()}_pointer_holder" @staticmethod def _allocatable_holder_destroy_bridge_name(type_name: str) -> str: + """Return the exported bridge symbol for destroying an allocatable holder.""" return f"bind_c_prik_destroy_{type_name.casefold()}_allocatable_holder" @staticmethod def _allocatable_holder_presence_bridge_name(type_name: str) -> str: + """Return the exported bridge symbol for inquiring an allocatable holder payload.""" return f"bind_c_prik_{type_name.casefold()}_allocatable_holder_present" @staticmethod def _pointer_holder_destroy_bridge_name(type_name: str) -> str: + """Return the exported bridge symbol for destroying a pointer holder.""" return f"bind_c_prik_destroy_{type_name.casefold()}_pointer_holder" @staticmethod def _pointer_holder_presence_bridge_name(type_name: str) -> str: + """Return the exported bridge symbol for inquiring a pointer holder payload.""" return f"bind_c_prik_{type_name.casefold()}_pointer_holder_present" @staticmethod def _module_derived_presence_bridge_name(plan: ModuleVariablePlan) -> str: + """Return the exported bridge symbol for one nullable derived module variable's presence inquiry.""" return f"bind_c_prik_module_{plan.symbol_name.casefold()}_present" def _external_interfaces(self, plan: ModulePlan) -> tuple[FortranInterface, ...]: - procedures = tuple( + """Declare ordinary standalone wrapper targets with explicit interfaces.""" + native_procedures = tuple( self._external_interface_procedure(function) for function in self._functions(plan) if function.bridge.external_declaration is ExternalDeclarationMode.EXPLICIT_INTERFACE ) - return (FortranInterface(procedures),) if procedures else () + return (FortranInterface(native_procedures),) if native_procedures else () + + def _prototype_interfaces( + self, + plan: ModulePlan, + ) -> tuple[FortranInterface, ...]: + """Emit every used prototype through the one abstract-interface path.""" + prototypes = self._prototype_plans(plan) + procedures = tuple(self._procedure_prototype_interface(item) for item in prototypes) + return (FortranInterface(procedures, abstract=True),) if procedures else () + + def _prototype_plans(self, plan: ModulePlan) -> tuple[ProcedurePrototypePlan, ...]: + """Deduplicate callback and direct-call uses by generated interface symbol.""" + candidates = ( + *(callback.prototype for callback in self._callback_sites(plan)), + *( + declaration.prototype + for function in self._functions(plan) + for declaration in function.declaration_callables + if declaration.prototype is not None + ), + ) + prototypes: dict[str, ProcedurePrototypePlan] = {} + for prototype in candidates: + key = prototype.interface_symbol.casefold() + previous = prototypes.setdefault(key, prototype) + if previous is not prototype and not self._same_procedure_prototype(previous, prototype): + raise ValueError(f"Conflicting prototype signatures for {prototype.name!r}") + return tuple(prototypes.values()) + + def _same_procedure_prototype( + self, + left: ProcedurePrototypePlan, + right: ProcedurePrototypePlan, + ) -> bool: + """Compare only characteristics represented in one abstract interface.""" + return self._procedure_prototype_interface(left) == self._procedure_prototype_interface(right) + + def _procedure_prototype_interface( + self, + prototype: ProcedurePrototypePlan, + ) -> FortranInterfaceProcedure: + """Lower one shared signature to its generated abstract interface body.""" + result = prototype.result + return FortranInterfaceProcedure( + name=prototype.interface_symbol, + imports=self._procedure_prototype_imports(prototype), + parameters=tuple(self._procedure_prototype_parameter(item) for item in prototype.arguments), + result_name="prik_result" if result is not None else None, + result_type=self._procedure_prototype_result_type(result) if result is not None else None, + is_subroutine=result is None, + pure=prototype.pure, + ) + + def _procedure_prototype_parameter( + self, + argument: ProcedurePrototypeArgumentPlan, + ) -> FortranParameter: + """Declare one exact dummy from the shared prototype plan.""" + attributes = [] + if argument.passed_by_value: + attributes.append("value") + if argument.intent is not None: + attributes.append(f"intent({argument.intent})") + if argument.rank: + attributes.append(f"dimension({self._procedure_prototype_shape(argument.array, argument.owner_path)})") + return FortranParameter( + argument.name, + self._procedure_prototype_type(argument), + tuple(attributes), + ) + + def _procedure_prototype_result_type( + self, + result: ProcedurePrototypeResultPlan, + ) -> str: + """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)})" + return result_type + + def _procedure_prototype_type( + self, + value: ProcedurePrototypeArgumentPlan | ProcedurePrototypeResultPlan, + ) -> str: + """Return the native type shared by callback and direct prototype uses.""" + if value.derived_backend_symbol is not None: + return f"type({self._derived_native_alias(value.derived_backend_symbol)})" + if value.semantic_type_name == "String": + if value.character_length is None: + raise ValueError(f"Prototype value {value.owner_path!r} has no fixed character length") + return f"character(kind=c_char, len={value.character_length})" + return PrimitiveScalarTypeRegistry.type_for(value.semantic_type_name).fortran_spelling + + @staticmethod + def _procedure_prototype_shape(array: ArrayHandoffPlan | None, owner_path: str) -> str: + """Render an exact prototype array shape without backend role substitution.""" + if array is None or array.rank is None: + raise ValueError(f"Prototype value {owner_path!r} has no concrete shape") + return ", ".join(render_declaration_extent(expression, {}, target="fortran") for expression in array.shape) + + def _procedure_prototype_imports( + self, + prototype: ProcedurePrototypePlan, + ) -> tuple[str, ...]: + """Import every kind or derived alias referenced by an interface body.""" + values = ( + *prototype.arguments, + *((prototype.result,) if prototype.result is not None else ()), + ) + return tuple(dict.fromkeys(self._procedure_prototype_import(item) for item in values)) + + def _procedure_prototype_import( + self, + value: ProcedurePrototypeArgumentPlan | ProcedurePrototypeResultPlan, + ) -> str: + """Return the host symbol needed to spell one prototype value type.""" + if value.derived_backend_symbol is not None: + return self._derived_native_alias(value.derived_backend_symbol) + if value.semantic_type_name == "String": + return "c_char" + return self._iso_symbol(value.semantic_type_name) + + def _prototype_entity_declarations(self, plan: ModulePlan) -> tuple[FortranDeclaration, ...]: + """Declare directly called standalone entities from their abstract signatures.""" + entities: dict[str, DeclarationCallablePlan] = {} + for function in self._functions(plan): + for declaration in function.declaration_callables: + if declaration.action is not DeclarationCallableAction.STANDALONE_PROCEDURE: + continue + previous = entities.setdefault(declaration.backend_symbol.casefold(), declaration) + if previous is not declaration and not self._same_prototype_entity(previous, declaration): + raise ValueError(f"Conflicting standalone prototype entities for {declaration.native_name!r}") + return tuple( + FortranDeclaration( + declaration.backend_symbol, + f"procedure({self._required_declaration_prototype(declaration).interface_symbol})", + ) + for declaration in entities.values() + ) + + def _same_prototype_entity( + self, + left: DeclarationCallablePlan, + right: DeclarationCallablePlan, + ) -> bool: + """Return whether two direct uses declare the same native entity exactly.""" + return self._same_procedure_prototype( + self._required_declaration_prototype(left), + self._required_declaration_prototype(right), + ) + + @staticmethod + def _required_declaration_prototype( + declaration: DeclarationCallablePlan, + ) -> ProcedurePrototypePlan: + """Return a direct entity's completed prototype or reject an edited plan.""" + if declaration.prototype is None: + raise ValueError(f"Declaration callable {declaration.owner_path!r} has no prototype") + return declaration.prototype def _native_external_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, ...]: """Lower the completed implicit-external declaration mode.""" @@ -6580,10 +7202,6 @@ def _callback_native_imports(self, callback: CallbackHandoffPlan) -> tuple[str, imports.append(self._iso_symbol(transfer.semantic_type_name)) return tuple(dict.fromkeys(imports)) - @staticmethod - def _callback_imported_prototype_symbol(callback: CallbackHandoffPlan) -> str: - return f"{callback.adapter_symbol}_prototype" - def _derived_call_interfaces(self, plan: ModulePlan) -> tuple[FortranInterface, ...]: """Declare the typed callback ABI shared by every scalar-derived call.""" if not any( @@ -6652,6 +7270,7 @@ def _derived_array_callback_interfaces(self, plan: ModulePlan) -> tuple[FortranI return (FortranInterface(procedures),) if procedures else () def _direct_ordinary_array_callback_interfaces(self, plan: ModulePlan) -> tuple: + """Return callback interfaces required by direct ordinary-array field procedures.""" return tuple( self._ordinary_array_callback_interface( field, @@ -6663,6 +7282,7 @@ def _direct_ordinary_array_callback_interfaces(self, plan: ModulePlan) -> tuple: ) def _module_ordinary_array_callback_interfaces(self, plan: ModulePlan) -> tuple: + """Return callback interfaces required by module ordinary-array member procedures.""" return tuple( self._ordinary_array_callback_interface( member.field, @@ -6674,6 +7294,7 @@ def _module_ordinary_array_callback_interfaces(self, plan: ModulePlan) -> tuple: ) def _direct_handle_callback_interfaces(self, plan: ModulePlan) -> tuple: + """Return callback interfaces required by direct native-array-handle field procedures.""" return tuple( self._native_handle_callback_interface( field, @@ -6685,6 +7306,7 @@ def _direct_handle_callback_interfaces(self, plan: ModulePlan) -> tuple: ) def _module_handle_callback_interfaces(self, plan: ModulePlan) -> tuple: + """Return callback interfaces required by module native-array-handle member procedures.""" return tuple( self._native_handle_callback_interface( member.field, @@ -6826,19 +7448,12 @@ def _native_result_slots_need_allocator(self, function: FunctionPlan) -> bool: ) def _external_interface_procedure(self, plan: FunctionPlan) -> FortranInterfaceProcedure: + """Declare one standalone native target from its completed function plan.""" slots = tuple(sorted(plan.native_call_slots, key=lambda item: item.native_position)) arguments = {argument.owner_path: argument for argument in plan.arguments} parameters = tuple(self._external_interface_slot_parameter(plan, slot, arguments) for slot in slots) - imports = tuple( - dict.fromkeys( - self._iso_symbol(slot.semantic_type_name) for slot in slots if slot.semantic_type_name is not None - ) - ) - result_name = None if plan.bridge.native_is_subroutine else "native_result" - direct_result = self._direct_result(plan) - result_type = self._native_result_type(plan, direct_result) if result_name is not None else None - if result_type is not None and direct_result is not None: - imports = tuple(dict.fromkeys((*imports, self._iso_symbol(direct_result.semantic_type_name)))) + result_name, result_type, direct_result = self._external_interface_result(plan) + imports = self._external_interface_imports(plan, slots, direct_result) return FortranInterfaceProcedure( name=plan.bridge.native_name, imports=imports, @@ -6849,6 +7464,33 @@ def _external_interface_procedure(self, plan: FunctionPlan) -> FortranInterfaceP is_subroutine=plan.bridge.native_is_subroutine, ) + def _external_interface_result( + self, + plan: FunctionPlan, + ) -> tuple[str | None, str | None, ResultPlan | None]: + """Return the standalone target's result declaration, if it is a function.""" + if plan.bridge.native_is_subroutine: + return None, None, None + direct_result = self._direct_result(plan) + return "native_result", self._native_result_type(plan, direct_result), direct_result + + def _external_interface_imports( + self, + plan: FunctionPlan, + slots: tuple[NativeCallSlotPlan, ...], + direct_result: ResultPlan | None, + ) -> tuple[str, ...]: + """Collect type and declaration-callable symbols visible in the interface body.""" + imports = [self._iso_symbol(slot.semantic_type_name) for slot in slots if slot.semantic_type_name is not None] + imports.extend( + declaration.backend_symbol + for declaration in plan.declaration_callables + if declaration.action is DeclarationCallableAction.STANDALONE_PROCEDURE + ) + if direct_result is not None: + imports.append(self._iso_symbol(direct_result.semantic_type_name)) + return tuple(dict.fromkeys(imports)) + @staticmethod def _external_interface_parameter_declarations( slots: tuple[NativeCallSlotPlan, ...], @@ -6868,6 +7510,8 @@ def _external_interface_parameter_declarations( if dependencies <= emitted_roles: declarations.append(parameter) emitted_roles.add(slot.symbolic_role) + if slot.array is not None: + emitted_roles.update(slot.array.extent_roles) pending.pop(index) break else: @@ -7099,7 +7743,7 @@ def _external_array_dimension_from_plan(self, array: ArrayHandoffPlan, plan: Fun return ".." if array.category in {"assumed_shape", "deferred_shape"}: return ", ".join(":" for _ in range(array.rank)) - shape = list(self._array_shape_from_roles(array, plan.arguments)) + shape = list(self._array_shape_from_roles(array, plan)) if array.native_order == "ORDER_C": shape.reverse() if array.category != "assumed_size": @@ -7116,6 +7760,7 @@ def _external_assumed_size_dimension(array: ArrayHandoffPlan, shape: list[str]) @staticmethod def _is_scalar_storage_array(array: ArrayHandoffPlan | None) -> bool: + """Return whether an array plan represents rank-zero scalar storage rather than an ordinary array.""" return bool(array is not None and array.rank == 0 and array.category == SCALAR_STORAGE_CATEGORY) # Ordinary-array result-shape lowering. @@ -7123,37 +7768,91 @@ def _array_result_shape(self, plan: FunctionPlan, result: ResultPlan) -> tuple[s """Lower one result shape through the plan's native scalar roles.""" if result.array is None: raise ValueError(f"Array result {result.owner_path!r} has no shape plan") - return self._array_shape_from_roles(result.array, plan.arguments) + return self._array_shape_from_roles(result.array, plan) def _array_output_shape(self, plan: FunctionPlan, slot: NativeCallSlotPlan) -> tuple[str, ...]: """Lower one hidden-output shape through the plan's native scalar roles.""" if slot.array is None: raise ValueError(f"Array output {slot.owner_path!r} has no shape plan") - return self._array_shape_from_roles(slot.array, plan.arguments) - - def _array_shape_from_roles(self, array, arguments) -> tuple[str, ...]: - """Replace validated shape references with their native dummy names.""" - lowered_shape = [] - role_names = {argument.binding.handoff_role: argument.bridge.native_name.lower() for argument in arguments} - for axis, expression in enumerate(array.shape): - lowered = expression - for role in array.extent_reference_roles[axis]: - native_name = role_names.get(role) - if native_name is None: - reference_name = role.rsplit(".", 1)[-1].split(":", 1)[0] - native_name = reference_name - reference_name = role.rsplit(".", 1)[-1].split(":", 1)[0] - lowered = re.sub(rf"\b{re.escape(reference_name)}\b", native_name, lowered) - lowered_shape.append(lowered) - return tuple(lowered_shape) + return self._array_shape_from_roles(slot.array, plan) + + def _array_shape_from_roles(self, array: ArrayHandoffPlan, plan: FunctionPlan) -> tuple[str, ...]: + """Render validated shape tokens with their planned native role names.""" + role_names = self._array_shape_role_names(plan) + return tuple( + self._render_array_shape_axis(array, axis, expression, role_names) + for axis, expression in enumerate(array.shape) + ) + + @staticmethod + def _array_shape_role_names(plan: FunctionPlan) -> dict[str, str]: + """Map planned scalar, extent, and callable roles to bridge spellings.""" + role_names = {argument.binding.handoff_role: argument.bridge.native_name.lower() for argument in plan.arguments} + role_names.update( + { + role: f"{argument.bridge.native_name.lower()}_extent_{axis}" + for argument in plan.arguments + if argument.array is not None + for axis, role in enumerate(argument.array.extent_roles) + } + ) + role_names.update( + {declaration.symbolic_role: declaration.backend_symbol for declaration in plan.declaration_callables} + ) + return role_names + + def _render_array_shape_axis( + self, + array: ArrayHandoffPlan, + axis: int, + expression: str, + role_names: dict[str, str], + ) -> str: + """Render one axis after resolving its value and callable symbols.""" + substitutions = self._shape_role_substitutions( + array.extent_reference_tokens[axis], + array.extent_reference_roles[axis], + role_names, + "Array extent role", + "bridge value", + ) + substitutions.update( + self._shape_role_substitutions( + array.extent_callable_tokens[axis], + array.extent_callable_roles[axis], + role_names, + "Array extent callable role", + "bridge symbol", + ) + ) + return render_declaration_extent(expression, substitutions, target="fortran") + + @staticmethod + def _shape_role_substitutions( + tokens: tuple[str, ...], + roles: tuple[str, ...], + role_names: dict[str, str], + label: str, + value_label: str, + ) -> dict[str, str]: + """Resolve one aligned token-role list or reject a missing bridge producer.""" + substitutions = {} + for token, role in zip(tokens, roles, strict=True): + try: + substitutions[token] = role_names[role] + except KeyError: + raise ValueError(f"{label} {role!r} has no {value_label}") from None + return substitutions def _has_optional_arguments(self, plan: FunctionPlan) -> bool: + """Return whether a function plan contains a nullable-value or descriptor optional argument.""" return any( argument.bridge.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} for argument in plan.arguments ) def _literal_expression(self, value: object) -> str: + """Render a Python literal as the equivalent Fortran expression used by a planned native call.""" if isinstance(value, bool): return ".true." if value else ".false." if isinstance(value, complex): @@ -7161,29 +7860,38 @@ def _literal_expression(self, value: object) -> str: return str(value) def _bridge_function_name(self, plan: FunctionPlan) -> str: + """Return the stable exported bridge name for one function plan.""" return f"bind_c_{plan.symbol_name}" def _module_bridge_getter_name(self, plan: ModuleVariablePlan) -> str: + """Return the stable exported getter bridge name for one module variable.""" return f"bind_c_get_{plan.symbol_name}" def _module_bridge_setter_name(self, plan: ModuleVariablePlan) -> str: + """Return the stable exported setter bridge name for one module variable.""" return f"bind_c_set_{plan.symbol_name}" def _native_function_name(self, plan: FunctionPlan) -> str: - return plan.bridge.native_name if plan.bridge.external else f"native_{plan.symbol_name}" + """Return the in-module alias or standalone symbol selected for the native procedure.""" + return plan.bridge.native_name if plan.bridge.standalone else f"native_{plan.symbol_name}" def _native_variable_name(self, plan: ModuleVariablePlan) -> str: + """Return the imported native alias used by one module variable.""" return f"native_{plan.symbol_name}" def _functions(self, plan: ModulePlan) -> tuple[FunctionPlan, ...]: + """Flatten namespaces into function plans while preserving module and namespace order.""" return tuple(function for namespace in plan.namespaces for function in namespace.functions) def _variables(self, plan: ModulePlan) -> tuple[ModuleVariablePlan, ...]: + """Flatten namespaces into module-variable plans while preserving module and namespace order.""" return tuple(variable for namespace in plan.namespaces for variable in namespace.variables) def _iso_symbol(self, semantic_type_name: str) -> str: + """Return the iso_c_binding symbol required by one semantic primitive type.""" + if is_boolean_semantic_type_name(semantic_type_name): + return "c_bool" symbols = { - "Bool": "c_bool", "Int8": "c_int8_t", "Int16": "c_int16_t", "Int32": "c_int32_t", @@ -7197,6 +7905,7 @@ def _iso_symbol(self, semantic_type_name: str) -> str: return symbols[semantic_type_name] def _iso_c_symbols(self, plan: ModulePlan) -> tuple[str, ...]: + """Return the de-duplicated iso_c_binding import set required by the completed module plan.""" symbols = [ "c_associated", "c_bool", @@ -7225,6 +7934,7 @@ def _iso_c_symbols(self, plan: ModulePlan) -> tuple[str, ...]: return tuple(dict.fromkeys(symbols)) def _uses_c_function_pointer_symbols(self, plan: ModulePlan) -> bool: + """Return whether completed module or field descriptor actions require C procedure-pointer support.""" module_descriptors = any( self._uses_module_allocatable_descriptor(variable) for variable in self._variables(plan) ) @@ -7240,8 +7950,64 @@ def _uses_c_function_pointer_symbols(self, plan: ModulePlan) -> bool: return module_descriptors or field_descriptors def _uses_derived_interop_symbols(self, plan: ModulePlan) -> bool: + """Return whether completed derived call or module-variable actions require derived interop support.""" derived_calls = any( argument.derived_call is not None for function in self._functions(plan) for argument in function.arguments ) derived_variables = any(variable.derived is not None for variable in self._variables(plan)) return derived_calls or derived_variables + + +if __name__ == "__main__": + from prik.codegen.planner import WrapperPlanner + from prik.semantics.models import SemanticArgument, SemanticFunction, SemanticModule, SemanticType + from prik.semantics.policy_completion import complete_semantic_policies + + module = SemanticModule( + name="bridge_demo", + functions=[ + SemanticFunction( + name="double_value", + native_name="DOUBLE_VALUE", + arguments=[SemanticArgument("value", SemanticType("Float64"))], + return_type=SemanticType("Float64"), + ) + ], + ) + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + function_plan = plan.namespaces[0].functions[0] + bridge = FortranBridgeGenerator() + bridge.require_supported(plan) + fortran_module = bridge.visit(plan) + procedure = fortran_module.procedures[0] + + print(f"Native procedure: {function_plan.bridge.native_name}") + print( + "Native call slots:", + ", ".join(f"{slot.source_kind}:{slot.native_name}" for slot in function_plan.native_call_slots), + ) + print(f"Bridge module: {fortran_module.name}") + print("Module uses:") + for use in fortran_module.uses: + only = f", only: {', '.join(use.only)}" if use.only else "" + print(f" use {use.module}{only}") + print(f"Bridge procedure: {procedure.name}") + print(f"Binding name: {procedure.bind_name}") + print(f"Procedure kind: {'subroutine' if procedure.is_subroutine else 'function'}") + print(f"Result: {procedure.result_name} :: {procedure.result_type}") + print("Parameters:") + for parameter in procedure.parameters: + attributes = f", {', '.join(parameter.attributes)}" if parameter.attributes else "" + print(f" {parameter.name}: {parameter.type_name}{attributes}") + print("Declarations:") + if procedure.declarations: + for declaration in procedure.declarations: + attributes = f", {', '.join(declaration.attributes)}" if declaration.attributes else "" + print(f" {declaration.name}: {declaration.type_name}{attributes}") + else: + print(" (none)") + print("Body nodes:") + for statement in procedure.body: + print(f" {statement!r}") + print("Internal procedures:", ", ".join(item.name for item in procedure.internal_procedures) or "(none)") diff --git a/prik/wrapper_codegen/generator.py b/prik/codegen/generator.py similarity index 86% rename from prik/wrapper_codegen/generator.py rename to prik/codegen/generator.py index efad5b2eb..e02839754 100644 --- a/prik/wrapper_codegen/generator.py +++ b/prik/codegen/generator.py @@ -1,4 +1,12 @@ -"""Public direct-generation boundary for editable wrapper plans.""" +"""Freeze, validate, lower, and render editable wrapper plans. + +``WrapperCodeGenerator`` is the final consumer of an editable ``ModulePlan``. +It validates cross-view plan consistency, asks each backend to preflight its +own lowering capability, renders the resulting C, Fortran, and header nodes, +and returns source-bearing wrapper artifacts for build integration. Semantic +policy remains upstream: this module validates and lowers completed decisions +without selecting replacements. +""" from __future__ import annotations @@ -27,6 +35,7 @@ from prik.semantics.metadata import SCALAR_STORAGE_CATEGORY from prik.semantics.wrapper_policy import ( ArgumentHandoffMode, + ArrayLogicalABI, ArrayWritebackABI, BridgeDataAction, CallbackABIKind, @@ -49,6 +58,7 @@ DerivedOwnerRetention, DerivedRelease, DerivedWriteback, + DeclarationCallableAction, DirectResultABI, LifecycleOperation, FIXED_STRING_RESULT_COPY_REASON, @@ -76,6 +86,7 @@ PythonExceptionKind, RAW_STRING_ADDRESS_COPY_REASON, SCALAR_DESCRIPTOR_RESULT_COPY_REASON, + ScalarLogicalABI, STRING_INPUT_COPY_REASON, STRING_REPLACEMENT_COPY_REASON, STRING_STORAGE_COPY_REASON, @@ -84,15 +95,17 @@ WritebackPhase, overload_builtin_scalar_family, ) -from prik.wrapper_codegen.c.binding import CBindingGenerator -from prik.wrapper_codegen.fortran.bridge import FortranBridgeGenerator -from prik.wrapper_codegen.plan import ( +from prik.codegen.c.binding import CBindingGenerator +from prik.codegen.fortran.bridge import FortranBridgeGenerator +from prik.codegen.plan import ( + ArrayHandoffPlan, ArgumentTransferPlan, CallbackHandoffPlan, CallbackTransferPlan, OverloadPlan, ClassSurfacePlan, DatatypeFamily, + DeclarationCallablePlan, FunctionPlan, LifecycleActionPlan, ModulePlan, @@ -100,14 +113,25 @@ NativeArrayHandlePlan, NativeCallSlotPlan, NamespacePlan, + ProcedurePrototypeArgumentPlan, + ProcedurePrototypePlan, + ProcedurePrototypeResultPlan, ResultPlan, WrapperPlanDiagnostic, ) -from prik.wrapper_codegen.printers import CSourcePrinter, FortranSourcePrinter +from prik.codegen.printers import CSourcePrinter, FortranSourcePrinter class WrapperCodeGenerator: - """Freeze, validate, directly lower, and print one wrapper plan.""" + """Turn one editable ``ModulePlan`` into rendered wrapper artifacts. + + Use :meth:`generate` after ``WrapperPlanner.build`` and before build + integration writes or compiles sources. This class owns the plan's final + freeze and cross-backend consistency validation, then delegates backend + node construction and source printing to the injected or default C and + Fortran components. Its private sections cover the generation entrypoint, + typed plan diagnostics, and artifact assembly. + """ def __init__( self, @@ -117,23 +141,46 @@ def __init__( c_printer: CSourcePrinter | None = None, fortran_printer: FortranSourcePrinter | None = None, ): + """Create a generator with default or explicitly supplied backend components. + + Supplying a generator or printer is useful when an established caller + needs to observe or substitute a backend implementation. Omitted + components use the standard direct-lowering and printing paths; no + plan policy is stored or inferred during initialization. + """ self._c_generator = c_generator or CBindingGenerator() self._fortran_generator = fortran_generator or FortranBridgeGenerator() self._c_printer = c_printer or CSourcePrinter() self._fortran_printer = fortran_printer or FortranSourcePrinter() + # Public entrypoint: freeze, validate, preflight, lower, print, and assemble. def generate( self, plan: ModulePlan, *, progress: Callable[[str, float | None], None] | None = None, ) -> RenderedGeneratedWrapperArtifacts: - """Consume exactly one editable plan and return rendered artifacts.""" + """Render one editable plan into C, Fortran, header, and build artifacts. + + The received ``plan`` is frozen before validation, so later mutation + raises the stage-record error. ``progress``, when provided, receives + a stage label with ``None`` before each rendering operation and the + same label with its elapsed seconds afterward. The result is normally + passed to build integration, which owns writing and compilation. + + Raises: + ValueError: If the frozen plan is inconsistent or a selected + backend cannot lower one of its completed actions. + """ + # Freeze the exact editable handoff, then validate cross-backend plan facts. plan.freeze() self._validate_plan(plan) + + # Each backend preflights only the typed mechanisms it is responsible for. self._c_generator.require_supported(plan) self._fortran_generator.require_supported(plan) + # Lower and print binding translation units in their established progress order. if progress is not None: progress("Generate binding source", None) started = time.perf_counter() @@ -142,6 +189,7 @@ def generate( if progress is not None: progress("Generate binding source", time.perf_counter() - started) + # Lower and print the single bridge module after its binding counterpart. if progress is not None: progress("Generate bridge source", None) started = time.perf_counter() @@ -150,6 +198,7 @@ def generate( if progress is not None: progress("Generate bridge source", time.perf_counter() - started) + # Render the shared binding header after all source nodes are available. if progress is not None: progress("Generate binding header", None) started = time.perf_counter() @@ -158,6 +207,7 @@ def generate( if progress is not None: progress("Generate binding header", time.perf_counter() - started) + # Assemble source text with the stable filenames consumed by build integration. return self._rendered_artifacts( plan.owner_path, c_sources, @@ -167,20 +217,36 @@ def generate( required_headers=plan.required_headers, ) + # Plan-consistency diagnostics: module graph first, then typed member records. def _validate_plan(self, plan: ModulePlan) -> None: - """Reject complete-plan inconsistencies in the final frozen plan.""" + """Raise one ordered summary when the final frozen plan is inconsistent. + + The method consumes the exact frozen plan passed to :meth:`generate`. + It retains every typed diagnostic in collection order so callers see a + stable, owner-local error summary before either backend lowers nodes. + """ diagnostics = self._plan_diagnostics(plan) if diagnostics: raise ValueError(self._diagnostic_summary(diagnostics)) def _plan_diagnostics(self, plan: ModulePlan) -> tuple[WrapperPlanDiagnostic, ...]: - """Return binding/bridge graph diagnostics before backend preflight.""" + """Collect ordered cross-backend diagnostics for one frozen module plan. + + Module identity and namespace structure are checked first, followed by + each namespace-owned function, variable, class, and overload. Global + class, symbol, and header consistency checks finish the collection. + The method only reports facts already present in the plan. + """ diagnostics = [] + + # Validate module ownership and the complete namespace tree before member links. if plan.binding.owner_path != plan.owner_path: diagnostics.append(self._diagnostic(plan.owner_path, "binding-module-owner", plan.binding.owner_path)) if plan.bridge.owner_path != plan.owner_path: diagnostics.append(self._diagnostic(plan.owner_path, "bridge-module-owner", plan.bridge.owner_path)) diagnostics.extend(self._namespace_tree_diagnostics(plan)) + + # Validate every typed member against the shared records in its namespace. for namespace in plan.namespaces: diagnostics.extend(self._namespace_diagnostics(namespace)) for function in namespace.functions: @@ -192,6 +258,8 @@ def _plan_diagnostics(self, plan: ModulePlan) -> tuple[WrapperPlanDiagnostic, .. functions = {id(function) for function in namespace.functions} for overload in namespace.overloads: diagnostics.extend(self._overload_diagnostics(overload, functions)) + + # Validate graph-wide ordering, generated spellings, and artifact dependencies. diagnostics.extend(self._class_graph_diagnostics(plan)) diagnostics.extend(self._generated_symbol_diagnostics(plan)) diagnostics.extend(self._required_header_diagnostics(plan)) @@ -285,6 +353,7 @@ def _namespace_diagnostics(self, plan: NamespacePlan) -> tuple[WrapperPlanDiagno *self._derived_type_diagnostics(plan), ) + # Namespace, class, derived-type, and module-variable diagnostics. # Class validation keeps construction and method references mechanical. def _class_surface_diagnostics( self, @@ -592,6 +661,12 @@ def _derived_field_family_diagnostics(self, field) -> tuple[WrapperPlanDiagnosti @staticmethod def _valid_scalar_derived_field(field) -> bool: + """Return whether one field is the completed scalar-value field variant. + + The helper reads only the field's planned access, action, and rank. A + false result lets the caller report the field's existing object-kind + mismatch without choosing a replacement representation. + """ return ( field.access is DerivedFieldAccessMechanism.SCALAR_VALUE and field.getter_action is CodegenAction.DIRECT_VALUE @@ -600,6 +675,11 @@ def _valid_scalar_derived_field(field) -> bool: @staticmethod def _valid_string_derived_field(field) -> bool: + """Return whether one field is the completed fixed-string copy variant. + + A valid string field has scalar rank, a positive fixed length, and the + precise access and copy-out action already selected by policy. + """ return ( field.access is DerivedFieldAccessMechanism.FIXED_STRING_COPY and field.getter_action is CodegenAction.COPY_OUT @@ -610,6 +690,12 @@ def _valid_string_derived_field(field) -> bool: @staticmethod def _valid_array_derived_field(field) -> bool: + """Return whether one field uses its selected ordinary-array mechanism. + + Native-handle fields require the handle access mechanism; other array + fields require the ordinary descriptor mechanism. Both retain the + planned borrowed-view action and array facet. + """ expected_access = ( DerivedFieldAccessMechanism.NATIVE_ARRAY_HANDLE if field.native_array_handle is not None @@ -623,6 +709,12 @@ def _valid_array_derived_field(field) -> bool: @staticmethod def _valid_nested_derived_field(field) -> bool: + """Return whether one field is a borrowed nested-derived-object view. + + The check preserves the completed parent-retention and reference + handoff facts. It does not resolve nested types or create ownership + policy; callers turn a false result into one field diagnostic. + """ handoff = field.derived return bool( field.access is DerivedFieldAccessMechanism.NESTED_OBJECT @@ -821,6 +913,8 @@ def _module_getter_diagnostics(self, plan: ModuleVariablePlan) -> tuple[WrapperP return self._binding_constant_getter_diagnostics(plan) if action is ModuleGetterAction.NATIVE_CONSTANT_VALUE: return self._native_constant_getter_diagnostics(plan) + if action is ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE: + return self._native_constant_array_getter_diagnostics(plan) if action is ModuleGetterAction.DERIVED_OBJECT: return self._derived_module_getter_role_diagnostics(plan) if plan.bridge.getter_role is None: @@ -862,6 +956,26 @@ def _native_constant_getter_diagnostics( diagnostics.append(self._diagnostic(plan.owner_path, "native-constant-has-binding-value", plan.owner_path)) return tuple(diagnostics) + def _native_constant_array_getter_diagnostics( + self, + plan: ModuleVariablePlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate one compiler-evaluated immutable parameter-array snapshot.""" + diagnostics = [] + if plan.array is None or plan.array.rank is None or plan.array.rank <= 0: + diagnostics.append(self._diagnostic(plan.owner_path, "missing-module-constant-array", plan.array)) + if plan.bridge.getter_role is None: + diagnostics.append( + self._diagnostic( + plan.owner_path, + "missing-module-getter-role", + plan.binding.getter_action.value, + ) + ) + if plan.binding.constant_value is not None: + diagnostics.append(self._diagnostic(plan.owner_path, "native-constant-has-binding-value", plan.owner_path)) + return tuple(diagnostics) + def _module_borrowed_array_view_diagnostics( self, plan: ModuleVariablePlan, @@ -1021,12 +1135,21 @@ def _module_nonwriting_action_diagnostics( if action is SetterAction.OMIT and plan.binding.getter_action not in { ModuleGetterAction.CONSTANT_VALUE, ModuleGetterAction.NATIVE_CONSTANT_VALUE, + ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE, }: return (self._diagnostic(plan.owner_path, "omitted-nonconstant-module-setter", action.value),) return () + # Function, declaration-callable, and argument diagnostics. def _function_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnostic, ...]: - """Return ordering, handoff, result, and lifecycle diagnostics.""" + """Collect one function's ordered graph and typed-transfer diagnostics. + + Function-wide ordering, role, output, invocation, and status checks + run before individual slots, arguments, results, and lifecycle actions. + This preserves diagnostic order and lets each typed helper validate the + lowest record that contains its compared binding and bridge facts. + """ + # Check the function-wide producer/consumer graph before its individual records. diagnostics = [ *self._sequence_diagnostics( plan.owner_path, @@ -1050,6 +1173,8 @@ def _function_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnost *self._native_invocation_diagnostics(plan), *self._optional_literal_combination_diagnostics(plan), ] + + # Validate shared slots and their typed consumers in native/result order. slots = {slot.native_position: slot for slot in plan.native_call_slots} for slot in plan.native_call_slots: diagnostics.extend(self._native_slot_diagnostics(slot)) @@ -1059,6 +1184,10 @@ def _function_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnost diagnostics.extend(self._result_diagnostics(result, slots, plan.available_roles)) for action in (*plan.writeback_actions, *plan.cleanup_actions, *plan.release_actions): diagnostics.extend(self._lifecycle_diagnostics(action, plan.available_roles)) + for declaration in plan.declaration_callables: + diagnostics.extend(self._declaration_callable_diagnostics(declaration)) + + # Validate function-wide lifecycle coverage after every producer is known. diagnostics.extend(self._writeback_phase_diagnostics(plan)) diagnostics.extend(self._string_writeback_diagnostics(plan)) return tuple(diagnostics) @@ -1077,9 +1206,98 @@ def _binding_conversion_order_diagnostics( positions = {owner: position for position, owner in enumerate(order)} role_owners = {argument.binding.handoff_role: argument.owner_path for argument in plan.arguments} + role_owners.update( + { + role: argument.owner_path + for argument in plan.arguments + if argument.array is not None + for role in argument.array.extent_roles + } + ) diagnostics.extend(self._late_binding_extent_conversion_diagnostics(plan, positions, role_owners)) return tuple(diagnostics) + def _declaration_callable_diagnostics( + self, + declaration: DeclarationCallablePlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate one already-selected declaration-function lowering action.""" + diagnostics = [] + if not declaration.symbolic_role or not declaration.expression_token or not declaration.backend_symbol: + diagnostics.append( + self._diagnostic(declaration.owner_path, "incomplete-declaration-callable-role", declaration) + ) + if declaration.action is DeclarationCallableAction.MODULE_IMPORT: + diagnostics.extend(self._module_declaration_callable_diagnostics(declaration)) + return tuple(diagnostics) + if declaration.action is DeclarationCallableAction.STANDALONE_PROCEDURE: + diagnostics.extend(self._standalone_declaration_callable_diagnostics(declaration)) + return tuple(diagnostics) + diagnostics.append( + self._diagnostic(declaration.owner_path, "unknown-declaration-callable-action", declaration.action) + ) + return tuple(diagnostics) + + def _module_declaration_callable_diagnostics( + self, + declaration: DeclarationCallablePlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate a completed module-import declaration dependency.""" + diagnostics = [] + if declaration.native_scope is None: + diagnostics.append( + self._diagnostic(declaration.owner_path, "module-declaration-callable-missing-scope", None) + ) + if declaration.prototype is not None: + diagnostics.append( + self._diagnostic(declaration.owner_path, "module-declaration-callable-has-prototype", None) + ) + return tuple(diagnostics) + + def _standalone_declaration_callable_diagnostics( + self, + declaration: DeclarationCallablePlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate a completed standalone-procedure declaration dependency.""" + diagnostics = [] + if declaration.native_scope is not None: + diagnostics.append( + self._diagnostic( + declaration.owner_path, + "standalone-declaration-callable-has-scope", + declaration.native_scope, + ) + ) + prototype = declaration.prototype + if prototype is None or not prototype.pure or prototype.result is None: + diagnostics.append( + self._diagnostic( + declaration.owner_path, + "incomplete-standalone-declaration-callable-prototype", + declaration.native_name, + ) + ) + else: + diagnostics.extend(self._procedure_prototype_diagnostics(prototype)) + return tuple(diagnostics) + + def _procedure_prototype_diagnostics( + self, + prototype: ProcedurePrototypePlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate the shared signature and generated abstract-interface symbol.""" + diagnostics = [] + symbol = prototype.interface_symbol + if not symbol or not symbol.isidentifier() or not symbol.casefold().startswith("prik_"): + diagnostics.append(self._diagnostic(prototype.owner_path, "invalid-prototype-interface-symbol", symbol)) + if not prototype.name or not prototype.identity: + diagnostics.append(self._diagnostic(prototype.owner_path, "incomplete-prototype-identity", prototype.name)) + if any(not argument.owner_path or not argument.name for argument in prototype.arguments): + diagnostics.append( + self._diagnostic(prototype.owner_path, "incomplete-prototype-arguments", prototype.arguments) + ) + return tuple(diagnostics) + def _late_binding_extent_conversion_diagnostics( self, plan: FunctionPlan, @@ -1219,11 +1437,12 @@ def _array_writeback_abi_diagnostics( if plan.bridge.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER and ( plan.mutates_native or self._publishes_array_replacement(plan) ): - expected = ( - ArrayWritebackABI.LOGICAL_LOW_BIT_INT8 - if plan.datatype_family is DatatypeFamily.BOOL - else ArrayWritebackABI.NATIVE_ARRAY - ) + if plan.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY: + expected = ArrayWritebackABI.NOT_APPLICABLE + elif plan.datatype_family is DatatypeFamily.BOOL: + expected = ArrayWritebackABI.LOGICAL_LOW_BIT_INT8 + else: + expected = ArrayWritebackABI.NATIVE_ARRAY if plan.array_writeback_abi is expected: return () return ( @@ -1393,6 +1612,8 @@ def _callback_argument_diagnostics( *self._callback_outer_handoff_diagnostics(plan), *self._callback_runtime_diagnostics(plan.owner_path, callback), *self._callback_symbol_diagnostics(plan.owner_path, callback), + *self._procedure_prototype_diagnostics(callback.prototype), + *self._callback_prototype_alignment_diagnostics(callback), *( diagnostic for position, transfer in enumerate(callback.arguments) @@ -1401,6 +1622,86 @@ def _callback_argument_diagnostics( *self._callback_result_diagnostics(callback), ) + def _callback_prototype_alignment_diagnostics( + self, + callback: CallbackHandoffPlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Require callback actions to remain subordinate to the one shared signature.""" + prototype = callback.prototype + if len(prototype.arguments) != len(callback.arguments): + return (self._diagnostic(callback.owner_path, "inconsistent-callback-prototype-arguments", prototype.name),) + if any( + not self._prototype_argument_matches_transfer(argument, transfer) + for argument, transfer in zip(prototype.arguments, callback.arguments, strict=True) + ): + return (self._diagnostic(callback.owner_path, "inconsistent-callback-prototype-arguments", prototype.name),) + prototype_result = prototype.result + transfer = callback.result.transfer + if (prototype_result is None) != (transfer is None): + return (self._diagnostic(callback.owner_path, "inconsistent-callback-prototype-result", prototype.name),) + if ( + prototype_result is not None + and transfer is not None + and not self._prototype_result_matches_transfer(prototype_result, transfer) + ): + return (self._diagnostic(callback.owner_path, "inconsistent-callback-prototype-result", prototype.name),) + return () + + @staticmethod + def _prototype_argument_matches_transfer( + argument: ProcedurePrototypeArgumentPlan, + transfer: CallbackTransferPlan, + ) -> bool: + """Compare signature characteristics without callback conversion actions.""" + return ( + argument.name, + argument.semantic_type_name, + argument.rank, + argument.passed_by_value, + argument.intent, + argument.character_length, + WrapperCodeGenerator._prototype_array_shape(argument.array), + argument.derived_type_identity, + argument.derived_backend_symbol, + ) == ( + transfer.name, + transfer.semantic_type_name, + transfer.rank, + transfer.passed_by_value, + transfer.intent, + transfer.character_length, + WrapperCodeGenerator._prototype_array_shape(transfer.array), + transfer.derived_type_identity, + transfer.derived_backend_symbol, + ) + + @staticmethod + def _prototype_result_matches_transfer( + result: ProcedurePrototypeResultPlan, + transfer: CallbackTransferPlan, + ) -> bool: + """Compare function-result characteristics without conversion actions.""" + return ( + result.semantic_type_name, + result.rank, + result.character_length, + WrapperCodeGenerator._prototype_array_shape(result.array), + result.derived_type_identity, + result.derived_backend_symbol, + ) == ( + transfer.semantic_type_name, + transfer.rank, + transfer.character_length, + WrapperCodeGenerator._prototype_array_shape(transfer.array), + transfer.derived_type_identity, + transfer.derived_backend_symbol, + ) + + @staticmethod + def _prototype_array_shape(array: ArrayHandoffPlan | None) -> tuple[str, ...] | None: + """Return signature-relevant shape text without comparing ABI roles.""" + return array.shape if array is not None else None + def _callback_outer_handoff_diagnostics( self, plan: ArgumentTransferPlan, @@ -1486,7 +1787,12 @@ def _callback_scalar_projection_diagnostics( 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} + and transfer.adapter_action + in { + CallbackTransferAction.COPY_IN, + CallbackTransferAction.COPY_OUT, + CallbackTransferAction.COPY_IN_OUT, + } ) return ( () @@ -1618,6 +1924,13 @@ def _derived_call_diagnostics( return tuple(diagnostics) def _derived_call_case_diagnostics(self, plan: ArgumentTransferPlan) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate the complete actual-storage matrix for one derived dummy. + + The completed call policy must name each ``DerivedObjectStorage`` + exactly once. Each case's compatibility, access, failure, and ABI + code fields are checked mechanically; no dummy-category behavior is + inferred or changed here. + """ call = plan.derived_call diagnostics = [] storages = tuple(case.actual_storage for case in call.cases) @@ -1637,6 +1950,12 @@ def _derived_call_writeback_diagnostics( self, plan: ArgumentTransferPlan, ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate the completed derived writeback selected for one dummy. + + The expected value is derived solely from the stored dummy category and + mutability fact so this validator can detect projection drift. It + returns diagnostics without modifying lifecycle policy. + """ call = plan.derived_call diagnostics = [] expected_writeback = ( @@ -1655,8 +1974,16 @@ def _derived_call_writeback_diagnostics( return tuple(diagnostics) def _derived_handoff_identity_diagnostics(self, owner_path, handoff) -> tuple[WrapperPlanDiagnostic, ...]: - """Validate canonical native identity and a supported typed call mechanism.""" + """Validate completed native identity, storage, and lifetime facts. + + ``handoff`` is a plan-projected derived-object record. The method + checks its canonical type identity, origin-compatible storage, owner + retention/release pair, and pointer-target lifetime in that order. It + emits diagnostics only and never supplies a missing lifetime policy. + """ diagnostics = [] + + # Check the canonical native identity and origin-specific storage first. if handoff.type_identity != (handoff.native_scope, handoff.native_type_name): diagnostics.append(self._diagnostic(owner_path, "inconsistent-derived-type-identity", handoff)) allowed_storage = { @@ -1678,6 +2005,8 @@ def _derived_handoff_identity_diagnostics(self, owner_path, handoff) -> tuple[Wr }.get(handoff.origin, set()) if handoff.storage not in allowed_storage: diagnostics.append(self._diagnostic(owner_path, "invalid-derived-storage", handoff.storage)) + + # Then check the primary retention/release pair required by that origin. expected_lifetime = { DerivedObjectOrigin.CALLER_WRAPPER: ( DerivedOwnerRetention.CALLER_WRAPPER, @@ -1708,6 +2037,8 @@ def _derived_handoff_identity_diagnostics(self, owner_path, handoff) -> tuple[Wr diagnostics.append(self._diagnostic(owner_path, "invalid-derived-owner-retention", handoff.owner_retention)) if handoff.release is not expected_release: diagnostics.append(self._diagnostic(owner_path, "invalid-derived-release", handoff.release)) + + # Finally validate the separate target lifetime, including pointer storage. target_lifetime = (handoff.target_owner_retention, handoff.target_release) allowed_target_lifetimes = { (DerivedOwnerRetention.NONE, DerivedRelease.NONE), @@ -1735,6 +2066,11 @@ def _array_extent_reference_diagnostics( for axis_roles in plan.array.extent_reference_roles for role in axis_roles if role not in available_roles + ) + tuple( + self._diagnostic(plan.owner_path, "unavailable-array-extent-callable", role) + for axis_roles in plan.array.extent_callable_roles + for role in axis_roles + if role not in available_roles ) def _argument_policy_consistency_diagnostics( @@ -1829,6 +2165,64 @@ def _argument_completed_fact_diagnostics( plan.native_call_slot.object_kind, ) ) + diagnostics.extend(self._logical_argument_slot_diagnostics(plan)) + return tuple(diagnostics) + + def _logical_argument_slot_diagnostics( + self, + plan: ArgumentTransferPlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Return scalar and array logical-policy drift from the native slot. + + The helper consumes one completed transfer plan and returns diagnostics + without changing it. It compares only copied plan facts; it does not + infer an ABI from the semantic datatype. + """ + diagnostics = [] + if plan.native_call_slot.scalar_logical_abi is not plan.scalar_logical_abi: + diagnostics.append( + self._diagnostic( + plan.owner_path, + "inconsistent-scalar-logical-abi", + plan.native_call_slot.scalar_logical_abi.value, + ) + ) + if plan.native_call_slot.scalar_native_type != plan.scalar_native_type: + diagnostics.append( + self._diagnostic( + plan.owner_path, + "inconsistent-scalar-native-type", + plan.native_call_slot.scalar_native_type, + ) + ) + if plan.native_call_slot.array_logical_abi is not plan.array_logical_abi: + diagnostics.append( + self._diagnostic( + plan.owner_path, + "inconsistent-array-logical-abi", + plan.native_call_slot.array_logical_abi.value, + ) + ) + if plan.native_call_slot.array_native_type != plan.array_native_type: + diagnostics.append( + self._diagnostic( + plan.owner_path, + "inconsistent-array-native-type", + plan.native_call_slot.array_native_type, + ) + ) + if plan.native_call_slot.array_copy_in != plan.array_copy_in: + diagnostics.append( + self._diagnostic(plan.owner_path, "inconsistent-array-copy-in", plan.native_call_slot.array_copy_in) + ) + if plan.native_call_slot.array_copy_out != plan.array_copy_out: + diagnostics.append( + self._diagnostic( + plan.owner_path, + "inconsistent-array-copy-out", + plan.native_call_slot.array_copy_out, + ) + ) return tuple(diagnostics) def _argument_slot_consistency_diagnostics( @@ -1874,6 +2268,10 @@ def _expected_argument_data_action(self, plan: ArgumentTransferPlan) -> BridgeDa """Return the data action implied by completed orthogonal selectors.""" if plan.callback is not None: return BridgeDataAction.DIRECT_TRANSFER + if plan.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY: + return BridgeDataAction.COPY_REPRESENTATION + if plan.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: + return BridgeDataAction.COPY_REPRESENTATION if self._uses_typed_derived_value(plan): return BridgeDataAction.COPY_REPRESENTATION return self._expected_handoff_data_action(plan) @@ -1960,7 +2358,7 @@ def _scalar_boundary_diagnostics( diagnostics.append(self._diagnostic(plan.owner_path, "optional-scalar-address-boundary", action.value)) return tuple(diagnostics) - # Ordinary-array argument validation. + # Ordinary-array and native-array-handle argument validation. def _array_boundary_diagnostics( self, plan: ArgumentTransferPlan, @@ -1980,7 +2378,6 @@ def _array_boundary_diagnostics( ] return tuple(diagnostics) - # Native-array-handle argument validation. def _native_array_handle_argument_diagnostics( self, plan: ArgumentTransferPlan, @@ -2594,7 +2991,12 @@ def _array_buffer_action_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, "invalid-array-handoff-mode", plan.bridge.handoff_mode.value) ) - if plan.bridge.data_action is not BridgeDataAction.ASSOCIATE_VIEW: + expected_data_action = ( + BridgeDataAction.COPY_REPRESENTATION + if plan.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY + else BridgeDataAction.ASSOCIATE_VIEW + ) + if plan.bridge.data_action is not expected_data_action: diagnostics.append( self._diagnostic(plan.owner_path, "invalid-array-data-action", plan.bridge.data_action.value) ) @@ -2777,17 +3179,91 @@ def _array_handoff_role_diagnostics( array = plan.array if array is None: return () + return ( + *self._array_data_role_diagnostics(plan, array), + *self._array_axis_role_diagnostics( + plan.owner_path, + array, + array.extent_reference_tokens, + array.extent_reference_roles, + "invalid-array-extent-token-count", + "invalid-array-extent-reference-count", + "inconsistent-array-extent-references", + ), + *self._array_axis_role_diagnostics( + plan.owner_path, + array, + array.extent_callable_tokens, + array.extent_callable_roles, + "invalid-array-extent-callable-token-count", + "invalid-array-extent-callable-count", + "inconsistent-array-extent-callables", + ), + *self._array_extent_evaluation_diagnostics(plan.owner_path, array), + ) + + def _array_data_role_diagnostics( + self, + plan: ArgumentTransferPlan, + array: ArrayHandoffPlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate the primary data role and nonempty extent producers.""" diagnostics = [] if array.data_role != plan.binding.handoff_role: diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-array-data-role", array.data_role)) if any(not role for role in array.extent_roles): diagnostics.append(self._diagnostic(plan.owner_path, "missing-array-extent-role", array.extent_roles)) - if len(array.extent_reference_roles) != len(array.shape): - diagnostics.append( - self._diagnostic(plan.owner_path, "invalid-array-extent-reference-count", array.extent_reference_roles) - ) return tuple(diagnostics) + def _array_axis_role_diagnostics( + self, + owner_path: str, + array: ArrayHandoffPlan, + tokens: tuple[tuple[str, ...], ...], + roles: tuple[tuple[str, ...], ...], + token_count_code: str, + role_count_code: str, + alignment_code: str, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate one per-axis token and symbolic-role mapping.""" + diagnostics = [] + axis_count = len(array.shape) + if len(roles) != axis_count: + diagnostics.append(self._diagnostic(owner_path, role_count_code, roles)) + if len(tokens) != axis_count: + diagnostics.append(self._diagnostic(owner_path, token_count_code, tokens)) + elif len(roles) == axis_count and not self._array_axis_roles_align(tokens, roles, axis_count): + diagnostics.append(self._diagnostic(owner_path, alignment_code, tokens)) + return tuple(diagnostics) + + def _array_extent_evaluation_diagnostics( + self, + owner_path: str, + array: ArrayHandoffPlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Require each axis to name the backend that can evaluate its extent.""" + if len(array.extent_evaluation) != len(array.shape) or any( + evaluation not in {"binding", "bridge"} for evaluation in array.extent_evaluation + ): + return (self._diagnostic(owner_path, "invalid-array-extent-evaluation", array.extent_evaluation),) + if len(array.extent_callable_roles) == len(array.shape) and any( + (evaluation == "bridge") != bool(callables) + for evaluation, callables in zip(array.extent_evaluation, array.extent_callable_roles, strict=True) + ): + return (self._diagnostic(owner_path, "inconsistent-array-extent-evaluation", array.extent_evaluation),) + return () + + @staticmethod + def _array_axis_roles_align( + tokens: tuple[tuple[str, ...], ...], + roles: tuple[tuple[str, ...], ...], + axis_count: int, + ) -> bool: + """Return whether every axis has aligned expression tokens and roles.""" + if len(tokens) != axis_count or len(roles) != axis_count: + return False + return all(len(axis_tokens) == len(axis_roles) for axis_tokens, axis_roles in zip(tokens, roles, strict=True)) + def _array_itemsize_diagnostics( self, plan: ArgumentTransferPlan, @@ -3238,6 +3714,7 @@ def _optional_native_diagnostics( ) return tuple(diagnostics) + # Result validation: general result graph, then typed result families. def _result_diagnostics( self, plan: ResultPlan, @@ -3269,6 +3746,8 @@ def _result_diagnostics( diagnostics.append(self._diagnostic(plan.owner_path, "unknown-result-source", plan.source_kind)) diagnostics.extend(self._direct_result_abi_diagnostics(plan)) diagnostics.extend(self._result_family_diagnostics(plan)) + if plan.array is not None: + diagnostics.extend(self._array_extent_reference_diagnostics(plan, available_roles)) return tuple(diagnostics) def _direct_result_abi_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagnostic, ...]: @@ -3518,6 +3997,11 @@ def _string_result_ownership_diagnostics(self, plan: ResultPlan) -> tuple[Wrappe return tuple(diagnostics) def _nonstring_result_length_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagnostic, ...]: + """Reject a character length copied onto a non-string result plan. + + ``None`` is the only valid non-string value. The returned diagnostic + captures projection drift without reclassifying the result family. + """ if plan.character_length is None: return () return ( @@ -3529,6 +4013,12 @@ def _nonstring_result_length_diagnostics(self, plan: ResultPlan) -> tuple[Wrappe ) def _direct_string_result_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate the direct-return fixed-string actions already in the plan. + + Direct strings copy into Python without a native output address. This + helper only compares those two stored actions and returns diagnostics + in the function result's established order. + """ diagnostics = [] if plan.binding.codegen_action is not CodegenAction.COPY_OUT: diagnostics.append( @@ -3549,6 +4039,12 @@ def _direct_string_result_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPl return tuple(diagnostics) def _hidden_string_result_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate the hidden-output fixed-string actions already in the plan. + + Hidden strings copy into Python and cross the native boundary through + their call-local address. The helper reports mismatches but does not + allocate or replace the missing slot. + """ diagnostics = [] if plan.binding.codegen_action is not CodegenAction.COPY_OUT: diagnostics.append( @@ -3573,11 +4069,23 @@ def _result_role_diagnostics( plan: ResultPlan, available_roles: tuple[str, ...], ) -> tuple[WrapperPlanDiagnostic, ...]: + """Require a result's native producer role to be advertised by its function. + + ``available_roles`` is the function-wide role list already projected + by planning. The returned diagnostic identifies a consumer whose + stored producer is unavailable; no role is added here. + """ if plan.bridge.native_result_role not in available_roles: return (self._diagnostic(plan.owner_path, "unavailable-result-role", plan.bridge.native_result_role),) return () def _direct_result_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate a direct result's absence of slots and data-action choice. + + Direct results must not own a native call slot or ABI position. Their + stored object kind and descriptor state determine the required + completed bridge action, which is compared without altering the plan. + """ diagnostics = [] if plan.native_call_slot is not None or plan.bridge.abi_position is not None: diagnostics.append(self._diagnostic(plan.owner_path, "direct-result-has-native-slot", plan.source_kind)) @@ -3598,6 +4106,13 @@ def _hidden_result_diagnostics( plan: ResultPlan, function_slots: dict[int, NativeCallSlotPlan], ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate a hidden result's shared slot and function-wide registration. + + ``function_slots`` indexes the function's native slots by ABI position. + A hidden result must reference the exact indexed slot and then satisfy + its shape and completed-action checks. Missing or mismatched records + become diagnostics rather than replacement slots. + """ if plan.native_call_slot is None or plan.bridge.abi_position is None: return (self._diagnostic(plan.owner_path, "missing-result-native-slot", plan.bridge.native_name),) slot = plan.native_call_slot @@ -3822,14 +4337,40 @@ def _array_result_rank_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanD def _array_result_shape_count_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagnostic, ...]: """Require one shape and extent role per result axis.""" array = plan.array - if ( - array is not None - and array.rank is not None - and (len(array.shape) != array.rank or len(array.extent_roles) != array.rank) + if array is None or array.rank is None: + return () + if len(array.shape) != array.rank or len(array.extent_roles) != array.rank: + return (self._diagnostic(plan.owner_path, "inconsistent-array-result-shape", array.shape),) + if not self._array_axis_roles_align( + array.extent_reference_tokens, + array.extent_reference_roles, + array.rank, + ): + return (self._diagnostic(plan.owner_path, "inconsistent-array-result-shape", array.shape),) + if not self._array_axis_roles_align( + array.extent_callable_tokens, + array.extent_callable_roles, + array.rank, ): return (self._diagnostic(plan.owner_path, "inconsistent-array-result-shape", array.shape),) + if not self._array_extent_evaluation_is_consistent(array): + return (self._diagnostic(plan.owner_path, "inconsistent-array-result-shape", array.shape),) return () + @staticmethod + def _array_extent_evaluation_is_consistent(array: ArrayHandoffPlan) -> bool: + """Return whether every axis uses bridge evaluation exactly for native calls.""" + if len(array.extent_evaluation) != len(array.shape): + return False + return all( + evaluation in {"binding", "bridge"} and ((evaluation == "bridge") == bool(callables)) + for evaluation, callables in zip( + array.extent_evaluation, + array.extent_callable_roles, + strict=True, + ) + ) + def _array_result_extent_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagnostic, ...]: """Reject unresolved ordinary array result extent spellings.""" array = plan.array @@ -3882,8 +4423,15 @@ def _array_result_source_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPla @staticmethod def _is_scalar_storage_array(array) -> bool: + """Return whether an array facet denotes rank-zero scalar storage. + + The predicate is shared by argument and result validation to select + only already-planned scalar-storage rules. ``None`` and all other + array categories return ``False`` without mutation. + """ return bool(array is not None and array.rank == 0 and array.category == SCALAR_STORAGE_CATEGORY) + # Native-call-slot and generic lifecycle validation. def _native_slot_diagnostics(self, plan: NativeCallSlotPlan) -> tuple[WrapperPlanDiagnostic, ...]: """Return hidden literal and hidden result slot diagnostics.""" diagnostics = list( @@ -3977,6 +4525,7 @@ def _result_slot_data_action_diagnostics( BridgeDataAction.COPY_REPRESENTATION if plan.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY, ObjectKind.DERIVED_TYPE} or plan.scalar_descriptor is not None + or plan.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY else BridgeDataAction.DIRECT_TRANSFER ) if plan.bridge_data_action is not expected: @@ -4400,13 +4949,12 @@ def _sequence_diagnostics( diagnostics.append(self._diagnostic(owner_path, code, position)) return tuple(diagnostics) + # Function-wide symbolic-role validation. def _duplicate_role_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnostic, ...]: """Return duplicate symbolic producer/consumer role diagnostics.""" - roles = [argument.binding.handoff_role for argument in plan.arguments] - roles.extend(slot.symbolic_role for slot in plan.native_call_slots if slot.source_kind == "literal") - roles.extend(slot.symbolic_role for slot in plan.native_call_slots if slot.source_kind == "result") - roles.extend( - result.bridge.native_result_role for result in plan.results if result.source_kind == "direct_return" + roles = ( + *self._expected_available_roles(plan), + *self._native_slot_roles(plan.native_call_slots, "literal"), ) return tuple( self._diagnostic(plan.owner_path, "duplicate-symbolic-role", role) @@ -4416,28 +4964,86 @@ def _duplicate_role_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDi def _available_role_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnostic, ...]: """Require the advertised roles to match argument and result producers.""" - expected = [argument.binding.handoff_role for argument in plan.arguments] - expected.extend( + expected = self._expected_available_roles(plan) + if Counter(plan.available_roles) != Counter(expected): + return (self._diagnostic(plan.owner_path, "inconsistent-available-roles", plan.available_roles),) + return () + + def _expected_available_roles(self, plan: FunctionPlan) -> tuple[str, ...]: + """Return every role advertised after binding conversion or the native call.""" + return ( + *self._argument_handoff_roles(plan.arguments), + *self._argument_extent_roles(plan.arguments), + *self._argument_descriptor_output_roles(plan.arguments), + *self._native_slot_roles(plan.native_call_slots, "result"), + *self._direct_result_roles(plan.results), + *self._declaration_callable_roles(plan.declaration_callables), + ) + + @staticmethod + def _argument_handoff_roles(arguments: tuple[ArgumentTransferPlan, ...]) -> tuple[str, ...]: + """Return the primary binding-produced role for every argument.""" + return tuple(argument.binding.handoff_role for argument in arguments) + + @staticmethod + def _argument_descriptor_output_roles(arguments: tuple[ArgumentTransferPlan, ...]) -> tuple[str, ...]: + """Return optional descriptor roles produced during argument conversion.""" + return tuple( role - for argument in plan.arguments + for argument in arguments for role in ( argument.bridge.descriptor_output_role, argument.bridge.descriptor_output_presence_role, ) if role is not None ) - expected.extend(slot.symbolic_role for slot in plan.native_call_slots if slot.source_kind == "result") - expected.extend( - result.bridge.native_result_role for result in plan.results if result.source_kind == "direct_return" + + @staticmethod + def _native_slot_roles( + slots: tuple[NativeCallSlotPlan, ...], + source_kind: str, + ) -> tuple[str, ...]: + """Return symbolic roles produced by one native-slot category.""" + return tuple(slot.symbolic_role for slot in slots if slot.source_kind == source_kind) + + @staticmethod + def _direct_result_roles(results: tuple[ResultPlan, ...]) -> tuple[str, ...]: + """Return native result roles produced by direct-return plans.""" + return tuple(result.bridge.native_result_role for result in results if result.source_kind == "direct_return") + + @staticmethod + def _declaration_callable_roles( + declarations: tuple[DeclarationCallablePlan, ...], + ) -> tuple[str, ...]: + """Return bridge-resolved declaration-callable symbol roles.""" + return tuple(item.symbolic_role for item in declarations) + + @staticmethod + def _argument_extent_roles(arguments: tuple[ArgumentTransferPlan, ...]) -> tuple[str, ...]: + """Return every binding-produced array extent role in argument order.""" + return tuple( + role + for argument in arguments + for role in (argument.array.extent_roles if argument.array is not None else ()) ) - if Counter(plan.available_roles) != Counter(expected): - return (self._diagnostic(plan.owner_path, "inconsistent-available-roles", plan.available_roles),) - return () + # Diagnostic formatting and rendered-artifact assembly. def _diagnostic(self, owner_path: str, code: str, detail: object) -> WrapperPlanDiagnostic: + """Create one normalized diagnostic from an owner, stable code, and detail. + + All validation paths use this helper so summaries preserve one string + representation of arbitrary details. It has no logging or mutation + side effect. + """ return WrapperPlanDiagnostic(owner_path, code, str(detail)) def _diagnostic_summary(self, diagnostics: tuple[WrapperPlanDiagnostic, ...]) -> str: + """Format ordered diagnostics into the public generation failure message. + + ``diagnostics`` must already be in collection order. The method joins + each owner-local record without sorting or deduplicating it, preserving + the error text established by the validation traversal. + """ details = "; ".join(f"{item.owner_path}:{item.code}:{item.message}" for item in diagnostics) return f"Invalid edited wrapper plan before generation: {details}" @@ -4450,6 +5056,14 @@ def _rendered_artifacts( native_support_keys: tuple[str, ...], required_headers: tuple[str, ...], ) -> RenderedGeneratedWrapperArtifacts: + """Package rendered source text with the filenames owned by build integration. + + Binding translation-unit paths preserve the primary file followed by + zero-padded worker shards. The returned artifacts place bridge, C + sources, and header text in that stable order; this helper does not + write files or freeze the newly assembled artifact records. + """ + # Name bridge, binding, and header files before pairing each with rendered text. binding_sources = ( Path(f"{module_name}_wrapper.c"), *(Path(f"{module_name}_wrapper_{index:03d}.c") for index in range(1, len(c_sources))), @@ -4462,6 +5076,8 @@ def _rendered_artifacts( native_support_keys=native_support_keys, required_headers=required_headers, ) + + # Preserve build-consumed source ordering: bridge, binding units, then header. return RenderedGeneratedWrapperArtifacts( artifacts=artifacts, extension_init_name=f"PyInit_{module_name}", @@ -4474,3 +5090,27 @@ def _rendered_artifacts( GeneratedSourceFile(artifacts.header_files[0], c_header), ), ) + + +if __name__ == "__main__": + from prik.codegen.planner import WrapperPlanner + from prik.semantics.models import SemanticArgument, SemanticFunction, SemanticModule, SemanticType + from prik.semantics.policy_completion import complete_semantic_policies + + module = SemanticModule( + name="generator_demo", + functions=[ + SemanticFunction( + name="double_value", + native_name="DOUBLE_VALUE", + arguments=[SemanticArgument("value", SemanticType("Float64"))], + return_type=SemanticType("Float64"), + ) + ], + ) + complete_semantic_policies(module) + rendered = WrapperCodeGenerator().generate(WrapperPlanner().build(module)) + + print(f"Extension initializer: {rendered.extension_init_name}") + print("Rendered sources:", ", ".join(source.path.name for source in rendered.sources)) + print("Native support:", ", ".join(rendered.artifacts.native_support_keys) or "none") diff --git a/prik/wrapper_codegen/naming.py b/prik/codegen/naming.py similarity index 100% rename from prik/wrapper_codegen/naming.py rename to prik/codegen/naming.py diff --git a/prik/wrapper_codegen/nodes.py b/prik/codegen/nodes.py similarity index 98% rename from prik/wrapper_codegen/nodes.py rename to prik/codegen/nodes.py index fed658ec8..428808058 100644 --- a/prik/wrapper_codegen/nodes.py +++ b/prik/codegen/nodes.py @@ -377,6 +377,7 @@ class FortranInterfaceProcedure(StageRecord): is_subroutine: bool = False bind_name: str | None = None bind_c: bool = False + pure: bool = False @dataclass @@ -424,5 +425,6 @@ class FortranModule(StageRecord): uses: tuple[FortranUse, ...] = () type_definitions: tuple[FortranTypeDefinition, ...] = () interfaces: tuple[FortranInterface, ...] = () + declarations: tuple[FortranDeclaration, ...] = () procedures: tuple[FortranFunction, ...] = () - external_procedures: tuple[FortranFunction, ...] = () + standalone_procedures: tuple[FortranFunction, ...] = () diff --git a/prik/wrapper_codegen/plan.py b/prik/codegen/plan.py similarity index 52% rename from prik/wrapper_codegen/plan.py rename to prik/codegen/plan.py index a2878dcfc..5d98b20c9 100644 --- a/prik/wrapper_codegen/plan.py +++ b/prik/codegen/plan.py @@ -1,4 +1,16 @@ -"""Editable wrapper-plan records for the isolated wrapper-plan route.""" +"""Typed, editable records shared by wrapper planning and direct generation. + +``WrapperPlanner`` constructs these records only after post-IR policy has made +every ownership, transfer, and projection decision. The generator then +validates and freezes the same object graph before binding or bridge lowering. +This module deliberately models those completed facts; it never infers policy +from a datatype, native ``intent``, shape, or backend-local condition. + +Records appear in plan-tree order: shared vocabulary; derived and class +surfaces; array and descriptor facets; module and procedure views; callback +and transfer records; then module-level orchestration. Consumers normally use +the ``ModulePlan`` root returned by :class:`prik.codegen.WrapperPlanner`. +""" from __future__ import annotations @@ -21,6 +33,7 @@ from prik.semantics.wrapper_policy import ( ArgumentConversionPhase, ArgumentHandoffMode, + ArrayLogicalABI, ArrayWritebackABI, BridgeDataAction, CallbackABIKind, @@ -49,6 +62,7 @@ DerivedTargetLifetime, DerivedWriteback, DirectResultABI, + DeclarationCallableAction, ExternalDeclarationMode, ModuleGetterAction, ModuleObjectAccessMechanism, @@ -69,6 +83,7 @@ NativeArraySourceKind, NativeDescriptorHandoffABI, NativeInvocationKind, + ScalarLogicalABI, OptionalMode, PythonExceptionKind, TransformationAction, @@ -78,8 +93,18 @@ from prik.stage_values import StageRecord +# ============================================================================ +# Shared plan vocabulary +# ============================================================================ + + class DatatypeFamily(Enum): - """Backend-relevant datatype family copied from semantic type facts.""" + """Classify a completed value for the datatype-specific validation route. + + The planner copies this coarse family from semantic type facts onto + transfer records. Validators and backend dispatch use it alongside the + already-completed action selectors; it does not by itself choose policy. + """ BOOL = "bool" INTEGER = "integer" @@ -90,9 +115,19 @@ class DatatypeFamily(Enum): CALLBACK = "callback" +# ============================================================================ +# Derived types and generated class surfaces +# ============================================================================ + + @dataclass class DerivedHandoffPlan(StageRecord): - """Editable scalar-derived identity, origin, and lifetime facet.""" + """Store completed identity, origin, ownership, and ABI facts for a derived value. + + Derived fields, arguments, results, and module objects reference this + facet. The generator consumes its preselected retention, release, storage, + and native-handoff values without rediscovering lifetime policy. + """ type_name: str type_identity: tuple[str, str] @@ -111,7 +146,11 @@ class DerivedHandoffPlan(StageRecord): @dataclass class DerivedCallCasePlan(StageRecord): - """Editable exhaustive actual-storage/dummy-form compatibility cell.""" + """Describe one completed actual-storage and dummy-category compatibility case. + + ``DerivedCallPlan`` keeps these cells in the policy-established order so + lowering can select the recorded ABI code, access mode, and failure path. + """ actual_storage: DerivedObjectStorage action: DerivedCallAction @@ -125,7 +164,11 @@ class DerivedCallCasePlan(StageRecord): @dataclass class DerivedCallPlan(StageRecord): - """Dummy-driven scalar-derived matrix and ordered transaction facts.""" + """Collect a derived dummy's compatibility matrix and transaction ordering. + + Argument transfers reference this record when a derived actual needs + dummy-driven access, writeback, acquisition, or cleanup behavior. + """ dummy_category: DerivedDummyCategory cases: tuple[DerivedCallCasePlan, ...] @@ -138,7 +181,12 @@ class DerivedCallPlan(StageRecord): @dataclass class DerivedFieldPlan(StageRecord): - """One field policy subordinate to its owning derived type.""" + """Represent one completed field surface within a derived type or class. + + Array, native-handle, and nested-derived facets are attached only when + selected by policy. Printers and lowerers consume the stored getter, + setter, assignment, and role choices directly. + """ owner_path: str name: str @@ -163,7 +211,11 @@ class DerivedFieldPlan(StageRecord): @dataclass class DerivedMemberPathPlan(StageRecord): - """One typed finite module-proxy member path.""" + """Map one finite Python member path to its declaring type and native path. + + Module-object access uses this immutable path description to expose a + known field without searching or inferring members during generation. + """ path: tuple[str, ...] native_path: tuple[str, ...] @@ -174,7 +226,11 @@ class DerivedMemberPathPlan(StageRecord): @dataclass class DerivedTypePlan(StageRecord): - """One namespace-owned opaque derived runtime type definition.""" + """Describe one namespace-owned runtime wrapper type for a native derived type. + + The planner supplies identity, native naming, fields, and finalizers; + generated class assembly uses this record as the authoritative type shape. + """ owner_path: str type_name: str @@ -191,7 +247,7 @@ class DerivedTypePlan(StageRecord): @dataclass class ConstructorFieldPlan(StageRecord): - """One editable keyword-only generated-constructor field.""" + """Describe one generated constructor keyword and its completed setter action.""" owner_path: str name: str @@ -201,7 +257,12 @@ class ConstructorFieldPlan(StageRecord): @dataclass class ConstructorPlan(StageRecord): - """Editable constructor selection and owned-instance lifecycle.""" + """Record a class constructor route, target, and owned-instance lifecycle. + + A class surface uses either explicit fields, a concrete function target, or + an overload. Rejection text and lifecycle actions are already decided + before this record reaches generation. + """ kind: ClassConstructorKind fields: tuple[ConstructorFieldPlan, ...] @@ -216,7 +277,11 @@ class ConstructorPlan(StageRecord): @dataclass class ClassMethodPlan(StageRecord): - """One class descriptor linked to an ordinary editable function plan.""" + """Link one Python class descriptor to its ordinary function plan. + + ``kind`` and ``passed_object_position`` preserve the completed receiver + convention while the referenced function retains the common call details. + """ owner_path: str python_name: str @@ -229,7 +294,7 @@ class ClassMethodPlan(StageRecord): @dataclass class OverloadArgumentMatchPlan(StageRecord): - """One editable exact-type predicate for overload dispatch.""" + """Store one exact argument predicate selected for overload dispatch.""" python_name: str kind: OverloadMatchKind @@ -242,7 +307,11 @@ class OverloadArgumentMatchPlan(StageRecord): @dataclass class OverloadPlan(StageRecord): - """One exact-match overload and its concrete editable candidates.""" + """Describe an exact-match overload and the function candidates it owns. + + Candidate and match tuples remain parallel in planner order. Class and + namespace surfaces consume this record to emit one deterministic dispatch. + """ owner_path: str python_name: str @@ -257,7 +326,11 @@ class OverloadPlan(StageRecord): @dataclass class ClassSurfacePlan(StageRecord): - """Namespace-owned generated class composed over a derived type plan.""" + """Compose one namespace-owned Python class over a completed derived-type plan. + + Constructor, methods, overloads, registration, and rendered documentation + are all stored here so class lowering has no semantic discovery work. + """ owner_path: str type_identity: tuple[str, str] @@ -272,7 +345,7 @@ class ClassSurfacePlan(StageRecord): @dataclass class DerivedModuleObjectPlan(StageRecord): - """Live derived module-object access subordinate to module state.""" + """Describe live module-state access for a derived object and its members.""" handoff: DerivedHandoffPlan access: ModuleObjectAccessMechanism @@ -280,9 +353,19 @@ class DerivedModuleObjectPlan(StageRecord): member_paths: tuple[DerivedMemberPathPlan, ...] +# ============================================================================ +# Array buffers, descriptors, and native handles +# ============================================================================ + + @dataclass class ArrayHandoffPlan(StageRecord): - """Editable array storage or raw-pointee layout and ABI roles.""" + """Store completed array layout, extent, and ABI handoff roles. + + Array transfers, results, fields, and descriptor handles share this record. + Its role tuples are already bound to visible producers, so backends render + them without evaluating declaration expressions or inventing extents. + """ rank: int | None shape: tuple[str, ...] @@ -296,17 +379,26 @@ class ArrayHandoffPlan(StageRecord): category: str | None data_role: str extent_roles: tuple[str, ...] + extent_reference_tokens: tuple[tuple[str, ...], ...] = () extent_reference_roles: tuple[tuple[str, ...], ...] = () + extent_callable_tokens: tuple[tuple[str, ...], ...] = () + extent_callable_roles: tuple[tuple[str, ...], ...] = () + extent_evaluation: tuple[str, ...] = () upper_bound_roles: tuple[str, ...] = () stride_roles: tuple[str, ...] = () dense_actual_role: str | None = None runtime_rank_role: str | None = None itemsize_role: str | None = None + display_shape: tuple[str, ...] = () @dataclass class NativeArrayActualPlan(StageRecord): - """Editable accepted-source facts for the ordinary array-buffer ABI.""" + """Describe the accepted Python source and validation contract for one array ABI. + + The binding uses these precomputed source, dtype, layout, and mutability + requirements when extracting an ordinary array-buffer actual. + """ accepted_sources: tuple[NativeArraySourceKind, ...] dtype: str @@ -323,7 +415,11 @@ class NativeArrayActualPlan(StageRecord): @dataclass class NativeDescriptorHandoffPlan(StageRecord): - """Editable descriptor ABI roles subordinate to one native handle.""" + """Store the descriptor-ABI roles that carry one native-array handle. + + This facet is subordinate to ``NativeArrayHandlePlan`` and names every + descriptor component required by the selected ABI and operations. + """ abi: NativeDescriptorHandoffABI descriptor_pointer_role: str | None @@ -340,7 +436,12 @@ class NativeDescriptorHandoffPlan(StageRecord): @dataclass class NativeArrayDefaultHandlePlan(StageRecord): - """Caller-created descriptor storage and lifecycle selected before lowering.""" + """Describe caller-created descriptor storage and its completed lifecycle. + + Native-array defaults use this only when policy selected caller construction + instead of a native-produced handle; lowerers follow its operations and + release behavior verbatim. + """ construction: NativeArrayDefaultConstruction descriptor_ownership: NativeArrayDescriptorOwnership | None @@ -353,7 +454,12 @@ class NativeArrayDefaultHandlePlan(StageRecord): @dataclass class NativeArrayHandlePlan(StageRecord): - """One typed editable native-array handle policy and descriptor handoff.""" + """Represent one completed native-array handle policy and descriptor handoff. + + The record joins identity, ownership, nullability, getter/setter behavior, + allocation, release, descriptor ABI, and required headers. It is the single + source for handle lowering; no backend may derive missing choices locally. + """ descriptor_kind: NativeArrayDescriptorKind handle_kind: NativeArrayHandleKind @@ -384,7 +490,7 @@ class NativeArrayHandlePlan(StageRecord): @dataclass class ScalarDescriptorResultPlan(StageRecord): - """Editable nullable rank-zero descriptor result copy facts.""" + """Describe a nullable rank-zero descriptor result and its copy/release contract.""" descriptor_kind: NativeArrayDescriptorKind runtime_length: bool @@ -396,7 +502,12 @@ class ScalarDescriptorResultPlan(StageRecord): @dataclass class TransformationPlan(StageRecord): - """One explicitly layer-owned transformation subordinate to a transfer.""" + """Record one explicitly layer-owned representation transformation. + + Transfers list transformations in policy order. ``phase`` and ``layer`` + make the owner of each conversion and its reason visible to validation and + generation. + """ phase: WritebackPhase layer: TransformationLayer @@ -406,9 +517,18 @@ class TransformationPlan(StageRecord): reason: str +# ============================================================================ +# Module, procedure, and native-call views +# ============================================================================ + + @dataclass class BindingStatusErrorPlan(StageRecord): - """Binding-owned post-call native status projection.""" + """Describe binding-owned conversion of a completed native status into an exception. + + Function plans attach this optional facet when policy selected status and + message projection after the native call completes. + """ status_role: str message_role: str | None @@ -418,21 +538,25 @@ class BindingStatusErrorPlan(StageRecord): @dataclass class BindingModulePlan(StageRecord): - """Binding-facing module facts.""" + """Store the binding-facing owner identity for one generated module.""" owner_path: str @dataclass class BridgeModulePlan(StageRecord): - """Bridge-facing module facts.""" + """Store the bridge-facing owner identity for one generated module.""" owner_path: str @dataclass class BindingModuleVariablePlan(StageRecord): - """Python module-attribute behavior for one state value.""" + """Describe Python module-attribute access and initialization for one value. + + ``python_names`` retains every public spelling. The binding consumes the + completed getter and setter actions plus the selected initializer/value. + """ python_names: tuple[str, ...] getter_action: ModuleGetterAction @@ -443,7 +567,11 @@ class BindingModuleVariablePlan(StageRecord): @dataclass class BridgeModuleVariablePlan(StageRecord): - """Native module-variable access behavior selected by completed policy.""" + """Describe native module-variable access selected by completed policy. + + Binding and bridge views remain separate: this record contains native names, + assignment behavior, descriptor form, and symbolic getter/setter roles. + """ native_name: str native_module: str @@ -456,7 +584,11 @@ class BridgeModuleVariablePlan(StageRecord): @dataclass class ModuleVariablePlan(StageRecord): - """One concise shared module-variable plan.""" + """Join binding and bridge views of one module-state value. + + Optional array, native-handle, and derived-object facets are attached only + when policy selected them. Namespace plans own these records for emission. + """ owner_path: str symbol_name: str @@ -472,7 +604,11 @@ class ModuleVariablePlan(StageRecord): @dataclass class BindingFunctionPlan(StageRecord): - """Binding-facing function facts.""" + """Store Python-visible call behavior for one generated binding function. + + The planner fixes public naming, documentation, GIL handling, optional + status projection, and argument-conversion order before generation. + """ python_name: str docstring: str @@ -484,12 +620,16 @@ class BindingFunctionPlan(StageRecord): @dataclass class BridgeFunctionPlan(StageRecord): - """Bridge-facing function facts.""" + """Store native invocation and declaration facts for one bridge procedure. + + The bridge dispatches its recorded invocation, standalone, and external + declaration mode; it does not infer a native interface from the call. + """ native_name: str native_invocation: NativeInvocationKind native_operator: str | None - external: bool + standalone: bool external_declaration: ExternalDeclarationMode native_module: str | None native_is_subroutine: bool @@ -497,7 +637,7 @@ class BridgeFunctionPlan(StageRecord): @dataclass class ClassCallPlan(StageRecord): - """Native receiver and invocation facts for one class-owned call.""" + """Describe the completed receiver and invocation route for one class-owned call.""" kind: ClassMethodKind passed_object_position: int | None @@ -507,7 +647,11 @@ class ClassCallPlan(StageRecord): @dataclass class BindingArgumentPlan(StageRecord): - """Python input conversion and binding-to-bridge handoff facts.""" + """Describe Python input conversion and the binding-to-bridge handoff. + + Argument transfers own this binding view. Its barrier action, conversion + phase, optionality, mutability, and symbolic role are complete policy facts. + """ python_name: str python_action: PythonBarrierAction @@ -523,7 +667,12 @@ class BindingArgumentPlan(StageRecord): @dataclass class BridgeArgumentPlan(StageRecord): - """Bridge ABI and native argument conversion facts.""" + """Describe bridge ABI transport and native argument conversion. + + The transfer's matching binding view supplies the same handoff role; this + record adds the native ABI position, data action, copy reason, and optional + descriptor-output roles required by bridge lowering. + """ native_name: str native_action: NativeBarrierAction @@ -542,7 +691,7 @@ class BridgeArgumentPlan(StageRecord): @dataclass class BindingResultPlan(StageRecord): - """Binding-facing result projection facts.""" + """Describe binding-side projection of one completed native result.""" codegen_action: CodegenAction python_action: PythonBarrierAction @@ -551,7 +700,7 @@ class BindingResultPlan(StageRecord): @dataclass class BridgeResultPlan(StageRecord): - """Bridge-facing result production facts.""" + """Describe bridge-side production, ABI transport, and data action for one result.""" codegen_action: CodegenAction native_action: NativeBarrierAction @@ -564,7 +713,7 @@ class BridgeResultPlan(StageRecord): @dataclass class BindingLifecyclePlan(StageRecord): - """Binding-owned lifecycle action facts.""" + """Describe the binding-owned portion of one ordered lifecycle action.""" source_role: str codegen_action: CodegenAction @@ -577,14 +726,19 @@ class BindingLifecyclePlan(StageRecord): @dataclass class BridgeLifecyclePlan(StageRecord): - """Bridge-owned lifecycle action facts.""" + """Describe the bridge-owned symbolic source for one lifecycle action.""" source_role: str @dataclass class NativeCallSlotPlan(StageRecord): - """One ordered ABI slot referenced by its owning transfer when applicable.""" + """Represent one ordered native ABI slot shared with its owning transfer. + + Function plans index slots in native-call order, while argument and hidden + result transfers hold references to the same mutable records. The shared + identity is intentional: validation checks the views agree before freezing. + """ owner_path: str native_position: int @@ -599,6 +753,12 @@ class NativeCallSlotPlan(StageRecord): bridge_data_action: BridgeDataAction bridge_copy_reason: str | None object_kind: ObjectKind | None + scalar_logical_abi: ScalarLogicalABI = ScalarLogicalABI.NOT_APPLICABLE + scalar_native_type: str | None = None + array_logical_abi: ArrayLogicalABI = ArrayLogicalABI.NOT_APPLICABLE + array_native_type: str | None = None + array_copy_in: bool = False + array_copy_out: bool = False literal_type: str | None = None literal_value: Any = None result_position: int | None = None @@ -613,7 +773,7 @@ class NativeCallSlotPlan(StageRecord): @dataclass class PolymorphicVariantPlan(StageRecord): - """One concrete class accepted by an enumerated polymorphic input.""" + """Describe one concrete derived type accepted by an enumerated polymorphic input.""" type_identity: tuple[str, str] backend_symbol: str @@ -623,15 +783,69 @@ class PolymorphicVariantPlan(StageRecord): @dataclass class PolymorphicDispatchPlan(StageRecord): - """Stable concrete-type dispatch for one scalar input dummy.""" + """Store stable concrete-type dispatch variants for one scalar input dummy.""" owner_path: str variants: tuple[PolymorphicVariantPlan, ...] +@dataclass +class ProcedurePrototypeArgumentPlan(StageRecord): + """Describe exact native dummy characteristics shared by every prototype use. + + Prototype declarations consume this record when they need an abstract + interface or a matching concrete native procedure entity. + """ + + owner_path: str + name: str + semantic_type_name: str + rank: int + passed_by_value: bool + intent: str | None + character_length: int | None + array: ArrayHandoffPlan | None + derived_type_identity: tuple[str, str] | None + derived_backend_symbol: str | None + + +@dataclass +class ProcedurePrototypeResultPlan(StageRecord): + """Describe exact native function-result characteristics for one prototype.""" + + owner_path: str + semantic_type_name: str + rank: int + character_length: int | None + array: ArrayHandoffPlan | None + derived_type_identity: tuple[str, str] | None + derived_backend_symbol: str | None + + +@dataclass +class ProcedurePrototypePlan(StageRecord): + """Represent one reusable exact native signature and abstract-interface symbol. + + Callback and standalone declaration paths share this normalized prototype. + Its ``interface_symbol`` avoids collisions with a concrete native entity. + """ + + owner_path: str + name: str + identity: str + interface_symbol: str + pure: bool + arguments: tuple[ProcedurePrototypeArgumentPlan, ...] + result: ProcedurePrototypeResultPlan | None + + @dataclass class CallbackTransferPlan(StageRecord): - """One typed native-to-Python transfer inside a callback adapter.""" + """Describe one typed native-to-Python transfer inside a callback adapter. + + Callback handoff records preserve the exact native characteristics and the + completed adapter/Python actions for each argument or result transfer. + """ owner_path: str name: str @@ -639,6 +853,7 @@ class CallbackTransferPlan(StageRecord): object_kind: ObjectKind rank: int passed_by_value: bool + intent: str | None abi: CallbackABIKind adapter_action: CallbackTransferAction python_action: PythonBarrierAction @@ -653,7 +868,7 @@ class CallbackTransferPlan(StageRecord): @dataclass class CallbackResultPlan(StageRecord): - """Typed callback result conversion and its optional transfer.""" + """Join an optional callback result transfer to its completed result action.""" transfer: CallbackTransferPlan | None action: CallbackResultAction @@ -661,12 +876,14 @@ class CallbackResultPlan(StageRecord): @dataclass class CallbackHandoffPlan(StageRecord): - """Call-scoped callback context, symbols, transfers, and fatal contract.""" + """Describe one call-scoped callback context, adapter, transfers, and fatal contract. + + This record gives callback lowering its generated symbols plus already + completed lifecycle, thread, GIL, and fatal-error actions. + """ owner_path: str - prototype_name: str - prototype_module: str | None - declaration_mode: ExternalDeclarationMode + prototype: ProcedurePrototypePlan context_type_symbol: str context_current_symbol: str adapter_symbol: str @@ -680,9 +897,20 @@ class CallbackHandoffPlan(StageRecord): fatal_action: CallbackFatalAction +# ============================================================================ +# Shared transfers, lifecycle, and plan-tree orchestration +# ============================================================================ + + @dataclass class ArgumentTransferPlan(StageRecord): - """One shared Python-to-native transfer, including its native-call slot.""" + """Represent one complete Python-to-native transfer and its shared ABI slot. + + This is the primary datatype-varying plan record. It combines completed + ownership, storage, nullability, mutation, projection, ABI, optional + array/derived/callback facets, and binding/bridge views. The planner shares + ``native_call_slot`` with ``FunctionPlan.native_call_slots`` by identity. + """ owner_path: str python_position: int @@ -690,6 +918,12 @@ class ArgumentTransferPlan(StageRecord): semantic_type_name: str datatype_family: DatatypeFamily character_length: int | None + scalar_logical_abi: ScalarLogicalABI + scalar_native_type: str | None + array_logical_abi: ArrayLogicalABI + array_native_type: str | None + array_copy_in: bool + array_copy_out: bool array_writeback_abi: ArrayWritebackABI object_kind: ObjectKind ownership_owner: OwnershipOwner @@ -717,7 +951,12 @@ class ArgumentTransferPlan(StageRecord): @dataclass class ResultPlan(StageRecord): - """One native-to-Python transfer and its hidden ABI slot when applicable.""" + """Represent one complete native-to-Python transfer and optional hidden ABI slot. + + Direct function results omit ``native_call_slot``; hidden output results + share the corresponding function-wide slot. Binding and bridge facets hold + the completed projection and production choices consumed by each backend. + """ owner_path: str semantic_type_name: str @@ -745,7 +984,12 @@ class ResultPlan(StageRecord): @dataclass class LifecycleActionPlan(StageRecord): - """One transfer-owned action kept in function-wide execution order.""" + """Record one transfer-owned action in a function-wide execution sequence. + + Functions keep writeback, cleanup, and release tuples separately in their + completed order. Optional binding or bridge facets state which backend owns + the operation without duplicating the transfer policy. + """ owner_path: str phase: WritebackPhase @@ -762,7 +1006,12 @@ class LifecycleActionPlan(StageRecord): @dataclass class FunctionPlan(StageRecord): - """Stable orchestration plus ordered ABI and lifecycle transfer indexes.""" + """Orchestrate one generated call with stable ABI and lifecycle indexes. + + Namespace plans own functions. Arguments/results hold datatype-specific + facts, while this record owns native-call order, callable declarations, + available roles, and function-wide writeback, cleanup, and release order. + """ owner_path: str symbol_name: str @@ -772,15 +1021,41 @@ class FunctionPlan(StageRecord): arguments: tuple[ArgumentTransferPlan, ...] results: tuple[ResultPlan, ...] native_call_slots: tuple[NativeCallSlotPlan, ...] + declaration_callables: tuple[DeclarationCallablePlan, ...] available_roles: tuple[str, ...] writeback_actions: tuple[LifecycleActionPlan, ...] = () cleanup_actions: tuple[LifecycleActionPlan, ...] = () release_actions: tuple[LifecycleActionPlan, ...] = () +@dataclass +class DeclarationCallablePlan(StageRecord): + """Describe one planned module import or standalone native procedure entity. + + Declaration expressions refer to ``expression_token`` and ``symbolic_role``. + The completed action tells the bridge whether to use a visible entity, + imported procedure, or prototype-backed standalone declaration. + """ + + owner_path: str + source_name: str + native_name: str + native_scope: str | None + backend_symbol: str + symbolic_role: str + expression_token: str + action: DeclarationCallableAction + prototype: ProcedurePrototypePlan | None = None + + @dataclass class NamespacePlan(StageRecord): - """One Python namespace containing directly exported wrapper owners.""" + """Represent one Python namespace and its directly exported wrapper owners. + + ``python_path`` identifies the root or child module path; contained tuples + preserve planner order for functions, variables, types, classes, and + overloads. ``ModulePlan`` groups these namespaces into one generation unit. + """ owner_path: str python_path: tuple[str, ...] @@ -794,7 +1069,13 @@ class NamespacePlan(StageRecord): @dataclass class ModulePlan(StageRecord): - """One shared generation-unit plan containing an explicit namespace tree.""" + """Serve as the root editable plan for one generated extension module. + + Constructed by ``WrapperPlanner.build()``, this root joins binding and + bridge module views with an explicit namespace tree and required headers. + Pass it to ``WrapperCodeGenerator.generate()``; generation validates then + freezes the graph before it renders artifacts. + """ owner_path: str binding: BindingModulePlan @@ -805,8 +1086,53 @@ class ModulePlan(StageRecord): @dataclass class WrapperPlanDiagnostic(StageRecord): - """One owner-path diagnostic produced before backend generation.""" + """Store one owner-path diagnostic produced before backend generation begins.""" owner_path: str code: str message: str + + +if __name__ == "__main__": + # ``plan.py`` owns the typed representation rather than semantic-policy + # completion or source generation. Constructing the smallest procedure + # plan is therefore its nearest deterministic stage-local demonstration. + binding_function = BindingFunctionPlan( + python_name="ping", + docstring="Call the native PING subroutine.", + release_gil=False, + status_error=None, + argument_conversion_order=(), + ) + bridge_function = BridgeFunctionPlan( + native_name="PING", + native_invocation=NativeInvocationKind.PROCEDURE, + native_operator=None, + standalone=True, + external_declaration=ExternalDeclarationMode.IMPLICIT_EXTERNAL, + native_module=None, + native_is_subroutine=True, + ) + function = FunctionPlan( + owner_path="demo.ping", + symbol_name="ping", + binding=binding_function, + bridge=bridge_function, + class_call=None, + arguments=(), + results=(), + native_call_slots=(), + declaration_callables=(), + available_roles=(), + ) + plan = ModulePlan( + owner_path="demo", + binding=BindingModulePlan(owner_path="demo"), + bridge=BridgeModulePlan(owner_path="demo"), + namespaces=(NamespacePlan(owner_path="demo", python_path=(), functions=(function,)),), + ) + + print(f"Plan owner: {plan.owner_path}") + print(f"Python export: {plan.namespaces[0].functions[0].binding.python_name}") + print(f"Native procedure: {plan.namespaces[0].functions[0].bridge.native_name}") + print(f"Native slots: {len(plan.namespaces[0].functions[0].native_call_slots)}") diff --git a/prik/wrapper_codegen/planner.py b/prik/codegen/planner.py similarity index 83% rename from prik/wrapper_codegen/planner.py rename to prik/codegen/planner.py index a45566144..ac766ca09 100644 --- a/prik/wrapper_codegen/planner.py +++ b/prik/codegen/planner.py @@ -1,4 +1,12 @@ -"""Hierarchical wrapper-plan construction from completed semantic policy.""" +"""Project completed semantic policy into an editable wrapper plan. + +``WrapperPlanner`` is the boundary between post-IR policy completion and +code-generation lowering. It consumes a fully completed +``SemanticModule`` and produces one ``ModulePlan`` that binding and bridge +generators can consume without making further semantic decisions. The +planner groups public exports into namespaces, shares native-call records +between their consumers, and names the backend roles needed by later stages. +""" from __future__ import annotations @@ -14,6 +22,7 @@ CallbackHandoffPolicy, CallbackResultPolicy, CallbackTransferPolicy, + DeclarationCallablePolicy, ClassSurfacePolicy, DerivedCallPolicy, DerivedFieldPolicy, @@ -38,6 +47,9 @@ NativeDescriptorHandoffPolicy, NativeStatusErrorPolicy, PolymorphicDispatchPolicy, + ProcedurePrototypeArgumentPolicy, + ProcedurePrototypePolicy, + ProcedurePrototypeResultPolicy, ResultPolicy, ScalarDescriptorResultPolicy, TransformationPolicy, @@ -49,8 +61,8 @@ ) from prik.semantics.wrapper_exports import PythonExportPolicy from prik.semantics.ownership import NativeBarrierAction, SetterAction -from prik.wrapper_codegen.docstrings import WrapperDocstringBuilder -from prik.wrapper_codegen.plan import ( +from prik.codegen.docstrings import WrapperDocstringBuilder +from prik.codegen.plan import ( ArrayHandoffPlan, ArgumentTransferPlan, BindingArgumentPlan, @@ -77,6 +89,7 @@ ConstructorFieldPlan, ConstructorPlan, DatatypeFamily, + DeclarationCallablePlan, DerivedFieldPlan, DerivedCallCasePlan, DerivedCallPlan, @@ -96,16 +109,20 @@ NativeDescriptorHandoffPlan, PolymorphicDispatchPlan, PolymorphicVariantPlan, + ProcedurePrototypeArgumentPlan, + ProcedurePrototypePlan, + ProcedurePrototypeResultPlan, ResultPlan, ScalarDescriptorResultPlan, TransformationPlan, ) -from prik.wrapper_codegen.naming import NativeSymbolNames -from prik.wrapper_codegen.visitor import ClassVisitor +from prik.codegen.naming import NativeSymbolNames +from prik.codegen.visitor import ClassVisitor +from prik.types.numpy import BOOLEAN_SEMANTIC_TYPE_NAMES _DATATYPE_FAMILIES = { - "Bool": DatatypeFamily.BOOL, + **dict.fromkeys(BOOLEAN_SEMANTIC_TYPE_NAMES, DatatypeFamily.BOOL), "Int8": DatatypeFamily.INTEGER, "Int16": DatatypeFamily.INTEGER, "Int32": DatatypeFamily.INTEGER, @@ -119,29 +136,65 @@ class WrapperPlanner(ClassVisitor): - """Project completed semantic policies into one editable shared plan.""" + """Project a policy-completed semantic module into an editable ``ModulePlan``. + + Use :meth:`build` after ``complete_semantic_policies`` and before calling + the wrapper code generator. The planner preserves the completed policy + decisions; it only organizes them into namespace, binding, bridge, and + shared native-call records. A returned plan remains editable until the + code generator validates and freezes it. + """ def __init__(self): - """Create a planner for one policy-completed semantic module.""" + """Initialize visitor state and the shared plan-docstring builder. + + Per-module caches are reset at the beginning of :meth:`build`, so a + planner instance can safely project more than one semantic module. + """ super().__init__() self.docstrings = WrapperDocstringBuilder() def build(self, module: models.SemanticModule) -> ModulePlan: - """Mechanically project one editable wrapper plan.""" + """Build an editable wrapper plan from one policy-completed module. + + Call this after post-IR policy completion. ``module`` supplies all + ownership, transfer, ABI, export, and lifecycle decisions; this method + does not infer or replace them. The returned ``ModulePlan`` is the + normal input to ``WrapperCodeGenerator.generate``. + + Raises: + ValueError: If completed policy is missing, inconsistent, or has + no public wrapper exports. + """ return self.visit(module) + # Module-level orchestration: initialize indexes, project members, then link namespaces. def _visit_SemanticModule(self, module: models.SemanticModule) -> ModulePlan: - """Return one module plan, failing at the first inconsistent owner.""" + """Project one module in the established initialization-to-freeze order. + + The method resets per-build caches, projects every completed public + member into namespace groups, exposes generated private callables to + their namespace, and finally materializes the ordered namespace tree. + It raises before plan construction when the module has no public + exports; it does not mutate semantic policy. + """ + # Initialize the per-module indexes used by derived and field projections. self._derived_type_names = {semantic_class.name for semantic_class in module.classes} self._derived_field_plans: dict[str, DerivedFieldPlan] = {} self._complete_derived_backend_symbols(module) + + # Project every public surface before linking private callable entries. functions, variables, derived_types, classes, overloads = self._namespace_member_plans(module) if not any( (*functions.values(), *variables.values(), *derived_types.values(), *classes.values(), *overloads.values()) ): raise ValueError(f"Semantic module {module.name!r} has no public wrapper exports") + + # Link class and overload dispatch targets into the shared function tables. self._attach_class_functions(functions, classes) 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) return ModulePlan( owner_path=module.name, @@ -261,7 +314,7 @@ def _derived_backend_symbol(self, type_identity: tuple[str, str]) -> str: except KeyError as exc: raise ValueError(f"Missing derived backend symbol for {type_identity!r}") from exc - # Derived-type definition and field planning. + # Derived-type definitions, fields, and class surfaces. def _derived_types_by_namespace( self, module: models.SemanticModule, @@ -705,6 +758,7 @@ def _derived_field_plan(self, policy: DerivedFieldPolicy) -> DerivedFieldPlan: self._derived_field_plans[policy.owner_path] = plan return plan + # Module functions, overloads, variables, and namespace assembly. def _functions_by_namespace(self, module: models.SemanticModule) -> dict[tuple[str, ...], list[FunctionPlan]]: """Group exported function plans by completed Python namespace.""" functions = defaultdict(list) @@ -857,6 +911,15 @@ def _module_variable_plan( python_names: tuple[str, ...], module_name: str, ) -> ModuleVariablePlan: + """Project one completed module-variable policy into its shared plan record. + + ``policy`` supplies all accessor, setter, descriptor, and derived + object decisions. ``namespace`` and ``python_names`` select the + exported owner path and binding aliases. The result shares array and + derived-field projections with the rest of the module; no accessor or + ownership policy is selected here. + """ + # Roles are present only where the completed accessor policy requires them. getter_role = self._module_getter_role(policy) setter_role = f"{policy.owner_path}:setter" if policy.setter_action is SetterAction.WRITE_THROUGH else None plan = ModuleVariablePlan( @@ -926,11 +989,25 @@ def _function_plan( *, public: bool = True, ) -> FunctionPlan: - """Return one exported function plan from completed policy.""" + """Project one completed function policy for a particular Python export. + + Native slots are built first so argument and result transfers share + their exact ABI records. The returned function contains binding and + bridge views of already completed policy, ordered lifecycle actions, + and all named roles required by lowering. ``public`` only controls + the generated binding-table visibility for private overload targets. + """ + # Share native-call records before projecting their argument and result consumers. native_call_slots = self._native_slot_plans(policy) arguments = self._argument_plans(policy, native_call_slots) results = self._result_plans(policy, native_call_slots) + declaration_callables = tuple(self._declaration_callable_plan(item) for item in policy.declaration_callables) status_error = self._status_error_plan(policy.status_error, native_call_slots) + + # Retain the completed action order; later stages only dispatch from it. + writeback_actions = tuple(self.visit(action) for action in policy.writeback_actions) + cleanup_actions = tuple(self.visit(action) for action in policy.cleanup_actions) + release_actions = tuple(self.visit(action) for action in policy.release_actions) return FunctionPlan( owner_path=self._export_owner_path(module_name, export.namespace, export.name), symbol_name=export.name.casefold(), @@ -951,7 +1028,7 @@ def _function_plan( policy.native_name, policy.native_invocation, policy.native_operator, - policy.external, + policy.standalone, policy.external_declaration, policy.native_module, policy.native_is_subroutine, @@ -960,10 +1037,16 @@ def _function_plan( arguments=arguments, results=results, native_call_slots=native_call_slots, - available_roles=self._available_roles(arguments, results, native_call_slots), - writeback_actions=tuple(self.visit(action) for action in policy.writeback_actions), - cleanup_actions=tuple(self.visit(action) for action in policy.cleanup_actions), - release_actions=tuple(self.visit(action) for action in policy.release_actions), + declaration_callables=declaration_callables, + available_roles=self._available_roles( + arguments, + results, + native_call_slots, + declaration_callables, + ), + writeback_actions=writeback_actions, + cleanup_actions=cleanup_actions, + release_actions=release_actions, ) @staticmethod @@ -971,7 +1054,7 @@ def _binding_argument_conversion_order( arguments: tuple[ArgumentTransferPlan, ...], ) -> tuple[str, ...]: """Plan a stable conversion order that satisfies array-extent dependencies.""" - role_owners = {argument.binding.handoff_role: argument.owner_path for argument in arguments} + role_owners = WrapperPlanner._argument_role_owners(arguments) dependencies = WrapperPlanner._binding_conversion_dependencies(arguments, role_owners) ordered: list[str] = [] converted: set[str] = set() @@ -986,6 +1069,31 @@ def _binding_argument_conversion_order( converted.add(argument.owner_path) return tuple(ordered) + @staticmethod + def _argument_role_owners( + arguments: tuple[ArgumentTransferPlan, ...], + ) -> dict[str, str]: + """Map every binding-produced value or extent role to its argument owner.""" + owners = {argument.binding.handoff_role: argument.owner_path for argument in arguments} + owners.update( + { + role: argument.owner_path + for argument in arguments + if argument.array is not None + for role in argument.array.extent_roles + } + ) + return owners + + @staticmethod + def _argument_extent_roles(arguments: tuple[ArgumentTransferPlan, ...]) -> tuple[str, ...]: + """Return every binding-produced array extent role in argument order.""" + return tuple( + role + for argument in arguments + for role in (argument.array.extent_roles if argument.array is not None else ()) + ) + @staticmethod def _binding_conversion_dependencies( arguments: tuple[ArgumentTransferPlan, ...], @@ -1095,7 +1203,14 @@ def _visit_ArgumentPolicy( *, native_slot: NativeCallSlotPlan, ) -> ArgumentTransferPlan: - """Return one transfer whose backend views share one handoff role.""" + """Project one completed argument transfer around its shared native slot. + + The supplied ``native_slot`` is the already planned ABI source for the + argument. This method names its value and optional character roles, + then forms binding and bridge views from the completed policy without + re-evaluating ownership, conversion, or descriptor choices. + """ + # Derive only symbolic role names; all transfer behavior is policy-owned. role = self._value_role(policy.owner_path) native_array_handle = native_slot.native_array_handle length_role = self._argument_length_role(policy) @@ -1110,6 +1225,12 @@ def _visit_ArgumentPolicy( callback=policy.callback, ), character_length=policy.character_length, + scalar_logical_abi=policy.scalar_logical_abi, + scalar_native_type=policy.scalar_native_type, + array_logical_abi=policy.array_logical_abi, + array_native_type=policy.array_native_type, + array_copy_in=policy.array_copy_in, + array_copy_out=policy.array_copy_out, array_writeback_abi=policy.array_writeback_abi, object_kind=policy.ownership.kind, ownership_owner=policy.ownership.owner, @@ -1145,9 +1266,7 @@ def _callback_handoff_plan( stem = NativeSymbolNames.compact(policy.owner_path, "callback", limit=24) return CallbackHandoffPlan( owner_path=policy.owner_path, - prototype_name=policy.prototype_name, - prototype_module=policy.prototype_module, - declaration_mode=policy.declaration_mode, + prototype=self._procedure_prototype_plan(policy.prototype), context_type_symbol=f"prik_callback_context_{stem}", context_current_symbol=f"prik_callback_current_{stem}", adapter_symbol=f"prik_callback_adapter_{stem}", @@ -1178,6 +1297,7 @@ def _callback_transfer_plan(self, policy: CallbackTransferPolicy) -> CallbackTra object_kind=policy.object_kind, rank=policy.rank, passed_by_value=policy.passed_by_value, + intent=policy.intent, abi=policy.abi, adapter_action=policy.adapter_action, python_action=policy.python_action, @@ -1194,6 +1314,66 @@ def _callback_transfer_plan(self, policy: CallbackTransferPolicy) -> CallbackTra length_role=(f"{policy.owner_path}:callback-length" if policy.character_length is not None else None), ) + def _procedure_prototype_plan( + self, + policy: ProcedurePrototypePolicy, + ) -> ProcedurePrototypePlan: + """Project one shared exact signature and assign its generated interface name.""" + return ProcedurePrototypePlan( + owner_path=policy.owner_path, + name=policy.name, + identity=policy.identity, + interface_symbol=NativeSymbolNames.compact( + policy.identity, + f"prik_{policy.name}", + limit=48, + ), + pure=policy.pure, + arguments=tuple(self._procedure_prototype_argument_plan(item) for item in policy.arguments), + result=(self._procedure_prototype_result_plan(policy.result) if policy.result is not None else None), + ) + + def _procedure_prototype_argument_plan( + self, + policy: ProcedurePrototypeArgumentPolicy, + ) -> ProcedurePrototypeArgumentPlan: + """Project one exact prototype dummy without adding entity-use policy.""" + return ProcedurePrototypeArgumentPlan( + owner_path=policy.owner_path, + name=policy.name, + semantic_type_name=policy.semantic_type_name, + rank=policy.rank, + passed_by_value=policy.passed_by_value, + intent=policy.intent, + character_length=policy.character_length, + array=self._array_plan(policy.array, policy.owner_path), + derived_type_identity=policy.derived_type_identity, + derived_backend_symbol=( + self._derived_backend_symbol(policy.derived_type_identity) + if policy.derived_type_identity is not None + else None + ), + ) + + def _procedure_prototype_result_plan( + self, + policy: ProcedurePrototypeResultPolicy, + ) -> ProcedurePrototypeResultPlan: + """Project one exact prototype function result.""" + return ProcedurePrototypeResultPlan( + owner_path=policy.owner_path, + semantic_type_name=policy.semantic_type_name, + rank=policy.rank, + character_length=policy.character_length, + array=self._array_plan(policy.array, policy.owner_path), + derived_type_identity=policy.derived_type_identity, + derived_backend_symbol=( + self._derived_backend_symbol(policy.derived_type_identity) + if policy.derived_type_identity is not None + else None + ), + ) + def _polymorphic_dispatch_plan( self, policy: PolymorphicDispatchPolicy | None, @@ -1343,10 +1523,18 @@ def _visit_ResultPolicy( *, native_slot: NativeCallSlotPlan | None, ) -> ResultPlan: - """Return one result with binding consumer and bridge producer views.""" + """Project one completed result with shared binding and bridge views. + + Hidden outputs must reuse their completed native slot; direct results + project their own array, handle, and descriptor records. The returned + plan preserves result ordering and raises when a hidden output has no + slot from which to obtain its ABI details. + """ native_role = f"{policy.owner_path}:native-result" if policy.source_kind == "hidden_output" and native_slot is None: raise ValueError(f"{policy.owner_path!r} hidden result requires its completed native-call slot") + + # Reuse hidden-output records, or project the direct-result facets once. array = self._result_array_plan(policy, native_slot) native_array_handle = self._result_native_array_handle_plan(policy, native_slot, array) return ResultPlan( @@ -1422,7 +1610,14 @@ def _result_scalar_descriptor_plan( return self._scalar_descriptor_result_plan(policy.scalar_descriptor, policy.owner_path) def _native_slot_plan(self, slot: NativeCallSlotPolicy, role: str) -> NativeCallSlotPlan: - """Return one shared ABI slot without selecting backend behavior.""" + """Project one completed ABI slot shared by arguments, results, and calls. + + ``role`` is its externally visible symbolic source. Buffer and dense + array roles are included only when the completed native action requires + them. The method copies completed actions and ABI facts into one plan + record; it never chooses a backend mechanism. + """ + # The completed native action determines only which already-selected roles are needed. include_buffer_roles = slot.native_barrier_action is NativeBarrierAction.PASS_ARRAY_BUFFER array = self._array_plan( slot.array, @@ -1444,6 +1639,12 @@ def _native_slot_plan(self, slot: NativeCallSlotPolicy, role: str) -> NativeCall bridge_data_action=slot.bridge_data_action, bridge_copy_reason=slot.bridge_copy_reason, object_kind=slot.object_kind, + scalar_logical_abi=slot.scalar_logical_abi, + scalar_native_type=slot.scalar_native_type, + array_logical_abi=slot.array_logical_abi, + array_native_type=slot.array_native_type, + array_copy_in=slot.array_copy_in, + array_copy_out=slot.array_copy_out, literal_type=slot.literal_type, literal_value=slot.literal_value, result_position=slot.result_position, @@ -1556,7 +1757,13 @@ def _native_array_handle_plan( *, array: ArrayHandoffPlan | None = None, ) -> NativeArrayHandlePlan | None: - """Project one typed handle policy and its subordinate descriptor roles.""" + """Project one completed typed-handle policy and descriptor-role graph. + + ``array`` reuses a caller's projected array facet when available; + otherwise the method projects the policy's own array facet. A handle + without that facet is inconsistent and raises ``ValueError``. Storage, + ownership, getter, and release behavior remain policy-owned. + """ if policy is None: return None array_plan = array or self._array_plan(policy.array, owner_path, include_buffer_roles=False) @@ -1690,7 +1897,13 @@ def _array_plan( include_buffer_roles: bool = True, include_dense_actual_role: bool = False, ) -> ArrayHandoffPlan | None: - """Mechanically add only the ABI roles selected by completed transport.""" + """Project one completed ordinary-array transport policy. + + The result carries shape references and only the buffer, dense-view, + runtime-rank, and itemsize roles requested by the caller's completed + transport. ``None`` is preserved for non-array transfers; this helper + does not validate or alter shape semantics. + """ if policy is None: return None abi_rank, runtime_rank_role, itemsize_role = self._array_transport_roles( @@ -1711,7 +1924,11 @@ def _array_plan( category=policy.category, data_role=self._value_role(owner_path), extent_roles=tuple(f"{owner_path}:extent:{axis}" for axis in range(abi_rank)), - extent_reference_roles=self._array_extent_reference_roles(owner_path, policy.extent_references), + extent_reference_tokens=policy.extent_references, + extent_reference_roles=policy.extent_reference_roles, + extent_callable_tokens=policy.extent_callable_references, + extent_callable_roles=policy.extent_callable_roles, + extent_evaluation=policy.extent_evaluation, upper_bound_roles=self._array_layout_roles(owner_path, abi_rank, policy.contiguous, "upper-bound"), stride_roles=self._array_layout_roles(owner_path, abi_rank, policy.contiguous, "stride"), dense_actual_role=self._array_dense_actual_role( @@ -1721,6 +1938,27 @@ def _array_plan( ), runtime_rank_role=runtime_rank_role, itemsize_role=itemsize_role, + display_shape=policy.display_shape or policy.shape, + ) + + def _declaration_callable_plan(self, policy: DeclarationCallablePolicy) -> DeclarationCallablePlan: + """Add only a collision-resistant backend spelling to completed callable policy.""" + identity = f"{policy.native_scope or 'standalone'}.{policy.native_name}" + backend_symbol = ( + policy.native_name + if policy.native_scope is None + else NativeSymbolNames.compact(identity, f"prik_decl_{policy.native_name}", limit=48) + ) + return DeclarationCallablePlan( + owner_path=policy.owner_path, + source_name=policy.source_name, + native_name=policy.native_name, + native_scope=policy.native_scope, + backend_symbol=backend_symbol, + symbolic_role=policy.symbolic_role, + expression_token=policy.expression_token, + action=policy.action, + prototype=(self._procedure_prototype_plan(policy.prototype) if policy.prototype is not None else None), ) @staticmethod @@ -1773,15 +2011,6 @@ def _array_layout_roles( return () return tuple(f"{owner_path}:{label}:{axis}" for axis in range(rank)) - def _array_extent_reference_roles( - self, - owner_path: str, - references: tuple[tuple[str, ...], ...], - ) -> tuple[tuple[str, ...], ...]: - """Resolve completed extent names to existing argument handoff roles.""" - function_path = owner_path.rsplit(".", 1)[0] - return tuple(tuple(f"{function_path}.{name}:value" for name in axis) for axis in references) - def _status_error_plan( self, policy: NativeStatusErrorPolicy | None, @@ -1829,10 +2058,28 @@ def _available_roles( arguments: tuple[ArgumentTransferPlan, ...], results: tuple[ResultPlan, ...], native_call_slots: tuple[NativeCallSlotPlan, ...], + declaration_callables: tuple[DeclarationCallablePlan, ...], ) -> tuple[str, ...]: """Return symbolic roles available after the native call.""" - roles = [argument.binding.handoff_role for argument in arguments] - roles.extend( + roles = ( + *self._argument_handoff_roles(arguments), + *self._argument_extent_roles(arguments), + *self._argument_descriptor_output_roles(arguments), + *self._native_result_roles(native_call_slots), + *self._direct_result_roles(results), + *self._declaration_callable_roles(declaration_callables), + ) + return tuple(dict.fromkeys(roles)) + + @staticmethod + def _argument_handoff_roles(arguments: tuple[ArgumentTransferPlan, ...]) -> tuple[str, ...]: + """Return the primary binding-produced role for every argument.""" + return tuple(argument.binding.handoff_role for argument in arguments) + + @staticmethod + def _argument_descriptor_output_roles(arguments: tuple[ArgumentTransferPlan, ...]) -> tuple[str, ...]: + """Return optional descriptor outputs produced by argument conversion.""" + return tuple( role for argument in arguments for role in ( @@ -1841,9 +2088,13 @@ def _available_roles( ) if role is not None ) - roles.extend(self._native_result_roles(native_call_slots)) - roles.extend(self._direct_result_roles(results)) - return tuple(dict.fromkeys(roles)) + + @staticmethod + def _declaration_callable_roles( + declaration_callables: tuple[DeclarationCallablePlan, ...], + ) -> tuple[str, ...]: + """Return bridge-resolved declaration-callable symbol roles.""" + return tuple(item.symbolic_role for item in declaration_callables) def _required_headers(self, namespaces: tuple[NamespacePlan, ...]) -> tuple[str, ...]: """Return the union of headers selected by completed handle plans.""" @@ -1939,6 +2190,12 @@ def _namespace_paths(self, declared_paths: tuple[tuple[str, ...], ...]) -> tuple return tuple(sorted(paths, key=lambda item: (len(item), item))) def _namespace_owner_path(self, module_name: str, namespace: tuple[str, ...]) -> str: + """Return the stable dotted owner path for a module or child namespace. + + The root namespace keeps ``module_name`` unchanged; child components + are appended in their supplied order. The helper has no allocation or + mutation side effects beyond constructing the returned string. + """ return ".".join((module_name, *namespace)) if namespace else module_name def _export_owner_path( @@ -1947,6 +2204,12 @@ def _export_owner_path( namespace: tuple[str, ...], python_name: str, ) -> str: + """Return the stable dotted owner path for one namespace export. + + ``python_name`` is always appended after the module and namespace + components, preserving the owner-path form shared by planning and + validation. Inputs are not normalized or mutated here. + """ return ".".join((module_name, *namespace, python_name)) def _symbol_name(self, namespace: tuple[str, ...], local_name: str) -> str: @@ -1988,3 +2251,28 @@ def _public_result_for_slot( ), None, ) + + +if __name__ == "__main__": + from prik.semantics.models import SemanticArgument, SemanticFunction, SemanticModule, SemanticType + from prik.semantics.policy_completion import complete_semantic_policies + + module = SemanticModule( + name="planner_demo", + functions=[ + SemanticFunction( + name="double_value", + native_name="DOUBLE_VALUE", + arguments=[SemanticArgument("value", SemanticType("Float64"))], + return_type=SemanticType("Float64"), + ) + ], + ) + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + function = plan.namespaces[0].functions[0] + + print(f"Plan owner: {plan.owner_path}") + print(f"Python export: {function.binding.python_name}") + print(f"Native target: {function.bridge.native_name}") + print(f"Conversion order: {function.binding.argument_conversion_order}") diff --git a/prik/wrapper_codegen/primitive_scalar_types.py b/prik/codegen/primitive_scalar_types.py similarity index 88% rename from prik/wrapper_codegen/primitive_scalar_types.py rename to prik/codegen/primitive_scalar_types.py index a650c7f28..b6919d3fb 100644 --- a/prik/wrapper_codegen/primitive_scalar_types.py +++ b/prik/codegen/primitive_scalar_types.py @@ -5,24 +5,28 @@ from dataclasses import replace from typing import ClassVar -from prik.wrapper_codegen.nodes import BackendScalarType +from prik.types.numpy import BOOLEAN_SEMANTIC_TYPE_NAMES +from prik.codegen.nodes import BackendScalarType + + +_BOOL_BACKEND_TYPE = BackendScalarType( + "Bool", + "bool", + "logical(c_bool)", + "O", + "NPY_BOOL", + "python", + "bool", + "python", + "CFI_type_Bool", +) class PrimitiveScalarTypeRegistry: """Return first-lane scalar facts without coupling binding and bridge emitters.""" TYPES: ClassVar[dict[str, BackendScalarType]] = { - "Bool": BackendScalarType( - "Bool", - "bool", - "logical(c_bool)", - "O", - "NPY_BOOL", - "python", - "bool", - "python", - "CFI_type_Bool", - ), + **{name: replace(_BOOL_BACKEND_TYPE, semantic_name=name) for name in BOOLEAN_SEMANTIC_TYPE_NAMES}, "Int8": BackendScalarType( "Int8", "int8_t", diff --git a/prik/wrapper_codegen/printers/__init__.py b/prik/codegen/printers/__init__.py similarity index 100% rename from prik/wrapper_codegen/printers/__init__.py rename to prik/codegen/printers/__init__.py diff --git a/prik/wrapper_codegen/printers/pyi_printer.py b/prik/codegen/printers/pyi_printer.py similarity index 92% rename from prik/wrapper_codegen/printers/pyi_printer.py rename to prik/codegen/printers/pyi_printer.py index 2545555cf..4352272ab 100644 --- a/prik/wrapper_codegen/printers/pyi_printer.py +++ b/prik/codegen/printers/pyi_printer.py @@ -1,3 +1,11 @@ +"""Render semantic IR as compact, editable semantic .pyi contracts. + +Use PyiPrinter or emit_module after semantic conversion to inspect, persist, or +hand an editable contract to the .pyi loader. This module renders semantic +facts and projection metadata already present in the IR; it does not complete +ownership policy or select wrapper behavior. +""" + from __future__ import annotations import ast @@ -33,6 +41,7 @@ PYTHON_STATIC_METADATA, PYTHON_VALUE_IMMUTABLE, PYTHON_VALUE_MUTABILITY_METADATA, + PROTOTYPE_INTENT_METADATA, PROTOTYPE_REF_METADATA, RUNTIME_RELEASE_GIL_METADATA, RUNTIME_STATUS_ERROR_METADATA, @@ -58,16 +67,20 @@ _WRAPPED_CALLABLE_TYPE_METADATA = "pyi_wrapped_callable_type" _CONTRACT_MODULE = "prik.contracts" -_CONTRACT_ALIAS_PREFIX = "_prik_" +_CONTRACT_ALIAS_PREFIX = "prik_" _FLAT_DIMENSION_PRINT_SENTINEL = "@prik.Flat" class PyiPrinter(ClassVisitor): - """Emit Python stub text from semantic IR models. + """Emit editable Python stub text from semantic IR models. The class follows the same reading order as ``FortranParser``: its public entrypoint comes first, semantic model visitors follow in model-flow order, and formatting helpers remain next to the visitor group that owns them. + Use emit for a module or individual semantic model. Module emission + temporarily tracks imports, aliases, and namespace names, then restores + previous state so one printer instance can safely emit more than one + independent module. """ # ------------------------------------------------------------------ @@ -75,7 +88,13 @@ class PyiPrinter(ClassVisitor): # ------------------------------------------------------------------ def __init__(self, *, normalize_fortran_public_names: bool = False): - """Configure whether source-generated Fortran contracts use Python public names.""" + """Configure a printer and initialize its per-emission state. + + Set normalize_fortran_public_names when emitting source-derived Fortran + contracts whose public names need Python normalization. Import, alias, + and namespace state is reset or restored around module emission; normal + individual-node emission does not mutate the input model. + """ self._normalize_fortran_public_names = normalize_fortran_public_names self._naming_policy = NamingPolicy() self._public_namespace: tuple[str, ...] = () @@ -86,14 +105,23 @@ def __init__(self, *, normalize_fortran_public_names: bool = False): self._default_array_order: str | None = None def emit(self, node) -> str: - """Emit the supported semantic model passed by the caller.""" + """Render one supported semantic model to semantic .pyi text. + + Pass a SemanticModule for a complete contract or another supported + semantic record for an isolated representation. Module emission + installs source-language array defaults and, when requested, isolated + public-name normalization state; both are restored before this method + returns or raises. + """ if not isinstance(node, SemanticModule): return self._visit(node) + # Stage 1: install source-specific defaults for this module emission. previous_default_order = self._default_array_order self._default_array_order = self._native_default_array_order(node.origin.source_language) try: if not self._normalize_fortran_public_names: return self._visit(node) + # Stage 2: normalize public names in isolated naming state. previous_policy = self._naming_policy previous_namespace = self._public_namespace previous_reserved = self._reserved_public_names @@ -175,7 +203,7 @@ def _visit_SemanticFunction(self, func: SemanticFunction) -> str: return self._emit_function(func) def _visit_SemanticPrototype(self, prototype: SemanticPrototype) -> str: - """Emit one semantic-only named callback prototype.""" + """Emit one reusable exact native procedure signature.""" return_type = prototype.return_type or SemanticType("None", dtype="None") arguments = [] for argument in prototype.arguments: @@ -183,11 +211,15 @@ def _visit_SemanticPrototype(self, prototype: SemanticPrototype) -> str: if argument.optional: text += " = ..." arguments.append(text) + decorators = [] + if prototype.pure: + decorators.append(f"@{self._contract('pure')}") + decorators.append(f"@{self._contract('prototype')}") return self._emit_callable( name=prototype.name, arguments=arguments, return_type=self._visit(return_type), - decorator=f"@{self._contract('prototype')}\n", + decorator="\n".join(decorators) + "\n", def_indent="", parameter_indent=" ", ) @@ -294,7 +326,13 @@ def _native_type_decorator(self, cls: SemanticClass) -> str: return f"@{self._contract('native_type')}({', '.join(parts)})" if parts else "" def _visit_SemanticModule(self, module: SemanticModule) -> str: - """Emit module syntax.""" + """Render one module through alias setup, ordered bodies, and imports. + + Class names and contract imports are temporary state because nested + visitors resolve references through them. The previous state is restored + even when a body visitor rejects invalid semantic input. + """ + # Stage 1: establish names and aliases visible to nested visitors. previous_class_names = self._semantic_class_names previous_contract_imports = self._contract_imports previous_contract_aliases = self._contract_aliases @@ -307,6 +345,7 @@ def _visit_SemanticModule(self, module: SemanticModule) -> str: self._contract_aliases = self._contract_aliases_for_module(module) body_sections: list[str] = [] try: + # Stage 2: render public bodies in stable contract order. self._append_items(body_sections, self._contract_items(module.classes), self.emit) self._append_items(body_sections, module.prototypes, self._visit) self._append_items(body_sections, self._contract_items(module.variables), self._emit_module_variable) @@ -317,6 +356,7 @@ def _visit_SemanticModule(self, module: SemanticModule) -> str: self._visit, ) self._append_items(body_sections, module.overload_sets, self._visit) + # Stage 3: synthesize imports after visitors have recorded requirements. sections: list[str] = [] self._append_imports(sections, module) sections.extend(body_sections) @@ -590,9 +630,21 @@ def _visible_scalar_descriptor_type(semantic_type: SemanticType) -> SemanticType return visible def _emit_prototype_argument(self, argument: SemanticArgument) -> str: - """Emit one prototype dummy using the public callback transport rules.""" + """Emit one exact prototype dummy with direction around transport.""" if self._is_prototype_descriptor_type(argument.semantic_type): - return self._prototype_descriptor_type_text(argument.semantic_type) + transport = self._prototype_descriptor_type_text(argument.semantic_type) + else: + transport = self._prototype_argument_transport(argument) + intent = getattr(argument.origin, "metadata", {}).get(PROTOTYPE_INTENT_METADATA) + if intent is None: + return transport + wrapper = {"in": "In", "out": "Out", "inout": "InOut"}.get(str(intent).casefold()) + if wrapper is None: + raise ValueError(f"Unsupported prototype intent {intent!r} on {argument.name!r}") + return f"{self._contract(wrapper)}({transport})" + + def _prototype_argument_transport(self, argument: SemanticArgument) -> str: + """Emit one prototype dummy's exact value or reference transport.""" inner = self._prototype_argument_inner_type(argument.semantic_type) if bool(getattr(argument.origin, "metadata", {}).get("value")): if self._is_prototype_primitive_value(argument.semantic_type): @@ -627,6 +679,11 @@ def _prototype_descriptor_type_text(self, semantic_type: SemanticType) -> str: @staticmethod def _is_prototype_primitive_value(semantic_type: SemanticType) -> bool: + """Return whether a prototype dummy is an unwrapped primitive value. + + Descriptor metadata remains an exclusion even when rank and base type + otherwise look scalar, so exact native signature rendering wins. + """ storage = semantic_type.storage return bool( semantic_type.rank == 0 @@ -638,6 +695,11 @@ def _is_prototype_primitive_value(semantic_type: SemanticType) -> bool: @staticmethod def _is_prototype_primitive_reference(semantic_type: SemanticType) -> bool: + """Return whether a prototype dummy is a single-level primitive reference. + + Arrays, descriptors, strings, and multi-level pointers take dedicated + native-signature paths instead of this compact predicate. + """ storage = semantic_type.storage return bool( semantic_type.rank == 0 @@ -651,6 +713,7 @@ def _is_prototype_primitive_reference(semantic_type: SemanticType) -> bool: @staticmethod def _is_prototype_descriptor_type(semantic_type: SemanticType) -> bool: + """Return whether metadata requires descriptor-style prototype spelling.""" return any( semantic_type.metadata.get(name) for name in ( @@ -1149,9 +1212,58 @@ def _effective_imports(cls, module: SemanticModule) -> list[str | SemanticImport cls._validate_procedure_namespace_imports(module, procedure_namespaces, imports) 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_procedure_namespace_imports(procedure_namespaces, satisfied_namespaces)) return imports + @classmethod + def _missing_expression_callable_imports( + cls, + module: SemanticModule, + imports: list[str | SemanticImport], + ) -> list[SemanticImport]: + """Return explicit imports needed to preserve declaration-call origins. + + The semantic array provenance is consumed without changing its call + expression. Existing explicit imports win; wildcard-like native module + imports gain only the specific callable names needed by the generated + contract, which makes a later `.pyi` load unambiguous. + """ + existing = { + (item.target or item.source).casefold(): (imported.module, item.source) + for imported in imports + if isinstance(imported, SemanticImport) + for item in imported.items + } + local_names = {function.name.casefold() for function in module.functions} + required: dict[str, list[SemanticImportItem]] = {} + for semantic_type in _iter_module_semantic_types(module): + storage = semantic_type.storage + array = storage.array if storage is not None else None + if array is None: + continue + for axis_references in array.expression_callables: + for reference in axis_references: + if reference.native_scope is None or reference.name.casefold() in local_names: + continue + local_name = reference.name.rsplit(".", 1)[-1] + native_name = reference.native_name or local_name + previous = existing.get(local_name.casefold()) + if previous is not None: + if previous != (reference.native_scope, native_name): + raise ValueError( + f"Declaration-expression callable import collides with existing name: {local_name!r}" + ) + continue + required.setdefault(reference.native_scope, []).append( + SemanticImportItem( + source=native_name, + target=local_name if local_name != native_name else None, + ) + ) + existing[local_name.casefold()] = (reference.native_scope, native_name) + return [SemanticImport(module=module_name, items=items) for module_name, items in required.items()] + @classmethod def _synthetic_flat_external_type_imports( cls, @@ -1646,7 +1758,7 @@ def _decorators(self, func: SemanticFunction, *, indent: str = "", emitted_name: and not isinstance(func, SemanticMethod) and not func.metadata.get(OVERLOAD_TARGET_METADATA) ): - decorators.append(f"{indent}@{self._contract('external')}") + decorators.append(f"{indent}@{self._contract('standalone')}") if not func.metadata.get(OVERLOAD_TARGET_METADATA) and self._requires_native_call(func): decorators.append( f"{indent}{self._native_call(self._pyi_projection(func), self._native_result_projection(func))}" @@ -2032,7 +2144,12 @@ def _module_list(modules: SemanticModule | Iterable[SemanticModule] | None) -> l def emit_module(module: SemanticModule, *, normalize_fortran_public_names: bool = False) -> str: - """Emit one semantic module through the default stateless printer.""" + """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 an isolated normalized printer; + otherwise the reusable default printer restores its module-local state. + """ if normalize_fortran_public_names: return PyiPrinter(normalize_fortran_public_names=True).emit(module) return _DEFAULT_PRINTER.emit(module) @@ -2043,7 +2160,12 @@ def opaque_dependency_modules( *, available_modules: Iterable[SemanticModule] | None = None, ) -> list[SemanticModule]: - """Build missing opaque dependency modules required by emitted stubs.""" + """Build semantic modules for opaque types referenced but not supplied. + + Use this before package emission when a contract refers to C opaque types + from absent modules. The input modules are inspected but not mutated; the + returned list is ordered deterministically by module and type name. + """ source_modules = PyiPrinter._module_list(modules) known_modules = PyiPrinter._module_list(available_modules) if available_modules is not None else source_modules known_classes = { @@ -2084,7 +2206,13 @@ def emit_module_stubs( available_modules: Iterable[SemanticModule] | None = None, normalize_fortran_public_names: bool = False, ) -> dict[str, str]: - """Emit a mapping of module names to complete stub texts.""" + """Render complete stub text for semantic modules and opaque dependencies. + + Inputs are deep-copied before dependency insertion and policy completion, + so callers retain their original semantic modules. The returned mapping is + keyed by module name and is normally written into a generated contract + package by a pipeline stage. + """ from prik.semantics.policy_completion import complete_semantic_policies source_modules = PyiPrinter._module_list(modules) @@ -2110,3 +2238,19 @@ def emit_module_stubs( ).strip() for module_name, module in emitted_modules.items() } + + +if __name__ == "__main__": + module = SemanticModule( + name="printer_demo", + functions=[ + SemanticFunction( + name="double_value", + native_name="DOUBLE_VALUE", + arguments=[SemanticArgument("value", SemanticType("Float64"))], + return_type=SemanticType("Float64"), + ) + ], + ) + print("Semantic module: printer_demo") + print(PyiPrinter().emit(module).strip()) diff --git a/prik/wrapper_codegen/printers/source_printers.py b/prik/codegen/printers/source_printers.py similarity index 68% rename from prik/wrapper_codegen/printers/source_printers.py rename to prik/codegen/printers/source_printers.py index 23143f6fe..ecb53f099 100644 --- a/prik/wrapper_codegen/printers/source_printers.py +++ b/prik/codegen/printers/source_printers.py @@ -1,10 +1,15 @@ -"""Source printers for direct wrapper-plan backend nodes.""" +"""Render lowered C and Fortran backend nodes into compilable source text. + +This module is the final text-rendering boundary for generated wrapper source. +It consumes only backend syntax nodes; semantic policy and wrapper planning are +completed by earlier stages. +""" from __future__ import annotations import re -from prik.wrapper_codegen.nodes import ( +from prik.codegen.nodes import ( CAllowThreadsBegin, CAllowThreadsEnd, CComment, @@ -46,20 +51,35 @@ FortranUse, ) from prik.stage_values import StageRecord -from prik.wrapper_codegen.visitor import ClassVisitor +from prik.codegen.visitor import ClassVisitor + + +# C source rendering class CSourcePrinter(ClassVisitor): - """Print isolated C source and header nodes.""" + """Render lowered C backend nodes into source or header text. + + Use this printer after the binding generator has produced C syntax nodes. + It accepts individual nodes, C translation units, and headers, then returns + their source representation. Rendering a stage record freezes that record, + matching the immutable handoff used by the rest of code generation. + """ def doprint(self, node: object) -> str: - """Render one isolated C backend node.""" + """Render one C backend node and return its source text. + + Use this public entrypoint for C headers, translation units, and their + constituent nodes. A StageRecord is frozen before visitor dispatch; + unsupported node types retain the visitor's existing exception. + """ if isinstance(node, StageRecord): node.freeze() return self.visit(node) def _visit_CModule(self, node: CModule) -> str: - """Render a complete C source module.""" + """Render one C translation unit in compiler-required source order.""" + # Definitions must precede includes, declarations, and function bodies. parts = [self.visit(define) for define in node.defines] parts.extend(self.visit(include) for include in node.includes) parts.extend(self.visit(declaration) for declaration in node.declarations) @@ -67,7 +87,7 @@ def _visit_CModule(self, node: CModule) -> str: return "\n\n".join(part for part in parts if part) def _visit_CHeader(self, node: CHeader) -> str: - """Render a complete C header module.""" + """Render one guarded C header from its includes and prototypes.""" lines = [f"#ifndef {node.guard}", f"#define {node.guard}"] lines.extend(self.visit(include) for include in node.includes) lines.extend(self.visit(prototype) for prototype in node.prototypes) @@ -75,60 +95,60 @@ def _visit_CHeader(self, node: CHeader) -> str: return "\n".join(lines) def _visit_CInclude(self, node: CInclude) -> str: - """Render one C include directive.""" + """Render one C include directive, preserving the system-header mode.""" if node.system: return f"#include <{node.header}>" return f'#include "{node.header}"' def _visit_CMacroDefinition(self, node: CMacroDefinition) -> str: - """Render one C preprocessor macro definition.""" + """Render one C macro, omitting its value when the node has none.""" if node.value is None: return f"#define {node.name}" return f"#define {node.name} {node.value}" def _visit_CComment(self, node: CComment) -> str: - """Render one generated C line comment.""" + """Render one generated C line comment from the node text.""" return f"// {node.text}" def _visit_CFunction(self, node: CFunction) -> str: - """Render one C function definition.""" + """Render one C function definition with each body statement indented.""" prefix = f"{node.storage} " if node.storage else "" body = "\n".join(self._indented(self.visit(statement)) for statement in node.body) return f"{prefix}{self._signature(node.return_type, node.name, node.parameters)} {{\n{body}\n}}" def _visit_CFunctionPrototype(self, node: CFunctionPrototype) -> str: - """Render one C function prototype.""" + """Render one C prototype using the shared signature renderer.""" prefix = f"{node.storage} " if node.storage else "" return f"{prefix}{self._signature(node.return_type, node.name, node.parameters)};" def _visit_CFunctionPointerType(self, node: CFunctionPointerType) -> str: - """Render one named typed function pointer without an object-pointer cast.""" + """Render one typed function-pointer alias with explicit void parameters.""" parameters = ", ".join(node.parameter_types) or "void" return f"typedef {node.return_type} (*{node.name})({parameters});" def _visit_CStructDefinition(self, node: CStructDefinition) -> str: - """Render one typed runtime operation table definition.""" + """Render one C struct definition and preserve field declaration order.""" lines = [f"typedef struct {node.name} {{"] lines.extend(f" {self.visit(field)};" for field in node.fields) lines.append(f"}} {node.name};") return "\n".join(lines) def _visit_CMethodDefTable(self, node: CMethodDefTable) -> str: - """Render one CPython method table.""" + """Render one CPython method table and append its required sentinel.""" lines = [f"static PyMethodDef {node.name}[] = {{"] lines.extend(f" {self.visit(entry)}," for entry in node.entries) lines.extend((" {NULL, NULL, 0, NULL}", "};")) return "\n".join(lines) def _visit_CMethodDefEntry(self, node: CMethodDefEntry) -> str: - """Render one CPython method table entry.""" + """Render one CPython method-table entry with safely quoted strings.""" return ( f"{{{self._c_string_literal(node.python_name)}, " f"(PyCFunction){node.wrapper_name}, {node.flags}, {self._c_string_literal(node.docstring)}}}" ) def _visit_CModuleDef(self, node: CModuleDef) -> str: - """Render one CPython module definition.""" + """Render one CPython module-definition initializer from its node fields.""" return "\n".join( ( f"static struct PyModuleDef {node.name} = {{", @@ -142,7 +162,12 @@ def _visit_CModuleDef(self, node: CModuleDef) -> str: ) def _visit_CModulePropertySupport(self, node: CModulePropertySupport) -> str: - """Render module get/set routing through a generated heap subtype.""" + """Render all generated module-property routing support in stable order. + + The node supplies getter and setter entries plus the heap subtype name. + This method returns the three dependent C definitions: attribute getter, + attribute setter, and module-type installer. + """ return "\n\n".join( ( self._module_getattro_source(node), @@ -152,6 +177,12 @@ def _visit_CModulePropertySupport(self, node: CModulePropertySupport) -> str: ) def _module_getattro_source(self, node: CModulePropertySupport) -> str: + """Build the module attribute getter for every declared property entry. + + The returned function compares only Unicode attribute names, delegates + matching names to generated getters, and preserves the base module + fallback for all other attributes. + """ lines = [f"static PyObject *{node.name}_getattro(PyObject *self, PyObject *name)", "{"] lines.append(" if (PyUnicode_Check(name)) {") for entry in node.entries: @@ -160,6 +191,12 @@ def _module_getattro_source(self, node: CModulePropertySupport) -> str: return "\n".join(lines) def _module_getter_entry_source(self, node: CModulePropertyEntry) -> tuple[str, ...]: + """Build one getter dispatch branch from a property entry. + + The tuple is inserted into the enclosing Unicode-name guard. It returns + NULL on comparison failure and calls exactly the getter named by the + supplied entry when its Python name matches. + """ name = self._c_string_literal(node.python_name) return ( " {", @@ -170,6 +207,11 @@ def _module_getter_entry_source(self, node: CModulePropertyEntry) -> tuple[str, ) def _module_setattro_source(self, node: CModulePropertySupport) -> str: + """Build the module attribute setter for every declared property entry. + + The returned function dispatches writable properties to their generated + setters and keeps the base module setter as the nonmatching fallback. + """ lines = [f"static int {node.name}_setattro(PyObject *self, PyObject *name, PyObject *value)", "{"] lines.append(" if (PyUnicode_Check(name)) {") for entry in node.entries: @@ -178,6 +220,12 @@ def _module_setattro_source(self, node: CModulePropertySupport) -> str: return "\n".join(lines) def _module_setter_entry_source(self, node: CModulePropertyEntry) -> tuple[str, ...]: + """Build one setter dispatch branch and its node-selected error path. + + The tuple rejects replacement for read-only entries. Writable entries + reject deletion before calling their generated setter with the supplied + value; those rules are already encoded by the backend node. + """ name = self._c_string_literal(node.python_name) lines = [ " {", @@ -206,6 +254,12 @@ def _module_setter_entry_source(self, node: CModulePropertyEntry) -> tuple[str, return tuple(lines) def _module_property_type_source(self, node: CModulePropertySupport) -> str: + """Build C slots, type spec, and installer for module property support. + + The returned definitions are ordered so the installer can reference the + generated slots and type spec without forward declarations. The node's + name is reused consistently for all emitted symbols. + """ return "\n".join( ( f"static PyType_Slot {node.name}_slots[] = {{", @@ -235,20 +289,20 @@ def _module_property_type_source(self, node: CModulePropertySupport) -> str: ) def _visit_CParameter(self, node: CParameter) -> str: - """Render one C parameter.""" + """Render one C parameter, including typed callback parameters.""" if node.function_parameters is not None: parameters = ", ".join(node.function_parameters) or "void" return f"{node.type_name} (*{node.name})({parameters})" return f"{node.type_name} {node.name}" def _visit_CDeclaration(self, node: CDeclaration) -> str: - """Render one C declaration.""" + """Render one C declaration and optional initializer expression.""" if node.initializer is None: return f"{node.type_name} {node.name};" return f"{node.type_name} {node.name} = {node.initializer.text};" def _visit_CExpressionStatement(self, node: CExpressionStatement) -> str: - """Render one C expression statement.""" + """Render one C expression statement and add its terminating semicolon.""" return f"{node.expression.text};" def _visit_CAllowThreadsBegin(self, _node: CAllowThreadsBegin) -> str: @@ -260,7 +314,7 @@ def _visit_CAllowThreadsEnd(self, _node: CAllowThreadsEnd) -> str: return "Py_END_ALLOW_THREADS" def _visit_CIf(self, node: CIf) -> str: - """Render one C conditional statement.""" + """Render one C conditional and preserve optional else-body ordering.""" lines = [f"if ({node.condition.text}) {{"] lines.extend(self._indented(self.visit(statement)) for statement in node.body) if node.else_body: @@ -270,59 +324,84 @@ def _visit_CIf(self, node: CIf) -> str: return "\n".join(lines) def _visit_CFor(self, node: CFor) -> str: - """Render one compact table-dispatch loop.""" + """Render one C for-loop with each generated statement indented.""" lines = [f"for ({node.initializer}; {node.condition.text}; {node.increment.text}) {{"] lines.extend(self._indented(self.visit(statement)) for statement in node.body) lines.append("}") return "\n".join(lines) def _visit_CBreak(self, _node: CBreak) -> str: - """Render one loop break.""" + """Render one C loop-break statement.""" return "break;" def _visit_CReturn(self, node: CReturn) -> str: - """Render one C return statement.""" + """Render one C return with or without the node expression.""" if node.expression is None: return "return;" return f"return {node.expression.text};" def _signature(self, return_type: str, name: str, parameters: tuple[CParameter, ...]) -> str: - """Render a C function signature.""" + """Render a C signature from its return type, name, and parameters. + + Empty parameter tuples become void so both function declarations and + definitions retain C's explicit no-argument form. + """ rendered = ", ".join(self.visit(parameter) for parameter in parameters) or "void" return f"{return_type} {name}({rendered})" def _c_string_literal(self, value: str) -> str: - """Render a minimal escaped C string literal.""" + """Escape one Python string into the C literal used by generated tables.""" escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") return f'"{escaped}"' def _indented(self, text: str) -> str: + """Indent every line of rendered C text for a containing block.""" return "\n".join(f" {line}" for line in text.splitlines()) +# Fortran source rendering + + class FortranSourcePrinter(ClassVisitor): - """Print isolated Fortran source nodes.""" + """Render lowered Fortran backend nodes into free-form source text. + + Use this printer after bridge lowering has produced Fortran syntax nodes. + It freezes stage records before rendering, wraps generated free-form lines + at safe boundaries, and rejects source that exceeds the compiler-safe line + limit after wrapping. + """ _LINE_LIMIT = 112 _MAX_LINE_LENGTH = 132 def doprint(self, node: object) -> str: - """Render one isolated Fortran backend node.""" + """Render one Fortran backend node into validated free-form source. + + This is the public entrypoint for modules, procedures, and individual + statements. StageRecord inputs are frozen before dispatch. A ValueError + is raised only when an overlong generated line has no safe continuation. + """ if isinstance(node, StageRecord): node.freeze() - source = self._format_line_lengths(self.visit(node)) - self._validate_line_lengths(source) - return source + rendered = self.visit(node) + formatted = self._format_line_lengths(rendered) + self._validate_line_lengths(formatted) + return formatted def _format_line_lengths(self, source: str) -> str: - """Wrap overlong free-form lines at syntax-safe token boundaries.""" + """Wrap every overlong rendered line and return the recombined source.""" lines = [] for line in source.splitlines(): lines.extend(self._wrap_rendered_line(line)) return "\n".join(lines) def _wrap_rendered_line(self, line: str) -> tuple[str, ...]: - """Add free-form continuations without splitting tokens or literals.""" + """Split one overlong line at safe syntax or literal boundaries. + + The input is one already-rendered Fortran line. The returned tuple + preserves indentation and literal value; an unsplittable token remains + intact so final validation can report the original compiler limit. + """ if len(line) <= self._MAX_LINE_LENGTH: return (line,) indentation = line[: len(line) - len(line.lstrip())] @@ -331,6 +410,7 @@ def _wrap_rendered_line(self, line: str) -> tuple[str, ...]: remaining = line[len(indentation) :] continued_quote = None wrapped = [] + # Each continuation consumes the remaining source from left to right. while len(prefix) + len(remaining) > self._MAX_LINE_LENGTH: budget = self._MAX_LINE_LENGTH - len(prefix) - len(" &") split = self._safe_fortran_break(remaining, budget, initial_quote=continued_quote) @@ -358,7 +438,12 @@ def _safe_fortran_break( *, initial_quote: str | None = None, ) -> tuple[int, str | None] | None: - """Find the preferred safe code or character-literal continuation.""" + """Choose the rightmost safe break at or below the available width. + + The helper consumes text after existing indentation and returns a source + offset plus an active quote when a literal continuation is required. + It never splits comments or doubled quote escapes. + """ literal_quotes = FortranSourcePrinter._fortran_literal_quotes(text, initial_quote=initial_quote) literal_positions = set(literal_quotes) window = text[: budget + 1] @@ -372,7 +457,11 @@ def _safe_fortran_break( @staticmethod def _fortran_literal_quotes(text: str, *, initial_quote: str | None = None) -> dict[int, str]: - """Map character offsets protected by Fortran literals to their quote.""" + """Map literal-character offsets in text to their active quote marker. + + An optional initial quote continues a literal begun on a prior line. + Doubled quotes remain protected so callers cannot split an escape pair. + """ positions = {} quote = initial_quote index = 0 @@ -400,7 +489,11 @@ def _fortran_literal_break( budget: int, literal_quotes: dict[int, str], ) -> tuple[int, str] | None: - """Find a character-literal continuation that preserves its exact value.""" + """Choose a literal-internal break that preserves the character value. + + The literal map comes from _fortran_literal_quotes. The returned offset + and quote identify a valid continuation, or None when no split fits. + """ candidates = [] for position in range(1, min(len(text), budget + 1)): quote = literal_quotes.get(position) @@ -416,12 +509,12 @@ def _fortran_literal_break( @staticmethod def _has_fortran_comment(text: str, literal_positions: set[int]) -> bool: - """Identify a comment marker that is not protected by a literal.""" + """Report whether text has a comment marker outside protected literals.""" return any(character == "!" and index not in literal_positions for index, character in enumerate(text)) @staticmethod def _fortran_break_candidates(text: str, literal_positions: set[int]) -> tuple[int, ...]: - """Collect token boundaries that preserve free-form statement syntax.""" + """Collect comma and whitespace boundaries outside Fortran literals.""" candidates = [] for index, character in enumerate(text): if index in literal_positions: @@ -433,7 +526,11 @@ def _fortran_break_candidates(text: str, literal_positions: set[int]) -> tuple[i return tuple(candidates) def _validate_line_lengths(self, source: str) -> None: - """Reject generated free-form source that a standard compiler truncates.""" + """Reject source whose rendered free-form lines still exceed the limit. + + Source has already passed through the wrapper. The raised ValueError + includes the original line number and text for an actionable diagnostic. + """ for line_number, line in enumerate(source.splitlines(), start=1): if len(line) > self._MAX_LINE_LENGTH: raise ValueError( @@ -441,21 +538,30 @@ def _validate_line_lengths(self, source: str) -> None: f"the free-form limit is {self._MAX_LINE_LENGTH}: {line}" ) + # Fortran backend-node visitors + def _visit_FortranModule(self, node: FortranModule) -> str: - """Render a complete Fortran module.""" + """Render one Fortran module in specification-part and body order. + + The node supplies uses, types, interfaces, declarations, and procedures. + Abstract interfaces are emitted before declarations, concrete interfaces + after them, and standalone procedures after the enclosing module. + """ lines = [f"module {node.name}"] lines.extend(self._indented(self.visit(use)) for use in node.uses) lines.append(" implicit none") lines.extend(self._indented(self.visit(definition)) for definition in node.type_definitions) - lines.extend(self._indented(self.visit(interface)) for interface in node.interfaces) + lines.extend(self._indented(self.visit(interface)) for interface in node.interfaces if interface.abstract) + lines.extend(self._indented(self.visit(declaration)) for declaration in node.declarations) + lines.extend(self._indented(self.visit(interface)) for interface in node.interfaces if not interface.abstract) lines.append("contains") lines.extend(self._indented(self.visit(procedure)) for procedure in node.procedures) lines.append(f"end module {node.name}") - lines.extend(self.visit(procedure) for procedure in node.external_procedures) + lines.extend(self.visit(procedure) for procedure in node.standalone_procedures) return "\n".join(lines) def _visit_FortranUse(self, node: FortranUse) -> str: - """Render one Fortran use statement.""" + """Render one Fortran use statement and wrap a long ONLY list.""" if node.only: rendered = f"use {node.module}, only: {', '.join(node.only)}" if len(rendered) <= 100: @@ -468,7 +574,11 @@ def _visit_FortranUse(self, node: FortranUse) -> str: return f"use {node.module}" def _visit_FortranFunction(self, node: FortranFunction) -> str: - """Render one Fortran function.""" + """Render one Fortran function or subroutine from its backend node. + + The returned text contains its signature, specification part, body, and + optional internal procedures in Fortran's required source order. + """ signature = self._function_signature(node) lines = [signature, *self._fortran_function_specification(node)] lines.extend(self._indented(self.visit(statement)) for statement in node.body) @@ -479,7 +589,11 @@ def _visit_FortranFunction(self, node: FortranFunction) -> str: return "\n".join(lines) def _fortran_function_specification(self, node: FortranFunction) -> list[str]: - """Render use, declaration, and local-interface specification lines.""" + """Render the ordered specification part for one procedure node. + + Uses, implicit-none, parameter/result declarations, local declarations, + and interfaces are returned as complete lines before the executable body. + """ lines = [] lines.extend(self._indented(self.visit(use)) for use in node.uses) if node.implicit_none: @@ -492,7 +606,11 @@ def _fortran_function_specification(self, node: FortranFunction) -> list[str]: return lines def _visit_FortranParameter(self, node: FortranParameter) -> str: - """Render one Fortran parameter declaration.""" + """Render one procedure parameter and preserve assumed-size syntax. + + Assumed-size dimensions must follow the parameter name rather than remain + an attribute; all other attributes keep their original order. + """ assumed_size = next( (attribute for attribute in node.attributes if attribute.startswith("dimension(") and "*" in attribute), None, @@ -504,26 +622,31 @@ def _visit_FortranParameter(self, node: FortranParameter) -> str: return self._declaration(node.type_name, node.name, node.attributes) def _visit_FortranDeclaration(self, node: FortranDeclaration) -> str: - """Render one Fortran declaration.""" + """Render one non-parameter declaration from its type and attributes.""" return self._declaration(node.type_name, node.name, node.attributes) def _visit_FortranTypeDefinition(self, node: FortranTypeDefinition) -> str: - """Render one typed holder shared by producers and consumers.""" + """Render one derived-type definition and preserve component order.""" lines = [f"type :: {node.name}"] lines.extend(self._indented(self.visit(component)) for component in node.components) lines.append(f"end type {node.name}") return "\n".join(lines) def _visit_FortranAssignment(self, node: FortranAssignment) -> str: - """Render one Fortran assignment.""" + """Render one Fortran assignment, using expression continuations if needed.""" return self._continued_assignment(node.target, "=", node.expression.text) def _visit_FortranPointerAssignment(self, node: FortranPointerAssignment) -> str: - """Render one Fortran pointer association.""" + """Render one Fortran pointer association with the shared wrapper.""" return self._continued_assignment(node.target, "=>", node.expression.text) def _continued_assignment(self, target: str, operator: str, expression: str) -> str: - """Wrap a long assignment whose expression has parenthesized items.""" + """Render an assignment and wrap only a suitable parenthesized expression. + + Target, operator, and expression are already rendered backend values. + Short or opaque expressions retain their exact text; only a recognized + parenthesized argument list is delegated to continuation rendering. + """ rendered = f"{target} {operator} {expression}" if len(rendered) <= self._LINE_LIMIT: return rendered @@ -534,28 +657,28 @@ def _continued_assignment(self, target: str, operator: str, expression: str) -> return self._continued_call(f"{target} {operator} {function_name}(", arguments) def _visit_FortranNullify(self, node: FortranNullify) -> str: - """Render one pointer nullification statement.""" + """Render one Fortran pointer-nullification statement.""" return f"nullify({node.target})" def _visit_FortranAllocate(self, node: FortranAllocate) -> str: - """Render one explicit allocation statement.""" + """Render one allocation with optional extents and status destination.""" shape = f"({', '.join(item.text for item in node.extents)})" if node.extents else "" status = f", stat={node.status}" if node.status is not None else "" return f"allocate({node.target}{shape}{status})" def _visit_FortranDeallocate(self, node: FortranDeallocate) -> str: - """Render one explicit deallocation statement.""" + """Render one explicit deallocation for the node target.""" return f"deallocate({node.target})" def _visit_FortranCall(self, node: FortranCall) -> str: - """Render one Fortran call statement.""" + """Render one Fortran call and wrap its already-rendered arguments.""" return self._continued_call( f"call {node.function_name}(", tuple(argument.text for argument in node.arguments), ) def _visit_FortranIf(self, node: FortranIf) -> str: - """Render one Fortran conditional statement.""" + """Render one Fortran conditional with optional else body in node order.""" lines = [self._continued_condition(node.condition.text)] lines.extend(self._indented(self.visit(statement)) for statement in node.body) if node.else_body: @@ -565,7 +688,11 @@ def _visit_FortranIf(self, node: FortranIf) -> str: return "\n".join(lines) def _continued_condition(self, condition: str) -> str: - """Wrap a long logical condition at explicit Fortran operators.""" + """Render a condition and wrap it only at explicit logical operators. + + The condition is an already-rendered expression. Conditions without + recognized .and. or .or. boundaries remain intact for final validation. + """ rendered = f"if ({condition}) then" if len(rendered) <= self._LINE_LIMIT: return rendered @@ -581,7 +708,7 @@ def _continued_condition(self, condition: str) -> str: return "\n".join(lines) def _visit_FortranSelectCase(self, node: FortranSelectCase) -> str: - """Render runtime-rank dispatch without hiding branch structure.""" + """Render one select-case statement and preserve case/body ordering.""" lines = [f"select case ({node.expression.text})"] for case in node.cases: selector = "default" if case.value is None else f"({case.value})" @@ -591,14 +718,19 @@ def _visit_FortranSelectCase(self, node: FortranSelectCase) -> str: return "\n".join(lines) def _visit_FortranInterface(self, node: FortranInterface) -> str: - """Render one explicit interface block.""" + """Render one abstract or concrete interface block from procedure nodes.""" lines = ["abstract interface" if node.abstract else "interface"] lines.extend(self._indented(self.visit(procedure)) for procedure in node.procedures) lines.append("end interface") return "\n".join(lines) def _visit_FortranInterfaceProcedure(self, node: FortranInterfaceProcedure) -> str: - """Render one native procedure declaration inside an interface.""" + """Render one interface procedure with declarations and optional result. + + Parameter declarations take precedence when supplied, otherwise the + procedure parameters are reused. This mirrors the completed backend node + rather than inferring any native procedure details. + """ kind = "subroutine" if node.is_subroutine else "function" lines = [self._interface_procedure_signature(node, kind)] lines.extend(self._interface_import_lines(node)) @@ -609,36 +741,41 @@ def _visit_FortranInterfaceProcedure(self, node: FortranInterfaceProcedure) -> s return "\n".join(lines) def _interface_procedure_signature(self, node: FortranInterfaceProcedure, kind: str) -> str: - """Render the ordered parameter list and optional native binding.""" + """Render one interface signature from its procedure node and kind. + + The returned declaration preserves parameter order, pure mode, result + naming, and the binding clause selected by the backend node. + """ suffix = f" result({node.result_name})" if node.result_name is not None else "" binding = self._interface_binding_suffix(node) + prefix = "pure " if node.pure else "" return self._continued_call( - f"{kind} {node.name}(", + f"{prefix}{kind} {node.name}(", tuple(parameter.name for parameter in node.parameters), suffix=f"){binding}{suffix}", ) @staticmethod def _interface_binding_suffix(node: FortranInterfaceProcedure) -> str: - """Spell one named, unnamed, or absent C binding clause.""" + """Return the named, unnamed, or absent C binding suffix for one node.""" if node.bind_name is not None: return f' bind(c, name="{node.bind_name}")' return " bind(c)" if node.bind_c else "" def _interface_import_lines(self, node: FortranInterfaceProcedure) -> tuple[str, ...]: - """Render the optional interface import declaration.""" + """Return the indented import line for a procedure, or no lines.""" if not node.imports: return () return (self._indented(f"import :: {', '.join(node.imports)}"),) def _interface_result_lines(self, node: FortranInterfaceProcedure) -> tuple[str, ...]: - """Render a complete function result declaration when present.""" + """Return an indented result declaration only when both fields exist.""" if node.result_name is None or node.result_type is None: return () return (self._indented(f"{node.result_type} :: {node.result_name}"),) def _function_signature(self, node: FortranFunction) -> str: - """Render a Fortran function signature.""" + """Render a function or subroutine signature from the procedure node.""" suffix = f" result({node.result_name})" if node.result_name is not None else "" bind = f' bind(c, name="{node.bind_name}")' if node.bind_name is not None else " bind(c)" if node.bind_c else "" kind = "subroutine" if node.is_subroutine else "function" @@ -648,6 +785,8 @@ def _function_signature(self, node: FortranFunction) -> str: suffix=f"){suffix}{bind}", ) + # Continuation and declaration layout + def _continued_call( self, prefix: str, @@ -655,7 +794,12 @@ def _continued_call( *, suffix: str = ")", ) -> str: - """Wrap one comma-separated Fortran argument list with continuations.""" + """Render and, when needed, wrap a comma-separated argument list. + + Prefix and suffix are source fragments from a caller. Arguments retain + their established order; recognized nested forms are delegated only after + the one-line representation exceeds the preferred line limit. + """ rendered = f"{prefix}{', '.join(arguments)}{suffix}" if len(rendered) <= self._LINE_LIMIT: return rendered @@ -683,7 +827,12 @@ def _continued_argument_lines( last_argument: bool, suffix: str, ) -> tuple[str, ...]: - """Wrap one outer-call argument without interpreting semantic policy.""" + """Return continuation lines for one already-rendered outer-call argument. + + Simple constructors and parenthesized values receive structural wrapping; + all other expressions remain opaque text. This helper performs layout + only and never interprets wrapper or ownership policy. + """ array_items = self._array_constructor_items(argument) if array_items is not None: return self._continued_array_constructor_lines(array_items, last_argument, suffix) @@ -699,7 +848,11 @@ def _continued_array_constructor_lines( last_argument: bool, suffix: str, ) -> tuple[str, ...]: - """Wrap one simple Fortran array constructor item by item.""" + """Return continuation lines for a simple array constructor. + + Items arrive in source order. Long multiplicative items may split between + factors; other item text is retained unchanged on a single continuation. + """ lines = [] last_item_index = len(items) - 1 for item_index, item in enumerate(items): @@ -722,7 +875,11 @@ def _continued_parenthesized_lines( last_argument: bool, suffix: str, ) -> tuple[str, ...]: - """Wrap one nested array section or other simple parenthesized value.""" + """Return continuation lines for a parsed parenthesized value. + + The name and items are produced by _parenthesized_items. Each item stays + ordered, with the final item receiving the caller's outer suffix. + """ name, items = expression lines = [f" & {name}(&"] last_item_index = len(items) - 1 @@ -733,13 +890,17 @@ def _continued_parenthesized_lines( return tuple(lines) def _continued_item_ending(self, last_item: bool, last_argument: bool, suffix: str) -> str: - """Return the continuation or outer-call ending for one nested item.""" + """Choose the separator for one nested item from its final-position flags.""" if not last_item: return ", &" return suffix if last_argument else ", &" def _array_constructor_items(self, expression: str) -> tuple[str, ...] | None: - """Return simple array-constructor items that need their own lines.""" + """Parse a simple bracketed constructor into item text, or return None. + + This intentionally recognizes only the shallow layout form used by the + continuation renderer; nested semantic expression parsing belongs earlier. + """ if not (expression.startswith("[") and expression.endswith("]")): return None content = expression[1:-1] @@ -753,7 +914,11 @@ def _parenthesized_items( *, minimum_items: int = 2, ) -> tuple[str, tuple[str, ...]] | None: - """Return simple parenthesized items that need continuation lines.""" + """Parse one shallow parenthesized value into its name and item texts. + + The optional minimum keeps callers from expanding short forms. Unmatched, + nameless, or too-short expressions return None and remain opaque source. + """ opening = expression.find("(") if opening < 1 or not expression.endswith(")"): return None @@ -763,10 +928,35 @@ def _parenthesized_items( return expression[:opening], items def _declaration(self, type_name: str, name: str, attributes: tuple[str, ...]) -> str: - """Render a Fortran declaration.""" + """Render a Fortran declaration while preserving attribute order.""" suffix = f", {', '.join(attributes)}" if attributes else "" return f"{type_name}{suffix} :: {name}" def _indented(self, text: str) -> str: - """Indent a rendered procedure inside a module body.""" + """Indent every rendered Fortran line for its containing source block.""" return "\n".join(f" {line}" for line in text.splitlines()) + + +if __name__ == "__main__": + from prik.codegen.nodes import CodeExpression + + bridge_module = FortranModule( + name="bind_c_printer_demo_wrapper", + uses=( + FortranUse("iso_c_binding", ("c_double",)), + FortranUse("printer_demo", ("native_double_value => DOUBLE_VALUE",)), + ), + procedures=( + FortranFunction( + name="bind_c_double_value", + parameters=(FortranParameter("value", "real(c_double)", ("value",)),), + result_name="result", + result_type="real(c_double)", + bind_name="DOUBLE_VALUE", + body=(FortranAssignment("result", CodeExpression("native_double_value(value)")),), + ), + ), + ) + + print("Rendered Fortran bridge source:") + print(FortranSourcePrinter().doprint(bridge_module)) diff --git a/prik/wrapper_codegen/visitor.py b/prik/codegen/visitor.py similarity index 100% rename from prik/wrapper_codegen/visitor.py rename to prik/codegen/visitor.py diff --git a/prik/contracts/__init__.py b/prik/contracts/__init__.py index 00a95d8c3..80738f71f 100644 --- a/prik/contracts/__init__.py +++ b/prik/contracts/__init__.py @@ -137,6 +137,10 @@ def apply(target): Bool = _contract_type("Bool", np.bool_) +Bool8 = _contract_type("Bool8", np.bool_) +Bool16 = _contract_type("Bool16", np.bool_) +Bool32 = _contract_type("Bool32", np.bool_) +Bool64 = _contract_type("Bool64", np.bool_) Byte = _contract_type("Byte", constructor_error="Byte has no portable NumPy scalar default") CEnum = _contract_type("CEnum", constructor_error="CEnum requires a resolved native underlying type") Char = _contract_type("Char", constructor_error="Char has no portable NumPy scalar default") @@ -188,9 +192,12 @@ def apply(target): Bounded = _expression Destruction = _expression Finite = _expression +In = _expression +InOut = _expression IsPresent = _expression Len = _expression Ownership = _expression +Out = _expression Pass = _expression PointerAssociation = _expression PointerPolicy = _expression @@ -202,13 +209,14 @@ def apply(target): Work = _expression bind = _decorator -external = _decorator nogil = _decorator native_call = _decorator native_type = _decorator overload = _decorator prototype = _decorator +pure = _decorator raises = _decorator +standalone = _decorator CAnonymous = _contract_type("CAnonymous") CAnonymousMember = _contract_type("CAnonymousMember") @@ -230,6 +238,10 @@ def apply(target): "ArrayCategory", "AssumedType", "Bool", + "Bool8", + "Bool16", + "Bool32", + "Bool64", "Bounded", "Byte", "CAnonymous", @@ -253,6 +265,8 @@ def apply(target): "Float128", "FortranAllocatable", "Immutable", + "In", + "InOut", "Int", "Int8", "Int16", @@ -268,6 +282,7 @@ def apply(target): "ORDER_C", "ORDER_F", "Ownership", + "Out", "Pass", "Pointer", "PointerAssociation", @@ -292,14 +307,15 @@ def apply(target): "Work", "WrappedType", "bind", - "external", "nogil", "native_call", "native_type", "overload", "prototype", + "pure", "private", "raises", + "standalone", } ) @@ -310,6 +326,10 @@ def apply(target): "Annotated", "Any", "Bool", + "Bool8", + "Bool16", + "Bool32", + "Bool64", "Byte", "CAnonymous", "CAnonymousMember", diff --git a/prik/extensions/__init__.py b/prik/extensions/__init__.py deleted file mode 100644 index 163ff8b85..000000000 --- a/prik/extensions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Optional third-party extension resources used by the compiler pipeline.""" diff --git a/prik/parsers/c/parser.py b/prik/parsers/c/parser.py index 00c40b06e..2ab480dba 100644 --- a/prik/parsers/c/parser.py +++ b/prik/parsers/c/parser.py @@ -52,6 +52,14 @@ Compiler/preprocessed input is parsed by the same route after linemarkers are mapped back to original source locations. +The parser deliberately stops at parser-stage facts. In a single `CFile`, a +use such as `struct state *value` can contain a source-local reference object +separate from the `struct state { ... }` definition. `parse_c_project(...)` +assembles the explicitly supplied files, indexes their declarations, and uses +the type resolver to link those references to the project-wide definitions. +It records includes as graph facts; it never follows includes to discover more +parser inputs. + Executable walkthroughs live in ``tests/c/parsing/test_c_parser_developer_tutorial.py``. """ @@ -64,7 +72,7 @@ from collections.abc import Mapping, Sequence from pathlib import Path, PurePosixPath -from .lexer import ( +from prik.parsers.c.lexer import ( CLogicalRecord, CTopLevelSegment, lex_c_source, @@ -76,7 +84,7 @@ top_level_split, top_level_split_with_offsets, ) -from .models import ( +from prik.parsers.c.models import ( CArray, CAtomic, CBool, @@ -121,8 +129,8 @@ CLongDoubleComplex, CLongLong, ) -from .preprocessor import collect_preprocessor_metadata -from .type_resolver import resolve_project_types +from prik.parsers.c.preprocessor import collect_preprocessor_metadata +from prik.parsers.c.type_resolver import resolve_project_types _C_SOURCE_SUFFIXES = {".c", ".h", ".i"} _IDENTIFIER_RE = re.compile(r"[A-Za-z_]\w*") @@ -280,14 +288,14 @@ @dataclass class _PointerOp: - """Declarator operation representing one pointer layer.""" + """Store one pointer layer and its source-order qualifier spellings.""" qualifiers: list[str] @dataclass class _ArrayOp: - """Declarator operation representing one array suffix.""" + """Store one array suffix, including bound and parameter-array modifiers.""" size: str | None = None static: bool = False @@ -297,7 +305,7 @@ class _ArrayOp: @dataclass class _FunctionOp: - """Declarator operation representing one function suffix.""" + """Store one function suffix with parsed parameters and prototype facts.""" parameters: list[CParameter] variadic: bool = False @@ -306,7 +314,7 @@ class _FunctionOp: @dataclass class _ParsedDeclarator: - """Name plus type-construction operations parsed from a declarator.""" + """Store a declarator name and operations in syntax-to-type construction order.""" name: str | None operations: list[_PointerOp | _ArrayOp | _FunctionOp] @@ -384,9 +392,14 @@ def _is_source_key(key: str) -> bool: class CParser: """Parser orchestration object for the partial typed C model. - The instance carries no parse stack; per-call input and preprocessing - configuration flow explicitly through `parse_file` and `parse_project`. - See the module sketch and developer tutorial tests for the helper path. + Use this class when parsing several files with a deliberately scoped + configuration, or use `parse_c_file` / `parse_c_project` for ordinary + calls. The instance carries no parse stack; per-call input and + preprocessing configuration flow explicitly through `parse_file` and + `parse_project`. Each call returns a parse-only model for semantic + conversion or wrapper planning, and malformed supported syntax raises + `CParseError`. See the module sketch and developer tutorial tests for the + helper path. Class section map: - public file/project parse entrypoints; @@ -410,12 +423,25 @@ def parse_file( preprocessing: str = "raw", encoding: str = "utf-8", ) -> CFile: - """Parse one source string/path into a `CFile` parser model. - - The current implementation supports raw preprocessing metadata, - compiler-fed preprocessed text, and the partial grammar subset - documented in `docs/c_parser.md`. + """Parse one C source string or existing path into a `CFile` model. + + Use this entrypoint for one explicit translation unit. Pass inline + source with `filename` to retain diagnostic provenance, or an existing + path to read source using `encoding`. In `raw` mode, quoted/system + includes and pragmas become metadata but macros and conditional + directives raise `CParseError`; use `compiler` or `preprocessed` for + compiler-expanded input and line-marker provenance. The returned + parser-stage model is normally passed to `parse_project` or semantic + conversion. It does not resolve cross-file references or recursively + parse includes. + + Raises: + CParseError: If supported C syntax is malformed or raw source + needs compiler preprocessing. + ValueError: If `preprocessing` is not `raw`, `compiler`, or + `preprocessed`. """ + # Stage 1: obtain source text and determine its preprocessing mode. source_path: Path | None = None if _looks_like_existing_source_path(source_or_path): path = Path(source_or_path) @@ -434,6 +460,7 @@ def parse_file( parsed = CFile(filename=filename, preprocessing=preprocessing) if preprocessing == "raw": + # Stage 2a: collect non-expanding preprocessing metadata and parse. self._raise_for_raw_preprocessing_directives(source, filename) effective_include_dirs = list(include_dirs or ()) if source_path is not None: @@ -462,6 +489,7 @@ def parse_file( parsed.diagnostics.extend(parser_diagnostics) self._normalize_redeclarations(parsed) elif preprocessing in {"compiler", "preprocessed"}: + # Stage 2b: parse compiler output and recover original locations. parsed.preprocessed_source_path = inferred_preprocessed_path functions, structs, unions, enums, typedefs, variables, parser_diagnostics = self._parse_translation_unit( source, @@ -557,13 +585,23 @@ def parse_project( preprocessing: str = "raw", encoding: str = "utf-8", ) -> CProject: - """Parse explicit project inputs without recursively parsing includes. - - A directory input explicitly supplies all supported source files below - that directory. Include directives are recorded and resolved as graph - facts where possible, but they never cause another file to be opened. + """Parse explicit C files and assemble their resolved `CProject` view. + + Use this entrypoint when declarations in supplied files need common + indexes and type links. `files` may be a mapping of file names to + source, explicit paths, or a directory of `.c`, `.h`, and `.i` files. + Each requested input follows `parse_file`'s preprocessing rules, then + project assembly resolves typedef/tag uses among those inputs. Includes + remain metadata and include-graph edges; even a resolved header is not + opened unless it was explicitly supplied. + + Raises: + CParseError: If a requested source contains unsupported malformed + syntax or raw preprocessing that requires a compiler. + ValueError: If `preprocessing` is not a supported parser mode. """ if isinstance(files, Mapping): + # Stage 1: parse caller-owned in-memory sources without discovery. parsed_files = { name: self.parse_file( source, @@ -576,6 +614,7 @@ def parse_project( } return self._assemble_project(parsed_files) + # Stage 1: turn explicit paths or one directory into stable inputs. paths: list[Path] = [] root: Path | None = None if isinstance(files, str | Path): @@ -588,6 +627,7 @@ def parse_project( else: paths = [Path(p) for p in files] + # Stage 2: parse each explicit input, then assemble project-wide links. parsed_files: dict[str, CFile] = {} for path in sorted(paths): key = path.name if root is not None else str(path) @@ -607,6 +647,8 @@ def _assemble_project(self, files: Mapping[str, CFile]) -> CProject: This helper is useful when an orchestration layer preprocesses each source first and attaches recipe metadata before project resolution. + It consumes caller-owned file models, makes an internal mapping copy, + and returns indexes/type links without parsing or opening more files. Example: >>> parser = CParser() @@ -2823,12 +2865,16 @@ def _parse_translation_unit( list[CVariable], list[CDiagnostic], ]: - """Dispatch top-level C external declarations by grammar role. - - The ordering here is intentional: aggregate definitions are parsed - before function/declaration fallback, and ordinary `;` declarations - all flow through the shared declaration backend. + """Dispatch top-level segments and return their typed parser facts. + + `source` has already been assigned a preprocessing mode by + `parse_file`. The ordering is intentional: aggregate definitions are + parsed before function/declaration fallback, and ordinary `;` + declarations all flow through the shared declaration backend. Invalid + syntax raises `CParseError`; tolerable unsupported declarators become + ordered diagnostics rather than partial declarations. """ + # Stage 1: reject old-style definitions before splitting source. self._raise_for_unsupported_old_style_definitions( source, filename, @@ -2836,6 +2882,7 @@ def _parse_translation_unit( normalize_compiler_extensions=normalize_compiler_extensions, ) + # Stage 2: prepare result collections in declaration encounter order. functions: list[CFunction] = [] structs: list[CStruct] = [] unions: list[CUnion] = [] @@ -2844,6 +2891,7 @@ def _parse_translation_unit( variables: list[CVariable] = [] diagnostics: list[CDiagnostic] = [] + # Stage 3: normalize compiler extensions and dispatch each grammar role. for segment in split_top_level_c_source( source, filename=filename, @@ -2919,9 +2967,17 @@ def _parse_translation_unit( return functions, structs, unions, enums, typedefs, variables, diagnostics def _build_project(self, parsed_files: dict[str, CFile]) -> CProject: - """Build project indexes, include graph facts, and resolved type links.""" + """Build indexes, include graph facts, and canonical type links. + + Consumes only already parsed explicit files. It preserves file-level + declaration ordering, merges project indexes according to existing + redeclaration rules, and appends project diagnostics without opening + include targets. + """ project = CProject(files=parsed_files) all_functions: list[CFunction] = [] + + # Stage 1: index each file's local declarations and include facts. for filename, file in parsed_files.items(): project.functions_by_file[filename] = [function.name for function in file.functions] all_functions.extend(file.functions) @@ -2947,8 +3003,12 @@ def _build_project(self, parsed_files: dict[str, CFile]) -> CProject: project.includes[f"{filename}:{include.target}"] = include project.diagnostics.extend(file.diagnostics) self._index_file_includes(project, filename, file) + + # Stage 2: derive cross-file reporting relations and canonical types. self._index_header_source_pairs(project) resolve_project_types(project) + + # Stage 3: choose one project function per compatible declaration set. normalized_functions = self._deduplicate_functions(all_functions, project.diagnostics) for function in normalized_functions: project.functions[function.name] = function @@ -3118,7 +3178,13 @@ def parse_c_file( preprocessing: str = "raw", encoding: str = "utf-8", ) -> CFile: - """Parse one C source string/path using the default parser instance. + """Parse one C source string or path with the shared `CParser`. + + This is the usual public API for a single translation unit. It has the + same source/path and preprocessing behavior as `CParser.parse_file`; use + `filename` for inline-source diagnostics, and consume the returned + `CFile` directly or supply it to project/semantic stages. Syntax and raw + preprocessing failures raise `CParseError`. Example: >>> parse_c_file("int answer(void);", filename="api.h").functions[0].name @@ -3140,7 +3206,13 @@ def parse_c_project( preprocessing: str = "raw", encoding: str = "utf-8", ) -> CProject: - """Parse multiple C files or a directory using the default parser instance. + """Parse explicitly supplied C inputs into one resolved project model. + + Use this convenience API when the supplied units must share typedef/tag + resolution and project indexes. Inputs may be a source mapping, paths, or + a directory; includes are recorded but never recursively parsed. The + returned `CProject` is normally consumed by semantic conversion or wrapper + generation, while parse/preprocessing errors propagate as `CParseError`. Example: >>> project = parse_c_project({"api.h": "int answer(void);"}) @@ -3153,3 +3225,24 @@ def parse_c_project( preprocessing=preprocessing, encoding=encoding, ) + + +if __name__ == "__main__": + source = """\ +typedef unsigned long api_size; +struct state { int id; }; +api_size count(void); +void step(struct state *value); +""" + + parsed = parse_c_file(source, filename="state_api.h") + count = next(function for function in parsed.functions if function.name == "count") + step = next(function for function in parsed.functions if function.name == "step") + state = parsed.structs[0] + state_reference = step.parameters[0].type.components[-1] + + print(f"Parsed: {parsed.filename}") + print(f"Typedef: {parsed.typedefs[0].name} -> unsigned long") + print(f"Struct: {state.name} ({state.members[0].name})") + print(f"Function: {count.name}() -> {count.result_type.name}") + print(f"Function: {step.name}({step.parameters[0].name}) -> pointer to struct {state_reference.name}") diff --git a/prik/parsers/c/preprocessor.py b/prik/parsers/c/preprocessor.py index fcb283464..f706ee77b 100644 --- a/prik/parsers/c/preprocessor.py +++ b/prik/parsers/c/preprocessor.py @@ -1,3 +1,12 @@ +"""Collect safe raw-preprocessor metadata for the C parser. + +This parser-local module deliberately does not expand macros or choose +conditional-compilation branches; compiler-backed expansion belongs to +``prik.pipeline.preprocessing``. Before raw C grammar parsing, it normalizes +comments and continuations, records literal ``#include`` and ``#pragma`` facts, +and reports unresolved quoted includes without reading included source. +""" + from __future__ import annotations import re @@ -5,10 +14,11 @@ from dataclasses import dataclass, field from pathlib import Path -from .lexer import CLogicalRecord, NormalizedCSource, normalize_c_source -from .models import CDiagnostic, CInclude, CMacro, CRawDirective, CSourceLocation +from prik.parsers.c.lexer import CLogicalRecord, NormalizedCSource, normalize_c_source +from prik.parsers.c.models import CDiagnostic, CInclude, CMacro, CRawDirective, CSourceLocation +# Raw directive recognition and the subset preserved as parser provenance. _INCLUDE_RE = re.compile(r'^\s*#\s*include\s*(?:"([^"]+)"|<([^>]+)>)') _DIRECTIVE_RE = re.compile(r"^\s*#\s*([A-Za-z_]\w*)\b(.*)$") _RAW_PROVENANCE_DIRECTIVES = {"pragma"} @@ -16,6 +26,14 @@ @dataclass class CPreprocessorMetadata: + """Return raw directive facts collected before C declaration parsing. + + ``includes`` preserves literal include order, ``raw_directives`` retains + supported provenance directives, and ``diagnostics`` records recoverable + local-include lookup failures. ``macros`` is retained for the parser model + shape but raw collection never evaluates or creates macro definitions. + """ + includes: list[CInclude] = field(default_factory=list) macros: list[CMacro] = field(default_factory=list) raw_directives: list[CRawDirective] = field(default_factory=list) @@ -23,6 +41,12 @@ class CPreprocessorMetadata: def _record_location(record: CLogicalRecord) -> CSourceLocation: + """Build one directive location from a normalized logical source record. + + The returned location points at the original physical line and at the + first ``#`` when available. Records without original source text retain + the established column-one fallback. + """ source_line = record.source_line column = 1 if source_line is not None: @@ -42,6 +66,12 @@ def _resolve_local_include( filename: str | None, include_dirs: Sequence[str | Path] | None, ) -> str | None: + """Resolve one quoted include for metadata, without parsing the target. + + Candidates are checked beside ``filename`` first and then in ``include_dirs`` + in the supplied order. The first regular file is returned as a string; + filesystem errors are ignored so later include directories remain usable. + """ candidates: list[Path] = [] if filename: candidates.append(Path(filename).parent / target) @@ -62,9 +92,21 @@ def collect_preprocessor_metadata( *, include_dirs: Sequence[str | Path] | None = None, ) -> CPreprocessorMetadata: + """Collect safe raw-preprocessor facts from one C source string. + + Use this immediately before raw C grammar parsing. The result preserves + literal ``#include`` directives, ``#pragma`` provenance, and warnings for + unresolved quoted includes; it neither expands macros nor reads includes. + The C parser consumes these collections as file metadata, while directives + requiring actual preprocessing are rejected elsewhere by the raw-mode + guard. + """ + + # Stage 1: normalize comments and continuations while retaining locations. normalized = normalize_c_source(source, filename=filename) metadata = CPreprocessorMetadata() + # Stage 2: retain directives that are safe parser provenance. for record in normalized.records: directive_match = _DIRECTIVE_RE.match(record.text) if directive_match: @@ -79,6 +121,7 @@ def collect_preprocessor_metadata( ) continue + # Stage 3: classify literal includes and report unresolved local paths. include_match = _INCLUDE_RE.match(record.text) if include_match: local_target, system_target = include_match.groups() @@ -115,3 +158,29 @@ def collect_preprocessor_metadata( "collect_preprocessor_metadata", "normalize_c_source", ) + + +if __name__ == "__main__": + from tempfile import TemporaryDirectory + + source = """\ +#pragma once +#include "state.h" +#include +""" + + metadata = collect_preprocessor_metadata(source) + + print(f"Raw directive: #{metadata.raw_directives[0].directive} {metadata.raw_directives[0].argument}") + print("Includes: " + ", ".join(f"{include.kind} {include.target}" for include in metadata.includes)) + print(f"Diagnostic: {metadata.diagnostics[0].code}") + + with TemporaryDirectory() as directory: + header_path = Path(directory) / "api.h" + (header_path.parent / "state.h").write_text("struct state;\n", encoding="utf-8") + resolved_metadata = collect_preprocessor_metadata(source, filename=str(header_path)) + + print( + f"Resolved include: {Path(resolved_metadata.includes[0].resolved_path).name} " + f"(diagnostics: {len(resolved_metadata.diagnostics)})" + ) diff --git a/prik/parsers/c/type_resolver.py b/prik/parsers/c/type_resolver.py index 7ba2e073f..3af714dc3 100644 --- a/prik/parsers/c/type_resolver.py +++ b/prik/parsers/c/type_resolver.py @@ -1,8 +1,30 @@ -"""Basic C project type resolution for parser models.""" +"""Resolve basic C typedef and tag references in completed parser projects. + +This parser-stage module runs after every explicit C input has been parsed and +project indexes have been assembled. It canonicalizes supported typedef chains +and unqualified struct, union, and enum tags while preserving unresolved +references and emitting stable diagnostics for typedef cycles. It does not +make semantic or wrapper-policy decisions. + +For example, consider two parsed files that together declare:: + + typedef unsigned long api_size; + api_size count(void); + + struct state { int id; }; + void step(struct state *value); + +Before resolution, ``count`` and ``step`` may hold separate parser reference +objects named ``api_size`` and ``state``. Afterwards, ``count.result_type`` +is ``project.typedefs["api_size"]``, and the ``struct state`` component in +``step``'s pointer type is ``project.structs["state"]``. Typedef chains are +linked recursively too: ``typedef raw_size api_size;`` makes +``api_size.type`` refer to ``raw_size``'s canonical typedef object. +""" from __future__ import annotations -from .models import ( +from prik.parsers.c.models import ( CComposedType, CDiagnostic, CEnum, @@ -18,12 +40,29 @@ ) +# Public project-resolution entrypoint. + + def resolve_project_types(project: CProject) -> CProject: - """Resolve basic typedef and tag references inside a parsed C project.""" + """Canonicalize basic cross-file type references in a parsed C project. + + Call this after the parser has populated ``project`` and all of its indexes. + The resolver mutates declaration types in place, returns the same project + for fluent parser orchestration, leaves unknown names as ``CTypedef`` + references, and appends one diagnostic per typedef cycle. + + The supported scope is deliberately limited to typedef chains and + unqualified struct, union, and enum tag links; broader conflict policy and + semantic datatype interpretation remain downstream responsibilities. + """ + emitted_cycles: set[tuple[str, ...]] = set() + + # Stage 1: resolve the canonical typedef index before use sites. for typedef in project.typedefs.values(): _resolve_typedef_definition(project, typedef, [], emitted_cycles) + # Stage 2: resolve every declaration category in each parsed file's order. for file in project.files.values(): for function in file.functions: _resolve_function(project, function, emitted_cycles) @@ -38,11 +77,19 @@ def resolve_project_types(project: CProject) -> CProject: return project +# Declaration traversal helpers. Each mutates parser-owned model fields. + + def _resolve_function( project: CProject, function: CFunction, emitted_cycles: set[tuple[str, ...]], ) -> None: + """Resolve one function result and parameters in declaration order. + + ``function`` is updated in place. Resolving the result before parameters + preserves the existing traversal order and shared typedef-cycle state. + """ function.result_type = _resolve_type(project, function.result_type, [], emitted_cycles) for parameter in function.parameters: _resolve_parameter(project, parameter, emitted_cycles) @@ -53,6 +100,12 @@ def _resolve_parameter( parameter: CParameter, emitted_cycles: set[tuple[str, ...]], ) -> None: + """Resolve one parameter's effective and, when present, declared type. + + Both fields are parser facts: arrays and function parameters may have a + distinct declared type. The parameter is mutated in place while sharing + the project's cycle-diagnostic set. + """ parameter.type = _resolve_type(project, parameter.type, [], emitted_cycles) if parameter.declared_type is not None: parameter.declared_type = _resolve_type(project, parameter.declared_type, [], emitted_cycles) @@ -63,6 +116,7 @@ def _resolve_variable( variable: CVariable, emitted_cycles: set[tuple[str, ...]], ) -> None: + """Resolve the type stored by one variable or aggregate member in place.""" variable.type = _resolve_type(project, variable.type, [], emitted_cycles) @@ -72,17 +126,35 @@ def _resolve_typedef_definition( stack: list[str], emitted_cycles: set[tuple[str, ...]], ) -> None: + """Resolve a typedef definition while extending its current alias stack. + + Typedefs without a definition are left untouched. The copied stack records + the alias path for cycle detection without mutating a caller's recursion + state. + """ if typedef.type is None: return typedef.type = _resolve_type(project, typedef.type, [*stack, typedef.name], emitted_cycles) +# Recursive type and reference resolution. + + def _resolve_type( project: CProject, type_: CType, stack: list[str], emitted_cycles: set[tuple[str, ...]], ) -> CType: + """Resolve nested parser type components and supported named references. + + Composite and function types retain their original container objects while + their child references are replaced in place. Typedefs and unqualified + tags return the project-indexed object when known; all other type facts are + returned unchanged. + """ + + # Stage 1: recurse through containers before resolving their leaf references. if isinstance(type_, CComposedType): type_.components = [_resolve_type(project, component, stack, emitted_cycles) for component in type_.components] return type_ @@ -92,6 +164,8 @@ def _resolve_type( _resolve_type(project, parameter_type, stack, emitted_cycles) for parameter_type in type_.parameter_types ] return type_ + + # Stage 2: resolve aliases and only canonical unqualified tag references. if isinstance(type_, CTypedef): return _resolve_typedef_reference(project, type_, stack, emitted_cycles) if isinstance(type_, CStruct) and type_.name and not type_.qualifiers: @@ -109,6 +183,12 @@ def _resolve_typedef_reference( stack: list[str], emitted_cycles: set[tuple[str, ...]], ) -> CType: + """Resolve one typedef use without replacing unknown or cyclic references. + + A matching definition is resolved first so typedef chains share canonical + objects. If the name is absent or appears in ``stack``, the original use + remains in place and a normalized cycle diagnostic is recorded when needed. + """ target = project.typedefs.get(reference.name) if target is None: return reference @@ -125,12 +205,22 @@ def _record_typedef_cycle( typedef: CTypedef, emitted_cycles: set[tuple[str, ...]], ) -> None: + """Append one stable diagnostic for a typedef loop, regardless of rotation. + + ``cycle`` may include an acyclic alias prefix. The helper isolates the + actual loop, rotates it to a canonical spelling, and uses ``emitted_cycles`` + to avoid duplicate diagnostics from later declaration use sites. + """ + + # Stage 1: isolate the repeated portion from any acyclic alias prefix. first = cycle[-1] start = cycle.index(first) loop = cycle[start:-1] rotations = [tuple(loop[index:] + loop[:index]) for index in range(len(loop))] normalized_loop = min(rotations) normalized = (*normalized_loop, normalized_loop[0]) + + # Stage 2: emit the canonical loop at most once for the whole project. if normalized in emitted_cycles: return emitted_cycles.add(normalized) @@ -147,3 +237,25 @@ def _record_typedef_cycle( __all__ = ("resolve_project_types",) + + +if __name__ == "__main__": + canonical_struct = CStruct(name="state") + state_handle = CTypedef(name="state_handle", type=CStruct(name="state")) + raw_state = CTypedef(name="raw_state", type=CStruct(name="state")) + state_alias = CTypedef(name="state_alias", type=CTypedef(name="raw_state")) + example_project = CProject( + structs={"state": canonical_struct}, + typedefs={ + "state_handle": state_handle, + "raw_state": raw_state, + "state_alias": state_alias, + }, + ) + + resolve_project_types(example_project) + + print("Tag reference:") + print(f"{state_handle.name} -> {state_handle.type.reference_name}") + print("Typedef chain:") + print(f"{state_alias.name} -> {state_alias.type.name} -> {state_alias.type.type.reference_name}") diff --git a/prik/parsers/fortran/cli.py b/prik/parsers/fortran/cli.py index 483cfb992..983f51617 100644 --- a/prik/parsers/fortran/cli.py +++ b/prik/parsers/fortran/cli.py @@ -81,7 +81,7 @@ def _parse_paths(paths: list[str]) -> dict[str, dict]: def _semantic_report(paths: list[str]) -> dict[str, dict]: """Generate semantic IR and pyi text per parsed file.""" from prik.semantics.fortran2ir import fortran_module_to_semantic_module - from prik.wrapper_codegen.printers import emit_module + from prik.codegen.printers import emit_module parsed = _parse_paths(paths) semantic_out: dict[str, dict] = {} diff --git a/prik/parsers/fortran/models.py b/prik/parsers/fortran/models.py index ab29cecf2..74d6a597b 100644 --- a/prik/parsers/fortran/models.py +++ b/prik/parsers/fortran/models.py @@ -7,6 +7,8 @@ from dataclasses import dataclass, field from typing import Any +from prik.utilities.declaration_expressions import split_dimension_bounds, split_top_level_expression + _ANSI = { "bold": "\033[1m", @@ -22,30 +24,13 @@ def _parse_shape_dim(dim: str) -> dict[str, str | None]: token = (dim or "").strip() if not token: return {"raw": "", "lower": None, "upper": None} - if ":" not in token: - return {"raw": token, "lower": "1", "upper": token} - lo, hi = token.split(":", 1) - lo = lo.strip() or None - hi = hi.strip() or None + lo, hi = split_dimension_bounds(token) return {"raw": token, "lower": lo, "upper": hi} def _split_top_level(text: str, delimiter: str) -> list[str]: - parts: list[str] = [] - current: list[str] = [] - depth = 0 - for char in text: - if char == "(": - depth += 1 - elif char == ")" and depth > 0: - depth -= 1 - if char == delimiter and depth == 0: - parts.append("".join(current).strip()) - current = [] - continue - current.append(char) - parts.append("".join(current).strip()) - return parts + """Split lightweight model syntax using the shared balanced-expression scanner.""" + return split_top_level_expression(text, delimiter) def _split_top_level_csv(text: str) -> list[str]: diff --git a/prik/parsers/fortran/parser.py b/prik/parsers/fortran/parser.py index e9bbd1a3a..d6a0c30a1 100644 --- a/prik/parsers/fortran/parser.py +++ b/prik/parsers/fortran/parser.py @@ -9,15 +9,20 @@ from __future__ import annotations import re -import ast from copy import deepcopy -from pathlib import Path from dataclasses import dataclass, replace +from pathlib import Path +from prik.utilities.declaration_expressions import ( + evaluate_integer_expression, + split_declaration_assignment, + split_dimension_bounds, + split_top_level_expression, +) from prik.utilities.visitor import ClassVisitor -from .lexer import preprocess_lines -from .models import ( +from prik.parsers.fortran.lexer import preprocess_lines +from prik.parsers.fortran.models import ( FortranArgument, FortranBlockData, FortranDerivedType, @@ -34,8 +39,8 @@ FortranUseMapping, FortranVariable, ) -from .type_resolver import extract_kind_from_type_spec -from .utils import split_csv +from prik.parsers.fortran.type_resolver import extract_kind_from_type_spec +from prik.parsers.fortran.utils import split_csv _PARSER_ARCHITECTURE_GUIDE = """ Parser architecture quick guide @@ -157,6 +162,7 @@ r'^\s*#\s*(?:line\s+)?\d+(?:\s+(?:"(?:[^"\\]|\\.)*"|\S+))?(?:\s+\d+)*\s*$', re.IGNORECASE, ) +_INTRINSIC_COMPILE_TIME_MODULES = frozenset({"iso_c_binding", "iso_fortran_env"}) _PreprocessedLines = list[tuple[str, int | None, str | None]] @@ -165,6 +171,13 @@ @dataclass(frozen=True) class SourceUnit: + """Represent one recursively sliced Fortran grammar unit. + + The slicer retains the unit's normalized lines and original source bounds + so visitors can parse a local region without losing diagnostic locations. + Concrete subclasses select the matching ``_visit_*`` handler. + """ + kind: str name: str | None lines: _PreprocessedLines @@ -226,6 +239,14 @@ class EnumUnit(SourceUnit): @dataclass class _ParserScope: + """Carry explicit ownership and mutable state while visiting one unit. + + ``model`` receives parsed declarations, ``parent`` preserves lexical + ownership, and procedure visitors use ``state`` for their temporary symbol + table. Helpers receive this record explicitly rather than relying on + parser-global scope. + """ + kind: str name: str | None model: object | None = None @@ -236,6 +257,8 @@ class _ParserScope: @dataclass(frozen=True) class _UnitGrammar: + """Describe the regions and declaration role allowed by one unit kind.""" + kind: str has_execution_part: bool = False has_contains_part: bool = False @@ -245,6 +268,12 @@ class _UnitGrammar: @dataclass(frozen=True) class _UnitParts: + """Store a sliced unit's header, grammar regions, and optional footer. + + The four regions retain their original line mappings. Visitors parse only + the regions supported by their corresponding :class:`_UnitGrammar`. + """ + header: tuple[str, int | None, str | None] | None specification: _PreprocessedLines execution: _PreprocessedLines @@ -254,6 +283,12 @@ class _UnitParts: @dataclass class _ParsedFileUnits: + """Accumulate visited file-level models before building ``FortranFile``. + + Interface procedures remain attached to their interface rather than the + standalone-procedure list; all other collections preserve source order. + """ + modules: list[FortranModule] submodules: list[FortranSubmodule] programs: list[FortranProgram] @@ -285,8 +320,8 @@ def resolve(self, expr: str, prefer_symbolic: bool = True, resolving: frozenset[ if cache_key in self.cache: return self.cache[cache_key] - if ":" in text: - parts = text.split(":") + parts = split_top_level_expression(text, ":") + if len(parts) > 1: resolved = ":".join(self.resolve(p, prefer_symbolic=prefer_symbolic) if p.strip() else p for p in parts) self.cache[cache_key] = resolved return resolved @@ -379,13 +414,27 @@ def parse_file( filename: str | None = None, encoding: str = "utf-8", ) -> FortranFile: - """Parse one source string/path into a `FortranFile` aggregate model.""" + """Parse one source string or path into a ``FortranFile`` model. + + Use this primary entrypoint for one Fortran translation unit. A path + is read with ``encoding`` when ``filename`` is omitted; otherwise the + input is treated as source text and ``filename`` supplies diagnostic + provenance. The returned parse-only model feeds project parsing or + semantic conversion and raises :class:`FortranParseError` for malformed + or unsupported wrapper-relevant syntax. + """ + + # Stage 1: obtain normalized input and slice direct file-level units. code, filename = self._helper_read_source(source_or_path, filename, encoding) lines, root_scope, top_units = self._helper_prepare_source_units(code, filename) + + # Stage 2: visit each unit and attach cross-unit parser facts. units = self._helper_parse_file_units(top_units, root_scope, filename) self._helper_resolve_file_types(units) interfaces = self._helper_attach_file_interfaces(lines, filename, units) self._helper_resolve_file_kinds(lines, filename, units) + + # Stage 3: assemble the stable file model and its source metadata. return self._helper_build_fortran_file(code, filename, encoding, units, interfaces) def parse_project( @@ -394,8 +443,19 @@ def parse_project( *, encoding: str = "utf-8", ) -> FortranProject: - """Parse many sources and merge them into one dependency-aware project model.""" + """Parse explicit sources or paths into one dependency-aware project. + + Use this after collecting a related set of files, or pass a directory + for the supported Fortran source forms. The parser preserves the file + models while resolving project-level kind references and indexing + modules, procedures, and types. Duplicate project symbols and source + failures raise :class:`FortranParseError`. + """ + + # Stage 1: parse each requested source in dependency-aware order. parsed_files = self._helper_parse_project_files(files, encoding) + + # Stage 2: complete cross-file kinds and construct project indexes. self._helper_resolve_project_kinds(parsed_files) project = FortranProject(files=parsed_files) for parsed_file in parsed_files: @@ -403,7 +463,13 @@ def parse_project( return project def parse_module(self, code: _SourceOrLines, filename: str | None = None) -> FortranModule: - """Parse exactly one module unit from inline source or normalized lines. + """Parse exactly one module unit from source text or normalized lines. + + Use this narrow entrypoint when the caller expects a single module + rather than a whole ``FortranFile``. Its result includes module + variables, imports, contained procedures, interfaces, and derived + types. Inputs with zero or multiple module units raise + :class:`FortranParseError`. Example: >>> FortranParser().parse_module("module m\\nend module m\\n").name @@ -426,7 +492,13 @@ def parse_module(self, code: _SourceOrLines, filename: str | None = None) -> For return self._visit(unit, parent_scope=root_scope, filename=filename) def parse_submodule(self, code: _SourceOrLines, filename: str | None = None) -> FortranSubmodule: - """Parse exactly one submodule unit from inline source or normalized lines.""" + """Parse exactly one submodule from source text or normalized lines. + + Use this narrow entrypoint when a caller already knows the input is one + submodule. It returns the submodule's parent/ancestor metadata and + wrapper-relevant specification facts, rejecting zero or multiple + submodule units with :class:`FortranParseError`. + """ _lines, root_scope, all_units = self._helper_prepare_source_units(code, filename) unit = self._expect_single_parse_result( [unit for unit in all_units if unit.kind == "submodule"], @@ -437,7 +509,13 @@ def parse_submodule(self, code: _SourceOrLines, filename: str | None = None) -> return self._visit(unit, parent_scope=root_scope, filename=filename) def parse_interface(self, code: _SourceOrLines, filename: str | None = None) -> FortranInterface: - """Parse exactly one interface block, including nested procedure declarations.""" + """Parse exactly one interface block and its procedure declarations. + + Use this for an isolated interface source fragment. The returned + model preserves generic specifics and interface-only procedure facts; + source containing zero or multiple interface blocks raises + :class:`FortranParseError`. + """ unit, scope = self._expect_single_parse_result( self._collect_interface_source_units(code, filename), parser_name="parse_interface", @@ -447,7 +525,13 @@ def parse_interface(self, code: _SourceOrLines, filename: str | None = None) -> return self._visit(unit, parent_scope=scope, filename=filename) def parse_derived_type(self, code: _SourceOrLines, filename: str | None = None) -> FortranDerivedType: - """Parse exactly one derived-type block and its wrapper-relevant fields.""" + """Parse exactly one derived type and its wrapper-relevant fields. + + Use this for an isolated ``type`` definition or a containing source + with one discoverable derived type. The result includes inheritance, + fields, and type-bound declarations; ambiguous input raises + :class:`FortranParseError`. + """ unit, scope = self._expect_single_parse_result( self._collect_derived_type_source_units(code, filename), parser_name="parse_derived_type", @@ -457,7 +541,13 @@ def parse_derived_type(self, code: _SourceOrLines, filename: str | None = None) return self._visit(unit, parent_scope=scope, filename=filename) def parse_program(self, code: _SourceOrLines, filename: str | None = None) -> FortranProgram: - """Parse exactly one program unit and its specification declarations.""" + """Parse exactly one program unit and its specification declarations. + + Use this when inspecting a single main program. Executable statements + are intentionally not represented, while declarations, imports, and + supported enumerations become parser facts. Ambiguous input raises + :class:`FortranParseError`. + """ _lines, root_scope, all_units = self._helper_prepare_source_units(code, filename) unit = self._expect_single_parse_result( [unit for unit in all_units if unit.kind == "program"], @@ -468,7 +558,12 @@ def parse_program(self, code: _SourceOrLines, filename: str | None = None) -> Fo return self._visit(unit, parent_scope=root_scope, filename=filename) def parse_block_data(self, code: _SourceOrLines, filename: str | None = None) -> FortranBlockData: - """Parse exactly one block-data unit and its specification declarations.""" + """Parse exactly one block-data unit and its specification declarations. + + Use this narrow entrypoint for a single ``block data`` source unit. + It returns common-block and variable facts but no execution model, and + raises :class:`FortranParseError` when the input is not singular. + """ _lines, root_scope, all_units = self._helper_prepare_source_units(code, filename) unit = self._expect_single_parse_result( [unit for unit in all_units if unit.kind == "block_data"], @@ -553,10 +648,23 @@ def _parse_children_of_type(self, child_units, unit_type, *, scope, filename): @staticmethod def _belongs_to_module_like(item, target, *, exclude_interface: bool = False) -> bool: + """Return whether one visited model belongs to a module-like owner. + + Ownership comparison is case-insensitive, matching Fortran naming. + ``exclude_interface`` keeps interface procedure signatures attached to + their interface instead of duplicating them in ``target.procedures``. + """ belongs = bool(item.module and item.module.lower() == target.name.lower()) return belongs and not (exclude_interface and item.in_interface) def _populate_module_like_children(self, target, child_units, *, scope, filename) -> None: + """Visit direct children and append the ones owned by ``target``. + + The method preserves source order within each child category and shares + the caller's scope/filename for diagnostics. Interface-contained + procedure declarations are deliberately excluded from a module's + standalone procedure collection. + """ signatures = self._parse_children_of_type(child_units, ProcedureUnit, scope=scope, filename=filename) types = self._parse_children_of_type(child_units, DerivedTypeUnit, scope=scope, filename=filename) interfaces = self._parse_children_of_type(child_units, InterfaceUnit, scope=scope, filename=filename) @@ -908,6 +1016,12 @@ def _helper_resolve_file_kinds( self._resolve_module_variable_kinds(unit, module_params) for procedure in self._helper_file_procedures(units): self._resolve_signature_kinds(procedure, module_params, resolve_shapes=False) + derived_types = [ + *units.derived_types, + *(derived_type for module in (*units.modules, *units.submodules) for derived_type in module.derived_types), + ] + for derived_type in derived_types: + self._resolve_derived_type_field_kinds(derived_type, module_params) @staticmethod def _helper_file_procedures(units: _ParsedFileUnits): @@ -964,11 +1078,8 @@ def _helper_parse_project_files( return [self.parse_file(path, encoding=encoding) for path in files] def _helper_resolve_project_kinds(self, parsed_files: list[FortranFile]) -> None: - """Resolve cross-file procedure kinds once per procedure model.""" - module_params: dict[str, dict[str, str]] = {} - for parsed_file in parsed_files: - if parsed_file.source is not None: - module_params.update(self._collect_module_parameters(parsed_file.source, parsed_file.filename)) + """Resolve project procedure and module-variable kinds from shared symbols.""" + module_params = self._helper_project_module_symbols(parsed_files) seen_procedures: set[int] = set() for parsed_file in parsed_files: @@ -976,15 +1087,83 @@ def _helper_resolve_project_kinds(self, parsed_files: list[FortranFile]) -> None if id(procedure) not in seen_procedures: self._resolve_signature_kinds(procedure, module_params, resolve_shapes=False) seen_procedures.add(id(procedure)) + for owner in ( + *parsed_file.modules, + *parsed_file.submodules, + *parsed_file.programs, + *parsed_file.block_data_units, + ): + self._resolve_module_variable_kinds(owner, module_params) + for derived_type in parsed_file.derived_types: + self._resolve_derived_type_field_kinds(derived_type, module_params) + for module in parsed_file.modules: + for derived_type in module.derived_types: + self._resolve_derived_type_field_kinds(derived_type, module_params) + + def _helper_project_module_symbols(self, parsed_files: list[FortranFile]) -> dict[str, dict[str, str]]: + """Resolve module symbols and submodule host associations.""" + module_params: dict[str, dict[str, str]] = {} + owners: dict[str, FortranModule | FortranSubmodule] = {} + for parsed_file in parsed_files: + if parsed_file.source is not None: + module_params.update(self._collect_module_parameters(parsed_file.source, parsed_file.filename)) + owners.update((module.name.lower(), module) for module in parsed_file.modules) + owners.update((submodule.name.lower(), submodule) for submodule in parsed_file.submodules) + + resolved = self._resolve_module_parameter_values(module_params) + for _ in range(len(owners) + 1): + changed = False + for owner_name, owner in owners.items(): + symbols = dict(resolved.get(owner_name, {})) + if isinstance(owner, FortranSubmodule): + if owner.ancestor: + symbols.update(resolved.get(owner.ancestor.lower(), {})) + symbols.update(resolved.get(owner.parent.lower(), {})) + symbols.update(self._helper_owner_imported_symbols(owner, resolved)) + updated = self._resolve_module_parameter_values({owner_name: symbols})[owner_name] + if updated != resolved.get(owner_name, {}): + resolved[owner_name] = updated + changed = True + if not changed: + break + return resolved + + @staticmethod + def _helper_owner_imported_symbols( + owner: FortranModule | FortranSubmodule, + resolved_modules: dict[str, dict[str, str]], + ) -> dict[str, str]: + """Return explicit compile-time symbols imported into one owner.""" + imported: dict[str, str] = {} + for dependency, mappings in owner.uses.items(): + dependency_name = dependency.lower() + dependency_symbols = resolved_modules.get(dependency_name, {}) + if not mappings: + imported.update(dependency_symbols) + continue + for mapping in mappings: + source_name = mapping.source.lower() + expression = dependency_symbols.get(source_name) + if expression is None and dependency_name in _INTRINSIC_COMPILE_TIME_MODULES: + expression = mapping.source + if expression is not None: + imported[mapping.local_name.lower()] = expression + return imported @staticmethod def _helper_project_file_procedures(parsed_file: FortranFile): - """Yield one parsed file's procedures in established project order.""" + """Yield direct and interface procedures in project resolution order.""" yield from parsed_file.procedures + for interface in parsed_file.interfaces: + yield from interface.procedures for module in parsed_file.modules: yield from module.procedures + for interface in module.interfaces: + yield from interface.procedures for submodule in parsed_file.submodules: yield from submodule.procedures + for interface in submodule.interfaces: + yield from interface.procedures def _helper_index_project_file(self, project: FortranProject, parsed_file: FortranFile) -> None: """Add one parsed file's public models to project registries.""" @@ -2011,6 +2190,13 @@ def _parse_enum_item( symbols: dict[str, str], next_value: int | None, ) -> tuple[FortranEnumerator, int | None]: + """Parse one enumerator and calculate the following implicit value. + + ``symbols`` is updated only when this item resolves to a value, so later + explicit expressions can refer to it. The returned counter is ``None`` + after a non-integer value, preserving the existing no-guessing rule for + later implicit enumerators. + """ stripped = item.strip() match = re.fullmatch(r"(?P[A-Za-z_]\w*)(?:\s*=\s*(?P.+))?", stripped) if match is None: @@ -2376,12 +2562,22 @@ def _init_derived_type( else: normalized_attrs.append(lowered) - return FortranDerivedType( + derived_type = FortranDerivedType( name=type_name, module=current_module, extends=extends, attributes=normalized_attrs, ) + parameter_match = re.search( + rf"::\s*{re.escape(type_name)}\s*\((?P[^)]*)\)\s*$", + line, + re.IGNORECASE, + ) + derived_type._type_parameters = tuple( + parameter.strip().casefold() + for parameter in split_csv(parameter_match.group("parameters") if parameter_match else "") + ) + return derived_type @staticmethod def _parse_interface_header(line: str) -> tuple[bool, str | None]: @@ -2423,6 +2619,12 @@ def _parse_procedure_header( ) def _module_procedure_scope(self, match, module: str | None, in_interface: bool): + """Create temporary procedure state for one ``module procedure`` header. + + Such implementation declarations have no explicit dummy list here. + The returned state owns an empty symbol table and is finalized through + the same procedure path as regular subroutines and functions. + """ sig = FortranProcedureSignature( name=match.group("name"), kind="module procedure", @@ -2433,6 +2635,12 @@ def _module_procedure_scope(self, match, module: str | None, in_interface: bool) return self._new_procedure_scope_state(sig, symbols={}) def _subroutine_scope(self, match, module: str | None, in_interface: bool): + """Create procedure state from one recognized subroutine header. + + The header match supplies attributes, dummy names, optional ``bind(c)`` + metadata, and interface ownership. Dummy arguments become the initial + case-insensitive symbol table for subsequent declarations. + """ attributes = self._attrs(match.group("prefix"), match.group("tail")) args = [ FortranArgument(name=name, procedure=match.group("name")) for name in split_csv(match.group("args") or "") @@ -2458,6 +2666,13 @@ def _function_scope( lineno: int | None, source_line: str | None, ): + """Create procedure state from one recognized function header. + + The helper initializes dummy arguments and the result symbol, parses an + optional result-type prefix, and raises a source-located error for an + unsupported prefix. Its result symbol stays in the same scope table as + arguments so finalization can detect shadowing and missing declarations. + """ prefix = (match.group("prefix") or "").strip() args = [ FortranArgument(name=name, procedure=match.group("name")) for name in split_csv(match.group("args") or "") @@ -2882,6 +3097,11 @@ def _parse_module_like_spec_line( self._raise_unsupported_module_like_declaration(target, stripped, filename, lineno, source_line) def _raise_unsupported_openmp_declaration(self, target, line, filename, lineno, source_line) -> None: + """Raise the stable diagnostic for an unsupported OpenMP declaration. + + ``target`` determines the parser scope label; the remaining arguments + preserve the original source location in the emitted error. + """ owner_kind, owner_name = self._variable_scope_label(target) raise FortranParseError( f"Unsupported OpenMP declarative directive in {owner_kind} '{owner_name or ''}': {line}", @@ -2893,6 +3113,12 @@ def _raise_unsupported_openmp_declaration(self, target, line, filename, lineno, @staticmethod def _apply_default_module_visibility(scope: _ParserScope, target, line: str) -> bool: + """Apply a bare module-wide ``public`` or ``private`` statement. + + Returns ``True`` only when it consumed a module visibility statement; + callers then avoid treating the line as a declaration. ``target`` is + mutated in place and all other scope kinds are left untouched. + """ if scope.kind != "module" or line not in {"private", "public"}: return False target.default_visibility = line @@ -2900,6 +3126,13 @@ def _apply_default_module_visibility(scope: _ParserScope, target, line: str) -> @staticmethod def _apply_module_attribute_statement(scope: _ParserScope, target, attribute: str, value: str) -> bool: + """Apply a supported module attribute statement and report consumption. + + The helper records named ``public``/``private`` visibility in source + order or updates the default visibility for an empty list. It also + consumes grammar-only ``module procedure`` and ``import`` lines; other + attributes return ``False`` for normal declaration handling. + """ if attribute in {"module procedure", "import"}: return True if scope.kind != "module" or attribute not in {"public", "private"}: @@ -2912,6 +3145,12 @@ def _apply_module_attribute_statement(scope: _ParserScope, target, attribute: st return True def _handle_non_declaration_spec_line(self, scope, target, line, filename, lineno, source_line) -> bool: + """Consume ignored specification text or reject illegal executable text. + + Returns ``True`` when the caller should stop processing ``line``. A + program may contain execution statements after its declaration region; + other module-like scopes receive a source-located error instead. + """ executable = self._is_executable_statement_start(line) if not executable and not self._is_ignored_spec_statement(line): return False @@ -2928,6 +3167,12 @@ def _handle_non_declaration_spec_line(self, scope, target, line, filename, linen return True def _raise_unsupported_module_like_declaration(self, target, line, filename, lineno, source_line) -> None: + """Raise the precise unsupported-declaration error for one scope line. + + Clearly non-declarative text is first classified as invalid syntax; + declaration-like text retains the established unsupported-datatype + diagnostic. Both errors preserve the owner label and source location. + """ owner_kind, owner_name = self._variable_scope_label(target) if "::" not in line and not self._looks_like_declaration_or_spec(line): self._raise_invalid_fortran_syntax_line( @@ -3396,8 +3641,8 @@ def _helper_push_declaration_to_scope( ) for entity in split_csv(right): - initializer = entity.split("=", 1)[1].strip() if "=" in entity else None - raw_name, shape = self._var(entity) + declared_entity, initializer = split_declaration_assignment(entity) + raw_name, shape = self._var(declared_entity) if not raw_name: continue entity_meta = self._entity_decl_meta(raw_name, meta) @@ -3415,14 +3660,32 @@ def _helper_push_declaration_to_scope( continue var = FortranArgument(name=normalized_name) self._apply(var, entity_meta, shape) + self._record_declaration_visibility(scope, target, var, entity_meta) if initializer is not None and meta["parameter"]: var.value = self._normalize_parameter_value(initializer) var.symbolic_value = initializer var.value_type = "expression" target.variables.append(var) + @staticmethod + def _record_declaration_visibility(scope: _ParserScope, target, var: FortranArgument, meta: dict) -> None: + """Make declaration-level module visibility survive finalization.""" + visibility = meta.get("explicit_visibility") + if scope.kind != "module" or visibility not in {"public", "private"}: + return + symbols = getattr(target, f"{visibility}_symbols") + if var.name.casefold() not in {name.casefold() for name in symbols}: + symbols.append(var.name) + @staticmethod def _entity_decl_meta(raw_name: str, meta: dict) -> dict: + """Copy character metadata when one entity supplies ``*length`` syntax. + + Non-character entities and declarations without an entity-level star + return the original ``meta`` mapping unchanged. For ``character`` + entities, a copied mapping records the length as ``kind`` without + mutating sibling entities' declaration metadata. + """ if meta["base_type"] != "character": return meta match = re.search(r"\*\s*(\([^)]*\)|\*|[A-Za-z_]\w*|\d+)\s*$", raw_name) @@ -3457,6 +3720,7 @@ def _new_decl_meta(base_type: str, kind: str | None) -> dict: "parameter": False, "polymorphic": False, "visibility": "public", + "explicit_visibility": None, } @staticmethod @@ -3525,6 +3789,7 @@ def _apply_decl_attrs(meta: dict, attrs: list[str], *, include_argument_access: meta["parameter"] = True elif la in {"public", "private"}: meta["visibility"] = la + meta["explicit_visibility"] = la elif la.startswith("dimension") and "(" in a and ")" in a: shape = split_csv(a[a.find("(") + 1 : a.rfind(")")]) meta["shape"] = shape @@ -3554,12 +3819,9 @@ def _strip_legacy_star_kind_prefix(left: str) -> str: @staticmethod def _var(entry: str): """Split one declaration entity into its name and inline dimensions.""" - e = entry.strip() + e, _initializer = split_declaration_assignment(entry) if not e: # pragma: no cover - split_csv omits empty declaration entities for valid declarations. return "", [] - if "=" in e: - # Keep only the declared entity name/shape; drop initializer text. - e = e.split("=", 1)[0].strip() if "(" in e and e.endswith(")"): name = e[: e.find("(")].strip() return name, split_csv(e[e.find("(") + 1 : -1]) @@ -3605,15 +3867,7 @@ def _apply_internal_type_metadata(arg: FortranVariable, meta: dict) -> None: @staticmethod def _split_dim_bounds(dim: str) -> tuple[str | None, str | None]: """Normalize one dimension into lower and upper bound text.""" - part = dim.strip() - if not part: # pragma: no cover - empty dimensions are invalid Fortran and not emitted by split_csv. - return None, None - if ":" not in part: - return "1", part - lo, hi = part.split(":", 1) - lo = lo.strip() or None - hi = hi.strip() or None - return lo, hi + return split_dimension_bounds(dim) @staticmethod def _extract_bounds(shape: list[str]) -> tuple[list[str | None], list[str | None]]: @@ -4195,7 +4449,10 @@ def _resolve_module_parameter_values(module_params: dict[str, dict[str, str]]) - resolved: dict[str, dict[str, str]] = {} for module_name, params in module_params.items(): resolver = _CompileTimeResolver(params) - resolved[module_name.lower()] = {name.lower(): resolver.resolve(value) for name, value in params.items()} + resolved[module_name.lower()] = { + name.lower(): resolver.resolve(FortranParser._resolve_symbol_reference(value, resolver.symbols)) + for name, value in params.items() + } return resolved @staticmethod @@ -4287,6 +4544,32 @@ def _resolve_module_variable_kinds( var.shape = [resolver.resolve(dim) for dim in var.shape] var.lbound, var.ubound = FortranParser._extract_bounds(var.shape) + @staticmethod + def _resolve_derived_type_field_kinds( + derived_type: FortranDerivedType, + module_params: dict[str, dict[str, str]], + ) -> None: + """Resolve kind and shape parameters for fields in their module scope. + + The helper consumes the same module parameter table as module-variable + resolution and mutates only the parsed field facts. Field declaration + order is preserved, and unresolved native expressions remain symbolic. + """ + resolved_params = FortranParser._resolve_module_parameter_values(module_params) + local_parameters = set(getattr(derived_type, "_type_parameters", ())) + symbols = { + name: value + for name, value in resolved_params.get(str(derived_type.module or "").casefold(), {}).items() + if name.casefold() not in local_parameters + } + resolver = _CompileTimeResolver(symbols) + for field in derived_type.fields: + if field.kind: + field.kind = FortranParser._resolve_kind_expression(field.kind, symbols, resolver=resolver) + if field.shape: + field.shape = [resolver.resolve(dimension) for dimension in field.shape] + field.lbound, field.ubound = FortranParser._extract_bounds(field.shape) + @staticmethod def _resolve_kind_expression( expr: str, @@ -4414,129 +4697,11 @@ def _resolve_variables( def _safe_eval_int_expr(expr: str) -> int | None: """Safely evaluate a restricted integer-only Python expression. - Used to fold simple arithmetic after symbol substitution. Only numeric - constants and basic arithmetic operators are allowed. Any other syntax - returns None rather than raising. + This parser compatibility method delegates to the shared declaration + expression layer. Unsupported or nonintegral expressions return + ``None`` without executing source-language code. """ - normalized = expr.strip() - normalized = re.sub(r"(?<=\d)_[A-Za-z_][A-Za-z0-9_]*", "", normalized) - normalized = re.sub(r"\.and\.", " and ", normalized, flags=re.IGNORECASE) - normalized = re.sub(r"\.or\.", " or ", normalized, flags=re.IGNORECASE) - normalized = re.sub(r"\.not\.", " not ", normalized, flags=re.IGNORECASE) - normalized = re.sub(r"\.true\.", "True", normalized, flags=re.IGNORECASE) - normalized = re.sub(r"\.false\.", "False", normalized, flags=re.IGNORECASE) - - try: - node = ast.parse(normalized, mode="eval") - except SyntaxError: - return None - - allowed_binops = (ast.Add, ast.Sub, ast.Mult, ast.Div, ast.FloorDiv, ast.Mod, ast.Pow) - allowed_unary = (ast.UAdd, ast.USub) - - def _eval(n): - """Evaluate one allowed AST node, returning None when unsupported.""" - if isinstance(n, ast.Expression): - return _eval(n.body) - if isinstance(n, ast.Constant) and isinstance(n.value, int | float | str | bool): - return n.value - if isinstance(n, ast.BinOp) and isinstance(n.op, allowed_binops): - left = _eval(n.left) - right = _eval(n.right) - if ( - left is None - or right is None - or not isinstance(left, int | float) - or not isinstance(right, int | float) - ): - return None - if isinstance(n.op, ast.Add): - return left + right - if isinstance(n.op, ast.Sub): - return left - right - if isinstance(n.op, ast.Mult): - return left * right - if isinstance(n.op, ast.Div): - return left / right - if isinstance(n.op, ast.FloorDiv): - return left // right - if isinstance(n.op, ast.Mod): - return left % right - if isinstance(n.op, ast.Pow): - return left**right - if isinstance(n, ast.UnaryOp) and isinstance(n.op, allowed_unary): - v = _eval(n.operand) - if v is None or not isinstance(v, int | float): - return None - return +v if isinstance(n.op, ast.UAdd) else -v - if isinstance(n, ast.Call) and isinstance(n.func, ast.Name): - name = n.func.id.lower() - args = [_eval(arg) for arg in n.args] - if any(arg is None for arg in args): - return None - try: - if name == "abs" and len(args) == 1 and isinstance(args[0], int | float): - return abs(args[0]) - if name == "max" and args and all(isinstance(arg, int | float) for arg in args): - return max(args) - if name == "min" and args and all(isinstance(arg, int | float) for arg in args): - return min(args) - if name == "mod" and len(args) == 2 and all(isinstance(arg, int | float) for arg in args): - return args[0] % args[1] - if name == "int" and args and isinstance(args[0], int | float): - return int(args[0]) - if name == "len" and len(args) == 1 and isinstance(args[0], str): - return len(args[0]) - if name == "len_trim" and len(args) == 1 and isinstance(args[0], str): - return len(args[0].rstrip()) - if name == "iachar" and len(args) == 1 and isinstance(args[0], str) and args[0]: - return ord(args[0][0]) - except (OverflowError, ValueError, ZeroDivisionError): - return None - if isinstance(n, ast.BoolOp): - values = [_eval(value) for value in n.values] - if any(value is None for value in values): - return None - if isinstance(n.op, ast.And): - return all(bool(value) for value in values) - if isinstance(n.op, ast.Or): - return any(bool(value) for value in values) - if isinstance(n, ast.Compare): - left = _eval(n.left) - if left is None: - return None - for op, comparator in zip(n.ops, n.comparators, strict=False): - right = _eval(comparator) - if right is None: - return None - if isinstance(op, ast.Gt): - ok = left > right - elif isinstance(op, ast.GtE): - ok = left >= right - elif isinstance(op, ast.Lt): - ok = left < right - elif isinstance(op, ast.LtE): - ok = left <= right - elif isinstance(op, ast.Eq): - ok = left == right - elif isinstance(op, ast.NotEq): - ok = left != right - else: - return None - if not ok: - return False - left = right - return True - return None - - val = _eval(node) - if val is None: - return None - if isinstance(val, bool): - return int(val) - if isinstance(val, float) and val.is_integer(): - return int(val) - return val if isinstance(val, int) else None + return evaluate_integer_expression(expr) # ------------------------------------------------------------------ # Project diagnostics @@ -4945,7 +5110,14 @@ def parse_fortran_file( filename: str | None = None, encoding: str = "utf-8", ) -> FortranFile: - """Parse one Fortran source string or path with the shared parser. + """Parse one source string or path using the shared ``FortranParser``. + + This is the normal public API for parser clients. Pass inline source with + an optional ``filename`` for diagnostics, or pass an existing path without + ``filename`` to read it using ``encoding``. The returned ``FortranFile`` + is parser-stage data that is normally passed to project aggregation or + semantic conversion; malformed or unsupported source raises + :class:`FortranParseError`. Example: >>> parse_fortran_file("subroutine ping()\\nend subroutine ping\\n").procedures[0].name @@ -4961,9 +5133,35 @@ def parse_fortran_file( def parse_fortran_project(files, *, encoding: str = "utf-8") -> FortranProject: """Parse explicit Fortran sources or a directory into one project model. + Use this module-level convenience API for cross-file parsing. ``files`` + may be a mapping of names to source, explicit paths, or a directory; the + returned project indexes the parsed files and completed project symbols. + File and duplicate-symbol errors propagate as :class:`FortranParseError`. + Example: >>> project = parse_fortran_project({"types.f90": "module types\\nend module types\\n"}) >>> sorted(project.modules) ['types'] """ return _DEFAULT_PARSER.parse_project(files, encoding=encoding) + + +if __name__ == "__main__": + source = """\ +module metrics + integer, parameter :: n = 4 +contains + subroutine scale(values) + real, intent(inout) :: values(n) + end subroutine scale +end module metrics +""" + + parsed = parse_fortran_file(source, filename="metrics.f90") + module = parsed.modules[0] + procedure = module.procedures[0] + argument = procedure.arguments[0] + + print(f"Module: {module.name}") + print(f"Parameter: {module.variables[0].name} = {module.variables[0].value}") + print(f"Procedure: {procedure.name}({argument.name}: {argument.base_type}[{argument.rank}])") diff --git a/prik/parsers/fortran/type_resolver.py b/prik/parsers/fortran/type_resolver.py index 897648384..05a75f118 100644 --- a/prik/parsers/fortran/type_resolver.py +++ b/prik/parsers/fortran/type_resolver.py @@ -1,22 +1,45 @@ +"""Extract parser-level intrinsic kind metadata from Fortran type specifications. + +This module operates after the declaration parser has separated an intrinsic +base type from its parenthesized type specifier. It preserves the syntax that +later parser and semantic stages need; it does not evaluate kind expressions or +make semantic datatype decisions. +""" + from __future__ import annotations -from .utils import split_csv +from prik.parsers.fortran.utils import split_csv def extract_kind_from_type_spec(base_type: str, type_spec: str) -> str | None: + """Return the parser-facing kind metadata encoded in one type specifier. + + Use this after declaration parsing has identified an intrinsic ``base_type`` + and isolated its parenthesized ``type_spec``. Positional specs such as + ``(8)`` and named ``kind=...`` specs return their kind expression. For + ``character`` declarations with ``len=...``, the complete inner spec is + preserved because parser models carry character length and kind together. + + The function only extracts syntax: nested expressions remain unchanged and + unsupported or empty forms return ``None`` for the caller to handle. + """ + + # Stage 1: normalize the already-isolated parenthesized specifier. if not type_spec: return None inside = type_spec[1:-1].strip() if not inside: return None + # Stage 2: split only top-level comma-separated keyword arguments. items = split_csv(inside) - keywords = {} + keywords: dict[str, str] = {} for item in items: key, sep, value = item.partition("=") if sep: keywords[key.strip().lower()] = value.strip() + # Stage 3: apply the parser's intrinsic-type metadata rules. if base_type == "character" and "len" in keywords: return inside if "kind" in keywords: @@ -24,3 +47,14 @@ def extract_kind_from_type_spec(base_type: str, type_spec: str) -> str | None: if len(items) == 1 and "=" not in items[0]: return items[0].strip() return None + + +if __name__ == "__main__": + examples = [ + ("integer", "(4)"), + ("real", "(kind=selected_real_kind(15, 307))"), + ("character", "(len=16, kind=c_char)"), + ] + + for base_type, type_spec in examples: + print(f"{base_type}{type_spec} -> {extract_kind_from_type_spec(base_type, type_spec)}") diff --git a/prik/parsers/fortran/utils.py b/prik/parsers/fortran/utils.py index ca2484e69..27fbcdd99 100644 --- a/prik/parsers/fortran/utils.py +++ b/prik/parsers/fortran/utils.py @@ -1,5 +1,7 @@ from __future__ import annotations +from prik.utilities.declaration_expressions import split_top_level_expression + def detect_source_form(code: str, filename: str | None = None) -> str: """Detect whether a source looks like fixed-form or free-form Fortran. @@ -28,7 +30,7 @@ def detect_source_form(code: str, filename: str | None = None) -> str: def split_csv(text: str | None) -> list[str]: - """Split a comma-separated list while respecting parenthesis nesting. + """Split a comma-separated list while respecting nested expression syntax. This is used for things that *look* like CSV in Fortran but may contain parenthesized expressions, e.g.: @@ -37,24 +39,8 @@ def split_csv(text: str | None) -> list[str]: - attribute lists: ``dimension(n, m), contiguous`` - shape lists: ``a(1:n, 0:m)`` - Only commas at parenthesis depth 0 are treated as separators. + Only commas outside brackets and quoted literals are separators. """ if not text: return [] - out, cur, depth = [], [], 0 - for ch in text: - if ch in "([": - depth += 1 - elif ch in ")]" and depth > 0: - depth -= 1 - if ch == "," and depth == 0: - piece = "".join(cur).strip() - if piece: - out.append(piece) - cur = [] - continue - cur.append(ch) - piece = "".join(cur).strip() - if piece: - out.append(piece) - return out + return [piece for piece in split_top_level_expression(text, ",") if piece] diff --git a/prik/pipeline/build.py b/prik/pipeline/build.py index a5b1a1cb8..d80cad739 100644 --- a/prik/pipeline/build.py +++ b/prik/pipeline/build.py @@ -5,17 +5,24 @@ from collections.abc import Callable, Iterable from concurrent.futures import Future, ThreadPoolExecutor from dataclasses import dataclass, field, replace +from importlib.util import module_from_spec, spec_from_file_location import json import os from pathlib import Path import shlex +import sys import time +from types import ModuleType from prik.compiling.objects import ObjectFile from prik.compiling.compilers import Compiler, get_condaless_search_path from prik.compiling.native_support import install_native_support from prik.parsers.fortran.parser import parse_fortran_project -from prik.probes.fortran_types import evaluate_fortran_type_facts, evaluate_fortran_type_requirements +from prik.probes.fortran_types import ( + evaluate_fortran_type_facts, + evaluate_fortran_type_requirements, + resolve_fortran_logical_storage_types, +) from prik.pipeline.preprocessing import PreprocessingConfig, preprocess_source from prik.pipeline.wrapper_artifacts import GeneratedSourceFile, RenderedGeneratedWrapperArtifacts from prik.semantics.fortran2ir import ( @@ -33,6 +40,7 @@ SemanticModule, SemanticPrototype, SemanticVariable, + _iter_module_semantic_types, ) from prik.semantics.native_contract import NATIVE_CONTRACT_PREPARED_METADATA, validate_pyi_native_contract from prik.semantics.native_array_handles import ( @@ -42,7 +50,8 @@ from prik.semantics.policy_completion import complete_semantic_policies from prik.pipeline.pyi import _PyiSemanticModuleCache from prik.semantics.pyi_metadata import PYI_LOADED_METADATA -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner +from prik.types.numpy import boolean_storage_bits, is_boolean_semantic_type_name _DEFAULT_BUILD_DIR_NAME = "__prik__" @@ -68,6 +77,9 @@ } +# Build configuration, timing, and mode validation + + def _print_verbose_timing(verbose: bool | int, elapsed: float) -> None: """Print the elapsed time for the immediately preceding build operation.""" if verbose: @@ -116,9 +128,47 @@ def _normalize_compile_jobs(jobs: int | None) -> int: return jobs +def _resolve_build_mode( + *, + makefile: bool, + generate_sources: bool, + jobs: int | None, + verbose: bool | int, +) -> tuple[bool, int]: + """Validate build-output mode and resolve its compiler job limit.""" + if makefile and generate_sources: + raise ValueError("source-only and Makefile generation are mutually exclusive") + generation_only = makefile or generate_sources + compile_jobs = _normalize_compile_jobs(jobs) + if generation_only and verbose: + raise ValueError("source/Makefile generation and verbose direct compilation are separate modes") + return generation_only, compile_jobs + + +# Public build records + + @dataclass(frozen=True) class NativeCompilationUnit: - """One caller-supplied native source and the object it produces.""" + """Describe one native source file that a wrapper build must compile. + + Use this record to inspect the compilation portion of + :attr:`WrapperBuildResult.native_build_plan`; callers normally provide the + corresponding ``native_fortran_sources`` and ``native_fortran_flags`` to a + build entrypoint rather than construct this record themselves. + + Parameters + ---------- + source + Native source path passed to the compiler. + object_path + Object path produced in the build directory. + language + Compiler language selected for ``source``. + module_dir, include_dirs, flags + Module-output location, header/module search paths, and per-source + compiler flags recorded for reproducible builds. + """ source: Path object_path: Path @@ -128,6 +178,12 @@ class NativeCompilationUnit: flags: tuple[str, ...] = () def __post_init__(self) -> None: + """Normalize path and flag fields after dataclass construction. + + The caller may supply path-like values and any iterable of flags. The + frozen record is changed in place with ``Path`` and tuple values so + later manifest and compiler code can rely on a stable representation. + """ object.__setattr__(self, "source", Path(self.source)) object.__setattr__(self, "object_path", Path(self.object_path)) if self.module_dir is not None: @@ -136,6 +192,11 @@ def __post_init__(self) -> None: object.__setattr__(self, "flags", tuple(str(flag) for flag in self.flags)) def to_dict(self) -> dict[str, object]: + """Return a JSON-ready representation of this compilation unit. + + Paths become strings and tuples become lists. The method does not + write a manifest or modify the compilation unit. + """ return { "source": str(self.source), "object": str(self.object_path), @@ -148,17 +209,30 @@ def to_dict(self) -> dict[str, object]: @dataclass(frozen=True) class NativePrebuiltArtifact: - """One caller-supplied native artifact used by the extension link.""" + """Describe one existing object, archive, or shared library to link. + + Build callers usually pass the artifact path through ``native_objects``. + This record appears in the resulting native build plan, where ``kind`` is + one of ``"object"``, ``"archive"``, or ``"shared_library"``. + """ path: Path kind: str def __post_init__(self) -> None: + """Validate the artifact kind and normalize its path field. + + Raises + ------ + ValueError + If ``kind`` cannot be represented as a filesystem link input. + """ if self.kind not in _NATIVE_PATH_LINK_KINDS: raise ValueError(f"Unsupported native artifact kind: {self.kind!r}") object.__setattr__(self, "path", Path(self.path)) def to_dict(self) -> dict[str, object]: + """Return the artifact kind and string path for JSON serialization.""" return { "kind": self.kind, "path": str(self.path), @@ -167,12 +241,24 @@ def to_dict(self) -> dict[str, object]: @dataclass(frozen=True) class NativeLinkItem: - """One ordered item in the native implementation link plan.""" + """Describe one ordered linker input for a wrapper extension. + + Pass these records through ``native_link_items`` when link order matters. + Filesystem items use ``"object"``, ``"archive"``, or + ``"shared_library"`` with a path value; ``"named_library"`` uses a bare + library name and ``"linker_argument"`` passes an argument through + unchanged. + """ kind: str value: Path | str def __post_init__(self) -> None: + """Validate ``kind`` and normalize its value to a path or string. + + Path-based items retain a ``Path`` for file validation. Named + libraries and raw linker arguments retain strings for command output. + """ if self.kind not in _NATIVE_LINK_KINDS: raise ValueError(f"Unsupported native link item kind: {self.kind!r}") if self.kind in _NATIVE_PATH_LINK_KINDS: @@ -181,6 +267,11 @@ def __post_init__(self) -> None: object.__setattr__(self, "value", str(self.value)) def to_dict(self) -> dict[str, object]: + """Return the item in the dictionary shape accepted by build APIs. + + Path-based kinds use ``path``, named libraries use ``name``, and raw + arguments use ``argument``. The record itself remains unchanged. + """ if self.kind in _NATIVE_PATH_LINK_KINDS: return { "kind": self.kind, @@ -199,7 +290,13 @@ def to_dict(self) -> dict[str, object]: @dataclass(frozen=True) class NativeBuildPlan: - """Extension-level native implementation build and link plan.""" + """Record the native compilation and link inputs selected for a build. + + Inspect :attr:`WrapperBuildResult.native_build_plan` to learn which native + sources will compile, which artifacts and libraries will link, and which + include or module directories the generated wrapper uses. It is an + immutable build report, not a command executor. + """ compilation_units: tuple[NativeCompilationUnit, ...] = () produced_objects: tuple[Path, ...] = () @@ -210,6 +307,11 @@ class NativeBuildPlan: link_items: tuple[NativeLinkItem, ...] = () def __post_init__(self) -> None: + """Normalize every collection and filesystem field in this plan. + + This converts accepted iterable/path-like constructor values to tuples + and ``Path`` objects. No files are created, compiled, or linked. + """ object.__setattr__(self, "compilation_units", tuple(self.compilation_units)) object.__setattr__(self, "produced_objects", tuple(Path(path) for path in self.produced_objects)) object.__setattr__(self, "prebuilt_artifacts", tuple(self.prebuilt_artifacts)) @@ -219,6 +321,7 @@ def __post_init__(self) -> None: object.__setattr__(self, "link_items", tuple(self.link_items)) def to_dict(self) -> dict[str, object]: + """Return a complete JSON-ready snapshot of the native build plan.""" return { "compilation_units": [unit.to_dict() for unit in self.compilation_units], "produced_objects": [str(path) for path in self.produced_objects], @@ -232,7 +335,16 @@ def to_dict(self) -> dict[str, object]: @dataclass(frozen=True) class WrapperBuildResult: - """Artifacts produced by one wrapper build.""" + """Report the generated artifacts and mode selected by one build call. + + Every public build entrypoint returns this record. Check ``compiled`` to + distinguish a built extension from ``generate_sources=True`` or + ``makefile=True`` output; then use ``shared_library``, + ``generated_sources``, ``build_makefile``, and ``build_manifest`` as + applicable. Call :meth:`import_module` to explicitly load an existing + extension artifact. ``native_build_plan`` explains the native inputs that + were compiled or linked. + """ sources: tuple[Path, ...] module_name: str @@ -246,7 +358,57 @@ class WrapperBuildResult: build_manifest: Path | None = None manifest: dict[str, object] | None = None + def import_module(self) -> ModuleType: + """Import and return this result's built extension module. + + The shared-library artifact is loaded under ``module_name`` without + changing ``sys.path``. Direct-build results can be imported + immediately; source-only and Makefile results become importable after + their shared-library path exists. Repeated calls return the cached + module for the same artifact. A different module already cached under + the same name raises ``ImportError`` instead of silently returning it. + + Raises + ------ + FileNotFoundError + If ``shared_library`` has not been built yet. + ImportError + If Python cannot create a loader for the artifact or the module + name is already bound to a different module. + """ + if not self.shared_library.is_file(): + raise FileNotFoundError(f"Built extension not found: {self.shared_library}") + + cached_module = sys.modules.get(self.module_name) + if cached_module is not None: + cached_path = getattr(cached_module, "__file__", None) + if cached_path is not None and Path(cached_path).resolve(strict=False) == self.shared_library.resolve( + strict=False + ): + return cached_module + raise ImportError( + f"Cannot import {self.shared_library}: module name {self.module_name!r} " + f"is already bound to {cached_path!r}" + ) + + spec = spec_from_file_location(self.module_name, self.shared_library) + if spec is None or spec.loader is None: + raise ImportError(f"Cannot create an import loader for {self.shared_library}") + module = module_from_spec(spec) + sys.modules[self.module_name] = module + try: + spec.loader.exec_module(module) + except BaseException: + sys.modules.pop(self.module_name, None) + raise + return module + def to_dict(self) -> dict[str, object]: + """Return all result paths and nested plans in JSON-ready form. + + This is suitable for logging or caller-owned serialization. It does + not write a file, trigger compilation, or mutate the result. + """ return { "sources": [str(source) for source in self.sources], "module_name": self.module_name, @@ -262,7 +424,16 @@ def to_dict(self) -> dict[str, object]: } +# Shared build utilities + + def _default_preprocessing_config() -> PreprocessingConfig: + """Create the default compiler-backed preprocessing configuration. + + The build pipeline calls this only when a caller did not provide a + ``PreprocessingConfig``. It returns a fresh configuration for ``gfortran`` + so a build cannot modify shared default lists. + """ return PreprocessingConfig( mode="compiler", compiler="gfortran", @@ -272,13 +443,23 @@ def _default_preprocessing_config() -> PreprocessingConfig: def _fortran_source_for_pipeline(path: Path, preprocessing: PreprocessingConfig) -> str: + """Read one source path in the form required by the Fortran parser. + + Compiler-backed preprocessing produces the expanded source text; other + modes read UTF-8 text directly. The helper reads ``path`` but does not + change the source file or preprocessing configuration. + """ if preprocessing.uses_compiler: return preprocess_source(path, language="fortran", config=preprocessing).source return path.read_text(encoding="utf-8") def _compiler_flags(flags: Iterable[str] | None) -> tuple[str, ...]: - """Normalize caller-supplied compiler flags.""" + """Normalize optional caller compiler flags into an immutable tuple. + + ``None`` becomes an empty tuple and each supplied value becomes a string. + The helper consumes the iterable without invoking a compiler. + """ return tuple(str(flag) for flag in (flags or ())) @@ -288,6 +469,13 @@ def _new_compiler( debug: bool = False, input_compiler: str | None = None, ) -> Compiler: + """Create the compiler configured for generated wrapper code. + + ``input_compiler`` overrides the default ``gfortran`` executable; + ``execute_commands`` selects real compilation versus command recording, + and ``debug`` enables the compiler's debug configuration. The returned + compiler has a Conda-free search path and has not run any commands yet. + """ return Compiler.from_fortran_executable( input_compiler or "gfortran", debug=debug, @@ -296,6 +484,22 @@ def _new_compiler( ) +def _validated_wrapper_module_name(requested_name: str | None, default_name: str) -> str: + """Choose a requested or default extension name and validate it. + + Returns the requested name when present, otherwise ``default_name``. A + non-identifier cannot be imported as a Python extension and raises + ``ValueError`` before files are generated. + """ + module_name = requested_name or default_name + if not module_name.isidentifier(): + raise ValueError(f"Output name must be a valid Python identifier: {module_name!r}") + return module_name + + +# Rendered wrapper artifacts and native compilation + + def _expected_generated_files( *, source_objects: tuple[ObjectFile, ...], @@ -303,6 +507,12 @@ def _expected_generated_files( module_name: str, shared_library: Path, ) -> tuple[Path, ...]: + """Collect the build artifacts that currently exist on disk. + + Combines caller-native object paths, expected generated bridge/binding + files, the shared library, and installed native-support files. Missing + optional outputs are omitted; this helper only reads filesystem state. + """ candidates = [ *(source_obj.object_path for source_obj in source_objects), output_dir / f"bind_c_{module_name}.mod", @@ -462,11 +672,19 @@ def _rendered_wrapper_link_language( @dataclass(frozen=True) class _CompiledObject: + """Store the recorded compiler command and elapsed time for one object.""" + command: tuple[str, ...] | None elapsed: float def _compile_one_object(compiler: Compiler, object_file: ObjectFile) -> _CompiledObject: + """Compile one object and return its command record plus elapsed time. + + The supplied ``compiler`` performs the compile and may create the object + file. A tuple command is retained for Makefile generation; other compiler + return values are represented as ``None``. + """ started = time.perf_counter() command = compiler.compile_object(object_file, verbose=False) return _CompiledObject( @@ -482,6 +700,12 @@ def _report_compiled_object( label: str, verbose: bool | int, ) -> None: + """Print verbose diagnostics for one completed object compilation. + + Receives the object and timing record produced by ``_compile_one_object``. + When ``verbose`` is false it changes nothing; otherwise it writes the + labelled source-to-object mapping, command, and duration to standard out. + """ if not verbose: return _print_verbose_step(verbose, f"{label}: {object_file.source} -> {object_file.object_path}") @@ -508,6 +732,11 @@ def _submit_object_stage( compiler: Compiler, object_files: Iterable[ObjectFile], ) -> tuple[tuple[ObjectFile, Future[_CompiledObject]], ...]: + """Submit one independent compilation group to an executor. + + Each input object produces one ``(object_file, future)`` pair. The helper + schedules work but does not wait for it or report verbose output. + """ return tuple( (object_file, executor.submit(_compile_one_object, compiler, object_file)) for object_file in object_files ) @@ -519,6 +748,12 @@ def _finish_object_stage( label: str, verbose: bool | int, ) -> None: + """Wait for a submitted compilation group and report each result. + + ``pending`` comes from ``_submit_object_stage``. Calling ``future.result`` + propagates compiler failures; successful objects are reported in input + order when verbose output is enabled. + """ for object_file, future in pending: _report_compiled_object(object_file, future.result(), label=label, verbose=verbose) @@ -570,6 +805,7 @@ def _build_rendered_wrapper_extension( verbose: bool | int = False, ) -> WrapperBuildResult: """Build one extension from rendered wrapper-plan artifacts.""" + # Materialize the canonical wrapper output before creating compiler inputs. rendered.freeze() output_path = Path(output_dir) output_path.mkdir(parents=True, exist_ok=True) @@ -577,6 +813,7 @@ def _build_rendered_wrapper_extension( shared_output_path.mkdir(parents=True, exist_ok=True) _write_rendered_wrapper_sources(rendered, output_path, verbose=verbose) + # Prepare generated-object inputs and their native support files. compiler = compiler or _new_compiler() resolved_native_build_plan = native_build_plan or NativeBuildPlan() bridge_objects, binding_objects = _rendered_wrapper_object_stages( @@ -597,6 +834,8 @@ def _build_rendered_wrapper_extension( prik_dirpath=str(output_path), verbose=verbose, ) + + # Compile dependency-ready native sources, then the bridge and binding. _compile_extension_objects( compiler, native_batches=native_compile_batches, @@ -606,6 +845,7 @@ def _build_rendered_wrapper_extension( verbose=verbose, ) + # Link the generated and caller-supplied objects into the extension. linking_started = time.perf_counter() shared_library = compiler.link_extension( module_name=rendered.artifacts.module_name, @@ -618,12 +858,12 @@ def _build_rendered_wrapper_extension( verbose=verbose, ) _print_verbose_timing(verbose, time.perf_counter() - linking_started) - generated_sources = tuple( + generated_source_paths = tuple( path for path in rendered.artifacts.generated_files if _rendered_artifact_output_path(output_path, path).exists() ) - generated_sources = tuple(_rendered_artifact_output_path(output_path, path) for path in generated_sources) + generated_sources = tuple(_rendered_artifact_output_path(output_path, path) for path in generated_source_paths) return WrapperBuildResult( sources=tuple(Path(source) for source in sources), module_name=rendered.artifacts.module_name, @@ -668,6 +908,30 @@ def _attach_build_makefile( ) +def _finalize_build_mode( + result: WrapperBuildResult, + *, + makefile: bool, + generate_sources: bool, + compiler: Compiler, + source_objects: tuple[ObjectFile, ...], + extra_dependencies: tuple[Path, ...] = (), + build_manifest: Path | None = None, +) -> WrapperBuildResult: + """Turn a planned build into its requested source-only or Makefile result.""" + if makefile: + return _attach_build_makefile( + result, + compiler=compiler, + source_objects=source_objects, + extra_dependencies=extra_dependencies, + build_manifest=build_manifest, + ) + if generate_sources: + return replace(result, compiled=False) + return result + + def _render_wrapper_plan( module: SemanticModule, *, @@ -691,6 +955,12 @@ def _generated_wrapper_plan_artifacts( _print_verbose_timing(verbose, time.perf_counter() - policy_started) def render_progress(label: str, elapsed: float | None) -> None: + """Translate generator progress events into this build's verbose output. + + A missing duration starts a labelled step; a present duration completes + the previous step's timing. The callback only writes optional console + output and does not affect generation. + """ if elapsed is None: _print_verbose_step(verbose, label) return @@ -699,6 +969,9 @@ def render_progress(label: str, elapsed: float | None) -> None: return _render_wrapper_plan(module, progress=render_progress) +# Native source compilation scheduling + + def _source_compile_object( source_path: Path, output_dir: Path, @@ -707,6 +980,13 @@ def _source_compile_object( flags: Iterable[str] = (), include_dirs: Iterable[Path] = (), ) -> ObjectFile: + """Describe the object compilation for one caller-native source. + + Uses ``object_stem`` beneath ``output_dir`` to avoid collisions, preserves + the supplied flags, and appends the output directory to include paths so + later sources can locate generated Fortran module files. It returns only + an ``ObjectFile`` description and does not compile it. + """ target = output_dir / f"{object_stem}.o" return ObjectFile( source=source_path, @@ -718,10 +998,22 @@ def _source_compile_object( def _serial_compile_batches(object_files: Iterable[ObjectFile]) -> tuple[tuple[ObjectFile, ...], ...]: + """Place each object in its own ordered compilation batch. + + This conservative fallback consumes ``object_files`` and returns singleton + tuples, ensuring that a caller compiles sources serially when dependency + information is unavailable or cyclic. + """ return tuple((object_file,) for object_file in object_files) def _fortran_owner_used_modules(owner: object) -> set[str]: + """Return lowercased modules used directly or indirectly by one owner. + + ``owner`` may be a parsed module, program, procedure, or submodule. The + helper reads its ``uses`` mappings and the uses of contained procedures and + interface procedures, returning a new set without changing the parsed AST. + """ used = {str(name).lower() for name in getattr(owner, "uses", {})} for procedure in getattr(owner, "procedures", ()): used.update(str(name).lower() for name in getattr(procedure, "uses", {})) @@ -732,6 +1024,12 @@ def _fortran_owner_used_modules(owner: object) -> set[str]: def _fortran_file_used_modules(parsed_file: object) -> set[str]: + """Return lowercased module dependencies declared by one parsed file. + + Scans top-level parsed owners, interfaces, and submodule parent/ancestor + relationships. The returned names let the scheduler order object files; + the parsed file remains unmodified. + """ owners = ( *getattr(parsed_file, "modules", ()), *getattr(parsed_file, "submodules", ()), @@ -755,6 +1053,13 @@ def _dependency_compile_batches( object_files: tuple[ObjectFile, ...], dependencies: dict[Path, set[Path]], ) -> tuple[tuple[ObjectFile, ...], ...]: + """Topologically group object files into safe parallel compile batches. + + ``dependencies`` maps normalized source paths to provider source paths. + Every returned batch depends only on earlier batches. If no source is + ready, a cycle or incomplete graph is present, so the function returns the + serial fallback rather than guess an unsafe order. + """ object_by_source = {_path_key(object_file.source): object_file for object_file in object_files} remaining = list(object_by_source) completed: set[Path] = set() @@ -774,6 +1079,7 @@ def _project_compile_batches( object_files: tuple[ObjectFile, ...], ) -> tuple[tuple[ObjectFile, ...], ...]: """Group parsed project objects into dependency-ready compiler batches.""" + # Every compiled source must correspond to one parsed project file. parsed_files = tuple(getattr(parsed_project, "files", ())) parsed_by_source = { _path_key(Path(parsed_file.filename)): parsed_file @@ -784,6 +1090,7 @@ def _project_compile_batches( if set(parsed_by_source) != object_sources: return _serial_compile_batches(object_files) + # Map providers before resolving each file's module dependencies. module_sources: dict[str, Path] = {} for source, parsed_file in parsed_by_source.items(): for module in getattr(parsed_file, "modules", ()): @@ -801,7 +1108,17 @@ def _project_compile_batches( return _dependency_compile_batches(object_files, dependencies) +# Source and semantic-contract inputs + + def _source_paths(sources: str | Path | Iterable[str | Path]) -> tuple[Path, ...]: + """Validate and expand wrapper source inputs into a unique ordered tuple. + + A file must have a supported Fortran suffix; a directory is recursively + expanded in sorted order. The result preserves the caller's input order + while removing repeated paths. Missing files, empty directories, and + unsupported suffixes raise clear input errors before parsing begins. + """ inputs = (Path(sources),) if isinstance(sources, str | Path) else tuple(Path(source) for source in sources) if not inputs: raise ValueError("wrapper build requires at least one Fortran source file or directory") @@ -837,6 +1154,12 @@ def _wrapper_output_paths(output_dir: str | Path | None) -> tuple[Path, Path]: def _pyi_entry_path(contract: str | Path) -> Path: + """Validate and return the single semantic ``.pyi`` entry contract path. + + The public ``build_pyi_extension`` API accepts exactly one existing + ``.pyi`` file. This helper rejects collections, other suffixes, and + missing paths, then returns the unmodified ``Path`` for contract loading. + """ if not isinstance(contract, str | Path): raise TypeError(".pyi wrapper build accepts exactly one entry contract path") path = Path(contract) @@ -849,6 +1172,8 @@ def _pyi_entry_path(contract: str | Path) -> Path: @dataclass(frozen=True) class _PyiContractBundle: + """Keep one resolved ``.pyi`` import graph and its native contract leaves.""" + entry: Path leaves: tuple[Path, ...] paths: tuple[Path, ...] @@ -857,6 +1182,8 @@ class _PyiContractBundle: @dataclass(frozen=True) class _NativeBuildInputs: + """Hold validated native source, artifact, include, and link input groups.""" + source_paths: tuple[Path, ...] source_flags: tuple[str, ...] artifact_paths: tuple[Path, ...] @@ -868,9 +1195,20 @@ class _NativeBuildInputs: explicit_include_dirs: tuple[Path, ...] +# Semantic `.pyi` contract loading and export projection + + def _pyi_contract_bundle( entry: Path, ) -> _PyiContractBundle: + """Load one semantic contract graph and retain its native declaration leaves. + + Starting at ``entry``, this resolves relative imports through one cache, + validates package-placement rules, projects Python exports, and validates + native contracts. It returns the entry, all discovered paths, and only + modules with native declarations; no generated sources are written. + """ + # Load the complete relative-import graph through one semantic-module cache. module_cache = _PyiSemanticModuleCache() discovered = {entry, *_discover_pyi_imports(entry, module_cache)} sorted_paths = tuple(sorted(discovered)) @@ -878,6 +1216,8 @@ def _pyi_contract_bundle( modules_by_path = dict(zip(sorted_paths, loaded_modules, strict=True)) _validate_pyi_bundle_placement(entry, modules_by_path) _apply_pyi_python_exports(entry, modules_by_path) + + # Keep only contract leaves that declare a native API to wrap. leaves = [path for path in sorted_paths if _module_has_native_declarations(modules_by_path[path])] if not leaves: raise ValueError("Entry contract does not resolve any native declarations") @@ -898,12 +1238,12 @@ def _validate_pyi_bundle_placement(entry: Path, modules_by_path: dict[Path, Sema invalid = [ declaration.name for declaration in _module_declarations(entry_module) - if not _declaration_is_external(declaration) + if not _declaration_is_standalone(declaration) ] if invalid: raise ValueError( "Package entry contracts cannot contain native module declarations; " - "import module leaves or mark standalone procedures with @external. " + "import module leaves or mark standalone procedures with @standalone. " f"Invalid declaration: {invalid[0]}" ) @@ -911,25 +1251,37 @@ def _validate_pyi_bundle_placement(entry: Path, modules_by_path: dict[Path, Sema for path in namespace_imports: module = modules_by_path[path] invalid = [ - declaration.name for declaration in _module_declarations(module) if _declaration_is_external(declaration) + declaration.name for declaration in _module_declarations(module) if _declaration_is_standalone(declaration) ] if invalid: raise ValueError( - "A contract imported as a Python child namespace cannot contain @external declarations; " - "keep standalone procedures in the entry contract or import external fragments by name. " + "A contract imported as a Python child namespace cannot contain @standalone declarations; " + "keep standalone procedures in the entry contract or import standalone fragments by name. " f"Invalid declaration: {invalid[0]} in {path}" ) -def _declaration_is_external(declaration: object) -> bool: +def _declaration_is_standalone(declaration: object) -> bool: + """Return whether a declaration represents a standalone Fortran procedure. + + Individual semantic functions are standalone when they have no native scope; + overload sets are standalone only when every candidate is standalone. Other + declaration kinds return ``False`` and are not modified. + """ if isinstance(declaration, ProcedureOverloadSet): - return bool(declaration.procedures) and all(_declaration_is_external(item) for item in declaration.procedures) + return bool(declaration.procedures) and all(_declaration_is_standalone(item) for item in declaration.procedures) if isinstance(declaration, SemanticFunction): return declaration.origin.source_language == "fortran" and declaration.origin.native_scope is None return False def _namespace_imported_pyi_paths(entry: Path, modules_by_path: dict[Path, SemanticModule]) -> set[Path]: + """Find relative ``.pyi`` modules imported as child Python namespaces. + + Traverses the semantic import graph rooted at ``entry``. Named child + module imports are collected separately from direct declaration imports so + placement validation can enforce their different standalone-procedure rule. + """ namespace_imports: set[Path] = set() pending = [entry] seen: set[Path] = set() @@ -956,6 +1308,12 @@ def _namespace_imported_pyi_paths(entry: Path, modules_by_path: dict[Path, Seman def _discover_pyi_imports(root: Path, module_cache: _PyiSemanticModuleCache | None = None) -> tuple[Path, ...]: + """Resolve every relative semantic ``.pyi`` import reachable from ``root``. + + Reuses an optional semantic-module cache, follows only relative imports, + and returns sorted dependency paths excluding the root. A referenced but + missing contract raises ``FileNotFoundError`` instead of being skipped. + """ module_cache = module_cache or _PyiSemanticModuleCache() discovered: set[Path] = set() pending = [root] @@ -973,6 +1331,12 @@ def _discover_pyi_imports(root: Path, module_cache: _PyiSemanticModuleCache | No def _relative_pyi_dependencies(path: Path, module: SemanticModule) -> tuple[Path, ...]: + """Translate a module's relative imports into candidate contract paths. + + ``path`` anchors the package-relative calculation and ``module`` supplies + parsed import records. The returned paths may not exist yet; existence is + checked by the graph discovery caller. + """ dependencies: list[Path] = [] for semantic_import in module.imports: if not isinstance(semantic_import, SemanticImport) or not semantic_import.module.startswith("."): @@ -990,6 +1354,12 @@ def _relative_pyi_dependencies(path: Path, module: SemanticModule) -> tuple[Path def _pyi_dependency_path(parent: Path, dotted_name: str) -> Path: + """Choose the file or package-entry path for one relative import target. + + Forms the dotted target below ``parent`` and returns ``name.pyi`` unless + the target is an existing directory, in which case it returns its + ``__init__.pyi`` entry. It does not create either path. + """ target = parent.joinpath(*dotted_name.split(".")) module_file = target.with_suffix(".pyi") if module_file.is_file() or not target.is_dir(): @@ -998,17 +1368,27 @@ def _pyi_dependency_path(parent: Path, dotted_name: str) -> Path: def _module_has_native_declarations(module: SemanticModule) -> bool: + """Return whether a semantic module contributes any native wrapper surface.""" return bool(module.variables or module.functions or module.classes or module.overload_sets) @dataclass class _PyiExportNode: + """Represent declarations and nested Python exports at one namespace node.""" + declarations: list[object] = field(default_factory=list) children: dict[str, _PyiExportNode] = field(default_factory=dict) origins: set[Path] = field(default_factory=set) def _apply_pyi_python_exports(entry: Path, modules_by_path: dict[Path, SemanticModule]) -> None: + """Replace contract declaration export metadata with the resolved tree. + + Clears each loaded declaration's current Python exports, marks its module + as prepared, resolves exports rooted at ``entry``, and writes the resulting + namespace paths back into declaration metadata. The semantic modules are + deliberately mutated before wrapper policy completion. + """ for module in modules_by_path.values(): module.metadata[PYTHON_EXPORTS_PREPARED_METADATA] = True for declaration in _module_declarations(module): @@ -1025,6 +1405,12 @@ def _pyi_export_tree( cache: dict[Path, _PyiExportNode], pending: set[Path], ) -> _PyiExportNode: + """Build and cache the export tree rooted at one semantic contract file. + + The recursive graph combines public declarations, prototypes, and relative + imports. ``cache`` shares completed nodes, while ``pending`` detects an + import cycle and raises ``ValueError`` rather than recurse forever. + """ if path in cache: return cache[path] if path in pending: @@ -1066,6 +1452,13 @@ def _merge_relative_import( cache: dict[Path, _PyiExportNode], pending: set[Path], ) -> None: + """Merge one relative import's exports into the current namespace tree. + + Direct imports select declaration children from the dependency; child + namespace imports attach the entire dependency tree. Invalid names or + collisions are reported by the lookup and merge helpers; ``tree`` changes + in place. + """ imported_module = semantic_import.module.lstrip(".") if imported_module: dependency = _relative_import_path(path, semantic_import.module, imported_module) @@ -1087,6 +1480,7 @@ def _merge_relative_import( def _relative_import_path(path: Path, module: str, imported_module: str) -> Path: + """Resolve one dotted relative import from its importing contract path.""" level = len(module) - len(module.lstrip(".")) parent = path.parent for _ in range(level - 1): @@ -1100,12 +1494,19 @@ def _required_export_tree( cache: dict[Path, _PyiExportNode], pending: set[Path], ) -> _PyiExportNode: + """Return a dependency export tree or reject an absent imported contract.""" if path not in modules_by_path: raise FileNotFoundError(f"Imported semantic .pyi contract not found: {path}") return _pyi_export_tree(path, modules_by_path, cache=cache, pending=pending) def _merge_export_child(tree: _PyiExportNode, name: str, child: _PyiExportNode, *, origin: Path) -> None: + """Insert one named export into ``tree`` or reject a conflicting origin. + + Existing identical nodes are retained. Distinct nodes with the same name + cause a detailed ``ValueError`` naming both source origins; otherwise the + supplied child becomes part of the tree. + """ existing = tree.children.get(name) if existing is None or existing is child: tree.children[name] = child @@ -1119,6 +1520,12 @@ def _merge_export_child(tree: _PyiExportNode, name: str, child: _PyiExportNode, def _record_pyi_exports(tree: _PyiExportNode, namespace: tuple[str, ...] = ()) -> None: + """Write resolved namespace paths from an export tree into declarations. + + Walks ``tree`` recursively, skips prototypes, and appends de-duplicated + ``namespace``/``name`` records to each semantic declaration's metadata. + The declaration metadata is intentionally mutated for later planning. + """ for name, child in tree.children.items(): for declaration in child.declarations: if isinstance(declaration, SemanticPrototype): @@ -1131,10 +1538,17 @@ def _record_pyi_exports(tree: _PyiExportNode, namespace: tuple[str, ...] = ()) - def _module_declarations(module: SemanticModule) -> tuple[object, ...]: + """Return every declaration category that can receive export metadata.""" return (*module.variables, *module.functions, *module.overload_sets, *module.classes) def _declaration_metadata(declaration: object) -> dict[str, object]: + """Return the mutable metadata dictionary for one supported declaration. + + Overload sets use their first candidate's metadata because that is where + their shared export projection is stored. Unsupported objects raise + ``TypeError`` rather than silently lose metadata. + """ if isinstance(declaration, ProcedureOverloadSet): if not declaration.procedures: return {} @@ -1145,16 +1559,24 @@ def _declaration_metadata(declaration: object) -> dict[str, object]: def _declaration_exports(declaration: object) -> list[dict[str, object]]: + """Return and initialize the declaration's mutable Python export list.""" metadata = _declaration_metadata(declaration) return metadata.setdefault(PYTHON_EXPORTS_METADATA, []) def _set_declaration_exports(declaration: object, exports: list[dict[str, object]]) -> None: + """Replace one declaration's stored Python export projection in place.""" metadata = _declaration_metadata(declaration) metadata[PYTHON_EXPORTS_METADATA] = exports def _apply_source_python_exports(modules: list[SemanticModule]) -> None: + """Project direct Fortran source declarations to their Python namespaces. + + Marks every module as export-prepared and overwrites declaration metadata. + Public module members receive their module namespace; standalone public + procedures receive the root namespace; private declarations receive none. + """ for module in modules: module.metadata[PYTHON_EXPORTS_PREPARED_METADATA] = True namespace = (module.name.casefold(),) if module.origin.source_kind == "module" else () @@ -1169,12 +1591,21 @@ def _apply_source_python_exports(modules: list[SemanticModule]) -> None: ) +# Native build inputs and link planning + + def _existing_paths( paths: Iterable[str | Path] | None, *, kind: str, require_directory: bool = False, ) -> tuple[Path, ...]: + """Validate caller paths and return them as a tuple of ``Path`` values. + + Files are required by default; ``require_directory`` instead requires each + path to be a directory. Missing inputs raise a kind-specific + ``FileNotFoundError`` and no filesystem state is changed. + """ resolved = tuple(Path(path) for path in (paths or ())) for path in resolved: if require_directory: @@ -1186,6 +1617,7 @@ def _existing_paths( def _native_artifact_kind(path: Path) -> str: + """Classify a native artifact path for linker and manifest representation.""" name = path.name.lower() suffix = path.suffix.lower() if suffix in {".a", ".lib"}: @@ -1196,6 +1628,7 @@ def _native_artifact_kind(path: Path) -> str: def _unique_paths(paths: Iterable[Path]) -> tuple[Path, ...]: + """Return input paths once each, preserving their first-seen order.""" return tuple(dict.fromkeys(Path(path) for path in paths)) @@ -1212,6 +1645,13 @@ def _native_build_plan( include_dirs: tuple[Path, ...], module_dir: Path | None, ) -> NativeBuildPlan: + """Assemble the ordered native compile and link plan for one extension. + + Combines compiled source objects, prebuilt artifacts, named libraries, and + explicit or complete link items. The returned immutable plan preserves + link order and includes derived module/include directories; it does not + compile, link, or validate that prebuilt paths exist. + """ produced_objects = tuple(source_object.object_path for source_object in source_objects) source_link_items = tuple(NativeLinkItem("object", object_path) for object_path in produced_objects) prebuilt_artifacts = tuple( @@ -1252,6 +1692,12 @@ def _native_build_plan( def _native_link_args(link_items: Iterable[NativeLinkItem]) -> tuple[str, ...]: + """Convert ordered link records to the command-line arguments they require. + + Files become paths, bare named libraries acquire ``-l`` when needed, and + raw linker arguments pass through. The resulting tuple preserves input + order and is ready for the compiler linker invocation. + """ args = [] for item in link_items: if item.kind in _NATIVE_PATH_LINK_KINDS: @@ -1275,6 +1721,12 @@ def _rendered_wrapper_native_link_args(plan: NativeBuildPlan) -> tuple[str, ...] def _coerce_native_link_items(items: Iterable[NativeLinkItem | dict[str, object]] | None) -> tuple[NativeLinkItem, ...]: + """Normalize public native link-item records or dictionaries. + + Dictionary inputs must use the same ``kind`` and value-key conventions as + :meth:`NativeLinkItem.to_dict`. Returns immutable ``NativeLinkItem`` + records, or raises a precise type/value error before a build is started. + """ if items is None: return () result = [] @@ -1308,14 +1760,17 @@ def _coerce_native_link_items(items: Iterable[NativeLinkItem | dict[str, object] def _link_item_paths(link_items: Iterable[NativeLinkItem]) -> tuple[Path, ...]: + """Extract only filesystem-backed paths from ordered native link items.""" return tuple(Path(item.value) for item in link_items if item.kind in _NATIVE_PATH_LINK_KINDS) def _path_key(path: Path) -> Path: + """Return a non-strict resolved path suitable for equality and lookup.""" return path.resolve(strict=False) def _shared_library_dirs(link_items: Iterable[NativeLinkItem]) -> tuple[Path, ...]: + """Return parent directories of shared-library link items in input order.""" return tuple(Path(item.value).parent for item in link_items if item.kind == "shared_library") @@ -1330,6 +1785,14 @@ def _native_build_inputs( native_library_dirs: Iterable[str | Path] | None, native_include_dirs: Iterable[str | Path] | None, ) -> _NativeBuildInputs: + """Validate and normalize all caller-native inputs for a build request. + + Accepts optional sources, artifacts, libraries, ordered link records, and + search paths, derives shared-library search directories, and returns one + internal input record. It rejects missing files/directories and a request + with no native implementation input before any generated code is compiled. + """ + # Validate independent source, artifact, and explicit-link inputs first. source_paths = _existing_paths(native_fortran_sources, kind="Native Fortran source") source_flags = tuple(str(flag) for flag in (native_fortran_flags or ())) artifact_paths = _existing_paths(native_objects, kind="Native artifact") @@ -1340,6 +1803,8 @@ def _native_build_inputs( ) selected_link_items = explicit_link_items if complete_link_items is None else complete_link_items link_item_paths = _link_item_paths(selected_link_items) + + # Derive search paths after the final ordered link input is known. library_dirs = _unique_paths( ( *_existing_paths(native_library_dirs, kind="Native library", require_directory=True), @@ -1348,6 +1813,8 @@ def _native_build_inputs( ) ) explicit_include_dirs = _existing_paths(native_include_dirs, kind="Native include", require_directory=True) + + # A wrapper has no native implementation without at least one link input. if ( not source_paths and not artifact_paths @@ -1373,6 +1840,12 @@ def _native_build_inputs( def _native_include_dirs(inputs: _NativeBuildInputs, *, output_path: Path) -> tuple[Path, ...]: + """Derive de-duplicated include/module search paths for native compilation. + + Includes the build directory when source compilation produces module files, + caller include directories, and parents of linked artifacts. Returns the + paths without creating directories or changing ``inputs``. + """ module_include_dirs = (output_path,) if inputs.source_paths else () inferred_include_dirs = _unique_paths((*inputs.artifact_paths, *inputs.link_item_paths)) return _unique_paths( @@ -1384,12 +1857,38 @@ def _native_include_dirs(inputs: _NativeBuildInputs, *, output_path: Path) -> tu ) +def _source_object_stems(source_paths: tuple[Path, ...]) -> tuple[str, ...]: + """Return collision-free object stems for an ordered source-path sequence. + + Unique basenames retain their stem. Repeated stems gain a deterministic + one-based suffix in source order so distinct native files never target the + same object path. + """ + totals: dict[str, int] = {} + for source_path in source_paths: + totals[source_path.stem] = totals.get(source_path.stem, 0) + 1 + + seen: dict[str, int] = {} + stems = [] + for source_path in source_paths: + stem = source_path.stem + seen[stem] = seen.get(stem, 0) + 1 + stems.append(stem if totals[stem] == 1 else f"{stem}_{seen[stem]}") + return tuple(stems) + + def _native_source_objects( inputs: _NativeBuildInputs, *, output_path: Path, include_dirs: tuple[Path, ...], ) -> tuple[ObjectFile, ...]: + """Create uncompiled object descriptions for all validated native sources. + + Pairs each source with its collision-free stem and applies the normalized + source flags and include directories. Returns the planned objects in + source order without invoking the compiler. + """ return tuple( _source_compile_object( source_path, @@ -1403,13 +1902,56 @@ def _native_source_objects( def _validate_native_link_paths(plan: NativeBuildPlan) -> None: + """Reject missing filesystem link inputs that this build will not produce. + + Produced object paths are allowed before compilation. Every other path + referenced by an ordered link item must already be a file; the plan itself + is not modified. + """ produced_object_keys = {_path_key(path) for path in plan.produced_objects} for path in _link_item_paths(plan.link_items): if _path_key(path) not in produced_object_keys and not path.is_file(): raise FileNotFoundError(f"Native link item not found: {path}") +def _prepare_native_build_plan( + inputs: _NativeBuildInputs, + *, + output_path: Path, +) -> tuple[tuple[ObjectFile, ...], NativeBuildPlan]: + """Create and validate compiler objects and link inputs for one build.""" + include_dirs = _native_include_dirs(inputs, output_path=output_path) + source_objects = _native_source_objects( + inputs, + output_path=output_path, + include_dirs=include_dirs, + ) + plan = _native_build_plan( + source_paths=inputs.source_paths, + source_objects=source_objects, + artifact_paths=inputs.artifact_paths, + libraries=inputs.libraries, + explicit_link_items=inputs.explicit_link_items, + complete_link_items=inputs.complete_link_items, + library_dirs=inputs.library_dirs, + explicit_include_dirs=inputs.explicit_include_dirs, + include_dirs=include_dirs, + module_dir=output_path if source_objects else None, + ) + _validate_native_link_paths(plan) + return source_objects, plan + + +# Build manifest serialization + + def _manifest_path(path: str | Path, *, base: Path) -> str: + """Encode a path for a portable manifest relative to ``base`` when possible. + + Relative inputs are interpreted from the current working directory before + comparison. Paths on another filesystem fall back to their absolute text; + no files are read or written. + """ value = Path(path) absolute = value if value.is_absolute() else Path.cwd() / value try: @@ -1419,11 +1961,18 @@ def _manifest_path(path: str | Path, *, base: Path) -> str: def _resolve_manifest_path(path: str, *, base: Path) -> Path: + """Turn a manifest path string into an absolute or base-relative path.""" value = Path(path) return value if value.is_absolute() else base / value def _manifest_link_item(item: NativeLinkItem, *, base: Path) -> dict[str, object]: + """Serialize one native link record using manifest-relative file paths. + + File-backed items use a relative ``path`` where possible; named libraries + and raw arguments retain their string values. The input item is not + modified. + """ if item.kind in _NATIVE_PATH_LINK_KINDS: return { "kind": item.kind, @@ -1441,6 +1990,12 @@ def _manifest_link_item(item: NativeLinkItem, *, base: Path) -> dict[str, object def _manifest_native_plan(plan: NativeBuildPlan, *, base: Path) -> dict[str, object]: + """Serialize a complete native build plan for a replayable manifest. + + Converts every filesystem field in ``plan`` to a path relative to ``base`` + when possible and preserves ordered link items and compiler flags. The + returned dictionary is ready for JSON encoding. + """ return { "compilation_units": [ { @@ -1469,6 +2024,7 @@ def _manifest_native_plan(plan: NativeBuildPlan, *, base: Path) -> dict[str, obj def _manifest_native_array_requirements(requirements: NativeArrayBuildRequirements) -> dict[str, object]: + """Serialize native-array bridge requirements into plain manifest values.""" return { "pointer_c_descriptor_interop": requirements.pointer_c_descriptor_interop, "headers": list(requirements.headers), @@ -1503,6 +2059,13 @@ def _pyi_build_manifest( native_array_build_requirements: NativeArrayBuildRequirements, manifest_dir: Path, ) -> dict[str, object]: + """Build the complete in-memory manifest for a semantic ``.pyi`` build. + + Receives the resolved contract bundle, output and compiler choices, and + native plans/array requirements. It returns a schema-versioned plain + dictionary whose paths are relative to ``manifest_dir``; it neither writes + the manifest nor changes the build result. + """ return { "schema_version": _BUILD_MANIFEST_SCHEMA_VERSION, "build_kind": "pyi-wrapper", @@ -1532,6 +2095,12 @@ def _pyi_build_manifest( def _write_build_manifest(path: Path, manifest: dict[str, object]) -> Path: + """Write one deterministic, newline-terminated JSON build manifest. + + The parent directory must already exist. This creates or replaces + ``path`` with sorted, indented JSON and returns the same path for result + attachment. + """ path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8") return path @@ -1569,7 +2138,16 @@ def _with_pyi_manifest( return replace(result, manifest=manifest) +# Build manifest validation and replay inputs + + def _load_build_manifest(path: str | Path) -> tuple[Path, dict[str, object]]: + """Read and validate the top-level schema of a saved ``.pyi`` manifest. + + Returns the manifest path and its JSON object only when the file exists, + decodes to an object, and matches this module's schema version and build + kind. Invalid or incompatible files raise input errors before replay. + """ manifest_path = Path(path) if not manifest_path.is_file(): raise FileNotFoundError(f"Wrapper build manifest not found: {manifest_path}") @@ -1584,6 +2162,7 @@ def _load_build_manifest(path: str | Path) -> tuple[Path, dict[str, object]]: def _manifest_section(payload: dict[str, object], key: str) -> dict[str, object]: + """Return a required object section from a validated manifest payload.""" value = payload.get(key) if not isinstance(value, dict): raise ValueError(f"Wrapper build manifest missing object section: {key}") @@ -1591,6 +2170,7 @@ def _manifest_section(payload: dict[str, object], key: str) -> dict[str, object] def _manifest_string_list(section: dict[str, object], key: str) -> tuple[str, ...]: + """Return one optional manifest list field after enforcing string items.""" value = section.get(key, ()) if not isinstance(value, list) or any(not isinstance(item, str) for item in value): raise ValueError(f"Wrapper build manifest field {key!r} must be a list of strings") @@ -1598,6 +2178,7 @@ def _manifest_string_list(section: dict[str, object], key: str) -> tuple[str, .. def _manifest_bool(section: dict[str, object], key: str, *, default: bool = False) -> bool: + """Return one manifest boolean field or its explicit default after validation.""" value = section.get(key, default) if not isinstance(value, bool): raise ValueError(f"Wrapper build manifest field {key!r} must be a boolean") @@ -1605,6 +2186,7 @@ def _manifest_bool(section: dict[str, object], key: str, *, default: bool = Fals def _manifest_string(section: dict[str, object], key: str) -> str: + """Return a required non-empty manifest string field or raise ``ValueError``.""" value = section.get(key) if not isinstance(value, str) or not value: raise ValueError(f"Wrapper build manifest field {key!r} must be a non-empty string") @@ -1612,10 +2194,17 @@ def _manifest_string(section: dict[str, object], key: str) -> str: def _manifest_path_list(section: dict[str, object], key: str, *, base: Path) -> tuple[Path, ...]: + """Resolve an optional manifest string-list field into paths from ``base``.""" return tuple(_resolve_manifest_path(item, base=base) for item in _manifest_string_list(section, key)) def _native_link_item_from_manifest(item: object, *, base: Path) -> NativeLinkItem: + """Validate and reconstruct one ordered native link record from manifest JSON. + + File-backed item paths are resolved from ``base``; library names and raw + arguments remain strings. Unsupported shapes and kinds raise ``ValueError`` + instead of producing a partially replayable build. + """ if not isinstance(item, dict): raise ValueError("Wrapper build manifest link items must be objects") kind = item.get("kind") @@ -1640,6 +2229,7 @@ def _native_link_item_from_manifest(item: object, *, base: Path) -> NativeLinkIt def _manifest_link_items(section: dict[str, object], *, base: Path) -> tuple[NativeLinkItem, ...]: + """Reconstruct the ordered native link-item list stored in a manifest.""" value = section.get("link_items", ()) if not isinstance(value, list): raise ValueError("Wrapper build manifest field 'link_items' must be a list") @@ -1647,6 +2237,12 @@ def _manifest_link_items(section: dict[str, object], *, base: Path) -> tuple[Nat def _manifest_compilation_sources(section: dict[str, object], *, base: Path) -> tuple[Path, ...]: + """Return Fortran source paths recorded by a manifest's compilation units. + + Validates the unit-list shape and rejects source languages this replay path + cannot rebuild. Paths are resolved relative to ``base`` and returned in + recorded order without checking their current existence. + """ value = section.get("compilation_units", ()) if not isinstance(value, list): raise ValueError("Wrapper build manifest field 'compilation_units' must be a list") @@ -1660,21 +2256,16 @@ def _manifest_compilation_sources(section: dict[str, object], *, base: Path) -> return tuple(sources) -def _source_object_stems(source_paths: tuple[Path, ...]) -> tuple[str, ...]: - totals: dict[str, int] = {} - for source_path in source_paths: - totals[source_path.stem] = totals.get(source_path.stem, 0) + 1 - - seen: dict[str, int] = {} - stems = [] - for source_path in source_paths: - stem = source_path.stem - seen[stem] = seen.get(stem, 0) + 1 - stems.append(stem if totals[stem] == 1 else f"{stem}_{seen[stem]}") - return tuple(stems) +# Wrapper module assembly def _merge_wrapper_modules(modules: list[SemanticModule], *, name: str | None = None) -> SemanticModule: + """Flatten semantic source modules into the one extension-facing module. + + Concatenates every declaration category while preserving list order and + derives combined metadata and the origin from the first module. An empty + input cannot produce a wrapper and raises ``ValueError``. + """ if not modules: raise ValueError("wrapper build found no Fortran modules or standalone procedures") @@ -1691,6 +2282,12 @@ def _merge_wrapper_modules(modules: list[SemanticModule], *, name: str | None = def _wrapper_module_metadata(modules: list[SemanticModule]) -> dict[str, object]: + """Collect metadata needed by one merged wrapper module. + + Records native module scopes and propagates export/contract readiness flags + when any input module has them. It returns a fresh dictionary and does not + change the input semantic modules. + """ metadata: dict[str, object] = {"wrapper_native_modules": _wrapper_native_modules(modules)} if any(module.metadata.get(PYTHON_EXPORTS_PREPARED_METADATA) for module in modules): metadata[PYTHON_EXPORTS_PREPARED_METADATA] = True @@ -1701,6 +2298,7 @@ def _wrapper_module_metadata(modules: list[SemanticModule]) -> dict[str, object] def _wrapper_native_modules(modules: list[SemanticModule]) -> list[str]: + """Return unique native module names that require a generated native scope.""" return list( dict.fromkeys( str(module.origin.native_name or module.name) @@ -1711,13 +2309,22 @@ def _wrapper_native_modules(modules: list[SemanticModule]) -> list[str]: def _module_requires_native_scope(module: SemanticModule) -> bool: + """Return whether a module needs native-scope access in generated wrappers. + + Variables and classes always need a scope. Procedures need one only when + their native origin declares it; the module is inspected but not changed. + """ if module.variables or module.classes: return True functions = [*module.functions, *(procedure for item in module.overload_sets for procedure in item.procedures)] return any(function.origin.native_scope is not None for function in functions) +# Recorded compiler commands and Makefile output + + def _command_output(command: tuple[str, ...]) -> str | None: + """Return the argument following ``-o`` in one recorded compiler command.""" try: return command[command.index("-o") + 1] except (ValueError, IndexError): @@ -1725,6 +2332,7 @@ def _command_output(command: tuple[str, ...]) -> str | None: def _command_source(command: tuple[str, ...]) -> str | None: + """Return the first recognized native source argument in a compiler command.""" for part in command: if Path(part).suffix.lower() in _FORTRAN_SOURCE_SUFFIXES | _C_SOURCE_SUFFIXES: return part @@ -1732,6 +2340,7 @@ def _command_source(command: tuple[str, ...]) -> str | None: def _command_language(command: tuple[str, ...]) -> str | None: + """Infer ``fortran`` or ``c`` from a command's detected source suffix.""" source = _command_source(command) if source is None: return None @@ -1739,19 +2348,28 @@ def _command_language(command: tuple[str, ...]) -> str | None: def _absolute_command_path(path: str | Path, working_directory: Path) -> Path: + """Resolve a recorded command path against its original working directory.""" result = Path(path) return result if result.is_absolute() else working_directory / result def _make_target(path: Path) -> str: + """Escape a filesystem path for safe use as a GNU Make target or dependency.""" return str(path).replace("$", "$$").replace("#", r"\#").replace(" ", r"\ ") def _make_shell_literal(text: str) -> str: + """Escape dollar signs so Make passes a recorded literal through to the shell.""" return text.replace("$", "$$") def _make_recipe(command: tuple[str, ...], working_directory: Path) -> str: + """Convert one recorded compiler command into an overridable Make recipe. + + Selects the Fortran, C, or shared-linker variable from the command and + separates compiler-fixed arguments from caller-overridable flag variables. + The returned tab-prefixed recipe runs from ``working_directory``. + """ language = _command_language(command) if "-shared" in command: compiler_var, flags_var = "PRIK_LD", "PRIK_LDFLAGS" @@ -1768,6 +2386,11 @@ def _make_recipe(command: tuple[str, ...], working_directory: Path) -> str: def _compiler_executable(commands: tuple[tuple[str, ...], ...], *, language: str | None, shared: bool) -> str: + """Find the recorded compiler executable for one Makefile variable. + + Searches commands by source language or shared-link status and returns a + conservative GNU compiler default when no matching command was recorded. + """ for command in commands: if ("-shared" in command) == shared and (shared or _command_language(command) == language): return command[0] @@ -1783,6 +2406,7 @@ def _write_build_makefile( extra_dependencies: Iterable[Path] = (), ) -> Path: """Write a GNU Make build from recorded compiler commands.""" + # Separate recorded compile and link commands before constructing rules. compile_commands = tuple(command for command in commands if "-c" in command and _command_output(command)) link_command = next((command for command in reversed(commands) if "-shared" in command), None) if link_command is None: @@ -1795,6 +2419,8 @@ def _write_build_makefile( _absolute_command_path(_command_output(command), working_directory) for command in compile_commands ) makefile_path = path.resolve() + + # Preserve compiler selection while leaving caller-overridable flags empty. lines = [ "# Generated by prik. Edit variables or override them on the make command line.", "# User Fortran sources are conservatively chained in supplied order.", @@ -1811,6 +2437,7 @@ def _write_build_makefile( link_output = _absolute_command_path(_command_output(link_command), working_directory) lines.extend([".PHONY: all rebuild clean", f"all: {_make_target(link_output)}", ""]) + # User sources remain ordered; generated objects depend on all native objects. previous_user_output = None for command, output in zip(compile_commands, compile_outputs, strict=True): source = _absolute_command_path(_command_source(command), working_directory) @@ -1832,6 +2459,8 @@ def _write_build_makefile( all_link_dependencies = tuple(dict.fromkeys((*compile_outputs, *extra_dependencies))) object_dependencies = " ".join(_make_target(output) for output in all_link_dependencies) + + # Link, rebuild, and cleanup rules share the recorded artifact paths. lines.extend( [ f"{_make_target(link_output)}: {object_dependencies}", @@ -1850,7 +2479,11 @@ def _write_build_makefile( return path +# Fortran type probing + + def _can_probe_fortran_types(preprocessing: PreprocessingConfig) -> bool: + """Return whether the preprocessing configuration can invoke a compiler probe.""" return preprocessing.uses_compiler and bool(preprocessing.compiler) @@ -1877,6 +2510,12 @@ def _wrap_compile_time_values( cache_dir: str | Path | None = None, refresh: bool = False, ) -> dict[str, int] | None: + """Measure only the compile-time values required by a parsed source project. + + Returns ``None`` when no report/probe is possible or no values are needed. + Otherwise it delegates the parsed requirements and optional probe controls + to the type evaluator, which may read or refresh its cache. + """ if report is None and not _can_probe_fortran_types(preprocessing): return None requirements = collect_semantic_compile_time_requirements(parsed) @@ -1902,6 +2541,12 @@ def _wrap_type_facts( cache_dir: str | Path | None = None, refresh: bool = False, ) -> dict[tuple[str, str | None], dict[str, object]] | None: + """Measure native type-storage facts required by a parsed source project. + + Uses prior ``compile_time_values`` to derive requirements. Returns + ``None`` when probing is unavailable or unnecessary; otherwise delegates to + the type-fact evaluator, which may execute or reuse a compiler probe. + """ if report is None and not _can_probe_fortran_types(preprocessing): return None requirements = collect_fortran_type_storage_requirements(parsed, compile_time_values=compile_time_values) @@ -1917,6 +2562,107 @@ def _wrap_type_facts( ) +def _bundle_output_name(bundle: _PyiContractBundle) -> str: + """Derive a default extension name from a file or package-entry contract. + + Package ``__init__.pyi`` entries use their parent directory name; ordinary + contract files use their stem. The bundle is read only and name validation + happens separately. + """ + if bundle.entry.name == "__init__.pyi": + return bundle.entry.resolve().parent.name + return bundle.entry.stem + + +# Source-to-semantic preparation + + +def _fortran_wrapper_module( + source_paths: tuple[Path, ...], + *, + preprocessing: PreprocessingConfig, + type_probe_preprocessing: PreprocessingConfig, + output_name: str | None, + fortran_type_report, + fortran_type_probe_runner: list[str] | None, + fortran_type_probe_cache_dir: str | Path | None, + refresh_fortran_type_probe: bool, +) -> tuple[object, SemanticModule]: + """Parse Fortran sources, resolve type facts, and form one wrapper module.""" + # Preprocess and parse the complete source project. + preprocessed_sources = { + str(source_path): _fortran_source_for_pipeline(source_path, preprocessing) for source_path in source_paths + } + parsed = parse_fortran_project(preprocessed_sources) + + # Measure compiler-dependent values before building semantic IR. + compile_time_values = _wrap_compile_time_values( + parsed, + type_probe_preprocessing, + report=fortran_type_report, + runner=fortran_type_probe_runner, + cache_dir=fortran_type_probe_cache_dir, + refresh=refresh_fortran_type_probe, + ) + type_facts = _wrap_type_facts( + parsed, + type_probe_preprocessing, + compile_time_values=compile_time_values, + report=fortran_type_report, + runner=fortran_type_probe_runner, + cache_dir=fortran_type_probe_cache_dir, + refresh=refresh_fortran_type_probe, + ) + + # Preserve source export paths while flattening the wrapper-facing module. + modules = fortran_project_to_semantic_modules( + parsed, + compile_time_values=compile_time_values, + type_facts=type_facts, + ) + _apply_source_python_exports(modules) + module_name = _validated_wrapper_module_name(output_name, source_paths[0].stem) + return parsed, _merge_wrapper_modules(modules, name=module_name) + + +def _complete_pyi_fortran_boolean_types( + modules: list[SemanticModule], + *, + compiler: str, + compiler_args: Iterable[str], +) -> None: + """Attach exact compiler logical spellings to Boolean contract types. + + The helper consumes loaded semantic modules plus the selected Fortran + compiler target. It probes only when a Boolean type occurs, then mutates + each such type's source origin before policy completion. All Boolean names + retain their one-byte NumPy dtype; the attached spelling is solely the + native bridge representation and ambiguous widths fail through the probe. + """ + boolean_types = [ + semantic_type + for module in modules + for semantic_type in _iter_module_semantic_types(module) + if is_boolean_semantic_type_name(semantic_type.name) + ] + if not boolean_types: + return + native_types = resolve_fortran_logical_storage_types( + PreprocessingConfig( + mode="compiler", + compiler=compiler, + compiler_args=list(compiler_args), + ), + (boolean_storage_bits(semantic_type.name) for semantic_type in boolean_types), + ) + for semantic_type in boolean_types: + semantic_type.origin.source_language = "fortran" + semantic_type.origin.source_type = native_types[boolean_storage_bits(semantic_type.name)] + + +# Public build entry points + + def build_fortran_extension( sources: str | Path | Iterable[str | Path], *, @@ -1945,19 +2691,88 @@ def build_fortran_extension( wrapper_c_flags: Iterable[str] | None = None, _on_total_build_time: Callable[[float], None] | None = None, ) -> WrapperBuildResult: - """Build one extension, or generate its sources or Makefile, from ordered sources.""" + """Build a Python extension from one or more Fortran source files. - if makefile and generate_sources: - raise ValueError("source-only and Makefile generation are mutually exclusive") - generation_only = makefile or generate_sources - compile_jobs = _normalize_compile_jobs(jobs) - if generation_only and verbose: - raise ValueError("source/Makefile generation and verbose direct compilation are separate modes") + This is the source-first public build API. Supply a file, an ordered + iterable of files, or a directory of Fortran sources, along with the native + implementation that exports the wrapped procedures. On success, import + the built extension from ``result.shared_library.parent`` or inspect the + generated sources and native link plan in the returned result. + + For the usual direct build, use the same sources for parsing and native + compilation:: + + result = build_fortran_extension( + "solver.f90", output_dir="build/solver", output_name="solver" + ) + # result.shared_library is the importable extension artifact. + + Parameters + ---------- + sources + One supported Fortran source path, an ordered iterable of paths, or a + directory to discover recursively. Source order is preserved and is + used for compilation fallback ordering. + output_dir, output_name + Build directory and optional importable Python module name. Omit + ``output_dir`` to use ``__prik__`` in the current directory; omit + ``output_name`` to derive it from the first source. + preprocessing + Optional source preprocessing configuration. The default uses + compiler-backed ``gfortran`` preprocessing. + strict_wrapper_names + Reject generated Python names that cannot be represented without a + strict naming decision. + fortran_type_report, fortran_type_probe_runner, + fortran_type_probe_cache_dir, refresh_fortran_type_probe + Optional controls for compiler-probed Fortran type facts used while + constructing semantic IR. + compile_input_sources + Compile ``sources`` as native implementation inputs. Set false only + when their implementation is supplied separately as objects, libraries, + link items, or ``native_fortran_sources``. + native_fortran_sources, native_fortran_flags + Additional implementation sources and their compiler flags. + native_objects, native_libraries, native_link_items, + native_library_dirs, native_include_dirs + Existing artifacts, ``-l`` names, ordered linker records, and search + paths for the native implementation. Use ``native_link_items`` when + linker order is significant. + makefile, generate_sources + Choose a non-executing output mode. ``makefile=True`` writes a + replayable ``Makefile.prik``; ``generate_sources=True`` writes wrapper + artifacts only. They cannot be combined with each other or ``verbose``. + jobs + Positive maximum number of simultaneous compiler processes. ``None`` + uses the available processor count. + verbose, wrapper_compiler_debug, wrapper_fortran_flags, wrapper_c_flags + Build progress output, generated-wrapper debug mode, and additional + flags for generated bridge and binding compilation. + + Returns + ------- + WrapperBuildResult + Paths, generated files, compilation mode, and the complete native build + plan. ``compiled`` is false in source-only and Makefile modes. + + Raises + ------ + ValueError, FileNotFoundError + If inputs, module names, modes, native link items, or source paths are + invalid. + """ + + generation_only, compile_jobs = _resolve_build_mode( + makefile=makefile, + generate_sources=generate_sources, + jobs=jobs, + verbose=verbose, + ) build_started = time.perf_counter() - source_paths = _source_paths(sources) - primary_source = source_paths[0] + # 1. Collect the source and native implementation inputs. + source_paths = _source_paths(sources) output_path, shared_library_output_path = _wrapper_output_paths(output_dir) output_path.mkdir(parents=True, exist_ok=True) preprocessing = preprocessing or _default_preprocessing_config() @@ -1976,43 +2791,26 @@ def build_fortran_extension( ) type_probe_preprocessing = _type_probe_preprocessing(preprocessing, native_inputs.source_flags) - preprocessed_sources = { - str(source_path): _fortran_source_for_pipeline(source_path, preprocessing) for source_path in source_paths - } - parsed = parse_fortran_project(preprocessed_sources) - compile_time_values = _wrap_compile_time_values( - parsed, - type_probe_preprocessing, - report=fortran_type_report, - runner=fortran_type_probe_runner, - cache_dir=fortran_type_probe_cache_dir, - refresh=refresh_fortran_type_probe, + # 2. Parse source, resolve target facts, and assemble semantic IR. + parsed, module = _fortran_wrapper_module( + source_paths, + preprocessing=preprocessing, + type_probe_preprocessing=type_probe_preprocessing, + output_name=output_name, + fortran_type_report=fortran_type_report, + fortran_type_probe_runner=fortran_type_probe_runner, + fortran_type_probe_cache_dir=fortran_type_probe_cache_dir, + refresh_fortran_type_probe=refresh_fortran_type_probe, ) - type_facts = _wrap_type_facts( - parsed, - type_probe_preprocessing, - compile_time_values=compile_time_values, - report=fortran_type_report, - runner=fortran_type_probe_runner, - cache_dir=fortran_type_probe_cache_dir, - refresh=refresh_fortran_type_probe, - ) - modules = fortran_project_to_semantic_modules( - parsed, - compile_time_values=compile_time_values, - type_facts=type_facts, - ) - _apply_source_python_exports(modules) - requested_name = output_name or primary_source.stem - if not requested_name.isidentifier(): - raise ValueError(f"Output name must be a valid Python identifier: {requested_name!r}") - module = _merge_wrapper_modules(modules, name=requested_name) + + # 3. Complete wrapper policy and render the canonical artifacts. rendered_wrapper_plan = _generated_wrapper_plan_artifacts( module, strict_wrapper_names=strict_wrapper_names, verbose=verbose, ) + # 4. Prepare native compilation, dependency batches, and link inputs. wrapper_fortran_flags = _compiler_flags(wrapper_fortran_flags) wrapper_c_flags = _compiler_flags(wrapper_c_flags) compiler = _new_compiler( @@ -2020,27 +2818,10 @@ def build_fortran_extension( debug=wrapper_compiler_debug, input_compiler=preprocessing.compiler if preprocessing.uses_compiler else None, ) - include_dirs = _native_include_dirs(native_inputs, output_path=output_path) - native_source_objects = _native_source_objects( - native_inputs, - output_path=output_path, - include_dirs=include_dirs, - ) - native_build_plan = _native_build_plan( - source_paths=native_inputs.source_paths, - source_objects=native_source_objects, - artifact_paths=native_inputs.artifact_paths, - libraries=native_inputs.libraries, - explicit_link_items=native_inputs.explicit_link_items, - complete_link_items=None, - library_dirs=native_inputs.library_dirs, - explicit_include_dirs=native_inputs.explicit_include_dirs, - include_dirs=include_dirs, - module_dir=output_path if native_source_objects else None, - ) - _validate_native_link_paths(native_build_plan) + native_source_objects, native_build_plan = _prepare_native_build_plan(native_inputs, output_path=output_path) native_compile_batches = _project_compile_batches(parsed, native_source_objects) + # 5. Build the extension, or retain the generated source/Makefile plan. result = _build_rendered_wrapper_extension( rendered_wrapper_plan, output_dir=output_path, @@ -2056,15 +2837,14 @@ def build_fortran_extension( compile_jobs=1 if generation_only else compile_jobs, verbose=verbose, ) - if makefile: - result = _attach_build_makefile( - result, - compiler=compiler, - source_objects=native_source_objects, - extra_dependencies=_link_item_paths(native_build_plan.link_items), - ) - elif generate_sources: - result = replace(result, compiled=False) + result = _finalize_build_mode( + result, + makefile=makefile, + generate_sources=generate_sources, + compiler=compiler, + source_objects=native_source_objects, + extra_dependencies=_link_item_paths(native_build_plan.link_items), + ) _report_total_build_time( verbose, time.perf_counter() - build_started, @@ -2097,16 +2877,74 @@ def build_pyi_extension( wrapper_c_flags: Iterable[str] | None = None, _on_total_build_time: Callable[[float], None] | None = None, ) -> WrapperBuildResult: - """Build one extension, or generate its sources or Makefile, from one `.pyi` entry.""" + """Build a Python extension from an editable semantic ``.pyi`` contract. - if makefile and generate_sources: - raise ValueError("source-only and Makefile generation are mutually exclusive") - generation_only = makefile or generate_sources - compile_jobs = _normalize_compile_jobs(jobs) - if generation_only and verbose: - raise ValueError("source/Makefile generation and verbose direct compilation are separate modes") + Use this API when the public Python surface is defined by a semantic + contract and native code already exists separately. The entry contract may + import relative ``.pyi`` modules; all reachable native declarations are + validated, rendered, and linked into one extension. + + For example, compile an existing native implementation and pass its object + file to the contract build:: + + result = build_pyi_extension( + "api.pyi", native_objects=["build/api.o"], output_dir="build/api" + ) + + Parameters + ---------- + contract + Existing semantic ``.pyi`` entry file. Its relative-import graph is + loaded as one contract bundle. + input_compiler + Fortran compiler executable used for generated bridge code and optional + native source compilation. + native_fortran_sources, native_fortran_flags + Existing implementation source paths to compile and their flags. + native_objects, native_libraries, native_link_items, + native_library_dirs, native_include_dirs + Existing artifacts, ``-l`` names, ordered linker records, and search + paths. Use ``native_link_items`` to append ordered inputs, or + ``complete_native_link_items`` to supply the full ordered link plan. + output_name, output_dir + Optional Python extension name and build directory. The default name + comes from the contract file or package entry. + strict_wrapper_names + Enforce strict generated Python-name validation during policy + completion. + makefile, generate_sources + Select non-executing output: a replayable ``Makefile.prik`` or generated + sources only. These modes are mutually exclusive and cannot be verbose. + jobs + Positive compiler-process limit; omit to use available processors. + verbose, wrapper_compiler_debug, wrapper_fortran_flags, wrapper_c_flags + Progress, generated-wrapper debug mode, and generated bridge/binding + compiler flags. + + Returns + ------- + WrapperBuildResult + Generated artifact paths, a native build plan, and an in-memory build + manifest. Makefile mode also persists that manifest and records its + path in ``build_manifest``. + + Raises + ------ + ValueError, FileNotFoundError + If the contract graph, native inputs, requested mode, or link plan is + invalid. + """ + + generation_only, compile_jobs = _resolve_build_mode( + makefile=makefile, + generate_sources=generate_sources, + jobs=jobs, + verbose=verbose, + ) build_started = time.perf_counter() + + # 1. Load the contract graph and collect native implementation inputs. entry = _pyi_entry_path(contract) bundle = _pyi_contract_bundle(entry) native_inputs = _native_build_inputs( @@ -2125,42 +2963,31 @@ def build_pyi_extension( wrapper_fortran_flags = _compiler_flags(wrapper_fortran_flags) wrapper_c_flags = _compiler_flags(wrapper_c_flags) + # 2. Assemble semantic IR, complete policy, and render wrapper artifacts. modules = list(bundle.modules) - requested_name = output_name or _bundle_output_name(bundle) - if not requested_name.isidentifier(): - raise ValueError(f"Output name must be a valid Python identifier: {requested_name!r}") - module = _merge_wrapper_modules(modules, name=requested_name) + _complete_pyi_fortran_boolean_types( + modules, + compiler=input_compiler, + compiler_args=(*native_inputs.source_flags, *wrapper_fortran_flags), + ) + module_name = _validated_wrapper_module_name(output_name, _bundle_output_name(bundle)) + module = _merge_wrapper_modules(modules, name=module_name) rendered_wrapper_plan = _generated_wrapper_plan_artifacts( module, strict_wrapper_names=strict_wrapper_names, verbose=verbose, ) - include_dirs = _native_include_dirs(native_inputs, output_path=output_path) - native_source_objects = _native_source_objects( - native_inputs, - output_path=output_path, - include_dirs=include_dirs, - ) - native_build_plan = _native_build_plan( - source_paths=native_inputs.source_paths, - source_objects=native_source_objects, - artifact_paths=native_inputs.artifact_paths, - libraries=native_inputs.libraries, - explicit_link_items=native_inputs.explicit_link_items, - complete_link_items=native_inputs.complete_link_items, - library_dirs=native_inputs.library_dirs, - explicit_include_dirs=native_inputs.explicit_include_dirs, - include_dirs=include_dirs, - module_dir=output_path if native_source_objects else None, - ) - _validate_native_link_paths(native_build_plan) + # 3. Prepare native compilation and link inputs before selecting the compiler. + native_source_objects, native_build_plan = _prepare_native_build_plan(native_inputs, output_path=output_path) compiler = _new_compiler( execute_commands=not generation_only, debug=wrapper_compiler_debug, input_compiler=input_compiler, ) native_array_build_requirements = native_array_handle_build_requirements(module) + + # 4. Build the extension and attach its replayable manifest data. result = _build_rendered_wrapper_extension( rendered_wrapper_plan, output_dir=output_path, @@ -2188,22 +3015,26 @@ def build_pyi_extension( wrapper_c_flags=wrapper_c_flags, native_array_build_requirements=native_array_build_requirements, ) + + # 5. Optionally persist the manifest and Makefile instead of a direct build. + build_manifest = None + makefile_dependencies: tuple[Path, ...] = () if makefile: build_manifest = _write_build_manifest(output_path / _BUILD_MANIFEST_NAME, result.manifest) - dependencies = ( + makefile_dependencies = ( *bundle.paths, *_link_item_paths(native_build_plan.link_items), build_manifest, ) - result = _attach_build_makefile( - result, - compiler=compiler, - source_objects=native_source_objects, - extra_dependencies=dependencies, - build_manifest=build_manifest, - ) - elif generate_sources: - result = replace(result, compiled=False) + result = _finalize_build_mode( + result, + makefile=makefile, + generate_sources=generate_sources, + compiler=compiler, + source_objects=native_source_objects, + extra_dependencies=makefile_dependencies, + build_manifest=build_manifest, + ) _report_total_build_time( verbose, time.perf_counter() - build_started, @@ -2224,9 +3055,40 @@ def build_pyi_extension_from_manifest( verbose: bool | int = False, _on_total_build_time: Callable[[float], None] | None = None, ) -> WrapperBuildResult: - """Replay a saved semantic `.pyi` wrapper build manifest.""" + """Replay a saved semantic ``.pyi`` wrapper build manifest. + + First create a manifest with ``build_pyi_extension(..., makefile=True)``. + This entrypoint restores its contract, compiler choices, native compilation + sources, and ordered link plan, then delegates to ``build_pyi_extension``. + The current contract import graph must still match the recorded graph. + + Parameters + ---------- + manifest + Existing ``prik-build.json`` produced by a semantic ``.pyi`` build. + output_name, input_compiler, include_dirs + Optional replay overrides for the extension name, compiler executable, + and additional native include directories. All other build choices are + restored from the manifest. + makefile, generate_sources, jobs, verbose + Output mode and compilation controls with the same meanings as + :func:`build_pyi_extension`. + + Returns + ------- + WrapperBuildResult + The direct-build, source-only, or Makefile result produced by replay. + + Raises + ------ + FileNotFoundError, ValueError + If the manifest is absent or incompatible, recorded inputs are invalid, + or the present contract graph no longer matches the saved build. + """ build_started = time.perf_counter() + + # 1. Load the recorded build inputs and validate required sections. manifest_path, payload = _load_build_manifest(manifest) base = manifest_path.parent native_section = _manifest_section(payload, "native_build_plan") @@ -2248,6 +3110,7 @@ def build_pyi_extension_from_manifest( if requested_name is not None and not isinstance(requested_name, str): raise ValueError("Wrapper build manifest extension.requested_name must be a string or null") + # 2. Restore native include paths and compiler selection from the manifest. manifest_module_dirs = _manifest_path_list(native_section, "module_dirs", base=base) native_include_dirs = _unique_paths( ( @@ -2258,6 +3121,8 @@ def build_pyi_extension_from_manifest( selected_input_compiler = input_compiler if selected_input_compiler is None: selected_input_compiler = _manifest_string(compiler_section, "input_executable") + + # 3. Delegate execution to the regular `.pyi` build path. result = build_pyi_extension( _resolve_manifest_path(entry_contract, base=base), input_compiler=selected_input_compiler, @@ -2278,6 +3143,8 @@ def build_pyi_extension_from_manifest( complete_native_link_items=_manifest_link_items(native_section, base=base), _on_total_build_time=lambda _elapsed: None, ) + + # 4. Ensure the current contract graph still matches the recorded build. recorded_contracts = tuple( _resolve_manifest_path(path, base=base) for path in _manifest_string_list(payload, "contract_paths") ) @@ -2291,7 +3158,30 @@ def build_pyi_extension_from_manifest( return result -def _bundle_output_name(bundle: _PyiContractBundle) -> str: - if bundle.entry.name == "__init__.pyi": - return bundle.entry.resolve().parent.name - return bundle.entry.stem +# Direct-execution example + + +if __name__ == "__main__": + from tempfile import TemporaryDirectory + + import numpy as np + + source_text = """\ +real(8) function scale(value, factor) result(output) + real(8), intent(in) :: value + real(8), intent(in) :: factor + output = value * factor +end function scale +""" + with TemporaryDirectory() as temporary_dir: + temporary_path = Path(temporary_dir) + source_path = temporary_path / "scale.f90" + source_path.write_text(source_text, encoding="utf-8") + build = build_fortran_extension( + source_path, + output_dir=temporary_path / "build", + output_name="build_example", + ) + module = build.import_module() + value = module.scale(np.float64(3.0), np.float64(2.5)) + print(f"scale(3.0, 2.5) = {value}") diff --git a/prik/pipeline/preprocessing.py b/prik/pipeline/preprocessing.py index e23211880..06daf3f20 100644 --- a/prik/pipeline/preprocessing.py +++ b/prik/pipeline/preprocessing.py @@ -1,8 +1,11 @@ -"""Compiler-backed preprocessing support for prik wrapper pipelines. - -The parser frontends intentionally parse one source stream. This module owns the -compiler/preprocessor invocation, side-channel metadata, source provenance, and -the native Fortran INCLUDE expansion that GNU Fortran CPP leaves unresolved. +"""Prepare C and Fortran source for the parser frontends. + +The parsers intentionally consume one expanded source stream. This module +therefore owns compiler/preprocessor invocation, provenance and dependency +metadata, and the textual expansion of native Fortran ``INCLUDE`` statements +that compiler CPP leaves unresolved. It does not parse declarations or make +semantic policy decisions; callers pass :class:`PreprocessResult.source` to the +appropriate parser after this stage completes. """ from __future__ import annotations @@ -36,8 +39,27 @@ Exposure = Literal["public", "private"] +# Compiler output syntax and supported source forms. +_VALID_LANGUAGES = {"c", "fortran"} +_C_SOURCE_SUFFIXES = {".c", ".h", ".i"} +_FORTRAN_SOURCE_SUFFIXES = {".f", ".for", ".ftn", ".f77", ".f90", ".f95", ".f03", ".f08"} +_DEFINE_RE = re.compile(r"^\s*#\s*define\s+([A-Za-z_]\w*)(\(([^)]*)\))?(?:\s+(.*))?$") +_LINEMARKER_RE = re.compile( + r'^\s*#\s+(?P\d+)\s+(?:"(?P(?:[^"\\]|\\.)*)"|(?P\S+))(?P(?:\s+\d+)*)\s*$' +) +_LINE_DIRECTIVE_RE = re.compile( + r'^\s*#\s*line\s+(?P\d+)(?:\s+(?:"(?P(?:[^"\\]|\\.)*)"|(?P\S+)))?\s*$' +) +_FORTRAN_INCLUDE_RE = re.compile(r"^\s*include\s*(?P['\"])(?P[^'\"]+)(?P=quote)\s*$", re.IGNORECASE) + + class PreprocessingError(Exception): - """Raised when preprocessing configuration or execution fails.""" + """Report a preprocessing configuration, compiler, or include failure. + + Callers normally surface ``category`` and ``diagnostics`` through the CLI + or parser payload. The exception message remains the concise + user-facing summary. + """ def __init__( self, @@ -46,6 +68,13 @@ def __init__( category: PreprocessingCategory = "PREPROCESSOR_FAILED", diagnostics: Sequence[PreprocessingDiagnostic] | None = None, ) -> None: + """Initialize a failure with its stable category and diagnostic details. + + Args: + message: Concise explanation presented to callers. + category: Stable machine-readable failure classification. + diagnostics: Optional detailed diagnostics to retain with the error. + """ self.category = category self.diagnostics = list(diagnostics or []) super().__init__(message) @@ -53,7 +82,12 @@ def __init__( @dataclass class Invocation: - """Concrete command line used to obtain preprocessed source.""" + """Describe one concrete compiler command used to expand source. + + Invocation builders return this record when a caller needs to inspect the + exact argv and working directory before execution. ``preprocess_source`` + consumes it internally and records the same facts in its result recipe. + """ argv: list[str] cwd: str | None = None @@ -67,6 +101,12 @@ class Invocation: @dataclass class PreprocessingDiagnostic: + """Store one preprocessing diagnostic with optional source provenance. + + Results and errors retain these records so CLI and API callers can report + stable categories without reparsing compiler stderr. + """ + category: PreprocessingCategory message: str severity: Literal["error", "warning", "note"] = "error" @@ -75,6 +115,7 @@ class PreprocessingDiagnostic: command: list[str] = field(default_factory=list) def to_dict(self) -> dict[str, object]: + """Return a detached JSON-compatible representation of this diagnostic.""" return { "category": self.category, "message": self.message, @@ -87,6 +128,12 @@ def to_dict(self) -> dict[str, object]: @dataclass class PreprocessingPlan: + """Represent the requested preprocessing inputs before command selection. + + This JSON-compatible value is useful to callers that need to display or + persist a requested operation rather than execute it immediately. + """ + language: str source_path: str adapter: str @@ -101,6 +148,7 @@ class PreprocessingPlan: command_template: str | None = None def to_dict(self) -> dict[str, object]: + """Return a detached JSON-compatible representation of the requested plan.""" return { "language": self.language, "source_path": self.source_path, @@ -119,6 +167,13 @@ def to_dict(self) -> dict[str, object]: @dataclass class IncludedFile: + """Describe one root or include edge discovered during preprocessing. + + ``mechanism`` identifies whether compiler markers or native Fortran + expansion found the edge. Downstream parsers consume these records as + dependency and public-exposure facts. + """ + path: str included_by: str | None = None include_line: int | None = None @@ -127,6 +182,7 @@ class IncludedFile: exposure: Exposure = "public" def to_dict(self) -> dict[str, object]: + """Return a detached JSON-compatible representation of this include edge.""" return { "path": self.path, "included_by": self.included_by, @@ -139,12 +195,19 @@ def to_dict(self) -> dict[str, object]: @dataclass class SourceMapping: + """Map one generated source line back to its original source location. + + ``include_stack`` preserves the active inclusion chain for provenance-aware + parsers and diagnostics. + """ + generated_line: int original_path: str original_line: int include_stack: list[str] = field(default_factory=list) def to_dict(self) -> dict[str, object]: + """Return a detached JSON-compatible representation of this mapping.""" return { "generated_line": self.generated_line, "original_path": self.original_path, @@ -155,6 +218,13 @@ def to_dict(self) -> dict[str, object]: @dataclass class MacroDefinition: + """Record an active macro when compiler output exposes its definition. + + Macro metadata is descriptive rather than executable: semantic conversion + may consume supported object-like values, while callers retain function-like + definitions as provenance only. + """ + name: str value: str | None = None function_like: bool = False @@ -164,6 +234,7 @@ class MacroDefinition: builtin: bool = False def to_dict(self) -> dict[str, object]: + """Return a detached JSON-compatible representation of this macro.""" return { "name": self.name, "value": self.value, @@ -177,6 +248,13 @@ def to_dict(self) -> dict[str, object]: @dataclass class PreprocessResult: + """Return expanded source together with all preprocessing side-channel data. + + Pass ``source`` to a C or Fortran parser. ``recipe`` and the metadata + collections are normally attached to the parser or build report so later + stages can preserve compiler provenance. + """ + source: str recipe: dict[str, object] included_files: list[IncludedFile] = field(default_factory=list) @@ -185,6 +263,7 @@ class PreprocessResult: diagnostics: list[PreprocessingDiagnostic] = field(default_factory=list) def to_dict(self) -> dict[str, object]: + """Return a detached JSON-compatible representation of this result.""" return { "source": self.source, "recipe": dict(self.recipe), @@ -197,7 +276,12 @@ def to_dict(self) -> dict[str, object]: @dataclass class PreprocessingRecipe: - """JSON-compatible metadata about one preprocessing operation.""" + """Store JSON-compatible provenance for one completed preprocessing operation. + + Use this record when a caller wants expanded source and a typed recipe from + :func:`run_compiler_preprocessor_with_recipe`. ``to_dict`` is the payload + shape stored alongside parser results. + """ language: str compiler: str | None @@ -222,10 +306,11 @@ class PreprocessingRecipe: @property def std(self) -> str | None: - """Backward-compatible alias for older callers.""" + """Return the configured language standard under its historical name.""" return self.standard def to_dict(self) -> dict[str, object]: + """Return a detached JSON-compatible representation of this recipe.""" return { "language": self.language, "compiler": self.compiler, @@ -254,7 +339,13 @@ def to_dict(self) -> dict[str, object]: @dataclass class PreprocessingConfig: - """Configuration for compiler-backed preprocessing operations.""" + """Configure compiler-backed source expansion and dependency exposure. + + Use ``mode="compiler"`` with ``preprocess_source`` or either convenience + runner. Select a direct compiler, compile database, or command template; + include paths and macro options apply to the selected invocation and to + native Fortran ``INCLUDE`` expansion. + """ mode: str = "internal" compiler: str | None = None @@ -273,9 +364,16 @@ class PreprocessingConfig: @property def uses_compiler(self) -> bool: + """Whether this configuration authorizes compiler-backed preprocessing.""" return self.mode == "compiler" def fortran_internal_recipe(self, path: Path) -> dict[str, object] | None: + """Return parser-test macro metadata when compiler invocation is absent. + + ``None`` means no internal recipe is needed. The method does not read + ``path`` or execute a compiler; it only records the macros supplied to + the internal Fortran parser-test path. + """ if self.uses_compiler or not (self.defines or self.undefs): return None return PreprocessingRecipe( @@ -291,6 +389,12 @@ def fortran_internal_recipe(self, path: Path) -> dict[str, object] | None: class CompilerAdapter(Protocol): + """Describe the adapter façade used by callers with custom compiler families. + + Implementations build an invocation and expose metadata already present in + a :class:`PreprocessResult`; they do not run the compiler themselves. + """ + name: str capabilities: dict[str, bool] @@ -300,30 +404,36 @@ def build_preprocess_invocation( *, language: str, config: PreprocessingConfig, - ) -> Invocation: ... + ) -> Invocation: + """Build the adapter-specific command for ``source_path`` and ``language``.""" + ... - def collect_dependencies(self, result: PreprocessResult) -> list[IncludedFile]: ... + def collect_dependencies(self, result: PreprocessResult) -> list[IncludedFile]: + """Return the dependency records already collected in ``result``.""" + ... - def collect_macros(self, result: PreprocessResult) -> list[MacroDefinition]: ... + def collect_macros(self, result: PreprocessResult) -> list[MacroDefinition]: + """Return the macro records already collected in ``result``.""" + ... - def parse_linemarkers(self, source: str, filename: str | None = None) -> list[SourceMapping]: ... + def parse_linemarkers(self, source: str, filename: str | None = None) -> list[SourceMapping]: + """Map non-marker lines in compiler output back to their source locations.""" + ... -_VALID_LANGUAGES = {"c", "fortran"} -_C_SOURCE_SUFFIXES = {".c", ".h", ".i"} -_FORTRAN_SOURCE_SUFFIXES = {".f", ".for", ".ftn", ".f77", ".f90", ".f95", ".f03", ".f08"} -_DEFINE_RE = re.compile(r"^\s*#\s*define\s+([A-Za-z_]\w*)(\(([^)]*)\))?(?:\s+(.*))?$") -_LINEMARKER_RE = re.compile( - r'^\s*#\s+(?P\d+)\s+(?:"(?P(?:[^"\\]|\\.)*)"|(?P\S+))(?P(?:\s+\d+)*)\s*$' -) -_LINE_DIRECTIVE_RE = re.compile( - r'^\s*#\s*line\s+(?P\d+)(?:\s+(?:"(?P(?:[^"\\]|\\.)*)"|(?P\S+)))?\s*$' -) -_FORTRAN_INCLUDE_RE = re.compile(r"^\s*include\s*(?P['\"])(?P[^'\"]+)(?P=quote)\s*$", re.IGNORECASE) +# Configuration validation and command option normalization. def validate_macro_name(macro_str: str, context: str) -> None: - """Validate that a command-line macro definition has a usable name.""" + """Validate one ``-D`` or ``-U`` style macro argument before invocation. + + Use this at a configuration boundary when accepting macro text from a user. + The macro value, if any, is left untouched; only the identifier before the + first ``=`` is validated. + + Raises: + PreprocessingError: If the macro text has no valid identifier. + """ if not macro_str: raise PreprocessingError( @@ -344,6 +454,11 @@ def validate_macro_name(macro_str: str, context: str) -> None: def _require_language(language: str) -> None: + """Reject languages without a compiler-preprocessing adapter. + + The helper consumes the user-facing language selector and raises the stable + invalid-argument error before any command or filesystem work occurs. + """ if language not in _VALID_LANGUAGES: raise PreprocessingError( f"compiler preprocessing is not supported for language {language!r}", @@ -352,6 +467,11 @@ def _require_language(language: str) -> None: def _compiler_required(config: PreprocessingConfig, language: str) -> str: + """Return the explicit compiler required by a direct invocation. + + Direct mode intentionally requires an exact configured executable, while + compile-database and command-template modes obtain their command elsewhere. + """ if not config.compiler: raise PreprocessingError( f"{language} compiler preprocessing requires --compiler with an exact executable", @@ -361,7 +481,11 @@ def _compiler_required(config: PreprocessingConfig, language: str) -> str: def _fortran_preprocessor_profile(compiler: str) -> tuple[str, tuple[str, ...], dict[str, bool]]: - """Return vendor-specific preprocessing arguments and advertised facts.""" + """Return the Fortran adapter name, extra flags, and provenance capabilities. + + Unknown compilers retain the GNU-compatible profile. LLVM Flang suppresses + line markers, so its capability record explicitly reports that limitation. + """ try: token, _vendor, _c_compiler = fortran_compiler_family(compiler) except ValueError: @@ -379,6 +503,19 @@ def _fortran_preprocessor_profile(compiler: str) -> tuple[str, tuple[str, ...], ) +def _invocation_adapter_profile(language: str, compiler: str) -> tuple[str, dict[str, bool]]: + """Return the selected adapter label and a fresh capability mapping. + + Both direct and compile-database invocation builders use this shared + profile so their recorded adapter facts stay identical for the same + language/compiler pair. + """ + if language == "fortran": + adapter, _vendor_args, capabilities = _fortran_preprocessor_profile(compiler) + return adapter, capabilities + return "gcc-compatible-c", {"dependency_output": True, "macro_dump": True, "linemarkers": True} + + def _preprocessor_options( config: PreprocessingConfig, *, @@ -386,6 +523,13 @@ def _preprocessor_options( include_language_flag: bool, compiler: str, ) -> list[str]: + """Build common compiler flags in their established invocation order. + + The returned flags begin with compiler preprocessing mode, followed by the + language mode, include directories, macro controls, standard, and raw + compiler arguments. The caller decides where source and compile-database + arguments are placed around this sequence. + """ args: list[str] = ["-E"] if include_language_flag and language == "c": args.extend(["-x", "c"]) @@ -406,12 +550,23 @@ def _preprocessor_options( def _fortran_source_language_hint(source: Path) -> list[str]: + """Return a source-form hint only for Fortran paths with unknown suffixes.""" if source.suffix.lower() in _FORTRAN_SOURCE_SUFFIXES: return [] return ["-x", "f95-cpp-input"] +# Adapter facades for callers that need the protocol rather than direct execution. + + class GCCCompatibleCAdapter: + """Provide the GCC/Clang-compatible C adapter contract. + + Use this façade when a caller needs direct-command construction and + metadata access through the :class:`CompilerAdapter` protocol. Execution + remains owned by :func:`preprocess_source`. + """ + name = "gcc-compatible-c" capabilities: ClassVar[dict[str, bool]] = {"dependency_output": True, "macro_dump": True, "linemarkers": True} @@ -422,23 +577,31 @@ def build_preprocess_invocation( language: str, config: PreprocessingConfig, ) -> Invocation: + """Build this adapter's direct compiler command for one C source path.""" return build_direct_preprocess_invocation(source_path, language=language, config=config) def collect_dependencies(self, result: PreprocessResult) -> list[IncludedFile]: + """Return a shallow copy of the include records stored in ``result``.""" return list(result.included_files) def collect_macros(self, result: PreprocessResult) -> list[MacroDefinition]: + """Return a shallow copy of the macro records stored in ``result``.""" return list(result.macros) def parse_linemarkers(self, source: str, filename: str | None = None) -> list[SourceMapping]: + """Return source mappings parsed from this adapter's compiler output.""" return parse_linemarker_mappings(source, filename=filename) class GNUFortranAdapter(GCCCompatibleCAdapter): + """Expose GNU-compatible Fortran preprocessing through the shared façade.""" + name = "gnu-fortran" class CommandTemplateAdapter(GCCCompatibleCAdapter): + """Build custom-template commands while retaining shared metadata helpers.""" + name = "command-template" capabilities: ClassVar[dict[str, bool]] = {"dependency_output": False, "macro_dump": False, "linemarkers": False} @@ -449,16 +612,28 @@ def build_preprocess_invocation( language: str, config: PreprocessingConfig, ) -> Invocation: + """Expand this configuration's custom template for the selected source.""" return build_template_preprocess_invocation(source_path, language=language, config=config) +# Compiler command construction. + + def build_direct_preprocess_invocation( source_path: Path | str, *, language: str, config: PreprocessingConfig, ) -> Invocation: - """Build an exact direct compiler invocation for preprocessing.""" + """Build the exact direct compiler command used to expand one source file. + + Use this for inspection or tests when the caller has an explicit compiler. + It validates the language and compiler setting but neither reads the source + nor executes the returned command. + + Raises: + PreprocessingError: If the language is unsupported or no compiler was configured. + """ _require_language(language) compiler = _compiler_required(config, language) @@ -474,11 +649,7 @@ def build_direct_preprocess_invocation( *(_fortran_source_language_hint(source) if language == "fortran" else []), str(source), ] - if language == "fortran": - adapter, _vendor_args, capabilities = _fortran_preprocessor_profile(compiler) - else: - adapter = "gcc-compatible-c" - capabilities = {"dependency_output": True, "macro_dump": True, "linemarkers": True} + adapter, capabilities = _invocation_adapter_profile(language, compiler) return Invocation( argv=argv, cwd=None, @@ -490,6 +661,12 @@ def build_direct_preprocess_invocation( def _load_compile_commands(path: str | os.PathLike[str] | None) -> list[dict[str, object]]: + """Load and validate the top-level list in a compile-commands database. + + The helper reads UTF-8 JSON only and intentionally preserves each entry's + raw fields for recipe provenance. Entry-level validation happens when the + selected source is resolved. + """ if not path: raise PreprocessingError( "compile_commands database path is missing", @@ -519,6 +696,12 @@ def _load_compile_commands(path: str | os.PathLike[str] | None) -> list[dict[str def _entry_file_path(entry: dict[str, object]) -> Path: + """Resolve one compile-database entry's source path against its directory. + + The returned path may remain relative when the entry omits ``directory``; + that preserves compile-database working-directory semantics for later + source matching. + """ if "file" not in entry: raise PreprocessingError( "compile_commands entry is missing 'file'", @@ -532,6 +715,7 @@ def _entry_file_path(entry: dict[str, object]) -> Path: def _same_source(left: Path, right: Path) -> bool: + """Compare source paths while tolerating filesystem resolution failures.""" try: return left.resolve() == right.resolve() except OSError: @@ -539,6 +723,11 @@ def _same_source(left: Path, right: Path) -> bool: def _compile_command_argv(entry: dict[str, object]) -> list[str]: + """Extract one non-empty compiler argv from a compile-database entry. + + ``arguments`` is already tokenized; ``command`` is tokenized with shell + quoting rules. Invalid entry shapes raise the stable configuration error. + """ if "arguments" in entry: arguments = entry["arguments"] if not isinstance(arguments, list): @@ -569,6 +758,7 @@ def _compile_command_argv(entry: dict[str, object]) -> list[str]: def _is_source_arg(arg: str, source: Path, cwd: Path) -> bool: + """Return whether one compile argument names the selected source file.""" path = Path(arg) if not path.suffix: return False @@ -577,6 +767,12 @@ def _is_source_arg(arg: str, source: Path, cwd: Path) -> bool: def _filter_compile_only_args(args: list[str], source: Path, cwd: Path) -> list[str]: + """Remove compile-only output, dependency, and source arguments from ``args``. + + The remaining order is preserved because compiler target and include flags + can be significant. The helper deliberately does not normalize any other + argument text from the database. + """ filtered: list[str] = [] index = 0 while index < len(args): @@ -608,6 +804,11 @@ def _filter_compile_only_args(args: list[str], source: Path, cwd: Path) -> list[ def _compile_commands_entry(source_path: Path, database: list[dict[str, object]]) -> dict[str, object]: + """Select the sole compile-database entry that matches ``source_path``. + + Missing and ambiguous matches are configuration errors rather than an + arbitrary selection, so the recipe always identifies one exact command. + """ matches: list[dict[str, object]] = [] for entry in database: if not isinstance(entry, dict): @@ -637,7 +838,15 @@ def build_compile_commands_invocation( config: PreprocessingConfig, language: str = "c", ) -> Invocation: - """Build a preprocessing invocation from a compile_commands.json entry.""" + """Build one preprocessing command from a matching ``compile_commands`` entry. + + Use this when the project build command, rather than a standalone compiler + setting, is authoritative. Compile-only arguments are removed, then the + normal preprocessing flags are inserted ahead of retained build flags. + + Raises: + PreprocessingError: If the database cannot provide one valid matching entry. + """ _require_language(language) source = Path(source_path) @@ -658,11 +867,7 @@ def build_compile_commands_invocation( *compile_args, str(source), ] - if language == "fortran": - adapter, _vendor_args, capabilities = _fortran_preprocessor_profile(compiler) - else: - adapter = "gcc-compatible-c" - capabilities = {"dependency_output": True, "macro_dump": True, "linemarkers": True} + adapter, capabilities = _invocation_adapter_profile(language, compiler) return Invocation( argv=argv, cwd=str(cwd), @@ -676,6 +881,13 @@ def build_compile_commands_invocation( def _template_token_value(token: str, source: Path, language: str, config: PreprocessingConfig) -> list[str]: + """Expand one command-template token into zero or more argv elements. + + Collection placeholders retain the configured order, while ordinary tokens + use the scalar placeholders accepted by ``str.format``. Unknown format + fields intentionally propagate their ``KeyError`` to preserve template + validation behavior. + """ if token == "{source}": return [str(source)] if token == "{compiler}": @@ -708,6 +920,15 @@ def build_template_preprocess_invocation( language: str, config: PreprocessingConfig, ) -> Invocation: + """Build a custom-template preprocessing command without executing it. + + A template must expand to a non-empty command that writes preprocessed + source to stdout. Use the documented placeholders for source, compiler, + language, include paths, macro controls, standard, and compiler arguments. + + Raises: + PreprocessingError: If no template is configured or expansion is empty. + """ _require_language(language) if not config.command_template: raise PreprocessingError( @@ -738,7 +959,12 @@ def build_preprocess_invocation( language: str, config: PreprocessingConfig, ) -> Invocation: - """Build the selected compiler adapter invocation.""" + """Build the command selected by one preprocessing configuration. + + Command templates take precedence when configured, followed by compile + databases and then direct compiler mode. This function only selects and + builds the command; use :func:`preprocess_source` to execute it. + """ _require_language(language) if config.adapter == "command-template" or config.command_template: @@ -748,7 +974,15 @@ def build_preprocess_invocation( return build_direct_preprocess_invocation(source_path, language=language, config=config) +# Compiler provenance: line markers, dependency edges, and macro metadata. + + def _unescape_linemarker_filename(text: str) -> str: + """Decode the limited C-preprocessor escapes used in quoted marker paths. + + Unknown escape sequences intentionally lose only their escape marker, + matching compiler marker interpretation used by existing provenance tests. + """ out: list[str] = [] escaped = False for char in text: @@ -765,6 +999,11 @@ def _unescape_linemarker_filename(text: str) -> str: def _parse_linemarker(line: str) -> tuple[int, str | None, list[int]] | None: + """Parse one GCC-style marker or ``#line`` directive, if present. + + The result is ``(original_line, path, flags)``. Non-marker source lines + return ``None`` so callers can retain their generated-line accounting. + """ match = _LINE_DIRECTIVE_RE.match(line.strip()) if match is not None: filename = match.group("quoted") or match.group("bare") @@ -778,6 +1017,11 @@ def _parse_linemarker(line: str) -> tuple[int, str | None, list[int]] | None: def _dependency_kind(path: str, flags: Sequence[int] = ()) -> DependencyKind: + """Classify a marker path as a project or system dependency. + + Marker flag ``3`` and fully bracketed pseudo paths represent system inputs; + all other paths remain project dependencies until a caller marks the root. + """ if 3 in flags: return "system" if path.startswith("<") and path.endswith(">"): @@ -786,6 +1030,11 @@ def _dependency_kind(path: str, flags: Sequence[int] = ()) -> DependencyKind: def _exposure_for(path: str, kind: DependencyKind, config: PreprocessingConfig) -> Exposure: + """Choose public or private dependency exposure in precedence order. + + Explicit private patterns win, explicit public patterns come next, and the + remaining decision follows system/private and roots-only policy. + """ if any(Path(path).match(pattern) or pattern in path for pattern in config.private_includes): return "private" if any(Path(path).match(pattern) or pattern in path for pattern in config.public_includes): @@ -798,6 +1047,13 @@ def _exposure_for(path: str, kind: DependencyKind, config: PreprocessingConfig) def parse_linemarker_mappings(source: str, filename: str | None = None) -> list[SourceMapping]: + """Map each non-marker output line to original compiler-source provenance. + + Use this for GCC-style output or native Fortran expansion when downstream + parser diagnostics need original paths, lines, and nested include stacks. + Marker lines themselves have no mapping because they are directives rather + than parser input. + """ mappings: list[SourceMapping] = [] current_path = filename or "" current_line = 1 @@ -841,6 +1097,12 @@ def _included_files_from_linemarkers( language: str, config: PreprocessingConfig, ) -> list[IncludedFile]: + """Derive root and compiler-include edges from line-marker transitions. + + The returned list keeps first-seen compiler includes in output order while + always retaining the root as its first public dependency. Native Fortran + include edges are added separately by :func:`expand_native_fortran_includes`. + """ files: list[IncludedFile] = [ IncludedFile( path=str(root_path), @@ -887,6 +1149,12 @@ def _included_files_from_linemarkers( def _parse_macro_definitions(source: str, mappings: Sequence[SourceMapping]) -> list[MacroDefinition]: + """Extract ``#define`` records and attach available line-marker provenance. + + The helper only describes definitions present in the supplied source; it + does not evaluate macros or synthesize definitions absent from compiler + output. + """ macros: list[MacroDefinition] = [] mapping_by_generated = {mapping.generated_line: mapping for mapping in mappings} for generated_line, line in enumerate(source.splitlines(), start=1): @@ -914,6 +1182,7 @@ def _parse_macro_definitions(source: str, mappings: Sequence[SourceMapping]) -> def _mapping_for_generated_line( mappings: Sequence[SourceMapping], generated_line: int, fallback: Path ) -> SourceMapping: + """Return a generated-line mapping or construct the established root fallback.""" for mapping in mappings: if mapping.generated_line == generated_line: return mapping @@ -926,6 +1195,11 @@ def _mapping_for_generated_line( def _resolve_fortran_include(target: str, including_file: str, include_dirs: Sequence[str]) -> Path | None: + """Find a native Fortran include beside its source before configured paths. + + Filesystem lookup errors on one candidate do not prevent checking later + include directories. The first existing regular file wins. + """ candidates = [Path(including_file).parent / target] candidates.extend(Path(include_dir) / target for include_dir in include_dirs) for candidate in candidates: @@ -938,11 +1212,15 @@ def _resolve_fortran_include(target: str, including_file: str, include_dirs: Seq def _line_marker(line: int, path: str, flag: int | None = None) -> str: + """Render one escaped GCC-style line marker for expanded Fortran source.""" escaped = path.replace("\\", "\\\\").replace('"', '\\"') suffix = f" {flag}" if flag is not None else "" return f'# {line} "{escaped}"{suffix}' +# Native Fortran textual include expansion. + + def expand_native_fortran_includes( source: str, *, @@ -950,7 +1228,19 @@ def expand_native_fortran_includes( include_dirs: Sequence[str], config: PreprocessingConfig | None = None, ) -> tuple[str, list[IncludedFile], list[SourceMapping], list[PreprocessingDiagnostic]]: - """Resolve native Fortran INCLUDE statements by textual insertion.""" + """Expand native Fortran ``INCLUDE`` statements after compiler CPP output. + + Use this for a Fortran source stream that may still contain textual + ``include "file.inc"`` statements. The return value contains expanded + parser input, discovered include edges, generated-to-original mappings, and + recoverable diagnostics. Missing files and cycles are recorded while later + source lines continue to be emitted; :func:`preprocess_source` promotes + error diagnostics after it records the complete result. + + Relative includes resolve beside the including file before configured + include directories. Repeated non-cyclic includes are expanded repeatedly + and retain their separate dependency edges. + """ config = config or PreprocessingConfig() diagnostics: list[PreprocessingDiagnostic] = [] @@ -959,6 +1249,11 @@ def expand_native_fortran_includes( line_counter = 0 def emit_line(line: str, mapping: SourceMapping, out: list[str]) -> None: + """Append one output line and its corresponding generated-line mapping. + + ``line_counter`` is shared across recursive expansions so mappings + retain output order even when included text contributes many lines. + """ nonlocal line_counter out.append(line) line_counter += 1 @@ -972,6 +1267,12 @@ def emit_line(line: str, mapping: SourceMapping, out: list[str]) -> None: ) def expand_text(text: str, current_file: Path, stack: list[Path]) -> list[str]: + """Recursively replace include lines in one source fragment. + + ``stack`` contains resolved paths currently being expanded and is used + only for cycle detection. The function appends diagnostics instead of + raising so siblings and following source survive independent failures. + """ out: list[str] = [] mappings = parse_linemarker_mappings(text, filename=str(current_file)) mapping_by_line = {mapping.generated_line: mapping for mapping in mappings} @@ -1058,6 +1359,9 @@ def expand_text(text: str, current_file: Path, stack: list[Path]) -> list[str]: ) +# Result recipe construction and compiler execution. + + def _recipe_from_invocation( source_path: Path, language: str, @@ -1065,6 +1369,12 @@ def _recipe_from_invocation( invocation: Invocation, result: PreprocessResult | None = None, ) -> PreprocessingRecipe: + """Project an invocation and collected result metadata into a typed recipe. + + The function copies every mutable collection so the resulting recipe is a + stable snapshot of this operation rather than an alias of caller-owned + configuration or result records. + """ return PreprocessingRecipe( language=language, compiler=invocation.compiler, @@ -1089,21 +1399,51 @@ def _recipe_from_invocation( ) -def preprocess_source( - source_path: Path | str, - *, - language: str, - config: PreprocessingConfig, -) -> PreprocessResult: - """Run compiler preprocessing and return expanded source plus provenance.""" +def _recipe_from_result(result: PreprocessResult) -> PreprocessingRecipe: + """Restore the typed recipe record from a result's JSON-compatible payload. - if not config.uses_compiler: - raise PreprocessingError( - "Compiler preprocessing not configured", - category="INVALID_COMPILER_ARGUMENTS", - ) - source = Path(source_path) - invocation = build_preprocess_invocation(source, language=language, config=config) + ``PreprocessResult.recipe`` is intentionally a dictionary for parser + payload compatibility. This helper supplies the historic sparse-payload + defaults used by :func:`run_compiler_preprocessor_with_recipe`. + """ + return PreprocessingRecipe( + language=str(result.recipe.get("language")), + compiler=result.recipe.get("compiler") if isinstance(result.recipe.get("compiler"), str) else None, + mode=str(result.recipe.get("mode") or "compiler"), + adapter=str(result.recipe.get("adapter") or "direct"), + argv=list(result.recipe.get("argv") or []), + cwd=result.recipe.get("cwd") if isinstance(result.recipe.get("cwd"), str) else None, + include_dirs=list(result.recipe.get("include_dirs") or []), + defines=list(result.recipe.get("defines") or []), + undefs=list(result.recipe.get("undefs") or []), + standard=result.recipe.get("standard") if isinstance(result.recipe.get("standard"), str) else None, + compiler_args=list(result.recipe.get("compiler_args") or []), + source_path=result.recipe.get("source_path") if isinstance(result.recipe.get("source_path"), str) else None, + compile_commands=result.recipe.get("compile_commands") + if isinstance(result.recipe.get("compile_commands"), str) + else None, + compile_commands_entry=result.recipe.get("compile_commands_entry") + if isinstance(result.recipe.get("compile_commands_entry"), dict) + else None, + command_template=result.recipe.get("command_template") + if isinstance(result.recipe.get("command_template"), str) + else None, + included_files=list(result.recipe.get("included_files") or []), + source_mappings=list(result.recipe.get("source_mappings") or []), + macros=list(result.recipe.get("macros") or []), + diagnostics=list(result.recipe.get("diagnostics") or []), + capabilities=dict(result.recipe.get("capabilities") or {}), + ) + + +def _run_preprocess_invocation(invocation: Invocation) -> str: + """Execute one prepared compiler command and normalize execution failures. + + The helper performs the existing bare-executable availability check before + running the process. It returns stdout only after a zero exit status and + raises :class:`PreprocessingError` with the exact invocation attached to + each execution failure. + """ executable = invocation.argv[0] if invocation.argv else "" if executable and os.sep not in executable and shutil.which(executable) is None: raise PreprocessingError( @@ -1179,12 +1519,27 @@ def preprocess_source( ) ], ) + return completed.stdout + - expanded_source = completed.stdout - mappings = parse_linemarker_mappings(expanded_source, filename=str(source)) +def _collect_compiler_metadata( + expanded_source: str, + *, + source_path: Path, + language: str, + config: PreprocessingConfig, + invocation: Invocation, +) -> tuple[list[SourceMapping], list[IncludedFile], list[MacroDefinition], list[PreprocessingDiagnostic]]: + """Collect compiler-output provenance without changing the expanded source. + + The returned collections retain compiler output order. A no-linemarker + capability records the existing warning only when no source mappings can be + recovered from the output. + """ + mappings = parse_linemarker_mappings(expanded_source, filename=str(source_path)) included_files = _included_files_from_linemarkers( expanded_source, - root_path=source, + root_path=source_path, language=language, config=config, ) @@ -1198,8 +1553,65 @@ def preprocess_source( command=list(invocation.argv), ) ) - macros = _parse_macro_definitions(expanded_source, mappings) + return mappings, included_files, _parse_macro_definitions(expanded_source, mappings), diagnostics + + +def _raise_for_error_diagnostics(diagnostics: Sequence[PreprocessingDiagnostic]) -> None: + """Raise the first error diagnostic after all preprocessing facts are collected. + + Warnings return normally. Passing every diagnostic into the error preserves + sibling include failures and their original discovery order for callers. + """ + first_error = next((diagnostic for diagnostic in diagnostics if diagnostic.severity == "error"), None) + if first_error is not None: + raise PreprocessingError( + first_error.message, + category=first_error.category, + diagnostics=diagnostics, + ) + + +def preprocess_source( + source_path: Path | str, + *, + language: str, + config: PreprocessingConfig, +) -> PreprocessResult: + """Expand one C or Fortran source path and collect its preprocessing facts. + + Use this as the primary API before passing ``result.source`` to the + language parser. It validates compiler mode, selects and executes the + configured adapter, collects compiler provenance, then expands remaining + native Fortran includes. The returned recipe is ready to attach to parser + or build output. + + Raises: + PreprocessingError: If configuration, execution, or native include + expansion produces an error diagnostic. + """ + + # Stage 1: validate the requested compiler route and build its exact command. + if not config.uses_compiler: + raise PreprocessingError( + "Compiler preprocessing not configured", + category="INVALID_COMPILER_ARGUMENTS", + ) + source = Path(source_path) + invocation = build_preprocess_invocation(source, language=language, config=config) + + # Stage 2: execute the compiler and normalize process failures. + expanded_source = _run_preprocess_invocation(invocation) + + # Stage 3: collect compiler-provided source, dependency, and macro metadata. + mappings, included_files, macros, diagnostics = _collect_compiler_metadata( + expanded_source, + source_path=source, + language=language, + config=config, + invocation=invocation, + ) + # Stage 4: resolve native Fortran includes that compiler CPP does not expand. if language == "fortran": expanded_source, native_includes, native_mappings, native_diagnostics = expand_native_fortran_includes( expanded_source, @@ -1211,6 +1623,7 @@ def preprocess_source( mappings = native_mappings or parse_linemarker_mappings(expanded_source, filename=str(source)) diagnostics.extend(native_diagnostics) + # Stage 5: snapshot all facts into the result and recipe payload. result = PreprocessResult( source=expanded_source, recipe={}, @@ -1220,13 +1633,7 @@ def preprocess_source( diagnostics=diagnostics, ) result.recipe = _recipe_from_invocation(source, language, config, invocation, result).to_dict() - if any(diagnostic.severity == "error" for diagnostic in diagnostics): - first = next(diagnostic for diagnostic in diagnostics if diagnostic.severity == "error") - raise PreprocessingError( - first.message, - category=first.category, - diagnostics=diagnostics, - ) + _raise_for_error_diagnostics(diagnostics) return result @@ -1235,38 +1642,15 @@ def run_compiler_preprocessor_with_recipe( language: str, config: PreprocessingConfig, ) -> tuple[str, PreprocessingRecipe]: - """Run compiler preprocessing and return expanded source plus recipe.""" + """Return expanded parser input and a typed provenance recipe. + + Use this compatibility-shaped convenience API when a caller needs a tuple + rather than :class:`PreprocessResult`. It executes the same full workflow + as :func:`preprocess_source` and preserves sparse-recipe defaults. + """ result = preprocess_source(source_path, language=language, config=config) - recipe = PreprocessingRecipe( - language=str(result.recipe.get("language")), - compiler=result.recipe.get("compiler") if isinstance(result.recipe.get("compiler"), str) else None, - mode=str(result.recipe.get("mode") or "compiler"), - adapter=str(result.recipe.get("adapter") or "direct"), - argv=list(result.recipe.get("argv") or []), - cwd=result.recipe.get("cwd") if isinstance(result.recipe.get("cwd"), str) else None, - include_dirs=list(result.recipe.get("include_dirs") or []), - defines=list(result.recipe.get("defines") or []), - undefs=list(result.recipe.get("undefs") or []), - standard=result.recipe.get("standard") if isinstance(result.recipe.get("standard"), str) else None, - compiler_args=list(result.recipe.get("compiler_args") or []), - source_path=result.recipe.get("source_path") if isinstance(result.recipe.get("source_path"), str) else None, - compile_commands=result.recipe.get("compile_commands") - if isinstance(result.recipe.get("compile_commands"), str) - else None, - compile_commands_entry=result.recipe.get("compile_commands_entry") - if isinstance(result.recipe.get("compile_commands_entry"), dict) - else None, - command_template=result.recipe.get("command_template") - if isinstance(result.recipe.get("command_template"), str) - else None, - included_files=list(result.recipe.get("included_files") or []), - source_mappings=list(result.recipe.get("source_mappings") or []), - macros=list(result.recipe.get("macros") or []), - diagnostics=list(result.recipe.get("diagnostics") or []), - capabilities=dict(result.recipe.get("capabilities") or {}), - ) - return result.source, recipe + return result.source, _recipe_from_result(result) def run_compiler_preprocessor( @@ -1274,6 +1658,12 @@ def run_compiler_preprocessor( language: str, config: PreprocessingConfig, ) -> str: + """Return only compiler-expanded parser input for one configured source path. + + Use :func:`run_compiler_preprocessor_with_recipe` or + :func:`preprocess_source` instead when recipe or provenance metadata is + needed by the next pipeline stage. + """ source, _recipe = run_compiler_preprocessor_with_recipe(source_path, language, config) return source @@ -1304,3 +1694,61 @@ def run_compiler_preprocessor( "run_compiler_preprocessor_with_recipe", "validate_macro_name", ) + + +if __name__ == "__main__": + from tempfile import TemporaryDirectory + + with TemporaryDirectory() as directory: + example_directory = Path(directory) + root_path = example_directory / "greeting.F90" + include_path = example_directory / "constants.inc" + fortran_source = ( + "module greeting\n" + "include 'constants.inc'\n" + "contains\n" + "subroutine show_answer()\n" + "print *, answer\n" + "end subroutine show_answer\n" + "end module greeting\n" + ) + root_path.write_text(fortran_source, encoding="utf-8") + include_path.write_text("integer, parameter :: answer = 42\n", encoding="utf-8") + + # Native Fortran INCLUDE expansion after compiler CPP output. + print("Before Fortran include expansion:") + print(fortran_source, end="") + print() + expanded_source, included_files, _mappings, diagnostics = expand_native_fortran_includes( + fortran_source, + root_path=root_path, + include_dirs=[], + ) + parser_input = [line for line in expanded_source.splitlines() if not line.lstrip().startswith("#")] + + print("After Fortran include expansion:") + print("\n".join(parser_input)) + print(f"Native includes: {len(included_files)}; diagnostics: {len(diagnostics)}") + print() + + # Compiler-backed C include and macro expansion. + c_source_path = example_directory / "state.c" + c_header_path = example_directory / "state.h" + c_source = '#include "state.h"\nint state_id = STATE_ID;\n' + c_header_path.write_text("#define STATE_ID 42\n", encoding="utf-8") + c_source_path.write_text(c_source, encoding="utf-8") + + print("Before C compiler preprocessing:") + print(c_source, end="") + print() + c_result = preprocess_source( + c_source_path, + language="c", + config=PreprocessingConfig(mode="compiler", compiler="cc"), + ) + c_parser_input = [ + line.strip() for line in c_result.source.splitlines() if line.strip() and not line.lstrip().startswith("#") + ] + + print("After C compiler preprocessing:") + print("\n".join(c_parser_input)) diff --git a/prik/probes/c_types.py b/prik/probes/c_types.py index 00fa2be2d..8812714cd 100644 --- a/prik/probes/c_types.py +++ b/prik/probes/c_types.py @@ -24,38 +24,7 @@ from prik.pipeline.preprocessing import PreprocessingConfig, PreprocessingError, validate_macro_name -class CStandardTypeProbeError(ValueError): - """Raised when a compiler-derived C standard type probe cannot run.""" - - -@dataclass(frozen=True) -class CStandardTypeProbeRecipe: - """Commands and input flags needed to reproduce one probe result.""" - - compiler: str - compile_argv: list[str] - run_argv: list[str] - probe_standard: str = "c11" - requested_standard: str | None = None - include_dirs: list[str] | None = None - defines: list[str] | None = None - undefs: list[str] | None = None - compiler_args: list[str] | None = None - - -@dataclass(frozen=True) -class CStandardTypeProbeReport: - """JSON-stable ABI facts suitable as input to later C semantic mapping.""" - - types: dict[str, dict[str, object]] - recipe: CStandardTypeProbeRecipe - source_text: str - - def to_dict(self) -> dict[str, object]: - """Return a JSON-compatible report.""" - return asdict(self) - - +# Probe schema, fact classification, and cache identity. _SIGNED_INTEGER_TYPES = { "signed char", "short", @@ -87,11 +56,72 @@ def to_dict(self) -> dict[str, object]: "SDKROOT", "SYSROOT", ) + + +# Public result records. +class CStandardTypeProbeError(ValueError): + """Report that a compiler-derived C standard type probe could not complete. + + Callers normally surface this error when the configured compiler cannot + run, when a supplied configuration does not describe one target, or when a + reusable report does not contain the required ABI facts. + """ + + +@dataclass(frozen=True) +class CStandardTypeProbeRecipe: + """Record the commands and flags that reproduce one C ABI report. + + Each :class:`CStandardTypeProbeReport` includes this record so callers can + inspect or serialize the compiler invocation, runner, C11 probe standard, + and target-relevant preprocessing inputs that produced its facts. + """ + + compiler: str + compile_argv: list[str] + run_argv: list[str] + probe_standard: str = "c11" + requested_standard: str | None = None + include_dirs: list[str] | None = None + defines: list[str] | None = None + undefs: list[str] | None = None + compiler_args: list[str] | None = None + + +@dataclass(frozen=True) +class CStandardTypeProbeReport: + """Store JSON-stable target ABI facts for C semantic conversion or inspection. + + ``types`` maps modeled C spellings to measured and classified facts. + ``recipe`` records how they were measured, and ``source_text`` retains the + generated C11 query. Semantic conversion consumes the facts through the + CLI's direct compiler path or a caller-supplied report. + """ + + types: dict[str, dict[str, object]] + recipe: CStandardTypeProbeRecipe + source_text: str + + def to_dict(self) -> dict[str, object]: + """Return a detached JSON-compatible representation of this report. + + Use the returned mapping with :func:`json.dumps` when persisting or + displaying measured target facts. Nested recipe lists are copied by + :func:`dataclasses.asdict`. + """ + return asdict(self) + + _MEMORY_CACHE: dict[str, CStandardTypeProbeReport] = {} def build_c_standard_type_probe_source() -> str: - """Return the C11 source compiled by :func:`probe_c_standard_types`.""" + """Return the fixed C11 query compiled by the standard-type probe. + + The source measures modeled primitive and standard-library ABI facts for + one compiler target, including only pointer facts for opaque FILE. Callers + may inspect it for provenance; changing it changes the cached probe schema. + """ return r"""#include #include #include @@ -206,7 +236,12 @@ def build_c_standard_type_probe_source() -> str: def _probe_compile_flags(config: PreprocessingConfig) -> list[str]: - """Carry target-relevant compiler flags into the generated C11 probe.""" + """Return target-relevant compiler flags for the generated C11 query. + + The result carries explicit include, macro, and compiler arguments from + config while always selecting C11 for the probe's _Generic and _Alignof + use. It returns a new list without mutating the configuration. + """ flags = [f"-I{path}" for path in config.include_dirs] flags.extend(f"-D{define}" for define in config.defines) flags.extend(f"-U{undef}" for undef in config.undefs) @@ -216,7 +251,12 @@ def _probe_compile_flags(config: PreprocessingConfig) -> list[str]: def _semantic_type_facts(types: dict[str, dict[str, object]]) -> None: - """Classify arithmetic results for later semantic type conversion.""" + """Classify measured arithmetic facts in place for semantic conversion. + + Available entries initially marked arithmetic receive their semantic kind, + signedness where known, and category. Opaque or unavailable entries remain + unchanged; unrecognized arithmetic spellings become implementation_defined. + """ for fact in types.values(): if not fact.get("available") or fact.get("kind") != "arithmetic": continue @@ -253,69 +293,30 @@ def probe_c_standard_types( *, runner: Sequence[str] | None = None, ) -> CStandardTypeProbeReport: - """Compile and execute the standard-type probe for one compiler target. + """Compile and execute the C standard-type probe for one compiler target. - The default runner executes the produced binary directly, which is - appropriate for native builds. Cross-compiled targets must provide a - runner such as an emulator; the command is recorded in the result. + Supply the selected compiler and target-relevant flags in config. The + default runs the generated executable directly; cross targets pass an + emulator through runner. The report records measured and classified ABI + facts, generated C11 source, and both commands. Configuration, compiler, + runner, and malformed-output failures raise CStandardTypeProbeError. """ + # Confirm the selected compiler configuration before creating probe inputs. _validate_probe_config(config) + # Compile and run in an isolated directory removed before return. with tempfile.TemporaryDirectory(prefix="prik-c-type-probe-") as temp_dir: source_path = Path(temp_dir) / "c_standard_type_probe.c" - executable_path = Path(temp_dir) / ("c_standard_type_probe.exe" if os.name == "nt" else "c_standard_type_probe") + executable_name = "c_standard_type_probe.exe" if os.name == "nt" else "c_standard_type_probe" + executable_path = Path(temp_dir) / executable_name source_text = build_c_standard_type_probe_source() source_path.write_text(source_text, encoding="utf-8") + compile_argv = _compile_c_standard_type_probe(config, source_path, executable_path) + run_argv, output = _run_c_standard_type_probe(executable_path, runner) + payload = _probe_payload_from_output(output) - compile_argv = [ - config.compiler, - "-x", - "c", - *_probe_compile_flags(config), - str(source_path), - "-o", - str(executable_path), - ] - try: - compiled = subprocess.run( - compile_argv, - capture_output=True, - text=True, - check=False, - ) - except OSError as exc: - raise CStandardTypeProbeError(f"failed to run C type probe compiler {config.compiler!r}: {exc}") from exc - if compiled.returncode != 0: - command = " ".join(shlex.quote(arg) for arg in compile_argv) - detail = f": {compiled.stderr.strip()}" if compiled.stderr.strip() else "" - raise CStandardTypeProbeError(f"C standard type probe compilation failed with `{command}`{detail}") - - run_argv = [*(runner or ()), str(executable_path)] - try: - completed = subprocess.run( - run_argv, - capture_output=True, - text=True, - check=False, - ) - except OSError as exc: - raise CStandardTypeProbeError( - "failed to execute C standard type probe; provide a compatible " - f"runner for cross-compiled targets: {exc}" - ) from exc - if completed.returncode != 0: - command = " ".join(shlex.quote(arg) for arg in run_argv) - detail = f": {completed.stderr.strip()}" if completed.stderr.strip() else "" - raise CStandardTypeProbeError(f"C standard type probe execution failed with `{command}`{detail}") - try: - payload = json.loads(completed.stdout) - except json.JSONDecodeError as exc: - raise CStandardTypeProbeError(f"C standard type probe produced invalid JSON: {exc}") from exc - - types = payload.get("types") - if not isinstance(types, dict): - raise CStandardTypeProbeError("C standard type probe output is missing 'types'") - _semantic_type_facts(types) + # Validate and classify output before exposing it as semantic input. + types = _types_from_probe_payload(payload) return CStandardTypeProbeReport( types=types, recipe=CStandardTypeProbeRecipe( @@ -333,6 +334,13 @@ def probe_c_standard_types( def _validate_probe_config(config: PreprocessingConfig) -> None: + """Ensure config can describe one standalone C ABI probe. + + The probe requires an exact compiler command and reuses only explicit + target flags. Compile databases and custom preprocessing templates belong + to source preprocessing, so this helper rejects them and otherwise leaves + config unchanged. + """ if not config.compiler: raise CStandardTypeProbeError("C standard type probing requires an exact compiler executable") if config.compile_commands: @@ -347,8 +355,104 @@ def _validate_probe_config(config: PreprocessingConfig) -> None: ) +def _compile_c_standard_type_probe( + config: PreprocessingConfig, + source_path: Path, + executable_path: Path, +) -> list[str]: + """Compile generated C11 source and return its exact command. + + Source and executable paths must be in the active temporary directory. The + helper carries target-relevant flags from config and raises + CStandardTypeProbeError for launch or nonzero-exit failures. + """ + compile_argv = [ + config.compiler, + "-x", + "c", + *_probe_compile_flags(config), + str(source_path), + "-o", + str(executable_path), + ] + try: + compiled = subprocess.run( + compile_argv, + capture_output=True, + text=True, + check=False, + ) + except OSError as exc: + raise CStandardTypeProbeError(f"failed to run C type probe compiler {config.compiler!r}: {exc}") from exc + if compiled.returncode != 0: + command = " ".join(shlex.quote(arg) for arg in compile_argv) + detail = f": {compiled.stderr.strip()}" if compiled.stderr.strip() else "" + raise CStandardTypeProbeError(f"C standard type probe compilation failed with `{command}`{detail}") + return compile_argv + + +def _run_c_standard_type_probe(executable_path: Path, runner: Sequence[str] | None) -> tuple[list[str], str]: + """Execute one compiled C probe and return its command plus standard output. + + Runner prefixes the executable for cross targets. Missing runners or + executables, and nonzero program exits, raise CStandardTypeProbeError; + successful stdout is preserved for JSON validation. + """ + run_argv = [*(runner or ()), str(executable_path)] + try: + completed = subprocess.run( + run_argv, + capture_output=True, + text=True, + check=False, + ) + except OSError as exc: + raise CStandardTypeProbeError( + f"failed to execute C standard type probe; provide a compatible runner for cross-compiled targets: {exc}" + ) from exc + if completed.returncode != 0: + command = " ".join(shlex.quote(arg) for arg in run_argv) + detail = f": {completed.stderr.strip()}" if completed.stderr.strip() else "" + raise CStandardTypeProbeError(f"C standard type probe execution failed with `{command}`{detail}") + return run_argv, completed.stdout + + +def _probe_payload_from_output(output: str) -> Any: + """Decode the generated C query's JSON output. + + Successful output is returned unchanged as its decoded JSON value. Malformed + JSON raises CStandardTypeProbeError while the temporary probe directory is + still active, matching the compiler-execution failure boundary. + """ + try: + return json.loads(output) + except json.JSONDecodeError as exc: + raise CStandardTypeProbeError(f"C standard type probe produced invalid JSON: {exc}") from exc + + +def _types_from_probe_payload(payload: Any) -> dict[str, dict[str, object]]: + """Validate and classify the types mapping from decoded probe JSON. + + Payload must contain the types mapping emitted by the fixed query. The + returned mapping has available arithmetic entries mutated by + _semantic_type_facts; a missing mapping raises CStandardTypeProbeError. + """ + types = payload.get("types") + if not isinstance(types, dict): + raise CStandardTypeProbeError("C standard type probe output is missing 'types'") + _semantic_type_facts(types) + return types + + +# Report loading and cache management. def load_c_standard_type_probe_report(path: str | Path) -> CStandardTypeProbeReport: - """Load and validate a previously generated C ABI probe report.""" + """Load and validate a reusable C ABI probe report from JSON. + + Pass a report written by CStandardTypeProbeReport.to_dict when inspection + or direct semantic conversion needs an already measured target. Invalid or + unreadable files raise CStandardTypeProbeError; successful loads do not + probe or change the cache. + """ report_path = Path(path) try: payload = json.loads(report_path.read_text(encoding="utf-8")) @@ -364,7 +468,12 @@ def c_standard_type_probe_cache_key( *, runner: Sequence[str] | None = None, ) -> str: - """Return the cache key for one exact compiler target and probe schema.""" + """Return the cache key for one exact compiler target and probe schema. + + The digest covers generated C11 source, compiler identity, target-relevant + flags, working directory, selected environment, and runner. Callers + normally use probe_c_standard_types_cached rather than handling entries. + """ source_digest = hashlib.sha256(build_c_standard_type_probe_source().encode()).hexdigest() payload = { "schema_version": _PROBE_CACHE_SCHEMA_VERSION, @@ -392,9 +501,18 @@ def probe_c_standard_types_cached( cache_dir: str | Path | None = None, refresh: bool = False, ) -> CStandardTypeProbeReport: - """Return compiler ABI facts, reusing memory and persistent cache entries.""" + """Return ABI facts, reusing matching memory and persistent reports. + + Use this normal semantic-conversion path instead of the uncached probe. + Reports are reused first from process memory, then the selected cache + directory; a miss runs a probe and writes it back. Refresh skips both read + layers. Invalid cache entries are ignored, and a read-only cache never + prevents a successful measurement. + """ _validate_probe_config(config) cache_key = c_standard_type_probe_cache_key(config, runner=runner) + + # Reuse the fastest trustworthy result first. if not refresh and cache_key in _MEMORY_CACHE: return _MEMORY_CACHE[cache_key] @@ -408,6 +526,7 @@ def probe_c_standard_types_cached( _MEMORY_CACHE[cache_key] = report return report + # A cache miss or refresh measures and then offers the report for reuse. report = probe_c_standard_types(config, runner=runner) _MEMORY_CACHE[cache_key] = report _write_cached_report(cache_path, report) @@ -415,6 +534,12 @@ def probe_c_standard_types_cached( def _report_from_payload(payload: Any, *, source: str) -> CStandardTypeProbeReport: + """Construct a report only from a complete serialized report payload. + + Source labels validation failures. The helper verifies top-level facts, + recipe compiler, and generated source, then restores optional recipe lists + with their existing empty-list defaults. + """ if not isinstance(payload, dict): raise CStandardTypeProbeError(f"C type probe report {source} must contain a JSON object") types = payload.get("types") @@ -446,6 +571,13 @@ def _report_from_payload(payload: Any, *, source: str) -> CStandardTypeProbeRepo def _compiler_identity(compiler: str | None) -> dict[str, object]: + """Describe a compiler or runner command for cache invalidation. + + A command resolves through PATH when possible and records its path, size, + and modification time if stat succeeds. Missing commands remain + representable so callers can form a key before a later probe reports the + launch failure. + """ if compiler is None: return {"command": None} resolved = shutil.which(compiler) or compiler @@ -460,6 +592,12 @@ def _compiler_identity(compiler: str | None) -> dict[str, object]: def _probe_cache_dir(cache_dir: str | Path | None) -> Path: + """Select the persistent cache directory without creating it. + + An explicit cache directory wins, followed by PRIK_CACHE_DIR and + XDG_CACHE_HOME. The platform-default path is returned only when no override + exists; directory creation is deferred to cache writing. + """ if cache_dir is not None: return Path(cache_dir) if root := os.getenv("PRIK_CACHE_DIR"): @@ -470,6 +608,13 @@ def _probe_cache_dir(cache_dir: str | Path | None) -> Path: def _write_cached_report(path: Path, report: CStandardTypeProbeReport) -> None: + """Atomically offer report to the persistent cache at path. + + The helper creates the parent and replaces final JSON only after writing a + sibling temporary file. Cache-write failures are suppressed so unavailable + or read-only storage cannot fail semantic conversion; a leftover temporary + file is removed when possible. + """ temporary_path = path.with_name(f".{path.name}.{os.getpid()}.tmp") try: path.parent.mkdir(parents=True, exist_ok=True) @@ -483,8 +628,16 @@ def _write_cached_report(path: Path, report: CStandardTypeProbeReport) -> None: temporary_path.unlink(missing_ok=True) +# Standalone CLI. def main(argv: list[str] | None = None) -> int: - """Run a compiler-derived C standard type probe and write JSON.""" + """Run the C ABI probe CLI and print one report as indented JSON. + + Use this entrypoint from python -m prik.probes.c_types with an explicit + compiler. Argv is accepted for embedding and tests; otherwise command-line + arguments are parsed. Invalid macros and probe failures go through + argparse, while success writes the report to standard output and returns + zero. + """ parser = argparse.ArgumentParser( description="Probe modeled C arithmetic-primitive and standard-type ABI facts through an exact compiler." ) @@ -529,10 +682,6 @@ def main(argv: list[str] | None = None) -> int: return 0 -if __name__ == "__main__": # pragma: no cover - exercised through CLI tests. - raise SystemExit(main()) - - __all__ = ( "CStandardTypeProbeError", "CStandardTypeProbeRecipe", @@ -543,3 +692,16 @@ def main(argv: list[str] | None = None) -> int: "probe_c_standard_types", "probe_c_standard_types_cached", ) + + +if __name__ == "__main__": # pragma: no cover - exercised through CLI tests. + import sys + + if __spec__ is None and len(sys.argv) == 1: + compiler = shutil.which("cc") + if compiler is None: + raise SystemExit("The direct C type-probe example requires cc on PATH.") + report = probe_c_standard_types(PreprocessingConfig(mode="compiler", compiler=compiler)) + print(f"int: {report.types['int']['bits']}-bit signed") + else: + raise SystemExit(main()) diff --git a/prik/probes/fortran_types.py b/prik/probes/fortran_types.py index f36921ef6..d7d9d2fea 100644 --- a/prik/probes/fortran_types.py +++ b/prik/probes/fortran_types.py @@ -1,10 +1,10 @@ -"""Compiler-derived Fortran kind expression facts. +"""Compiler-derived Fortran kind and storage facts. -Fortran kind values and intrinsics such as ``selected_real_kind`` are -processor/compiler facts. This module evaluates the exact initialization -expressions requested by the semantic layer through the user-selected Fortran -compiler and flags, then returns values suitable for -``FortranToIRConverter(..., compile_time_values=...)``. +Fortran kind values and intrinsic expressions such as ``selected_real_kind`` +are compiler facts, not parser or semantic-policy decisions. This module +evaluates the exact expressions requested by the semantic layer with the +selected compiler and flags, caches the resulting facts, and returns values +suitable for ``FortranToIRConverter(..., compile_time_values=...)``. """ from __future__ import annotations @@ -27,13 +27,74 @@ from prik.pipeline.preprocessing import PreprocessingConfig, PreprocessingError, validate_macro_name +# Cache identity and generated-source configuration. +_PROBE_CACHE_SCHEMA_VERSION = 1 +_PROBE_ENVIRONMENT_VARIABLES = ( + "COMPILER_PATH", + "CPATH", + "GFORTRAN_UNBUFFERED_ALL", + "GFORTRAN_UNBUFFERED_PRECONNECTED", + "GFORTRAN_CONVERT_UNIT", + "GCC_EXEC_PREFIX", + "LIB", + "LIBRARY_PATH", + "QEMU_LD_PREFIX", + "SDKROOT", + "SYSROOT", +) +_SAFE_EXPRESSION_RE = re.compile(r"^[A-Za-z0-9_+\-*/().,= :]+$") +_TOKEN_RE = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\b") + +_ISO_FORTRAN_ENV_NAMES = { + "int8", + "int16", + "int32", + "int64", + "logical_kinds", + "real32", + "real64", + "real128", +} +_ISO_C_BINDING_NAMES = { + "c_bool", + "c_char", + "c_double", + "c_double_complex", + "c_float", + "c_float_complex", + "c_int", + "c_int16_t", + "c_int32_t", + "c_int64_t", + "c_int8_t", + "c_long", + "c_long_double", + "c_long_double_complex", + "c_long_long", + "c_short", + "c_signed_char", + "c_size_t", +} + + +# Public result records. class FortranTypeProbeError(ValueError): - """Raised when a compiler-derived Fortran type probe cannot run.""" + """Report that a compiler-derived Fortran type probe could not complete. + + Callers normally surface this error when the configured compiler cannot + run, an expression is unsafe to embed in generated source, or a reusable + report does not contain the facts required for semantic conversion. + """ @dataclass(frozen=True) class FortranTypeProbeRecipe: - """Commands and input flags needed to reproduce one probe result.""" + """Record the commands and flags that reproduce one probe result. + + Each :class:`FortranTypeProbeReport` includes this record so callers can + inspect or serialize the exact compiler invocation, runner, expressions, + and target-relevant preprocessing inputs that produced its values. + """ compiler: str compile_argv: list[str] @@ -48,26 +109,38 @@ class FortranTypeProbeRecipe: @dataclass(frozen=True) class FortranTypeProbeReport: - """JSON-stable Fortran kind expression values for semantic conversion.""" + """Store JSON-stable compiler facts for semantic conversion or inspection. + + ``values`` maps the exact requested expressions to integer results. + ``recipe`` captures how those facts were measured, and ``source_text`` is + the generated program used for the measurement. Pass this report to the + evaluation functions to reuse already measured expressions. + """ values: dict[str, int] recipe: FortranTypeProbeRecipe source_text: str def to_dict(self) -> dict[str, object]: - """Return a JSON-compatible report.""" + """Return a detached JSON-compatible representation of this report. + + Use the returned mapping with :func:`json.dumps` when persisting or + displaying compiler facts. Nested recipe lists are copied by + :func:`dataclasses.asdict`. + """ return asdict(self) def to_compile_time_values( self, requirements: Iterable[Mapping[str, object]] | None = None, ) -> dict[str, int]: - """Return values keyed for semantic compile-time substitution. + """Return values in the form consumed by semantic compile-time lookup. - Exact expression keys are always included. When semantic requirement - records are supplied, parameter requirements also add ``symbol -> value`` - mappings, so a parameter such as ``rk = selected_real_kind(12)`` resolves - both ``selected_real_kind(12)`` and later uses of ``rk``. + Exact expression keys are always included. When semantic requirement + records are supplied, ``parameter_value`` entries also add + ``symbol -> value`` mappings. Thus ``rk = selected_real_kind(12)`` + resolves both the expression and later uses of ``rk``. The returned + dictionary is independent of the report. """ values = dict(self.values) if requirements is None: @@ -83,61 +156,19 @@ def to_compile_time_values( return values -_PROBE_CACHE_SCHEMA_VERSION = 1 -_PROBE_ENVIRONMENT_VARIABLES = ( - "COMPILER_PATH", - "CPATH", - "GFORTRAN_UNBUFFERED_ALL", - "GFORTRAN_UNBUFFERED_PRECONNECTED", - "GFORTRAN_CONVERT_UNIT", - "GCC_EXEC_PREFIX", - "LIB", - "LIBRARY_PATH", - "QEMU_LD_PREFIX", - "SDKROOT", - "SYSROOT", -) _MEMORY_CACHE: dict[str, FortranTypeProbeReport] = {} -_SAFE_EXPRESSION_RE = re.compile(r"^[A-Za-z0-9_+\-*/().,= :]+$") -_TOKEN_RE = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\b") - -_ISO_FORTRAN_ENV_NAMES = { - "int8", - "int16", - "int32", - "int64", - "real32", - "real64", - "real128", -} -_ISO_C_BINDING_NAMES = { - "c_bool", - "c_char", - "c_double", - "c_double_complex", - "c_float", - "c_float_complex", - "c_int", - "c_int16_t", - "c_int32_t", - "c_int64_t", - "c_int8_t", - "c_long", - "c_long_double", - "c_long_double_complex", - "c_long_long", - "c_short", - "c_signed_char", - "c_size_t", -} - - +# Requirement collection and generated source. def fortran_type_probe_expressions( requirements: Iterable[Mapping[str, object]], ) -> list[str]: - """Return unique semantic requirement expressions that need probing.""" + """Return ordered, case-insensitively unique expressions from requirements. + + Use this when semantic requirement records need to become probe input. It + ignores records without an expression and preserves the first spelling of + each expression so that reports remain readable and deterministic. + """ expressions: list[str] = [] seen: set[str] = set() for item in requirements: @@ -153,7 +184,14 @@ def fortran_type_probe_expressions( def build_fortran_type_probe_source(expressions: Sequence[str]) -> str: - """Return free-form Fortran source that evaluates integer expressions.""" + """Build free-form Fortran source that prints integer expression results. + + Callers may inspect the returned source or pass it to a compiler. Blank and + duplicate expressions are removed case-insensitively, and recognized + ``iso_fortran_env`` and ``iso_c_binding`` names receive the imports needed + by the standalone generated program. Unsafe expressions raise + :class:`FortranTypeProbeError` before source is returned. + """ unique_expressions = _normalize_expressions(expressions) imports = _probe_import_lines(unique_expressions) declarations = [ @@ -182,84 +220,111 @@ def build_fortran_type_probe_source(expressions: Sequence[str]) -> str: return "\n".join(lines) +def _normalize_expressions(expressions: Sequence[str]) -> list[str]: + """Validate, trim, and deduplicate generated-source expressions. + + The input sequence may contain empty strings or equivalent Fortran names + with different case. The returned list preserves the first non-empty + spelling of each case-insensitive expression; invalid text raises before + any source is generated. + """ + normalized: list[str] = [] + seen: set[str] = set() + for expression in expressions: + text = str(expression).strip() + if not text: + continue + _validate_expression(text) + key = text.lower() + if key in seen: + continue + seen.add(key) + normalized.append(text) + return normalized + + +def _validate_expression(expression: str) -> None: + """Reject text that cannot safely occupy one parameter declaration. + + ``expression`` is already trimmed by the caller. The probe accepts only a + single initialization expression from a conservative character set, so it + cannot introduce another Fortran statement; failures raise + :class:`FortranTypeProbeError` without mutating state. + """ + if "\n" in expression or "\r" in expression or ";" in expression: + raise FortranTypeProbeError( + f"Fortran type probe expression is not a single initialization expression: {expression!r}" + ) + if _SAFE_EXPRESSION_RE.fullmatch(expression) is None: + raise FortranTypeProbeError(f"Fortran type probe expression contains unsupported characters: {expression!r}") + + +def _probe_import_lines(expressions: Sequence[str]) -> list[str]: + """Return intrinsic module imports required by the generated expressions. + + The helper scans the normalized expression tokens and emits at most one + import for each supported intrinsic module. Names are sorted so generated + source and cache keys stay deterministic. + """ + tokens = {token.lower() for expression in expressions for token in _TOKEN_RE.findall(expression)} + lines: list[str] = [] + env_names = sorted(tokens & _ISO_FORTRAN_ENV_NAMES) + c_names = sorted(tokens & _ISO_C_BINDING_NAMES) + if env_names: + lines.extend(_probe_import_statement("iso_fortran_env", env_names)) + if c_names: + lines.extend(_probe_import_statement("iso_c_binding", c_names)) + return lines + + +def _probe_import_statement(module: str, names: Sequence[str]) -> list[str]: + """Format one intrinsic ``use`` statement within the source line limit. + + ``module`` and the ordered imported ``names`` become either one line or a + continuation block. The returned lines preserve name order and never alter + the calling source builder's expression list. + """ + single_line = f" use, intrinsic :: {module}, only: {', '.join(names)}" + if len(single_line) <= 120: + return [single_line] + lines = [f" use, intrinsic :: {module}, only: &"] + lines.extend(f" {name}{', &' if index < len(names) - 1 else ''}" for index, name in enumerate(names)) + return lines + + +# Compiler execution. def probe_fortran_type_expressions( config: PreprocessingConfig, expressions: Sequence[str], *, runner: Sequence[str] | None = None, ) -> FortranTypeProbeReport: - """Compile and execute a Fortran expression probe for one compiler target. - - The default runner executes the produced binary directly, which matches the - current native-target assumption. Cross targets can pass an emulator/runner - command; the command is recorded in the result. + """Compile and execute a Fortran probe for one compiler target. + + Supply the selected compiler and target-relevant flags in ``config``, plus + the integer initialization ``expressions`` needed by semantic conversion. + The default directly executes the generated binary; cross targets pass an + emulator command through ``runner``. The returned report records the + generated source and both commands. Compilation, execution, malformed + output, and unsupported configuration failures raise + :class:`FortranTypeProbeError`. """ + # Prepare validated source before creating any temporary compiler inputs. _validate_probe_config(config) - unique_expressions = _normalize_expressions(expressions) source_text = build_fortran_type_probe_source(unique_expressions) + # Compile and run in an isolated directory that is removed before return. with tempfile.TemporaryDirectory(prefix="prik-fortran-type-probe-") as temp_dir: source_path = Path(temp_dir) / "fortran_type_probe.F90" - executable_path = Path(temp_dir) / ("fortran_type_probe.exe" if os.name == "nt" else "fortran_type_probe") + executable_name = "fortran_type_probe.exe" if os.name == "nt" else "fortran_type_probe" + executable_path = Path(temp_dir) / executable_name source_path.write_text(source_text, encoding="utf-8") + compile_argv = _compile_fortran_type_probe(config, source_path, executable_path) + run_argv, output = _run_fortran_type_probe(executable_path, runner) - compile_argv = [ - config.compiler, - *_probe_compile_flags(config), - str(source_path), - "-o", - str(executable_path), - ] - try: - compiled = subprocess.run( - compile_argv, - capture_output=True, - text=True, - check=False, - ) - except OSError as exc: - raise FortranTypeProbeError( - f"failed to run Fortran type probe compiler {config.compiler!r}: {exc}" - ) from exc - if compiled.returncode != 0: - command = " ".join(shlex.quote(arg) for arg in compile_argv) - detail = f": {compiled.stderr.strip()}" if compiled.stderr.strip() else "" - raise FortranTypeProbeError(f"Fortran type probe compilation failed with `{command}`{detail}") - - run_argv = [*(runner or ()), str(executable_path)] - try: - completed = subprocess.run( - run_argv, - capture_output=True, - text=True, - check=False, - ) - except OSError as exc: - raise FortranTypeProbeError( - f"failed to execute Fortran type probe; provide a compatible runner for cross-compiled targets: {exc}" - ) from exc - if completed.returncode != 0: - command = " ".join(shlex.quote(arg) for arg in run_argv) - detail = f": {completed.stderr.strip()}" if completed.stderr.strip() else "" - raise FortranTypeProbeError(f"Fortran type probe execution failed with `{command}`{detail}") - try: - payload = json.loads(completed.stdout) - except json.JSONDecodeError as exc: - raise FortranTypeProbeError(f"Fortran type probe produced invalid JSON: {exc}") from exc - - raw_values = payload.get("values") - if not isinstance(raw_values, list): - raise FortranTypeProbeError("Fortran type probe output is missing 'values'") - if len(raw_values) != len(unique_expressions): - raise FortranTypeProbeError("Fortran type probe output count does not match input expressions") - - values: dict[str, int] = {} - for expression, value in zip(unique_expressions, raw_values, strict=False): - if not isinstance(value, int): - raise FortranTypeProbeError(f"Fortran type probe value for {expression!r} is not an integer") - values[expression] = value - + # Validate compiler output before exposing it as semantic input. + values = _probe_values_from_output(output, unique_expressions) return FortranTypeProbeReport( values=values, recipe=FortranTypeProbeRecipe( @@ -278,6 +343,13 @@ def probe_fortran_type_expressions( def _validate_probe_config(config: PreprocessingConfig) -> None: + """Ensure ``config`` can describe one standalone compiler probe. + + The probe requires an exact compiler command and reuses only explicit + target flags. Compile databases and custom preprocessing templates belong + to source preprocessing, so this helper rejects them with a stable probe + error and otherwise leaves ``config`` unchanged. + """ if not config.compiler: raise FortranTypeProbeError("Fortran type probing requires an exact compiler executable") if config.compile_commands: @@ -292,8 +364,121 @@ def _validate_probe_config(config: PreprocessingConfig) -> None: ) +def _compile_fortran_type_probe( + config: PreprocessingConfig, + source_path: Path, + executable_path: Path, +) -> list[str]: + """Compile the generated source and return its exact command. + + ``source_path`` and ``executable_path`` must be inside the active temporary + directory. The helper carries target-relevant flags from ``config`` into + the compiler command, raises :class:`FortranTypeProbeError` on launch or + nonzero-exit failures, and does not retain compiler output on success. + """ + compile_argv = [ + config.compiler, + *_probe_compile_flags(config), + str(source_path), + "-o", + str(executable_path), + ] + try: + compiled = subprocess.run( + compile_argv, + capture_output=True, + text=True, + check=False, + ) + except OSError as exc: + raise FortranTypeProbeError(f"failed to run Fortran type probe compiler {config.compiler!r}: {exc}") from exc + if compiled.returncode != 0: + command = " ".join(shlex.quote(arg) for arg in compile_argv) + detail = f": {compiled.stderr.strip()}" if compiled.stderr.strip() else "" + raise FortranTypeProbeError(f"Fortran type probe compilation failed with `{command}`{detail}") + return compile_argv + + +def _probe_compile_flags(config: PreprocessingConfig) -> list[str]: + """Return target-relevant compiler flags for the generated probe source. + + The result deliberately mirrors the selected preprocessing inputs without + attempting source preprocessing itself. It is a new list, leaving the + configuration lists unmodified for callers and cache-key construction. + """ + flags = ["-cpp"] + flags.extend(f"-I{path}" for path in config.include_dirs) + flags.extend(f"-D{define}" for define in config.defines) + flags.extend(f"-U{undef}" for undef in config.undefs) + if config.std: + flags.append(f"-std={config.std}") + flags.extend(config.compiler_args) + return flags + + +def _run_fortran_type_probe(executable_path: Path, runner: Sequence[str] | None) -> tuple[list[str], str]: + """Execute one compiled probe and return its command plus standard output. + + ``runner`` prefixes the executable path for cross-compiled targets. On a + missing runner or executable, or a nonzero program exit, this helper raises + :class:`FortranTypeProbeError`; on success it preserves the compiler + program's stdout unchanged for the JSON-validation stage. + """ + run_argv = [*(runner or ()), str(executable_path)] + try: + completed = subprocess.run( + run_argv, + capture_output=True, + text=True, + check=False, + ) + except OSError as exc: + raise FortranTypeProbeError( + f"failed to execute Fortran type probe; provide a compatible runner for cross-compiled targets: {exc}" + ) from exc + if completed.returncode != 0: + command = " ".join(shlex.quote(arg) for arg in run_argv) + detail = f": {completed.stderr.strip()}" if completed.stderr.strip() else "" + raise FortranTypeProbeError(f"Fortran type probe execution failed with `{command}`{detail}") + return run_argv, completed.stdout + + +def _probe_values_from_output(output: str, expressions: Sequence[str]) -> dict[str, int]: + """Parse and validate the generated probe's JSON values. + + ``output`` must encode the list emitted by the generated Fortran program, + ordered to match normalized ``expressions``. The returned mapping retains + that expression spelling and order; malformed JSON, count mismatches, and + non-integer values raise :class:`FortranTypeProbeError`. + """ + try: + payload = json.loads(output) + except json.JSONDecodeError as exc: + raise FortranTypeProbeError(f"Fortran type probe produced invalid JSON: {exc}") from exc + + raw_values = payload.get("values") + if not isinstance(raw_values, list): + raise FortranTypeProbeError("Fortran type probe output is missing 'values'") + if len(raw_values) != len(expressions): + raise FortranTypeProbeError("Fortran type probe output count does not match input expressions") + + values: dict[str, int] = {} + for expression, value in zip(expressions, raw_values, strict=False): + if not isinstance(value, int): + raise FortranTypeProbeError(f"Fortran type probe value for {expression!r} is not an integer") + values[expression] = value + return values + + +# Report loading and cache management. def load_fortran_type_probe_report(path: str | Path) -> FortranTypeProbeReport: - """Load and validate a reusable compiler-derived Fortran type report.""" + """Load and validate a reusable compiler-derived type report from JSON. + + Pass a report written from :meth:`FortranTypeProbeReport.to_dict` when + semantic inspection needs an already measured target. Invalid or unreadable + files raise :class:`FortranTypeProbeError`; successful loads return the + typed report without probing or changing the cache. + """ report_path = Path(path) try: payload = json.loads(report_path.read_text(encoding="utf-8")) @@ -304,13 +489,56 @@ def load_fortran_type_probe_report(path: str | Path) -> FortranTypeProbeReport: return _report_from_payload(payload, source=str(report_path)) +def _report_from_payload(payload: Any, *, source: str) -> FortranTypeProbeReport: + """Construct a report only from a complete serialized report payload. + + ``source`` labels validation failures for the caller. This helper verifies + the top-level values, recipe compiler, and generated source, then restores + optional recipe lists with their existing empty-list defaults. + """ + if not isinstance(payload, dict): + raise FortranTypeProbeError(f"Fortran type probe report {source} must contain a JSON object") + values = payload.get("values") + recipe = payload.get("recipe") + source_text = payload.get("source_text") + if not isinstance(values, dict) or not all( + isinstance(key, str) and isinstance(value, int) for key, value in values.items() + ): + raise FortranTypeProbeError(f"Fortran type probe report {source} is missing valid 'values'") + if not isinstance(recipe, dict) or not isinstance(recipe.get("compiler"), str): + raise FortranTypeProbeError(f"Fortran type probe report {source} is missing a valid 'recipe'") + if not isinstance(source_text, str): + raise FortranTypeProbeError(f"Fortran type probe report {source} is missing valid 'source_text'") + return FortranTypeProbeReport( + values=values, + recipe=FortranTypeProbeRecipe( + compiler=recipe["compiler"], + compile_argv=list(recipe.get("compile_argv") or []), + run_argv=list(recipe.get("run_argv") or []), + expressions=list(recipe.get("expressions") or []), + requested_standard=recipe.get("requested_standard"), + include_dirs=list(recipe.get("include_dirs") or []), + defines=list(recipe.get("defines") or []), + undefs=list(recipe.get("undefs") or []), + compiler_args=list(recipe.get("compiler_args") or []), + ), + source_text=source_text, + ) + + def fortran_type_probe_cache_key( config: PreprocessingConfig, expressions: Sequence[str], *, runner: Sequence[str] | None = None, ) -> str: - """Return the cache key for one exact compiler target and expression set.""" + """Return the cache key for one exact compiler target and expression set. + + The digest covers normalized generated source, compiler identity, + target-relevant flags, working directory, selected environment, and runner. + Use it only to inspect cache identity; callers normally use + :func:`probe_fortran_type_expressions_cached` to retrieve a report. + """ normalized = _normalize_expressions(expressions) source_digest = hashlib.sha256(build_fortran_type_probe_source(normalized).encode()).hexdigest() payload = { @@ -332,6 +560,27 @@ def fortran_type_probe_cache_key( return hashlib.sha256(json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()).hexdigest() +def _compiler_identity(compiler: str | None) -> dict[str, object]: + """Describe a compiler or runner command for cache invalidation. + + A configured command resolves through ``PATH`` when possible and records + its path, size, and modification time if stat succeeds. Missing commands + remain representable so callers can still form a deterministic key before + a later probe reports the launch failure. + """ + if compiler is None: + return {"command": None} + resolved = shutil.which(compiler) or compiler + path = Path(resolved).expanduser().resolve() + identity: dict[str, object] = {"command": compiler, "path": str(path)} + try: + stat = path.stat() + except OSError: + return identity + identity.update({"size": stat.st_size, "mtime_ns": stat.st_mtime_ns}) + return identity + + def probe_fortran_type_expressions_cached( config: PreprocessingConfig, expressions: Sequence[str], @@ -340,9 +589,18 @@ def probe_fortran_type_expressions_cached( cache_dir: str | Path | None = None, refresh: bool = False, ) -> FortranTypeProbeReport: - """Return compiler facts, reusing memory and persistent cache entries.""" + """Return compiler facts, reusing matching memory and persistent reports. + + Call this instead of the uncached probe for normal semantic conversion. + Matching reports are first reused from process memory, then the selected + cache directory; a miss runs a new probe and writes it back. ``refresh`` + skips both read layers. A read-only cache never prevents a successful probe, + but invalid cached reports are ignored and measured again. + """ _validate_probe_config(config) cache_key = fortran_type_probe_cache_key(config, expressions, runner=runner) + + # Reuse the fastest trustworthy result first. if not refresh and cache_key in _MEMORY_CACHE: return _MEMORY_CACHE[cache_key] @@ -356,39 +614,52 @@ def probe_fortran_type_expressions_cached( _MEMORY_CACHE[cache_key] = report return report + # A cache miss or refresh measures and then offers the new report for reuse. report = probe_fortran_type_expressions(config, expressions, runner=runner) _MEMORY_CACHE[cache_key] = report _write_cached_report(cache_path, report) return report -def _report_for_expressions( - config: PreprocessingConfig, - expressions: Sequence[str], - *, - report: FortranTypeProbeReport | None = None, - runner: Sequence[str] | None = None, - cache_dir: str | Path | None = None, - refresh: bool = False, -) -> FortranTypeProbeReport: - normalized = _normalize_expressions(expressions) - if report is not None: - missing = [expression for expression in normalized if _value_for_expression(report.values, expression) is None] - if missing: - raise FortranTypeProbeError( - "Fortran type probe report is missing required expressions: " - + ", ".join(repr(item) for item in missing) - ) - return report - return probe_fortran_type_expressions_cached( - config, - normalized, - runner=runner, - cache_dir=cache_dir, - refresh=refresh, - ) +def _probe_cache_dir(cache_dir: str | Path | None) -> Path: + """Select the persistent cache directory without creating it. + + An explicit ``cache_dir`` wins, followed by ``PRIK_CACHE_DIR`` and + ``XDG_CACHE_HOME``. The platform-default cache location is returned only + when no override is present; directory creation is intentionally deferred + to the write path. + """ + if cache_dir is not None: + return Path(cache_dir) + if root := os.getenv("PRIK_CACHE_DIR"): + return Path(root) / "fortran_type_probe" + if root := os.getenv("XDG_CACHE_HOME"): + return Path(root) / "prik" / "fortran_type_probe" + return Path.home() / ".cache" / "prik" / "fortran_type_probe" + + +def _write_cached_report(path: Path, report: FortranTypeProbeReport) -> None: + """Atomically offer ``report`` to the persistent cache at ``path``. + + The helper creates the parent directory and replaces the final JSON only + after writing a sibling temporary file. Cache-write failures are suppressed + so unavailable or read-only cache storage cannot fail semantic conversion; + a leftover temporary file is removed when possible. + """ + temporary_path = path.with_name(f".{path.name}.{os.getpid()}.tmp") + try: + path.parent.mkdir(parents=True, exist_ok=True) + temporary_path.write_text(json.dumps(report.to_dict(), indent=2), encoding="utf-8") + os.replace(temporary_path, path) + except OSError: + # A read-only home/cache directory must not make semantic conversion fail. + pass + finally: + with suppress(OSError): + temporary_path.unlink(missing_ok=True) +# Semantic consumers of measured facts. def evaluate_fortran_type_requirements( config: PreprocessingConfig, requirements: Iterable[Mapping[str, object]], @@ -398,7 +669,14 @@ def evaluate_fortran_type_requirements( cache_dir: str | Path | None = None, refresh: bool = False, ) -> dict[str, int]: - """Evaluate collected semantic requirements into compile-time values.""" + """Resolve semantic compile-time requirements into substitution values. + + Pass semantic requirement records collected from parsed Fortran. If a + matching ``report`` is supplied it is validated and reused; otherwise this + function obtains a cached compiler measurement using ``config``. The result + maps both expressions and eligible parameter symbols to integers, or is + empty when no expression needs probing. + """ requirement_list = list(requirements) expressions = fortran_type_probe_expressions(requirement_list) if not expressions: @@ -423,7 +701,15 @@ def evaluate_fortran_type_facts( cache_dir: str | Path | None = None, refresh: bool = False, ) -> dict[tuple[str, str | None], dict[str, object]]: - """Measure storage facts for collected intrinsic Fortran type requirements.""" + """Resolve intrinsic storage requirements into semantic type facts. + + Supply the semantic type requirement records that contain a base type, + optional kind, and storage-size expression. This function reuses a supplied + report or cached measurement, then returns facts keyed by ``(base_type, + kind)`` for the Fortran semantic converter. It returns an empty mapping when + requirements contain no expressions and raises if a supplied report is + incomplete. + """ requirement_list = list(requirements) expressions = [str(item.get("expression") or "").strip() for item in requirement_list] expressions = [expression for expression in expressions if expression] @@ -437,6 +723,7 @@ def evaluate_fortran_type_facts( cache_dir=cache_dir, refresh=refresh, ) + facts: dict[tuple[str, str | None], dict[str, object]] = {} for item in requirement_list: expression = str(item.get("expression") or "").strip() @@ -457,134 +744,130 @@ def evaluate_fortran_type_facts( return facts -def _report_from_payload(payload: Any, *, source: str) -> FortranTypeProbeReport: - if not isinstance(payload, dict): - raise FortranTypeProbeError(f"Fortran type probe report {source} must contain a JSON object") - values = payload.get("values") - recipe = payload.get("recipe") - source_text = payload.get("source_text") - if not isinstance(values, dict) or not all( - isinstance(key, str) and isinstance(value, int) for key, value in values.items() - ): - raise FortranTypeProbeError(f"Fortran type probe report {source} is missing valid 'values'") - if not isinstance(recipe, dict) or not isinstance(recipe.get("compiler"), str): - raise FortranTypeProbeError(f"Fortran type probe report {source} is missing a valid 'recipe'") - if not isinstance(source_text, str): - raise FortranTypeProbeError(f"Fortran type probe report {source} is missing valid 'source_text'") - return FortranTypeProbeReport( - values=values, - recipe=FortranTypeProbeRecipe( - compiler=recipe["compiler"], - compile_argv=list(recipe.get("compile_argv") or []), - run_argv=list(recipe.get("run_argv") or []), - expressions=list(recipe.get("expressions") or []), - requested_standard=recipe.get("requested_standard"), - include_dirs=list(recipe.get("include_dirs") or []), - defines=list(recipe.get("defines") or []), - undefs=list(recipe.get("undefs") or []), - compiler_args=list(recipe.get("compiler_args") or []), - ), - source_text=source_text, - ) - - -def _compiler_identity(compiler: str | None) -> dict[str, object]: - if compiler is None: - return {"command": None} - resolved = shutil.which(compiler) or compiler - path = Path(resolved).expanduser().resolve() - identity: dict[str, object] = {"command": compiler, "path": str(path)} - try: - stat = path.stat() - except OSError: - return identity - identity.update({"size": stat.st_size, "mtime_ns": stat.st_mtime_ns}) - return identity - - -def _probe_cache_dir(cache_dir: str | Path | None) -> Path: - if cache_dir is not None: - return Path(cache_dir) - if root := os.getenv("PRIK_CACHE_DIR"): - return Path(root) / "fortran_type_probe" - if root := os.getenv("XDG_CACHE_HOME"): - return Path(root) / "prik" / "fortran_type_probe" - return Path.home() / ".cache" / "prik" / "fortran_type_probe" - - -def _write_cached_report(path: Path, report: FortranTypeProbeReport) -> None: - temporary_path = path.with_name(f".{path.name}.{os.getpid()}.tmp") - try: - path.parent.mkdir(parents=True, exist_ok=True) - temporary_path.write_text(json.dumps(report.to_dict(), indent=2), encoding="utf-8") - os.replace(temporary_path, path) - except OSError: - # A read-only home/cache directory must not make semantic conversion fail. - pass - finally: - with suppress(OSError): - temporary_path.unlink(missing_ok=True) - - -def _normalize_expressions(expressions: Sequence[str]) -> list[str]: - normalized: list[str] = [] - seen: set[str] = set() - for expression in expressions: - text = str(expression).strip() - if not text: - continue - _validate_expression(text) - key = text.lower() - if key in seen: - continue - seen.add(key) - normalized.append(text) - return normalized - +def resolve_fortran_logical_storage_types( + config: PreprocessingConfig, + storage_bits: Iterable[int], + *, + runner: Sequence[str] | None = None, + cache_dir: str | Path | None = None, + refresh: bool = False, +) -> dict[int, str]: + """Resolve Boolean storage widths to exact Fortran logical declarations. + + Use this when a language-neutral semantic ``.pyi`` contains ``Bool`` or a + numbered Boolean contract but no source-language kind spelling. The + selected compiler is queried for ``logical_kinds`` and ``c_bool``. The + result maps each requested bit width to ``logical(kind=...)``; eight-bit + ``c_bool`` storage is preferred when it matches. Unsupported or ambiguous + widths raise :class:`FortranTypeProbeError` rather than guessing an ABI. + + The probe uses the normal reusable cache and leaves ``config`` and the + caller's iterable unchanged. + """ + requested = tuple(sorted({int(bits) for bits in storage_bits})) + if not requested: + return {} + if any(bits <= 0 for bits in requested): + raise FortranTypeProbeError("Fortran logical storage widths must be positive integers") -def _validate_expression(expression: str) -> None: - if "\n" in expression or "\r" in expression or ";" in expression: - raise FortranTypeProbeError( - f"Fortran type probe expression is not a single initialization expression: {expression!r}" + summary = probe_fortran_type_expressions_cached( + config, + [ + "size(logical_kinds)", + "c_bool", + "storage_size(logical(.false., kind=c_bool))", + ], + runner=runner, + cache_dir=cache_dir, + refresh=refresh, + ) + kind_count = summary.values["size(logical_kinds)"] + c_bool_kind = summary.values["c_bool"] + c_bool_bits = summary.values["storage_size(logical(.false., kind=c_bool))"] + if kind_count <= 0: + raise FortranTypeProbeError("Fortran compiler reported no supported logical kinds") + + expressions = [ + expression + for index in range(1, kind_count + 1) + for expression in ( + f"logical_kinds({index})", + f"storage_size(logical(.false., kind=logical_kinds({index})))", ) - if _SAFE_EXPRESSION_RE.fullmatch(expression) is None: - raise FortranTypeProbeError(f"Fortran type probe expression contains unsupported characters: {expression!r}") - - -def _probe_import_lines(expressions: Sequence[str]) -> list[str]: - tokens = {token.lower() for expression in expressions for token in _TOKEN_RE.findall(expression)} - lines: list[str] = [] - env_names = sorted(tokens & _ISO_FORTRAN_ENV_NAMES) - c_names = sorted(tokens & _ISO_C_BINDING_NAMES) - if env_names: - lines.extend(_probe_import_statement("iso_fortran_env", env_names)) - if c_names: - lines.extend(_probe_import_statement("iso_c_binding", c_names)) - return lines - + ] + details = probe_fortran_type_expressions_cached( + config, + expressions, + runner=runner, + cache_dir=cache_dir, + refresh=refresh, + ) + kinds_by_bits: dict[int, list[int]] = {} + for index in range(1, kind_count + 1): + kind = details.values[f"logical_kinds({index})"] + bits = details.values[f"storage_size(logical(.false., kind=logical_kinds({index})))"] + kinds_by_bits.setdefault(bits, []).append(kind) + + resolved: dict[int, str] = {} + for bits in requested: + candidates = list(dict.fromkeys(kinds_by_bits.get(bits, ()))) + if bits == c_bool_bits and c_bool_kind in candidates: + resolved[bits] = "logical(kind=c_bool)" + elif len(candidates) == 1: + resolved[bits] = f"logical(kind={candidates[0]})" + elif not candidates: + raise FortranTypeProbeError(f"Fortran compiler has no logical kind with {bits}-bit storage") + else: + candidate_text = ", ".join(str(kind) for kind in candidates) + raise FortranTypeProbeError( + f"Fortran compiler has ambiguous logical kinds for {bits}-bit storage: {candidate_text}" + ) + return resolved -def _probe_import_statement(module: str, names: Sequence[str]) -> list[str]: - single_line = f" use, intrinsic :: {module}, only: {', '.join(names)}" - if len(single_line) <= 120: - return [single_line] - lines = [f" use, intrinsic :: {module}, only: &"] - lines.extend(f" {name}{', &' if index < len(names) - 1 else ''}" for index, name in enumerate(names)) - return lines +def _report_for_expressions( + config: PreprocessingConfig, + expressions: Sequence[str], + *, + report: FortranTypeProbeReport | None = None, + runner: Sequence[str] | None = None, + cache_dir: str | Path | None = None, + refresh: bool = False, +) -> FortranTypeProbeReport: + """Return a report proven to contain every requested expression. -def _probe_compile_flags(config: PreprocessingConfig) -> list[str]: - """Carry target-relevant compiler flags into the generated probe.""" - flags = ["-cpp"] - flags.extend(f"-I{path}" for path in config.include_dirs) - flags.extend(f"-D{define}" for define in config.defines) - flags.extend(f"-U{undef}" for undef in config.undefs) - if config.std: - flags.append(f"-std={config.std}") - flags.extend(config.compiler_args) - return flags + A caller-supplied ``report`` is checked case-insensitively against the + normalized expressions and returned unchanged when complete. Without one, + the helper delegates to the normal cached probe path, preserving its cache + and refresh behavior. Missing supplied values raise before semantic facts + are constructed. + """ + normalized = _normalize_expressions(expressions) + if report is not None: + missing = [expression for expression in normalized if _value_for_expression(report.values, expression) is None] + if missing: + raise FortranTypeProbeError( + "Fortran type probe report is missing required expressions: " + + ", ".join(repr(item) for item in missing) + ) + return report + return probe_fortran_type_expressions_cached( + config, + normalized, + runner=runner, + cache_dir=cache_dir, + refresh=refresh, + ) def _value_for_expression(values: Mapping[str, int], expression: str) -> int | None: + """Look up an expression value, accepting Fortran's case insensitivity. + + The exact dictionary key is checked first to keep the common path fast. + Otherwise the helper compares trimmed, lower-cased keys and returns the + first matching value; it returns ``None`` rather than raising when no fact + is available. + """ exact = values.get(expression) if exact is not None: return exact @@ -595,8 +878,17 @@ def _value_for_expression(values: Mapping[str, int], expression: str) -> int | N return None +# Standalone CLI. def main(argv: list[str] | None = None) -> int: - """Run a compiler-derived Fortran type probe and write JSON.""" + """Run the probe CLI and print one report as indented JSON. + + Use this entrypoint from ``python -m prik.probes.fortran_types`` with an + explicit ``--compiler`` and one or more ``--expr`` arguments. ``argv`` is + accepted for embedding and tests; otherwise command-line arguments are + parsed. Invalid macro definitions and probe failures are reported through + argparse, while success writes the report to standard output and returns + zero. + """ parser = argparse.ArgumentParser( description="Probe Fortran kind/compile-time expressions through an exact compiler." ) @@ -650,10 +942,6 @@ def main(argv: list[str] | None = None) -> int: return 0 -if __name__ == "__main__": # pragma: no cover - exercised through CLI tests. - raise SystemExit(main()) - - __all__ = ( "FortranTypeProbeError", "FortranTypeProbeRecipe", @@ -666,4 +954,21 @@ def main(argv: list[str] | None = None) -> int: "load_fortran_type_probe_report", "probe_fortran_type_expressions", "probe_fortran_type_expressions_cached", + "resolve_fortran_logical_storage_types", ) + + +if __name__ == "__main__": # pragma: no cover - exercised through CLI tests. + import sys + + if __spec__ is None and len(sys.argv) == 1: + compiler = shutil.which("gfortran") or shutil.which("f95") + if compiler is None: + raise SystemExit("The direct type-probe example requires gfortran or f95 on PATH.") + report = probe_fortran_type_expressions( + PreprocessingConfig(mode="compiler", compiler=compiler), + ["selected_int_kind(9)"], + ) + print(f"selected_int_kind(9) = {report.values['selected_int_kind(9)']}") + else: + raise SystemExit(main()) diff --git a/prik/probes/report.py b/prik/probes/report.py index 25629e7a9..a4d8fc9d9 100644 --- a/prik/probes/report.py +++ b/prik/probes/report.py @@ -1,4 +1,10 @@ -"""Generate target-specific native-to-semantic-to-NumPy mapping examples.""" +"""Generate target-specific native-to-semantic-to-NumPy mapping reports. + +The public functions measure compiler-dependent target facts through the probe +stage, convert the supported native spellings through existing semantic +converters, and render Markdown for documentation or inspection. They report +the selected target; they do not define parser facts or semantic policy. +""" from __future__ import annotations @@ -37,6 +43,7 @@ from prik.types.numpy import numpy_dtype_expression +# C report inventory. _C_TYPES = ( ("_Bool", CBool()), ("char", CChar()), @@ -60,6 +67,7 @@ ) +# Fortran report inventory. def _fortran_type( spelling: str, base_type: str, @@ -69,6 +77,13 @@ def _fortran_type( character_length_syntax: bool = False, declared_storage_bits: int | None = None, ) -> tuple[str, FortranVariable]: + """Build one report-only Fortran variable and its displayed spelling. + + The helper records metadata that the existing Fortran converter consumes + when deriving a target type key. It returns the spelling and configured + variable without mutating any caller-owned object; the private attributes + intentionally distinguish legacy storage and character-length forms. + """ variable = FortranVariable(name="value", base_type=base_type, kind=kind or "") if target_kind_expression: variable._target_kind_expression = target_kind_expression @@ -151,7 +166,13 @@ def _fortran_type( def target_profile() -> str: - """Return a stable platform label used by architecture-specific docs.""" + """Return the normalized platform label shown at the top of each report. + + Use this value to identify the local Python host named in a rendered table. + Common AMD64 and ARM64 machine aliases normalize to their conventional + architecture names; the result is not a compiler target triple and does + not alter probing. + """ machine = platform.machine().lower() machine = {"amd64": "x86_64", "arm64": "aarch64"}.get(machine, machine) return f"{platform.system().lower()}-{machine}" @@ -165,19 +186,32 @@ def c_type_mapping_markdown( cache_dir: str | None = None, refresh: bool = False, ) -> str: - """Generate the modeled C arithmetic mapping table for one compiler target.""" + """Render the modeled C native-to-semantic-to-NumPy mapping for one target. + + Use this inspection report when documenting or checking how the selected + compiler represents the supported C primitive and standard-library types. + Compiler arguments and an optional runner select a native or cross target; + cache options are forwarded to the existing C ABI probe. The returned + Markdown contains the target profile and one row per supported C spelling. + Probe and semantic-conversion failures propagate to the caller. + """ + # Measure target ABI facts once for every C spelling in this fixed report. report = probe_c_standard_types_cached( PreprocessingConfig(mode="compiler", compiler=compiler, compiler_args=list(compiler_args)), runner=runner, cache_dir=cache_dir, refresh=refresh, ) + + # Reuse the C semantic converter to project each measured native type. converter = CToIRConverter(standard_type_report=report) rows = [] for spelling, ctype in _C_TYPES: semantic_type = converter.visit(ctype, as_type=True) fact = report.types[spelling] rows.append((spelling, _c_fact_text(fact), _semantic_text(semantic_type), _numpy_dtype(semantic_type.dtype))) + + # Render the stable documentation table after all target conversion is complete. return _markdown_table("C type", rows) @@ -189,7 +223,16 @@ def fortran_type_mapping_markdown( cache_dir: str | None = None, refresh: bool = False, ) -> str: - """Generate the supported Fortran intrinsic mapping table for one target.""" + """Render the supported Fortran native-to-semantic-to-NumPy mapping for one target. + + Use this inspection report to show how the selected compiler and flags map + the maintained modern and legacy intrinsic spellings. It probes only + compiler-dependent storage expressions, models fixed legacy storage and + character code units directly, then returns a Markdown table. Compiler, + runner, and cache options use the existing Fortran probe path; its failures + and semantic-conversion failures propagate to the caller. + """ + # Associate every maintained spelling with its converter key and probe expression. key_converter = FortranToIRConverter() entries = [ ( @@ -205,6 +248,8 @@ def fortran_type_mapping_markdown( for spelling, variable in _FORTRAN_TYPES for key in [key_converter._target_type_key(variable)] ] + + # Measure all compiler-dependent storage expressions in one cached probe. expressions = [expression for _spelling, _variable, _key, expression in entries if expression is not None] config = PreprocessingConfig(mode="compiler", compiler=compiler, compiler_args=list(compiler_args)) report = probe_fortran_type_expressions_cached( @@ -214,6 +259,8 @@ def fortran_type_mapping_markdown( cache_dir=cache_dir, refresh=refresh, ) + + # Convert probe entries into the storage-fact records accepted by semantic IR. requirements = [ { "base_type": key[0], @@ -224,6 +271,8 @@ def fortran_type_mapping_markdown( if expression is not None ] converter = FortranToIRConverter(type_facts=evaluate_fortran_type_facts(config, requirements, report=report)) + + # Convert every displayed spelling with the shared target facts, then render it. rows = [] for spelling, variable, key, _expression in entries: semantic_type = converter.visit(variable) @@ -239,7 +288,12 @@ def fortran_type_mapping_markdown( def _fortran_fact_text(semantic_type, key: tuple[str, str | None]) -> str: - """Describe probed storage or the modeled character code unit.""" + """Format one Fortran row's target-storage description. + + Character entries intentionally bypass compiler metadata because the report + models their eight-bit code unit directly. Every other entry consumes the + converter metadata populated from the shared Fortran probe facts. + """ if key[0] == "character": return "8-bit storage" fact = semantic_type.metadata["fortran_type_fact"] @@ -247,6 +301,12 @@ def _fortran_fact_text(semantic_type, key: tuple[str, str | None]) -> str: def _c_fact_text(fact: dict[str, object]) -> str: + """Format one classified C probe fact for a Markdown table cell. + + The helper reads the existing measured and semantic-category fields without + changing them. Unknown or non-arithmetic fact kinds remain visible instead + of receiving a report-only fallback classification. + """ bits = int(fact.get("bits") or 0) if fact.get("kind") == "integer": signedness = "signed" if fact.get("signed") else "unsigned" @@ -261,12 +321,24 @@ def _c_fact_text(fact: dict[str, object]) -> str: def _semantic_text(semantic_type) -> str: + """Format semantic identity and concrete storage for a report cell. + + Stable semantic names that differ from their target dtype retain both + pieces of information; matching names render only once. The semantic value + is read-only and may originate from either language converter. + """ if semantic_type.name != semantic_type.dtype: return f"{semantic_type.name} ({semantic_type.dtype} storage)" return str(semantic_type.dtype) def _numpy_dtype(semantic_dtype: str | None) -> str: + """Return the NumPy expression displayed for one semantic dtype. + + Unsupported semantic dtypes render as a stable marker rather than raising + during documentation generation. String rows retain the distinct ABI-byte + note because the NumPy string type is not the native character layout. + """ try: expression = numpy_dtype_expression(semantic_dtype) except KeyError: @@ -277,6 +349,12 @@ def _numpy_dtype(semantic_dtype: str | None) -> str: def _markdown_table(native_header: str, rows: list[tuple[str, str, str, str]]) -> str: + """Render ordered native, target, semantic, and NumPy rows as Markdown. + + Native rows must already be in their supported-display order. The helper + adds the local target-profile heading and does not escape or reorder row + content, preserving the generated documentation snapshot format. + """ lines = [ f"Target profile: `{target_profile()}`", "", @@ -287,8 +365,16 @@ def _markdown_table(native_header: str, rows: list[tuple[str, str, str, str]]) - return "\n".join(lines) +# Standalone CLI. def main(argv: list[str] | None = None) -> int: - """Print one compiler-generated datatype mapping table.""" + """Print one compiler-generated C or Fortran datatype mapping table. + + Use this entrypoint from python -m prik.probes.report with a required + language and optional compiler, target, runner, and cache settings. Argv is + accepted for embedding and tests; on success the chosen report is written + to standard output and zero is returned. Compiler probe and conversion + failures are intentionally allowed to reach the caller. + """ parser = argparse.ArgumentParser(description="Generate a target-specific prik datatype mapping table.") parser.add_argument("--language", choices=("c", "fortran"), required=True) parser.add_argument("--compiler", help="Exact compiler executable; defaults to cc or gfortran.") @@ -310,12 +396,24 @@ def main(argv: list[str] | None = None) -> int: return 0 -if __name__ == "__main__": # pragma: no cover - exercised through executable documentation. - raise SystemExit(main()) - - __all__ = ( "c_type_mapping_markdown", "fortran_type_mapping_markdown", "target_profile", ) + + +if __name__ == "__main__": # pragma: no cover - exercised through executable documentation. + import shutil + import sys + import tempfile + + if __spec__ is None and len(sys.argv) == 1: + compiler = shutil.which("cc") + if compiler is None: + raise SystemExit("The direct type-mapping example requires cc on PATH.") + with tempfile.TemporaryDirectory(prefix="prik-type-mapping-example-") as cache_dir: + markdown = c_type_mapping_markdown(compiler=compiler, cache_dir=cache_dir, refresh=True) + print(next(line for line in markdown.splitlines() if line.startswith("| `int` |"))) + else: + raise SystemExit(main()) diff --git a/prik/semantics/README.md b/prik/semantics/README.md index 78e3f06e2..a2913e680 100644 --- a/prik/semantics/README.md +++ b/prik/semantics/README.md @@ -18,6 +18,27 @@ editable `.pyi` files, policy completion, and wrapper code generation. | `policy_completion.py` | Complete ownership, transfer, destruction, mutability/writeback, projection, nullability, release, storage, Python-barrier, native-barrier, and accessor decisions after full signatures are known. | | `ir2ast.py` | Semantic IR to codegen AST lowering for wrapper generation; consumes completed policies. | +## Declaration Expressions + +`utilities/declaration_expressions.py` owns the shared declaration-expression +grammar, normalization, callable/reference discovery, native-style rendering, +and deterministic integer evaluation. Semantic conversion does not evaluate an +arbitrary native specification function. Instead, it records the function's +native name, known module origin when available, and any exact contract +declaration. + +For an imported contract batch, `pyi2ir.py` reconciles those references with +the matching prototype or module function. Policy completion then classifies +whether the declaration has a usable boundary role or remains a named blocker. +Wrapper planning and code generation consume that completed decision; they do +not rediscover callable provenance or synthesize an interface. + +Use a `@prototype` declaration when a standalone native procedure needs an +exact interface. Its argument transport and result contract supply the +information needed to emit the interface body. A module procedure already +visible through a native `use` association does not need a duplicate +interface declaration. + ## Pipeline Position ```text diff --git a/prik/semantics/c2ir.py b/prik/semantics/c2ir.py index f31778a52..997d8049c 100644 --- a/prik/semantics/c2ir.py +++ b/prik/semantics/c2ir.py @@ -1,3 +1,11 @@ +"""Convert parsed C declarations into language-neutral semantic IR. + +The public helpers at the end of this module consume C parser models and +produce :class:`~prik.semantics.models.SemanticModule` objects. They preserve +C type, declaration, target-fact, and source-provenance information for later +semantic policy completion; they do not choose wrapper implementation policy. +""" + from __future__ import annotations import ast @@ -5,6 +13,8 @@ from pathlib import Path from typing import Any +from prik.types.numpy import BOOLEAN_STORAGE_BITS + from prik.parsers.c.models import ( CArray, CBool, @@ -47,7 +57,7 @@ ) from prik.utilities.visitor import ClassVisitor -from .models import ( +from prik.semantics.models import ( EXTERNAL_TYPE_REF_METADATA, ProjectionMapping, SemanticArgument, @@ -177,6 +187,13 @@ def __init__( standard_type_report: Any | None = None, primitive_type_map: dict[type[CType], str | None] | None = None, ): + """Configure conversion with optional compiler facts and primitive overrides. + + ``standard_type_report`` supplies target-measured C type facts, while + ``primitive_type_map`` replaces selected parser primitive mappings. + Mutable symbol registries are initialized for the current file or + project visitor and are restored after scoped conversion. + """ self.primitive_type_map = dict(_PRIMITIVE_TYPE_MAP) if primitive_type_map: self.primitive_type_map.update(primitive_type_map) @@ -188,7 +205,12 @@ def __init__( self.opaque_standard_types: set[str] = set() def visit(self, node, **context): - """Dispatch one parsed C model through its class visitor.""" + """Convert one supported C parser model through the shared class visitor. + + Use this for a specific model when a public convenience helper does not + match the input shape. The returned semantic type follows ``node``; + unsupported parser models raise :class:`TypeError`. + """ return self._visit(node, **context) @staticmethod @@ -196,7 +218,15 @@ def _visit_not_supported(node): """Reject parser models without a semantic conversion visitor.""" raise TypeError(f"Unsupported C parse object: {type(node)!r}") + # Project and translation-unit visitors + def _visit_CProject(self, project: CProject) -> list[SemanticModule]: + """Convert each project file in stable filename order with global type context. + + Project symbol registries let each file resolve cross-file declarations. + After conversion, the existing external-type classifier links consumers + to the module that owns each public aggregate type. + """ self.typedefs = dict(project.typedefs) self.structs = dict(project.structs) self.unions = dict(project.unions) @@ -220,6 +250,12 @@ def project_to_semantic_module( *, name: str = "c_project", ) -> SemanticModule: + """Merge a project registry into one synthetic semantic module. + + This compatibility entrypoint converts project-level registries without + file-module exposure processing. It restores every converter registry + in ``finally`` so a reused converter has no project-state leakage. + """ previous = self.typedefs, self.structs, self.unions, self.enums, self.opaque_standard_types self.typedefs = dict(project.typedefs) self.structs = dict(project.structs) @@ -268,6 +304,12 @@ def _visit_CFile( unions: dict[str, CUnion] | None = None, enums: dict[str, CEnum] | None = None, ) -> SemanticModule: + """Convert one translation unit while temporarily installing its type registries. + + The visitor converts functions, constants, variables, and aggregates in + parser order, then applies include exposure and private-class handling. + Registry state is restored even if conversion raises an existing error. + """ previous = self.typedefs, self.structs, self.unions, self.enums self.typedefs = typedefs or {typedef.name: typedef for typedef in c_file.typedefs} self.structs = structs or {struct.name: struct for struct in c_file.structs if struct.name} @@ -308,7 +350,15 @@ def _visit_CFile( finally: self.typedefs, self.structs, self.unions, self.enums = previous + # Declaration visitors + def _visit_CFunction(self, function: CFunction) -> SemanticFunction: + """Convert a C function declaration into arguments, result, and projection facts. + + Parameter order is retained for both native and Python projection + positions. Storage specifiers only determine the existing visibility + fact here; wrapper policy remains a later semantic stage. + """ arguments = [ self.visit(parameter, position=index, owner=function.name) for index, parameter in enumerate(function.parameters) @@ -351,6 +401,12 @@ def _visit_CParameter( position: int = 0, owner: str | None = None, ) -> SemanticArgument: + """Convert one parameter and retain native position and declaration provenance. + + Anonymous parameters receive the stable ``arg`` name. A + parser-detected function pointer is represented by the existing callback + placeholder rather than inferred wrapper policy. + """ name = parameter.name or f"arg{position}" source_type = parameter.declared_type or parameter.type semantic_type = self.visit(source_type, owner=f"{owner or ''}.{name}", as_type=True) @@ -379,6 +435,12 @@ def _visit_CVariable( binding_cls: type[SemanticVariable] = SemanticVariable, source_kind: str = "variable", ) -> SemanticVariable: + """Convert a global variable or aggregate field into the requested binding subtype. + + The result retains static visibility, initializer text, bit width, and + source origin. Callback candidates use the existing function-pointer + placeholder before the semantic binding is constructed. + """ name = variable.name or "" semantic_type = self.visit(variable.type, owner=name, as_type=True) if variable.callback_candidate: @@ -401,6 +463,12 @@ def _visit_CVariable( def _visit_CStruct( self, struct: CStruct, *, as_type: bool = False, owner: str | None = None ) -> SemanticClass | SemanticType: + """Convert a C struct as a semantic class or, in type mode, a named reference. + + Class conversion retains C kind, incompleteness, anonymous status, and + nested aggregate fields. ``as_type`` delegates to existing registry + resolution for declarations that only refer to the struct. + """ if as_type: return self._struct_type(struct, owner=owner) name = self._struct_name(struct) @@ -429,6 +497,12 @@ def _visit_CStruct( def _visit_CUnion( self, union: CUnion, *, as_type: bool = False, owner: str | None = None ) -> SemanticClass | SemanticType: + """Convert a C union as a semantic class or, in type mode, a named reference. + + The class path preserves union-specific metadata and nested aggregates; + the type path uses the existing union registry resolution without + changing the parser declaration's incomplete-state semantics. + """ if as_type: return self._union_type(union, owner=owner) metadata: dict[str, Any] = {"c_kind": "union", "incomplete": union.is_incomplete} @@ -457,6 +531,12 @@ def _aggregate_fields( self, members: list[CVariable], ) -> tuple[list[SemanticField], list[SemanticClass]]: + """Convert aggregate members into fields and separately owned anonymous classes. + + Unnamed nested structs and unions receive deterministic private names so + their field can reference the nested class. Nameless ordinary members + remain omitted, matching the established semantic surface. + """ fields: list[SemanticField] = [] nested_classes: list[SemanticClass] = [] anonymous_member_counts: dict[str, int] = {"struct": 0, "union": 0} @@ -491,6 +571,12 @@ def _aggregate_fields( return fields, nested_classes def _nested_aggregate_class(self, aggregate: CStruct | CUnion, *, name: str) -> SemanticClass: + """Build a semantic class for one anonymous nested struct or union. + + ``name`` is supplied by the parent-field naming pass. Nested members + are recursively converted, and the output records anonymous and opaque + facts without registering a new top-level parser symbol. + """ if isinstance(aggregate, CStruct): fields, nested_classes = self._aggregate_fields(aggregate.members) return SemanticClass( @@ -544,6 +630,7 @@ def _nested_aggregate_class(self, aggregate: CStruct | CUnion, *, name: str) -> @staticmethod def _aggregate_base_classes(kind: str, *, anonymous: bool, opaque: bool) -> list[str]: + """Return semantic marker bases for a struct/union and its declaration facts.""" base_classes = ["CStruct" if kind == "struct" else "CUnion"] if anonymous: base_classes.append("CAnonymous") @@ -552,6 +639,7 @@ def _aggregate_base_classes(kind: str, *, anonymous: bool, opaque: bool) -> list return base_classes def _aggregate_reference_type(self, aggregate: CStruct | CUnion, *, name: str) -> SemanticType: + """Create the type reference used by a field that owns an anonymous aggregate.""" kind = "struct" if isinstance(aggregate, CStruct) else "union" return SemanticType( name=name, @@ -572,6 +660,11 @@ def _aggregate_member_argument( semantic_type: SemanticType, anonymous_member: bool, ) -> SemanticField: + """Create an aggregate field, marking anonymous member access when required. + + The supplied ``semantic_type`` is mutated only to append the + ``CAnonymousMember`` constraint for a field with no native name. + """ if anonymous_member: semantic_type.constraints.append(SemanticConstraint("CAnonymousMember")) return SemanticField( @@ -589,6 +682,8 @@ def _aggregate_member_argument( ), ) + # Type visitors and conversion helpers + def _visit_CType( self, type_: CType, @@ -685,11 +780,19 @@ def _primitive_type(self, type_: CType, *, owner: str | None) -> SemanticType: ) def _return_type(self, type_: CType, *, owner: str) -> SemanticType | None: + """Convert a function result, using ``None`` for by-value C ``void``.""" if isinstance(type_, CVoid): return None return self.visit(type_, owner=owner, as_type=True) def _composed_type(self, type_: CComposedType, *, owner: str | None) -> SemanticType: + """Convert declarator composition through array, pointer, and callback stages. + + Leading arrays and pointers are handled before the remaining base type. + Unsupported mixtures return the existing explicit unsupported semantic + type rather than selecting a wrapper policy; source text is retained in + every resulting diagnostic type. + """ components = list(type_.components) if not components: return self._unsupported_type( @@ -759,6 +862,12 @@ def _composed_type(self, type_: CComposedType, *, owner: str | None) -> Semantic ) def _typedef_type(self, typedef: CTypedef, *, owner: str | None) -> SemanticType: + """Resolve a typedef chain, standard name, or unresolved reference into semantic IR. + + Resolved declarations append their spelling to ``c_typedefs`` metadata. + Names without a concrete declaration retain the existing unresolved-type + representation instead of being guessed from the typedef name. + """ resolved = self._resolve_typedef(typedef) if resolved is not None and resolved is not typedef: semantic_type = self.visit(resolved.type or resolved, owner=owner, as_type=True) @@ -783,6 +892,7 @@ def _typedef_type(self, typedef: CTypedef, *, owner: str | None) -> SemanticType ) def _struct_type(self, struct: CStruct, *, owner: str | None) -> SemanticType: + """Return a named semantic struct reference after consulting the file/project registry.""" if struct.name and struct.name in self.structs: struct = self.structs[struct.name] name = self._struct_name(struct) @@ -794,6 +904,7 @@ def _struct_type(self, struct: CStruct, *, owner: str | None) -> SemanticType: ) def _union_type(self, union: CUnion, *, owner: str | None) -> SemanticType: + """Return a named semantic union reference after consulting the file/project registry.""" if union.name and union.name in self.unions: union = self.unions[union.name] name = self._union_name(union) @@ -805,6 +916,7 @@ def _union_type(self, union: CUnion, *, owner: str | None) -> SemanticType: ) def _enum_type(self, enum: CEnum) -> SemanticType: + """Lower an enum reference to its underlying integer type with enum provenance.""" enum = self._resolved_enum(enum) underlying_type = self._enum_underlying_type(enum) underlying_type.metadata.update( @@ -820,6 +932,7 @@ def _enum_type(self, enum: CEnum) -> SemanticType: return underlying_type def _enum_underlying_type(self, enum: CEnum) -> SemanticType: + """Return compiler-probed enum storage or the documented C ``int`` assumption.""" fact = self.standard_type_facts.get(enum.reference_name) if fact is not None and fact.get("available", True): dtype = self._semantic_type_from_standard_fact(fact) or "Int" @@ -846,6 +959,12 @@ def _pointer_type( pointee_type: CType, source_type: CType, ) -> SemanticType: + """Apply C pointer depth, qualifiers, and aliasing facts to a pointee type in place. + + The returned object is ``pointee`` with borrowed reference/pointer + storage. Pointee ``const`` controls mutability and ``restrict`` controls + aliasing; no ownership-transfer policy is inferred. + """ pointer_depth = len(pointer_components) read_only = self._has_qualifier(pointee_type, CConst) pointer_qualifiers = [ @@ -876,6 +995,12 @@ def _array_type( source_type: CType, owner: str | None, ) -> SemanticType: + """Apply C array shape, C-order storage, and element constness to ``element``. + + The supplied element type is mutated with rank, shape, and borrowed + array storage. Array-component metadata preserves static, variable, + and flexible bound facts for later semantic consumers. + """ shape = [self._array_bound(component) for component in array_components] rank = len(array_components) read_only = self._has_qualifier(self._array_element_type(source_type), CConst) @@ -905,7 +1030,15 @@ def _array_type( ) return element + # Constant and module metadata helpers + def _enum_constants_for_enum(self, enum: CEnum) -> list[SemanticVariable]: + """Convert enum members into ordered constant semantic variables. + + Implicit values continue from a parseable integer predecessor. Native + expressions are always preserved, while a Python initializer is stored + only when the expression is valid and representable in a ``.pyi`` file. + """ variables: list[SemanticVariable] = [] enum = self._resolved_enum(enum) next_value: int | None = 0 @@ -950,9 +1083,16 @@ def _enum_constants_for_enum(self, enum: CEnum) -> list[SemanticVariable]: return variables def _macro_constants(self, c_file: CFile) -> list[SemanticVariable]: + """Convert eligible object-like macros declared by one translation unit.""" return self._macro_constants_from_macros(c_file.macros) def _macro_constants_from_macros(self, macros: list[CMacro]) -> list[SemanticVariable]: + """Resolve numeric object-like macros into ordered constant semantic variables. + + The first pass iterates to a fixed point so macros can refer to earlier + or later resolvable macro names. Function-like and nonnumeric macros + are intentionally excluded from the semantic module. + """ macro_types: dict[str, str] = {} pending = [macro for macro in macros if not macro.function_like and macro.value is not None] changed = True @@ -999,6 +1139,7 @@ def _macro_constants_from_macros(self, macros: list[CMacro]) -> list[SemanticVar @staticmethod def _integer_macro_expression(value: str, macro_types: dict[str, str]) -> bool: + """Return whether a macro expression is limited to known integer syntax and names.""" if not _INTEGER_EXPRESSION_CHARS_RE.fullmatch(value): return False identifiers = set(_C_IDENTIFIER_TOKEN_RE.findall(value)) @@ -1018,6 +1159,7 @@ def _integer_macro_expression(value: str, macro_types: dict[str, str]) -> bool: @staticmethod def _pyi_integer_expression(value: str | None) -> str | None: + """Return a Python-valid integer expression for a native macro value, if safe.""" if value is None or not _INTEGER_EXPRESSION_CHARS_RE.fullmatch(value): return None normalized = _C_INTEGER_LITERAL_SUFFIX_RE.sub(r"\1", value) @@ -1035,6 +1177,7 @@ def _pyi_integer_expression(value: str | None) -> str | None: return ast.unparse(expression.body) def _file_metadata(self, c_file: CFile) -> dict[str, Any]: + """Return the stable summary metadata attached to one semantic C module.""" metadata: dict[str, Any] = { "source_language": "c", "counts": { @@ -1053,6 +1196,7 @@ def _file_metadata(self, c_file: CFile) -> dict[str, Any]: @staticmethod def _private_recipe_paths(c_file: CFile) -> set[str]: + """Extract preprocessing-recipe include paths marked private to the source unit.""" recipe = c_file.preprocessing_recipe or {} private_paths: set[str] = set() for item in recipe.get("included_files") or []: @@ -1065,17 +1209,27 @@ def _private_recipe_paths(c_file: CFile) -> set[str]: @staticmethod def _source_filename(location: dict[str, Any] | None) -> str | None: + """Read a string filename from serialized source-location metadata.""" if not isinstance(location, dict): return None filename = location.get("filename") return filename if isinstance(filename, str) else None + # Include exposure and cross-module aggregate identity + def _apply_include_exposure(self, module: SemanticModule, c_file: CFile) -> None: + """Apply private-include visibility to module declarations in place. + + Functions and variables from private included files become private. + Private classes become opaque with their fields removed, preserving an + addressable dependency identity without exposing private layout. + """ private_paths = self._private_recipe_paths(c_file) if not private_paths: return def is_private_origin(origin: SemanticOrigin) -> bool: + """Return whether an origin filename appears in this file's private include set.""" filename = self._source_filename(origin.source_location) return filename in private_paths @@ -1095,6 +1249,12 @@ def is_private_origin(origin: SemanticOrigin) -> bool: cls.base_classes.append("Opaque") def _externalize_private_classes(self, module: SemanticModule) -> None: + """Mark references to foreign private opaque classes as external in place. + + Only opaque private classes whose source filename maps to another module + are tracked. The existing type walk then gives consumers an external + reference instead of retaining a local duplicate definition. + """ external_classes: dict[str, str] = {} for cls in module.classes: if not isinstance(cls, SemanticClass): @@ -1126,6 +1286,12 @@ def _classify_project_external_types( modules: list[SemanticModule], project: CProject, ) -> None: + """Attach owner-module metadata to aggregate references outside their defining file. + + Struct ownership is derived from source locations in the project + registry. The method mutates consumer type metadata and removes their + duplicate class declarations while leaving each owner module unchanged. + """ modules_by_filename = { module.origin.native_name: module for module in modules if module.origin.native_name is not None } @@ -1161,6 +1327,7 @@ def _set_external_type_ref( origin_module: str, wrapped: bool, ) -> None: + """Store the canonical wrapped/opaque external-type reference metadata in place.""" semantic_type.metadata[EXTERNAL_TYPE_REF_METADATA] = { "name": semantic_type.name, "local_name": semantic_type.name, @@ -1170,6 +1337,7 @@ def _set_external_type_ref( } def _project_metadata(self, project: CProject) -> dict[str, Any]: + """Return stable language and aggregate-count metadata for a merged project module.""" metadata: dict[str, Any] = { "source_language": "c", "counts": { @@ -1186,7 +1354,15 @@ def _project_metadata(self, project: CProject) -> dict[str, Any]: } return metadata + # Type lookup, target facts, and naming helpers + def _resolve_typedef(self, typedef: CTypedef, stack: tuple[str, ...] = ()) -> CTypedef | None: + """Resolve typedef aliases through the current registry without following cycles. + + ``stack`` records names already visited. A cycle, missing entry, or + non-typedef target returns ``None`` so callers preserve the existing + unresolved-type semantic representation. + """ if typedef.type is not None: return typedef target = self.typedefs.get(typedef.name) @@ -1197,6 +1373,12 @@ def _resolve_typedef(self, typedef: CTypedef, stack: tuple[str, ...] = ()) -> CT return target def _standard_semantic_type(self, name: str) -> SemanticType | None: + """Return a probe-aware semantic type for a recognized standard C typedef name. + + Referenced opaque handles are recorded so the enclosing module can emit + a matching opaque class; unknown names return ``None`` for the typedef + caller's ordinary unresolved-type path. + """ fact = self.standard_type_facts.get(name) if fact is not None: if fact.get("available", True) and fact.get("kind") == "opaque_handle": @@ -1224,12 +1406,14 @@ def _standard_semantic_type(self, name: str) -> SemanticType | None: ) def _c_int_fact(self) -> tuple[dict[str, Any], str]: + """Return the configured C ``int`` fact or the historical fallback and provenance.""" fact = self.standard_type_facts.get("int") if fact is not None and fact.get("available", True): return dict(fact), "compiler_probe" return dict(_C_INT_FALLBACK_FACT), "fallback" def _opaque_standard_type_classes(self) -> list[SemanticClass]: + """Create deterministic opaque classes for referenced standard handle types.""" return [ SemanticClass( name=name, @@ -1248,6 +1432,7 @@ def _opaque_standard_type_classes(self) -> list[SemanticClass]: @staticmethod def _semantic_type_from_standard_fact(fact: dict[str, Any]) -> str | None: + """Map one available compiler-reported standard type fact to a known semantic dtype.""" if not fact.get("available", True): return None if fact.get("kind") == "opaque_handle": @@ -1259,7 +1444,14 @@ def _semantic_type_from_standard_fact(fact: dict[str, Any]) -> str | None: if fact.get("signed") is True: return _SIGNED_WIDTH_TYPES.get(bits) if fact.get("kind") == "bool": - return "Bool" + return next( + ( + name + for name, storage_bits in BOOLEAN_STORAGE_BITS.items() + if name != "Bool" and storage_bits == bits + ), + None, + ) if fact.get("kind") == "real": return _REAL_WIDTH_TYPES.get(bits) if fact.get("kind") == "complex": @@ -1268,6 +1460,11 @@ def _semantic_type_from_standard_fact(fact: dict[str, Any]) -> str | None: @staticmethod def _standard_type_facts(report: Any | None) -> dict[str, dict[str, Any]]: + """Normalize a probe report or mapping into copied per-standard-type facts. + + Missing or malformed reports yield an empty lookup. Fact dictionaries + are copied so caller-owned reports cannot be mutated during conversion. + """ if report is None: return {} if hasattr(report, "types"): @@ -1289,6 +1486,11 @@ def _unresolved_type( code: str = "c_unresolved_type", message: str = "C type references must resolve before wrapping.", ) -> SemanticType: + """Represent an unresolved C spelling without fabricating a semantic mapping. + + ``code``, ``message``, and ``owner`` are accepted for callers' stable + diagnostics but intentionally do not alter this historical IR shape. + """ return SemanticType( name=name, dtype=name, @@ -1304,6 +1506,11 @@ def _unsupported_type( owner: str | None, source_type: str, ) -> SemanticType: + """Return the existing explicit placeholder for unsupported C composition. + + Diagnostic context parameters are deliberately not serialized here; the + resulting origin retains the native source spelling for later reporting. + """ return SemanticType( name="CUnsupported", dtype="CUnsupported", @@ -1312,6 +1519,7 @@ def _unsupported_type( ) def _callback_placeholder(self, type_: CType) -> SemanticType: + """Return the function-pointer semantic placeholder with source-type provenance.""" return SemanticType( name="CFunctionPointer", dtype="CFunctionPointer", @@ -1325,6 +1533,7 @@ def _callback_placeholder(self, type_: CType) -> SemanticType: @staticmethod def _module_name(c_file: CFile) -> str: + """Derive the stable semantic module name for a translation unit or unnamed fallback.""" if c_file.filename: return CToIRConverter._module_name_for_filename(c_file.filename) stem = "c_module" @@ -1332,10 +1541,12 @@ def _module_name(c_file: CFile) -> str: @staticmethod def _module_name_for_filename(filename: str) -> str: + """Normalize a source filename stem into a valid semantic module identifier.""" return CToIRConverter._identifier(Path(filename).stem or "c_module") @staticmethod def _identifier(name: str) -> str: + """Convert arbitrary native text into the stable nonempty semantic identifier form.""" text = _IDENTIFIER_RE.sub("_", str(name)).strip("_") if not text: text = "anonymous" @@ -1344,24 +1555,28 @@ def _identifier(name: str) -> str: return text def _struct_name(self, struct: CStruct) -> str: + """Choose a struct's explicit name, typedef alias, or anonymous fallback spelling.""" if struct.name: return self._identifier(struct.name) alias = self._typedef_alias_for_type(struct) return self._identifier(alias or struct.anonymous_id or "anonymous_struct") def _union_name(self, union: CUnion) -> str: + """Choose a union's explicit name, typedef alias, or anonymous fallback spelling.""" if union.name: return self._identifier(union.name) alias = self._typedef_alias_for_type(union) return self._identifier(alias or union.anonymous_id or "anonymous_union") def _enum_name(self, enum: CEnum) -> str: + """Choose an enum's explicit name, typedef alias, or anonymous fallback spelling.""" if enum.name: return self._identifier(enum.name) alias = self._typedef_alias_for_type(enum) return self._identifier(alias or enum.anonymous_id or "anonymous_enum") def _nested_aggregate_name(self, field_name: str, used_names: set[str]) -> str: + """Return a deterministic unused semantic class name for one nested aggregate field.""" base = self._identifier(field_name) candidate = self._identifier(f"{base}_type") index = 1 @@ -1371,12 +1586,14 @@ def _nested_aggregate_name(self, field_name: str, used_names: set[str]) -> str: return candidate def _resolved_enum(self, enum: CEnum) -> CEnum: + """Return the registry definition for a named enum when one is available.""" if enum.name and enum.name in self.enums: return self.enums[enum.name] return enum @staticmethod def _project_enum_declarations(project: CProject) -> list[CEnum]: + """Return project enums once, including anonymous declarations stored only on files.""" declarations = list(project.enums.values()) anonymous_ids: set[str | int] = {enum.anonymous_id or id(enum) for enum in declarations if enum.name is None} for c_file in project.files.values(): @@ -1391,6 +1608,7 @@ def _project_enum_declarations(project: CProject) -> list[CEnum]: return declarations def _typedef_alias_for_type(self, target: CType) -> str | None: + """Find the first registry typedef whose target is the same parser type object.""" for typedef in self.typedefs.values(): if typedef.type is target: return typedef.name @@ -1398,6 +1616,7 @@ def _typedef_alias_for_type(self, target: CType) -> str | None: @staticmethod def _leading_components(components: list[CType], cls: type) -> list: + """Return the consecutive leading declarator components of ``cls`` from ``components``.""" out = [] for component in components: if not isinstance(component, cls): @@ -1407,30 +1626,36 @@ def _leading_components(components: list[CType], cls: type) -> list: @staticmethod def _has_component(components: list[CType], cls: type) -> bool: + """Return whether any declarator component is an instance of ``cls``.""" return any(isinstance(component, cls) for component in components) @staticmethod def _contains_function_type(type_: CComposedType) -> bool: + """Return whether a composed declarator contains a C function type component.""" return any(isinstance(component, CFunctionType) for component in type_.components) @staticmethod def _array_bound(array: CArray) -> str: + """Return an array bound or the established ``:`` marker for an omitted bound.""" if array.bound: return array.bound return ":" @staticmethod def _array_element_type(source_type: CType) -> CType: + """Return the innermost parser type used to determine array element qualifiers.""" if isinstance(source_type, CComposedType) and source_type.components: return source_type.components[-1] return source_type @staticmethod def _has_qualifier(type_: CType, qualifier_type: type[CQualifier]) -> bool: + """Return whether a parsed type directly declares ``qualifier_type``.""" return any(isinstance(qualifier, qualifier_type) for qualifier in getattr(type_, "qualifiers", [])) @staticmethod def _integer_literal_value(value: str | None) -> int | None: + """Parse one C integer literal with suffixes, returning ``None`` for expressions.""" if value is None: return None cleaned = re.sub(r"[uUlL]+\Z", "", value.strip()) @@ -1441,6 +1666,7 @@ def _integer_literal_value(value: str | None) -> int | None: @staticmethod def _type_text(type_: CType) -> str: + """Return preserved source spelling when available, otherwise a stable model spelling.""" source_text = getattr(type_, "source_text", "") if source_text: return source_text @@ -1450,6 +1676,7 @@ def _type_text(type_: CType) -> str: @staticmethod def _type_metadata(type_: CType) -> dict[str, Any]: + """Return parser model kind and direct qualifier facts for a semantic type origin.""" qualifiers = [qualifier.spelling for qualifier in getattr(type_, "qualifiers", [])] metadata: dict[str, Any] = {"c_type": type(type_).__name__} if qualifiers: @@ -1458,6 +1685,7 @@ def _type_metadata(type_: CType) -> dict[str, Any]: @staticmethod def _type_origin(type_: CType, *, native_name: str | None = None) -> SemanticOrigin: + """Build C type provenance with an optional native declaration name.""" return SemanticOrigin( source_language="c", native_name=native_name, @@ -1468,6 +1696,7 @@ def _type_origin(type_: CType, *, native_name: str | None = None) -> SemanticOri @staticmethod def _location_dict(location) -> dict[str, Any]: + """Serialize the populated fields of a parser source location, or return an empty mapping.""" if location is None: return {} return { @@ -1487,6 +1716,16 @@ def c_type_to_semantic_type( *, standard_type_report: Any | None = None, ) -> SemanticType: + """Convert one parsed C type into its language-neutral semantic type. + + Use this for type-only inspection or when constructing a semantic binding + yourself. Supply a standard-type probe report when target ABI widths must + replace the documented fallback mapping. + + Example: + >>> c_type_to_semantic_type(CInt()).name + 'Int' + """ return CToIRConverter(standard_type_report=standard_type_report).visit(type_, as_type=True) @@ -1496,6 +1735,12 @@ def c_parameter_to_semantic_argument( position: int = 0, standard_type_report: Any | None = None, ) -> SemanticArgument: + """Convert one parsed C parameter into a semantic argument. + + ``position`` becomes the native argument index and determines the stable + fallback name for an anonymous parameter. Target type facts are consumed + the same way as in function and file conversion. + """ return CToIRConverter(standard_type_report=standard_type_report).visit( parameter, position=position, @@ -1507,6 +1752,12 @@ def c_function_to_semantic_function( *, standard_type_report: Any | None = None, ) -> SemanticFunction: + """Convert one parsed C function declaration into a semantic callable contract. + + The result keeps native parameter order, C storage/specifier facts, return + type, projection, and source provenance. Use file conversion when related + typedefs, aggregates, enums, or macros must also be resolved. + """ return CToIRConverter(standard_type_report=standard_type_report).visit(function) @@ -1515,6 +1766,12 @@ def c_struct_to_semantic_class( *, standard_type_report: Any | None = None, ) -> SemanticClass: + """Convert one parsed C struct into a semantic class with fields and nested aggregates. + + Use this for a self-contained struct model. File/project conversion is + preferable when a named struct reference must resolve through shared parser + registries or be classified as externally owned. + """ return CToIRConverter(standard_type_report=standard_type_report).visit(struct) @@ -1523,6 +1780,17 @@ def c_file_to_semantic_module( *, standard_type_report: Any | None = None, ) -> SemanticModule: + """Convert one parsed C translation unit into its semantic module. + + Use this normal C source-to-IR entrypoint after parsing. It converts + declarations and constants, resolves local parser registries, and applies + preprocessing include exposure; it does not run probes or policy completion. + + Example: + >>> parsed = CFile(filename="math.h", functions=[CFunction(name="add", result_type=CInt())]) + >>> c_file_to_semantic_module(parsed).name + 'math' + """ return CToIRConverter(standard_type_report=standard_type_report).visit(parsed_file) @@ -1531,6 +1799,12 @@ def c_file_to_semantic_modules( *, standard_type_report: Any | None = None, ) -> list[SemanticModule]: + """Return the one semantic module produced by a single parsed C file. + + This list-returning compatibility helper is useful to generic source + pipelines that always consume module lists. It preserves the same result + and target-fact behavior as :func:`c_file_to_semantic_module`. + """ return [c_file_to_semantic_module(parsed_file, standard_type_report=standard_type_report)] @@ -1539,6 +1813,17 @@ def c_project_to_semantic_modules( *, standard_type_report: Any | None = None, ) -> list[SemanticModule]: + """Convert every parsed C project file into an ordered semantic module list. + + Use this for multi-header input, where type ownership must be assigned to + defining modules and consumer modules retain external references. Project + files are emitted in stable filename order. + + Example: + >>> project = CProject(files={"math.h": CFile(filename="math.h")}) + >>> [module.name for module in c_project_to_semantic_modules(project)] + ['math'] + """ return CToIRConverter(standard_type_report=standard_type_report).visit(project) @@ -1548,6 +1833,12 @@ def c_project_to_semantic_module( name: str = "c_project", standard_type_report: Any | None = None, ) -> SemanticModule: + """Merge project registries into one synthetic semantic module. + + Use this compatibility entrypoint when consumers require one aggregate + module rather than file-level ownership and external references. ``name`` + is normalized into a semantic identifier; the project itself is not mutated. + """ return CToIRConverter(standard_type_report=standard_type_report).project_to_semantic_module( project, name=name, @@ -1565,3 +1856,26 @@ def c_project_to_semantic_module( "c_struct_to_semantic_class", "c_type_to_semantic_type", ) + + +if __name__ == "__main__": + from prik.parsers.c.models import CFile, CFunction, CInt, CParameter + + parsed_file = CFile( + filename="math.h", + functions=[ + CFunction( + name="scale", + result_type=CInt(), + parameters=[CParameter(name="value", type=CInt())], + ) + ], + ) + semantic_module = c_file_to_semantic_module(parsed_file) + semantic_function = semantic_module.functions[0] + semantic_argument = semantic_function.arguments[0] + print( + f"{semantic_module.name}.{semantic_function.name}" + f"({semantic_argument.name}): {semantic_function.return_type.name}" + f" <- {semantic_argument.semantic_type.name}" + ) diff --git a/prik/semantics/fortran2ir.py b/prik/semantics/fortran2ir.py index b273b96e6..fc5324864 100644 --- a/prik/semantics/fortran2ir.py +++ b/prik/semantics/fortran2ir.py @@ -1,6 +1,14 @@ +"""Convert parsed Fortran facts into language-neutral semantic IR. + +The public helpers at the end of this module accept parsed Fortran module, +file, or project models and return :class:`~prik.semantics.models.SemanticModule` +objects. They normalize types, storage, visibility, imported derived types, +and procedure projections. Ownership and wrapper-generation policy are +deliberately completed by later semantic stages. +""" + from __future__ import annotations -import ast from collections.abc import Iterable from copy import deepcopy from dataclasses import dataclass @@ -22,12 +30,21 @@ FortranUseMapping, FortranVariable, ) +from prik.utilities.declaration_expressions import ( + ArrayExpressionSource, + canonicalize_declaration_extent, + declaration_expression_calls, + fortran_extent_to_python, + is_declaration_expression_helper, + split_dimension_bounds, + split_top_level_expression, +) from prik.semantics.ownership import set_ownership_metadata from prik.semantics.metadata import BIND_TARGET_METADATA, PROJECTED_OUTPUT_METADATA, SCALAR_STORAGE_CATEGORY -from prik.types.numpy import SEMANTIC_SCALAR_TYPE_NAMES +from prik.types.numpy import BOOLEAN_STORAGE_BITS, SEMANTIC_SCALAR_TYPE_NAMES, is_boolean_semantic_type_name from prik.utilities.visitor import ClassVisitor -from .models import ( +from prik.semantics.models import ( EXTERNAL_TYPE_REF_METADATA, FORTRAN_GENERIC_NAME_METADATA, OVERLOAD_KIND_METADATA, @@ -36,11 +53,13 @@ PYTHON_BOUND_POSITION_METADATA, PYTHON_METHOD_NAME_METADATA, PYTHON_STATIC_METADATA, + PROTOTYPE_INTENT_METADATA, PROTOTYPE_REF_METADATA, SemanticArgument, SemanticArrayContract, SemanticClass, SemanticConstraint, + SemanticExpressionCallable, SemanticField, SemanticFunction, SemanticImport, @@ -125,14 +144,25 @@ _FORTRAN_INTRINSIC_TYPES = frozenset({"integer", "real", "complex", "logical", "character"}) _FORTRAN_STORAGE_PROBE_TYPES = frozenset({"integer", "real", "complex", "logical"}) _FORTRAN_STORAGE_TYPE_MAP = { + "logical": {bits: name for name, bits in BOOLEAN_STORAGE_BITS.items() if name != "Bool"}, "integer": {8: "Int8", 16: "Int16", 32: "Int32", 64: "Int64"}, "real": {32: "Float32", 64: "Float64", 80: "Float128", 96: "Float128", 128: "Float128"}, "complex": {64: "Complex64", 128: "Complex128", 160: "Complex256", 192: "Complex256", 256: "Complex256"}, } +# Internal conversion context + + @dataclass(frozen=True) class _DerivedTypeContext: + """Keep lexical derived-type lookup facts while one parser node is converted. + + The context records the owning module, imported names, procedure-local + imports, and locally declared type names. It is immutable so nested + visitors can safely derive narrower contexts without mutating their parent. + """ + module: str | None = None uses: dict[str, list[FortranUseMapping]] | None = None procedure_uses: dict[str, list[FortranUseMapping]] | None = None @@ -141,11 +171,27 @@ class _DerivedTypeContext: @dataclass(frozen=True) class _ResolvedDerivedTypeOrigin: + """Describe the module and spelling selected for an imported derived type. + + ``import_scope`` records when a procedure-local ``use`` made the selected + name visible, which determines whether the public semantic name is scoped. + """ + module: str | None name: str import_scope: str | None = None +@dataclass(frozen=True) +class _DeclarationCallableContext: + """Hold lexical procedure names and imports for expression-call resolution.""" + + module: str | None + local_procedures: dict[str, SemanticFunction] + local_interfaces: dict[str, SemanticPrototype] + uses: dict[str, list[FortranUseMapping]] + + def _normalize_compile_time_values( compile_time_values: dict[str, int | str] | None, ) -> dict[str, str]: @@ -189,6 +235,7 @@ def _resolve_compile_time_text(text: str, compile_time_values: dict[str, str]) - return exact def replace_symbol(match: re.Match[str]) -> str: + """Replace one identifier only when the supplied values define it.""" token = match.group(0) return compile_time_values.get(token.lower(), token) @@ -209,18 +256,33 @@ def __init__( wrapped_derived_types: Iterable[tuple[str, str]] | None = None, type_facts: dict[tuple[str, str | None], dict[str, object]] | None = None, ): + """Configure parser-fact conversion without performing any conversion. + + ``type_map`` overrides intrinsic kind-to-semantic-name mapping; + ``compile_time_values`` resolves parser-preserved expressions; + ``wrapped_derived_types`` marks imported types with generated wrappers; + and ``type_facts`` supplies compiler-measured storage facts. Inputs are + normalized into lookup-friendly forms and retained for later visitors. + """ self.type_map = FORTRAN_TYPE_MAP if type_map is None else type_map self.compile_time_values = _normalize_compile_time_values(compile_time_values) self.wrapped_derived_types = { (str(module).lower(), str(name).lower()) for module, name in (wrapped_derived_types or []) } + self._known_procedures: set[tuple[str, str]] = set() self.type_facts = { (str(base_type).lower(), None if kind is None else str(kind).lower()): dict(fact) for (base_type, kind), fact in (type_facts or {}).items() } def visit(self, node, **context): - """Dispatch one parsed Fortran model through its class visitor.""" + """Convert one supported parsed model using the shared class visitor. + + Call this for a specific parser model when the convenience module, + file, or project helpers do not match the desired input shape. The + result type follows ``node``; unsupported model classes raise + :class:`TypeError`. + """ return self._visit(node, **context) @staticmethod @@ -229,7 +291,13 @@ def _visit_not_supported(node): raise TypeError(f"Unsupported Fortran parse object: {type(node)!r}") def first_module(self, parsed): - """Accept a FortranModule, FortranFile, or legacy signature list in tests.""" + """Select one module from supported single-module conversion inputs. + + A :class:`FortranModule` is returned unchanged, a file contributes its + first module, and the legacy signature-list form becomes a synthetic + module. Empty files and unsupported shapes retain their established + :class:`ValueError` and :class:`TypeError` failures. + """ if isinstance(parsed, FortranModule): return parsed if isinstance(parsed, FortranFile): @@ -242,13 +310,22 @@ def first_module(self, parsed): return FortranModule(name=module_name or "", procedures=procedures) raise TypeError(f"Unsupported Fortran parse object: {type(parsed)!r}") + # Container visitors + def _visit_FortranFile( self, parsed_file: FortranFile, *, standalone_module_name: str | None = None, ) -> 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. + """ 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)) modules = [converter.visit(module) for module in parsed_file.modules] if parsed_file.procedures: modules.append( @@ -261,7 +338,14 @@ def _visit_FortranFile( return modules def _visit_FortranProject(self, project: FortranProject) -> list[SemanticModule]: + """Convert project files in order with project-wide type and callback context. + + Each file receives the known project type set plus its own declarations, + while imported callback interfaces are resolved against the project. + The returned module ordering matches the input file and parser order. + """ converter = self._with_additional_wrapped_types(self._wrapped_types_from_project(project)) + converter = converter._with_additional_known_procedures(self._known_procedures_from_project(project)) semantic_modules = [] for parsed_file in project.files: file_converter = converter._with_additional_wrapped_types(converter._wrapped_types_from_file(parsed_file)) @@ -282,34 +366,58 @@ def _visit_FortranProject(self, project: FortranProject) -> list[SemanticModule] ) return semantic_modules + # Variable and argument visitors + def _visit_FortranVariable( self, var: FortranVariable, *, derived_type_context: _DerivedTypeContext | None = None, + declaration_arrays: dict[str, ArrayExpressionSource] | None = None, as_type: bool = False, as_data_member: bool = False, binding_cls: type[SemanticVariable] = SemanticVariable, source_kind: str = "variable", ) -> SemanticType | SemanticVariable: - """Convert one parsed variable through the class visitor protocol.""" + """Convert a parser variable as a type or a semantic data member. + + ``as_type`` returns the normalized :class:`SemanticType`; ``as_data_member`` + produces the requested semantic variable subtype with origin metadata. + The default preserves the historical type-only conversion path. + """ if as_type: - return self._convert_variable_type(var, derived_type_context=derived_type_context) + return self._convert_variable_type( + var, + derived_type_context=derived_type_context, + declaration_arrays=declaration_arrays, + ) if as_data_member: return self._convert_data_member( var, derived_type_context=derived_type_context, binding_cls=binding_cls, source_kind=source_kind, + declaration_arrays=declaration_arrays, ) - return self._convert_variable_type(var, derived_type_context=derived_type_context) + return self._convert_variable_type( + var, + derived_type_context=derived_type_context, + declaration_arrays=declaration_arrays, + ) def _convert_variable_type( self, var: FortranVariable, *, derived_type_context: _DerivedTypeContext | None = None, + declaration_arrays: dict[str, ArrayExpressionSource] | None = None, ) -> SemanticType: + """Build a semantic datatype, storage contract, and source metadata for ``var``. + + This is the intrinsic/derived-type normalization stage. It resolves + known compile-time text, applies measured type facts, and records + source storage facts without deciding downstream ownership policy. + """ semantic_name = self._semantic_type_name(var) derived_type_ref = self._derived_type_ref(var, derived_type_context) metadata = {} @@ -334,7 +442,7 @@ def _convert_variable_type( metadata["fortran_pointer_association"] = "runtime" shape = [self._resolve_compile_time_text(dim) for dim in var.shape] if var.rank > 0: - storage = self._array_storage_contract(var, shape) + storage = self._array_storage_contract(var, shape, declaration_arrays=declaration_arrays) elif getattr(var, "pointer", False): storage = SemanticStorageContract(kind="reference", pointer_depth=1) else: @@ -352,6 +460,12 @@ def _convert_variable_type( return semantic_type def _character_length(self, var: FortranVariable) -> str: + """Return the resolved character length recorded by a parsed declaration. + + The helper reads the parser's mixed kind/length spelling, preferring an + explicit ``len=`` fragment and otherwise preserving legacy length syntax; + declarations with neither continue to use Fortran's length-one default. + """ raw = self._resolve_compile_time_text(str(var.kind or "")).strip() length_match = re.search(r"(?:^|,)\s*len\s*=\s*([^,]+)", raw, re.IGNORECASE) if length_match is not None: @@ -370,15 +484,28 @@ def _visit_FortranArgument( as_type: bool = False, binding_cls: type[SemanticVariable] = SemanticVariable, source_kind: str = "variable", + declaration_arrays: dict[str, ArrayExpressionSource] | None = None, ) -> SemanticArgument | SemanticVariable: + """Convert a dummy argument, including callback and storage facts. + + The visitor returns a type or data member in the explicit modes used by + related visitors; otherwise it produces a :class:`SemanticArgument`. + It mutates the freshly built semantic type with access and storage facts + inferred from the parser declaration before constructing the argument. + """ if as_type: - return self._convert_variable_type(arg, derived_type_context=derived_type_context) + return self._convert_variable_type( + arg, + derived_type_context=derived_type_context, + declaration_arrays=declaration_arrays, + ) if as_data_member: return self._convert_data_member( arg, derived_type_context=derived_type_context, binding_cls=binding_cls, source_kind=source_kind, + declaration_arrays=declaration_arrays, ) if arg.base_type.lower() == "procedure": semantic_type = self._callback_semantic_type( @@ -387,7 +514,11 @@ def _visit_FortranArgument( derived_type_context=derived_type_context, ) else: - semantic_type = self._convert_variable_type(arg, derived_type_context=derived_type_context) + semantic_type = self._convert_variable_type( + arg, + derived_type_context=derived_type_context, + declaration_arrays=declaration_arrays, + ) access = self._argument_access(arg, semantic_type) if semantic_type.storage is not None and semantic_type.storage.kind == "callback": pass @@ -406,7 +537,7 @@ def _visit_FortranArgument( metadata = {} if getattr(arg, "pass_by_value", False) and str(getattr(arg, "base_type", "")).casefold() == "derived": metadata[NATIVE_BY_VALUE_METADATA] = True - return SemanticArgument( + argument = SemanticArgument( name=arg.name, semantic_type=semantic_type, optional=getattr(arg, "optional", False), @@ -414,6 +545,11 @@ def _visit_FortranArgument( metadata=metadata, origin=self._argument_origin(arg), ) + # Source access is an internal optimization fact, not part of the + # serialized semantic contract. It lets policy omit a useless copy-in + # for intent(out) arrays without changing scalar ownership behavior. + argument._source_reads_argument = access[0] + return argument def _convert_data_member( self, @@ -422,8 +558,19 @@ def _convert_data_member( derived_type_context: _DerivedTypeContext | None = None, binding_cls: type[SemanticVariable] = SemanticVariable, source_kind: str = "variable", + declaration_arrays: dict[str, ArrayExpressionSource] | None = None, ) -> SemanticVariable: - semantic_type = self._convert_variable_type(var, derived_type_context=derived_type_context) + """Create a module variable or derived-type field from a parsed declaration. + + The result carries normalized type, initializer, visibility, and source + origin facts. Array storage receives allocation and pointer facts from + the declaration; no wrapper accessor or ownership policy is chosen here. + """ + semantic_type = self._convert_variable_type( + var, + derived_type_context=derived_type_context, + declaration_arrays=declaration_arrays, + ) if semantic_type.storage is not None and semantic_type.storage.array is not None: semantic_type.storage.array.allocatable = getattr(var, "allocatable", False) semantic_type.storage.array.pointer = getattr(var, "pointer", False) @@ -485,6 +632,13 @@ def _callback_semantic_type( *, derived_type_context: _DerivedTypeContext | None, ) -> SemanticType: + """Build the callback semantic type for a procedure dummy when resolvable. + + A pointer dummy or unknown interface follows ordinary variable + conversion. For a known interface, the helper converts its arguments + and result into prototype metadata and normalizes non-value callback + dummies to writable references. + """ 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) @@ -497,6 +651,7 @@ def _callback_semantic_type( 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) callback_return = ( self.visit(signature.result, derived_type_context=context, as_type=True) if signature.result @@ -568,19 +723,30 @@ def _normalize_callback_reference_storage( semantic_type.storage.mutable = True semantic_type.ownership.mutable = True + @staticmethod + def _record_prototype_argument_intent( + argument: SemanticArgument, + source_argument: FortranArgument | FortranVariable, + ) -> None: + """Retain exact dummy direction only inside an interface prototype.""" + intent = getattr(source_argument, "intent", None) + if intent is not None: + argument.origin.metadata[PROTOTYPE_INTENT_METADATA] = intent + def _module_prototypes( self, module: FortranModule, context: _DerivedTypeContext, referenced: set[str], + called: set[str], ) -> list[SemanticPrototype]: - """Convert abstract and callback-local interfaces into semantic prototypes.""" + """Convert every referenced interface into one exact prototype signature.""" prototypes: list[SemanticPrototype] = [] seen: set[str] = set() for interface in module.interfaces: for signature in interface.procedures: name = interface.name if interface.name and len(interface.procedures) == 1 else signature.name - if not interface.abstract and name.casefold() not in referenced: + if not (interface.abstract or name.casefold() in referenced or name.casefold() in called): continue if name in seen: continue @@ -588,6 +754,7 @@ def _module_prototypes( arguments = [self.visit(item, derived_type_context=context) for item in signature.arguments] for source_argument, argument in zip(signature.arguments, arguments, strict=True): self._normalize_callback_reference_storage(argument, source_argument) + self._record_prototype_argument_intent(argument, source_argument) return_type = ( self.visit(signature.result, derived_type_context=context, as_type=True) if signature.result is not None @@ -606,13 +773,41 @@ def _module_prototypes( native_name=name, native_scope=module.name, source_kind="prototype", + metadata={"fortran_interface_kind": "abstract" if interface.abstract else "explicit"}, ), + pure=any(attribute.casefold() == "pure" for attribute in signature.attributes), ) ) return prototypes + @staticmethod + def _module_declaration_call_names(module: FortranModule) -> set[str]: + """Collect bare call names appearing in module-owned declaration shapes.""" + variables = [ + *getattr(module, "variables", ()), + *(field for derived in module.derived_types for field in derived.fields), + *( + variable + for procedure in module.procedures + for variable in (*procedure.arguments, procedure.result) + if variable is not None + ), + ] + return { + call.casefold() + for variable in variables + for dimension in getattr(variable, "shape", ()) + for call in declaration_expression_calls(str(dimension)) + if call != "" and "." not in call + } + @staticmethod def _visit_FortranEnumerator(enumerator: FortranEnumerator, *, enum: FortranEnum) -> SemanticVariable: + """Convert one enum member into a constant integer semantic variable. + + The output preserves the parser value, visibility, enum identity, and + ``bind(C)`` provenance so printers and later stages can consume it. + """ semantic_type = SemanticType( "Int32", dtype="Int32", @@ -651,17 +846,35 @@ def _visit_FortranProcedureSignature( derived_type_context: _DerivedTypeContext | None = None, callback_interfaces: dict[str, FortranProcedureSignature] | None = None, ) -> SemanticFunction: + """Convert a parsed procedure signature into its callable semantic contract. + + Arguments are reordered only through the existing optional-argument + projection rule, receive derived-type and callback context, and then + drive the returned function's projection. Pointer results retain their + established result-policy metadata. + """ context = self._procedure_derived_type_context(proc, derived_type_context) + declaration_arrays = self._array_expression_sources((*proc.arguments, proc.result)) arguments = [ self.visit( arg, derived_type_context=context, callback_interfaces=callback_interfaces, + declaration_arrays=declaration_arrays, ) for arg in self._projected_procedure_arguments(proc) ] metadata = self._procedure_metadata(proc) - return_type = self.visit(proc.result, derived_type_context=context, as_type=True) if proc.result else None + return_type = ( + self.visit( + proc.result, + derived_type_context=context, + declaration_arrays=declaration_arrays, + as_type=True, + ) + if proc.result + else None + ) if return_type is not None and getattr(proc.result, "pointer", False): self._apply_pointer_result_policy(return_type) return SemanticFunction( @@ -688,6 +901,12 @@ def _visit_FortranDerivedType( *, derived_type_context: _DerivedTypeContext | None = None, ) -> SemanticClass: + """Convert a Fortran derived type into fields, bound methods, and overload sets. + + The returned class preserves component ordering and type attributes, + derives method bindings from already converted procedures, and records + declaration facts for later semantic and printing stages. + """ lookup = procedure_lookup or {} context = derived_type_context or _DerivedTypeContext( module=dtype.module, @@ -717,6 +936,7 @@ def _visit_FortranDerivedType( final_procedures = list(getattr(dtype, "final_procedures", [])) if final_procedures: metadata["fortran_final_procedures"] = final_procedures + declaration_arrays = self._array_expression_sources(dtype.fields) return SemanticClass( name=dtype.name, native_name=dtype.name, @@ -727,6 +947,7 @@ def _visit_FortranDerivedType( derived_type_context=context, binding_cls=SemanticField, source_kind="field", + declaration_arrays=declaration_arrays, ) for field in dtype.fields ], @@ -745,6 +966,7 @@ def _visit_FortranDerivedType( @staticmethod def _derived_type_component_fact(field: FortranArgument) -> dict[str, object]: + """Return the declaration facts retained for one derived-type component.""" return { "name": field.name, "source_type": FortranToIRConverter._fortran_source_type(field), @@ -762,11 +984,22 @@ def _visit_FortranModule( *, callback_interfaces: dict[str, FortranProcedureSignature] | None = None, ) -> SemanticModule: + """Assemble the semantic contents of one parsed Fortran module. + + The method establishes derived-type and callback context, converts + procedures before classes, then assembles interfaces, variables, enum + constants, imports, and visibility. It deliberately records facts only; + later policy completion owns wrapper behavior decisions. + """ context = self._module_derived_type_context(module) callback_interfaces = { **(callback_interfaces or {}), **self._callback_interface_lookup(module), } + source_procedures = [ + *module.procedures, + *self._module_explicit_interface_procedures(module), + ] semantic_functions = [ self.visit( proc, @@ -774,7 +1007,7 @@ def _visit_FortranModule( derived_type_context=context, callback_interfaces=callback_interfaces, ) - for proc in module.procedures + for proc in source_procedures ] callback_prototypes = { argument.semantic_type.name.casefold() @@ -782,8 +1015,21 @@ def _visit_FortranModule( for argument in function.arguments if argument.semantic_type.storage is not None and argument.semantic_type.storage.kind == "callback" } - prototypes = self._module_prototypes(module, context, callback_prototypes) + prototypes = self._module_prototypes( + module, + context, + callback_prototypes, + self._module_declaration_call_names(module), + ) procedure_lookup = {func.name.casefold(): func for func in semantic_functions} + for procedure, function in zip(source_procedures, semantic_functions, strict=True): + callable_context = self._declaration_callable_context( + module, + functions=semantic_functions, + prototypes=prototypes, + uses={**module.uses, **procedure.uses}, + ) + self._record_function_declaration_callables(function, callable_context) semantic_classes = [ self.visit( @@ -795,6 +1041,14 @@ def _visit_FortranModule( ] for semantic_cls in semantic_classes: semantic_cls.visibility = self._symbol_visibility(module, semantic_cls.name) + self._record_class_declaration_callables( + semantic_cls, + self._declaration_callable_context( + module, + functions=semantic_functions, + prototypes=prototypes, + ), + ) overload_sets = self._module_overload_sets( module, @@ -809,14 +1063,30 @@ def _visit_FortranModule( for enum in getattr(module, "enums", []) for enumerator in enum.enumerators ] + representable_variables = [ + var for var in getattr(module, "variables", []) if var.name.casefold() not in common_variables + ] + declaration_arrays = self._array_expression_sources(getattr(module, "variables", [])) module_variables = [ - self.visit(var, as_data_member=True, derived_type_context=context) - for var in getattr(module, "variables", []) - if var.name.casefold() not in common_variables + self.visit( + var, + as_data_member=True, + derived_type_context=context, + declaration_arrays=declaration_arrays, + ) + for var in representable_variables ] for variable in module_variables: if variable.origin.native_scope is None: variable.origin.native_scope = module.name + self._record_declaration_callables( + variable.semantic_type, + self._declaration_callable_context( + module, + functions=semantic_functions, + prototypes=prototypes, + ), + ) return SemanticModule( name=module.name, functions=semantic_functions, @@ -834,6 +1104,50 @@ def _visit_FortranModule( ), ) + def _record_function_declaration_callables( + self, + function: SemanticFunction, + context: _DeclarationCallableContext, + ) -> None: + """Record expression-call provenance for one function's arrays in place.""" + for argument in function.arguments: + self._record_declaration_callables(argument.semantic_type, context) + self._record_declaration_callables(function.return_type, context) + for variable in function.locals: + self._record_declaration_callables(variable.semantic_type, context) + + def _record_class_declaration_callables( + self, + semantic_class: SemanticClass, + context: _DeclarationCallableContext, + ) -> None: + """Record native calls for fields and recursively nested class members.""" + for field in semantic_class.fields: + self._record_declaration_callables(field.semantic_type, context) + for method in semantic_class.methods: + self._record_function_declaration_callables(method, context) + for nested in semantic_class.classes: + self._record_class_declaration_callables(nested, context) + + @staticmethod + def _array_expression_sources( + variables: Iterable[FortranVariable | None], + ) -> dict[str, ArrayExpressionSource]: + """Index declared arrays for inquiry translation within one source scope. + + The helper consumes parser variables, ignores absent/scalar entries, + and returns case-preserving names with rank and source lower bounds. + It does not resolve or mutate declaration expressions. + """ + return { + variable.name: ArrayExpressionSource( + rank=variable.rank, + lower_bounds=tuple(getattr(variable, "lbound", ()) or ()), + ) + for variable in variables + if variable is not None and int(variable.rank or 0) > 0 + } + def procedures_to_semantic_module( self, procedures: list[FortranProcedureSignature], @@ -841,9 +1155,27 @@ def procedures_to_semantic_module( name: str, callback_interfaces: dict[str, FortranProcedureSignature] | 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. + """ + semantic_functions = [self.visit(proc, callback_interfaces=callback_interfaces) 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( + function, + _DeclarationCallableContext( + module=None, + local_procedures=function_lookup, + local_interfaces={}, + uses=dict(procedure.uses), + ), + ) return SemanticModule( name=name, - functions=[self.visit(proc, callback_interfaces=callback_interfaces) for proc in procedures], + functions=semantic_functions, origin=SemanticOrigin( source_language="fortran", source_kind="external_root", @@ -852,6 +1184,7 @@ def procedures_to_semantic_module( @staticmethod def _module_imports(module: FortranModule) -> list[str | SemanticImport]: + """Translate parser ``use`` mappings while preserving parser declaration order.""" imports: list[str | SemanticImport] = [] for module_name, mappings in module.uses.items(): if not mappings: @@ -865,24 +1198,171 @@ def _module_imports(module: FortranModule) -> list[str | SemanticImport]: ) return imports + def _declaration_callable_context( + self, + module: FortranModule, + functions: Iterable[SemanticFunction] = (), + prototypes: Iterable[SemanticPrototype] = (), + *, + uses: dict[str, list[FortranUseMapping]] | None = None, + ) -> _DeclarationCallableContext: + """Build lexical procedure facts for one module-owned declaration.""" + return _DeclarationCallableContext( + module=module.name, + local_procedures={function.name.casefold(): function for function in functions}, + local_interfaces={prototype.name.casefold(): prototype for prototype in prototypes}, + uses=dict(module.uses if uses is None else uses), + ) + + def _record_declaration_callables( + self, + semantic_type: SemanticType | None, + context: _DeclarationCallableContext, + ) -> None: + """Attach native identities for calls in every axis of one array type. + + The helper consumes an already translated semantic shape and mutates + only its array provenance. Public helper calls are omitted unless + normal Fortran name resolution finds a native procedure that shadows + the helper name; unresolved user calls remain explicit references. + """ + storage = semantic_type.storage if semantic_type is not None else None + array = storage.array if storage is not None else None + if array is None: + return + array.expression_callables = [ + self._expression_callable_references(expression, context) for expression in array.shape + ] + + def _expression_callable_references( + self, + expression: str, + context: _DeclarationCallableContext, + ) -> list[SemanticExpressionCallable]: + """Resolve calls in one axis expression without evaluating their bodies.""" + references: list[SemanticExpressionCallable] = [] + for name in declaration_expression_calls(expression): + reference = self._resolve_declaration_callable(name, context) + if reference is not None: + references.append(reference) + elif name != "" and not is_declaration_expression_helper(name): + references.append( + SemanticExpressionCallable( + name=name, + native_name=name.rsplit(".", 1)[-1], + source_language="fortran", + ) + ) + return references + + def _resolve_declaration_callable( + self, + name: str, + context: _DeclarationCallableContext, + ) -> SemanticExpressionCallable | None: + """Resolve one bare call through local declarations and ``USE`` maps.""" + if "." in name: + return None + key = name.casefold() + interface = context.local_interfaces.get(key) + if interface is not None: + is_abstract_source = interface.origin.metadata.get("fortran_interface_kind") == "abstract" + return SemanticExpressionCallable( + name=name, + native_name=interface.native_name or interface.name, + native_scope=context.module if is_abstract_source else None, + source_language="fortran", + placement="abstract" if is_abstract_source else "standalone", + declaration=interface, + ) + + local = context.local_procedures.get(key) + if local is not None: + return SemanticExpressionCallable( + name=name, + native_name=local.native_name or local.name, + native_scope=context.module, + source_language="fortran", + placement="module" if context.module is not None else "standalone", + declaration=local, + ) + + explicit = [ + (module_name, mapping.source) + for module_name, mappings in context.uses.items() + for mapping in mappings + if mapping.local_name.casefold() == key + ] + if len(explicit) == 1: + return SemanticExpressionCallable( + name=name, + native_name=explicit[0][1], + native_scope=explicit[0][0], + source_language="fortran", + placement="module", + ) + if explicit: + return None + + wildcard_modules = [module_name for module_name, mappings in context.uses.items() if not mappings] + known_origins = [ + module_name for module_name in wildcard_modules if (module_name.casefold(), key) in self._known_procedures + ] + if len(known_origins) != 1: + return None + return SemanticExpressionCallable( + name=name, + native_name=name, + native_scope=known_origins[0], + source_language="fortran", + placement="module", + ) + def _with_additional_wrapped_types( self, wrapped_types: Iterable[tuple[str, str]], ) -> FortranToIRConverter: + """Return this converter or a clone that also recognizes ``wrapped_types``. + + The existing converter is reused when the normalized set is unchanged; + otherwise the clone preserves every other conversion configuration. + """ merged = self.wrapped_derived_types | { (str(module).lower(), str(name).lower()) for module, name in wrapped_types } if merged == self.wrapped_derived_types: return self - return FortranToIRConverter( + converter = FortranToIRConverter( type_map=self.type_map, compile_time_values=self.compile_time_values, wrapped_derived_types=merged, type_facts=self.type_facts, ) + converter._known_procedures = set(self._known_procedures) + return converter + + def _with_additional_known_procedures( + self, + procedures: Iterable[tuple[str, str]], + ) -> FortranToIRConverter: + """Return this converter or a clone with more module-procedure identities.""" + merged = self._known_procedures | { + (str(module).casefold(), str(name).casefold()) for module, name in procedures + } + if merged == self._known_procedures: + return self + converter = FortranToIRConverter( + type_map=self.type_map, + compile_time_values=self.compile_time_values, + wrapped_derived_types=self.wrapped_derived_types, + type_facts=self.type_facts, + ) + converter._known_procedures = merged + return converter @staticmethod def _wrapped_types_from_file(parsed_file: FortranFile) -> set[tuple[str, str]]: + """Collect module-qualified derived types declared by one parsed file.""" return { (dtype.module.lower(), dtype.name.lower()) for module in parsed_file.modules @@ -890,12 +1370,50 @@ def _wrapped_types_from_file(parsed_file: FortranFile) -> set[tuple[str, str]]: if dtype.module } + @staticmethod + def _known_procedures_from_file(parsed_file: FortranFile) -> set[tuple[str, str]]: + """Collect module-qualified procedures declared by one parsed file.""" + return {(module.name, procedure.name) for module in parsed_file.modules for procedure in module.procedures} + + @staticmethod + def _known_procedures_from_project(project: FortranProject) -> set[tuple[str, str]]: + """Collect module-qualified procedures known to one parsed project.""" + return {(module.name, procedure.name) for module in project.modules.values() for procedure in module.procedures} + + @staticmethod + def _module_explicit_interface_procedures( + module: FortranModule, + ) -> list[FortranProcedureSignature]: + """Return explicitly public procedures declared by unnamed interfaces. + + An explicit public list makes the module declaration the authoritative + wrapper contract. Other unnamed interface declarations remain + interface-only facts even when a matching implementation is parsed. + """ + public_names = {name.casefold() for name in module.public_symbols} + declared_names = {procedure.name.casefold() for procedure in module.procedures} + procedures: list[FortranProcedureSignature] = [] + for interface in module.interfaces: + if interface.name is not None or interface.abstract: + continue + for procedure in interface.procedures: + name = procedure.name.casefold() + if name in declared_names: + continue + if name not in public_names: + continue + declared_names.add(name) + procedures.append(procedure) + return procedures + @staticmethod def _wrapped_types_from_project(project: FortranProject) -> set[tuple[str, str]]: + """Collect project-known module-qualified derived types for import resolution.""" return {(dtype.module.lower(), dtype.name.lower()) for dtype in project.derived_types.values() if dtype.module} @staticmethod def _module_derived_type_context(module: FortranModule) -> _DerivedTypeContext: + """Create the lexical type lookup context owned by ``module``.""" return _DerivedTypeContext( module=module.name, uses=module.uses, @@ -907,6 +1425,11 @@ def _procedure_derived_type_context( proc: FortranProcedureSignature, parent: _DerivedTypeContext | None, ) -> _DerivedTypeContext: + """Extend a parent type context with one procedure's imports and scope. + + The new context keeps enclosing local types while separating procedure- + local imports, which later controls imported type qualification. + """ uses = dict(parent.uses or {}) if parent is not None else {} uses.update(proc.uses) return _DerivedTypeContext( @@ -921,6 +1444,11 @@ def _procedure_local_uses( proc: FortranProcedureSignature, parent: _DerivedTypeContext | None, ) -> dict[str, list[FortranUseMapping]]: + """Return imports introduced locally by ``proc`` relative to its parent. + + A parser-preserved ``_local_uses`` mapping takes precedence; otherwise + only imports differing from the parent context are returned. + """ local_uses = getattr(proc, "_local_uses", None) if isinstance(local_uses, dict): return dict(local_uses) @@ -933,6 +1461,12 @@ def _derived_type_ref( var: FortranVariable, context: _DerivedTypeContext | None, ) -> tuple[str, dict[str, object]] | None: + """Return an imported derived type's public name and reference metadata. + + Local, unresolved, and non-derived declarations produce ``None``. A + procedure-local import is deliberately qualified to avoid colliding with + module-level names; the returned metadata records wrapper availability. + """ if var.base_type.lower() != "derived": return None local_name = str(var.kind) @@ -963,6 +1497,12 @@ def _resolve_derived_type_origin( local_name: str, context: _DerivedTypeContext | None, ) -> _ResolvedDerivedTypeOrigin: + """Resolve ``local_name`` against local declarations and lexical imports. + + Unambiguous local declarations win. When a module import and a + procedure-local import identify the same type, the result records the + procedure scope so callers can preserve its qualified semantic spelling. + """ lname = local_name.lower() if context is None: return _ResolvedDerivedTypeOrigin(None, local_name) @@ -982,6 +1522,12 @@ def _resolve_derived_type_origin_from_uses( local_name: str, uses: dict[str, list[FortranUseMapping]] | None, ) -> _ResolvedDerivedTypeOrigin: + """Resolve one derived-type spelling from explicit or wildcard ``use`` maps. + + Only an unambiguous match is returned. Ambiguous explicit or wildcard + imports intentionally remain unresolved so this conversion stage does + not invent a native identity. + """ lname = local_name.lower() explicit: list[tuple[str, str]] = [] wildcard_modules: list[str] = [] @@ -1010,6 +1556,12 @@ def _resolve_derived_type_origin_from_uses( return _ResolvedDerivedTypeOrigin(None, local_name) def _semantic_type_name(self, var: FortranVariable) -> str: + """Map a parsed intrinsic, derived, or procedure declaration to its dtype name. + + Compiler storage facts take precedence over the configured kind map. + Unsupported or incomplete parser facts retain their established + :class:`ValueError` diagnostics rather than being silently approximated. + """ base_type = var.base_type.lower() if base_type == "unknown": raise ValueError(f"Unknown Fortran datatype for variable '{var.name}'") @@ -1039,6 +1591,7 @@ def _semantic_type_name(self, var: FortranVariable) -> str: return semantic_type def _semantic_kind_key(self, var: FortranVariable) -> str | None: + """Normalize the declaration's kind text for semantic type-map lookup.""" raw_kind = var.target_kind_expression or var.kind if not raw_kind: return None @@ -1055,6 +1608,11 @@ def _semantic_kind_key(self, var: FortranVariable) -> str | None: return kind def _target_type_key(self, var: FortranVariable) -> tuple[str, str | None]: + """Return the normalized lookup key used for compiler storage facts. + + Character length syntax intentionally maps to the default character + storage key, while explicit character kind clauses retain their kind. + """ base_type = var.base_type.lower() raw_kind = var.target_kind_expression or var.kind if not raw_kind: @@ -1073,6 +1631,7 @@ def _target_type_key(self, var: FortranVariable) -> tuple[str, str | None]: @staticmethod def _character_kind_key(kind: str, *, character_length_syntax: bool = False) -> str | None: + """Extract a character-kind key while ignoring length-only spellings.""" if character_length_syntax: return None kind_match = re.search(r"(?:^|,)\s*kind\s*=\s*([^,]+)", kind) @@ -1083,6 +1642,7 @@ def _character_kind_key(kind: str, *, character_length_syntax: bool = False) -> return kind or None def _target_type_fact(self, var: FortranVariable) -> dict[str, object] | None: + """Return legacy fixed-width or configured compiler facts for ``var``.""" if var.declared_storage_bits is not None: return { "base_type": var.base_type.lower(), @@ -1094,16 +1654,16 @@ def _target_type_fact(self, var: FortranVariable) -> dict[str, object] | None: @staticmethod def _semantic_type_from_target_fact(fact: dict[str, object]) -> str | None: + """Map one measured intrinsic storage fact to its semantic dtype, if known.""" base_type = str(fact.get("base_type") or "").lower() bits = int(fact.get("bits") or 0) - if base_type == "logical": - return "Bool" if base_type == "character": return "String" return _FORTRAN_STORAGE_TYPE_MAP.get(base_type, {}).get(bits) @staticmethod def _literal_kind_key(kind: str) -> str | None: + """Map a literal ``kind(...)`` real expression to its conventional width key.""" match = re.fullmatch(r"kind\(\s*[-+]?\d+(?:\.\d*)?([edq])[-+]?\d*\s*\)", kind) if match is None: return None @@ -1117,10 +1677,12 @@ def _literal_kind_key(kind: str) -> str | None: return None def _resolve_compile_time_text(self, text: str) -> str: + """Resolve this converter's known compile-time values in ``text``.""" return _resolve_compile_time_text(text, self.compile_time_values) @staticmethod def _variable_origin(var: FortranVariable) -> SemanticOrigin: + """Create source identity metadata for a non-argument declaration.""" return SemanticOrigin( source_language="fortran", native_name=var.name, @@ -1131,6 +1693,7 @@ def _variable_origin(var: FortranVariable) -> SemanticOrigin: @staticmethod def _argument_origin(arg: FortranArgument | FortranVariable) -> SemanticOrigin: + """Create source identity metadata for a dummy argument or variable.""" return SemanticOrigin( source_language="fortran", native_name=arg.name, @@ -1142,6 +1705,7 @@ def _argument_origin(arg: FortranArgument | FortranVariable) -> SemanticOrigin: @staticmethod def _data_origin(var: FortranArgument | FortranVariable, *, source_kind: str) -> SemanticOrigin: + """Create source identity metadata for a module variable or type field.""" return SemanticOrigin( source_language="fortran", native_name=var.name, @@ -1153,6 +1717,7 @@ def _data_origin(var: FortranArgument | FortranVariable, *, source_kind: str) -> @staticmethod def _fortran_source_type(var: FortranVariable) -> str: + """Render the parser declaration's native Fortran type spelling for metadata.""" if var.base_type == "derived": specifier = "class" if getattr(var, "polymorphic", False) else "type" dtype = str(var.kind or "*") @@ -1163,6 +1728,7 @@ def _fortran_source_type(var: FortranVariable) -> str: @staticmethod def _fortran_variable_metadata(var: FortranVariable) -> dict[str, object]: + """Return parser storage and declaration facts that semantic IR must retain.""" metadata: dict[str, object] = { "rank": var.rank, "shape": list(var.shape), @@ -1186,10 +1752,13 @@ def _fortran_variable_metadata(var: FortranVariable) -> dict[str, object]: metadata["polymorphic"] = True if getattr(var, "is_parameter", False): metadata["constant"] = True + if var.declared_storage_bits is not None: + metadata["declared_storage_bits"] = var.declared_storage_bits return metadata @staticmethod def _procedure_metadata(proc: FortranProcedureSignature) -> dict[str, object]: + """Return native procedure attributes and optional bind name for semantic IR.""" metadata: dict[str, object] = {} if proc.attributes: metadata["fortran_attributes"] = list(proc.attributes) @@ -1199,13 +1768,27 @@ def _procedure_metadata(proc: FortranProcedureSignature) -> dict[str, object]: metadata["fortran_bind_c_name"] = proc.bind_name return metadata + # Storage and argument-contract helpers + def _array_storage_contract( self, var: FortranVariable, shape: list[str], + *, + declaration_arrays: dict[str, ArrayExpressionSource] | None = None, ) -> SemanticStorageContract: + """Construct the semantic array storage contract for a parsed declaration. + + The helper derives category, axes, bounds, order, contiguity, allocation, + and pointer facts from ``var`` and already resolved ``shape`` text. + """ category = self._array_category(var, shape) - axes = self._array_axes(shape, category, contiguous=getattr(var, "contiguous", False)) + axes = self._array_axes( + shape, + category, + contiguous=getattr(var, "contiguous", False), + declaration_arrays=declaration_arrays, + ) rank = var.rank order = self._array_order(rank, category, contiguous=getattr(var, "contiguous", False)) lower_bounds, upper_bounds = self._array_bound_metadata(shape) @@ -1229,11 +1812,17 @@ def _array_storage_contract( ) def _resolve_optional_compile_time_text(self, text: str | None) -> str | None: + """Resolve optional bound text while preserving an absent bound as ``None``.""" if text is None: return None return self._resolve_compile_time_text(text) def _array_bound_metadata(self, shape: list[str]) -> tuple[list[str | None], list[str | None]]: + """Split Fortran dimension tokens into optional lower and upper bounds. + + Default lower bound ``1`` is omitted, assumed-size ``*`` is retained as + an upper marker, and fully absent bound lists stay empty for compatibility. + """ lower_bounds: list[str | None] = [] upper_bounds: list[str | None] = [] for dim in shape: @@ -1253,6 +1842,7 @@ def _array_bound_metadata(self, shape: list[str]) -> tuple[list[str | None], lis @staticmethod def _array_category(var: FortranVariable, shape: list[str]) -> str: + """Classify parsed array dimensions for semantic storage metadata.""" cleaned = [dim.strip() for dim in shape] if cleaned == [".."]: return "assumed_rank" @@ -1271,7 +1861,20 @@ def _array_category(var: FortranVariable, shape: list[str]) -> str: return "explicit_shape" @classmethod - def _array_axes(cls, shape: list[str], category: str, *, contiguous: bool) -> list[str]: + def _array_axes( + cls, + shape: list[str], + category: str, + *, + contiguous: bool, + declaration_arrays: dict[str, ArrayExpressionSource] | None = None, + ) -> list[str]: + """Convert native bounds into Python-form public extent expressions. + + The source ``shape`` remains unchanged elsewhere in the array contract. + Explicit bounds are converted to extents first, then the shared + expression layer translates Fortran inquiries and operators. + """ if category == "assumed_rank": return ["..."] if category == "assumed_shape" and not contiguous: @@ -1288,101 +1891,39 @@ def _array_axes(cls, shape: list[str], category: str, *, contiguous: bool) -> li continue lower, upper = cls._dimension_bounds(token) if lower in {None, "1"} and upper: - axes.append(cls._canonical_dimension_expression(upper)) + extent = upper elif lower is not None and upper is not None: - axes.append(cls._canonical_dimension_expression(f"({upper}) - ({lower}) + 1")) + extent = f"({upper}) - ({lower}) + 1" elif ":" in token: axes.append(":") + continue else: - axes.append(cls._canonical_dimension_expression(token)) + extent = token + public_extent = fortran_extent_to_python(extent, declaration_arrays) + axes.append(canonicalize_declaration_extent(public_extent)) return axes @staticmethod def _dimension_bounds(token: str) -> tuple[str | None, str | None]: - if ":" not in token: - return "1", token - lower, upper = token.split(":", 1) - return lower.strip() or None, upper.strip() or None + """Return the lower and upper parts of one Fortran dimension token.""" + return split_dimension_bounds(token) @staticmethod def _has_omitted_upper_bound(token: str) -> bool: - return ":" in token and token.split(":", 1)[1].strip() == "" - - @staticmethod - def _canonical_dimension_expression(expression: str) -> str: - try: - parsed = ast.parse(expression, mode="eval").body - return ast.unparse(FortranToIRConverter._simplify_additive_dimension(parsed)) - except SyntaxError: - return expression - - @staticmethod - def _simplify_additive_dimension(expression: ast.expr) -> ast.expr: - """Fold additive bound arithmetic into the public extent expression.""" - terms: list[tuple[int, ast.expr]] = [] - - def collect(node: ast.expr, sign: int = 1) -> None: - if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): - collect(node.left, sign) - collect(node.right, sign) - return - if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Sub): - collect(node.left, sign) - collect(node.right, -sign) - return - if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): - collect(node.operand, -sign) - return - terms.append((sign, node)) - - collect(expression) - constant = sum( - sign * node.value - for sign, node in terms - if isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool) - ) - symbolic = [ - (sign, node) - for sign, node in terms - if not (isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool)) - ] - - coefficients: dict[str, tuple[ast.expr, int]] = {} - order: list[str] = [] - for sign, node in symbolic: - key = ast.dump(node, include_attributes=False) - if key not in coefficients: - coefficients[key] = (node, 0) - order.append(key) - original, coefficient = coefficients[key] - coefficients[key] = (original, coefficient + sign) - - result: ast.expr | None = None - for key in order: - node, coefficient = coefficients[key] - for _ in range(abs(coefficient)): - if result is None: - result = node if coefficient > 0 else ast.UnaryOp(op=ast.USub(), operand=node) - else: - operator: ast.operator = ast.Add() if coefficient > 0 else ast.Sub() - result = ast.BinOp(left=result, op=operator, right=node) - - if result is None: - return ast.Constant(value=constant) - if constant > 0: - return ast.BinOp(left=result, op=ast.Add(), right=ast.Constant(value=constant)) - if constant < 0: - return ast.BinOp(left=result, op=ast.Sub(), right=ast.Constant(value=-constant)) - return result + """Return whether a dimension's colon form has no explicit upper bound.""" + bounds = split_top_level_expression(token, ":") + return len(bounds) > 1 and not ":".join(bounds[1:]).strip() @staticmethod def _array_order(rank: int, category: str, *, contiguous: bool) -> str | None: + """Return Fortran order for multidimensional arrays and ``None`` otherwise.""" if rank <= 1: return None return "ORDER_F" @staticmethod def _array_contiguous(category: str, *, contiguous: bool) -> bool | None: + """Derive contiguity certainty from parser category and explicit attribute.""" if contiguous: return True if category in {"explicit_shape", "assumed_size", "deferred_shape"}: @@ -1393,10 +1934,12 @@ 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 @staticmethod def _reference_storage_contract(*, writes_argument: bool) -> SemanticStorageContract: + """Create the scalar by-reference contract implied by argument access.""" read_only = not writes_argument return SemanticStorageContract( kind="reference", @@ -1407,6 +1950,7 @@ def _reference_storage_contract(*, writes_argument: bool) -> SemanticStorageCont @staticmethod def _scalar_storage_contract(*, writes_argument: bool) -> SemanticStorageContract: + """Create the rank-zero descriptor contract used for writable optional scalars.""" read_only = not writes_argument return SemanticStorageContract( kind="array", @@ -1426,6 +1970,12 @@ def _apply_array_argument_contract( *, writes_argument: bool, ) -> None: + """Apply argument mutability and array declaration facts in place. + + ``semantic_type`` must already carry an array storage contract. Missing + storage is intentionally ignored, preserving callers that cannot form + an array contract for unsupported declarations. + """ if semantic_type.storage is None: return read_only = not writes_argument @@ -1439,15 +1989,18 @@ def _apply_array_argument_contract( @staticmethod def _add_variable_constraints(semantic_type: SemanticType, var: FortranVariable) -> None: + """Append declaration-implied constraints to ``semantic_type`` in place.""" if getattr(var, "is_parameter", False): semantic_type.constraints.append(SemanticConstraint("Constant")) @staticmethod def _apply_argument_ownership(semantic_type: SemanticType, *, writes_argument: bool) -> None: + """Record argument mutability on the already constructed ownership metadata.""" semantic_type.ownership.mutable = writes_argument @staticmethod def _apply_pointer_input_policy(semantic_type: SemanticType) -> None: + """Record the existing caller-owned policy for a non-writing pointer input.""" set_ownership_metadata( semantic_type.metadata, owner="caller", @@ -1457,6 +2010,7 @@ def _apply_pointer_input_policy(semantic_type: SemanticType) -> None: @staticmethod def _apply_pointer_result_policy(semantic_type: SemanticType) -> None: + """Record the existing Python snapshot policy for a scalar pointer result.""" if semantic_type.rank > 0: return set_ownership_metadata( @@ -1471,6 +2025,7 @@ def _argument_access( arg: FortranArgument | FortranVariable, semantic_type: SemanticType, ) -> tuple[bool, bool]: + """Return parser-provided read/write facts or the established conservative default.""" reads = getattr(arg, "reads_argument", None) writes = getattr(arg, "writes_argument", None) if reads is None or writes is None: @@ -1481,17 +2036,27 @@ def _argument_access( @staticmethod def _argument_has_writable_storage(argument: SemanticArgument) -> bool: + """Return whether semantic ownership or storage marks an argument writable.""" storage = argument.semantic_type.storage return bool( argument.semantic_type.ownership.mutable or (storage is not None and (storage.mutable or not storage.read_only)) ) + # Derived-type binding and generic-interface helpers + def _bound_methods( self, dtype: FortranDerivedType, procedure_lookup: dict[str, SemanticFunction], ) -> list[SemanticMethod]: + """Project resolved type-bound procedure bindings into semantic methods. + + Procedures missing from ``procedure_lookup`` are ignored as before. + For matched bindings, this marks the original procedure as type-bound, + then copies the relevant signature and binding-only metadata into a + method without leaking temporary mutation into the method metadata. + """ methods: list[SemanticMethod] = [] bindings = getattr(dtype, "procedure_bindings", ()) or [ {"name": method_name, "attrs": []} for method_name in dtype.methods @@ -1543,6 +2108,12 @@ def _module_overload_sets( context: _DerivedTypeContext, semantic_classes: list[SemanticClass], ) -> list[ProcedureOverloadSet]: + """Convert module generic interfaces into function or class overload sets. + + Normal procedure generics remain module overloads. Defined operators + and assignment are attached to their derived classes, while unsupported + constructors preserve the existing descriptive conversion failure. + """ overload_sets: list[ProcedureOverloadSet] = [] class_map = {semantic_class.name.casefold(): semantic_class for semantic_class in semantic_classes} for interface in module.interfaces: @@ -1551,7 +2122,7 @@ def _module_overload_sets( inline_lookup = { signature.name.casefold(): self.visit( signature, - visibility=self._symbol_visibility(module, interface.name), + visibility=self._symbol_visibility(module, signature.name), derived_type_context=context, ) for signature in interface.procedures @@ -1595,6 +2166,12 @@ def _bound_overload_sets( dtype: FortranDerivedType, methods: list[SemanticMethod], ) -> list[ProcedureOverloadSet]: + """Convert a derived type's generic bindings into method overload sets. + + Resolved method targets keep binding visibility; malformed or missing + generic targets preserve the previous empty-placeholder behavior for + ordinary procedure names and are otherwise omitted. + """ lookup = {method.name.casefold(): method for method in methods} overload_sets: list[ProcedureOverloadSet] = [] for binding in dtype.generic_bindings: @@ -1640,6 +2217,12 @@ def _apply_assignment_projection_to_originals( lookup: dict[str, SemanticFunction], classes: dict[str, SemanticClass], ) -> None: + """Replace valid defined-assignment projections on their original procedures. + + The generic candidates are only a view; this mutates the matching + original lookup entry so later consumers see the same bound-object + result projection as the defined-assignment overload. + """ kind, token = self._defined_generic_identity(generic_name) if kind != "assignment": return @@ -1655,6 +2238,7 @@ def _merge_overload_sets( overload_sets: list[ProcedureOverloadSet], incoming: list[ProcedureOverloadSet], ) -> None: + """Append incoming overload candidates to matching named sets in place.""" for overload_set in incoming: existing = next((item for item in overload_sets if item.name == overload_set.name), None) if existing is None: @@ -1664,6 +2248,11 @@ def _merge_overload_sets( @staticmethod def _normal_overload_set(name: str, procedures: list[SemanticFunction]) -> ProcedureOverloadSet: + """Copy regular generic candidates and attach generic dispatch metadata. + + Type-bound methods are projected back to ordinary functions while + retaining their bound-object position or static flag in metadata. + """ candidates = [] for procedure in procedures: candidate = deepcopy(procedure) @@ -1698,6 +2287,12 @@ def _defined_overload_sets( procedures: list[SemanticFunction], classes: dict[str, SemanticClass], ) -> list[tuple[SemanticClass, list[ProcedureOverloadSet]]]: + """Map valid defined operators or assignments onto affected semantic classes. + + Candidates failing the existing representation checks are skipped. For + valid assignment interfaces the candidate projection is changed to + return the passed object before its class-local overload set is built. + """ grouped: dict[str, tuple[SemanticClass, dict[str, ProcedureOverloadSet]]] = {} kind, token = self._defined_generic_identity(generic_name) if kind is None: @@ -1730,6 +2325,7 @@ def _defined_overload_sets( @staticmethod def _defined_generic_identity(name: str) -> tuple[str | None, str]: + """Classify a Fortran defined generic spelling and return its normalized token.""" compact = re.sub(r"\s+", "", name).casefold() if compact == "assignment(=)": return "assignment", "=" @@ -1770,6 +2366,7 @@ def _defined_procedure_error( procedure: SemanticFunction, classes: dict[str, SemanticClass], ) -> str | None: + """Return the representation error for a defined generic, or ``None`` when valid.""" arguments = procedure.arguments if kind == "assignment": if len(arguments) != 2 or procedure.return_type is not None: @@ -1790,7 +2387,7 @@ def _defined_procedure_error( return f"defined operator {token!r} must be a function with {sorted(expected_arities)} operand count" if not any(argument.semantic_type.name.casefold() in classes for argument in arguments): return "defined operator must have at least one wrapped derived-type operand" - if token in _COMPARISON_OPERATOR_METHODS and procedure.return_type.dtype != "Bool": + if token in _COMPARISON_OPERATOR_METHODS and not is_boolean_semantic_type_name(procedure.return_type.dtype): return "defined relational operator must return Bool" return None @@ -1801,6 +2398,12 @@ def _defined_python_bindings( procedure: SemanticFunction, classes: dict[str, SemanticClass], ) -> list[tuple[SemanticClass, str, str, int]]: + """Return class, overload-set, method, and bound-position projections for a generic. + + Assignment maps to ``assign`` on its left-hand derived type. Operators + are emitted for each distinct derived-type operand, preserving reflected + and comparison method naming rules. + """ if kind == "assignment": semantic_class = classes[procedure.arguments[0].semantic_type.name.casefold()] return [(semantic_class, "assign", "assign", 0)] @@ -1843,6 +2446,7 @@ def _defined_overload_candidate( method_name: str, bound_position: int, ) -> SemanticFunction: + """Copy a generic candidate and attach its overload and binding metadata.""" candidate = deepcopy(procedure) candidate.metadata[FORTRAN_GENERIC_NAME_METADATA] = generic_name candidate.metadata[OVERLOAD_KIND_METADATA] = ( @@ -1868,6 +2472,12 @@ def _defined_overload_candidate( @staticmethod def _assignment_projection(procedure: SemanticFunction, bound_position: int) -> list[ProjectionMapping]: + """Return ``procedure`` projection with its assigned object as result zero. + + Native order is preserved; visible Python argument positions are + recomputed around hidden outputs, and copied mapping values prevent + mutation of the original projection. + """ projection = [] python_position = 0 for mapping in sorted(procedure.projection, key=lambda item: item.native_position or 0): @@ -1904,6 +2514,7 @@ def _assignment_projection(procedure: SemanticFunction, bound_position: int) -> @staticmethod def _as_semantic_function(procedure: SemanticFunction) -> SemanticFunction: + """Copy a procedure-like candidate into the plain function representation used by overloads.""" return SemanticFunction( name=procedure.native_name or procedure.name, native_name=procedure.native_name, @@ -1919,6 +2530,7 @@ def _as_semantic_function(procedure: SemanticFunction) -> SemanticFunction: @staticmethod 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 @staticmethod @@ -1928,6 +2540,7 @@ def _resolve_overload_targets( *, visibility: str | None, ) -> tuple[list[SemanticFunction], list[str]]: + """Copy resolved generic targets and list target names absent from ``procedure_lookup``.""" procedures: list[SemanticFunction] = [] missing: list[str] = [] for target_name in target_names: @@ -1946,6 +2559,12 @@ def _passed_object_argument( proc: SemanticFunction, binding_attributes: tuple[str, ...], ) -> tuple[str | None, int | None]: + """Identify a type-bound procedure's passed-object argument and position. + + ``nopass`` returns no object. An omitted ``pass`` attribute defaults to + the first argument; explicit missing names keep the established + :class:`ValueError` diagnostics. + """ if "nopass" in binding_attributes: return None, None @@ -1970,6 +2589,7 @@ def _passed_object_argument( @staticmethod def _procedure_binding_names(name: str) -> tuple[str, str]: + """Split a Fortran binding ``local => target`` spelling into both names.""" if "=>" not in name: return name.strip(), name.strip() binding_name, target_name = name.split("=>", 1) @@ -1977,6 +2597,7 @@ def _procedure_binding_names(name: str) -> tuple[str, str]: @staticmethod def _projected_procedure_arguments(proc: FortranProcedureSignature) -> list[FortranArgument]: + """Return procedure dummies with required arguments preceding optional ones.""" args = list(proc.arguments) return [ *[arg for arg in args if not getattr(arg, "optional", False)], @@ -1993,6 +2614,7 @@ def _is_returned_output_argument( is_character_replacement: bool, is_descriptor_replacement: bool, ) -> bool: + """Return whether an output/replacement must be exposed as a Python result.""" if ( is_primitive_scalar_replacement or is_allocatable_replacement @@ -2013,6 +2635,7 @@ def _is_hidden_output_argument( is_output: bool, semantic_type: SemanticType | None, ) -> bool: + """Return whether a nonoptional pure-output dummy is hidden from Python input.""" if not is_output or getattr(native_arg, "optional", False): return False return ( @@ -2026,6 +2649,12 @@ def _procedure_projection( proc: FortranProcedureSignature, arguments: list[SemanticArgument], ) -> list[ProjectionMapping]: + """Build native-to-Python argument and result mappings for one procedure. + + The existing flow classifies each dummy's access and storage form, marks + returned outputs, assigns stable Python/result positions, and records + descriptor or by-value projection details consumed by later stages. + """ by_name = {arg.name: arg for arg in arguments} projection: list[ProjectionMapping] = [] @@ -2091,6 +2720,7 @@ def _procedure_projection( @staticmethod def _is_allocatable_array(semantic_type: SemanticType | None) -> bool: + """Return whether a semantic type has allocatable array storage.""" return bool( semantic_type is not None and semantic_type.storage is not None @@ -2100,6 +2730,7 @@ def _is_allocatable_array(semantic_type: SemanticType | None) -> bool: @staticmethod def _is_native_descriptor_output(semantic_type: SemanticType | None) -> bool: + """Return whether output storage uses an allocatable, pointer, or scalar descriptor.""" if semantic_type is None: return False if FortranToIRConverter._is_scalar_descriptor(semantic_type): @@ -2110,6 +2741,7 @@ def _is_native_descriptor_output(semantic_type: SemanticType | None) -> bool: @staticmethod def _is_scalar_descriptor(semantic_type: SemanticType | None) -> bool: + """Return whether a rank-zero type is represented by an allocatable or pointer descriptor.""" return bool( semantic_type is not None and semantic_type.rank == 0 @@ -2118,6 +2750,7 @@ def _is_scalar_descriptor(semantic_type: SemanticType | None) -> bool: @staticmethod def _is_primitive_scalar_replacement(semantic_type: SemanticType | None) -> bool: + """Return whether a mutable rank-zero primitive is returned as a replacement value.""" return bool( semantic_type is not None and semantic_type.rank == 0 @@ -2160,6 +2793,7 @@ def _scalar_descriptor_projection_value( @staticmethod def _is_python_value_scalar_output(semantic_type: SemanticType | None) -> bool: + """Return whether a rank-zero non-descriptor maps directly to a Python value.""" return bool( semantic_type is not None and semantic_type.rank == 0 @@ -2169,10 +2803,12 @@ def _is_python_value_scalar_output(semantic_type: SemanticType | None) -> bool: @staticmethod def _is_scalar_character(semantic_type: SemanticType | None) -> bool: + """Return whether ``semantic_type`` is a rank-zero semantic string.""" return bool(semantic_type is not None and semantic_type.rank == 0 and semantic_type.name == "String") @staticmethod def _base_classes(dtype: FortranDerivedType) -> list[str]: + """Return the optional Fortran extension parent as semantic class names.""" if not dtype.extends: return [] if isinstance(dtype.extends, str): @@ -2181,12 +2817,14 @@ def _base_classes(dtype: FortranDerivedType) -> list[str]: @staticmethod def _standalone_module_name(parsed_file: FortranFile) -> str: + """Choose the stable synthetic module name for file-level procedures.""" if parsed_file.filename: return Path(parsed_file.filename).stem return "standalone" @staticmethod def _symbol_visibility(module: FortranModule, symbol_name: str) -> str: + """Resolve explicit private/public lists before the module default visibility.""" lname = symbol_name.lower() public_set = {s.lower() for s in getattr(module, "public_symbols", [])} private_set = {s.lower() for s in getattr(module, "private_symbols", [])} @@ -2197,11 +2835,15 @@ def _symbol_visibility(module: FortranModule, symbol_name: str) -> str: return getattr(module, "default_visibility", "public") +# Compile-time requirement collection and specialization + + def _requirement_unit_name( *, module: str | None = None, unit_name: str | None = None, ) -> str: + """Return a diagnostic unit label from optional module and local-unit names.""" if module and unit_name: return f"{module}.{unit_name}" return unit_name or module or "" @@ -2326,6 +2968,7 @@ def _module_variable_contexts( *, unit_kind: str, ): + """Yield variable, procedure, and type contexts owned by a module-like node.""" owner = node.name for variable in node.variables: yield _variable_context(variable, unit_kind=unit_kind, unit=owner, module=owner, role="variable") @@ -2336,6 +2979,7 @@ def _module_variable_contexts( def _variable_context(variable, *, unit_kind, unit, module, role, **extra): + """Pair one parser variable with the stable diagnostic context used by collectors.""" return variable, { "unit_kind": unit_kind, "unit": unit, @@ -2347,6 +2991,7 @@ def _variable_context(variable, *, unit_kind, unit, module, role, **extra): def _compile_time_requirement_message(code: str, symbol: str, expression: str) -> str: + """Build the existing user-facing message for one unresolved-value requirement.""" if code == "parameter_value": return f"Parameter '{symbol}' needs a compile-time value for expression '{expression}'." if code == "unsupported_kind": @@ -2355,7 +3000,17 @@ def _compile_time_requirement_message(code: str, symbol: str, expression: str) - def fortran_type_storage_expression(base_type: str, kind: str | None = None) -> str: - """Return the compiler expression that measures one intrinsic type.""" + """Return the Fortran ``storage_size`` expression for one intrinsic type. + + Use this when executing a requirement returned by + :func:`collect_fortran_type_storage_requirements` with a target compiler. + ``base_type`` must be an integer, real, complex, or logical intrinsic; + unsupported probe types raise :class:`ValueError`. + + Example: + >>> fortran_type_storage_expression("real", "8") + 'storage_size(real(0.0,kind=8))' + """ constructors = { "integer": "int(0)", "real": "real(0.0)", @@ -2376,7 +3031,18 @@ def collect_fortran_type_storage_requirements( *, compile_time_values: dict[str, int | str] | None = None, ) -> list[dict[str, object]]: - """Collect unique compiler storage queries needed by semantic conversion.""" + """Collect distinct compiler storage queries required for semantic conversion. + + Pass a parsed Fortran module, file, or project before invoking a target + probe. Each result keeps the normalized base type, kind, expression, and + first parser context that needs it; callers normally feed probe facts back + to the conversion helpers through ``type_facts``. + + Example: + >>> parsed = FortranFile(variables=[FortranVariable(name="value", base_type="real", kind="8")]) + >>> collect_fortran_type_storage_requirements(parsed)[0]["expression"] + 'storage_size(real(0.0,kind=8))' + """ converter = FortranToIRConverter(compile_time_values=compile_time_values) requirements: list[dict[str, object]] = [] seen: set[tuple[str, str | None]] = set() @@ -2418,7 +3084,7 @@ def collect_semantic_compile_time_requirements( Example: >>> from prik import parse_fortran_file - >>> parsed = parse_fortran_file("module m\ninteger, parameter :: rk = selected_real_kind(12)\nend module") + >>> parsed = parse_fortran_file("module m\\ninteger, parameter :: rk = selected_real_kind(12)\\nend module") >>> reqs = collect_semantic_compile_time_requirements(parsed) >>> reqs[0]["symbol"] 'rk' @@ -2431,6 +3097,7 @@ def collect_semantic_compile_time_requirements( def add_requirement( code: str, ctx: dict, *, expression: str, base_type: str | None = None, kind: str | None = None ) -> None: + """Append one unique unresolved-value requirement using parser diagnostic context.""" symbol = str(ctx.get("symbol") or "") item = { "code": code, @@ -2481,6 +3148,7 @@ def add_requirement( def _resolve_semantic_value(value, compile_time_values: dict[str, str]): + """Recursively resolve compile-time text inside a semantic metadata value.""" if isinstance(value, str): return _resolve_compile_time_text(value, compile_time_values) if isinstance(value, list): @@ -2496,6 +3164,7 @@ def _resolve_semantic_type_compile_time_values( semantic_type: SemanticType | None, compile_time_values: dict[str, str], ) -> None: + """Resolve shape, constraint, and storage text on one semantic type in place.""" if semantic_type is None: return semantic_type.shape = [_resolve_compile_time_text(dim, compile_time_values) for dim in semantic_type.shape] @@ -2526,6 +3195,7 @@ def _resolve_semantic_argument_compile_time_values( arg: SemanticArgument | SemanticVariable, compile_time_values: dict[str, str], ) -> None: + """Resolve type, default, and metadata text on one semantic argument in place.""" _resolve_semantic_type_compile_time_values(arg.semantic_type, compile_time_values) arg.default_value = _resolve_semantic_value(arg.default_value, compile_time_values) arg.metadata = _resolve_semantic_value(arg.metadata, compile_time_values) @@ -2535,6 +3205,7 @@ def _resolve_semantic_function_compile_time_values( func: SemanticFunction, compile_time_values: dict[str, str], ) -> None: + """Resolve all type-bearing fields and projection values on one function in place.""" for arg in func.arguments: _resolve_semantic_argument_compile_time_values(arg, compile_time_values) for local in func.locals: @@ -2549,6 +3220,7 @@ def _resolve_semantic_module_compile_time_values( module: SemanticModule, compile_time_values: dict[str, str], ) -> None: + """Resolve compile-time text across one module's variables, functions, and classes.""" for var in module.variables: _resolve_semantic_argument_compile_time_values(var, compile_time_values) for func in module.functions: @@ -2589,6 +3261,12 @@ def _converter_for( wrapped_derived_types: Iterable[tuple[str, str]] | None = None, type_facts: dict[tuple[str, str | None], dict[str, object]] | None = None, ) -> FortranToIRConverter: + """Return the shared default converter or an isolated configured converter. + + The shared instance is used only for the all-default case. Supplying any + conversion input creates a new instance so per-call compile-time values and + facts never leak into unrelated conversions. + """ if compile_time_values is None and wrapped_derived_types is None and type_facts is None: return _DEFAULT_CONVERTER return FortranToIRConverter( @@ -2608,6 +3286,24 @@ def fortran_module_to_semantic_module( wrapped_derived_types: Iterable[tuple[str, str]] | None = None, type_facts: dict[tuple[str, str | None], dict[str, object]] | None = None, ) -> SemanticModule: + """Convert one parsed Fortran module into a :class:`SemanticModule`. + + Use this for a selected :class:`FortranModule`, or for a file whose first + module is intentionally the target. Optional compile-time values and + compiler storage facts complete parser-known datatype conversion; imported + wrapper identities may be supplied through ``wrapped_derived_types``. + Unsupported types or an empty file raise the existing conversion errors. + + Example: + >>> parsed = FortranModule( + ... name="math", + ... procedures=[FortranProcedureSignature(name="scale", kind="subroutine", arguments=[ + ... FortranArgument(name="value", base_type="real", kind="8") + ... ])], + ... ) + >>> fortran_module_to_semantic_module(parsed).functions[0].arguments[0].semantic_type.name + 'Float64' + """ converter = _converter_for(compile_time_values, wrapped_derived_types, type_facts) return converter.visit(converter.first_module(module)) @@ -2620,6 +3316,17 @@ 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, ) -> 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. + + Example: + >>> parsed = FortranFile(procedures=[FortranProcedureSignature(name="tick", kind="subroutine")]) + >>> [module.name for module in fortran_file_to_semantic_modules(parsed)] + ['standalone'] + """ return _converter_for(compile_time_values, wrapped_derived_types, type_facts).visit( parsed_file, standalone_module_name=standalone_module_name, @@ -2632,8 +3339,39 @@ def fortran_project_to_semantic_modules( compile_time_values: dict[str, int | str] | None = None, type_facts: dict[tuple[str, str | None], dict[str, object]] | None = None, ) -> list[SemanticModule]: + """Convert an ordered parsed Fortran project with project-wide type context. + + Use this for multi-file conversion where imported derived types and callback + interfaces must resolve across files. The output preserves project file + and parser module order; type facts and compile-time values apply to every + converted module. + + Example: + >>> project = FortranProject(files=[FortranFile(modules=[FortranModule(name="math")])]) + >>> [module.name for module in fortran_project_to_semantic_modules(project)] + ['math'] + """ return _converter_for(compile_time_values, type_facts=type_facts).visit(project) if __name__ == "__main__": - pass + from prik.parsers.fortran.models import FortranArgument, FortranModule, FortranProcedureSignature + + parsed_module = FortranModule( + name="math", + procedures=[ + FortranProcedureSignature( + name="scale", + kind="subroutine", + module="math", + arguments=[FortranArgument(name="value", base_type="real", kind="8", procedure="scale")], + ) + ], + ) + semantic_module = fortran_module_to_semantic_module(parsed_module) + semantic_argument = semantic_module.functions[0].arguments[0] + print( + f"{semantic_module.name}.{semantic_module.functions[0].name}" + f"({semantic_argument.name}): {semantic_argument.semantic_type.name}" + f" via {semantic_argument.semantic_type.storage.kind} storage" + ) diff --git a/prik/semantics/models.py b/prik/semantics/models.py index 701ec9cb6..d496dc0f2 100644 --- a/prik/semantics/models.py +++ b/prik/semantics/models.py @@ -7,6 +7,7 @@ EXTERNAL_TYPE_REF_METADATA = "external_type_ref" PROTOTYPE_REF_METADATA = "prototype_ref" +PROTOTYPE_INTENT_METADATA = "prototype_intent" 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" @@ -18,6 +19,7 @@ RUNTIME_RETAIN_RESULT_OWNER_METADATA = "runtime_retain_result_owner" RUNTIME_STATUS_ERROR_METADATA = "runtime_status_error" RESOLVED_RUNTIME_STATUS_ERROR_POLICY_METADATA = "resolved_runtime_status_error_policy" +DECLARATION_EXPRESSION_CALLABLES_METADATA = "declaration_expression_callables" # ============================================================ @@ -72,8 +74,36 @@ class SemanticOrigin: metadata: dict[str, Any] = field(default_factory=dict) +@dataclass +class SemanticExpressionCallable: + """Identify a native callable referenced by a declaration expression. + + ``name`` is the spelling used by the semantic contract. ``native_name`` + and ``native_scope`` preserve the source declaration reached by normal + language name resolution. ``placement`` distinguishes a concrete standalone + interface from an unresolved name when neither has a module scope. The + optional declaration link is semantic-only and lets post-IR policy validate + exact prototype characteristics without duplicating them on the call site. + """ + + name: str + native_name: str | None = None + native_scope: str | None = None + source_language: str | None = None + placement: str | None = None + declaration: SemanticFunction | None = field(default=None, compare=False, repr=False) + + @dataclass class SemanticArrayContract: + """Describe an array's semantic shape, storage layout, and provenance. + + Shape entries use the public language-neutral expression dialect. + The ``expression_callables`` property is parallel to ``shape`` and records + native procedure identities used by each axis; source bounds retain the + original declaration syntax for diagnostics and source-oriented consumers. + """ + rank: int | None = None shape: list[str] = field(default_factory=list) # Native-source provenance, excluded from public contract equality. @@ -89,6 +119,29 @@ class SemanticArrayContract: pointer: bool = False metadata: dict[str, Any] = field(default_factory=dict) + @property + def expression_callables(self) -> list[list[SemanticExpressionCallable]]: + """Return native callable references aligned with the array's shape axes. + + Empty axes are represented by empty lists. Arrays without any callable + reference return an empty list and do not gain serialized metadata. + """ + value = self.metadata.get(DECLARATION_EXPRESSION_CALLABLES_METADATA, []) + return value if isinstance(value, list) else [] + + @expression_callables.setter + def expression_callables(self, value: list[list[SemanticExpressionCallable]]) -> None: + """Store nonempty per-axis references or remove empty provenance. + + The setter consumes a shape-aligned list and mutates only ``metadata``. + Omitting all-empty data preserves the historical serialization of + arrays that do not use declaration functions. + """ + if any(value): + self.metadata[DECLARATION_EXPRESSION_CALLABLES_METADATA] = value + else: + self.metadata.pop(DECLARATION_EXPRESSION_CALLABLES_METADATA, None) + @dataclass class SemanticStorageContract: @@ -275,6 +328,7 @@ def __eq__(self, other: object) -> bool: getattr(self, "passed_object_name", None), getattr(self, "passed_object_position", None), getattr(self, "binding_attributes", ()), + getattr(self, "pure", None), ) == ( other.name, other.native_name, @@ -289,6 +343,7 @@ def __eq__(self, other: object) -> bool: getattr(other, "passed_object_name", None), getattr(other, "passed_object_position", None), getattr(other, "binding_attributes", ()), + getattr(other, "pure", None), ) @@ -299,7 +354,9 @@ def __eq__(self, other: object) -> bool: @dataclass(eq=False) class SemanticPrototype(SemanticFunction): - """Named native callback signature with no runtime Python export.""" + """Reusable exact native procedure signature with no Python export.""" + + pure: bool = False # ============================================================ diff --git a/prik/semantics/native_contract.py b/prik/semantics/native_contract.py index af6e763f4..164f9e670 100644 --- a/prik/semantics/native_contract.py +++ b/prik/semantics/native_contract.py @@ -180,7 +180,7 @@ def _function_issues( issues.append( NativeContractIssue( "pyi_native_procedure_scope_mismatch", - "Native procedure scope contradicts its leaf or @external placement.", + "Native procedure scope contradicts its leaf or @standalone placement.", owner, ) ) diff --git a/prik/semantics/ownership.py b/prik/semantics/ownership.py index e010520bc..e0a3623b2 100644 --- a/prik/semantics/ownership.py +++ b/prik/semantics/ownership.py @@ -22,6 +22,7 @@ PROJECTED_OUTPUT_METADATA, SCALAR_STORAGE_CATEGORY, ) +from prik.types.numpy import BOOLEAN_SEMANTIC_TYPE_NAMES OWNERSHIP_POLICY_METADATA = "ownership_policy" @@ -42,7 +43,17 @@ PYTHON_VALUE_IMMUTABLE = "immutable" +# Completed policy vocabulary + + class ObjectKind(str, Enum): + """Classify the Python-facing category selected by ownership policy. + + ``OwnershipPolicyResolver`` chooses a kind before selecting lifetime and + ABI actions. Strict lowering dispatchers consume this value as part of + their completed-policy key. + """ + SCALAR = "scalar" STRING = "string" NUMPY_ARRAY = "numpy_array" @@ -50,6 +61,8 @@ class ObjectKind(str, Enum): class OwnershipOwner(str, Enum): + """Name the party responsible for the represented object's storage.""" + PYTHON = "python" CALLER = "caller" NATIVE = "native" @@ -59,6 +72,8 @@ class OwnershipOwner(str, Enum): class TransferMode(str, Enum): + """Describe how a value or storage reference crosses the wrapper boundary.""" + BY_VALUE = "by_value" IN_PLACE = "in_place" COPY_RETURN = "copy_return" @@ -70,6 +85,8 @@ class TransferMode(str, Enum): class DestructionPolicy(str, Enum): + """Describe who, if anyone, releases native or Python-side resources.""" + PYTHON_REFCOUNT = "python_refcount" CALLER = "caller" WRAPPER_DEALLOC = "wrapper_dealloc" @@ -80,12 +97,16 @@ class DestructionPolicy(str, Enum): class StorageMode(str, Enum): + """Select stable storage for a contract value or ABI boundary representation.""" + STACK = "stack" HEAP = "heap" ALIAS = "alias" class CodegenAction(str, Enum): + """Identify the completed lowering action for a supported ownership decision.""" + DIRECT_VALUE = "direct_value" CALL_LOCAL_INPUT = "call_local_input" IN_PLACE_ARGUMENT = "in_place_argument" @@ -99,6 +120,8 @@ class CodegenAction(str, Enum): class PythonBarrierAction(str, Enum): + """Identify how a Python-visible argument crosses into wrapper storage.""" + SCALAR_VALUE = "scalar_value" SCALAR_STORAGE = "scalar_storage" ARRAY_STORAGE = "array_storage" @@ -111,6 +134,8 @@ class PythonBarrierAction(str, Enum): class NativeBarrierAction(str, Enum): + """Identify how wrapper storage crosses the native ABI boundary.""" + PASS_VALUE = "pass_value" PASS_CALL_LOCAL_ADDRESS = "pass_call_local_address" PASS_STORAGE_ADDRESS = "pass_storage_address" @@ -123,12 +148,16 @@ class NativeBarrierAction(str, Enum): class AssignmentMode(str, Enum): + """Describe whether a setter copies a value, aliases storage, or is unavailable.""" + NONE = "none" VALUE_COPY = "value_copy" ALIAS = "alias" class SetterAction(str, Enum): + """Describe the Python property setter behavior selected by policy completion.""" + WRITE_THROUGH = "write_through" REJECT_REPLACEMENT = "reject_replacement" OMIT = "omit" @@ -136,9 +165,17 @@ class SetterAction(str, Enum): @dataclass(frozen=True) class PolicyActionDispatcher: + """Route a completed object-kind/codegen-action pair to a named lowering method. + + Backends use this dispatcher only after policy completion has attached an + ``OwnershipDecision``. Missing pairs fail closed with ``ValueError``; + the dispatcher never derives an alternative action from a datatype. + """ + handlers: Mapping[tuple[ObjectKind, CodegenAction], str] def handler_name_for_decision(self, decision: OwnershipDecision, name: str) -> str: + """Return the registered handler for ``decision`` or reject the subject ``name``.""" key = (decision.kind, decision.codegen_action) try: return self.handlers[key] @@ -148,11 +185,13 @@ def handler_name_for_decision(self, decision: OwnershipDecision, name: str) -> s ) from None def handler_name(self, var: Any) -> tuple[OwnershipDecision, str]: + """Read a variable's completed decision and return it with its handler name.""" decision = ownership_decision_for_codegen_variable(var) name = str(getattr(var, "name", type(var).__name__)) return decision, self.handler_name_for_decision(decision, name) def dispatch(self, target: Any, var: Any, *args: Any, **kwargs: Any) -> Any: + """Invoke this policy pair's named method on ``target`` with ``var`` and its decision.""" decision, handler_name = self.handler_name(var) handler = getattr(target, handler_name) return handler(var, decision, *args, **kwargs) @@ -165,7 +204,12 @@ def dispatch_decision( *args: Any, **kwargs: Any, ) -> Any: - """Dispatch an accessor or nested policy stored beside its subject.""" + """Dispatch an accessor or nested decision stored beside ``subject``. + + The supplied decision is used directly, so callers can dispatch + getter, setter, or nested policies without attaching it as an + ``ownership_decision`` attribute first. + """ name = str(getattr(subject, "name", getattr(subject, "python_name", type(subject).__name__))) handler = getattr(target, self.handler_name_for_decision(decision, name)) return handler(subject, decision, *args, **kwargs) @@ -173,9 +217,17 @@ def dispatch_decision( @dataclass(frozen=True) class PolicyProjectionDispatcher: + """Route output projections using kind, codegen action, and result projection. + + Projection lowering needs the extra ``projects_result`` axis because the + same native action can either remain an input or appear in Python output. + Missing combinations raise ``ValueError`` rather than choosing a fallback. + """ + handlers: Mapping[tuple[ObjectKind, CodegenAction, bool], str] def handler_name_for_decision(self, decision: OwnershipDecision, name: str) -> str: + """Return the projection handler for ``decision`` or reject the subject ``name``.""" key = (decision.kind, decision.codegen_action, decision.projects_result) try: return self.handlers[key] @@ -186,6 +238,7 @@ def handler_name_for_decision(self, decision: OwnershipDecision, name: str) -> s ) from None def dispatch(self, target: Any, var: Any, *args: Any, **kwargs: Any) -> Any: + """Invoke the selected projection method on ``target`` using ``var``'s decision.""" decision = ownership_decision_for_codegen_variable(var) name = str(getattr(var, "name", type(var).__name__)) handler = getattr(target, self.handler_name_for_decision(decision, name)) @@ -194,9 +247,12 @@ def dispatch(self, target: Any, var: Any, *args: Any, **kwargs: Any) -> Any: @dataclass(frozen=True) class PythonBarrierDispatcher: + """Route a completed Python-boundary action to a named lowering method.""" + handlers: Mapping[PythonBarrierAction, str] def handler_name_for_decision(self, decision: OwnershipDecision, name: str) -> str: + """Return the Python-boundary handler for ``decision`` or reject ``name``.""" try: return self.handlers[decision.python_barrier_action] except KeyError: @@ -205,6 +261,7 @@ def handler_name_for_decision(self, decision: OwnershipDecision, name: str) -> s ) from None def dispatch(self, target: Any, var: Any, *args: Any, **kwargs: Any) -> Any: + """Invoke the selected Python-boundary method with ``var`` and its policy.""" decision = ownership_decision_for_codegen_variable(var) name = str(getattr(var, "name", type(var).__name__)) handler = getattr(target, self.handler_name_for_decision(decision, name)) @@ -213,9 +270,12 @@ def dispatch(self, target: Any, var: Any, *args: Any, **kwargs: Any) -> Any: @dataclass(frozen=True) class NativeBarrierDispatcher: + """Route a completed native-ABI action to a named lowering method.""" + handlers: Mapping[NativeBarrierAction, str] def handler_name_for_decision(self, decision: OwnershipDecision, name: str) -> str: + """Return the native-boundary handler for ``decision`` or reject ``name``.""" try: return self.handlers[decision.native_barrier_action] except KeyError: @@ -224,6 +284,7 @@ def handler_name_for_decision(self, decision: OwnershipDecision, name: str) -> s ) from None def dispatch(self, target: Any, var: Any, *args: Any, **kwargs: Any) -> Any: + """Invoke the selected native-boundary method with ``var`` and its policy.""" decision = ownership_decision_for_codegen_variable(var) name = str(getattr(var, "name", type(var).__name__)) handler = getattr(target, self.handler_name_for_decision(decision, name)) @@ -232,9 +293,12 @@ def dispatch(self, target: Any, var: Any, *args: Any, **kwargs: Any) -> Any: @dataclass(frozen=True) class SetterActionDispatcher: + """Route a completed setter action to a named lowering method without inference.""" + handlers: Mapping[SetterAction, str] def dispatch(self, target: Any, subject: Any, decision: OwnershipDecision, *args: Any) -> Any: + """Invoke ``subject``'s selected setter handler or reject an unregistered action.""" try: handler_name = self.handlers[decision.setter_action] except KeyError: @@ -245,9 +309,12 @@ def dispatch(self, target: Any, subject: Any, decision: OwnershipDecision, *args @dataclass(frozen=True) class DestructionPolicyDispatcher: + """Route a completed release responsibility to a named cleanup method.""" + handlers: Mapping[DestructionPolicy, str] def dispatch(self, target: Any, subject: Any, decision: OwnershipDecision, *args: Any) -> Any: + """Invoke ``subject``'s selected release handler or reject an unregistered policy.""" try: handler_name = self.handlers[decision.destruction] except KeyError: @@ -258,7 +325,7 @@ def dispatch(self, target: Any, subject: Any, decision: OwnershipDecision, *args _STANDARD_SCALAR_TYPES = frozenset( { - "Bool", + *BOOLEAN_SEMANTIC_TYPE_NAMES, "Byte", "CEnum", "Char", @@ -317,8 +384,19 @@ def dispatch(self, target: Any, subject: Any, decision: OwnershipDecision, *args } +# Semantic context and completed decisions + + @dataclass(frozen=True) class OwnershipContext: + """Describe where a semantic value appears and how native code may use it. + + Construct one of the named factories for normal result, argument, field, + or module-variable cases. The resolver combines these flags with storage + facts to select an ownership decision; callers do not need to infer a + codegen action themselves. + """ + location: str = "value" reads_argument: bool = True writes_argument: bool = False @@ -331,6 +409,7 @@ class OwnershipContext: @classmethod def result(cls) -> OwnershipContext: + """Create the context for a direct Python result produced by native code.""" return cls(location="result", reads_argument=False, writes_argument=True, is_result=True) @classmethod @@ -342,6 +421,7 @@ def argument( projects_result: bool = False, python_visible: bool = True, ) -> OwnershipContext: + """Create an argument context from read, write, projection, and visibility facts.""" return cls( location="argument", reads_argument=bool(reads_argument), @@ -353,15 +433,24 @@ def argument( @classmethod def field(cls) -> OwnershipContext: + """Create the context for storage owned by a derived-type instance.""" return cls(location="derived_field", is_field=True) @classmethod def module_variable(cls) -> OwnershipContext: + """Create the context for persistent storage owned by a native module.""" return cls(location="module_variable", is_module_variable=True) def ownership_context_for_argument(function: Any, argument: Any) -> OwnershipContext: - """Build full-signature policy context for one semantic argument.""" + """Build the completed-use context for one semantic function argument. + + The function's projection table and argument metadata determine Python + visibility and result projection; the argument storage then determines + whether native code may write it. The returned context is consumed by + ``OwnershipPolicyResolver`` and does not mutate either input. + """ + # Derive result projection and Python visibility from the full signature. projection = tuple(getattr(function, "projection", ())) argument_name = str(getattr(argument, "name", "")).casefold() mapping = next( @@ -377,6 +466,7 @@ def ownership_context_for_argument(function: Any, argument: Any) -> OwnershipCon explicit_policy = type_metadata.get(OWNERSHIP_POLICY_METADATA) transfer = explicit_policy.get("transfer") if isinstance(explicit_policy, Mapping) else None explicit_call_local_input = transfer == TransferMode.CALL_LOCAL.value and not projects_result + # A source-free descriptor is a normal input unless its contract projects a result. writes_argument = bool( projects_result or ( @@ -469,6 +559,13 @@ def _argument_has_mutable_storage(argument: Any, storage: Any) -> bool: @dataclass(frozen=True) class OwnershipDecision: + """The complete ownership and lowering contract for one semantic value. + + Policy completion stores this immutable record beside semantic values and + wrapper planning projects it into backend-neutral records. A blocked + decision carries its diagnostic in ``blocker`` and must not be lowered. + """ + kind: ObjectKind owner: OwnershipOwner transfer: TransferMode @@ -491,19 +588,24 @@ class OwnershipDecision: @property def owner_label(self) -> str: + """Return the user-facing label for the selected storage owner.""" return _OWNER_LABELS[self.owner] @property def is_blocked(self) -> bool: + """Report whether this decision intentionally prevents wrapper lowering.""" return self.transfer is TransferMode.BLOCKED or self.destruction is DestructionPolicy.BLOCKED @property def is_copy_return(self) -> bool: + """Report whether the Python result receives independent copied storage.""" return self.transfer in {TransferMode.COPY_RETURN, TransferMode.SNAPSHOT_COPY} @dataclass(frozen=True) class _StorageFacts: + """Normalized read-only storage facts used internally by resolver decision branches.""" + rank: int name: str constant: bool = False @@ -521,10 +623,26 @@ class _StorageFacts: Handler = Callable[[_StorageFacts, OwnershipContext], OwnershipDecision] +# Ownership-policy resolution + + class OwnershipPolicyResolver: - """Resolve ownership for semantic types and codegen variables.""" + """Resolve semantic storage and use contexts into completed ownership policy. + + Use this resolver during post-IR policy completion. It classifies the + semantic type, applies declared ownership metadata, validates unsupported + combinations, and attaches lowering actions to the returned immutable + ``OwnershipDecision``. Backends consume those actions but do not call the + resolver to invent a policy during lowering. + """ def __init__(self, handlers: Mapping[ObjectKind, Handler] | None = None): + """Initialize standard kind handlers, optionally replacing selected resolver branches. + + ``handlers`` is an internal extension point for callers that need a + different decision function for an existing object kind. Unspecified + kinds keep the standard policy methods. + """ self._handlers: dict[ObjectKind, Handler] = { ObjectKind.SCALAR: self._scalar_decision, ObjectKind.STRING: self._string_decision, @@ -535,13 +653,25 @@ def __init__(self, handlers: Mapping[ObjectKind, Handler] | None = None): self._handlers.update(handlers) def decide_semantic_type(self, semantic_type: Any, context: OwnershipContext) -> OwnershipDecision: + """Complete ownership, lifetime, and ABI policy for one semantic type. + + Use this primitive when the caller already knows the type's semantic + location. It reads type metadata without mutation and returns either + a lowering-ready decision or a fail-closed decision whose ``blocker`` + explains the unsupported contract. + """ + # Normalize source/contract representation into resolver-specific facts. facts = self._semantic_facts(semantic_type) - decision = self._apply_overrides(self._decide(facts, context), facts, context) + # Choose the default policy for the type kind and semantic location. + decision = self._decide(facts, context) + # Apply explicit contract policy, then reject unsafe or contradictory combinations. + decision = self._apply_overrides(decision, facts, context) decision = self._validate_aliased_decision(decision, facts, context) decision = self._validate_pointer_decision(decision, facts, context) decision = self._complete_immutable_policy(decision, facts, context) decision = self._validate_result_projection(decision, context) decision = self._validate_policy_combination(decision) + # Derive lowering actions only after the lifetime contract is final. completed = replace( decision, boundary_storage_mode=decision.boundary_storage_mode or decision.storage_mode, @@ -560,6 +690,12 @@ def decide_semantic_variable( variable: Any, context: OwnershipContext | None = None, ) -> OwnershipDecision: + """Complete policy for a semantic variable, inferring its usual location when absent. + + ``context`` overrides automatic field/argument inference. Optional + projected outputs gain nullability on the returned decision; neither + the variable nor its semantic type is modified. + """ actual_context = context or self._semantic_variable_context(variable) decision = self.decide_semantic_type(variable.semantic_type, actual_context) if bool(getattr(variable, "optional", False)) and actual_context.projects_result: @@ -571,7 +707,12 @@ def decide_semantic_getter( variable: Any, context: OwnershipContext, ) -> OwnershipDecision: - """Decide the value exposed by a field or module-variable getter.""" + """Complete the value policy exposed by a field or module-variable getter. + + Array and derived storage retains its storage decision; scalar/string + getters normally receive result policy so Python observes a value + rather than native container storage. + """ storage = self.decide_semantic_variable(variable, context) if storage.is_blocked or storage.kind in {ObjectKind.NUMPY_ARRAY, ObjectKind.DERIVED_TYPE}: return storage @@ -584,7 +725,12 @@ def decide_semantic_setter( variable: Any, context: OwnershipContext, ) -> OwnershipDecision: - """Decide setter availability and its incoming value conversion.""" + """Complete setter exposure and incoming conversion for a field or module variable. + + Constants and blocked storage omit the setter. Supported storage uses + argument policy for the incoming value, then records whether lowering + must copy, alias, reject replacement, or expose write-through. + """ storage = self.decide_semantic_variable(variable, context) if self._is_semantic_constant(variable.semantic_type): return replace( @@ -613,7 +759,12 @@ def _setter_action( incoming: OwnershipDecision, context: OwnershipContext, ) -> SetterAction: - """Select Python property setter exposure from completed storage and input policy.""" + """Select Python setter exposure from completed storage and incoming policy. + + The result is a pure action choice. It preserves special scalar, + string-field, and derived module-variable rules already decided by the + ownership contract. + """ if storage.kind is ObjectKind.SCALAR: if storage.transfer is TransferMode.SNAPSHOT_COPY and storage.nullable: return SetterAction.REJECT_REPLACEMENT @@ -627,6 +778,12 @@ def _setter_action( return SetterAction.REJECT_REPLACEMENT def decide_semantic_function(self, function: Any, prefix: str = "") -> dict[str, OwnershipDecision]: + """Return completed decisions for a function's ordered arguments and direct return. + + ``prefix`` namespaces the stable mapping keys for enclosing class or + overload owners. Argument contexts use the complete projection table; + no decision is written back to ``function``. + """ name = f"{prefix}{function.name}" decisions = { f"{name}.{argument.name}": self.decide_semantic_variable( @@ -641,6 +798,12 @@ def decide_semantic_function(self, function: Any, prefix: str = "") -> dict[str, return decisions def decide_semantic_class(self, semantic_class: Any, prefix: str = "") -> dict[str, OwnershipDecision]: + """Return completed decisions for one class, including nested classes and methods. + + Mapping keys follow declaration ownership paths. Fields use field + context while methods reuse function processing; the class remains + unchanged. + """ name = f"{prefix}{semantic_class.name}" decisions = { f"{name}.{field.name}": self.decide_semantic_variable(field, OwnershipContext.field()) @@ -653,6 +816,13 @@ def decide_semantic_class(self, semantic_class: Any, prefix: str = "") -> dict[s return decisions def decide_semantic_module(self, module: Any) -> dict[str, OwnershipDecision]: + """Return completed decisions for module state, classes, functions, and overloads. + + Results are keyed by stable module-qualified paths in declaration order + where each source collection supplies that order. This convenience + traversal is read-only and is normally used for diagnostics or tests; + policy completion owns metadata attachment. + """ name = str(getattr(module, "name", "module")) decisions = { f"{name}.{variable.name}": self.decide_semantic_variable( @@ -672,6 +842,7 @@ def decide_semantic_module(self, module: Any) -> dict[str, OwnershipDecision]: return decisions def _decide(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Choose the unoverridden decision branch for normalized facts and location.""" if context.is_module_variable: return self._module_variable_decision(facts, context) if context.is_field: @@ -680,6 +851,7 @@ def _decide(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipD return self._handlers[kind](facts, context) def _kind(self, facts: _StorageFacts, context: OwnershipContext) -> ObjectKind: + """Classify normalized storage into the resolver's four policy categories.""" if facts.scalar_storage and not facts.is_string and not facts.allocatable and not facts.pointer: return ObjectKind.NUMPY_ARRAY if facts.rank > 0 or facts.is_ndarray: @@ -691,6 +863,7 @@ def _kind(self, facts: _StorageFacts, context: OwnershipContext) -> ObjectKind: return ObjectKind.SCALAR def _scalar_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return default scalar policy, delegating specialized storage and address cases.""" if facts.scalar_storage: return self._scalar_storage_decision(facts, context) if facts.address_role == ADDRESS_ROLE_PROJECTION: @@ -753,6 +926,7 @@ def _scalar_decision(self, facts: _StorageFacts, context: OwnershipContext) -> O @staticmethod def _scalar_storage_decision(facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return policy for rank-zero scalar storage exposed through an array-like boundary.""" if context.is_result: return OwnershipDecision( ObjectKind.SCALAR, @@ -780,6 +954,7 @@ def _scalar_storage_decision(facts: _StorageFacts, context: OwnershipContext) -> @staticmethod def _address_projection_scalar_decision(facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return policy for a scalar passed through its explicit native-address projection.""" if context.is_result: return OwnershipDecision( ObjectKind.SCALAR, @@ -817,6 +992,7 @@ def _address_projection_scalar_decision(facts: _StorageFacts, context: Ownership ) def _allocatable_scalar_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return detached accessor or descriptor-boundary policy for an allocatable scalar.""" if context.is_field or context.is_module_variable: return OwnershipDecision( ObjectKind.SCALAR, @@ -835,6 +1011,7 @@ def _allocatable_scalar_decision(self, facts: _StorageFacts, context: OwnershipC ) def _pointer_scalar_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return detached accessor or descriptor-boundary policy for a pointer scalar.""" if context.is_field or context.is_module_variable: return OwnershipDecision( ObjectKind.SCALAR, @@ -860,6 +1037,12 @@ def _function_scalar_descriptor_decision( *, reason: str, ) -> OwnershipDecision: + """Return scalar descriptor policy for a function argument or result boundary. + + ``boundary_storage_mode`` identifies allocatable versus pointer ABI + storage. Writable descriptors must project an output or return a + blocked decision, keeping replacement semantics explicit. + """ if context.is_result: return OwnershipDecision( ObjectKind.SCALAR, @@ -923,6 +1106,7 @@ def _function_scalar_descriptor_decision( ) def _string_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return default string policy after handling descriptor, raw-address, and storage cases.""" descriptor_decision = self._string_descriptor_decision(facts, context) if descriptor_decision is not None: return descriptor_decision @@ -962,6 +1146,7 @@ def _string_descriptor_decision( facts: _StorageFacts, context: OwnershipContext, ) -> OwnershipDecision | None: + """Return a scalar string descriptor decision when storage requires one, else ``None``.""" if not (facts.allocatable or facts.pointer): return None @@ -997,6 +1182,7 @@ def _string_descriptor_decision( @staticmethod def _scalar_string_storage_decision(context: OwnershipContext) -> OwnershipDecision: + """Return aliasing or call-local policy for rank-zero mutable character storage.""" if context.is_result: return OwnershipDecision( ObjectKind.STRING, @@ -1026,6 +1212,7 @@ def _scalar_string_storage_decision(context: OwnershipContext) -> OwnershipDecis @staticmethod def _string_output_argument_decision(context: OwnershipContext) -> OwnershipDecision: + """Return string-output policy, copying only when its mutation is projected to Python.""" if not context.projects_result: return OwnershipDecision( ObjectKind.STRING, @@ -1046,6 +1233,7 @@ def _string_output_argument_decision(context: OwnershipContext) -> OwnershipDeci @staticmethod def _string_update_argument_decision(context: OwnershipContext) -> OwnershipDecision: + """Return string update policy, preserving immutable Python replacement semantics.""" if not context.projects_result: return OwnershipDecision( ObjectKind.STRING, @@ -1065,6 +1253,7 @@ def _string_update_argument_decision(context: OwnershipContext) -> OwnershipDeci ) def _array_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return array or native-descriptor policy for the current semantic location.""" if _is_native_array_handle_facts(facts): if context.is_argument: if context.projects_result and not context.python_visible: @@ -1115,7 +1304,11 @@ def _native_array_handle_argument_decision( facts: _StorageFacts, context: OwnershipContext, ) -> OwnershipDecision: - """Pass a native descriptor handle as a caller-owned descriptor argument.""" + """Pass a native descriptor handle as a caller-owned descriptor argument. + + Writable pointer descriptors require explicit pointer-policy metadata; + all other supported handle inputs retain the caller's handle identity. + """ if context.writes_argument: if facts.pointer and not isinstance((facts.metadata or {}).get(POINTER_POLICY_METADATA), Mapping): return OwnershipDecision( @@ -1189,6 +1382,7 @@ def _native_array_handle_result_decision(facts: _StorageFacts) -> OwnershipDecis ) def _allocatable_array_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return allocatable-array policy for fields, module state, calls, or results.""" if context.is_field: return OwnershipDecision( ObjectKind.NUMPY_ARRAY, @@ -1234,6 +1428,7 @@ def _allocatable_array_decision(self, facts: _StorageFacts, context: OwnershipCo ) def _pointer_array_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return pointer-array policy without claiming ownership of an unknown target.""" if context.is_field or context.is_module_variable: owner = OwnershipOwner.WRAPPER if context.is_field else OwnershipOwner.NATIVE destruction = DestructionPolicy.WRAPPER_DEALLOC if context.is_field else DestructionPolicy.NATIVE_OWNER @@ -1276,6 +1471,7 @@ def _pointer_array_decision(self, facts: _StorageFacts, context: OwnershipContex ) def _derived_type_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return wrapper-instance policy for derived values, arguments, and outputs.""" descriptor_boundary = facts.allocatable or facts.pointer boundary_storage = StorageMode.HEAP if facts.allocatable else StorageMode.ALIAS argument_boundary_storage = StorageMode.ALIAS @@ -1335,6 +1531,7 @@ def _derived_type_decision(self, facts: _StorageFacts, context: OwnershipContext ) def _module_variable_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return policy for persistent module storage, preserving native ownership by default.""" if facts.constant: return self._module_constant_decision(facts) if facts.is_custom: @@ -1377,12 +1574,11 @@ def _module_constant_decision(self, facts: _StorageFacts) -> OwnershipDecision: if facts.rank > 0 or facts.is_ndarray: return OwnershipDecision( ObjectKind.NUMPY_ARRAY, - OwnershipOwner.UNKNOWN, - TransferMode.BLOCKED, - DestructionPolicy.BLOCKED, - storage_mode=StorageMode.STACK, - blocker="array constants need explicit immutable value-copy policy", - reason="module constants are values rather than mutable native array storage", + OwnershipOwner.PYTHON, + TransferMode.BY_VALUE, + DestructionPolicy.PYTHON_REFCOUNT, + storage_mode=StorageMode.HEAP, + reason="module array constants are materialized once as immutable Python-owned snapshots", ) if facts.is_custom: return OwnershipDecision( @@ -1403,6 +1599,7 @@ def _module_constant_decision(self, facts: _StorageFacts) -> OwnershipDecision: ) def _derived_field_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: + """Return policy for storage that remains owned by the containing derived wrapper.""" if facts.allocatable and facts.rank == 0: return self._allocatable_scalar_decision(facts, context) if facts.pointer and facts.rank == 0: @@ -1450,6 +1647,12 @@ def _apply_overrides( facts: _StorageFacts, context: OwnershipContext, ) -> OwnershipDecision: + """Apply declared ownership metadata without bypassing later safety validation. + + Pointer container metadata stays separate from general ownership + overrides. Unsupported borrowed pointer views become blocked here so + lower stages cannot fabricate target retention. + """ metadata = facts.metadata or {} raw = metadata.get(OWNERSHIP_POLICY_METADATA) pointer_policy = metadata.get(POINTER_POLICY_METADATA) @@ -1508,6 +1711,7 @@ def _validate_pointer_decision( facts: _StorageFacts, context: OwnershipContext, ) -> OwnershipDecision: + """Block pointer cases whose requested lifetime or reassociation mechanism is unsupported.""" if not facts.pointer or decision.is_blocked: return decision if (context.is_argument or context.is_result) and _is_native_array_handle_facts(facts): @@ -1576,6 +1780,12 @@ def _complete_immutable_policy( facts: _StorageFacts, context: OwnershipContext, ) -> OwnershipDecision: + """Adapt writable immutable values to replacement, discarded-copy, or blocked policy. + + Only writable argument contexts with explicit immutable metadata are + changed. The returned decision makes replacement projection explicit + before ABI action selection. + """ metadata = facts.metadata or {} if metadata.get(PYTHON_VALUE_MUTABILITY_METADATA) != PYTHON_VALUE_IMMUTABLE: return decision @@ -1654,6 +1864,7 @@ def _validate_result_projection( decision: OwnershipDecision, context: OwnershipContext, ) -> OwnershipDecision: + """Block copy-return arguments that have no declared Python result projection.""" if ( decision.is_blocked or not context.is_argument @@ -1706,6 +1917,7 @@ def _validate_policy_combination(decision: OwnershipDecision) -> OwnershipDecisi @staticmethod def _codegen_action(decision: OwnershipDecision, context: OwnershipContext) -> CodegenAction: + """Derive the strict lowering action after a lifetime decision has been validated.""" if decision.is_blocked: return CodegenAction.BLOCKED if context.is_argument and context.writes_argument and not context.reads_argument: @@ -1729,6 +1941,7 @@ def _python_barrier_action( facts: _StorageFacts, context: OwnershipContext, ) -> PythonBarrierAction: + """Derive the Python-to-wrapper barrier action for a completed argument decision.""" if decision.is_blocked: return PythonBarrierAction.BLOCKED if not context.is_argument or not context.python_visible: @@ -1759,6 +1972,7 @@ def _native_barrier_action( facts: _StorageFacts, context: OwnershipContext, ) -> NativeBarrierAction: + """Derive the wrapper-to-native ABI action for a completed argument decision.""" if decision.is_blocked: return NativeBarrierAction.BLOCKED if not context.is_argument: @@ -1796,6 +2010,7 @@ def _uses_descriptor_call_local_boundary( facts: _StorageFacts, context: OwnershipContext, ) -> bool: + """Report whether a scalar descriptor needs call-local address storage at the ABI boundary.""" return bool( context.is_argument and decision.kind is ObjectKind.SCALAR @@ -1806,6 +2021,7 @@ def _uses_descriptor_call_local_boundary( @staticmethod def _passes_scalar_storage_address(decision: OwnershipDecision, facts: _StorageFacts) -> bool: + """Report whether scalar storage or identity output passes its caller storage address.""" return bool( facts.scalar_storage or ( @@ -1816,6 +2032,7 @@ def _passes_scalar_storage_address(decision: OwnershipDecision, facts: _StorageF @staticmethod def _passes_scalar_alias_address(decision: OwnershipDecision, facts: _StorageFacts) -> bool: + """Report whether a scalar alias must cross the ABI as a storage address.""" return bool( decision.kind is ObjectKind.SCALAR and decision.storage_mode is StorageMode.ALIAS @@ -1824,6 +2041,11 @@ def _passes_scalar_alias_address(decision: OwnershipDecision, facts: _StorageFac @staticmethod def _enum_value(enum_type: type[Enum], value: object, default: Any) -> Any: + """Convert one optional metadata value to an enum, preserving ``default`` when absent. + + Invalid present values raise ``ValueError`` listing the accepted enum + values so malformed contracts fail during policy completion. + """ if value is None: return default try: @@ -1838,6 +2060,7 @@ def _storage_for_override( transfer: TransferMode, default: StorageMode, ) -> StorageMode: + """Choose storage implied by an override while preserving pointer/allocatable invariants.""" if facts.pointer: return StorageMode.ALIAS if facts.allocatable: @@ -1848,6 +2071,12 @@ def _storage_for_override( @staticmethod def _semantic_facts(semantic_type: Any) -> _StorageFacts: + """Normalize a semantic type's storage and metadata into resolver-specific facts. + + This is read-only: it consumes the semantic representation and returns + a compact immutable record that keeps type inspection out of policy + branches. + """ metadata = getattr(semantic_type, "metadata", {}) or {} constraints = getattr(semantic_type, "constraints", ()) or () storage = getattr(semantic_type, "storage", None) @@ -1877,11 +2106,18 @@ def _semantic_facts(semantic_type: Any) -> _StorageFacts: @staticmethod def _is_semantic_constant(semantic_type: Any) -> bool: + """Report whether a semantic type carries the ``Constant`` constraint.""" constraints = getattr(semantic_type, "constraints", ()) or () return any(getattr(constraint, "name", None) == "Constant" for constraint in constraints) @staticmethod def _semantic_variable_context(variable: Any) -> OwnershipContext: + """Infer field or argument context from a semantic variable's concrete model type. + + Arguments preserve mutability and output-projection facts; all other + unrecognized variables use neutral value context. The variable is not + changed. + """ class_name = type(variable).__name__ if class_name == "SemanticField": return OwnershipContext.field() @@ -1897,6 +2133,9 @@ def _semantic_variable_context(variable: Any) -> OwnershipContext: return OwnershipContext(location="value") +# Contract metadata and lowering gates + + def set_ownership_metadata( metadata: dict[str, Any], *, @@ -1904,6 +2143,12 @@ def set_ownership_metadata( transfer: str | None = None, destruction: str | None = None, ) -> None: + """Store validated owner, transfer, and destruction metadata on a semantic mapping. + + Use this when constructing or editing a semantic contract. Provided + values are normalized through their enums; an existing non-dictionary + ownership policy raises ``ValueError`` rather than being overwritten. + """ policy = metadata.setdefault(OWNERSHIP_POLICY_METADATA, {}) if not isinstance(policy, dict): raise ValueError(f"{OWNERSHIP_POLICY_METADATA!r} metadata must be a dictionary") @@ -1916,7 +2161,12 @@ def set_ownership_metadata( def set_pointer_policy_metadata(metadata: dict[str, Any], **policy_values: Any) -> None: - """Store a complete semantic pointer policy after validating its shape.""" + """Store a complete semantic pointer policy after validating its shape. + + Callers must provide exactly ``POINTER_POLICY_FIELDS``. The helper mutates + ``metadata`` with the checked policy and its ``fortran_pointer`` marker; + malformed values raise ``ValueError`` before policy resolution. + """ missing = [name for name in POINTER_POLICY_FIELDS if name not in policy_values] extra = [name for name in policy_values if name not in POINTER_POLICY_FIELDS] if missing or extra: @@ -1940,6 +2190,12 @@ def set_pointer_policy_metadata(metadata: dict[str, Any], **policy_values: Any) def ownership_decision_for_codegen_variable(var: Any) -> OwnershipDecision: + """Return a lowering variable's completed decision or reject incomplete semantic policy. + + Bridge and binding code use this gate instead of reconstructing ownership + from backend datatypes. Missing policy raises ``ValueError`` with the + required post-IR completion step. + """ decision = getattr(var, "ownership_decision", None) if decision is None: name = getattr(var, "name", type(var).__name__) @@ -1951,12 +2207,36 @@ def ownership_decision_for_codegen_variable(var: Any) -> OwnershipDecision: def codegen_action_for_variable(var: Any) -> CodegenAction: + """Return ``var``'s completed lowering action after enforcing policy presence.""" return ownership_decision_for_codegen_variable(var).codegen_action def python_barrier_action_for_variable(var: Any) -> PythonBarrierAction: + """Return ``var``'s completed Python-boundary action after enforcing policy presence.""" return ownership_decision_for_codegen_variable(var).python_barrier_action def native_barrier_action_for_variable(var: Any) -> NativeBarrierAction: + """Return ``var``'s completed native-ABI action after enforcing policy presence.""" return ownership_decision_for_codegen_variable(var).native_barrier_action + + +# Direct ownership-resolution example + + +if __name__ == "__main__": + from prik.semantics.models import SemanticArgument, SemanticFunction, SemanticType + + semantic_function = SemanticFunction( + name="scale", + arguments=[SemanticArgument("value", SemanticType("Float64", dtype="Float64"))], + return_type=SemanticType("Float64", dtype="Float64"), + ) + semantic_argument = semantic_function.arguments[0] + argument_context = ownership_context_for_argument(semantic_function, semantic_argument) + print(f"before: math.scale({semantic_argument.name}): {semantic_argument.semantic_type.name} semantic IR") + decision = default_ownership_policy.decide_semantic_type(semantic_argument.semantic_type, argument_context) + print( + f"after: {decision.kind.value}/{decision.owner.value}/{decision.transfer.value}; " + f"{decision.python_barrier_action.value} -> {decision.native_barrier_action.value}" + ) diff --git a/prik/semantics/policy_completion.py b/prik/semantics/policy_completion.py index 530c9d7b7..17e454922 100644 --- a/prik/semantics/policy_completion.py +++ b/prik/semantics/policy_completion.py @@ -1,4 +1,10 @@ -"""Complete post-IR semantic policies before wrapper planning or lowering.""" +"""Complete semantic policy decisions after IR construction and before planning. + +This module turns full semantic signatures into the ownership, transfer, +destruction, mutability, storage, accessor, projection, and ABI decisions that +wrapper planning consumes. It is the final semantic authority: lower stages +may dispatch from its completed policies but must not infer replacements. +""" from __future__ import annotations @@ -89,6 +95,9 @@ ) +# Public completion entrypoint + + def complete_semantic_policies( semantic_ir: models.SemanticModule | Iterable[models.SemanticModule], *, @@ -96,22 +105,34 @@ def complete_semantic_policies( ) -> list[models.SemanticModule]: """Complete policy decisions for semantic modules after parser-to-IR conversion. - This is the shared post-IR boundary for policies that need full semantic - context. It completes entry export reachability, ownership, transfer, destruction, - mutability/writeback, projection, nullability, release, codegen action, and - contract/boundary storage modes, getter behavior, native setter assignment, - and Python setter exposure. Future policy passes must be added here instead - of in wrapper planning, lowering, bridges, or bindings. + Use this after semantic conversion and before wrapper planning. It accepts + either one module or any iterable of modules, mutates each in place, and + returns an ordered list of those same objects for pipeline chaining. + ``strict_wrapper_names`` is forwarded to export and class-surface policy + validation. Invalid or incomplete semantic contracts raise ``ValueError`` + rather than leaving a lower stage to choose a fallback. + + This shared post-IR boundary completes entry export reachability, ownership, + transfer, destruction, mutability/writeback, projection, nullability, + release, codegen action, contract/boundary storage modes, getter behavior, + native setter assignment, and Python setter exposure. Future policy passes + belong here, not in planning, lowering, bridges, or bindings. """ modules = list(semantic_ir) if not isinstance(semantic_ir, models.SemanticModule) else [semantic_ir] for module in modules: + # Limit entry declarations before completing their dependent policies. _complete_entry_export_policy(module) complete_python_export_policy(module, strict_wrapper_names=strict_wrapper_names) + + # Resolve all remaining ownership and wrapper-facing semantic choices. _complete_ownership_policies(module, strict_wrapper_names=strict_wrapper_names) return modules +# Entry export reachability + + def _complete_entry_export_policy(module: models.SemanticModule) -> None: """Remove public declarations not reachable from an explicit entry export policy.""" if not module.metadata.get(models.PYTHON_EXPORTS_PREPARED_METADATA): @@ -124,12 +145,14 @@ def _complete_entry_export_policy(module: models.SemanticModule) -> None: def _is_entry_export_reachable(declaration: object) -> bool: + """Keep private declarations and public declarations selected by entry exports.""" if getattr(declaration, "visibility", "public") == "private": return True return bool(_entry_exports(declaration)) def _entry_exports(declaration: object) -> object: + """Return a declaration's entry-export metadata, with overloads using their first procedure.""" if isinstance(declaration, models.ProcedureOverloadSet): if not declaration.procedures: return () @@ -151,11 +174,15 @@ def _complete_ownership_policies( signatures are known and before ``ir2ast`` lowering. """ + # Resolve identities before any class, field, or callable policy uses them. _complete_local_derived_type_identities(module) + + # Complete persistent module state and its accessors first. for variable in module.variables: _complete_variable(variable, OwnershipContext.module_variable()) _complete_accessor_policies(variable, OwnershipContext.module_variable()) _complete_module_variable_initializer(variable) + # Complete classes, derived-type graph facts, and wrapper-facing surfaces. for semantic_class in module.classes: class_scope = str(semantic_class.origin.native_scope or module.name) _complete_class(semantic_class, f"{class_scope}.{semantic_class.name}") @@ -173,6 +200,7 @@ def _complete_ownership_policies( polymorphic_variants, ) _complete_class_overload_policies(module.classes) + # Attach module-variable policies after their type and accessor facts exist. for variable in module.variables: variable_scope = str(variable.origin.native_scope or module.name) variable.metadata[models.RESOLVED_MODULE_VARIABLE_POLICY_METADATA] = build_module_variable_policy( @@ -180,6 +208,7 @@ def _complete_ownership_policies( module_name=variable_scope, derived_types=derived_types, ) + # Complete direct functions and overload candidates with their full context. for function in module.functions: function_scope = str(function.origin.native_scope or module.name) _complete_function( @@ -197,6 +226,7 @@ def _complete_ownership_policies( f"{procedure_scope}.{overload_set.name}.{procedure.name}", derived_types=derived_types, ) + # Build resolved module overload tables after every candidate is complete. overload_functions = { f"{(procedure.origin.native_scope or module.name)!s}.{overload_set.name}.{procedure.name}": procedure for overload_set in module.overload_sets @@ -214,12 +244,16 @@ def _complete_ownership_policies( return module +# Derived-type identity and class policy completion + + def _complete_local_derived_type_identities(module: models.SemanticModule) -> None: """Attach canonical native identities to unqualified local type references.""" identities: dict[str, set[tuple[str, str]]] = {} scoped_identities: dict[tuple[str, str], set[tuple[str, str]]] = {} def collect_class(semantic_class: models.SemanticClass) -> None: + """Index one nested class under unscoped and native-scope identity keys.""" identity = ( str(semantic_class.origin.native_scope or module.name), str(semantic_class.native_name or semantic_class.name), @@ -233,6 +267,7 @@ def collect_class(semantic_class: models.SemanticClass) -> None: collect_class(semantic_class) def annotate(semantic_type: models.SemanticType | None, native_scope: str) -> None: + """Attach a unique local derived identity while leaving external references untouched.""" if semantic_type is None or models.EXTERNAL_TYPE_REF_METADATA in semantic_type.metadata: return matches = scoped_identities.get((native_scope, semantic_type.name), set()) @@ -242,12 +277,14 @@ def annotate(semantic_type: models.SemanticType | None, native_scope: str) -> No semantic_type.metadata[models.RESOLVED_DERIVED_TYPE_IDENTITY_METADATA] = next(iter(matches)) def annotate_function(function: models.SemanticFunction) -> None: + """Annotate one function's argument and direct-return type identities.""" native_scope = str(function.origin.native_scope or module.name) for argument in function.arguments: annotate(argument.semantic_type, native_scope) annotate(function.return_type, native_scope) def annotate_class(semantic_class: models.SemanticClass) -> None: + """Recursively annotate one class's fields, methods, overloads, and nested classes.""" native_scope = str(semantic_class.origin.native_scope or module.name) for field in semantic_class.fields: annotate(field.semantic_type, native_scope) @@ -271,6 +308,11 @@ def annotate_class(semantic_class: models.SemanticClass) -> None: def _complete_class(semantic_class: models.SemanticClass, owner_path: str) -> None: + """Complete one class's instance, self, field, accessor, and derived-type policies. + + The class and its fields are mutated in place, then nested classes receive + the corresponding owner path. The method does not construct wrapper plans. + """ class_type = models.SemanticType(name=semantic_class.name, dtype=semantic_class.name) semantic_class.metadata[models.RESOLVED_CLASS_INSTANCE_POLICY_METADATA] = ( default_ownership_policy.decide_semantic_type(class_type, OwnershipContext.result()) @@ -301,6 +343,7 @@ def _derived_type_policy_map( policies: dict[tuple[str, str], DerivedTypePolicy] = {} def collect(semantic_class: models.SemanticClass) -> None: + """Store an already-completed class policy under its canonical identity.""" policy = semantic_class.metadata.get(models.RESOLVED_DERIVED_TYPE_POLICY_METADATA) if isinstance(policy, DerivedTypePolicy): policies[policy.type_identity] = policy @@ -319,6 +362,7 @@ def _complete_derived_type_graph_policies( policies = _derived_type_policy_map(classes) def complete(semantic_class: models.SemanticClass) -> None: + """Complete recursive member-path policies for one class before indexing it.""" policy = semantic_class.metadata.get(models.RESOLVED_DERIVED_TYPE_POLICY_METADATA) if isinstance(policy, DerivedTypePolicy): _paths, graph_blockers = derived_member_path_policies(policy, policies) @@ -800,6 +844,7 @@ def _polymorphic_variant_map( bases = {surface.type_identity: surface.base_identities for surface in surfaces} def extends(candidate: tuple[str, str], base: tuple[str, str]) -> bool: + """Report whether a completed class identity is the base or extends it transitively.""" return candidate == base or any(extends(parent, base) for parent in bases.get(candidate, ())) identities = tuple(surface.type_identity for surface in surfaces) @@ -846,7 +891,16 @@ def _complete_function( polymorphic_variants: dict[tuple[str, str], tuple[tuple[str, str], ...]] | None = None, native_dispatch_name: str | None = None, ) -> None: + """Complete one function's arguments, result, special policies, and wrapper policy. + + The function is mutated in place. It first validates/normalizes address + projections, then resolves per-argument and result ownership, adds native + handle and status policies, and finally builds the wrapper-facing policy. + """ + # Normalize signature-level ABI facts before ownership uses them. _complete_callable_address_policy(function) + + # Complete inputs and the direct return's ownership policy. for argument in function.arguments: _complete_variable( argument, @@ -861,6 +915,7 @@ def _complete_function( else: function.metadata.pop(models.RESOLVED_RETURN_OWNERSHIP_POLICY_METADATA, None) function.metadata.pop(models.RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA, None) + # Complete cross-result status handling before the wrapper policy consumes it. _complete_native_status_error_policy(function, owner_path) function.metadata[models.RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] = build_function_wrapper_policy( function, @@ -956,6 +1011,7 @@ def _native_status_output( def _is_compatible_status_handoff(decision: OwnershipDecision) -> bool: + """Report whether a hidden scalar/string result has a valid status handoff action.""" expected_action = { ObjectKind.SCALAR: CodegenAction.DIRECT_VALUE, ObjectKind.STRING: CodegenAction.COPY_OUT, @@ -964,6 +1020,7 @@ def _is_compatible_status_handoff(decision: OwnershipDecision) -> bool: def _is_scalar_integer_status(semantic_type_name: str) -> bool: + """Report whether a semantic scalar name can serve as a native status code.""" return semantic_type_name in { "Byte", "CEnum", @@ -982,6 +1039,7 @@ def _is_scalar_integer_status(semantic_type_name: str) -> bool: def _fixed_character_length(semantic_type: models.SemanticType) -> int | None: + """Return a positive rank-zero character length if the contract provides one.""" if semantic_type.rank != 0: return None value = semantic_type.metadata.get("fortran_character_length") @@ -996,6 +1054,7 @@ def _complete_native_array_handle_result_policy( function: models.SemanticFunction, decision: OwnershipDecision, ) -> None: + """Attach or clear a direct-result native-array-handle policy on a function.""" return_type = function.return_type descriptor_kind = native_array_descriptor_kind(return_type) if descriptor_kind is None or return_type is None: @@ -1023,6 +1082,12 @@ def _complete_native_array_handle_variable_policy( variable: models.SemanticVariable, context: OwnershipContext, ) -> None: + """Attach or clear a native-array-handle policy for one variable or argument. + + The variable's resolved ownership policy must already exist; a descriptor + type receives a completed handle policy derived from that decision and its + semantic context. + """ descriptor_kind = native_array_descriptor_kind(variable.semantic_type) if descriptor_kind is None: variable.metadata.pop(models.RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA, None) @@ -1046,6 +1111,12 @@ def _native_array_handle_policy( *, variable: models.SemanticVariable | None = None, ) -> NativeArrayHandlePolicy: + """Build the complete runtime contract for one native allocatable/pointer handle. + + This pure policy builder combines descriptor kind, completed ownership, and + declaration context into ownership, access, lifetime, operations, and ABI + facts. It does not mutate the semantic type or generate wrapper code. + """ optional_absent = bool( variable is not None and variable.optional and semantic_type.metadata.get(OPTIONAL_ABSENT_HANDLE_METADATA) ) @@ -1096,6 +1167,7 @@ def _native_array_handle_kind( *, optional_absent: bool, ) -> str: + """Classify a descriptor by declaration context and optional-absence state.""" if optional_absent: return "optional_absent_handle" if context.is_module_variable: @@ -1129,6 +1201,7 @@ def _native_array_default_construction( def _native_array_handle_origin(context: OwnershipContext) -> str: + """Return the semantic origin label used by a completed descriptor policy.""" if context.is_module_variable: return "module_variable" if context.is_field: @@ -1143,6 +1216,7 @@ def _native_array_handle_origin(context: OwnershipContext) -> str: def _native_array_handle_owner(handle_kind: str) -> str: + """Return the owner category prescribed for one completed descriptor kind.""" return { "argument_descriptor": "caller", "borrowed_field_descriptor": "wrapper", @@ -1153,6 +1227,7 @@ def _native_array_handle_owner(handle_kind: str) -> str: def _native_array_owner_retention(handle_kind: str) -> str: + """Return the object or storage that retains a descriptor policy's owner.""" return { "argument_descriptor": "caller_handle", "borrowed_field_descriptor": "parent_wrapper", @@ -1163,6 +1238,7 @@ def _native_array_owner_retention(handle_kind: str) -> str: def _native_array_descriptor_ownership(handle_kind: str) -> str: + """Return whether the descriptor object itself is owned, borrowed, or unknown.""" if handle_kind == "owned_result_descriptor": return "owned" if handle_kind == "unsupported": @@ -1171,6 +1247,7 @@ def _native_array_descriptor_ownership(handle_kind: str) -> str: def _native_array_getter_behavior(handle_kind: str, context: OwnershipContext, blocker: str | None) -> str: + """Choose a descriptor getter result only when the completed policy is supported.""" if blocker is not None: return "blocked" if context.is_module_variable or context.is_field: @@ -1181,6 +1258,7 @@ def _native_array_getter_behavior(handle_kind: str, context: OwnershipContext, b def _native_array_python_setter(variable: models.SemanticVariable | None) -> str: + """Return the resolved Python setter action for a descriptor variable, if applicable.""" setter = None if variable is None else variable.metadata.get(models.RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA) if setter is None: return "none" @@ -1188,6 +1266,7 @@ def _native_array_python_setter(variable: models.SemanticVariable | None) -> str def _native_array_native_setter(variable: models.SemanticVariable | None) -> str: + """Return the resolved native assignment action for a descriptor variable, if applicable.""" setter = None if variable is None else variable.metadata.get(models.RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA) if setter is None: return "none" @@ -1199,6 +1278,7 @@ def _native_array_output_projection( handle_kind: str, context: OwnershipContext, ) -> str: + """Return how a descriptor becomes a Python-visible output in its context.""" if handle_kind == "unsupported": return "unsupported" if context.is_result: @@ -1214,6 +1294,7 @@ def _native_array_result_allocation( context: OwnershipContext, semantic_type: models.SemanticType, ) -> str: + """Return the allocation guarantee for an allocatable descriptor function result.""" if context.is_result and descriptor_kind == "allocatable" and handle_kind == "owned_result_descriptor": if semantic_type.metadata.get(MAYBE_UNALLOCATED_METADATA): return "maybe_unallocated" @@ -1222,6 +1303,7 @@ def _native_array_result_allocation( def _native_array_release_responsibility(handle_kind: str) -> str: + """Return the component responsible for releasing one descriptor handle kind.""" return { "argument_descriptor": "none", "borrowed_field_descriptor": "wrapper_dealloc", @@ -1238,6 +1320,7 @@ def _native_array_target_lifetime( semantic_type: models.SemanticType, blocker: str | None, ) -> str: + """Return the declared target lifetime, honoring explicit pointer-lifetime metadata.""" if handle_kind == "unsupported": return "unknown" if descriptor_kind == "pointer": @@ -1258,6 +1341,7 @@ def _native_array_target_lifetime( def _native_array_destroy_behavior(handle_kind: str, blocker: str | None) -> str: + """Return the allowed destruction behavior, blocking unsafe lifetime/release policies.""" if handle_kind == "unsupported" or ( blocker is not None and any(reason in blocker for reason in ("release policy", "stable owner storage", "target lifetime")) @@ -1279,6 +1363,7 @@ def _native_array_to_numpy_policy( decision: OwnershipDecision, semantic_type: models.SemanticType, ) -> str: + """Return the supported NumPy exposure policy for a completed descriptor handle.""" if handle_kind == "unsupported": return "unsupported" if descriptor_kind == "pointer": @@ -1291,6 +1376,7 @@ def _native_array_to_numpy_policy( def _native_array_pointer_to_numpy_policy(semantic_type: models.SemanticType) -> str: + """Choose pointer NumPy exposure from explicit pointer-policy metadata only.""" pointer_policy = _pointer_policy_metadata(semantic_type) if not pointer_policy: return "unsupported" @@ -1305,6 +1391,7 @@ def _native_array_handle_operations( context: OwnershipContext, semantic_type: models.SemanticType, ) -> tuple[str, ...]: + """Return the sorted descriptor operations allowed by kind, context, and metadata.""" if handle_kind == "unsupported": return () if descriptor_kind == "allocatable": @@ -1337,6 +1424,7 @@ def _native_array_descriptor_interop_requirement( handle_kind: str, semantic_type: models.SemanticType, ) -> str: + """Return the C-descriptor interop mechanism required by a supported handle.""" if descriptor_kind == "allocatable" and handle_kind == "owned_result_descriptor": return "owned_allocatable_c_descriptor" if ( @@ -1351,24 +1439,29 @@ def _native_array_descriptor_interop_requirement( def _pointer_policy_metadata(semantic_type: models.SemanticType) -> dict[str, object]: + """Copy pointer-policy metadata into a safe mutable mapping, or return an empty mapping.""" policy = semantic_type.metadata.get(POINTER_POLICY_METADATA) return dict(policy) if isinstance(policy, dict) else {} def _pointer_policy_value(policy: dict[str, object], key: str) -> str: + """Normalize one string pointer-policy value for membership checks.""" value = policy.get(key) return str(value).strip().casefold() if isinstance(value, str) else "" def _pointer_policy_allows_allocate(policy: dict[str, object]) -> bool: + """Report whether reassociation metadata permits pointer allocation.""" return _pointer_policy_value(policy, "reassociation") in _POINTER_ALLOCATE_PERMISSION_VALUES def _pointer_policy_allows_deallocate(policy: dict[str, object]) -> bool: + """Report whether deallocation metadata permits pointer deallocation.""" return _pointer_policy_value(policy, "deallocation") in _POINTER_DEALLOCATE_PERMISSION_VALUES def _pointer_policy_allows_resize(policy: dict[str, object]) -> bool: + """Report whether both pointer-policy dimensions permit resize operations.""" reassociation = _pointer_policy_value(policy, "reassociation") deallocation = _pointer_policy_value(policy, "deallocation") return reassociation in _POINTER_RESIZE_PERMISSION_VALUES and deallocation in _POINTER_RESIZE_PERMISSION_VALUES @@ -1379,6 +1472,7 @@ def _native_array_handle_blocker( handle_kind: str, decision: OwnershipDecision, ) -> str | None: + """Return the inherited ownership blocker or reject an unsupported descriptor origin.""" if decision.is_blocked: return decision.blocker or decision.reason if handle_kind == "unsupported": @@ -1386,6 +1480,9 @@ def _native_array_handle_blocker( return None +# Callable projections and raw-address validation + + def _complete_callable_address_policy(function: models.SemanticFunction) -> None: """Validate Python/native address boundaries and complete scalar projections.""" _complete_hidden_scalar_output_projections(function) @@ -1424,6 +1521,7 @@ def _complete_hidden_scalar_output_projections(function: models.SemanticFunction def _complete_native_address_projections(function: models.SemanticFunction) -> None: + """Validate ``Addr(Arg(i))`` mappings and mutate eligible scalars to address storage.""" arguments_by_name = {argument.name: argument for argument in function.arguments} for mapping in function.projection: if mapping.value_kind != "addr": @@ -1450,6 +1548,12 @@ def _complete_native_address_projections(function: models.SemanticFunction) -> N def _apply_scalar_address_projection(argument: models.SemanticArgument) -> None: + """Replace an eligible scalar argument's storage with its completed address projection. + + The argument type is mutated in place. A declared call-local transfer stays + read-only unless the argument projects a result; all other scalar address + projections become writable native storage. + """ semantic_type = argument.semantic_type storage = semantic_type.storage metadata = dict(storage.metadata) if storage is not None else {} @@ -1476,6 +1580,7 @@ def _is_primitive_scalar_value( *, allow_completed_projection: bool = False, ) -> bool: + """Report whether a type is a plain scalar value or an allowed completed address projection.""" if semantic_type.rank != 0 or semantic_type.name == "String": return False if (semantic_type.dtype or semantic_type.name) not in SEMANTIC_SCALAR_TYPE_NAMES: @@ -1492,6 +1597,7 @@ def _is_primitive_scalar_value( def _is_visible_extent_source(semantic_type: models.SemanticType) -> bool: + """Report whether a scalar type can safely supply an array/raw-address extent name.""" if _is_primitive_scalar_value(semantic_type, allow_completed_projection=True): return True storage = semantic_type.storage @@ -1512,6 +1618,12 @@ def _validate_raw_address_type( item: str, visible_scalar_names: set[str], ) -> None: + """Reject unsupported raw-address shape, type, depth, and extent combinations. + + Types without raw-address metadata are ignored. Raw arrays and strings + must be primitive and fully described by literals or visible scalar + arguments; unsupported contracts fail before wrapper planning. + """ storage = semantic_type.storage if storage is None or storage.metadata.get(ADDRESS_ROLE_METADATA) != ADDRESS_ROLE_RAW: return @@ -1548,6 +1660,7 @@ def _validate_raw_address_type( def _semantic_shape(semantic_type: models.SemanticType) -> list[str]: + """Return a type's semantic shape, falling back to array storage source shape.""" if semantic_type.shape: return [str(dimension) for dimension in semantic_type.shape] storage = semantic_type.storage @@ -1558,6 +1671,7 @@ def _semantic_shape(semantic_type: models.SemanticType) -> list[str]: def _is_resolved_extent(value: object, visible_scalar_names: set[str]) -> bool: + """Report whether an extent is concrete or references only visible scalar inputs.""" text = str(value).strip() if not text or text in {":", "*", "...", ".."} or ":" in text: return False @@ -1571,6 +1685,12 @@ def _complete_variable( *, owner_path: str | None = None, ) -> None: + """Attach ownership, callback, and descriptor-handle policies to one variable. + + The variable and nested callback type metadata are mutated in place. + ``MaybeUnallocated`` is rejected outside direct function returns so later + stages never receive an ambiguous variable contract. + """ if variable.semantic_type.metadata.get(MAYBE_UNALLOCATED_METADATA): raise ValueError( f"MaybeUnallocated metadata on {owner_path or variable.name!r} is only valid on function return types" @@ -1582,6 +1702,7 @@ def _complete_variable( def _complete_accessor_policies(variable: models.SemanticVariable, context: OwnershipContext) -> None: + """Attach resolved getter and setter decisions, then refresh descriptor-handle facts.""" variable.metadata[models.RESOLVED_GETTER_OWNERSHIP_POLICY_METADATA] = ( default_ownership_policy.decide_semantic_getter(variable, context) ) @@ -1592,6 +1713,7 @@ def _complete_accessor_policies(variable: models.SemanticVariable, context: Owne def _complete_module_variable_initializer(variable: models.SemanticVariable) -> None: + """Record a safe scalar write-through module initializer, or remove stale metadata.""" variable.metadata.pop(models.RESOLVED_MODULE_VARIABLE_INITIALIZER_METADATA, None) if variable.default_value is None or _is_constant(variable): return @@ -1602,14 +1724,24 @@ def _complete_module_variable_initializer(variable: models.SemanticVariable) -> def _is_constant(variable: models.SemanticVariable) -> bool: + """Report whether a semantic variable is constrained as a compile-time constant.""" return any(constraint.name == "Constant" for constraint in variable.semantic_type.constraints) +# Callback prototype completion + + def _complete_prototype_reference_policy( semantic_type: models.SemanticType, *, owner_path: str, ) -> None: + """Complete callback handoff policy and validate nested callback ABI contracts. + + Non-callback types are left untouched. Callback arguments and returns are + completed in place with callback-specific ownership contexts before the + final handoff policy is stored on the outer semantic type. + """ if semantic_type.storage is None or semantic_type.storage.kind != "callback": return @@ -1653,12 +1785,14 @@ def _complete_prototype_reference_policy( def _callback_argument_ownership_context(argument: models.SemanticArgument) -> OwnershipContext: + """Return the read-only/value or writable/reference context declared by a callback argument.""" if bool(getattr(argument.origin, "metadata", {}).get("value")): return OwnershipContext.argument(reads_argument=True, writes_argument=False) return OwnershipContext.argument(reads_argument=True, writes_argument=True) def _validate_callback_argument_contract(argument: models.SemanticArgument) -> None: + """Require reference callback strings to use mutable scalar character storage.""" semantic_type = argument.semantic_type if semantic_type.name != "String": return @@ -1670,6 +1804,7 @@ def _validate_callback_argument_contract(argument: models.SemanticArgument) -> N def _is_scalar_string_storage(semantic_type: models.SemanticType) -> bool: + """Report whether a type uses the rank-zero scalar-character storage representation.""" storage = semantic_type.storage array = storage.array if storage is not None else None return bool( @@ -1679,3 +1814,32 @@ def _is_scalar_string_storage(semantic_type: models.SemanticType) -> bool: and array.rank == 0 and array.category == SCALAR_STORAGE_CATEGORY ) + + +if __name__ == "__main__": + from prik.semantics.models import SemanticArgument, SemanticFunction, SemanticModule, SemanticType + + semantic_module = SemanticModule( + name="math", + functions=[ + SemanticFunction( + name="scale", + arguments=[SemanticArgument("value", SemanticType("Float64", dtype="Float64"))], + return_type=SemanticType("Float64", dtype="Float64"), + ) + ], + ) + semantic_function = semantic_module.functions[0] + semantic_argument = semantic_function.arguments[0] + print( + f"before: {semantic_module.name}.{semantic_function.name}({semantic_argument.name}): " + f"{semantic_argument.semantic_type.name} semantic IR" + ) + complete_semantic_policies(semantic_module) + function_policy = semantic_function.metadata[models.RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + argument_policy = function_policy.arguments[0] + native_slot = function_policy.native_call_slots[0] + print( + f"after: {semantic_module.name}.{semantic_function.name}({argument_policy.python_name}): " + f"{argument_policy.python_barrier_action.value} -> {native_slot.native_barrier_action.value}" + ) diff --git a/prik/semantics/pyi2ir.py b/prik/semantics/pyi2ir.py index 6e5465d5f..54f041305 100644 --- a/prik/semantics/pyi2ir.py +++ b/prik/semantics/pyi2ir.py @@ -1,3 +1,12 @@ +"""Translate parsed editable semantic ``.pyi`` contracts into semantic IR. + +The public entrypoints consume Python AST produced by :mod:`prik.parsers.pyi`. +They validate the supported contract subset, retain declared native facts and +projections, and return ``SemanticModule`` objects for native-contract +validation and post-IR policy completion. They deliberately do not choose +wrapper ownership or lowering policy. +""" + from __future__ import annotations import ast @@ -6,6 +15,11 @@ from dataclasses import dataclass, field from prik.contracts import CONTRACT_SYMBOLS, CONTRACT_TYPE_NAMES +from prik.utilities.declaration_expressions import ( + declaration_expression_calls, + is_declaration_expression_helper, + is_public_declaration_expression, +) from prik.types.numpy import SEMANTIC_SCALAR_TYPE_NAMES from prik.semantics.ownership import OWNERSHIP_POLICY_METADATA, set_ownership_metadata, set_pointer_policy_metadata from prik.semantics.metadata import ( @@ -24,7 +38,7 @@ from prik.semantics.native_array_handles import mark_native_array_handle, native_array_descriptor_kind from prik.utilities.visitor import ClassVisitor -from .models import ( +from prik.semantics.models import ( EXTERNAL_TYPE_REF_METADATA, FORTRAN_GENERIC_NAME_METADATA, OVERLOAD_KIND_METADATA, @@ -35,6 +49,7 @@ PYTHON_STATIC_METADATA, PYTHON_VALUE_IMMUTABLE, PYTHON_VALUE_MUTABILITY_METADATA, + PROTOTYPE_INTENT_METADATA, PROTOTYPE_REF_METADATA, RUNTIME_RELEASE_GIL_METADATA, RUNTIME_STATUS_ERROR_METADATA, @@ -44,6 +59,7 @@ SemanticArrayContract, SemanticClass, SemanticConstraint, + SemanticExpressionCallable, SemanticField, SemanticFunction, SemanticImport, @@ -67,10 +83,16 @@ _STRIDED_DIMENSION_SENTINEL = "@prik.Strided" +# Contract-conversion state and public entrypoints + + @dataclass(frozen=True) -class _CallbackArgumentSpec: +class _PrototypeArgumentSpec: + """Store one exact interface dummy's type, transport, and direction.""" + semantic_type: SemanticType passes_by_value: bool + intent: str | None = None def convert_pyi_to_ir( @@ -80,21 +102,43 @@ def convert_pyi_to_ir( source: str = "", native_language: str = "fortran", ) -> SemanticModule: - """Convert a parsed semantic `.pyi` AST into semantic IR.""" + """Convert one parsed editable semantic ``.pyi`` contract into semantic IR. + + Use this after :func:`prik.parsers.pyi.parse_pyi_text` when the caller + already owns AST parsing. ``tree`` must be an ``ast.Module``; ``source`` + is retained only to preserve source spelling for array dimensions. + ``native_language`` selects declared array-layout defaults (``"fortran"`` + or ``"c"``). The returned module is normally passed to native-contract + validation and policy completion; malformed supported-contract syntax + raises ``TypeError`` or ``ValueError`` before those later stages. + + Examples: + >>> import ast + >>> contract = "from prik.contracts import Float64\\n" "def scale(value: Float64) -> Float64: ...\\n" + >>> module = convert_pyi_to_ir(ast.parse(contract), module_name="math", source=contract) + >>> module.functions[0].return_type.name + 'Float64' + """ if not isinstance(tree, ast.Module): raise TypeError("convert_pyi_to_ir expects a Python ast.Module parsed by prik.parsers.pyi") + + # Interpret top-level declarations and validate their local relationships. module = _PyiAstParser( module_name=module_name, source=source, native_language=native_language, ).parse(tree) + + # Mark imported names so batch reconciliation can bind cross-module types. _annotate_imported_external_type_refs(module) return module @dataclass class _Decorators: + """Accumulate validated declaration decorators before IR construction.""" + visibility: str = "public" projection: list[ProjectionMapping] = field(default_factory=list) native_result: ProjectionMapping | None = None @@ -103,15 +147,18 @@ class _Decorators: overload_generic: str | None = None bind_target: str | None = None native_type: dict[str, object] | None = None - external: bool = False + standalone: bool = False is_static: bool = False release_gil: bool = False error_status_policy: dict[str, object] | None = None prototype: bool = False + pure: bool = False @dataclass class _PendingOverload: + """Keep an overload declaration until its specific target is available.""" + owner: SemanticModule | SemanticClass declaration: SemanticFunction target: str @@ -119,7 +166,20 @@ class _PendingOverload: class _PyiAstParser: + """Stateful AST visitor that builds one semantic module from a contract. + + The parser stores import bindings, declared user type names, and unresolved + overloads while visitors create the module's declarations. Resolution runs + only after the complete module body has been visited. + """ + def __init__(self, *, module_name: str, source: str = "", native_language: str = "fortran"): + """Initialize module-building state from the declared target language. + + ``native_language`` is normalized once and must be ``"c"`` or + ``"fortran"``. The initializer creates the mutable semantic module + and empty resolution registries; invalid languages fail immediately. + """ native_language = native_language.casefold() if native_language not in {"c", "fortran"}: raise ValueError(f"Unsupported semantic .pyi native language: {native_language!r}") @@ -131,11 +191,23 @@ def __init__(self, *, module_name: str, source: str = "", native_language: str = self._user_type_names: set[str] = set() def parse(self, tree: ast.Module) -> SemanticModule: + """Visit a module AST and finalize relationships that require all declarations. + + The method mutates this parser's module in declaration order, then + resolves pending overloads and local prototype references. It returns + that same completed ``SemanticModule`` and propagates validation errors. + """ + # Build imports and declarations in source order. _ModuleVisitor(self)._visit(tree) + + # Resolve references whose targets can appear later in the module. self._resolve_overloads() self._resolve_local_prototype_references() + self._resolve_declaration_expression_callables() return self.module + # Module declarations and imports + def _resolve_local_prototype_references(self) -> None: """Bind local prototype annotations after all declarations are known.""" prototypes = {prototype.name: prototype for prototype in self.module.prototypes} @@ -157,7 +229,143 @@ def _resolve_local_prototype_references(self) -> None: source_name=prototype.name, ) + def _resolve_declaration_expression_callables(self) -> None: + """Reconstruct native call provenance from local declarations and imports. + + This finalization pass consumes every array shape after the whole + contract is known and mutates only each array's parallel callable + provenance. Calls remain declarative annotation text and are never + imported or executed by the loader. + """ + local_functions = {function.name.casefold(): function for function in self.module.functions} + local_prototypes = {prototype.name.casefold(): prototype for prototype in self.module.prototypes} + explicit_imports, namespace_imports = self._declaration_callable_imports() + for semantic_type in _iter_module_semantic_types(self.module): + storage = semantic_type.storage + array = storage.array if storage is not None else None + if array is None: + continue + array.expression_callables = [ + self._expression_callable_references( + expression, + local_functions, + local_prototypes, + explicit_imports, + namespace_imports, + ) + for expression in array.shape + ] + + def _declaration_callable_imports( + self, + ) -> tuple[dict[str, tuple[str, str]], dict[str, str]]: + """Index explicit imported names and visible module namespaces.""" + explicit: dict[str, tuple[str, str]] = {} + namespaces: dict[str, str] = {} + for imported in self.module.imports: + if isinstance(imported, SemanticImport): + if imported.items: + for item in imported.items: + explicit[(item.target or item.source).casefold()] = (imported.module, item.source) + else: + namespaces[imported.module.split(".", 1)[0].casefold()] = imported.module + continue + for item in str(imported).split(","): + module_name, _, alias = item.strip().partition(" as ") + namespaces[(alias or module_name.split(".", 1)[0]).casefold()] = module_name + return explicit, namespaces + + def _expression_callable_references( + self, + expression: str, + local_functions: dict[str, SemanticFunction], + local_prototypes: dict[str, SemanticPrototype], + explicit_imports: dict[str, tuple[str, str]], + namespace_imports: dict[str, str], + ) -> list[SemanticExpressionCallable]: + """Resolve one axis's calls to semantic native identities when possible.""" + references = [] + for name in declaration_expression_calls(expression): + reference = self._resolve_expression_callable( + name, + local_functions, + local_prototypes, + explicit_imports, + namespace_imports, + ) + if reference is not None: + references.append(reference) + elif name != "" and not is_declaration_expression_helper(name): + references.append( + SemanticExpressionCallable( + name=name, + native_name=name.rsplit(".", 1)[-1], + source_language=self.native_language, + ) + ) + return references + + def _resolve_expression_callable( + self, + name: str, + local_functions: dict[str, SemanticFunction], + local_prototypes: dict[str, SemanticPrototype], + explicit_imports: dict[str, tuple[str, str]], + namespace_imports: dict[str, str], + ) -> SemanticExpressionCallable | None: + """Resolve one contract call against local, flattened, or qualified names.""" + if "." in name: + namespace, native_name = name.rsplit(".", 1) + native_scope = namespace_imports.get(namespace.casefold()) + if native_scope is None: + return None + return SemanticExpressionCallable( + name=name, + native_name=native_name, + native_scope=native_scope, + source_language=self.native_language, + placement="module", + ) + + prototype = local_prototypes.get(name.casefold()) + if prototype is not None: + return SemanticExpressionCallable( + name=name, + native_name=prototype.native_name or prototype.name, + native_scope=None, + source_language=self.native_language, + placement="standalone", + declaration=prototype, + ) + + function = local_functions.get(name.casefold()) + if function is not None: + standalone = function.origin.source_language == "fortran" and function.origin.native_scope is None + return SemanticExpressionCallable( + name=name, + native_name=function.native_name or function.name, + native_scope=None if standalone else (function.origin.native_scope or self.module.name), + source_language=function.origin.source_language or self.native_language, + placement="standalone" if standalone else "module", + declaration=function, + ) + imported = explicit_imports.get(name.casefold()) + if imported is not None: + return SemanticExpressionCallable( + name=name, + native_name=imported[1], + native_scope=imported[0], + source_language=self.native_language, + placement="module", + ) + return None + def import_from(self, node: ast.ImportFrom) -> SemanticImport: + """Convert one non-contract ``from`` import AST node into semantic metadata. + + Relative-dot depth and aliases are preserved exactly in the returned + ``SemanticImport``; this method does not mutate the module. + """ module_name = "." * node.level + (node.module or "") return SemanticImport( module=module_name, @@ -165,6 +373,12 @@ def import_from(self, node: ast.ImportFrom) -> SemanticImport: ) def register_contract_import(self, node: ast.ImportFrom) -> bool: + """Register imported ``prik.contracts`` names and report whether it was one. + + The method consumes a ``from`` import, records local aliases in parser + state, and returns ``False`` for every other module. Duplicate or + unknown contract bindings fail rather than being interpreted by name. + """ module_name = "." * node.level + (node.module or "") if module_name != _CONTRACT_MODULE: return False @@ -181,6 +395,7 @@ def register_contract_import(self, node: ast.ImportFrom) -> bool: return True def import_name(self, node: ast.Import) -> str: + """Render a plain import AST node for the module import list without mutation.""" return ", ".join(f"{alias.name} as {alias.asname}" if alias.asname else alias.name for alias in node.names) def register_user_type_names(self, node: ast.Module) -> None: @@ -206,6 +421,13 @@ def class_def( visibility: str, native_type: dict[str, object] | None = None, ) -> SemanticClass: + """Convert one class AST node, its body, and supported native metadata. + + The class-body visitor supplies fields, methods, nested classes, and + delayed overload declarations. This method records those overloads on + parser state, preserves field-constructor rules, and returns the new + ``SemanticClass`` without inserting it into the module itself. + """ body = _ClassBodyVisitor(self, class_name=node.name) body._walk_nodes(node.body) if body.constructor_from_fields and body.has_bound_constructor: @@ -249,6 +471,7 @@ def class_def( @staticmethod def _class_metadata(base_classes: list[str]) -> dict[str, object]: + """Derive representation markers from supported contract base-class names.""" metadata: dict[str, object] = {} if "CStruct" in base_classes: metadata["c_kind"] = "struct" @@ -269,6 +492,7 @@ def base_class_name(self, node: ast.expr) -> str: @staticmethod def _origin(*, source_language: str | None = None, user_private: bool = False) -> SemanticOrigin: + """Create declaration provenance, marking user-private declarations when requested.""" origin = SemanticOrigin(source_language=source_language) if user_private: origin.metadata[USER_PRIVATE_METADATA] = True @@ -282,11 +506,18 @@ def function_def( projection: list[ProjectionMapping] | None = None, native_result: ProjectionMapping | None = None, native_name: str | None = None, - external: bool = False, + standalone: bool = False, has_native_call: bool = False, release_gil: bool = False, error_status_policy: dict[str, object] | None = None, ) -> SemanticFunction: + """Convert a module-level stub into a semantic function declaration. + + The method consumes validated decorator facts and a typed function AST, + builds argument/result projections, and returns a declaration without + appending it to the module. Native binding and runtime-status metadata + are copied verbatim from the decorators. + """ actual_projection = projection if projection is not None else [] semantic_args, return_type = self._callable_parts( node, @@ -301,10 +532,10 @@ def function_def( if error_status_policy is not None: metadata[RUNTIME_STATUS_ERROR_METADATA] = dict(error_status_policy) origin = self._origin( - source_language="fortran" if external else None, + source_language="fortran" if standalone else None, user_private=visibility == "private", ) - if external: + if standalone: origin.source_kind = "function" if return_type is not None else "subroutine" origin.native_name = native_name or node.name return SemanticFunction( @@ -318,21 +549,30 @@ def function_def( origin=origin, ) - def prototype_def(self, node: ast.FunctionDef, *, visibility: str) -> SemanticPrototype: - """Convert one named callback prototype without creating a runtime function.""" + def prototype_def( + self, + node: ast.FunctionDef, + *, + visibility: str, + pure: bool, + ) -> SemanticPrototype: + """Convert one exact native interface without creating a runtime function.""" self._validate_callable_header(node) arguments = [] for argument, default in zip(node.args.args, self._argument_defaults(node), strict=False): if argument.annotation is None: raise ValueError(f"Expected typed prototype argument: {argument.arg!r}") spec = self._prototype_argument_spec(argument.annotation) + origin_metadata: dict[str, object] = {"value": spec.passes_by_value} + if spec.intent is not None: + origin_metadata[PROTOTYPE_INTENT_METADATA] = spec.intent arguments.append( SemanticArgument( argument.arg, spec.semantic_type, optional=self.default_marks_optional(default), visibility=visibility, - origin=SemanticOrigin(metadata={"value": spec.passes_by_value}), + origin=SemanticOrigin(metadata=origin_metadata), ) ) return_type = ( @@ -340,17 +580,20 @@ def prototype_def(self, node: ast.FunctionDef, *, visibility: str) -> SemanticPr if isinstance(node.returns, ast.Constant) and node.returns.value is None else self.semantic_type(node.returns) ) + metadata = {"fortran_attributes": ["pure"]} if pure else {} return SemanticPrototype( name=node.name, native_name=node.name, arguments=arguments, return_type=return_type, + metadata=metadata, visibility=visibility, origin=SemanticOrigin( native_name=node.name, native_scope=self.module.name, source_kind="prototype", ), + pure=pure, ) def method_def( @@ -368,6 +611,13 @@ def method_def( release_gil: bool = False, error_status_policy: dict[str, object] | None = None, ) -> SemanticMethod: + """Convert a class stub into a semantic method declaration. + + It consumes decorator facts and typed arguments, inserting an internal + passed-object argument for non-static bound methods when required. The + insertion rewrites later projection positions in place so Python and + native positions remain aligned; the method returns the declaration. + """ actual_projection = projection if projection is not None else [] semantic_args, return_type = self._callable_parts( node, @@ -426,6 +676,12 @@ def method_def( @staticmethod def _restore_pass_projection(projection: list[ProjectionMapping], passed_position: int) -> None: + """Replace ``Pass()`` markers after inserting ``self`` into a method signature. + + The projection list is mutated in place: the pass marker becomes the + passed object's normal Python mapping and later argument references are + shifted by one to preserve their original targets. + """ for mapping in projection: if mapping.value_kind == "pass": mapping.value_kind = None @@ -443,6 +699,12 @@ def ann_assign( *, binding_cls: type[SemanticVariable] = SemanticVariable, ) -> SemanticVariable: + """Convert one annotated assignment into a variable or field declaration. + + ``binding_cls`` selects the concrete semantic binding type. The method + validates writable-value metadata, applies source-name and visibility + metadata, and returns a new binding without attaching it to an owner. + """ name = self.annotation_target(node.target) visibility, semantic_type, original_name = self.visible_type(node.annotation) if original_name is not None: @@ -463,17 +725,43 @@ def ann_assign( binding.optional = self.default_marks_optional(node.value) return binding + # Decorators and overload resolution + def decorators(self, nodes: list[ast.expr], *, context: str) -> _Decorators: + """Validate a declaration's decorators and return their normalized facts. + + Decorators are processed in source order into a fresh internal record. + Incompatible combinations, including overload plus native-call, raise + before a declaration is constructed. + """ parsed = _Decorators() for node in nodes: self._apply_decorator(parsed, node, context=context) if parsed.overload_target is not None and parsed.has_native_call: raise ValueError("overload cannot be combined with native_call; put native_call on the specific procedure") - if parsed.prototype and len(nodes) != 1: - raise ValueError("prototype cannot be combined with other decorators") + if parsed.pure and not parsed.prototype: + raise ValueError("pure requires prototype") + if parsed.prototype: + if parsed.standalone: + raise ValueError( + "prototype cannot be combined with standalone; " + "prototype use already determines its native procedure role" + ) + if parsed.has_native_call or parsed.overload_target is not None or parsed.bind_target is not None: + raise ValueError("prototype cannot be combined with native_call, overload, or bind") + if parsed.release_gil or parsed.error_status_policy is not None or parsed.native_type is not None: + raise ValueError("prototype cannot carry wrapper or native-type decorators") + if parsed.visibility != "public" or parsed.is_static: + raise ValueError("prototype cannot be private or static") return parsed def _apply_decorator(self, parsed: _Decorators, node: ast.expr, *, context: str) -> None: + """Dispatch one decorator AST node to its validator, mutating ``parsed``. + + Built-in Python ``staticmethod`` and the supported contract decorators + are recognized through imports. Any other decorator fails closed with + context-specific diagnostics. + """ if self.matches_name(node, "private"): parsed.visibility = "private" return @@ -484,11 +772,12 @@ def _apply_decorator(self, parsed: _Decorators, node: ast.expr, *, context: str) handlers = { "overload": self._apply_overload_decorator, "bind": self._apply_bind_decorator, - "external": self._apply_external_decorator, + "standalone": self._apply_standalone_decorator, "nogil": self._apply_nogil_decorator, "native_call": self._apply_native_call_decorator, "native_type": self._apply_native_type_decorator, "prototype": self._apply_prototype_decorator, + "pure": self._apply_pure_decorator, "raises": self._apply_raises_decorator, } handler = next((value for name, value in handlers.items() if self.matches_name(target, name)), None) @@ -498,6 +787,7 @@ def _apply_decorator(self, parsed: _Decorators, node: ast.expr, *, context: str) @staticmethod def _apply_prototype_decorator(parsed: _Decorators, node: ast.expr, context: str) -> None: + """Mark a module-level declaration as an exact native interface.""" if isinstance(node, ast.Call): raise ValueError("prototype does not accept arguments") if context != ".pyi": @@ -506,7 +796,19 @@ def _apply_prototype_decorator(parsed: _Decorators, node: ast.expr, context: str raise ValueError("Duplicate prototype decorator") parsed.prototype = True + @staticmethod + def _apply_pure_decorator(parsed: _Decorators, node: ast.expr, context: str) -> None: + """Mark an exact interface with the native pure characteristic.""" + if isinstance(node, ast.Call): + raise ValueError("pure does not accept arguments") + if context != ".pyi": + raise ValueError("pure is only valid for module-level prototype declarations") + if parsed.pure: + raise ValueError("Duplicate pure decorator") + parsed.pure = True + def _apply_overload_decorator(self, parsed: _Decorators, node: ast.expr, context: str) -> None: + """Record one specific-procedure target for deferred overload resolution.""" if not isinstance(node, ast.Call): raise ValueError("overload expects one specific procedure name") if parsed.overload_target is not None: @@ -527,6 +829,7 @@ def _apply_overload_decorator(self, parsed: _Decorators, node: ast.expr, context @staticmethod def _required_string_decorator_argument(node: ast.expr, name: str) -> str: + """Extract the sole non-empty string argument required by a named decorator.""" if not isinstance(node, ast.Call) or len(node.args) != 1 or node.keywords: raise ValueError(f"{name} expects one native symbol name") target = ast.literal_eval(node.args[0]) @@ -535,12 +838,14 @@ def _required_string_decorator_argument(node: ast.expr, name: str) -> str: return target def _apply_bind_decorator(self, parsed: _Decorators, node: ast.expr, context: str) -> None: + """Store one native symbol binding in decorator state, rejecting duplicates.""" if parsed.bind_target is not None: raise ValueError(f"Duplicate {context} bind decorator") parsed.bind_target = self._required_string_decorator_argument(node, "bind") @staticmethod def _apply_nogil_decorator(parsed: _Decorators, node: ast.expr, context: str) -> None: + """Record a no-argument GIL-release request in decorator state.""" if isinstance(node, ast.Call): raise ValueError("nogil does not accept arguments") if parsed.release_gil: @@ -548,15 +853,17 @@ def _apply_nogil_decorator(parsed: _Decorators, node: ast.expr, context: str) -> parsed.release_gil = True @staticmethod - def _apply_external_decorator(parsed: _Decorators, node: ast.expr, context: str) -> None: + def _apply_standalone_decorator(parsed: _Decorators, node: ast.expr, context: str) -> None: + """Record a standalone native declaration marker in decorator state.""" if isinstance(node, ast.Call): - raise ValueError("external does not accept arguments") - if parsed.external: - raise ValueError(f"Duplicate {context} external decorator") - parsed.external = True + raise ValueError("standalone does not accept arguments") + if parsed.standalone: + raise ValueError(f"Duplicate {context} standalone decorator") + parsed.standalone = True @staticmethod def _apply_native_type_decorator(parsed: _Decorators, node: ast.expr, context: str) -> None: + """Validate and store class-level native type attributes and finalizers.""" if parsed.native_type is not None: raise ValueError(f"Duplicate {context} native_type decorator") if not isinstance(node, ast.Call) or node.args: @@ -575,6 +882,7 @@ def _apply_native_type_decorator(parsed: _Decorators, node: ast.expr, context: s parsed.native_type = values def _apply_native_call_decorator(self, parsed: _Decorators, node: ast.expr, context: str) -> None: + """Parse ``native_call`` projection facts into decorator state.""" del context if not isinstance(node, ast.Call): raise ValueError("native_call expects a single list argument") @@ -582,6 +890,7 @@ def _apply_native_call_decorator(self, parsed: _Decorators, node: ast.expr, cont parsed.projection, parsed.native_result = self.native_call(node) def _apply_raises_decorator(self, parsed: _Decorators, node: ast.expr, context: str) -> None: + """Parse a native status-error policy into decorator state, once only.""" if not isinstance(node, ast.Call): raise ValueError("raises expects keyword arguments") if parsed.error_status_policy is not None: @@ -589,6 +898,11 @@ def _apply_raises_decorator(self, parsed: _Decorators, node: ast.expr, context: parsed.error_status_policy = self.error_status_policy(node) def native_call(self, node: ast.Call) -> tuple[list[ProjectionMapping], ProjectionMapping | None]: + """Parse a ``native_call`` AST into ordered argument and optional result mappings. + + Projection entries retain their native-list positions. Invalid list or + result syntax raises before the mappings are returned. + """ if len(node.args) != 1: raise ValueError("native_call expects one native-argument list") if len(node.keywords) > 1 or any(keyword.arg != "result" for keyword in node.keywords): @@ -616,6 +930,7 @@ def native_result_projection(self, node: ast.AST) -> ProjectionMapping: @staticmethod def error_status_policy(node: ast.Call) -> dict[str, object]: + """Validate ``raises`` keyword syntax and return its immutable policy facts.""" if node.args: raise ValueError("raises accepts keyword arguments only") allowed = {"status", "message", "success"} @@ -647,6 +962,12 @@ def error_status_policy(node: ast.Call) -> dict[str, object]: return policy def _resolve_overloads(self) -> None: + """Resolve all pending overload declarations into owner overload sets. + + This consumes the parser's delayed-overload list after every declaration + is known, deep-copies validated target signatures, and appends them to + their semantic owners. Missing, duplicate, or incompatible targets fail. + """ for pending in self._pending_overloads: target = self._resolve_overload_target(pending.owner, pending.target) candidate = self._validated_overload_candidate( @@ -670,12 +991,14 @@ def _resolve_overloads(self) -> None: @classmethod def _iter_classes(cls, classes: list[SemanticClass]): + """Yield classes and nested classes depth first in source-list order.""" for semantic_class in classes: yield semantic_class yield from cls._iter_classes(semantic_class.classes) @staticmethod def _overload_set_name(owner: SemanticModule | SemanticClass, declaration_name: str) -> str: + """Return the semantic overload-set name, normalizing reflected class operators.""" if isinstance(owner, SemanticModule): return declaration_name return { @@ -693,6 +1016,7 @@ def _resolve_overload_target( owner: SemanticModule | SemanticClass, target_name: str, ) -> SemanticFunction: + """Find exactly one specific procedure visible to a pending overload declaration.""" candidates = [ function for function in self.module.functions if target_name in {function.name, function.native_name} ] @@ -712,6 +1036,12 @@ def _validated_overload_candidate( *, generic_name: str | None, ) -> SemanticFunction: + """Copy and validate an overload target for the declaration's owner context. + + Module overloads become generic procedure entries; class overloads also + record Python method, binding, and passed-object facts. The returned + copy is safe to attach to an overload set without mutating its target. + """ candidate = deepcopy(target) candidate.visibility = declaration.visibility candidate.metadata[OVERLOAD_TARGET_METADATA] = target.name @@ -761,6 +1091,12 @@ def _validate_overload_signature( *, bound_position: int | None = None, ) -> None: + """Reject an overload whose public signature differs from its target. + + Address-backed projected scalars are compared through their visible value + form. A class overload may instead expose a projected bound-object + return; every other mismatch raises ``ValueError``. + """ visible_declaration_arguments = [_PyiAstParser._visible_overload_argument(arg) for arg in declaration.arguments] visible_call_arguments = [_PyiAstParser._visible_overload_argument(arg) for arg in call_arguments] if visible_declaration_arguments == visible_call_arguments and ( @@ -776,12 +1112,14 @@ def _validate_overload_signature( @staticmethod def _visible_overload_argument(argument: SemanticArgument) -> SemanticArgument: + """Copy one overload argument with its type normalized for public comparison.""" visible = deepcopy(argument) visible.semantic_type = _PyiAstParser._visible_overload_type(argument.semantic_type) return visible @staticmethod def _visible_overload_type(semantic_type: SemanticType | None) -> SemanticType | None: + """Return a visible comparison type, hiding projection-only scalar addresses.""" if semantic_type is None: return None storage = semantic_type.storage @@ -807,6 +1145,7 @@ def _matches_bound_projection_return( target: SemanticFunction, bound_position: int | None, ) -> bool: + """Check whether a method overload returns its projected bound object.""" if bound_position is None or declaration.return_type is None: return False if not 0 <= bound_position < len(target.arguments): @@ -828,6 +1167,12 @@ def _class_overload_bound_position( declaration: SemanticFunction, target: SemanticFunction, ) -> int | None: + """Locate the unique native wrapped-object argument for a class overload. + + Static methods need no bound object. Instance methods must match one + target argument whose type is the owning class and whose removal leaves + the declared Python arguments in order; ambiguity is an error. + """ if isinstance(declaration, SemanticMethod) and declaration.is_static: return None remaining_names = [argument.name for argument in declaration.arguments] @@ -856,6 +1201,12 @@ def _class_overload_identity( *, generic_name: str | None, ) -> tuple[str, str]: + """Map a Python class-overload spelling to its Fortran generic identity. + + Operator, comparison, constructor, assignment, and named-operator forms + have explicit identities. The returned kind and native generic name are + consumed by overload resolution; incompatible bound positions fail. + """ direct_operators = { "__add__": "+", "__sub__": "-", @@ -926,6 +1277,7 @@ def _validated_generic_override( identity: tuple[str, str], generic_name: str | None, ) -> tuple[str, str]: + """Validate an explicit generic override against an operator's allowed names.""" if generic_name is None: return identity compact = re.sub(r"\s+", "", generic_name).casefold() @@ -937,7 +1289,15 @@ def _validated_generic_override( raise ValueError(f"overload generic {generic_name!r} is incompatible with method {method_name!r}") return identity[0], generic_name + # Native-call projection parsing + def native_projection_entry(self, node: ast.AST, native_position: int) -> ProjectionMapping: + """Convert one ``native_call`` list item into a positioned projection mapping. + + Shape, address, descriptor, typed-literal, and named helper forms are + dispatched here. ``native_position`` is preserved in the returned + mapping; unsupported or untyped expressions fail closed. + """ shape_mapping = self.native_shape_projection_entry(node, native_position) if shape_mapping is not None: return shape_mapping @@ -1014,6 +1374,7 @@ def _native_helper_projection_entry( node: ast.Call, native_position: int, ) -> ProjectionMapping: + """Dispatch a named projection helper after its call shape has been checked.""" handlers = { "Arg": self._native_arg_projection_entry, "Return": self._native_return_projection_entry, @@ -1030,6 +1391,7 @@ def _native_helper_projection_entry( @staticmethod def _native_arg_projection_entry(node: ast.Call, native_position: int) -> ProjectionMapping: + """Parse ``Arg(i)`` into a mapping for one visible Python argument.""" if len(node.args) != 1: raise ValueError("Arg expects one positional index") return ProjectionMapping( @@ -1039,6 +1401,7 @@ def _native_arg_projection_entry(node: ast.Call, native_position: int) -> Projec @staticmethod def _native_return_projection_entry(node: ast.Call, native_position: int) -> ProjectionMapping: + """Parse ``Return`` output syntax into a native mapping and result slot.""" if len(node.args) not in {1, 2}: raise ValueError("Return expects one positional index or a name and index") native_name = "" @@ -1054,6 +1417,7 @@ def _native_return_projection_entry(node: ast.Call, native_position: int) -> Pro @staticmethod def _native_pass_projection_entry(node: ast.Call, native_position: int) -> ProjectionMapping: + """Parse ``Pass()`` as the temporary passed-object mapping for a method.""" if node.args: raise ValueError("Pass does not accept arguments") return ProjectionMapping( @@ -1062,6 +1426,7 @@ def _native_pass_projection_entry(node: ast.Call, native_position: int) -> Proje ) def _native_len_projection_entry(self, node: ast.Call, native_position: int) -> ProjectionMapping: + """Parse ``Len(value)`` into a hidden native length projection.""" if len(node.args) != 1: raise ValueError("Len expects one value reference") return ProjectionMapping( @@ -1071,6 +1436,7 @@ def _native_len_projection_entry(self, node: ast.Call, native_position: int) -> ) def _native_is_present_projection_entry(self, node: ast.Call, native_position: int) -> ProjectionMapping: + """Parse ``IsPresent(Arg(i))`` into a hidden optional-presence projection.""" if len(node.args) != 1: raise ValueError("IsPresent expects one value reference") return ProjectionMapping( @@ -1081,6 +1447,7 @@ def _native_is_present_projection_entry(self, node: ast.Call, native_position: i @staticmethod def _native_work_projection_entry(node: ast.Call, native_position: int) -> ProjectionMapping: + """Parse ``Work(name)`` into a hidden named workspace projection.""" if len(node.args) != 1: raise ValueError("Work expects one workspace name") return ProjectionMapping( @@ -1094,6 +1461,7 @@ def native_literal_projection_entry( node: ast.Call, native_position: int, ) -> ProjectionMapping | None: + """Parse a typed hidden native literal, or return ``None`` for other calls.""" native_type = self._native_literal_type(node.func) if native_type is None: return None @@ -1109,6 +1477,7 @@ def native_literal_projection_entry( ) def _native_literal_type(self, node: ast.AST) -> str | None: + """Return the imported scalar type name accepted for a typed hidden literal.""" if isinstance(node, ast.Subscript) and self.matches_name(node.value, "String"): length = self._native_literal_string_length(node) return f"String[{length}]" @@ -1122,6 +1491,7 @@ def _native_literal_type(self, node: ast.AST) -> str | None: return None def _native_literal_string_length(self, node: ast.Subscript) -> str: + """Validate and return the fixed ``String[n]`` length for a hidden literal.""" items = self.subscript_items(node) if len(items) != 1: raise ValueError("native_call string literals require exactly one String length") @@ -1131,6 +1501,7 @@ def _native_literal_string_length(self, node: ast.Subscript) -> str: return self.dimension_text(item) def native_address_projection_entry(self, node: ast.Call, native_position: int) -> ProjectionMapping: + """Parse an ``Addr`` projection and preserve its depth and referenced value.""" if len(node.args) != 1: raise ValueError("Addr projection expects one Arg(...), Return(...), or Work(...) reference") if self._addr_depth(node.func) != 1: @@ -1152,6 +1523,7 @@ def native_shape_projection_entry( node: ast.AST, native_position: int, ) -> ProjectionMapping | None: + """Parse a ``value.shape[i]`` native projection, or return ``None`` if absent.""" if not isinstance(node, ast.Subscript) or not isinstance(node.value, ast.Attribute): return None attribute = node.value.attr @@ -1172,6 +1544,12 @@ def native_value_ref( *, allow_named_return: bool = False, ) -> dict[str, int | str]: + """Parse a projection value reference into its normalized dictionary form. + + ``Arg``, ``Return``, and ``Work`` references are validated here. The + returned dictionary is embedded in a projection mapping and never keeps + a live AST reference; named returns require explicit permission. + """ if not isinstance(node, ast.Call): raise ValueError("Expected Arg(...), Return(...), or Work(...) value reference") if node.keywords: @@ -1197,12 +1575,15 @@ def native_value_ref( return {"kind": "work", "name": str(ast.literal_eval(node.args[0]))} raise ValueError("Expected Arg(...), Return(...), or Work(...) value reference") + # Semantic type and metadata conversion + def visible_type( self, node: ast.expr, *, allow_optional_absent_handle: bool = False, ) -> tuple[str, SemanticType, str | None]: + """Load a type annotation and split visibility and optional source-name metadata.""" if self.is_subscript_of(node, "private"): semantic_type, original_name = self.semantic_type_annotation( self.subscript_slice(node), @@ -1221,6 +1602,7 @@ def semantic_type_annotation( *, allow_optional_absent_handle: bool = False, ) -> tuple[SemanticType, str | None]: + """Interpret ``Annotated`` and nullable-handle wrappers before loading their base type.""" optional_item = self._optional_union_item(node) if optional_item is not None: semantic_type = self.semantic_type(optional_item) @@ -1251,6 +1633,12 @@ def semantic_type_annotation( return semantic_type, original_name def semantic_type(self, node: ast.expr) -> SemanticType: + """Convert one supported type-expression AST node into a semantic type. + + Contract imports determine the allowed spelling. Descriptor, address, + array, character, and user/external forms are normalized; malformed or + unimported contract spellings raise ``ValueError``. + """ self._reject_unimported_contract_type(node) optional_item = self._optional_union_item(node) if optional_item is not None: @@ -1296,6 +1684,7 @@ def semantic_type(self, node: ast.expr) -> SemanticType: return self.array_type(node) def _reject_unimported_contract_type(self, node: ast.expr) -> None: + """Reject a bare known contract type that was not imported into this contract.""" name_node = node.value if isinstance(node, ast.Subscript) else node if not isinstance(name_node, ast.Name): return @@ -1306,6 +1695,7 @@ def _reject_unimported_contract_type(self, node: ast.expr) -> None: raise ValueError(f"Contract type {name_node.id!r} must be imported from prik.contracts") def _descriptor_type(self, node: ast.Subscript, descriptor: str) -> SemanticType: + """Load ``Allocatable[T]`` or ``Pointer[T]`` and mark its descriptor storage.""" items = self.subscript_items(node) if len(items) != 1: raise ValueError(f"{descriptor} expects exactly one type: {ast.unparse(node)!r}") @@ -1318,6 +1708,7 @@ def _descriptor_type(self, node: ast.Subscript, descriptor: str) -> SemanticType @classmethod def _array_descriptor_handle_type(cls, semantic_type: SemanticType, descriptor: str) -> SemanticType: + """Copy an array type into an explicit native descriptor-handle contract.""" storage = semantic_type.storage if storage is None or storage.array is None or semantic_type.rank <= 0: raise ValueError(f"{descriptor}[...] array handles require an array type such as {descriptor}[Float64[:]]") @@ -1327,6 +1718,7 @@ def _array_descriptor_handle_type(cls, semantic_type: SemanticType, descriptor: @staticmethod def _apply_scalar_descriptor_kind(semantic_type: SemanticType, descriptor: str) -> None: + """Mark a rank-zero semantic type as an allocatable or pointer descriptor.""" if semantic_type.rank > 0 or (semantic_type.storage is not None and semantic_type.storage.array is not None): raise ValueError(f"{descriptor.capitalize()} projection supports scalar values only") if descriptor == "allocatable": @@ -1339,6 +1731,7 @@ def _apply_scalar_descriptor_kind(semantic_type: SemanticType, descriptor: str) semantic_type.storage = SemanticStorageContract(kind="reference", mutable=True, pointer_depth=1) def _final_type(self, node: ast.Subscript) -> SemanticType: + """Load ``Final[T]`` and append the immutable compile-time-value constraint.""" items = self.subscript_items(node) if len(items) != 1: raise ValueError(f"Final expects exactly one type: {ast.unparse(node)!r}") @@ -1348,6 +1741,7 @@ def _final_type(self, node: ast.Subscript) -> SemanticType: return semantic_type def _address_type(self, node: ast.Call) -> SemanticType: + """Load ``Addr(T)`` and create mutable native address storage for ``T``.""" if len(node.args) != 1 or node.keywords: raise ValueError(f"Addr type expects one argument: {ast.unparse(node)!r}") pointee = self.semantic_type(node.args[0]) @@ -1368,6 +1762,7 @@ def _address_type(self, node: ast.Call) -> SemanticType: return pointee def array_type(self, node: ast.Subscript) -> SemanticType: + """Load a bracketed scalar type as an array or fixed-length character contract.""" if isinstance(node.value, ast.Subscript): if self.matches_name(node.value.value, "String"): semantic_type = self._character_type(node.value, allow_deferred_length=True) @@ -1396,6 +1791,7 @@ def _string_subscript_is_array_dimensions(self, node: ast.Subscript) -> bool: ) def array_dimension_texts(self, node: ast.Subscript) -> list[str]: + """Return normalized source dimension spellings from a bracketed type AST.""" items = self.subscript_items(node) raw_items = self._source_dimension_items(node) if raw_items is None or len(raw_items) != len(items): @@ -1409,6 +1805,7 @@ def array_dimension_texts(self, node: ast.Subscript) -> list[str]: return dimensions def _source_dimension_items(self, node: ast.Subscript) -> list[str] | None: + """Recover source-preserving dimension tokens when original contract text exists.""" if not self.source: return None source = ast.get_source_segment(self.source, node.slice) @@ -1418,6 +1815,7 @@ def _source_dimension_items(self, node: ast.Subscript) -> list[str] | None: @staticmethod def _split_top_level_dimensions(source: str) -> list[str]: + """Split a dimension fragment at commas outside nested syntax and strings.""" items = [] start = 0 depth = 0 @@ -1449,11 +1847,13 @@ def _split_top_level_dimensions(source: str) -> list[str]: @staticmethod def _is_empty_step_slice(text: str) -> bool: + """Report whether a three-part source slice omits its final step value.""" parts = text.split(":") return len(parts) == 3 and parts[2].strip() == "" @staticmethod def _strided_dimension_text(text: str) -> str: + """Replace a syntactic empty stride with the internal strided-dimension marker.""" lower, upper, _step = text.split(":", 2) return f"{lower.strip()}:{upper.strip()}:{_STRIDED_DIMENSION_SENTINEL}" @@ -1464,6 +1864,7 @@ def _array_type_from_dimensions( *, metadata: dict[str, object] | None = None, ) -> SemanticType: + """Build array storage, bounds, axes, and layout from already-parsed dimensions.""" strided_axes = [_STRIDED_DIMENSION_SENTINEL in dim for dim in dims] dims, category, source_shape, lower_bounds, upper_bounds = _PyiAstParser._flat_array_dimensions(dims) if not dims: @@ -1499,6 +1900,7 @@ def _array_type_from_dimensions( def _flat_array_dimensions( dims: list[str], ) -> tuple[list[str], str | None, list[str], list[str | None], list[str | None]]: + """Normalize ``Flat`` placement and derive shape, category, and bounds metadata.""" if not dims: return [], SCALAR_STORAGE_CATEGORY, [], [], [] if _FLAT_DIMENSION_SENTINEL not in dims: @@ -1535,6 +1937,7 @@ def _array_order_for_dimensions( rank: int | None, source_shape: list[str], ) -> str | None: + """Choose the declared default layout for a concrete multidimensional array.""" if rank is None or rank <= 1: return None if category == "assumed_size": @@ -1543,11 +1946,13 @@ def _array_order_for_dimensions( @staticmethod def _flat_array_order(source_shape: list[str], rank: int | None) -> str | None: + """Infer flat-array layout from whether its ``*`` dimension is first or final.""" if rank is None or rank <= 1 or "*" not in source_shape: return None return "ORDER_C" if source_shape.index("*") == 0 else "ORDER_F" def _character_type(self, node: ast.Subscript, *, allow_deferred_length: bool = False) -> SemanticType: + """Load a fixed or allowed deferred ``String`` length annotation.""" items = self.subscript_items(node) if len(items) != 1 or (isinstance(items[0], ast.Constant) and items[0].value is Ellipsis): raise ValueError("Fixed character types use String[length]; use String for non-fixed length") @@ -1571,6 +1976,12 @@ def _character_type(self, node: ast.Subscript, *, allow_deferred_length: bool = ) def apply_annotation_metadata(self, semantic_type: SemanticType, node: ast.expr) -> None: + """Apply one ``Annotated`` metadata AST item to a semantic type in place. + + Imported contract markers update recognized storage or policy-input + metadata; other names become user constraints. Unsupported expressions + fail rather than silently discarding contract information. + """ if isinstance(node, ast.Name): name = self.contract_name(node) if name is None: @@ -1584,6 +1995,7 @@ def apply_annotation_metadata(self, semantic_type: SemanticType, node: ast.expr) raise ValueError(f"Unsupported Annotated metadata: {ast.unparse(node)!r}") def _apply_annotation_metadata_call(self, semantic_type: SemanticType, node: ast.Call) -> None: + """Dispatch one callable metadata form, mutating type metadata or constraints.""" helper = self._annotation_metadata_call_helper(semantic_type, node) if helper is None: return @@ -1617,6 +2029,7 @@ def _apply_annotation_metadata_call(self, semantic_type: SemanticType, node: ast ) def _annotation_metadata_call_helper(self, semantic_type: SemanticType, node: ast.Call) -> str | None: + """Resolve a metadata helper name, applying an unimported user constraint directly.""" helper = self.contract_name(node.func) if helper is not None: return helper @@ -1628,6 +2041,7 @@ def _annotation_metadata_call_helper(self, semantic_type: SemanticType, node: as return None def _apply_user_constraint_metadata_call(self, semantic_type: SemanticType, node: ast.Call) -> None: + """Append a positional user-defined constraint call to a semantic type.""" if not isinstance(node.func, ast.Name): raise ValueError(f"Expected user constraint name: {ast.unparse(node)!r}") if node.keywords: @@ -1640,17 +2054,20 @@ def _apply_user_constraint_metadata_call(self, semantic_type: SemanticType, node @staticmethod def _require_single_metadata_argument(node: ast.Call, helper: str): + """Return a helper's sole literal metadata argument or raise for another call shape.""" if len(node.args) != 1 or node.keywords: raise ValueError(f"{helper} metadata expects one argument: {ast.unparse(node)!r}") return ast.literal_eval(node.args[0]) def _apply_pointer_association_metadata(self, semantic_type: SemanticType, node: ast.Call) -> None: + """Record a pointer-association fact and mark the type as a Fortran pointer.""" value = self._require_single_metadata_argument(node, "PointerAssociation") semantic_type.metadata["fortran_pointer_association"] = str(value) semantic_type.metadata["fortran_pointer"] = True @staticmethod def _apply_pointer_policy_metadata(semantic_type: SemanticType, node: ast.Call) -> None: + """Validate ``PointerPolicy`` keywords and delegate their metadata update in place.""" if node.args: raise ValueError(f"PointerPolicy metadata accepts keyword arguments only: {ast.unparse(node)!r}") values = {} @@ -1663,6 +2080,7 @@ def _apply_pointer_policy_metadata(semantic_type: SemanticType, node: ast.Call) set_pointer_policy_metadata(semantic_type.metadata, **values) def _apply_ownership_annotation_metadata(self, semantic_type: SemanticType, node: ast.Call, helper: str) -> None: + """Store one declared ownership, transfer, or destruction policy input on a type.""" value = str(self._require_single_metadata_argument(node, helper)) set_ownership_metadata( semantic_type.metadata, @@ -1672,6 +2090,12 @@ def _apply_ownership_annotation_metadata(self, semantic_type: SemanticType, node ) def _apply_metadata_name(self, semantic_type: SemanticType, name: str) -> bool: + """Apply one known bare metadata marker and report whether it was recognized. + + Array layout/copy facts and policy inputs update ``semantic_type`` in + place. The boolean lets the caller preserve unknown imported names as + ordinary constraints while contradictions raise immediately. + """ if name in {"ORDER_C", "ORDER_F", "ORDER_ANY"}: array = self._require_array_storage(semantic_type) if array.rank is None or array.rank <= 1: @@ -1741,6 +2165,7 @@ def _append_constraint_metadata( name: str, arguments: list[object], ) -> None: + """Append validated user constraint metadata, rejecting obsolete built-in spellings.""" if name == "Constant": raise ValueError("Constant metadata is not supported; use Final[...]") if name == "Shape": @@ -1749,6 +2174,7 @@ def _append_constraint_metadata( @staticmethod def _validate_python_value_policy(semantic_type: SemanticType, *, writable: bool, owner: str) -> None: + """Reject the unsupported writable immutable borrowed-view policy combination.""" if semantic_type.metadata.get(PYTHON_VALUE_MUTABILITY_METADATA) != PYTHON_VALUE_IMMUTABLE: return if not writable: @@ -1765,6 +2191,7 @@ def _validate_python_value_policy(semantic_type: SemanticType, *, writable: bool @staticmethod def _require_array_storage(semantic_type: SemanticType) -> SemanticArrayContract: + """Ensure a semantic type owns array storage and return that mutable contract.""" if semantic_type.storage is None: semantic_type.storage = SemanticStorageContract(kind="array") if semantic_type.storage.array is None: @@ -1776,6 +2203,7 @@ def _require_array_storage(semantic_type: SemanticType) -> SemanticArrayContract @staticmethod def _bounds_from_source_shape(shape: list[str]) -> tuple[list[str | None], list[str | None]]: + """Derive normalized lower and upper bounds from source dimension text.""" lower_bounds: list[str | None] = [] upper_bounds: list[str | None] = [] for dim in shape: @@ -1795,18 +2223,21 @@ def _bounds_from_source_shape(shape: list[str]) -> tuple[list[str | None], list[ @staticmethod def _type_uses_writable_storage(semantic_type: SemanticType) -> bool: + """Report whether a type's existing storage can be written by native code.""" storage = semantic_type.storage if storage is None: return False return storage.kind in {"reference", "array", "pointer", "callback", "address"} and not storage.read_only def _is_addr_call(self, node: ast.Call) -> bool: + """Recognize imported ``Addr`` calls, including explicit address-depth subscripts.""" return self.matches_name(node.func, "Addr") or ( isinstance(node.func, ast.Subscript) and self.matches_name(node.func.value, "Addr") ) @staticmethod def _addr_depth(node: ast.AST) -> int: + """Return an ``Addr`` pointer depth, rejecting the redundant depth-one form.""" if isinstance(node, ast.Subscript): depth = int(ast.literal_eval(node.slice)) if depth <= 1: @@ -1815,6 +2246,7 @@ def _addr_depth(node: ast.AST) -> int: return 1 def _is_array_subscript(self, node: ast.Subscript) -> bool: + """Distinguish dimension subscriptions from contract metadata subscriptions.""" if isinstance(node.value, ast.Subscript): return self._is_array_subscript(node.value) items = self.subscript_items(node) @@ -1838,10 +2270,15 @@ def _is_array_subscript(self, node: ast.Subscript) -> bool: return False if any(isinstance(item, ast.Call) for item in items): return True - return any(isinstance(item, ast.BinOp | ast.UnaryOp) for item in items) + return any( + isinstance(item, ast.BinOp | ast.UnaryOp | ast.BoolOp | ast.Compare | ast.IfExp) + or (isinstance(item, ast.Attribute | ast.Subscript) and is_public_declaration_expression(ast.unparse(item))) + for item in items + ) @staticmethod def _non_dimension_subscription_names() -> set[str]: + """Return imported helper names that cannot be interpreted as array dimensions.""" return { "Allocatable", "Constant", @@ -1862,6 +2299,7 @@ def _non_dimension_subscription_names() -> set[str]: } def dimension_text(self, node: ast.expr) -> str: + """Render one validated array-dimension AST item into canonical source text.""" if isinstance(node, ast.Constant) and node.value is Ellipsis: return "..." if isinstance(node, ast.Slice): @@ -1870,11 +2308,13 @@ def dimension_text(self, node: ast.expr) -> str: return str(node.value) if self.matches_name(node, "Flat"): return _FLAT_DIMENSION_SENTINEL - if isinstance(node, ast.Attribute | ast.Subscript): - raise ValueError(f"Unsupported array dimension expression: {ast.unparse(node)!r}") - return ast.unparse(node) + expression = ast.unparse(node) + if not is_public_declaration_expression(expression): + raise ValueError(f"Unsupported array dimension expression: {expression!r}") + return expression def slice_text(self, node: ast.Slice) -> str: + """Render one dimension slice, preserving the contract's strided marker.""" lower = "" if node.lower is None else ast.unparse(node.lower) upper = "" if node.upper is None else ast.unparse(node.upper) step = "" @@ -1884,7 +2324,24 @@ def slice_text(self, node: ast.Slice) -> str: return f"{lower}:{upper}:{step}" return f"{lower}:{upper}" - def _prototype_argument_spec(self, node: ast.expr) -> _CallbackArgumentSpec: + # Callback and result conversion + + def _prototype_argument_spec(self, node: ast.expr) -> _PrototypeArgumentSpec: + """Convert one prototype annotation into exact direction and transport facts.""" + if isinstance(node, ast.Call): + wrapper = self.contract_name(node.func) + if wrapper in {"In", "Out", "InOut"}: + if len(node.args) != 1 or node.keywords: + raise ValueError(f"{wrapper} expects one prototype argument type") + nested = self._prototype_argument_spec(node.args[0]) + if nested.intent is not None: + raise ValueError("prototype intent wrappers cannot be nested") + intent = {"In": "in", "Out": "out", "InOut": "inout"}[wrapper] + return _PrototypeArgumentSpec(nested.semantic_type, nested.passes_by_value, intent) + return self._prototype_transport_spec(node) + + def _prototype_transport_spec(self, node: ast.expr) -> _PrototypeArgumentSpec: + """Convert the transport-bearing inner type of one prototype dummy.""" if isinstance(node, ast.Call): wrapper = self.contract_name(node.func) if wrapper == "Value": @@ -1904,18 +2361,18 @@ def _prototype_argument_spec(self, node: ast.expr) -> _CallbackArgumentSpec: or self._has_callback_descriptor_metadata(semantic_type) ): raise ValueError("Value(...) callback arguments are only valid for rank-zero wrapped types") - return _CallbackArgumentSpec(semantic_type, True) + return _PrototypeArgumentSpec(semantic_type, True) if self._is_addr_call(node): return self._prototype_address_argument_spec(node) semantic_type = self.semantic_type(node) if self._is_primitive_scalar_value_type(semantic_type): - return _CallbackArgumentSpec(semantic_type, True) + return _PrototypeArgumentSpec(semantic_type, True) self._mark_callback_reference_type(semantic_type) - return _CallbackArgumentSpec(semantic_type, False) + return _PrototypeArgumentSpec(semantic_type, False) - def _prototype_address_argument_spec(self, node: ast.Call) -> _CallbackArgumentSpec: - """Parse the callback-only primitive reference marker.""" + def _prototype_address_argument_spec(self, node: ast.Call) -> _PrototypeArgumentSpec: + """Parse the prototype-only primitive reference marker.""" if len(node.args) != 1 or node.keywords: raise ValueError(f"Addr type expects one callback argument type: {ast.unparse(node)!r}") if self._addr_depth(node.func) != 1: @@ -1927,10 +2384,11 @@ def _prototype_address_argument_spec(self, node: ast.Call) -> _CallbackArgumentS "arrays, strings, and wrapped objects already use reference storage" ) self._mark_callback_reference_type(semantic_type) - return _CallbackArgumentSpec(semantic_type, False) + return _PrototypeArgumentSpec(semantic_type, False) @staticmethod def _is_primitive_scalar_value_type(semantic_type: SemanticType) -> bool: + """Report whether a callback type is a plain native scalar passed by value.""" return bool( semantic_type.rank == 0 and semantic_type.name not in {"String", "Void"} @@ -1941,6 +2399,7 @@ def _is_primitive_scalar_value_type(semantic_type: SemanticType) -> bool: @staticmethod def _has_callback_descriptor_metadata(semantic_type: SemanticType) -> bool: + """Report whether callback metadata requires non-value storage treatment.""" return any( semantic_type.metadata.get(name) for name in ( @@ -1953,6 +2412,7 @@ def _has_callback_descriptor_metadata(semantic_type: SemanticType) -> bool: @staticmethod def _mark_callback_reference_type(semantic_type: SemanticType) -> None: + """Mutate a callback argument type into writable reference-compatible storage.""" storage = semantic_type.storage if semantic_type.name == "String" and semantic_type.rank == 0: semantic_type.storage = SemanticStorageContract( @@ -1975,6 +2435,7 @@ def _mark_callback_reference_type(semantic_type: SemanticType) -> None: @staticmethod def _semantic_shape_dimensions(semantic_type: SemanticType) -> list[tuple[str, bool]]: + """Return semantic shape dimensions paired with their strided-axis markers.""" storage = semantic_type.storage array = storage.array if storage is not None else None dimensions = list(semantic_type.shape) @@ -1987,6 +2448,7 @@ def _semantic_shape_dimensions(semantic_type: SemanticType) -> list[tuple[str, b @staticmethod def _callback_metadata(arguments: list[SemanticType] | None, return_type: SemanticType) -> dict[str, object]: + """Build the fixed callback ABI metadata dictionary from signature semantic types.""" return { "arguments": arguments, "return": return_type, @@ -1998,6 +2460,7 @@ def _callback_metadata(arguments: list[SemanticType] | None, return_type: Semant @staticmethod def _callback_storage() -> SemanticStorageContract: + """Return the standard borrowed, call-lifetime storage contract for a callback.""" return SemanticStorageContract( kind="callback", ownership="borrowed", @@ -2010,6 +2473,13 @@ def return_projection( *, optional_return_positions: set[int] | None = None, ) -> tuple[SemanticType | None, list[SemanticArgument]]: + """Split a stub return annotation into direct and projected semantic results. + + The first plain result is the direct function result; later plain items + and every ``Returns[name, T]`` item become ordered output arguments. + Nullable result slots are retained only where native projections allow + them, and the resulting list preserves source tuple order. + """ if isinstance(node, ast.Constant) and node.value is None: return None, [] @@ -2050,6 +2520,7 @@ def return_projection( return return_type, returned_args def _return_item_type(self, node: ast.expr, *, unwrap_optional: bool) -> tuple[SemanticType, bool]: + """Load one return item and report whether an allowed ``| None`` was unwrapped.""" if not unwrap_optional: return self.semantic_type(node), False optional_node = self._optional_union_item(node) @@ -2059,6 +2530,7 @@ def _return_item_type(self, node: ast.expr, *, unwrap_optional: bool) -> tuple[S @staticmethod def _optional_union_item(node: ast.expr) -> ast.expr | None: + """Return the non-``None`` item of a two-way optional union, if present.""" if not isinstance(node, ast.BinOp) or not isinstance(node.op, ast.BitOr): return None left_none = isinstance(node.left, ast.Constant) and node.left.value is None @@ -2068,6 +2540,7 @@ def _optional_union_item(node: ast.expr) -> ast.expr | None: return node.right if left_none else node.left def returned_argument(self, node: ast.expr) -> SemanticArgument | None: + """Convert ``Returns[name, T]`` into a mutable output argument, or return ``None``.""" if not self.is_subscript_of(node, "Returns"): return None items = self.subscript_items(node) @@ -2084,6 +2557,7 @@ def returned_argument(self, node: ast.expr) -> SemanticArgument | None: ) def name_metadata(self, node: ast.expr) -> str | None: + """Return the native name from supported ``SourceName`` metadata, if any.""" if isinstance(node, ast.Call) and self.matches_name(node.func, "SourceName"): if len(node.args) != 1: raise ValueError(f"SourceName metadata expects one argument: {ast.unparse(node)!r}") @@ -2092,6 +2566,7 @@ def name_metadata(self, node: ast.expr) -> str | None: @staticmethod def annotation_target(node: ast.AST) -> str: + """Return an assignment target name, including the supported ``var[...]`` escape.""" if isinstance(node, ast.Name): return node.id if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name) and node.value.id == "var": @@ -2100,10 +2575,12 @@ def annotation_target(node: ast.AST) -> str: @staticmethod def default_marks_optional(node: ast.expr | None) -> bool: + """Report whether an ellipsis or ``None`` default marks a contract value optional.""" return isinstance(node, ast.Constant) and node.value in {Ellipsis, None} @staticmethod def literal_default_value(node: ast.expr | None) -> str | None: + """Validate and render an immutable literal default, returning ``None`` for optional defaults.""" if node is None or _PyiAstParser.default_marks_optional(node): return None try: @@ -2114,6 +2591,7 @@ def literal_default_value(node: ast.expr | None) -> str | None: @staticmethod def assignment_default_value(node: ast.expr | None, semantic_type: SemanticType) -> str | None: + """Render an assignment default, allowing non-literals only for ``Final`` constants.""" if node is None or _PyiAstParser.default_marks_optional(node): return None if any(constraint.name == "Constant" for constraint in semantic_type.constraints): @@ -2122,6 +2600,7 @@ def assignment_default_value(node: ast.expr | None, semantic_type: SemanticType) @staticmethod def qualified_name(node: ast.AST) -> tuple[str, ...] | None: + """Return a dotted name's components, or ``None`` for an unsupported AST expression.""" if isinstance(node, ast.Name): return (node.id,) if isinstance(node, ast.Attribute): @@ -2132,39 +2611,47 @@ def qualified_name(node: ast.AST) -> tuple[str, ...] | None: return None def contract_name(self, node: ast.AST) -> str | None: + """Resolve one local name through the contract-import bindings without mutation.""" if not isinstance(node, ast.Name): return None return self._contract_bindings.get(node.id) def matches_name(self, node: ast.AST, name: str) -> bool: + """Report whether an AST name resolves to a particular imported contract symbol.""" return self.contract_name(node) == name @staticmethod def matches_plain_name(node: ast.AST, name: str) -> bool: + """Report whether an AST node is exactly an unqualified Python name.""" return isinstance(node, ast.Name) and node.id == name def required_name(self, node: ast.AST) -> str: + """Resolve an imported contract helper or raise a closed diagnostic for other names.""" name = self.contract_name(node) if name is None: raise ValueError(f"Expected imported prik contract helper: {ast.unparse(node)!r}") return name def is_subscript_of(self, node: ast.AST, name: str) -> bool: + """Report whether a subscript uses one particular imported contract helper.""" return isinstance(node, ast.Subscript) and self.matches_name(node.value, name) @staticmethod def subscript_slice(node: ast.AST) -> ast.expr: + """Return a subscript's slice or reject expressions that are not subscriptions.""" if not isinstance(node, ast.Subscript): raise ValueError(f"Unsupported type annotation: {ast.unparse(node)!r}") return node.slice def subscript_items(self, node: ast.AST) -> list[ast.expr]: + """Return a subscript slice as a one-or-many list while preserving tuple order.""" value = self.subscript_slice(node) if isinstance(value, ast.Tuple): return list(value.elts) return [value] def type_name(self, node: ast.AST) -> str: + """Render a type base name while replacing imported aliases with contract names.""" if isinstance(node, ast.Subscript): contract_name = self.contract_name(node.value) return contract_name or ast.unparse(node.value) @@ -2173,6 +2660,8 @@ def type_name(self, node: ast.AST) -> str: return contract_name return ast.unparse(node) + # Callable construction + def _callable_parts( self, node: ast.FunctionDef, @@ -2181,11 +2670,21 @@ def _callable_parts( native_result: ProjectionMapping | None = None, drop_untyped_self: bool = False, ) -> tuple[list[SemanticArgument], SemanticType | None]: + """Build a callable's arguments, results, and native projection metadata. + + The ordered stages validate the stub header, load typed arguments, + apply input projection storage, construct direct and projected results, + and then complete mapping names. ``projection`` is intentionally + mutated when identity mappings are required for ``Returns`` syntax. + """ + # Validate and load the callable's visible input contract. self._validate_callable_header(node) semantic_args = self._callable_semantic_arguments(node, projection, drop_untyped_self=drop_untyped_self) visible_args = list(semantic_args) self._apply_argument_value_projections(visible_args, projection) self._apply_argument_descriptor_projections(visible_args, projection) + + # Construct direct and projected outputs from the Python return shape. optional_return_positions = self._optional_native_return_positions(projection, native_result) return_type, returned_args = self.return_projection( node.returns, @@ -2194,6 +2693,8 @@ def _callable_parts( self._validate_callable_descriptor_return(return_type, native_result) return_type, returned_args = self._apply_native_call_returns(return_type, returned_args, projection) return_type = self._apply_native_result_projection(return_type, native_result) + + # Complete native output placement and the mapping's visible names. return_positions = self._return_positions_by_name(returned_args) self._apply_projected_returns(semantic_args, returned_args) if returned_args and not projection: @@ -2293,6 +2794,12 @@ def _callable_argument( *, nullable_descriptor: bool = False, ) -> SemanticArgument: + """Convert one typed stub parameter into a semantic argument declaration. + + Nullable descriptor arguments are unwrapped only when their projection + requires them. The returned argument records visibility, optionality, + and writable storage facts; contradictory optional-handle policies fail. + """ if arg.annotation is None: raise ValueError(f"Expected typed argument: {arg.arg!r}") annotation = arg.annotation @@ -2330,6 +2837,7 @@ def _validate_optional_native_array_handle_argument( default: ast.expr | None, semantic_type: SemanticType, ) -> None: + """Require consistent nullable spelling and default syntax for array handles.""" descriptor_kind = native_array_descriptor_kind(semantic_type) if descriptor_kind is None: return @@ -2351,6 +2859,7 @@ def _apply_argument_descriptor_projections( arguments: list[SemanticArgument], projection: list[ProjectionMapping], ) -> None: + """Apply scalar allocatable/pointer projection kinds to referenced arguments in place.""" for mapping in projection: if mapping.value_kind not in {"allocatable", "pointer"} or mapping.python_position is None: continue @@ -2384,6 +2893,7 @@ def _apply_argument_value_projections( @staticmethod def _semantic_scalar_descriptor_kind(semantic_type: SemanticType | None) -> str | None: + """Return the declared scalar descriptor kind, excluding arrays and plain values.""" if semantic_type is None or semantic_type.rank != 0: return None if semantic_type.metadata.get("fortran_allocatable"): @@ -2397,6 +2907,7 @@ def _apply_native_result_projection( return_type: SemanticType | None, native_result: ProjectionMapping | None, ) -> SemanticType | None: + """Apply the nullable scalar descriptor mapping to the direct result type.""" if native_result is None: return return_type if return_type is None: @@ -2408,12 +2919,14 @@ def _apply_native_result_projection( @staticmethod def _argument_defaults(node: ast.FunctionDef) -> list[ast.expr | None]: + """Align positional parameter defaults with every declared positional argument.""" defaults: list[ast.expr | None] = [None] * (len(node.args.args) - len(node.args.defaults)) defaults.extend(node.args.defaults) return defaults @staticmethod def _validate_stub_callable(node: ast.FunctionDef) -> None: + """Require the semantic-contract stub body to consist solely of an ellipsis.""" if len(node.body) != 1: raise ValueError(f"Unsupported function header: {_node_text(node)!r}") body = node.body[0] @@ -2422,6 +2935,7 @@ def _validate_stub_callable(node: ast.FunctionDef) -> None: @staticmethod def _apply_projected_returns(semantic_args: list[SemanticArgument], returned_args: list[SemanticArgument]) -> None: + """Merge ``Returns`` outputs into native arguments and mark their storage writable.""" by_name = {arg.name: arg for arg in semantic_args} for returned in returned_args: existing = by_name.get(returned.name) @@ -2440,6 +2954,7 @@ def _apply_projected_returns(semantic_args: list[SemanticArgument], returned_arg @staticmethod def _mark_projected_output(semantic_type: SemanticType) -> None: + """Mutate a projected result type so later stages see writable output storage.""" semantic_type.ownership.mutable = True if semantic_type.storage is not None: semantic_type.storage.read_only = False @@ -2471,6 +2986,7 @@ def _apply_native_call_returns( returned_args: list[SemanticArgument], projection: list[ProjectionMapping], ) -> tuple[SemanticType | None, list[SemanticArgument]]: + """Move native-call output mappings from Python result slots into output arguments.""" output_by_result = { mapping.result_position: mapping for mapping in projection @@ -2561,6 +3077,7 @@ def _apply_descriptor_output_kind( @staticmethod def _return_positions_by_name(returned_args: list[SemanticArgument]) -> dict[str, int | None]: + """Index projected return names by their original tuple result positions.""" return {returned.name: returned.metadata.get("return_position") for returned in returned_args} @staticmethod @@ -2569,6 +3086,7 @@ def _apply_native_call_argument_names( return_positions: dict[str, int | None], projection: list[ProjectionMapping], ) -> None: + """Complete projection names and output slots from their referenced semantic arguments.""" for mapping in projection: if mapping.python_position is None: continue @@ -2587,12 +3105,14 @@ def _shift_argument_value_ref( old_position: int, new_position: int, ) -> None: + """Update an embedded argument value reference after a preceding insertion.""" if mapping.value_kind not in {"addr", "allocatable", "pointer", "value"} or not isinstance(mapping.value, dict): return if mapping.value.get("kind") == "arg" and mapping.value.get("position") == old_position: mapping.value["position"] = new_position def return_items(self, node: ast.expr) -> list[ast.expr]: + """Flatten supported ``tuple[...]`` returns, otherwise keep one return expression.""" if isinstance(node, ast.Subscript) and ( self.matches_plain_name(node.value, "tuple") or self.matches_plain_name(node.value, "Tuple") ): @@ -2600,8 +3120,14 @@ def return_items(self, node: ast.expr) -> list[ast.expr]: return [node] +# AST visitor adapters + + class _ClassBodyVisitor(ClassVisitor): + """Collect declarations from one class body before constructing its semantic class.""" + def __init__(self, parser: _PyiAstParser, *, class_name: str): + """Initialize empty class members and constructor/overload bookkeeping state.""" self.parser = parser self.class_name = class_name self.fields: list[SemanticField] = [] @@ -2627,8 +3153,8 @@ def _visit_AnnAssign(self, node: ast.AnnAssign) -> None: def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: """Convert a method, constructor, or overload declaration.""" decorators = self.parser.decorators(node.decorator_list, context="class body") - if decorators.external: - raise ValueError("external is not valid for a class method") + if decorators.standalone: + raise ValueError("standalone is not valid for a class method") if decorators.native_type is not None: raise ValueError("native_type is only valid for classes") if not node.decorator_list and self._is_generated_constructor(node): @@ -2666,6 +3192,7 @@ def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: @staticmethod def _is_generated_constructor(node: ast.FunctionDef) -> bool: + """Recognize the printer's self-only or all-default-keyword constructor stub.""" args = node.args if ( node.name == "__init__" @@ -2700,7 +3227,7 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: or decorators.bind_target is not None or decorators.release_gil or decorators.error_status_policy is not None - or decorators.external + or decorators.standalone ): raise ValueError(f"Unsupported class body decorator: {ast.unparse(node.decorator_list[-1])!r}") if ( @@ -2726,7 +3253,10 @@ def _visit_not_supported(node: ast.AST) -> None: class _ModuleVisitor(ClassVisitor): + """Dispatch supported top-level AST nodes into a parser's mutable semantic module.""" + def __init__(self, parser: _PyiAstParser): + """Keep the parser whose module receives visited top-level declarations.""" self.parser = parser def _visit_Module(self, node: ast.Module) -> None: @@ -2760,7 +3290,7 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: or decorators.bind_target is not None or decorators.release_gil or decorators.error_status_policy is not None - or decorators.external + or decorators.standalone ): raise ValueError(f"Unsupported class decorator: {ast.unparse(node.decorator_list[-1])!r}") if ( @@ -2785,7 +3315,13 @@ def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: if decorators.native_type is not None: raise ValueError("native_type is only valid for classes") if decorators.prototype: - self.parser.module.prototypes.append(self.parser.prototype_def(node, visibility=decorators.visibility)) + self.parser.module.prototypes.append( + self.parser.prototype_def( + node, + visibility=decorators.visibility, + pure=decorators.pure, + ) + ) return function = self.parser.function_def( node, @@ -2793,7 +3329,7 @@ def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: projection=decorators.projection, native_result=decorators.native_result, native_name=decorators.bind_target, - external=decorators.external, + standalone=decorators.standalone, has_native_call=decorators.has_native_call, release_gil=decorators.release_gil, error_status_policy=decorators.error_status_policy, @@ -2816,12 +3352,22 @@ def _visit_not_supported(node: ast.AST) -> None: raise ValueError(f"Unsupported .pyi node: {_node_text(node)!r}") +# Cross-module reference reconciliation + + def _node_text(node: ast.AST) -> str: + """Render the first line of an AST node for concise validation diagnostics.""" text = ast.unparse(node) return text.splitlines()[0] if text else type(node).__name__ def _annotate_imported_external_type_refs(module: SemanticModule) -> None: + """Mark types named by imports as unresolved external references in place. + + The function reads module imports and semantic types, adds default reference + metadata only when absent, and leaves cross-module representation resolution + to :func:`reconcile_external_type_refs`. + """ imported = _imported_type_refs(module) for semantic_type in _iter_module_semantic_types(module): imported_ref = imported.get(semantic_type.name) @@ -2841,6 +3387,7 @@ def _annotate_imported_external_type_refs(module: SemanticModule) -> None: def _imported_type_refs(module: SemanticModule) -> dict[str, tuple[str, str, str]]: + """Index direct and dotted imported type spellings by local semantic type name.""" imported: dict[str, tuple[str, str, str]] = {} imported_namespaces: dict[str, str] = {} for imp in module.imports: @@ -2869,6 +3416,7 @@ def _imported_type_refs(module: SemanticModule) -> dict[str, tuple[str, str, str def _relative_imported_namespace(module_name: str, source_name: str) -> str: + """Join a relative import's namespace and imported name without package context.""" module_path = module_name.lstrip(".") if not module_path: return source_name @@ -2915,8 +3463,17 @@ def _bind_prototype_reference( def reconcile_external_type_refs(modules: list[SemanticModule]) -> list[SemanticModule]: + """Resolve imported class and prototype references across converted modules. + + Use this for a complete batch after each module has passed + :func:`convert_pyi_to_ir`. The input list and referenced type metadata are + mutated in place: matching prototypes become callback references, while + classes are marked ``wrapped`` or ``opaque``. The same list is returned for + 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} + functions = {(module.name, function.name): function for module in modules for function in module.functions} for module in modules: for semantic_type in _iter_module_semantic_types(module): ref = semantic_type.metadata.get(EXTERNAL_TYPE_REF_METADATA) @@ -2952,4 +3509,109 @@ def reconcile_external_type_refs(modules: list[SemanticModule]) -> list[Semantic ) ref["wrapped"] = wrapped ref["representation"] = "wrapped" if wrapped else "opaque" + _reconcile_declaration_expression_callables(module, prototypes, functions) return modules + + +def _reconcile_declaration_expression_callables( + module: SemanticModule, + prototypes: dict[tuple[str, str], SemanticPrototype], + functions: dict[tuple[str, str], SemanticFunction], +) -> None: + """Link imported declaration calls to exact batch declarations when present.""" + for reference in _unresolved_declaration_expression_callables(module): + scopes = _declaration_callable_scope_candidates(reference.native_scope) + native_name = reference.native_name or reference.name.rsplit(".", 1)[-1] + prototype = _declaration_callable_prototype(prototypes, scopes, native_name) + if prototype is not None: + _bind_declaration_expression_callable(reference, prototype, native_scope=None, placement="standalone") + continue + function_match = _declaration_callable_function(functions, scopes, native_name) + if function_match is not None: + function, native_scope = function_match + _bind_declaration_expression_callable(reference, function, native_scope=native_scope, placement="module") + + +def _unresolved_declaration_expression_callables(module: SemanticModule): + """Yield imported array-expression callables that still need batch binding. + + The semantic module remains unmodified while traversing. References that + already have a declaration or lack a native scope are intentionally omitted + because their provenance is complete or explicitly unresolved. + """ + for semantic_type in _iter_module_semantic_types(module): + storage = semantic_type.storage + array = storage.array if storage is not None else None + if array is None: + continue + for references in array.expression_callables: + for reference in references: + if reference.declaration is None and reference.native_scope is not None: + yield reference + + +def _declaration_callable_scope_candidates(native_scope: str) -> tuple[str, str, str]: + """Return the exact, relative-stripped, and leaf module spellings to match.""" + return ( + native_scope, + native_scope.lstrip("."), + native_scope.lstrip(".").rsplit(".", 1)[-1], + ) + + +def _declaration_callable_prototype( + prototypes: dict[tuple[str, str], SemanticPrototype], + scopes: tuple[str, str, str], + native_name: str, +) -> SemanticPrototype | None: + """Return the first prototype matching the established scope-candidate order.""" + for scope in scopes: + prototype = prototypes.get((scope, native_name)) + if scope and prototype is not None: + return prototype + return None + + +def _declaration_callable_function( + functions: dict[tuple[str, str], SemanticFunction], + scopes: tuple[str, str, str], + native_name: str, +) -> tuple[SemanticFunction, str] | None: + """Return the first matching function together with its resolved module scope.""" + for scope in scopes: + function = functions.get((scope, native_name)) + if scope and function is not None: + return function, scope + return None + + +def _bind_declaration_expression_callable( + reference: SemanticExpressionCallable, + declaration: SemanticPrototype | SemanticFunction, + *, + native_scope: str | None, + placement: str, +) -> None: + """Mutate one expression reference with its resolved declaration provenance.""" + reference.declaration = declaration + reference.native_name = declaration.native_name or declaration.name + reference.native_scope = native_scope + reference.placement = placement + + +if __name__ == "__main__": + from prik.parsers.pyi import parse_pyi_text + + contract = """from prik.contracts import Float64 + +def scale(value: Float64) -> Float64: ... +""" + parsed_contract = parse_pyi_text(contract, filename="math.pyi") + semantic_module = convert_pyi_to_ir(parsed_contract, module_name="math", source=contract) + semantic_function = semantic_module.functions[0] + semantic_argument = semantic_function.arguments[0] + print( + f"{semantic_module.name}.{semantic_function.name}" + f"({semantic_argument.name}): {semantic_argument.semantic_type.name}" + f" -> {semantic_function.return_type.name}" + ) diff --git a/prik/semantics/wrapper_policy.py b/prik/semantics/wrapper_policy.py index 1d821bada..3392e47a8 100644 --- a/prik/semantics/wrapper_policy.py +++ b/prik/semantics/wrapper_policy.py @@ -1,4 +1,12 @@ -"""Completed wrapper policies for the currently supported generation lanes.""" +"""Project completed semantic decisions into backend-neutral wrapper policies. + +This module consumes semantic signatures and ownership decisions completed by +``policy_completion``. It produces immutable records for wrapper planning: +Python/native boundaries, ordered call slots, result projections, lifecycle, +module and derived-object access, and fail-closed support blockers. Planners +and backend generators consume these records without inferring replacement +policy from a datatype or native declaration. +""" from __future__ import annotations @@ -35,11 +43,17 @@ StorageMode, TransferMode, ) +from prik.utilities.declaration_expressions import ( + declaration_expression_call_sites, + declaration_extent_references, + resolve_declaration_extent, +) +from prik.types.numpy import BOOLEAN_SEMANTIC_TYPE_NAMES, is_boolean_semantic_type_name _PLAN_PRIMITIVE_SCALAR_TYPES = frozenset( { - "Bool", + *BOOLEAN_SEMANTIC_TYPE_NAMES, "Int8", "Int16", "Int32", @@ -52,7 +66,7 @@ ) _NUMPY_DTYPE_NAMES = { - "Bool": "bool", + **dict.fromkeys(BOOLEAN_SEMANTIC_TYPE_NAMES, "bool"), "Int8": "int8", "Int16": "int16", "Int32": "int32", @@ -80,6 +94,8 @@ "materialize fixed-length Fortran character storage from a caller-supplied raw address and copy mutation back" ) DERIVED_VALUE_COPY_REASON = "pass an exact derived pointee through a typed native value dummy" +LOGICAL_SCALAR_KIND_COPY_REASON = "adapt a C-interoperable Boolean through storage with the native Fortran logical kind" +LOGICAL_ARRAY_KIND_COPY_REASON = "adapt a one-byte Boolean array through storage with the native Fortran logical kind" class OptionalMode(str, Enum): @@ -134,6 +150,22 @@ class ArrayWritebackABI(str, Enum): LOGICAL_LOW_BIT_INT8 = "logical_low_bit_int8" +class ScalarLogicalABI(str, Enum): + """Completed scalar logical adaptation between the C and native dummies.""" + + NOT_APPLICABLE = "not_applicable" + C_BOOL = "c_bool" + NATIVE_KIND_COPY = "native_kind_copy" + + +class ArrayLogicalABI(str, Enum): + """Completed Boolean-array adaptation between NumPy and native storage.""" + + NOT_APPLICABLE = "not_applicable" + C_BOOL_VIEW = "c_bool_view" + NATIVE_KIND_COPY = "native_kind_copy" + + _ARRAY_VALUE_OPTIONAL_MODES = frozenset({OptionalMode.REQUIRED, OptionalMode.NULLABLE_VALUE}) _ARRAY_DESCRIPTOR_OPTIONAL_MODES = frozenset({OptionalMode.REQUIRED, OptionalMode.DESCRIPTOR}) _ARRAY_VIEW_CODEGEN_ACTIONS = frozenset( @@ -243,6 +275,7 @@ class ModuleGetterAction(str, Enum): CONSTANT_VALUE = "constant_value" NATIVE_CONSTANT_VALUE = "native_constant_value" + NATIVE_CONSTANT_ARRAY_VALUE = "native_constant_array_value" DIRECT_VALUE = "direct_value" NULLABLE_SNAPSHOT = "nullable_snapshot" BORROWED_ARRAY_VIEW = "borrowed_array_view" @@ -413,9 +446,16 @@ class ExternalDeclarationMode(str, Enum): EXPLICIT_INTERFACE = "explicit_interface" +class DeclarationCallableAction(str, Enum): + """Completed bridge mechanism for one declaration-expression function.""" + + MODULE_IMPORT = "module_import" + STANDALONE_PROCEDURE = "standalone_procedure" + + def overload_builtin_scalar_family(semantic_type_name: str) -> str: """Return the Python scalar family admitted by reflected dispatch.""" - if semantic_type_name == "Bool": + if is_boolean_semantic_type_name(semantic_type_name): return "bool" if semantic_type_name.startswith("Int"): return "int" @@ -890,6 +930,66 @@ class ArrayHandoffPolicy: itemsize: int | None = None category: str | None = None extent_references: tuple[tuple[str, ...], ...] = () + extent_reference_roles: tuple[tuple[str, ...], ...] = () + extent_callable_references: tuple[tuple[str, ...], ...] = () + extent_callable_roles: tuple[tuple[str, ...], ...] = () + extent_evaluation: tuple[str, ...] = () + extent_blockers: tuple[tuple[str, ...], ...] = () + display_shape: tuple[str, ...] = () + + +@dataclass(frozen=True) +class ProcedurePrototypeArgumentPolicy: + """Exact native dummy characteristics shared by every prototype use.""" + + owner_path: str + name: str + semantic_type_name: str + rank: int + passed_by_value: bool + intent: str | None + character_length: int | None + array: ArrayHandoffPolicy | None + derived_type_identity: tuple[str, str] | None + + +@dataclass(frozen=True) +class ProcedurePrototypeResultPolicy: + """Exact native function-result characteristics for one prototype.""" + + owner_path: str + semantic_type_name: str + rank: int + character_length: int | None + array: ArrayHandoffPolicy | None + derived_type_identity: tuple[str, str] | None + + +@dataclass(frozen=True) +class ProcedurePrototypePolicy: + """One reusable exact signature, independent of its eventual entity role.""" + + owner_path: str + name: str + identity: str + pure: bool + arguments: tuple[ProcedurePrototypeArgumentPolicy, ...] + result: ProcedurePrototypeResultPolicy | None + + +@dataclass(frozen=True) +class DeclarationCallablePolicy: + """One native entity used while evaluating declared extents.""" + + owner_path: str + source_name: str + native_name: str + native_scope: str | None + symbolic_role: str + expression_token: str + action: DeclarationCallableAction + prototype: ProcedurePrototypePolicy | None = None + blockers: tuple[str, ...] = () @dataclass(frozen=True) @@ -1001,6 +1101,7 @@ class CallbackTransferPolicy: object_kind: ObjectKind rank: int passed_by_value: bool + intent: str | None abi: CallbackABIKind adapter_action: CallbackTransferAction python_action: PythonBarrierAction @@ -1022,9 +1123,7 @@ class CallbackHandoffPolicy: """Complete immediate-callback contract consumed by wrapper planning.""" owner_path: str - prototype_name: str - prototype_module: str | None - declaration_mode: ExternalDeclarationMode + prototype: ProcedurePrototypePolicy arguments: tuple[CallbackTransferPolicy, ...] result: CallbackResultPolicy lifecycle: tuple[CallbackLifecycleAction, ...] @@ -1047,6 +1146,12 @@ class ArgumentPolicy: native_position: int semantic_type_name: str rank: int + scalar_logical_abi: ScalarLogicalABI + scalar_native_type: str | None + array_logical_abi: ArrayLogicalABI + array_native_type: str | None + array_copy_in: bool + array_copy_out: bool array_writeback_abi: ArrayWritebackABI optional: bool optional_mode: OptionalMode @@ -1144,6 +1249,12 @@ class NativeCallSlotPolicy: bridge_data_action: BridgeDataAction bridge_copy_reason: str | None object_kind: ObjectKind | None + scalar_logical_abi: ScalarLogicalABI = ScalarLogicalABI.NOT_APPLICABLE + scalar_native_type: str | None = None + array_logical_abi: ArrayLogicalABI = ArrayLogicalABI.NOT_APPLICABLE + array_native_type: str | None = None + array_copy_in: bool = False + array_copy_out: bool = False literal_type: str | None = None literal_value: Any = None result_position: int | None = None @@ -1158,14 +1269,20 @@ class NativeCallSlotPolicy: @dataclass(frozen=True) class FunctionWrapperPolicy: - """Completed wrapper policy for one semantic function.""" + """Completed wrapper-facing contract for one semantic function. + + ``build_function_wrapper_policy`` constructs this record after ownership + policy completion. Wrapper planning consumes the ordered arguments, + results, native-call slots, and lifecycle actions; a policy with + ``supported=False`` must be rejected using its ``blockers``. + """ owner_path: str python_exports: tuple[PythonExportPolicy, ...] native_name: str native_invocation: NativeInvocationKind native_operator: str | None - external: bool + standalone: bool external_declaration: ExternalDeclarationMode native_module: str | None native_is_subroutine: bool @@ -1177,6 +1294,7 @@ class FunctionWrapperPolicy: arguments: tuple[ArgumentPolicy, ...] = () results: tuple[ResultPolicy, ...] = () native_call_slots: tuple[NativeCallSlotPolicy, ...] = () + declaration_callables: tuple[DeclarationCallablePolicy, ...] = () blockers: tuple[str, ...] = () writeback_actions: tuple[LifecyclePolicy, ...] = () cleanup_actions: tuple[LifecyclePolicy, ...] = () @@ -1216,9 +1334,10 @@ def build_derived_field_policy( owner_path=field_path, origin=DerivedObjectOrigin.BORROWED_FIELD, ) + array = _array_handoff_policy(field.semantic_type) blockers = ( *_runtime_semantic_validation_blockers(field.semantic_type, f"field {field.name!r}"), - *_derived_field_blockers(field, getter, setter, handle), + *_derived_field_blockers(field, getter, setter, handle, array), ) return DerivedFieldPolicy( owner_path=field_path, @@ -1236,7 +1355,7 @@ def build_derived_field_policy( native_assignment=setter.assignment_mode, owner_retention=_derived_field_owner_retention(getter.kind, handle), character_length=_character_length(field.semantic_type), - array=_array_handoff_policy(field.semantic_type), + array=array, native_array_handle=handle, derived=derived, supported=not blockers, @@ -1684,6 +1803,7 @@ def _derived_field_blockers( getter: OwnershipDecision, setter: OwnershipDecision, handle: NativeArrayHandleWrapperPolicy | None, + array: ArrayHandoffPolicy | None, ) -> list[str]: """Return exact unsupported public-field forms before lowering.""" return [ @@ -1691,6 +1811,7 @@ def _derived_field_blockers( *_derived_field_descriptor_blockers(field, handle), *_derived_field_object_kind_blockers(field, getter), *_derived_field_setter_blockers(field, setter), + *_persistent_array_extent_blockers(f"field {field.name!r}", array), ] @@ -1768,7 +1889,12 @@ def _derived_field_setter_blockers( def completed_module_variable_policy( variable: models.SemanticVariable, ) -> ModuleVariablePolicy: - """Return the completed module-variable policy or fail closed.""" + """Return a lowering-ready module-variable policy or fail before planning. + + Use this after post-IR policy completion. Missing or unsupported records + raise ``ValueError`` so getter/setter lowering cannot infer an alternate + storage or replacement policy. + """ policy = variable.metadata.get(models.RESOLVED_MODULE_VARIABLE_POLICY_METADATA) if not isinstance(policy, ModuleVariablePolicy): raise ValueError(f"Semantic variable {variable.name!r} has no completed module-variable policy") @@ -1784,8 +1910,15 @@ def build_module_variable_policy( module_name: str, derived_types: dict[tuple[str, str], DerivedTypePolicy] | None = None, ) -> ModuleVariablePolicy: - """Build one module-variable policy from completed decisions.""" + """Build one module-variable access policy from completed semantic decisions. + + The function selects an already-defined family for descriptor handles, + derived objects, ordinary arrays, or scalar values, then validates runtime + and initialization requirements. It returns a new record and does not + mutate ``variable``. + """ owner_path = f"{module_name}.{variable.name}" + # Gather semantic decisions shared by all module-variable policy families. getter = _ownership_decision(variable, models.RESOLVED_GETTER_OWNERSHIP_POLICY_METADATA) setter = _ownership_decision(variable, models.RESOLVED_SETTER_OWNERSHIP_POLICY_METADATA) descriptor_kind = _scalar_module_descriptor_kind(variable) @@ -1796,7 +1929,17 @@ def build_module_variable_policy( owner_path, ) array = _array_handoff_policy(variable.semantic_type) - if native_array_handle is not None: + # Select the one completed access family without backend-specific inference. + if _is_parameter_array(variable): + policy = _constant_array_module_variable_policy( + variable, + module_name, + owner_path, + getter, + setter, + array, + ) + elif native_array_handle is not None: policy = _native_array_module_variable_policy( variable, module_name, @@ -1834,6 +1977,7 @@ def build_module_variable_policy( descriptor_kind, constant, ) + # Add runtime and import-time initialization blockers to the selected family. return _complete_module_variable_policy(variable, policy) @@ -1852,7 +1996,7 @@ def _complete_module_variable_policy( supported=False, blockers=(*policy.blockers, *validation_blockers), ) - if variable.default_value is None or _is_scalar_module_constant(variable): + if variable.default_value is None or _is_scalar_module_constant(variable) or _is_parameter_array(variable): return policy if policy.initializer is not None: return policy @@ -1972,6 +2116,67 @@ def _ordinary_array_module_variable_policy( ) +def _constant_array_module_variable_policy( + variable: models.SemanticVariable, + module_name: str, + owner_path: str, + getter: OwnershipDecision | None, + setter: OwnershipDecision | None, + array: ArrayHandoffPolicy | None, +) -> ModuleVariablePolicy: + """Build one immutable Python-owned snapshot policy for a parameter array. + + Fortran ``parameter`` arrays have no addressable module storage. The + selected bridge therefore copies the compiler-evaluated values into a + module-owned NumPy allocation once during import; it never exposes or + aliases a native address. + """ + blockers = _constant_array_module_variable_blockers(variable, getter, setter, array) + return ModuleVariablePolicy( + **_module_variable_policy_base(variable, module_name, owner_path), + getter_action=ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE, + getter=getter, + setter_action=SetterAction.OMIT, + native_assignment=AssignmentMode.NONE, + setter=setter, + descriptor_kind=None, + initializer=None, + constant_value=None, + supported=not blockers, + blockers=tuple(blockers), + array=array, + ) + + +def _constant_array_module_variable_blockers( + variable: models.SemanticVariable, + getter: OwnershipDecision | None, + setter: OwnershipDecision | None, + array: ArrayHandoffPolicy | None, +) -> tuple[str, ...]: + """Validate the post-IR immutable-copy contract for one parameter array.""" + blockers = [] + if variable.visibility != "public": + blockers.append("module parameter array is not public") + if array is None or array.rank is None or array.rank <= 0 or len(array.shape) != array.rank: + blockers.append("module parameter array requires one concrete fixed rank") + if variable.semantic_type.name not in _PLAN_PRIMITIVE_SCALAR_TYPES: + blockers.append("module parameter array requires a primitive numeric element type") + expected_getter = ( + getter is not None + and getter.kind is ObjectKind.NUMPY_ARRAY + and getter.owner is OwnershipOwner.PYTHON + and getter.transfer is TransferMode.BY_VALUE + and getter.destruction is DestructionPolicy.PYTHON_REFCOUNT + and getter.storage_mode is StorageMode.HEAP + ) + if not expected_getter: + blockers.append("module parameter array is not a completed Python-owned immutable snapshot") + if setter is None or setter.setter_action is not SetterAction.OMIT: + blockers.append("module parameter array must omit native replacement assignment") + return tuple(blockers) + + def _scalar_module_variable_policy( variable: models.SemanticVariable, module_name: str, @@ -2040,11 +2245,36 @@ def _ordinary_array_module_variable_blockers( ) if setter is None or setter.setter_action is not SetterAction.REJECT_REPLACEMENT: blockers.append("ordinary module array must reject whole-array replacement") + blockers.extend(_persistent_array_extent_blockers(f"module variable {variable.name!r}", array)) return tuple(blockers) +def _persistent_array_extent_blockers( + owner: str, + array: ArrayHandoffPolicy | None, +) -> tuple[str, ...]: + """Reject persistent extents that still require unavailable runtime values. + + Module arrays and fields have no call-local scalar or input-array roles. + This consumes their role-free extent references and returns one diagnostic + per dependent axis; it does not mutate the array policy. + """ + if array is None: + return () + return tuple( + f"{owner} extent axis {axis} depends on unavailable declaration values {references}" + for axis, references in enumerate(array.extent_references) + if references + ) + + def completed_function_wrapper_policy(function: models.SemanticFunction) -> FunctionWrapperPolicy: - """Return a completed function wrapper policy or fail before planning.""" + """Return a lowering-ready function policy stored by post-IR completion. + + Use this at the planning boundary. Missing policy or an unsupported policy + raises ``ValueError`` with the completed-policy diagnostic, preventing + lower stages from substituting a fallback behavior. + """ policy = function.metadata.get(models.RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA) if not isinstance(policy, FunctionWrapperPolicy): @@ -2063,7 +2293,13 @@ def build_callback_handoff_policy( *, owner_path: str, ) -> CallbackHandoffPolicy: - """Complete one immediate callback without consulting lowering details.""" + """Complete one immediate callback's ABI, transfers, lifecycle, and blockers. + + The semantic type must carry a resolved callback prototype and nested + ownership decisions. The returned record is consumed by wrapper policy + and planning; unsupported signatures remain represented with blockers. + """ + # Validate the call-scoped callback envelope before inspecting its signature. raw_arguments = semantic_type.metadata.get("callback_arguments") return_type = semantic_type.metadata.get("return") blockers = list(_callback_envelope_blockers(semantic_type)) @@ -2073,6 +2309,7 @@ def build_callback_handoff_policy( blockers.append("callback signature is missing ordered argument records") arguments: tuple[CallbackTransferPolicy, ...] = () else: + # Complete each callback transfer and collect signature-level failures. arguments = tuple( _callback_transfer_policy(argument, owner_path=f"{owner_path}.callback_arg_{index}") for index, argument in enumerate(raw_arguments) @@ -2084,22 +2321,31 @@ def build_callback_handoff_policy( ) result = _callback_result_policy(return_type, owner_path=f"{owner_path}.callback_result") blockers.extend(_callback_result_blockers(return_type, result)) + # Complete the shared exact signature after argument and result ABI facts exist. prototype_ref = semantic_type.metadata.get(models.PROTOTYPE_REF_METADATA) - prototype_name = prototype_ref.get("name") if isinstance(prototype_ref, dict) else None - prototype_module = prototype_ref.get("origin_module") if isinstance(prototype_ref, dict) else None - if not isinstance(prototype_name, str) or not prototype_name: + source_name = prototype_ref.get("name") if isinstance(prototype_ref, dict) else None + local_name = prototype_ref.get("local_name") if isinstance(prototype_ref, dict) else None + origin_module = prototype_ref.get("origin_module") if isinstance(prototype_ref, dict) else None + if not isinstance(source_name, str) or not source_name: blockers.append("callback argument requires a resolved named prototype") - prototype_name = semantic_type.name - declaration_mode = _callback_declaration_mode(raw_arguments, return_type, semantic_type) - if declaration_mode is ExternalDeclarationMode.EXPLICIT_INTERFACE and not ( - isinstance(prototype_module, str) and prototype_module - ): - blockers.append(f"prototype {prototype_name!r} requires an importable native module") + source_name = semantic_type.name + if not isinstance(local_name, str) or not local_name: + local_name = semantic_type.name + prototype = _procedure_prototype_policy( + owner_path=owner_path, + name=local_name, + identity=f"{origin_module or owner_path}.{source_name}", + pure=_prototype_metadata_is_pure(semantic_type.metadata.get("prototype_metadata")), + arguments=tuple(raw_arguments) if isinstance(raw_arguments, list) else (), + result=return_type if isinstance(return_type, models.SemanticType) else None, + ) + if prototype.pure: + blockers.append( + "pure @prototype cannot be used as a Python callback because its adapter calls the Python runtime" + ) return CallbackHandoffPolicy( owner_path=owner_path, - prototype_name=prototype_name, - prototype_module=(prototype_module if isinstance(prototype_module, str) and prototype_module else None), - declaration_mode=declaration_mode, + prototype=prototype, arguments=arguments, result=result, lifecycle=( @@ -2118,58 +2364,34 @@ def build_callback_handoff_policy( ) -def _callback_declaration_mode( - arguments: object, - return_type: object, - semantic_type: models.SemanticType, -) -> ExternalDeclarationMode: - """Select the weakest correct adapter declaration from prototype facts.""" - if isinstance(arguments, list) and any( - isinstance(argument, models.SemanticArgument) and _prototype_argument_requires_explicit_interface(argument) - for argument in arguments - ): - return ExternalDeclarationMode.EXPLICIT_INTERFACE - if isinstance(return_type, models.SemanticType) and _prototype_result_requires_explicit_interface(return_type): - return ExternalDeclarationMode.EXPLICIT_INTERFACE - prototype_metadata = semantic_type.metadata.get("prototype_metadata") - attributes = prototype_metadata.get("fortran_attributes", ()) if isinstance(prototype_metadata, dict) else () - normalized = {str(attribute).casefold().replace(" ", "") for attribute in attributes} - if normalized & {"bind(c)", "elemental", "pure"}: - return ExternalDeclarationMode.EXPLICIT_INTERFACE - return ExternalDeclarationMode.IMPLICIT_EXTERNAL - - -def _prototype_argument_requires_explicit_interface(argument: models.SemanticArgument) -> bool: - semantic_type = argument.semantic_type - storage = semantic_type.storage - array = storage.array if storage is not None else None - if argument.optional: - return True - if any( - semantic_type.metadata.get(name) - for name in ( - "fortran_allocatable", - "fortran_pointer", - "fortran_polymorphic", - "fortran_assumed_type", - "fortran_target", - ) - ): - return True - return array is not None and array.category in {"assumed_shape", "deferred_shape", "assumed_rank"} +def _procedure_prototype_policy( + *, + owner_path: str, + name: str, + identity: str, + pure: bool, + arguments: tuple[models.SemanticArgument, ...], + result: models.SemanticType | None, +) -> ProcedurePrototypePolicy: + """Project one semantic signature for callback and direct-procedure uses.""" + return ProcedurePrototypePolicy( + owner_path=f"{owner_path}.prototype", + name=name, + identity=identity, + pure=pure, + arguments=tuple(_semantic_prototype_argument_policy(argument, owner_path=owner_path) for argument in arguments), + result=( + _semantic_prototype_result_policy(result, owner_path=owner_path) + if result is not None and result.name != "None" + else None + ), + ) -def _prototype_result_requires_explicit_interface(return_type: models.SemanticType) -> bool: - if return_type.name == "None": - return False - if return_type.rank > 0: - return True - if return_type.metadata.get("fortran_allocatable") or return_type.metadata.get("fortran_pointer"): - return True - if return_type.name != "String": - return False - length = return_type.metadata.get("fortran_character_length") - return length is None or str(length).strip() in {"", ":", "*"} +def _prototype_metadata_is_pure(metadata: object) -> bool: + """Return the one purity fact retained by a semantic prototype reference.""" + attributes = metadata.get("fortran_attributes", ()) if isinstance(metadata, dict) else () + return any(str(attribute).casefold() == "pure" for attribute in attributes) def _callback_envelope_blockers(semantic_type: models.SemanticType) -> tuple[str, ...]: @@ -2208,6 +2430,11 @@ def _callback_transfer_policy( object_kind=decision.kind, rank=int(semantic_type.rank or 0), passed_by_value=passed_by_value, + intent=( + str(intent) + if (intent := argument.origin.metadata.get(models.PROTOTYPE_INTENT_METADATA)) is not None + else None + ), abi=_callback_abi_kind(argument, derived=derived), adapter_action=_callback_adapter_action(argument), python_action=decision.python_barrier_action, @@ -2238,10 +2465,17 @@ def _callback_abi_kind( def _callback_adapter_action( argument: models.SemanticArgument, ) -> CallbackTransferAction: - """Select isolated primitive values or permissive non-scalar writeback.""" + """Select callback copy direction from the prototype's exact dummy intent.""" semantic_type = argument.semantic_type - if bool(argument.origin.metadata.get("value")) or ( - semantic_type.name in _PLAN_PRIMITIVE_SCALAR_TYPES and int(semantic_type.rank or 0) == 0 + 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) ): return CallbackTransferAction.COPY_IN return CallbackTransferAction.COPY_IN_OUT @@ -2295,6 +2529,7 @@ def _callback_result_policy( object_kind=decision.kind, rank=int(return_type.rank or 0), passed_by_value=False, + intent=None, abi=( CallbackABIKind.DERIVED_ADDRESS if derived @@ -2365,9 +2600,17 @@ def build_function_wrapper_policy( polymorphic_variants: dict[tuple[str, str], tuple[tuple[str, str], ...]] | None = None, native_dispatch_name: str | None = None, ) -> FunctionWrapperPolicy: - """Build typed function policy from completed post-IR decisions.""" + """Build a complete wrapper-facing function policy from post-IR decisions. + + Use this only after ownership, callback, status, and export policy are + complete. It preserves signature and projection order while producing + arguments, results, native call slots, lifecycle actions, and all support + blockers. The input function is read without mutation; callers normally + store the returned record in its resolved-policy metadata. + """ completed_derived_types = derived_types or {} + # Establish native ABI order before projecting Python-visible arguments. argument_native_positions, native_call_slots, slot_blockers = _native_call_slot_policies( function, owner_path, @@ -2382,11 +2625,24 @@ def build_function_wrapper_policy( polymorphic_variants or {}, class_call, ) + # Complete result representation and declaration call targets, then bind + # every array-extent producer to its immutable role. results, result_blockers = _result_policies(function, owner_path, completed_derived_types) + declaration_callables = _function_declaration_callable_policies(function, owner_path) + arguments, results, native_call_slots = _complete_function_array_extent_policies( + function, + owner_path, + arguments, + results, + native_call_slots, + declaration_callables, + ) + # Record ordered writeback, cleanup, and ownership-transfer lifecycle work. writeback_actions, lifecycle_blockers = _lifecycle_policies(arguments) cleanup_actions, release_actions = _derived_result_lifecycle_policies(results) status_error = _completed_native_status_error_policy(function) native_module = _native_module(function, owner_path) + # Aggregate all support validation before exposing the immutable plan input. blockers = ( _function_shape_blockers(function, class_call) + argument_blockers @@ -2394,23 +2650,24 @@ def build_function_wrapper_policy( + slot_blockers + lifecycle_blockers + _result_position_blockers(results, arguments) - + _array_extent_reference_blockers(function, arguments, results) + + _array_extent_reference_blockers(arguments, results) + + tuple(blocker for callable_policy in declaration_callables for blocker in callable_policy.blockers) + _runtime_status_plan_blockers(status_error) + _string_result_status_blockers(results, status_error) + _string_writeback_status_blockers(arguments, status_error) ) native_name = native_dispatch_name or _native_name(function) native_invocation, native_operator = _native_invocation_policy(native_name) - external = _is_external(function) + standalone = _is_standalone(function) return FunctionWrapperPolicy( owner_path=owner_path, python_exports=completed_python_exports(function, function.name), native_name=native_name, native_invocation=native_invocation, native_operator=native_operator, - external=external, + standalone=standalone, external_declaration=_external_declaration_mode( - external=external, + standalone=standalone, native_invocation=native_invocation, arguments=tuple(arguments), results=results, @@ -2428,6 +2685,7 @@ def build_function_wrapper_policy( arguments=tuple(arguments), results=results, native_call_slots=tuple(native_call_slots), + declaration_callables=declaration_callables, blockers=tuple(blockers), writeback_actions=writeback_actions, cleanup_actions=cleanup_actions, @@ -2447,14 +2705,14 @@ def _native_invocation_policy(native_name: str) -> tuple[NativeInvocationKind, s def _external_declaration_mode( *, - external: bool, + standalone: bool, native_invocation: NativeInvocationKind, arguments: tuple[ArgumentPolicy, ...], results: tuple[ResultPolicy, ...], native_call_slots: tuple[NativeCallSlotPolicy, ...], ) -> ExternalDeclarationMode: """Choose the weakest correct native declaration from completed ABI facts.""" - if not external: + if not standalone: return ExternalDeclarationMode.NONE if native_invocation is not NativeInvocationKind.PROCEDURE: return ExternalDeclarationMode.EXPLICIT_INTERFACE @@ -2568,6 +2826,11 @@ def _argument_policy( ) -> tuple[ArgumentPolicy, tuple[str, ...]]: """Complete one visible argument without mixing it with list traversal.""" argument_path = f"{owner_path}.{argument.name}" + scalar_logical_abi, scalar_native_type = _scalar_logical_argument_abi(argument) + array_logical_abi, array_native_type, array_copy_in, array_copy_out = _array_logical_argument_abi( + argument, + decision, + ) optional_mode = _optional_mode(argument, decision) callback = _callback_handoff_policy(argument) array_policy = _array_handoff_policy(argument.semantic_type) @@ -2588,6 +2851,7 @@ def _argument_policy( or _is_exported_passed_object_argument(function, native_position), ) bridge_data_action, bridge_copy_reason = _completed_argument_bridge_action( + argument, decision, optional_mode, native_slot, @@ -2617,11 +2881,18 @@ def _argument_policy( native_position=native_position, semantic_type_name=argument.semantic_type.name, rank=int(argument.semantic_type.rank or 0), + scalar_logical_abi=scalar_logical_abi, + scalar_native_type=scalar_native_type, + array_logical_abi=array_logical_abi, + array_native_type=array_native_type, + array_copy_in=array_copy_in, + array_copy_out=array_copy_out, array_writeback_abi=_array_writeback_abi( argument.semantic_type, decision, boundary.handoff_mode, array_policy, + array_logical_abi, ), optional=argument.optional, optional_mode=boundary.optional_mode, @@ -2715,6 +2986,7 @@ def _is_exported_passed_object_argument(function: models.SemanticFunction, nativ def _completed_argument_bridge_action( + argument: models.SemanticArgument, decision: OwnershipDecision, optional_mode: OptionalMode, native_slot: NativeCallSlotPolicy | None, @@ -2729,7 +3001,8 @@ def _completed_argument_bridge_action( optional_mode, native_slot.value_kind if native_slot is not None else None, ) - return _derived_argument_bridge_data_action(derived, action, reason) + action, reason = _derived_argument_bridge_data_action(derived, action, reason) + return _logical_argument_bridge_action(argument, decision, action, reason) def _argument_boundary_policy( @@ -2972,6 +3245,12 @@ def _hidden_result_policies( argument.semantic_type, descriptor_kind=mapping.value_kind, ) + bridge_data_action, bridge_copy_reason = _logical_argument_bridge_action( + argument, + decision, + bridge_data_action, + bridge_copy_reason, + ) if bridge_data_action is BridgeDataAction.BLOCKED and decision.kind is not ObjectKind.SCALAR: blockers = (*blockers, f"hidden result {argument.name!r} has no completed bridge data action") derived = _derived_handoff_policy( @@ -3031,6 +3310,12 @@ def _native_call_slot_policies( owner_path: str, derived_types: dict[tuple[str, str], DerivedTypePolicy], ) -> tuple[dict[int, int], tuple[NativeCallSlotPolicy, ...], tuple[str, ...]]: + """Complete ordered native call slots from explicit projections or declaration order. + + The returned mapping connects visible Python argument positions to native + positions; slot records preserve native ABI order and blockers diagnose + missing completed decisions without mutating ``function``. + """ if function.projection: return _projected_native_call_slot_policies(function, owner_path, derived_types) return _implicit_native_call_slot_policies(function, owner_path, derived_types) @@ -3157,6 +3442,11 @@ def _projected_argument_slot( argument_path = f"{owner_path}.{argument.name}" value_kind = _native_argument_value_kind(argument, mapping.value_kind or "arg") callback = _callback_handoff_policy(argument) + scalar_logical_abi, scalar_native_type = _scalar_logical_argument_abi(argument) + array_logical_abi, array_native_type, array_copy_in, array_copy_out = _array_logical_argument_abi( + argument, + decision, + ) derived = _argument_derived_handoff(argument, decision, callback, argument_path, derived_types) bridge_data_action, bridge_copy_reason = _completed_projected_bridge_action( argument, @@ -3185,6 +3475,12 @@ def _projected_argument_slot( bridge_data_action=bridge_data_action, bridge_copy_reason=bridge_copy_reason, object_kind=decision.kind, + scalar_logical_abi=scalar_logical_abi, + scalar_native_type=scalar_native_type, + array_logical_abi=array_logical_abi, + array_native_type=array_native_type, + array_copy_in=array_copy_in, + array_copy_out=array_copy_out, result_position=mapping.result_position, semantic_type_name=argument.semantic_type.name, character_length=_character_length(argument.semantic_type), @@ -3216,7 +3512,8 @@ def _completed_projected_bridge_action( _optional_mode(argument, decision), value_kind, ) - return _derived_argument_bridge_data_action(derived, action, reason) + action, reason = _derived_argument_bridge_data_action(derived, action, reason) + return _logical_argument_bridge_action(argument, decision, action, reason) def _native_slot_barrier_actions( @@ -3301,6 +3598,17 @@ def _hidden_result_native_call_slot_policy( argument.semantic_type, descriptor_kind=mapping.value_kind, ) + bridge_data_action, bridge_copy_reason = _logical_argument_bridge_action( + argument, + decision, + bridge_data_action, + bridge_copy_reason, + ) + scalar_logical_abi, scalar_native_type = _scalar_logical_argument_abi(argument) + array_logical_abi, array_native_type, array_copy_in, array_copy_out = _array_logical_argument_abi( + argument, + decision, + ) blockers = ( (f"native-call result slot {native_position} has no completed bridge data action",) if bridge_data_action is BridgeDataAction.BLOCKED @@ -3320,6 +3628,12 @@ def _hidden_result_native_call_slot_policy( bridge_data_action=bridge_data_action, bridge_copy_reason=bridge_copy_reason, object_kind=decision.kind, + scalar_logical_abi=scalar_logical_abi, + scalar_native_type=scalar_native_type, + array_logical_abi=array_logical_abi, + array_native_type=array_native_type, + array_copy_in=array_copy_in, + array_copy_out=array_copy_out, result_position=mapping.result_position, semantic_type_name=argument.semantic_type.name, character_length=_character_length(argument.semantic_type), @@ -3400,6 +3714,12 @@ def _implicit_native_call_slot_policies( owner_path: str, derived_types: dict[tuple[str, str], DerivedTypePolicy], ) -> tuple[dict[int, int], tuple[NativeCallSlotPolicy, ...], tuple[str, ...]]: + """Build declaration-ordered slots when no explicit native projection exists. + + Every argument consumes its completed ownership/callback/derived policy. + Missing or unsupported bridge decisions are accumulated as blockers while + the returned slots preserve source argument order. + """ slots: list[NativeCallSlotPolicy] = [] positions: dict[int, int] = {} blockers: list[str] = [] @@ -3409,6 +3729,11 @@ def _implicit_native_call_slot_policies( blockers.append(f"implicit native-call slot {position} references argument without completed policy") continue value_kind = _native_argument_value_kind(argument, "arg") + scalar_logical_abi, scalar_native_type = _scalar_logical_argument_abi(argument) + array_logical_abi, array_native_type, array_copy_in, array_copy_out = _array_logical_argument_abi( + argument, + decision, + ) callback = argument.semantic_type.metadata.get(models.RESOLVED_CALLBACK_POLICY_METADATA) callback = callback if isinstance(callback, CallbackHandoffPolicy) else None derived = ( @@ -3436,6 +3761,12 @@ def _implicit_native_call_slot_policies( bridge_data_action, bridge_copy_reason, ) + bridge_data_action, bridge_copy_reason = _logical_argument_bridge_action( + argument, + decision, + bridge_data_action, + bridge_copy_reason, + ) if bridge_data_action is BridgeDataAction.BLOCKED: blockers.append(f"implicit native-call slot {position} has no completed bridge data action") positions[position] = position @@ -3455,6 +3786,12 @@ def _implicit_native_call_slot_policies( bridge_data_action=bridge_data_action, bridge_copy_reason=bridge_copy_reason, object_kind=decision.kind, + scalar_logical_abi=scalar_logical_abi, + scalar_native_type=scalar_native_type, + array_logical_abi=array_logical_abi, + array_native_type=array_native_type, + array_copy_in=array_copy_in, + array_copy_out=array_copy_out, semantic_type_name=argument.semantic_type.name, character_length=_character_length(argument.semantic_type), array=_array_handoff_policy(argument.semantic_type), @@ -3697,6 +4034,12 @@ def _derived_payload_dummy_case( category: DerivedDummyCategory, storage: DerivedObjectStorage, ) -> DerivedCallCasePolicy: + """Return the compatibility matrix cell for a non-descriptor derived dummy. + + ``category`` distinguishes value from ordinary object dummies; ``storage`` + selects address access, required presence, and target lifetime without + changing the established matrix. + """ action = DerivedCallAction.TYPED_VALUE_COPY if category is DerivedDummyCategory.VALUE else None table = { DerivedObjectStorage.DIRECT: ( @@ -3760,6 +4103,7 @@ def _derived_payload_dummy_case( def _derived_allocatable_dummy_case(storage: DerivedObjectStorage) -> DerivedCallCasePolicy: + """Return the supported allocatable-dummy cell or an explicit incompatible result.""" if storage is DerivedObjectStorage.ALLOCATABLE_HOLDER: return DerivedCallCasePolicy( storage, @@ -3795,6 +4139,11 @@ def _derived_pointer_dummy_case( *, projects_result: bool, ) -> DerivedCallCasePolicy: + """Return the supported pointer-dummy cell, including projected-writeback rejection. + + Pointer holders and module pointers retain their distinct transaction + mechanisms. Other non-projected storage reuses payload input adaptation. + """ if storage is DerivedObjectStorage.POINTER_HOLDER: access = DerivedActualAccess.POINTER_HOLDER return DerivedCallCasePolicy( @@ -3837,6 +4186,7 @@ def _derived_incompatible_case( kind: str, message: str, ) -> DerivedCallCasePolicy: + """Create one explicit unsupported derived-actual matrix cell with its diagnostic.""" return DerivedCallCasePolicy( storage, DerivedCallAction.INCOMPATIBLE, @@ -4569,7 +4919,7 @@ def _direct_result_abi( """Complete the direct scalar return ABI before wrapper planning.""" if scalar_descriptor is not None or decision.kind is not ObjectKind.SCALAR or int(semantic_type.rank or 0) != 0: return DirectResultABI.NOT_APPLICABLE - if semantic_type.name == "Bool": + if is_boolean_semantic_type_name(semantic_type.name): return DirectResultABI.LOGICAL_LOW_BIT_INT8 if semantic_type.name in _PLAN_PRIMITIVE_SCALAR_TYPES: return DirectResultABI.NATIVE_SCALAR @@ -4950,11 +5300,13 @@ def _function_shape_blockers( def _completed_native_status_error_policy( function: models.SemanticFunction, ) -> NativeStatusErrorPolicy | None: + """Return the status-error record completed upstream, without reconstructing it.""" policy = function.metadata.get(models.RESOLVED_RUNTIME_STATUS_ERROR_POLICY_METADATA) return policy if isinstance(policy, NativeStatusErrorPolicy) else None def _runtime_status_output_owner_paths(function: models.SemanticFunction) -> frozenset[str]: + """Return stable owner paths for the status and optional message native outputs.""" policy = _completed_native_status_error_policy(function) if policy is None: return frozenset() @@ -4977,6 +5329,7 @@ def _runtime_status_plan_blockers(policy: NativeStatusErrorPolicy | None) -> tup def _character_length(semantic_type: models.SemanticType) -> int | None: + """Return a positive fixed Fortran character length, normalizing accepted metadata spellings.""" value = semantic_type.metadata.get("fortran_character_length") if isinstance(value, int) and not isinstance(value, bool) and value > 0: return value @@ -5044,6 +5397,7 @@ def action(result: ResultPolicy, operation: LifecycleOperation) -> LifecyclePoli def _native_position_blockers(native_positions: object) -> tuple[str, ...]: + """Reject slot positions that do not cover the contiguous native ABI order exactly once.""" positions = tuple(native_positions) if sorted(positions) != list(range(len(positions))): return ("native-call slots must cover each native position exactly once in order",) @@ -5051,11 +5405,13 @@ def _native_position_blockers(native_positions: object) -> tuple[str, ...]: def _ownership_decision(owner: object, metadata_key: str) -> OwnershipDecision | None: + """Read one typed completed ownership record from metadata, returning ``None`` when absent.""" decision = getattr(owner, "metadata", {}).get(metadata_key) return decision if isinstance(decision, OwnershipDecision) else None def _is_first_lane_scalar_type(semantic_type: models.SemanticType) -> bool: + """Report whether a rank-zero primitive uses the supported ordinary scalar lane.""" scalar_name = semantic_type.dtype or semantic_type.name return bool( int(semantic_type.rank or 0) == 0 @@ -5066,6 +5422,7 @@ def _is_first_lane_scalar_type(semantic_type: models.SemanticType) -> bool: def _is_scalar_storage_type(semantic_type: models.SemanticType) -> bool: + """Report whether a type carries rank-zero array-backed scalar storage metadata.""" storage = semantic_type.storage array = storage.array if storage is not None else None return bool(array is not None and array.category == SCALAR_STORAGE_CATEGORY) @@ -5084,7 +5441,13 @@ def _is_fixed_plan_string_result_type(semantic_type: models.SemanticType) -> boo def _is_first_lane_literal_type(literal_type: str) -> bool: """Return whether a hidden literal type belongs to the scalar input lane.""" - return literal_type in {"Bool", "Int32", "Float32", "Float64", "Complex64", "Complex128"} + return is_boolean_semantic_type_name(literal_type) or literal_type in { + "Int32", + "Float32", + "Float64", + "Complex64", + "Complex128", + } # Native-array-handle policy projection. @@ -5820,6 +6183,7 @@ def _scalar_module_getter_action( getter: OwnershipDecision | None, constant: bool, ) -> ModuleGetterAction: + """Select scalar module getter behavior from constant and completed getter policy.""" if constant: if _source_parameter_needs_native_getter(variable): return ModuleGetterAction.NATIVE_CONSTANT_VALUE @@ -5851,6 +6215,7 @@ def _scalar_module_native_assignment( def _scalar_module_descriptor_kind(variable: models.SemanticVariable) -> str | None: + """Return the scalar descriptor family recorded on a module variable, if any.""" metadata = variable.semantic_type.metadata if metadata.get("fortran_allocatable"): return "allocatable" @@ -5860,10 +6225,22 @@ def _scalar_module_descriptor_kind(variable: models.SemanticVariable) -> str | N def _is_scalar_module_constant(variable: models.SemanticVariable) -> bool: + """Report whether a module variable is constrained as a semantic constant.""" return any(constraint.name == "Constant" for constraint in variable.semantic_type.constraints) +def _is_parameter_array(variable: models.SemanticVariable) -> bool: + """Report whether a fixed Fortran parameter needs immutable-array lowering.""" + return bool( + variable.origin.source_language == "fortran" + and variable.origin.source_kind == "variable" + and int(variable.semantic_type.rank or 0) > 0 + and _is_scalar_module_constant(variable) + ) + + def _is_scalar_module_literal(value: object, semantic_type_name: str) -> bool: + """Report whether a module initializer parses as a supported scalar literal.""" try: _scalar_module_literal_value(value, semantic_type_name) except (TypeError, ValueError, SyntaxError): @@ -5876,7 +6253,7 @@ def _scalar_module_literal_value(value: object, semantic_type_name: str) -> obje if not isinstance(value, str): return value text = value.strip() - if semantic_type_name == "Bool": + if is_boolean_semantic_type_name(semantic_type_name): lowered = text.casefold() if lowered in {".true.", "true"}: return True @@ -5948,6 +6325,7 @@ def _array_argument_bridge_data_action( def _scalar_storage_array_bridge_uses_view(decision: OwnershipDecision, optional_mode: OptionalMode) -> bool: + """Report whether rank-zero scalar storage uses the ordinary array-view bridge path.""" return ( optional_mode in _ARRAY_VALUE_OPTIONAL_MODES and decision.python_barrier_action is PythonBarrierAction.SCALAR_STORAGE @@ -5957,6 +6335,7 @@ def _scalar_storage_array_bridge_uses_view(decision: OwnershipDecision, optional def _copy_in_out_array_bridge_uses_view(decision: OwnershipDecision, optional_mode: OptionalMode) -> bool: + """Report whether a projected copy-in/out array retains its supported view handoff.""" return ( optional_mode is OptionalMode.REQUIRED and decision.python_barrier_action is PythonBarrierAction.ARRAY_STORAGE @@ -5970,6 +6349,7 @@ def _native_descriptor_array_bridge_data_action( decision: OwnershipDecision, optional_mode: OptionalMode, ) -> BridgeDataAction | None: + """Return descriptor-handle bridge movement for a completed descriptor action, else ``None``.""" if optional_mode not in _ARRAY_DESCRIPTOR_OPTIONAL_MODES: return None if decision.python_barrier_action is not PythonBarrierAction.WRAPPER_INSTANCE: @@ -5984,6 +6364,7 @@ def _native_descriptor_array_bridge_data_action( def _raw_array_address_bridge_uses_view(decision: OwnershipDecision, optional_mode: OptionalMode) -> bool: + """Report whether a required raw array address can use the existing view bridge path.""" return ( optional_mode is OptionalMode.REQUIRED and decision.python_barrier_action is PythonBarrierAction.RAW_ADDRESS @@ -5993,6 +6374,7 @@ def _raw_array_address_bridge_uses_view(decision: OwnershipDecision, optional_mo def _array_storage_bridge_uses_view(decision: OwnershipDecision, optional_mode: OptionalMode) -> bool: + """Report whether ordinary array storage uses the established contiguous view handoff.""" return ( optional_mode in _ARRAY_VALUE_OPTIONAL_MODES and decision.python_barrier_action is PythonBarrierAction.ARRAY_STORAGE @@ -6033,7 +6415,101 @@ def _string_argument_bridge_data_action( return BridgeDataAction.BLOCKED, None -# Scalar bridge data policy. +# Logical bridge data policy. +def _fortran_logical_native_type(argument: models.SemanticArgument) -> str | None: + """Return one exact Fortran logical spelling retained by semantic IR. + + Native-source conversion stores the spelling on the argument origin; + semantic ``.pyi`` builds attach their probe-resolved spelling to the type + origin. The helper consumes either representation without changing it and + returns ``None`` for non-Fortran or non-logical declarations. + """ + semantic_type = argument.semantic_type + origins = (argument.origin, semantic_type.origin) + source_type = next( + ( + str(origin.source_type).strip() + for origin in origins + if origin.source_language == "fortran" and origin.source_type + ), + "", + ) + if not source_type.casefold().startswith("logical"): + return None + declared_bits = next( + ( + origin.metadata.get("declared_storage_bits") + for origin in origins + if isinstance(origin.metadata.get("declared_storage_bits"), int) + ), + None, + ) + if source_type.casefold() == "logical" and isinstance(declared_bits, int) and declared_bits > 0: + return f"logical(kind={declared_bits // 8})" + return source_type + + +def _scalar_logical_argument_abi( + argument: models.SemanticArgument, +) -> tuple[ScalarLogicalABI, str | None]: + """Complete exact native-kind storage for one Fortran logical scalar.""" + semantic_type = argument.semantic_type + if not is_boolean_semantic_type_name(semantic_type.name) or int(semantic_type.rank or 0) != 0: + return ScalarLogicalABI.NOT_APPLICABLE, None + source_type = _fortran_logical_native_type(argument) + if source_type is None: + if semantic_type.name in {"Bool", "Bool8"}: + return ScalarLogicalABI.C_BOOL, "logical(c_bool)" + return ScalarLogicalABI.NATIVE_KIND_COPY, None + compact = "".join(source_type.casefold().split()) + if compact == "logical(kind=c_bool)": + return ScalarLogicalABI.C_BOOL, "logical(c_bool)" + return ScalarLogicalABI.NATIVE_KIND_COPY, source_type + + +def _array_logical_argument_abi( + argument: models.SemanticArgument, + decision: OwnershipDecision, +) -> tuple[ArrayLogicalABI, str | None, bool, bool]: + """Complete native storage and directional copies for a Boolean array. + + The helper consumes semantic type/origin facts and completed ownership. It + returns the ABI selector, exact native spelling, and independent copy-in + and copy-out flags. Exact ``c_bool`` arrays borrow the NumPy buffer; other + Fortran logical kinds require a bridge-local representation. + """ + semantic_type = argument.semantic_type + if not is_boolean_semantic_type_name(semantic_type.name) or int(semantic_type.rank or 0) <= 0: + return ArrayLogicalABI.NOT_APPLICABLE, None, False, False + source_type = _fortran_logical_native_type(argument) + if source_type is None: + if semantic_type.name in {"Bool", "Bool8"}: + return ArrayLogicalABI.C_BOOL_VIEW, "logical(c_bool)", False, False + copy_in = bool(getattr(argument, "_source_reads_argument", True)) + return ArrayLogicalABI.NATIVE_KIND_COPY, None, copy_in, decision.mutates_native + if "".join(source_type.casefold().split()) == "logical(kind=c_bool)": + return ArrayLogicalABI.C_BOOL_VIEW, "logical(c_bool)", False, False + copy_in = bool(getattr(argument, "_source_reads_argument", True)) + copy_out = decision.mutates_native + return ArrayLogicalABI.NATIVE_KIND_COPY, source_type, copy_in, copy_out + + +def _logical_argument_bridge_action( + argument: models.SemanticArgument, + decision: OwnershipDecision, + action: BridgeDataAction, + reason: str | None, +) -> tuple[BridgeDataAction, str | None]: + """Select explicit representation copying for a non-C logical argument.""" + abi, _native_type = _scalar_logical_argument_abi(argument) + if abi is ScalarLogicalABI.NATIVE_KIND_COPY: + return BridgeDataAction.COPY_REPRESENTATION, LOGICAL_SCALAR_KIND_COPY_REASON + array_abi, _native_type, _copy_in, _copy_out = _array_logical_argument_abi(argument, decision) + if array_abi is ArrayLogicalABI.NATIVE_KIND_COPY: + return BridgeDataAction.COPY_REPRESENTATION, LOGICAL_ARRAY_KIND_COPY_REASON + return action, reason + + def _scalar_argument_bridge_data_action( decision: OwnershipDecision, optional_mode: OptionalMode, @@ -6178,12 +6654,21 @@ def _array_writeback_abi( decision: OwnershipDecision, handoff_mode: ArgumentHandoffMode, array: ArrayHandoffPolicy | None, + logical_abi: ArrayLogicalABI, ) -> ArrayWritebackABI: - """Complete mutable ordinary-array byte normalization before planning.""" + """Complete mutable ordinary-array byte normalization before planning. + + Exact-kind logical copies canonicalize bytes while copying out, so only a + direct ``c_bool`` view needs the separate low-bit normalization pass. + """ if array is None or handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER or not decision.mutates_native: return ArrayWritebackABI.NOT_APPLICABLE - if semantic_type.name == "Bool": - return ArrayWritebackABI.LOGICAL_LOW_BIT_INT8 + if is_boolean_semantic_type_name(semantic_type.name): + return ( + ArrayWritebackABI.LOGICAL_LOW_BIT_INT8 + if logical_abi is ArrayLogicalABI.C_BOOL_VIEW + else ArrayWritebackABI.NOT_APPLICABLE + ) return ArrayWritebackABI.NATIVE_ARRAY @@ -6214,7 +6699,7 @@ def _array_handoff_policy(semantic_type: models.SemanticType) -> ArrayHandoffPol flat_axis=_array_handoff_flat_axis(array), itemsize=_array_handoff_itemsize(semantic_type), category=array.category, - extent_references=tuple(_array_extent_references(item) for item in shape), + extent_references=tuple(declaration_extent_references(item) for item in shape), ) @@ -6304,6 +6789,7 @@ def _is_phase6_ordinary_array_type(semantic_type: models.SemanticType) -> bool: def _is_scalar_storage_array_policy(array_policy: ArrayHandoffPolicy | None) -> bool: + """Report whether a handoff policy represents rank-zero scalar array storage.""" return bool( array_policy is not None and array_policy.rank == 0 and array_policy.category == SCALAR_STORAGE_CATEGORY ) @@ -6351,67 +6837,336 @@ def _raw_array_handoff_policy(semantic_type: models.SemanticType) -> ArrayHandof contiguous=True, itemsize=_character_length(semantic_type) if semantic_type.name == "String" else None, category="raw_address", - extent_references=tuple(_array_extent_references(item) for item in shape), + extent_references=tuple(declaration_extent_references(item) for item in shape), ) -def _array_extent_references(expression: str) -> tuple[str, ...]: - """Return stable scalar names used by one declared extent expression.""" - if expression in {":", "::Strided", "...", "Flat"}: - return () - try: - tree = ast.parse(expression, mode="eval") - except SyntaxError: - return ("",) - if not _valid_array_extent_expression(tree): - return ("",) - return tuple(dict.fromkeys(node.id for node in ast.walk(tree) if isinstance(node, ast.Name))) - - -def _valid_array_extent_expression(tree: ast.AST) -> bool: - """Return whether an extent uses only integer arithmetic and scalar names.""" - allowed = ( - ast.Expression, - ast.BinOp, - ast.UnaryOp, - ast.Name, - ast.Load, - ast.Constant, - ast.Add, - ast.Sub, - ast.Mult, - ast.FloorDiv, - ast.Div, - ast.Mod, - ast.USub, - ast.UAdd, - ) - return all(isinstance(node, allowed) and _is_integer_constant(node) for node in ast.walk(tree)) - - -def _is_integer_constant(node: ast.AST) -> bool: - return not isinstance(node, ast.Constant) or (isinstance(node.value, int) and not isinstance(node.value, bool)) +def _complete_function_array_extent_policies( + function: models.SemanticFunction, + owner_path: str, + arguments: list[ArgumentPolicy], + results: tuple[ResultPolicy, ...], + native_call_slots: tuple[NativeCallSlotPolicy, ...], + declaration_callables: tuple[DeclarationCallablePolicy, ...], +) -> tuple[list[ArgumentPolicy], tuple[ResultPolicy, ...], tuple[NativeCallSlotPolicy, ...]]: + """Resolve callable shape expressions to completed scalar or array-extent roles.""" + scalar_roles, array_roles = _function_array_extent_sources(function, owner_path) + callable_roles = { + item.source_name.casefold(): (item.expression_token, item.symbolic_role) for item in declaration_callables + } + def complete(array: ArrayHandoffPolicy | None) -> ArrayHandoffPolicy | None: + if array is None: + return None + display_shape = array.display_shape or array.shape + resolutions = tuple( + resolve_declaration_extent(expression, scalar_roles, array_roles, callable_roles) + for expression in array.shape + ) + return replace( + array, + shape=tuple(item.expression for item in resolutions), + extent_references=tuple(item.references for item in resolutions), + extent_reference_roles=tuple(item.roles for item in resolutions), + extent_callable_references=tuple(item.callable_references for item in resolutions), + extent_callable_roles=tuple(item.callable_roles for item in resolutions), + extent_evaluation=tuple("bridge" if item.callable_roles else "binding" for item in resolutions), + extent_blockers=tuple(item.blockers for item in resolutions), + display_shape=display_shape, + ) -def _array_extent_reference_blockers( + def complete_argument(argument: ArgumentPolicy) -> ArgumentPolicy: + """Keep an argument's accepted-actual shape identical to its resolved handoff.""" + array = complete(argument.array) + native_actual = argument.native_array_actual + if native_actual is not None and array is not None: + native_actual = replace(native_actual, shape=array.shape) + return replace(argument, array=array, native_array_actual=native_actual) + + return ( + [complete_argument(argument) for argument in arguments], + tuple(replace(result, array=complete(result.array)) for result in results), + tuple(replace(slot, array=complete(slot.array)) for slot in native_call_slots), + ) + + +def _function_declaration_callable_policies( function: models.SemanticFunction, + owner_path: str, +) -> tuple[DeclarationCallablePolicy, ...]: + """Validate and classify every native call appearing in one function's extents.""" + entries: dict[str, tuple[models.SemanticExpressionCallable, list[int], bool]] = {} + for semantic_type in _function_declaration_types(function): + storage = semantic_type.storage + array = storage.array if storage is not None else None + if array is None: + continue + for axis, expression in enumerate(array.shape): + references = array.expression_callables[axis] if axis < len(array.expression_callables) else () + sites = declaration_expression_call_sites(expression) + for reference in references: + arities = [ + site.argument_count + for site in sites + if site.name.casefold() == reference.name.casefold() and not site.has_keywords + ] + keyword_use = any( + site.name.casefold() == reference.name.casefold() and site.has_keywords for site in sites + ) + key = reference.name.casefold() + if key not in entries: + entries[key] = (reference, arities, keyword_use) + else: + entries[key][1].extend(arities) + entries[key] = (entries[key][0], entries[key][1], entries[key][2] or keyword_use) + return tuple( + _declaration_callable_policy(reference, arities, has_keywords, owner_path, index) + for index, (reference, arities, has_keywords) in enumerate(entries.values()) + ) + + +def _function_declaration_types(function: models.SemanticFunction) -> tuple[models.SemanticType, ...]: + """Return function-owned semantic types that may carry declared extents.""" + return tuple( + semantic_type + for semantic_type in ( + *(argument.semantic_type for argument in function.arguments), + function.return_type, + *(variable.semantic_type for variable in function.locals), + ) + if isinstance(semantic_type, models.SemanticType) + ) + + +def _declaration_callable_policy( + reference: models.SemanticExpressionCallable, + arities: list[int], + has_keywords: bool, + owner_path: str, + index: int, +) -> DeclarationCallablePolicy: + """Complete one declaration call as a module import or explicit interface.""" + callable_path = f"{owner_path}.declaration_callable.{reference.name}" + blockers: list[str] = [] + declaration = reference.declaration + if has_keywords: + blockers.append(f"declaration callable {reference.name!r} does not accept keyword syntax") + if reference.placement == "module" or reference.native_scope is not None: + action = DeclarationCallableAction.MODULE_IMPORT + if declaration is not None and declaration.visibility != "public": + blockers.append(f"module declaration callable {reference.name!r} is not public") + prototype = None + if declaration is not None: + blockers.extend( + _specification_function_blockers( + declaration, + arities, + label=f"module declaration callable {reference.name!r}", + require_exact_reference_intent=False, + require_pure_diagnostic=True, + ) + ) + elif reference.placement == "standalone": + action = DeclarationCallableAction.STANDALONE_PROCEDURE + if not isinstance(declaration, models.SemanticPrototype): + blockers.append( + f"standalone declaration callable {reference.name!r} requires an exact @prototype signature" + ) + prototype = None + else: + prototype = _direct_prototype_policy( + declaration, + owner_path=callable_path, + local_name=reference.name, + ) + blockers.extend(_direct_prototype_blockers(declaration, arities)) + else: + action = DeclarationCallableAction.STANDALONE_PROCEDURE + prototype = None + kind = "abstract" if reference.placement == "abstract" else "unresolved" + blockers.append(f"declaration callable {reference.name!r} has {kind} native placement") + return DeclarationCallablePolicy( + owner_path=callable_path, + source_name=reference.name, + native_name=reference.native_name or reference.name.rsplit(".", 1)[-1], + native_scope=reference.native_scope, + symbolic_role=f"{callable_path}:function", + expression_token=f"__prik_callable_{index}", + action=action, + prototype=prototype, + blockers=tuple(blockers), + ) + + +def _direct_prototype_policy( + prototype: models.SemanticPrototype, + *, + owner_path: str, + local_name: str, +) -> ProcedurePrototypePolicy: + """Project one directly called signature into the shared prototype model.""" + return _procedure_prototype_policy( + owner_path=owner_path, + name=local_name, + identity=f"{prototype.origin.native_scope or owner_path}.{prototype.name}", + pure=prototype.pure, + arguments=tuple(prototype.arguments), + result=prototype.return_type, + ) + + +def _semantic_prototype_argument_policy( + argument: models.SemanticArgument, + *, + owner_path: str, +) -> ProcedurePrototypeArgumentPolicy: + """Copy one semantic prototype dummy into the shared signature model.""" + semantic_type = argument.semantic_type + return ProcedurePrototypeArgumentPolicy( + owner_path=f"{owner_path}.prototype_argument.{argument.name}", + name=argument.name, + semantic_type_name=semantic_type.name, + rank=int(semantic_type.rank or 0), + passed_by_value=bool(argument.origin.metadata.get("value")), + intent=( + str(intent) + if (intent := argument.origin.metadata.get(models.PROTOTYPE_INTENT_METADATA)) is not None + else None + ), + character_length=_character_length(semantic_type), + array=_array_handoff_policy(semantic_type) if int(semantic_type.rank or 0) > 0 else None, + derived_type_identity=( + _derived_type_identity(semantic_type, owner_path) if _is_scalar_derived_type(semantic_type) else None + ), + ) + + +def _semantic_prototype_result_policy( + semantic_type: models.SemanticType, + *, + owner_path: str, +) -> ProcedurePrototypeResultPolicy: + """Copy one semantic prototype result into the shared signature model.""" + return ProcedurePrototypeResultPolicy( + owner_path=f"{owner_path}.prototype_result", + semantic_type_name=semantic_type.name, + rank=int(semantic_type.rank or 0), + character_length=_character_length(semantic_type), + array=_array_handoff_policy(semantic_type) if int(semantic_type.rank or 0) > 0 else None, + derived_type_identity=( + _derived_type_identity(semantic_type, owner_path) if _is_scalar_derived_type(semantic_type) else None + ), + ) + + +def _direct_prototype_blockers( + prototype: models.SemanticPrototype, + arities: list[int], +) -> tuple[str, ...]: + """Require the exact prototype subset supported by direct bridge calls.""" + blockers = list( + _specification_function_blockers( + prototype, + arities, + label=f"direct prototype {prototype.name!r}", + require_exact_reference_intent=True, + require_pure_diagnostic=False, + ) + ) + if not prototype.pure: + blockers.insert( + 0, + f"direct prototype {prototype.name!r} used in a declaration expression must be @pure", + ) + return tuple(blockers) + + +def _specification_function_blockers( + declaration: models.SemanticFunction, + arities: list[int], + *, + label: str, + require_exact_reference_intent: bool, + require_pure_diagnostic: bool, +) -> tuple[str, ...]: + """Validate the scalar-integer specification-function subset used by extents.""" + blockers = [] + if require_pure_diagnostic and not _semantic_function_is_pure(declaration): + blockers.append(f"{label} must be pure") + result = declaration.return_type + if result is None or int(result.rank or 0) != 0 or not _is_integer_extent_scalar(result): + blockers.append(f"{label} must return one scalar integer") + if arities and any(arity != len(declaration.arguments) for arity in arities): + blockers.append( + f"{label} expects {len(declaration.arguments)} arguments, " + f"but declaration calls use {tuple(dict.fromkeys(arities))}" + ) + for argument in declaration.arguments: + intent = argument.origin.metadata.get(models.PROTOTYPE_INTENT_METADATA) + passed_by_value = bool(argument.origin.metadata.get("value")) + if argument.optional: + blockers.append(f"{label} argument {argument.name!r} cannot be optional") + if int(argument.semantic_type.rank or 0) != 0 or not _is_integer_extent_scalar(argument.semantic_type): + blockers.append(f"{label} argument {argument.name!r} must be a scalar integer") + if intent in {"out", "inout"}: + blockers.append(f"{label} argument {argument.name!r} cannot be {intent}") + if require_exact_reference_intent and not passed_by_value and intent != "in": + blockers.append(f"reference prototype argument {argument.name!r} requires exact In(...) direction") + return tuple(blockers) + + +def _semantic_function_is_pure(declaration: models.SemanticFunction | None) -> bool: + """Return source purity when a module declaration retained that characteristic.""" + if declaration is None: + return True + attributes = declaration.metadata.get("fortran_attributes", ()) + return any(str(attribute).casefold() == "pure" for attribute in attributes) + + +def _function_array_extent_sources( + function: models.SemanticFunction, + owner_path: str, +) -> tuple[dict[str, tuple[str, str]], dict[str, tuple[str, tuple[str, ...]]]]: + """Return visible scalar values and concrete input-array extents by source name.""" + scalar_roles: dict[str, tuple[str, str]] = {} + array_roles: dict[str, tuple[str, tuple[str, ...]]] = {} + for argument in function.arguments: + decision = _ownership_decision(argument, models.RESOLVED_OWNERSHIP_POLICY_METADATA) + if decision is None or not decision.python_visible: + continue + argument_path = f"{owner_path}.{argument.name}" + if int(argument.semantic_type.rank or 0) == 0 and _is_integer_extent_scalar(argument.semantic_type): + scalar_roles[argument.name.casefold()] = (argument.name, f"{argument_path}:value") + continue + array = _array_handoff_policy(argument.semantic_type) + if array is None or array.rank is None: + continue + roles = tuple(f"{argument_path}:extent:{axis}" for axis in range(array.rank)) + array_roles[argument.name.casefold()] = (argument.name, roles) + return scalar_roles, array_roles + + +def _is_integer_extent_scalar(semantic_type: models.SemanticType) -> bool: + """Return whether a visible scalar can safely supply a native array extent. + + Declaration extents consume integer values. Excluding Boolean, real, + character, derived, and callback arguments prevents their data pointers or + payloads from being substituted into generated integer expressions. + """ + name = semantic_type.name + return name == "SizeT" or name.startswith("Int") or name.startswith("UInt") + + +def _array_extent_reference_blockers( arguments: list[ArgumentPolicy], results: tuple[ResultPolicy, ...], ) -> tuple[str, ...]: - """Require every declared extent name to come from a visible scalar argument.""" - scalar_names = { - argument.name - for argument in function.arguments - if int(argument.semantic_type.rank or 0) == 0 - and (decision := _ownership_decision(argument, models.RESOLVED_OWNERSHIP_POLICY_METADATA)) is not None - and decision.python_visible - } + """Require every declared extent dependency to have a completed visible role.""" blockers = [] for owner in (*arguments, *results): if owner.array is None: continue - for axis, references in enumerate(owner.array.extent_references): - missing = tuple(name for name in references if name not in scalar_names) + for axis, missing in enumerate(owner.array.extent_blockers): if missing: blockers.append( f"array owner {owner.owner_path!r} extent axis {axis} has unavailable scalar references {missing}" @@ -6441,12 +7196,13 @@ def _visible_projected_arguments(function: models.SemanticFunction) -> tuple[mod def _native_name(function: models.SemanticFunction) -> str: + """Return the callable's resolved native spelling, preferring explicit semantic identity.""" return str(function.native_name or function.origin.native_name or function.name) def _native_module(function: models.SemanticFunction, owner_path: str) -> str | None: - """Return the completed native module scope for non-external procedures.""" - if _is_external(function): + """Return the completed native module scope for non-standalone procedures.""" + if _is_standalone(function): return None return str(function.origin.native_scope or owner_path.split(".", maxsplit=1)[0]) @@ -6456,7 +7212,8 @@ def _native_is_subroutine(function: models.SemanticFunction) -> bool: return function.origin.source_kind == "subroutine" or function.return_type is None -def _is_external(function: models.SemanticFunction) -> bool: +def _is_standalone(function: models.SemanticFunction) -> bool: + """Report whether a Fortran callable has standalone native placement.""" return bool(function.origin.source_language == "fortran" and function.origin.native_scope is None) @@ -6465,7 +7222,45 @@ def _argument_native_name( python_position: int, argument: models.SemanticArgument, ) -> str: + """Return an argument's projected native spelling, falling back to its semantic name.""" for mapping in function.projection: if mapping.python_position == python_position: return mapping.native_name or argument.name return argument.name + + +# Direct wrapper-policy example. + + +if __name__ == "__main__": + semantic_function = models.SemanticFunction( + name="scale", + arguments=[models.SemanticArgument("value", models.SemanticType("Float64", dtype="Float64"))], + return_type=models.SemanticType("Float64", dtype="Float64"), + ) + semantic_argument = semantic_function.arguments[0] + semantic_argument.metadata[models.RESOLVED_OWNERSHIP_POLICY_METADATA] = OwnershipDecision( + kind=ObjectKind.SCALAR, + owner=OwnershipOwner.CALLER, + transfer=TransferMode.CALL_LOCAL, + destruction=DestructionPolicy.NONE, + codegen_action=CodegenAction.CALL_LOCAL_INPUT, + python_barrier_action=PythonBarrierAction.SCALAR_VALUE, + native_barrier_action=NativeBarrierAction.PASS_VALUE, + ) + semantic_function.metadata[models.RESOLVED_RETURN_OWNERSHIP_POLICY_METADATA] = OwnershipDecision( + kind=ObjectKind.SCALAR, + owner=OwnershipOwner.PYTHON, + transfer=TransferMode.BY_VALUE, + destruction=DestructionPolicy.PYTHON_REFCOUNT, + codegen_action=CodegenAction.DIRECT_VALUE, + python_barrier_action=PythonBarrierAction.NONE, + native_barrier_action=NativeBarrierAction.NONE, + ) + print(f"before: math.scale({semantic_argument.name}): {semantic_argument.semantic_type.name} semantic IR") + policy = build_function_wrapper_policy(semantic_function, owner_path="math.scale") + print( + f"after: {policy.arguments[0].bridge_data_action.value}; " + f"result={policy.results[0].direct_result_abi.value}; " + f"native={policy.native_call_slots[0].native_barrier_action.value}" + ) diff --git a/prik/types/numpy.py b/prik/types/numpy.py index f5cdb3b29..106737976 100644 --- a/prik/types/numpy.py +++ b/prik/types/numpy.py @@ -8,8 +8,18 @@ from prik.semantics.models import SemanticType +BOOLEAN_STORAGE_BITS: Final[dict[str, int]] = { + "Bool": 8, + "Bool8": 8, + "Bool16": 16, + "Bool32": 32, + "Bool64": 64, +} +BOOLEAN_SEMANTIC_TYPE_NAMES: Final[frozenset[str]] = frozenset(BOOLEAN_STORAGE_BITS) + + SEMANTIC_DTYPE_TO_NUMPY_DTYPE: Final[dict[str, str]] = { - "Bool": "numpy.bool_", + **dict.fromkeys(BOOLEAN_SEMANTIC_TYPE_NAMES, "numpy.bool_"), "Int8": "numpy.int8", "Int16": "numpy.int16", "Int32": "numpy.int32", @@ -29,6 +39,25 @@ "SizeT": "numpy.uintp", } + +def is_boolean_semantic_type_name(name: str | None) -> bool: + """Return whether ``name`` identifies a supported Boolean storage contract. + + All supported names share NumPy's one-byte ``bool_`` boundary. Their + numeric suffix records native storage bits for language-specific lowering. + """ + return name in BOOLEAN_SEMANTIC_TYPE_NAMES + + +def boolean_storage_bits(name: str) -> int: + """Return the native storage bits represented by one Boolean contract. + + ``Bool`` and ``Bool8`` both return eight. Unknown names raise ``KeyError`` + so callers cannot silently invent a native Boolean representation. + """ + return BOOLEAN_STORAGE_BITS[name] + + SEMANTIC_SCALAR_TYPE_NAMES: Final[frozenset[str]] = frozenset( { *SEMANTIC_DTYPE_TO_NUMPY_DTYPE, @@ -75,8 +104,12 @@ def semantic_type_to_numpy_dtype(semantic_type: SemanticType) -> Any: __all__ = ( + "BOOLEAN_SEMANTIC_TYPE_NAMES", + "BOOLEAN_STORAGE_BITS", "SEMANTIC_DTYPE_TO_NUMPY_DTYPE", "SEMANTIC_SCALAR_TYPE_NAMES", + "boolean_storage_bits", + "is_boolean_semantic_type_name", "numpy_dtype_expression", "semantic_dtype_to_numpy_dtype", "semantic_dtype_to_numpy_dtype_map", diff --git a/prik/utilities/declaration_expressions.py b/prik/utilities/declaration_expressions.py new file mode 100644 index 000000000..f853262c3 --- /dev/null +++ b/prik/utilities/declaration_expressions.py @@ -0,0 +1,1624 @@ +"""Shared declaration-expression parsing, normalization, resolution, and rendering. + +Declaration owners provide Fortran-like bound text. Semantic conversion turns +that text into the public Python-expression dialect, post-IR policy binds every +visible value to a wrapper role, and code generators render the completed +expression for C or Fortran. Keeping those stages here prevents parsers, +policy, and generators from growing independent expression dialects. + +The public entrypoints are grouped in the same order as that flow: source-text +splitting, source normalization, public-expression inspection, role binding, +compile-time evaluation, and backend rendering. This module does not decide +whether a producer is available at a wrapper boundary; callers supply those +completed role maps to :func:`resolve_declaration_extent`. +""" + +from __future__ import annotations + +import ast +import re +from collections.abc import Mapping +from dataclasses import dataclass + +__all__ = ( + "ArrayExpressionSource", + "DeclarationExpressionCall", + "ResolvedDeclarationExtent", + "canonicalize_declaration_extent", + "declaration_expression_call_sites", + "declaration_expression_calls", + "declaration_extent_references", + "declaration_extent_uses_power", + "evaluate_integer_expression", + "fortran_extent_to_python", + "is_declaration_expression_helper", + "is_public_declaration_expression", + "render_declaration_extent", + "resolve_declaration_extent", + "split_declaration_assignment", + "split_dimension_bounds", + "split_top_level_expression", +) + + +_RUNTIME_DIMENSIONS = frozenset({":", "::Strided", "...", "Flat"}) +_FORTRAN_RELATIONAL_OPERATORS = { + ".eq.": "==", + ".ne.": "!=", + ".lt.": "<", + ".le.": "<=", + ".gt.": ">", + ".ge.": ">=", +} +_FORTRAN_LOGICAL_OPERATORS = { + ".and.": " and ", + ".or.": " or ", + ".not.": " not ", + ".eqv.": " == ", + ".neqv.": " != ", +} +_SUPPORTED_CALLS = frozenset({"abs", "int", "max", "min"}) +_PUBLIC_CALLS = frozenset({*_SUPPORTED_CALLS, "len", "sum"}) +_PUBLIC_ARRAY_ATTRIBUTES = frozenset({"size", "shape", "ndim"}) +_PUBLIC_EXPRESSION_NODES = ( + ast.Expression, + ast.BinOp, + ast.UnaryOp, + ast.BoolOp, + ast.Compare, + ast.IfExp, + ast.Name, + ast.Load, + ast.Constant, + ast.Call, + ast.Attribute, + ast.Subscript, + ast.List, + ast.Tuple, + ast.Add, + ast.Sub, + ast.Mult, + ast.MatMult, + ast.Div, + ast.FloorDiv, + ast.Mod, + ast.Pow, + ast.LShift, + ast.RShift, + ast.BitOr, + ast.BitXor, + ast.BitAnd, + ast.USub, + ast.UAdd, + ast.Invert, + ast.Not, + ast.And, + ast.Or, + ast.Eq, + ast.NotEq, + ast.Lt, + ast.LtE, + ast.Gt, + ast.GtE, +) +_RESOLVED_EXTENT_NODES = ( + ast.Expression, + ast.BinOp, + ast.UnaryOp, + ast.BoolOp, + ast.Compare, + ast.IfExp, + ast.Name, + ast.Load, + ast.Constant, + ast.Call, + ast.Add, + ast.Sub, + ast.Mult, + ast.Div, + ast.FloorDiv, + ast.Mod, + ast.Pow, + ast.USub, + ast.UAdd, + ast.Not, + ast.And, + ast.Or, + ast.Eq, + ast.NotEq, + ast.Lt, + ast.LtE, + ast.Gt, + ast.GtE, +) + + +# ============================================================================ +# Public records +# ============================================================================ + + +@dataclass(frozen=True) +class ArrayExpressionSource: + """Describe one declared array referenced by another extent expression. + + Use this record in the ``arrays`` mapping passed to + :func:`fortran_extent_to_python` when an expression queries an array with + ``size``, ``shape``, ``rank``, ``lbound``, or ``ubound``. ``rank`` and + source lower bounds preserve Fortran's declared index origin; omitted + lower bounds use the language default of one. + """ + + rank: int + lower_bounds: tuple[str | None, ...] = () + + +@dataclass(frozen=True) +class DeclarationExpressionCall: + """Describe one syntactically static declaration-expression call. + + :func:`declaration_expression_call_sites` returns these records before + semantic conversion resolves a call to a native procedure or public helper. + ``argument_count`` and ``has_keywords`` describe syntax only; they do not + validate the native interface. + """ + + name: str + argument_count: int + has_keywords: bool = False + + +@dataclass(frozen=True) +class ResolvedDeclarationExtent: + """Hold one public extent after its dependencies have been role-bound. + + ``expression`` contains backend-neutral reference tokens. ``references`` + and ``roles`` are parallel tuples consumed by wrapper plans; callable + tuples do the same for native specification functions. ``blockers`` names + syntax or values that policy could not supply, so callers can reject the + declaration without guessing a producer. + """ + + expression: str + references: tuple[str, ...] = () + roles: tuple[str, ...] = () + callable_references: tuple[str, ...] = () + callable_roles: tuple[str, ...] = () + blockers: tuple[str, ...] = () + + +# ============================================================================ +# Source declaration text +# ============================================================================ + + +def split_top_level_expression(text: str, delimiter: str) -> list[str]: + """Split ``text`` at one delimiter outside brackets and quoted literals. + + The returned pieces preserve nested calls, constructors, and substrings. + Unbalanced syntax is deliberately preserved for the parser's normal + diagnostic path instead of being repaired here. + + Raises: + ValueError: If ``delimiter`` is not exactly one character. + """ + if len(delimiter) != 1: + raise ValueError("declaration-expression delimiters must be one character") + + parts: list[str] = [] + start = 0 + stack: list[str] = [] + quote: str | None = None + index = 0 + while index < len(text): + char = text[index] + if quote is not None: + if char == quote: + if index + 1 < len(text) and text[index + 1] == quote: + index += 2 + continue + quote = None + index += 1 + continue + if char in {"'", '"'}: + quote = char + elif char in "([{": + stack.append(char) + elif char in ")]}" and stack: + stack.pop() + elif char == delimiter and not stack: + parts.append(text[start:index].strip()) + start = index + 1 + index += 1 + parts.append(text[start:].strip()) + return parts + + +def split_dimension_bounds(token: str) -> tuple[str | None, str | None]: + """Return one dimension's outer lower and upper bound expressions. + + A dimension without an outer colon has Fortran's implicit lower bound one. + Colons nested in calls, constructors, or subscripts remain part of the + corresponding bound. + """ + part = token.strip() + if not part: + return None, None + bounds = split_top_level_expression(part, ":") + if len(bounds) == 1: + return "1", part + lower = bounds[0].strip() or None + upper = ":".join(bounds[1:]).strip() or None + return lower, upper + + +def split_declaration_assignment(text: str) -> tuple[str, str | None]: + """Split one entity declaration from its top-level initializer. + + Equals signs in nested inquiry keywords, comparisons, and constructors do + not start an initializer. Both ordinary ``=`` and pointer ``=>`` forms are + recognized; the returned initializer omits the assignment operator. + """ + stack: list[str] = [] + quote: str | None = None + index = 0 + while index < len(text): + char = text[index] + if quote is not None: + if char == quote: + if index + 1 < len(text) and text[index + 1] == quote: + index += 2 + continue + quote = None + index += 1 + continue + if char in {"'", '"'}: + quote = char + elif char in "([{": + stack.append(char) + elif char in ")]}" and stack: + stack.pop() + elif char == "=" and not stack: + previous = text[index - 1] if index else "" + following = text[index + 1] if index + 1 < len(text) else "" + if previous not in {"<", ">", "=", "/"} and following != "=": + initializer_start = index + 1 + if following == ">": + initializer_start += 1 + return text[:index].strip(), text[initializer_start:].strip() + index += 1 + return text.strip(), None + + +# ============================================================================ +# Source-to-public normalization +# ============================================================================ + + +def fortran_extent_to_python( + expression: str, + arrays: Mapping[str, ArrayExpressionSource] | None = None, +) -> str: + """Translate a scalar Fortran extent to the semantic Python expression dialect. + + The conversion covers Fortran operators and wrapper-relevant array inquiry + intrinsics. Unknown but Python-shaped calls are retained so policy can emit + a precise unavailable-expression blocker. If the source cannot be parsed + safely, the original spelling is returned unchanged as provenance. + + ``arrays`` maps the visible Fortran array name to its declared rank and + lower bounds. Supply it when translating inquiry calls; omit it for scalar + expressions with no declared-array context. + """ + # Stage 1: make the source expression parseable without evaluating it. + normalized = _python_parseable_fortran_expression(expression) + tree = _parse_expression(normalized) + if tree is None: + return expression.strip() + + # Stage 2: translate only inquiries whose declared array facts are known. + translator = _FortranExtentTranslator(arrays or {}) + translated = translator.visit(tree) + ast.fix_missing_locations(translated) + return ast.unparse(translated) + + +def canonicalize_declaration_extent(expression: str) -> str: + """Cancel additive bound terms in one Python-form declaration extent. + + This consumes translated Python syntax and returns a concise equivalent. + Syntax outside that grammar is preserved unchanged for the later policy + diagnostic path. + """ + parsed = _parse_expression(expression) + if parsed is None: + return expression + return ast.unparse(_simplify_additive_expression(parsed.body)) + + +# ============================================================================ +# Public-expression inspection and role binding +# ============================================================================ + + +def resolve_declaration_extent( + expression: str, + scalar_roles: Mapping[str, tuple[str, str]], + array_roles: Mapping[str, tuple[str, tuple[str, ...]]], + callable_roles: Mapping[str, tuple[str, str]] | None = None, +) -> ResolvedDeclarationExtent: + """Bind one Python-form extent to visible scalar and array-extent roles. + + Runtime dimension markers require no roles. Invalid syntax, unsupported + calls, nonliteral shape indices, and unavailable names are returned as + blockers; the function never guesses a producer. + + ``scalar_roles`` maps a visible scalar spelling to its canonical source + name and completed role. ``array_roles`` maps an array spelling to its + canonical source name and one role per axis. ``callable_roles`` does the + same for a specification-function token. The returned record is normally + stored on completed policy and consumed by backend rendering. + """ + # Stage 1: preserve caller-owned runtime dimension markers. + if expression in _RUNTIME_DIMENSIONS: + return ResolvedDeclarationExtent(expression) + + # Stage 2: parse the public expression before binding any producer roles. + tree = _parse_expression(expression) + if tree is None: + return ResolvedDeclarationExtent(expression, ("",), blockers=("",)) + + # Stage 3: replace visible array properties and calls with completed tokens. + resolver = _ExtentRoleResolver(scalar_roles, array_roles, callable_roles or {}) + resolved = resolver.visit(tree) + ast.fix_missing_locations(resolved) + + # Stage 4: reject incomplete output rather than inventing a backend value. + if resolver.blockers or not _valid_resolved_extent(resolved, callable_names=frozenset(resolver.callables)): + blockers = tuple(dict.fromkeys(resolver.blockers or ("",))) + return ResolvedDeclarationExtent(expression, blockers, blockers=blockers) + + # Stage 5: retain ordered source names alongside their role substitutions. + return ResolvedDeclarationExtent( + ast.unparse(resolved) if resolver.changed else expression, + tuple(resolver.references), + tuple(resolver.references.values()), + tuple(resolver.callables), + tuple(resolver.callables.values()), + ) + + +def declaration_extent_references(expression: str) -> tuple[str, ...]: + """Return scalar names used by a role-free declaration extent. + + This compatibility helper is used before a callable's producer roles are + known. Array properties and unsupported syntax return ```` so the + later policy stage cannot accidentally treat them as scalar values. + """ + if expression in _RUNTIME_DIMENSIONS: + return () + tree = _parse_expression(expression) + if tree is None: + return ("",) + if not _valid_resolved_extent(tree): + return ("",) + function_names = { + node.func.id for node in ast.walk(tree) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) + } + return tuple( + dict.fromkeys( + node.id for node in ast.walk(tree) if isinstance(node, ast.Name) and node.id not in function_names + ) + ) + + +def declaration_expression_calls(expression: str) -> tuple[str, ...]: + """Return named call targets used by one Python-form declaration expression. + + The result preserves first-use order and includes both bare names and + qualified targets such as ``sizes.extent_for``. Malformed or dynamically + computed call targets are reported as ````. The helper only + inspects syntax; semantic conversion decides whether a name is a built-in + helper, a local native procedure, or an imported native procedure. + """ + tree = _parse_expression(expression) + if tree is None: + return ("",) + calls: list[str] = [] + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + name = _qualified_call_name(node.func) + calls.append(name or "") + return tuple(dict.fromkeys(calls)) + + +def declaration_expression_call_sites(expression: str) -> tuple[DeclarationExpressionCall, ...]: + """Return ordered static call sites with arity and keyword presence. + + Use this syntax-only helper before native procedure identity is known. An + invalid expression yields one ```` record; a dynamic callable name + yields ```` at that call site while retaining its argument shape. + """ + tree = _parse_expression(expression) + if tree is None: + return (DeclarationExpressionCall("", 0),) + return tuple( + DeclarationExpressionCall( + _qualified_call_name(node.func) or "", + len(node.args), + bool(node.keywords), + ) + for node in ast.walk(tree) + if isinstance(node, ast.Call) + ) + + +def is_declaration_expression_helper(name: str) -> bool: + """Return whether a bare call name belongs to the public helper dialect. + + Semantic name resolution should check native declarations and imports + first, because a source language may explicitly shadow an intrinsic name. + Qualified names are never treated as built-in helpers. + """ + return "." not in name and name.casefold() in _PUBLIC_CALLS + + +def declaration_extent_uses_power(expression: str) -> bool: + """Return whether parseable public text contains an integer power operation. + + Callers use this as a code-generation preparation hint. Invalid text simply + returns ``False`` here; normal policy validation still owns its diagnostic. + """ + tree = _parse_expression(expression) + if tree is None: + return False + return any(isinstance(node, ast.Pow) for node in ast.walk(tree)) + + +def is_public_declaration_expression(expression: str) -> bool: + """Return whether text uses the documented Python declaration grammar. + + This syntactic check admits unresolved scalar names but restricts array + attributes to ``size``, ``ndim``, and ``shape[index]`` forms. Policy later + verifies that every name and array actually has a visible producer role. + """ + tree = _parse_expression(expression) + if tree is None: + return False + call_targets = _declaration_expression_call_target_attributes(tree) + return all(_is_public_declaration_expression_node(node, call_targets) for node in ast.walk(tree)) + + +def _declaration_expression_call_target_attributes(tree: ast.AST) -> set[int]: + """Return attribute identities used as callable targets within one tree. + + Attribute calls are not array-property references. Identity tracking keeps + the public grammar check from applying array-property rules to attributes + inside a callable expression. + """ + return { + id(attribute) + for call in ast.walk(tree) + if isinstance(call, ast.Call) + for attribute in ast.walk(call.func) + if isinstance(attribute, ast.Attribute) + } + + +def _is_public_declaration_expression_node(node: ast.AST, call_targets: set[int]) -> bool: + """Return whether one parsed node is valid in the public expression grammar. + + The call-target set identifies attributes belonging to a callable name rather + than an array inquiry. This helper validates syntax only; producer roles and + callable provenance remain policy-stage responsibilities. + """ + if not isinstance(node, _PUBLIC_EXPRESSION_NODES) or not _is_integer_constant(node): + return False + if isinstance(node, ast.Call): + return _qualified_call_name(node.func) is not None and not node.keywords + if isinstance(node, ast.Attribute): + return id(node) in call_targets or _is_public_array_attribute(node) + if isinstance(node, ast.Subscript): + return _is_public_shape_index(node) + return True + + +def _is_public_array_attribute(node: ast.Attribute) -> bool: + """Return whether an attribute is one documented bare array property.""" + return isinstance(node.value, ast.Name) and node.attr in _PUBLIC_ARRAY_ATTRIBUTES + + +def _is_public_shape_index(node: ast.Subscript) -> bool: + """Return whether a subscript is one literal array.shape[index] inquiry.""" + return ( + isinstance(node.value, ast.Attribute) + and node.value.attr == "shape" + and isinstance(node.value.value, ast.Name) + and isinstance(node.slice, ast.Constant) + and _is_integer_constant(node.slice) + ) + + +# ============================================================================ +# Compile-time evaluation +# ============================================================================ + + +def evaluate_integer_expression(expression: str) -> int | None: + """Evaluate a self-contained Fortran/Python integer declaration expression. + + Only literals, constructors, arithmetic, comparisons, Boolean operators, + and the small pure intrinsic set implemented by ``_IntegerEvaluator`` are + executed. Names, arbitrary calls, invalid operations, and nonintegral final + values return ``None`` without side effects. + + Use it only for self-contained declarations such as parameter values. Its + integer result can then replace source metadata; a ``None`` result means + the source spelling must remain unresolved for a later stage. + """ + normalized = _python_parseable_fortran_expression(expression) + tree = _parse_expression(normalized) + if tree is None: + return None + value = _IntegerEvaluator().evaluate(tree) + if isinstance(value, bool): + return int(value) + if isinstance(value, float) and value.is_integer(): + return int(value) + return value if isinstance(value, int) else None + + +# ============================================================================ +# Shared syntax helpers +# ============================================================================ + + +def _parse_expression(expression: str) -> ast.Expression | None: + """Parse one expression without exposing ``SyntaxError`` to policy callers. + + The helper consumes already-normalized or public expression text and + returns an ``eval``-mode tree. It returns ``None`` only for syntax that the + public caller must preserve, block, or reframe with its own diagnostic; it + never modifies the supplied text. + """ + try: + return ast.parse(expression, mode="eval") + except SyntaxError: + return None + + +def _python_parseable_fortran_expression(expression: str) -> str: + """Convert lexical Fortran syntax to equivalent parseable Python text. + + This consumes raw declaration text and performs no semantic inquiry + translation. Unknown names and calls are intentionally retained for later + provenance or policy diagnostics. + """ + text = expression.strip() + text = _replace_fortran_array_constructors(text) + text = re.sub(r"(?i)(?<=\d)_[A-Za-z]\w*\b", "", text) + text = re.sub(r"(?i)(?<=\d)_[0-9]+\b", "", text) + text = re.sub(r"(?i)\b(\d+(?:\.\d*)?)[dD]([+-]?\d+)\b", r"\1e\2", text) + text = re.sub(r"(?i)\.true\.", "True", text) + text = re.sub(r"(?i)\.false\.", "False", text) + for source, replacement in _FORTRAN_RELATIONAL_OPERATORS.items(): + text = re.sub(re.escape(source), replacement, text, flags=re.IGNORECASE) + for source, replacement in _FORTRAN_LOGICAL_OPERATORS.items(): + text = re.sub(re.escape(source), replacement, text, flags=re.IGNORECASE) + text = text.replace("/=", "!=") + return text.replace("%", ".") + + +def _qualified_call_name(node: ast.AST) -> str | None: + """Return a dotted static call target, or ``None`` for computed callables.""" + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + parent = _qualified_call_name(node.value) + return f"{parent}.{node.attr}" if parent is not None else None + return None + + +def _replace_fortran_array_constructors(expression: str) -> str: + """Replace legacy ``(/ ... /)`` constructors outside quoted literals.""" + output: list[str] = [] + index = 0 + quote: str | None = None + while index < len(expression): + char = expression[index] + if quote is not None: + output.append(char) + if char == quote: + if index + 1 < len(expression) and expression[index + 1] == quote: + output.append(expression[index + 1]) + index += 2 + continue + quote = None + index += 1 + continue + if char in {"'", '"'}: + quote = char + output.append(char) + index += 1 + elif expression.startswith("(/", index): + output.append("[") + index += 2 + elif expression.startswith("/)", index): + output.append("]") + index += 2 + else: + output.append(expression[index]) + index += 1 + return "".join(output) + + +def _simplify_additive_expression(expression: ast.expr) -> ast.expr: + """Return an equivalent AST with repeated signed terms combined. + + The helper consumes one parsed Python-form expression and builds a new + additive tree without mutating the original nodes. It preserves first-use + order for symbolic terms, which keeps emitted declaration text stable. + """ + terms: list[tuple[int, ast.expr]] = [] + + def collect(node: ast.expr, sign: int = 1) -> None: + """Flatten signed additive terms into source order.""" + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): + collect(node.left, sign) + collect(node.right, sign) + return + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Sub): + collect(node.left, sign) + collect(node.right, -sign) + return + if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.USub): + collect(node.operand, -sign) + return + terms.append((sign, node)) + + collect(expression) + constant = sum( + sign * node.value + for sign, node in terms + if isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool) + ) + symbolic = [ + (sign, node) + for sign, node in terms + if not (isinstance(node, ast.Constant) and isinstance(node.value, int) and not isinstance(node.value, bool)) + ] + + coefficients: dict[str, tuple[ast.expr, int]] = {} + order: list[str] = [] + for sign, node in symbolic: + key = ast.dump(node, include_attributes=False) + if key not in coefficients: + coefficients[key] = (node, 0) + order.append(key) + original, coefficient = coefficients[key] + coefficients[key] = (original, coefficient + sign) + + result: ast.expr | None = None + for key in order: + node, coefficient = coefficients[key] + for _ in range(abs(coefficient)): + if result is None: + result = node if coefficient > 0 else ast.UnaryOp(op=ast.USub(), operand=node) + else: + operator: ast.operator = ast.Add() if coefficient > 0 else ast.Sub() + result = ast.BinOp(left=result, op=operator, right=node) + + if result is None: + return ast.Constant(value=constant) + if constant > 0: + return ast.BinOp(left=result, op=ast.Add(), right=ast.Constant(value=constant)) + if constant < 0: + return ast.BinOp(left=result, op=ast.Sub(), right=ast.Constant(value=-constant)) + return result + + +# ============================================================================ +# Fortran inquiry translation +# ============================================================================ + + +class _FortranExtentTranslator(ast.NodeTransformer): + """Translate known Fortran inquiries using declared array facts. + + Instances own a case-insensitive snapshot of the caller's array mapping. + They convert only syntactically valid inquiries for known arrays; unknown + calls and malformed inquiry forms remain in the AST for later policy + diagnostics instead of being guessed or evaluated. + """ + + def __init__(self, arrays: Mapping[str, ArrayExpressionSource]) -> None: + """Index source arrays case-insensitively without mutating caller state.""" + self.arrays = {name.casefold(): source for name, source in arrays.items()} + + def visit_BinOp(self, node: ast.BinOp) -> ast.AST: + """Recognize the canonical upper-minus-lower extent before recursion.""" + extent = self._bound_difference_extent(node) + return self.generic_visit(node) if extent is None else ast.copy_location(extent, node) + + def visit_Call(self, node: ast.Call) -> ast.AST: + """Translate one call, preserving unsupported forms for later stages. + + Array inquiries use stored declaration facts first. The remaining + branches normalize safe intrinsic spellings and reductions, then leave + arbitrary specification-function calls intact for semantic resolution. + """ + name = node.func.id.casefold() if isinstance(node.func, ast.Name) else "" + + array_inquiry = self._translated_array_inquiry(name, node) + if array_inquiry is not None: + return ast.copy_location(array_inquiry, node) + intrinsic = self._translated_intrinsic_call(name, node) + if intrinsic is not None: + return ast.copy_location(intrinsic, node) + reduction = self._translated_reduction_call(name, node) + if reduction is not None: + return ast.copy_location(reduction, node) + return self._normalize_untranslated_call(node) + + def _translated_array_inquiry(self, name: str, node: ast.Call) -> ast.AST | None: + """Return a translated inquiry when the call uses a known array source. + + Unknown calls and malformed inquiry forms return None so the unchanged + node retains semantic provenance for later diagnostics. + """ + if name not in {"size", "shape", "rank", "lbound", "ubound"}: + return None + return self._array_inquiry(name, node) + + def _translated_intrinsic_call(self, name: str, node: ast.Call) -> ast.AST | None: + """Return a direct Python equivalent for supported scalar intrinsics. + + The helper consumes only fully positional modulo and merge calls. Other + spellings return None so generic traversal preserves their call identity. + """ + if node.keywords: + return None + if name in {"mod", "modulo"} and len(node.args) == 2: + return ast.BinOp(self.visit(node.args[0]), ast.Mod(), self.visit(node.args[1])) + if name == "merge" and len(node.args) == 3: + return ast.IfExp(self.visit(node.args[2]), self.visit(node.args[0]), self.visit(node.args[1])) + return None + + def _translated_reduction_call(self, name: str, node: ast.Call) -> ast.AST | None: + """Return a normalized reduction call for one supported unary reduction. + + Product receives additional shape and literal-constructor folding. Other + reductions retain their argument after recursive inquiry translation. + """ + if name not in {"product", "sum", "maxval", "minval"} or len(node.args) != 1: + return None + argument = self.visit(node.args[0]) + if name == "product": + if isinstance(argument, ast.Attribute) and argument.attr == "shape": + return ast.Attribute(argument.value, "size", ast.Load()) + if isinstance(argument, ast.List | ast.Tuple) and argument.elts: + return self._fold_binary(list(argument.elts), ast.Mult()) + public_name = {"product": "product", "sum": "sum", "maxval": "max", "minval": "min"}[name] + return ast.Call(ast.Name(public_name, ast.Load()), [argument], []) + + def _normalize_untranslated_call(self, node: ast.Call) -> ast.AST: + """Normalize spelling and kind keywords while retaining an unresolved call. + + Arbitrary static call names must survive translation so semantic + conversion can establish their native provenance instead of guessing. + """ + if isinstance(node.func, ast.Name): + node.func.id = node.func.id.casefold() + node.keywords = [ + keyword for keyword in node.keywords if keyword.arg is None or keyword.arg.casefold() != "kind" + ] + return self.generic_visit(node) + + def _array_inquiry(self, name: str, node: ast.Call) -> ast.AST | None: + """Return one public Python property expression for a valid inquiry. + + ``node`` must name a known array as its first positional argument. The + result is ``None`` for unknown arrays, invalid DIM syntax, and + out-of-range dimensions so the original call can remain diagnostic + provenance rather than becoming a guessed expression. + """ + source_node = node.args[0] if node.args else None + if not isinstance(source_node, ast.Name): + return None + source_name = source_node.id + source = self.arrays.get(source_name.casefold()) + if source is None: + return None + dimension = self._inquiry_dimension(name, node) + if dimension is False: + return None + if name == "size": + return self._array_size(source_name, dimension) + if name == "shape": + return ast.Attribute(ast.Name(source_name, ast.Load()), "shape", ast.Load()) + if name == "rank": + return ast.Attribute(ast.Name(source_name, ast.Load()), "ndim", ast.Load()) + bounds = tuple( + self._axis_bound(source_name, source, axis, upper=name == "ubound") for axis in range(source.rank) + ) + if dimension is None: + return ast.Tuple(bounds, ast.Load()) + if not 1 <= dimension <= source.rank: + return None + return bounds[dimension - 1] + + def _bound_difference_extent(self, node: ast.BinOp) -> ast.AST | None: + """Translate ``ubound(a,d) - lbound(a,d) + 1`` directly to an extent.""" + if not ( + isinstance(node.op, ast.Add) + and isinstance(node.right, ast.Constant) + and _is_integer_constant(node.right) + and int(node.right.value) == 1 + and isinstance(node.left, ast.BinOp) + and isinstance(node.left.op, ast.Sub) + ): + return None + upper = self._bound_inquiry_signature(node.left.left, "ubound") + lower = self._bound_inquiry_signature(node.left.right, "lbound") + if upper is None or upper != lower: + return None + source_name, dimension = upper + return self._array_size(source_name, dimension) + + def _bound_inquiry_signature(self, node: ast.AST, name: str) -> tuple[str, int] | None: + """Return one recognized scalar bound inquiry's source name and dimension.""" + if not ( + isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id.casefold() == name + and node.args + and isinstance(node.args[0], ast.Name) + and node.args[0].id.casefold() in self.arrays + ): + return None + dimension = self._inquiry_dimension(name, node) + if not isinstance(dimension, int) or isinstance(dimension, bool): + return None + source_name = node.args[0].id + source = self.arrays[source_name.casefold()] + if not 1 <= dimension <= source.rank: + return None + return source_name, dimension + + @staticmethod + def _inquiry_dimension(name: str, node: ast.Call) -> int | None | bool: + """Return a one-based literal DIM, ``None``, or ``False`` for invalid syntax.""" + if not node.args: + return False + if name == "rank": + return None if len(node.args) == 1 and not node.keywords else False + dimension_node = None + if name in {"size", "lbound", "ubound"}: + if len(node.args) > 3: + return False + dimension_node = node.args[1] if len(node.args) >= 2 else None + elif name == "shape": + if len(node.args) > 2: + return False + seen_keywords: set[str] = set() + for keyword in node.keywords: + if keyword.arg is None: + return False + keyword_name = keyword.arg.casefold() + if keyword_name in seen_keywords or keyword_name not in {"dim", "kind"}: + return False + seen_keywords.add(keyword_name) + if keyword_name == "dim": + if name not in {"size", "lbound", "ubound"} or dimension_node is not None: + return False + dimension_node = keyword.value + if dimension_node is None: + return None + if isinstance(dimension_node, ast.Constant) and _is_integer_constant(dimension_node): + return int(dimension_node.value) + return False + + @staticmethod + def _array_size(source_name: str, dimension: int | None) -> ast.AST: + """Return total size or one zero-based Python shape selection.""" + array = ast.Name(source_name, ast.Load()) + if dimension is None: + return ast.Attribute(array, "size", ast.Load()) + return ast.Subscript(ast.Attribute(array, "shape", ast.Load()), ast.Constant(dimension - 1), ast.Load()) + + def _axis_bound( + self, + source_name: str, + source: ArrayExpressionSource, + axis: int, + *, + upper: bool, + ) -> ast.AST: + """Build one lower or upper bound while preserving zero-extent rules. + + Missing source lower bounds become one. For a zero-sized runtime axis, + Fortran inquiry semantics require lower bound one and upper bound zero; + the returned conditional AST enforces those results without evaluating + the runtime source array here. + """ + lower_text = source.lower_bounds[axis] if axis < len(source.lower_bounds) else None + lower_expression = fortran_extent_to_python(lower_text or "1", self.arrays) + try: + lower = ast.parse(lower_expression, mode="eval").body + except SyntaxError: + lower = ast.Name("__prik_invalid_lower_bound", ast.Load()) + extent = ast.Subscript( + ast.Attribute(ast.Name(source_name, ast.Load()), "shape", ast.Load()), + ast.Constant(axis), + ast.Load(), + ) + positive_extent = ast.Compare(extent, [ast.Gt()], [ast.Constant(0)]) + if not upper: + return ast.IfExp(positive_extent, lower, ast.Constant(1)) + declared_upper = ast.BinOp(ast.BinOp(lower, ast.Add(), extent), ast.Sub(), ast.Constant(1)) + return ast.IfExp(positive_extent, declared_upper, ast.Constant(0)) + + @staticmethod + def _fold_binary(items: list[ast.AST], operator: ast.operator) -> ast.AST: + """Fold nonempty constructor items left-to-right with ``operator``. + + Callers provide a nonempty sequence. The method creates a fresh binary + tree and preserves source order, which is significant for stable text. + """ + result = items[0] + for item in items[1:]: + result = ast.BinOp(result, operator, item) + return result + + +# ============================================================================ +# Compile-time AST evaluation +# ============================================================================ + + +class _IntegerEvaluator: + """Evaluate the side-effect-free compile-time subset of declaration syntax. + + This internal dispatcher never invokes arbitrary callables or resolves + names. Every unsupported node, value, intrinsic signature, or arithmetic + error becomes ``None`` so :func:`evaluate_integer_expression` can preserve + its no-exception, no-side-effect contract. + """ + + def evaluate(self, node: ast.AST): + """Return one supported AST value or ``None`` without mutating ``node``.""" + method = getattr(self, f"_evaluate_{type(node).__name__}", None) + return None if method is None else method(node) + + def _evaluate_Expression(self, node: ast.Expression): + """Evaluate an ``eval``-mode wrapper by delegating to its body node.""" + return self.evaluate(node.body) + + @staticmethod + def _evaluate_Constant(node: ast.Constant): + """Return a supported scalar literal without coercing its source value.""" + return node.value if isinstance(node.value, int | float | str | bool) else None + + def _evaluate_List(self, node: ast.List): + """Evaluate a constructor list, failing if any element is unsupported.""" + return self._evaluate_sequence(node.elts) + + def _evaluate_Tuple(self, node: ast.Tuple): + """Evaluate a constructor tuple using the same list representation.""" + return self._evaluate_sequence(node.elts) + + def _evaluate_sequence(self, nodes: list[ast.expr]): + """Evaluate constructor items in order or return ``None`` after a failed item.""" + values = [self.evaluate(item) for item in nodes] + return None if any(value is None for value in values) else values + + def _evaluate_BinOp(self, node: ast.BinOp): + """Evaluate one supported numeric binary operation.""" + left = self.evaluate(node.left) + right = self.evaluate(node.right) + if not isinstance(left, int | float) or not isinstance(right, int | float): + return None + operations = { + ast.Add: lambda: left + right, + ast.Sub: lambda: left - right, + ast.Mult: lambda: left * right, + ast.Div: lambda: left / right, + ast.FloorDiv: lambda: left // right, + ast.Mod: lambda: left % right, + ast.Pow: lambda: left**right, + } + operation = operations.get(type(node.op)) + if operation is None: + return None + try: + return operation() + except (OverflowError, ValueError, ZeroDivisionError): + return None + + def _evaluate_UnaryOp(self, node: ast.UnaryOp): + """Evaluate numeric signs and logical negation.""" + value = self.evaluate(node.operand) + if isinstance(node.op, ast.Not): + return None if value is None else not bool(value) + if not isinstance(value, int | float): + return None + return -value if isinstance(node.op, ast.USub) else +value if isinstance(node.op, ast.UAdd) else None + + def _evaluate_Call(self, node: ast.Call): + """Evaluate one supported side-effect-free Fortran intrinsic.""" + if not isinstance(node.func, ast.Name) or any( + keyword.arg is None or keyword.arg.casefold() != "kind" for keyword in node.keywords + ): + return None + arguments = [self.evaluate(argument) for argument in node.args] + if any(argument is None for argument in arguments): + return None + try: + return self._intrinsic_value(node.func.id.casefold(), arguments) + except (OverflowError, ValueError, ZeroDivisionError): + return None + + @staticmethod + def _intrinsic_value(name: str, arguments: list[object]): + """Return a supported intrinsic result or ``None`` for an invalid signature. + + ``arguments`` already contains recursively evaluated values. This method + performs only local deterministic arithmetic and lets its caller turn + arithmetic errors into the evaluator's ``None`` sentinel. + """ + if name in {"abs", "max", "min", "mod", "modulo"}: + return _IntegerEvaluator._numeric_intrinsic_value(name, arguments) + if name in {"product", "sum", "maxval", "minval"} and len(arguments) == 1: + return _IntegerEvaluator._reduction_value(name, arguments[0]) + if name == "merge" and len(arguments) == 3: + return arguments[0] if bool(arguments[2]) else arguments[1] + if name == "int" and arguments and isinstance(arguments[0], int | float): + return int(arguments[0]) + if name in {"len", "len_trim", "iachar"}: + return _IntegerEvaluator._string_intrinsic_value(name, arguments) + return None + + @staticmethod + def _numeric_intrinsic_value(name: str, arguments: list[object]): + """Evaluate one numeric scalar intrinsic or return None for invalid inputs. + + Arguments remain generic because callers recursively evaluate expression + nodes first. The supported names and arities match _intrinsic_value. + """ + if not all(isinstance(argument, int | float) for argument in arguments): + return None + if name == "abs": + return abs(arguments[0]) if len(arguments) == 1 else None + if name in {"max", "min"}: + if not arguments: + return None + return max(arguments) if name == "max" else min(arguments) + if name in {"mod", "modulo"} and len(arguments) == 2: + return arguments[0] % arguments[1] + return None + + @staticmethod + def _string_intrinsic_value(name: str, arguments: list[object]): + """Evaluate one string inquiry intrinsic or return None for invalid inputs.""" + if len(arguments) != 1 or not isinstance(arguments[0], str): + return None + value = arguments[0] + if name == "len": + return len(value) + if name == "len_trim": + return len(value.rstrip()) + return ord(value[0]) if value else None + + @staticmethod + def _reduction_value(name: str, values: object): + """Reduce a nonempty numeric constructor or return ``None`` when invalid.""" + if not isinstance(values, list) or not values or not all(isinstance(value, int | float) for value in values): + return None + if name == "product": + result = 1 + for value in values: + result *= value + return result + if name == "sum": + return sum(values) + return max(values) if name == "maxval" else min(values) + + def _evaluate_BoolOp(self, node: ast.BoolOp): + """Evaluate a Boolean conjunction or disjunction.""" + values = [self.evaluate(value) for value in node.values] + if any(value is None for value in values): + return None + return all(map(bool, values)) if isinstance(node.op, ast.And) else any(map(bool, values)) + + def _evaluate_IfExp(self, node: ast.IfExp): + """Evaluate one conditional expression after its condition is known.""" + condition = self.evaluate(node.test) + if condition is None: + return None + return self.evaluate(node.body if bool(condition) else node.orelse) + + def _evaluate_Compare(self, node: ast.Compare): + """Evaluate a supported comparison chain left-to-right, or return ``None``.""" + values = [self.evaluate(node.left), *(self.evaluate(item) for item in node.comparators)] + if any(value is None for value in values): + return None + operations = { + ast.Gt: lambda left, right: left > right, + ast.GtE: lambda left, right: left >= right, + ast.Lt: lambda left, right: left < right, + ast.LtE: lambda left, right: left <= right, + ast.Eq: lambda left, right: left == right, + ast.NotEq: lambda left, right: left != right, + } + for left, operator, right in zip(values[:-1], node.ops, values[1:], strict=True): + operation = operations.get(type(operator)) + if operation is None or not operation(left, right): + return False if operation is not None else None + return True + + +# ============================================================================ +# Public expression to completed roles +# ============================================================================ + + +class _ExtentRoleResolver(ast.NodeTransformer): + """Bind public-expression references to completed wrapper roles. + + The resolver snapshots case-insensitive scalar, array, and callable role + maps. Visitor methods mutate only this instance's result collections and + the visited AST; callers read ``references``, ``callables``, ``blockers``, + and ``changed`` after one complete traversal. + """ + + def __init__( + self, + scalar_roles: Mapping[str, tuple[str, str]], + array_roles: Mapping[str, tuple[str, tuple[str, ...]]], + callable_roles: Mapping[str, tuple[str, str]], + ) -> None: + """Snapshot role maps and initialize ordered per-expression results. + + Input mappings are not mutated. Dict insertion order records first use + for ``references`` and ``callables``; ``blockers`` preserves every + diagnostic occurrence until the public wrapper deduplicates it. + """ + self.scalar_roles = {name.casefold(): value for name, value in scalar_roles.items()} + self.array_roles = {name.casefold(): value for name, value in array_roles.items()} + self.callable_roles = {name.casefold(): value for name, value in callable_roles.items()} + self.references: dict[str, str] = {} + self.callables: dict[str, str] = {} + self.blockers: list[str] = [] + self.changed = False + + def visit_Name(self, node: ast.Name) -> ast.AST: + """Bind one scalar name or record that no visible integer role supplies it.""" + source = self.scalar_roles.get(node.id.casefold()) + if source is None: + self.blockers.append(node.id) + return node + self.references.setdefault(node.id, source[1]) + return node + + def visit_Attribute(self, node: ast.Attribute) -> ast.AST: + """Resolve ``array.size`` and ``array.ndim`` public properties.""" + if not isinstance(node.value, ast.Name): + self.blockers.append("") + return node + source = self.array_roles.get(node.value.id.casefold()) + if source is None: + self.blockers.append(node.value.id) + return node + source_name, roles = source + if node.attr == "size": + return self._extent_product(source_name, roles) + if node.attr == "ndim": + self.changed = True + return ast.copy_location(ast.Constant(len(roles)), node) + self.blockers.append("") + return node + + def visit_Subscript(self, node: ast.Subscript) -> ast.AST: + """Resolve one literal ``array.shape[index]`` reference.""" + value = node.value + if not ( + isinstance(value, ast.Attribute) + and value.attr == "shape" + and isinstance(value.value, ast.Name) + and isinstance(node.slice, ast.Constant) + and _is_integer_constant(node.slice) + ): + self.blockers.append("") + return node + source = self.array_roles.get(value.value.id.casefold()) + if source is None: + self.blockers.append(value.value.id) + return node + index = int(node.slice.value) + if index < 0: + index += len(source[1]) + if not 0 <= index < len(source[1]): + self.blockers.append("") + return node + return self._extent_token(source[0], source[1], index, node) + + def visit_Call(self, node: ast.Call) -> ast.AST: + """Resolve supported helpers or record an unsupported call by name. + + The resolver consumes a public Python-form call and either returns its + role-bound expression or leaves it unchanged while adding a blocker. + Naming an arbitrary specification function in that blocker is + important: policy must not confuse it with malformed expression syntax. + """ + # Stage 1: reject dynamic syntax before looking up any producer. + call_name = _qualified_call_name(node.func) + if call_name is None or node.keywords: + self.blockers.append("" if call_name is None else f"{call_name}()") + return node + resolved_callable = self._resolved_native_callable(call_name, node) + if resolved_callable is not None: + return resolved_callable + if not isinstance(node.func, ast.Name): + self.blockers.append(f"{call_name}()") + return node + name = node.func.id.casefold() + if name == "len": + return self._resolved_length_call(node) + if name in {"sum", "max", "min"} and len(node.args) == 1: + expanded = self._expanded_sequence_call(name, node.args[0], node) + if expanded is not None: + return expanded + return self._resolved_scalar_helper_call(name, node) + + def _resolved_native_callable(self, call_name: str, node: ast.Call) -> ast.AST | None: + """Return a tokenized call for one declaration callable, when registered. + + The callable-role mapping is the completed provenance input. A matching + call records the referenced callable and recursively resolves arguments; + no match returns None so built-in helper handling can continue. + """ + callable_source = self.callable_roles.get(call_name.casefold()) + if callable_source is None: + return None + token, role = callable_source + self.callables.setdefault(token, role) + self.changed = True + return ast.copy_location( + ast.Call( + func=ast.Name(id=token, ctx=ast.Load()), + args=[self.visit(argument) for argument in node.args], + keywords=[], + ), + node, + ) + + def _resolved_length_call(self, node: ast.Call) -> ast.AST: + """Resolve one len call to its first declared extent or record a blocker.""" + if len(node.args) != 1 or not isinstance(node.args[0], ast.Name): + self.blockers.append("") + return node + source = self.array_roles.get(node.args[0].id.casefold()) + if source is None or not source[1]: + self.blockers.append(node.args[0].id) + return node + return self._extent_token(source[0], source[1], 0, node) + + def _resolved_scalar_helper_call(self, name: str, node: ast.Call) -> ast.AST: + """Validate a scalar helper, resolve its arguments, or record its blocker. + + This helper receives only bare names after array-specific reductions have + been considered. It preserves existing invalid-call wording and performs + no semantic interpretation beyond the public helper grammar. + """ + if name not in _SUPPORTED_CALLS or (name in {"abs", "int"} and len(node.args) != 1): + self.blockers.append(f"{node.func.id}()") + return node + if name in {"min", "max"} and len(node.args) < 2: + self.blockers.append("") + return node + node.func.id = name + node.args = [self.visit(argument) for argument in node.args] + return node + + def _expanded_sequence_call(self, name: str, argument: ast.AST, node: ast.Call) -> ast.AST | None: + """Expand a fixed shape or constructor reduction to scalar helper syntax. + + The method returns ``None`` only when ``argument`` is not an expandable + shape or constructor. For a recognized but unavailable or empty source, + it records a blocker and returns the original call node unchanged. + """ + items: list[ast.AST] + if isinstance(argument, ast.Attribute) and argument.attr == "shape" and isinstance(argument.value, ast.Name): + source = self.array_roles.get(argument.value.id.casefold()) + if source is None: + self.blockers.append(argument.value.id) + return node + items = [self._extent_token(source[0], source[1], axis, node) for axis in range(len(source[1]))] + elif isinstance(argument, ast.Tuple | ast.List): + items = [self.visit(item) for item in argument.elts] + else: + return None + self.changed = True + if not items: + self.blockers.append("") + return node + if name == "sum": + return self._fold_binary(items, ast.Add()) + return ast.Call(ast.Name(name, ast.Load()), items, []) + + def _extent_product(self, source_name: str, roles: tuple[str, ...]) -> ast.AST: + """Return a product of role-backed axes, or a blocked placeholder for none.""" + if not roles: + self.blockers.append("") + return ast.Constant(0) + return self._fold_binary( + [self._extent_token(source_name, roles, axis, ast.Name(source_name)) for axis in range(len(roles))], + ast.Mult(), + ) + + def _extent_token(self, source_name: str, roles: tuple[str, ...], axis: int, node: ast.AST) -> ast.AST: + """Record and return one stable backend-neutral extent token. + + ``axis`` is already bounds-checked by the caller. The token's first-use + role is retained in ``references`` and the returned node copies the + input location for stable unparsing and diagnostics. + """ + token = f"__prik_extent_{source_name}_{axis}" + self.references.setdefault(token, roles[axis]) + self.changed = True + return ast.copy_location(ast.Name(token, ast.Load()), node) + + @staticmethod + def _fold_binary(items: list[ast.AST], operator: ast.operator) -> ast.AST: + """Fold nonempty items left-to-right with ``operator`` into a fresh AST.""" + result = items[0] + for item in items[1:]: + result = ast.BinOp(result, operator, item) + return result + + +# ============================================================================ +# Completed-expression validation and backend rendering +# ============================================================================ + + +def _valid_resolved_extent(tree: ast.AST, *, callable_names: frozenset[str] = frozenset()) -> bool: + """Return whether a role-bound AST belongs to the backend grammar. + + ``callable_names`` contains only tokens selected by completed policy. The + check accepts no attributes, subscripts, keywords, or dynamic calls, so a + backend can render the result without inferring declaration semantics. + """ + for node in ast.walk(tree): + if not isinstance(node, _RESOLVED_EXTENT_NODES) or not _is_integer_constant(node): + return False + if isinstance(node, ast.Call) and ( + not isinstance(node.func, ast.Name) + or node.func.id not in _SUPPORTED_CALLS | callable_names + or node.keywords + ): + return False + return True + + +def _is_integer_constant(node: ast.AST) -> bool: + """Accept integer and Boolean constants while rejecting other literal values.""" + return not isinstance(node, ast.Constant) or isinstance(node.value, bool | int) + + +class _ExtentRenderer: + """Render one validated, role-bound expression for a backend dialect. + + The renderer consumes only output accepted by :func:`_valid_resolved_extent`. + ``substitutions`` replaces completed policy tokens with backend-local names; + unknown names remain scalar locals so callers retain their original output. + """ + + def __init__(self, substitutions: Mapping[str, str], target: str) -> None: + """Store substitution lookups and a target already validated by the public API.""" + self.substitutions = substitutions + self.target = target + + def render(self, node: ast.AST) -> str: + """Render ``node`` recursively or raise ``ValueError`` for a policy-stage escape.""" + method = getattr(self, f"_render_{type(node).__name__}", None) + if method is None: + raise ValueError(f"unsupported completed declaration-expression node: {type(node).__name__}") + return method(node) + + def _render_Name(self, node: ast.Name) -> str: + """Substitute one completed role token or retain a scalar local name.""" + return self.substitutions.get(node.id, node.id) + + def _render_Constant(self, node: ast.Constant) -> str: + """Render integer and Boolean constants in the target dialect.""" + if isinstance(node.value, bool): + if self.target == "fortran": + return ".true." if node.value else ".false." + return "1" if node.value else "0" + return str(node.value) + + def _render_BinOp(self, node: ast.BinOp) -> str: + """Render arithmetic, modulo, and integer-power operations.""" + left = self._render_binary_operand(node.left, node.op, right=False) + right = self._render_binary_operand(node.right, node.op, right=True) + if isinstance(node.op, ast.Pow): + if self.target == "c": + return f"prik_extent_power(({left}), ({right}))" + return f"{left} ** {right}" + if isinstance(node.op, ast.Mod) and self.target == "fortran": + return f"mod(({left}), ({right}))" + operators = { + ast.Add: "+", + ast.Sub: "-", + ast.Mult: "*", + ast.Div: "/", + ast.FloorDiv: "/", + ast.Mod: "%", + } + return f"{left} {operators[type(node.op)]} {right}" + + def _render_binary_operand( + self, + node: ast.AST, + parent_operator: ast.operator, + *, + right: bool, + ) -> str: + """Render an operand and group it only when shared precedence requires it.""" + text = self.render(node) + if not isinstance(node, ast.BinOp): + return text + child_precedence = self._binary_precedence(node.op) + parent_precedence = self._binary_precedence(parent_operator) + same_precedence = child_precedence == parent_precedence + if isinstance(parent_operator, ast.Pow): + needs_grouping = child_precedence < parent_precedence or (not right and same_precedence) + else: + needs_grouping = child_precedence < parent_precedence or (right and same_precedence) + return f"({text})" if needs_grouping else text + + @staticmethod + def _binary_precedence(operator: ast.operator) -> int: + """Return the shared C/Fortran arithmetic precedence tier for ``operator``.""" + if isinstance(operator, ast.Pow): + return 3 + if isinstance(operator, ast.Mult | ast.Div | ast.FloorDiv | ast.Mod): + return 2 + return 1 + + def _render_UnaryOp(self, node: ast.UnaryOp) -> str: + """Render signed and logical unary operations.""" + operand = self.render(node.operand) + if isinstance(node.op, ast.Not): + operator = ".not." if self.target == "fortran" else "!" + return f"{operator} ({operand})" + operator = "-" if isinstance(node.op, ast.USub) else "+" + grouped = ( + f"({operand})" if isinstance(node.operand, ast.BinOp | ast.BoolOp | ast.Compare | ast.IfExp) else operand + ) + return f"{operator}{grouped}" + + def _render_BoolOp(self, node: ast.BoolOp) -> str: + """Render conjunctions and disjunctions with explicit grouping.""" + if self.target == "fortran": + operator = ".and." if isinstance(node.op, ast.And) else ".or." + else: + operator = "&&" if isinstance(node.op, ast.And) else "||" + return "(" + f" {operator} ".join(f"({self.render(value)})" for value in node.values) + ")" + + def _render_Compare(self, node: ast.Compare) -> str: + """Render one Python comparison chain as pairwise conjunctions.""" + operands = [node.left, *node.comparators] + operators = { + ast.Eq: "==" if self.target == "c" else ".eq.", + ast.NotEq: "!=" if self.target == "c" else ".ne.", + ast.Lt: "<" if self.target == "c" else ".lt.", + ast.LtE: "<=" if self.target == "c" else ".le.", + ast.Gt: ">" if self.target == "c" else ".gt.", + ast.GtE: ">=" if self.target == "c" else ".ge.", + } + comparisons = [ + f"(({self.render(left)}) {operators[type(operator)]} ({self.render(right)}))" + for left, operator, right in zip(operands[:-1], node.ops, operands[1:], strict=True) + ] + joiner = " .and. " if self.target == "fortran" else " && " + return "(" + joiner.join(comparisons) + ")" + + def _render_IfExp(self, node: ast.IfExp) -> str: + """Render Python conditional syntax as C ternary or Fortran ``merge``.""" + condition = self.render(node.test) + body = self.render(node.body) + otherwise = self.render(node.orelse) + if self.target == "fortran": + return f"merge(({body}), ({otherwise}), ({condition}))" + return f"(({condition}) ? ({body}) : ({otherwise}))" + + def _render_Call(self, node: ast.Call) -> str: + """Render one policy-approved pure integer helper call.""" + name = node.func.id + arguments = [self.render(argument) for argument in node.args] + if name not in _SUPPORTED_CALLS: + target = self.substitutions.get(name, name) + return f"{target}({', '.join(arguments)})" + if name == "int": + if self.target == "c": + return f"((npy_intp)({arguments[0]}))" + return f"int({arguments[0]})" + if name == "abs": + value = arguments[0] + if self.target == "fortran": + return f"abs({value})" + return f"(({value}) < 0 ? -({value}) : ({value}))" + if self.target == "fortran": + return f"{name}({', '.join(arguments)})" + comparison = ">" if name == "max" else "<" + result = arguments[0] + for argument in arguments[1:]: + result = f"(({result}) {comparison} ({argument}) ? ({result}) : ({argument}))" + return result + + +# ============================================================================ +# Public backend-rendering entrypoint +# ============================================================================ + + +def render_declaration_extent( + expression: str, + substitutions: Mapping[str, str], + *, + target: str, +) -> str: + """Render a completed role-token expression for ``c`` or ``fortran``. + + Use this after :func:`resolve_declaration_extent` and completed policy have + supplied backend-local substitutions. Runtime dimension markers are returned + unchanged for the caller's assumed-shape or rank-specific handling. + + Args: + expression: A public expression containing only validated role tokens. + substitutions: Maps completed policy tokens to backend-local values. + target: Either ``"c"`` or ``"fortran"``. + + Returns: + The expression rendered in the requested backend dialect. + + Raises: + ValueError: If ``target`` is unsupported or ``expression`` is not in + the completed renderable grammar. + """ + if target not in {"c", "fortran"}: + raise ValueError(f"unsupported declaration-expression target: {target!r}") + if expression in _RUNTIME_DIMENSIONS: + return expression + try: + node = ast.parse(expression, mode="eval").body + except SyntaxError as exc: + raise ValueError(f"invalid completed declaration expression: {expression!r}") from exc + return _ExtentRenderer(substitutions, target).render(node) + + +if __name__ == "__main__": + # A declared lower bound of zero makes the source-to-public translation + # visibly different from the original Fortran inquiry spelling. + fortran_expression = "ubound(source, 1) - lbound(source, 1) + 1" + source_arrays = {"source": ArrayExpressionSource(rank=2, lower_bounds=("0", "1"))} + public_expression = canonicalize_declaration_extent(fortran_extent_to_python(fortran_expression, source_arrays)) + resolved = resolve_declaration_extent( + public_expression, + scalar_roles={}, + array_roles={"source": ("source", ("source_extent_0", "source_extent_1"))}, + ) + rendered = render_declaration_extent( + resolved.expression, + {"__prik_extent_source_0": "native_source_extent_0"}, + target="fortran", + ) + + print(f"Fortran extent: {fortran_expression}") + print(f"Public expression: {public_expression}") + print(f"Role-bound expression: {resolved.expression}") + print(f"Fortran rendering: {rendered}") + print(f"Compile-time product: {evaluate_integer_expression('product((/ 2, 3 /))')}") diff --git a/pyproject.toml b/pyproject.toml index d5c0621cd..1859e0098 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [build-system] -requires = ["setuptools>=77.0.3", "wheel"] +requires = ["setuptools>=77.0.3"] build-backend = "setuptools.build_meta" [project] name = "prik" version = "0.1.1" -description = "Fortran-to-Python native extensions with editable .pyi contracts" +description = "PRIK generates native Python bindings from Fortran projects, producing importable extensions and editable .pyi contracts for Pythonic APIs." readme = "README.md" authors = [ { name = "Said Hadjout" , email = "saidaissa.hadjout@gmail.com"}, @@ -19,6 +19,10 @@ keywords = [ "cpython-extension", "numpy", "wrapper-generator", + "fortran-python", + "python-bindings", + "wrapper", + "f2py-alternative", "scientific-computing", ] classifiers = [ @@ -95,7 +99,7 @@ markers = [ "fortran_end_to_end: compiled, imported, and called Fortran feature tests", "fuzz: randomized parser robustness tests", "property: Hypothesis property-based tests", - "real_library: dedicated BLAS/LAPACK native-source end-to-end tests", + "real_library: opt-in full-library native-source numerical end-to-end tests", "regression: tests that pin previously fixed bugs", "slow: tests that are useful but too expensive for the default inner loop", "toolchain_smoke: portable compiled Fortran end-to-end cases reused across compiler, OS, and architecture lanes", diff --git a/tests/README.md b/tests/README.md index 01b495028..e2160a5cc 100644 --- a/tests/README.md +++ b/tests/README.md @@ -46,12 +46,22 @@ Within one Fortran feature, use only the stages that own real evidence: | `preprocessing/` | Source processing, dependencies, and mappings are correct | | `semantics/` | Parser or `.pyi` facts become the intended semantic IR | | `policy/` | Ownership, lifetime, projection, mutation, nullability, storage, and accessor decisions are complete | -| `wrapper_codegen/` | Completed policy selects a typed plan and named bridge/binding mechanisms | +| `codegen/` | Completed policy selects a typed plan and named bridge/binding mechanisms | | `compiling/` | Commands, objects, libraries, and link inputs are correct | | `pipeline/` | Build stages and generated artifacts transition correctly | | `runtime/` | Runtime support mechanisms behave correctly without owning a complete feature journey | | `end_to_end/` | Source or intentional `.pyi` input produces an imported extension whose public behavior is called and verified | +## Declaration-expression evidence + +Array declaration expressions have deliberate vertical coverage. The arrays +semantic tests preserve names, imports, and native callable provenance; the +arrays policy tests classify dependency roles and unsupported native calls; and +the arrays end-to-end tests compile representative dimensions, inquiry forms, +reductions, conditionals, powers, and logical-kind arrays. Contract-batch +reconciliation belongs with `tests/fortran/semantic_pyi_format/`, where +editable `.pyi` imports and prototypes are exercised. + Public cross-feature capabilities have explicit owners: `source_parsing/`, `source_preprocessing/`, `command_line_interface/`, and `semantic_ir/`. Only internal frameworks with no honest public-capability owner diff --git a/tests/architecture/fortran/test_contract_coverage_map.py b/tests/architecture/fortran/test_contract_coverage_map.py index 630781068..92fa37e20 100644 --- a/tests/architecture/fortran/test_contract_coverage_map.py +++ b/tests/architecture/fortran/test_contract_coverage_map.py @@ -36,7 +36,7 @@ "preprocessing", "semantics", "policy", - "wrapper_codegen", + "codegen", "compiling", "pipeline", "import", diff --git a/tests/architecture/fortran/test_language_ownership.py b/tests/architecture/fortran/test_language_ownership.py index aeadb60db..982b8f7eb 100644 --- a/tests/architecture/fortran/test_language_ownership.py +++ b/tests/architecture/fortran/test_language_ownership.py @@ -49,7 +49,7 @@ "preprocessing", "semantics", "policy", - "wrapper_codegen", + "codegen", "compiling", "pipeline", "runtime", @@ -62,7 +62,7 @@ } INFRASTRUCTURE_OWNERS = { "infrastructure/policy/", - "infrastructure/wrapper_codegen/", + "infrastructure/codegen/", } FEATURE_ROW = re.compile( r"^\| \[(?P[ x])\] \| \[[^]]+\]\((?P[^)]+)\) " diff --git a/tests/c/_support/fixture_outputs.py b/tests/c/_support/fixture_outputs.py index 03f1cffc3..ac4c5b062 100644 --- a/tests/c/_support/fixture_outputs.py +++ b/tests/c/_support/fixture_outputs.py @@ -10,7 +10,7 @@ from prik.parsers.c.cli import attach_preprocessing_recipe from prik.pipeline.preprocessing import PreprocessingConfig, preprocess_source from prik.semantics.c2ir import c_project_to_semantic_module -from prik.wrapper_codegen.printers import emit_module +from prik.codegen.printers import emit_module C_ROOT = Path(__file__).resolve().parents[1] diff --git a/tests/c/fixtures/parser/README.md b/tests/c/fixtures/parser/README.md index 9331825db..d82391464 100644 --- a/tests/c/fixtures/parser/README.md +++ b/tests/c/fixtures/parser/README.md @@ -6,7 +6,7 @@ Guidelines: - keep these tests separate from the Fortran parser tests - keep wrapper-plan support diagnostics under the owning Fortran feature's - `wrapper_codegen/` stage, not under C parser tests + `codegen/` stage, not under C parser tests - add parser snapshots only when the corresponding schema and preprocessing recipe are stable - keep the checked-in cJSON regression inputs active while a separately pinned diff --git a/tests/c/parsing/test_c_lexer_preprocessor.py b/tests/c/parsing/test_c_lexer_preprocessor.py index 1c07c3852..bc42e2c61 100644 --- a/tests/c/parsing/test_c_lexer_preprocessor.py +++ b/tests/c/parsing/test_c_lexer_preprocessor.py @@ -1,8 +1,30 @@ """C lexer and lightweight preprocessing coverage.""" +import subprocess +import sys +from pathlib import Path + import pytest +def test_c_preprocessor_module_direct_execution_example(): + """Run the raw-directive metadata example from the repository root.""" + repository_root = Path(__file__).parents[3] + + result = subprocess.run( + [sys.executable, "prik/parsers/c/preprocessor.py"], + cwd=repository_root, + capture_output=True, + text=True, + check=True, + ) + + assert result.stdout == ( + "Raw directive: #pragma once\nIncludes: local state.h, system stddef.h\nDiagnostic: C_UNRESOLVED_INCLUDE\n" + "Resolved include: state.h (diagnostics: 0)\n" + ) + + def test_lexer_removes_comments_without_changing_string_or_char_literals(): from prik.parsers.c.lexer import lex_c_source diff --git a/tests/c/parsing/test_c_project_resolution.py b/tests/c/parsing/test_c_project_resolution.py index b1c1699a5..442fee254 100644 --- a/tests/c/parsing/test_c_project_resolution.py +++ b/tests/c/parsing/test_c_project_resolution.py @@ -1,8 +1,26 @@ """Active coverage for current C project include/index behavior.""" +import subprocess +import sys from pathlib import Path +def test_c_type_resolver_module_direct_execution_example(): + repository_root = Path(__file__).parents[3] + + result = subprocess.run( + [sys.executable, "prik/parsers/c/type_resolver.py"], + cwd=repository_root, + capture_output=True, + text=True, + check=True, + ) + + assert result.stdout == ( + "Tag reference:\nstate_handle -> struct state\nTypedef chain:\nstate_alias -> raw_state -> struct state\n" + ) + + def test_project_include_graph_tracks_local_system_missing_and_cycles(tmp_path: Path): from prik.parsers.c import parse_c_project diff --git a/tests/c/parsing/test_c_public_api_skeleton.py b/tests/c/parsing/test_c_public_api_skeleton.py index d176d89e4..d1eb5da2f 100644 --- a/tests/c/parsing/test_c_public_api_skeleton.py +++ b/tests/c/parsing/test_c_public_api_skeleton.py @@ -1,8 +1,31 @@ """C parser public API coverage for the current partial subset.""" +import subprocess +import sys from pathlib import Path +def test_c_parser_module_direct_execution_example(): + """Run the documented source-to-`CFile` example from the repository root.""" + repository_root = Path(__file__).parents[3] + + result = subprocess.run( + [sys.executable, "prik/parsers/c/parser.py"], + cwd=repository_root, + capture_output=True, + text=True, + check=True, + ) + + assert result.stdout == ( + "Parsed: state_api.h\n" + "Typedef: api_size -> unsigned long\n" + "Struct: state (id)\n" + "Function: count() -> api_size\n" + "Function: step(value) -> pointer to struct state\n" + ) + + def test_c_parser_path_and_include_key_helpers_preserve_boundary_contracts(monkeypatch): from prik.parsers.c.parser import _include_key_from_current, _looks_like_existing_source_path diff --git a/tests/c/pipeline/test_c_pyi_contract_fixtures.py b/tests/c/pipeline/test_c_pyi_contract_fixtures.py index 8e1a67264..7f89b4654 100644 --- a/tests/c/pipeline/test_c_pyi_contract_fixtures.py +++ b/tests/c/pipeline/test_c_pyi_contract_fixtures.py @@ -11,7 +11,7 @@ iter_general_c_fixture_projects, ) from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text -from prik.wrapper_codegen.printers import emit_module +from prik.codegen.printers import emit_module C_FIXTURE_PROJECTS = iter_general_c_fixture_projects() diff --git a/tests/c/preprocessing/test_c_preprocessing_execution.py b/tests/c/preprocessing/test_c_preprocessing_execution.py index 8d167753f..a21fb6f27 100644 --- a/tests/c/preprocessing/test_c_preprocessing_execution.py +++ b/tests/c/preprocessing/test_c_preprocessing_execution.py @@ -9,9 +9,51 @@ pytest, run_compiler_preprocessor, run_compiler_preprocessor_with_recipe, + subprocess, + sys, ) +def test_preprocessing_module_direct_execution_example(): + repository_root = Path(__file__).parents[3] + + result = subprocess.run( + [sys.executable, "prik/pipeline/preprocessing.py"], + cwd=repository_root, + capture_output=True, + text=True, + check=True, + ) + + assert result.stdout == ( + "Before Fortran include expansion:\n" + "module greeting\n" + "include 'constants.inc'\n" + "contains\n" + "subroutine show_answer()\n" + "print *, answer\n" + "end subroutine show_answer\n" + "end module greeting\n" + "\n" + "After Fortran include expansion:\n" + "module greeting\n" + "integer, parameter :: answer = 42\n" + "contains\n" + "subroutine show_answer()\n" + "print *, answer\n" + "end subroutine show_answer\n" + "end module greeting\n" + "Native includes: 1; diagnostics: 0\n" + "\n" + "Before C compiler preprocessing:\n" + '#include "state.h"\n' + "int state_id = STATE_ID;\n" + "\n" + "After C compiler preprocessing:\n" + "int state_id = 42;\n" + ) + + def test_run_compiler_preprocessor_success_and_failures(monkeypatch, tmp_path: Path): config = PreprocessingConfig(mode="compiler", compiler="cc") source = tmp_path / "api.c" diff --git a/tests/c/probes/test_c_types.py b/tests/c/probes/test_c_types.py index 779e8e0d8..abf2b7f31 100644 --- a/tests/c/probes/test_c_types.py +++ b/tests/c/probes/test_c_types.py @@ -5,6 +5,7 @@ import shutil import subprocess import sys +from pathlib import Path from types import SimpleNamespace import pytest @@ -328,3 +329,19 @@ def test_c_standard_type_probe_module_cli_emits_json_for_semantic_input(): assert payload["types"]["FILE"]["kind"] == "opaque_handle" assert payload["recipe"]["compiler"] == compiler assert payload["source_text"].startswith("#include ") + + +def test_c_standard_type_probe_direct_script_runs_its_no_argument_example(): + completed = subprocess.run( + [sys.executable, "prik/probes/c_types.py"], + cwd=Path(__file__).resolve().parents[3], + capture_output=True, + text=True, + check=True, + ) + + label, separator, raw_value = completed.stdout.strip().partition(": ") + assert label == "int" + assert separator == ": " + assert raw_value.endswith("-bit signed") + assert int(raw_value.removesuffix("-bit signed")) >= 16 diff --git a/tests/c/semantics/conversion/_support.py b/tests/c/semantics/conversion/_support.py index e07760750..ea4654658 100644 --- a/tests/c/semantics/conversion/_support.py +++ b/tests/c/semantics/conversion/_support.py @@ -79,7 +79,7 @@ from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text -from prik.wrapper_codegen.printers import emit_module, emit_module_stubs +from prik.codegen.printers import emit_module, emit_module_stubs def _function(module, name): diff --git a/tests/c/semantics/conversion/test_projects_and_diagnostics.py b/tests/c/semantics/conversion/test_projects_and_diagnostics.py index a10660e7b..c89771d46 100644 --- a/tests/c/semantics/conversion/test_projects_and_diagnostics.py +++ b/tests/c/semantics/conversion/test_projects_and_diagnostics.py @@ -1,5 +1,9 @@ """Tests split by stable ownership concept from `test_functions_and_callbacks.py`.""" +from pathlib import Path +import subprocess +import sys + from tests.c.semantics.conversion._support import ( CEnum, CFile, @@ -33,6 +37,18 @@ ) +def test_c_to_ir_direct_script_runs_its_no_argument_example(): + completed = subprocess.run( + [sys.executable, "prik/semantics/c2ir.py"], + cwd=Path(__file__).resolve().parents[4], + capture_output=True, + text=True, + check=True, + ) + + assert completed.stdout.strip() == "math.scale(value): Int <- Int" + + def test_c2ir_explicit_project_headers_import_types_from_their_owner_module(): project = parse_c_project( { diff --git a/tests/c/semantics/conversion/test_types_and_constants.py b/tests/c/semantics/conversion/test_types_and_constants.py index 5d4090f4f..9ea5892ec 100644 --- a/tests/c/semantics/conversion/test_types_and_constants.py +++ b/tests/c/semantics/conversion/test_types_and_constants.py @@ -342,7 +342,7 @@ def test_c2ir_preserves_c_int_identity_and_stores_compiler_probed_precision(): (CUnsignedLong(), "unsigned long", {"kind": "integer", "signed": False, "bits": 32}, "UInt32"), (CLongDouble(), "long double", {"kind": "real", "bits": 64}, "Float64"), (CLongDoubleComplex(), "long double _Complex", {"kind": "complex", "bits": 128}, "Complex128"), - (CBool(), "_Bool", {"kind": "bool", "bits": 8}, "Bool"), + (CBool(), "_Bool", {"kind": "bool", "bits": 8}, "Bool8"), ], ) def test_c2ir_uses_compiler_probed_primitive_abi_facts(ctype, primitive, fact, expected): diff --git a/tests/fortran/CONTRACT_COVERAGE.md b/tests/fortran/CONTRACT_COVERAGE.md index 706bcc0c4..14b20f9d3 100644 --- a/tests/fortran/CONTRACT_COVERAGE.md +++ b/tests/fortran/CONTRACT_COVERAGE.md @@ -41,75 +41,75 @@ Authoritative sources: | [CLI Commands: Parse And Semantics](../../docs/user/reference/cli-commands.md#parse-and-semantics) | Supported | public parser module and top-level command modes; parse, semantics, `.pyi`, and diagnostic dispatch | `tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py::test_fortran_parser_main_public_api_modes_from_inline_source` | — | — | canonical | | [Semantic IR: Round Trips And Provenance](../../docs/user/reference/semantic-ir.md#round-trips-and-provenance) | Supported | deterministic source-to-IR conversion; preserved wrapper-relevant facts; checked fixture serialization | `tests/fortran/semantic_ir/semantics/test_fortran_conversion_properties.py::test_generated_fortran_ast_to_semantic_ir_is_deterministic` | — | — | canonical | | [Data Types: Example](../../docs/user/guide/data-types.md#example) | Supported | source generation; reviewed generated `.pyi`; source build; generated-`.pyi` replay | `tests/fortran/data_types/pipeline/test_generated_scalar_contract.py::test_generated_primitive_scalar_contract_matches_reviewed_package` | `tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[source]`
`tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[generated-pyi]` | — | canonical | -| [Data Types: Calling from Python](../../docs/user/guide/data-types.md#calling-from-python) | Supported | signed integer; real; complex; Boolean; exact visible values | `tests/fortran/data_types/wrapper_codegen/test_primitive_scalar_result_lowering.py::test_direct_scalar_result_registry_projects_supported_type_facts[Complex128-NPY_COMPLEX128-python]` | `tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[source]` | — | canonical | -| [Data Types: Scalar Type Mapping](../../docs/user/guide/data-types.md#scalar-type-mapping) | Supported | `Bool`; `Int8/16/32/64`; `Float32/64`; `Complex64/128`; intrinsic, ISO environment, and ISO C kinds | `tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py::test_intrinsic_builtin_kinds_map_to_semantic_types`
`tests/fortran/data_types/probes/test_fortran_type_probes.py::test_fortran_type_probe_evaluates_collected_semantic_requirements` | `tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[source]` | — | canonical | +| [Data Types: Calling from Python](../../docs/user/guide/data-types.md#calling-from-python) | Supported | signed integer; real; complex; Boolean; exact visible values | `tests/fortran/data_types/codegen/test_primitive_scalar_result_lowering.py::test_direct_scalar_result_registry_projects_supported_type_facts[Complex128-NPY_COMPLEX128-python]` | `tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[source]` | — | canonical | +| [Data Types: Scalar Type Mapping](../../docs/user/guide/data-types.md#scalar-type-mapping) | Supported | `Bool`/`Bool8/16/32/64`; `Int8/16/32/64`; `Float32/64`; `Complex64/128`; compiler-probed intrinsic, ISO environment, and ISO C kinds | `tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py::test_intrinsic_builtin_kinds_map_to_semantic_types`
`tests/fortran/data_types/probes/test_fortran_type_probes.py::test_fortran_type_probe_evaluates_collected_semantic_requirements`
`tests/fortran/data_types/probes/test_fortran_type_probes.py::test_fortran_type_probe_resolves_supported_logical_storage_widths` | `tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[source]` | — | canonical | | [Data Types: Runtime Default Constructors](../../docs/user/guide/data-types.md#runtime-default-constructors) | Supported | Boolean; fixed-width signed/unsigned integer, real, and complex; `SizeT`; zero values | `tests/fortran/data_types/runtime/test_contract_scalar_constructors.py::test_concrete_primitive_default_constructors_return_zero_numpy_scalars` | `tests/fortran/data_types/runtime/test_contract_scalar_constructors.py::test_concrete_primitive_default_constructors_return_zero_numpy_scalars` | `tests/fortran/data_types/runtime/test_contract_scalar_constructors.py::test_primitive_contract_constructors_reject_values_and_array_annotations` (`runtime`) | canonical | -| [Data Types: Important Rules](../../docs/user/guide/data-types.md#important-rules) | Supported | exact NumPy scalar acceptance; Python `int`/`float` rejection; wrong NumPy dtype rejection; compiler-resolved kinds | `tests/fortran/data_types/wrapper_codegen/test_primitive_scalar_input_lowering.py::test_scalar_input_registry_lowers_completed_type_into_the_native_support_api[Int32-int32_t-NPY_INT32]` | `tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[source]` | — | canonical | -| [Data Types: Values And Native Storage](../../docs/user/guide/data-types.md#values-and-native-storage) | Supported | bare scalar values; immutable replacement; `Int32[()]`; `Float64[()]`; direct and hidden rank-zero results; wrong dtype and read-only rejection | `tests/fortran/data_types/wrapper_codegen/test_primitive_scalar_input_lowering.py::test_scalar_input_registry_lowers_completed_type_into_the_native_support_api[Float64-double-NPY_FLOAT64]` | `tests/fortran/data_types/end_to_end/test_rank_zero_scalar_storage.py::test_scalar_values_and_rank_zero_storage_cross_the_native_boundary` | — | canonical | +| [Data Types: Important Rules](../../docs/user/guide/data-types.md#important-rules) | Supported | exact NumPy scalar acceptance; Python `int`/`float` rejection; wrong NumPy dtype rejection; compiler-resolved kinds | `tests/fortran/data_types/codegen/test_primitive_scalar_input_lowering.py::test_scalar_input_registry_lowers_completed_type_into_the_native_support_api[Int32-int32_t-NPY_INT32]` | `tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[source]` | — | canonical | +| [Data Types: Values And Native Storage](../../docs/user/guide/data-types.md#values-and-native-storage) | Supported | bare scalar values; immutable replacement; `Int32[()]`; `Float64[()]`; direct and hidden rank-zero results; wrong dtype and read-only rejection | `tests/fortran/data_types/codegen/test_primitive_scalar_input_lowering.py::test_scalar_input_registry_lowers_completed_type_into_the_native_support_api[Float64-double-NPY_FLOAT64]` | `tests/fortran/data_types/end_to_end/test_rank_zero_scalar_storage.py::test_scalar_values_and_rank_zero_storage_cross_the_native_boundary` | — | canonical | | [Data Types: Unsupported Widths And Forms](../../docs/user/guide/data-types.md#unsupported-widths-and-forms) | Blocked | real wider than 64 bits; complex wider than 128 total bits; unsupported logical storage; unknown compiler-probed widths | — | — | `tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py::test_unsupported_intrinsic_widths_fail_in_semantic_conversion` (`semantics`)
`tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py::test_compiler_probed_unknown_storage_widths_fail_in_semantic_conversion` (`semantics`) | canonical | | [Arrays: Complete Example](../../docs/user/guide/arrays.md#complete-example) | Supported | source build; visible extents; lower bound; assumed size; positive strides; automatic result; optional and no-`intent` storage | `tests/fortran/arrays/parsing/test_array_declarations_and_shapes.py::test_dimension_attribute_with_mixed_bounds_is_parsed`
`tests/fortran/arrays/semantics/test_array_semantics.py::test_explicit_bound_ranges_remain_shaped_storage_contracts` | `tests/fortran/arrays/end_to_end/test_documented_array_journey.py::test_documented_array_source_build_validates_layout_flat_strides_mutation_and_results` | — | canonical | | [Arrays: Python Usage](../../docs/user/guide/arrays.md#python-usage) | Supported | `float64`; Fortran-order mutation; lower-bound normalization; flattened rank; returned NumPy storage | — | `tests/fortran/arrays/end_to_end/test_documented_array_journey.py::test_documented_array_source_build_validates_layout_flat_strides_mutation_and_results` | — | canonical | -| [Arrays: What prik Validates](../../docs/user/guide/arrays.md#what-prik-validates) | Supported | exact dtype; rank; shape; layout; contiguity; alignment; byte order; writeability; positive strides; broadcasting and reversal rejection; zero size | `tests/fortran/arrays/wrapper_codegen/test_dense_array_shape_lowering.py::test_dense_array_lowering_uses_planned_shape_checks_and_bridge_orientation`
`tests/fortran/arrays/wrapper_codegen/test_strided_array_lowering.py::test_strided_array_lowering_validates_and_passes_one_explicit_bridge_slice` | `tests/fortran/arrays/end_to_end/test_array_contract_validation.py::test_remaining_array_contracts_are_validated_before_fortran_calls[source]` | `tests/fortran/arrays/end_to_end/test_layout_and_strided_arrays.py::test_rank2_assumed_shape_rejects_non_positive_strides[source]` (`runtime`)
`tests/fortran/arrays/end_to_end/test_documented_array_journey.py::test_documented_array_source_build_validates_layout_flat_strides_mutation_and_results` (`runtime`) | canonical | +| [Arrays: What prik Validates](../../docs/user/guide/arrays.md#what-prik-validates) | Supported | exact dtype; rank; shape; layout; contiguity; alignment; byte order; writeability; positive strides; broadcasting and reversal rejection; zero size | `tests/fortran/arrays/codegen/test_dense_array_shape_lowering.py::test_dense_array_lowering_uses_planned_shape_checks_and_bridge_orientation`
`tests/fortran/arrays/codegen/test_strided_array_lowering.py::test_strided_array_lowering_validates_and_passes_one_explicit_bridge_slice` | `tests/fortran/arrays/end_to_end/test_array_contract_validation.py::test_remaining_array_contracts_are_validated_before_fortran_calls[source]` | `tests/fortran/arrays/end_to_end/test_layout_and_strided_arrays.py::test_rank2_assumed_shape_rejects_non_positive_strides[source]` (`runtime`)
`tests/fortran/arrays/end_to_end/test_documented_array_journey.py::test_documented_array_source_build_validates_layout_flat_strides_mutation_and_results` (`runtime`) | canonical | | [Arrays: Layout Fortran First](../../docs/user/guide/arrays.md#layout-fortran-first) | Supported | dense rank two and three; explicit and assumed shape; Fortran contiguity | `tests/fortran/arrays/semantics/test_array_semantics.py::test_matrix_semantics` | `tests/fortran/arrays/end_to_end/test_layout_and_strided_arrays.py::test_rank2_contiguous_contract_requires_fortran_contiguous[source]` | `tests/fortran/arrays/end_to_end/test_layout_and_strided_arrays.py::test_rank3_contiguous_contract_requires_fortran_contiguous[source]` (`runtime`) | canonical | -| [Arrays: C-order Arrays](../../docs/user/guide/arrays.md#c-order-arrays) | Supported | edited semantic `.pyi`; direct `ORDER_C`; `ORDER_C` plus `COPY_F`; caller-visible copyback semantics | `tests/fortran/arrays/wrapper_codegen/test_dense_array_shape_lowering.py::test_copy_f_is_one_binding_owned_transformation_lifecycle` | `tests/fortran/arrays/end_to_end/test_edited_pyi_layout_contract.py::test_edited_pyi_selects_direct_c_storage_or_fortran_copy_semantics` | `tests/fortran/arrays/wrapper_codegen/test_dense_array_shape_lowering.py::test_copy_f_layer_edit_fails_central_validation` (`wrapper_codegen`) | canonical | -| [Arrays: Flat Storage](../../docs/user/guide/arrays.md#flat-storage) | Supported | ranks 1-15; order-dependent contiguous storage; flat final axis with checked prefix; zero-sized storage | `tests/fortran/arrays/wrapper_codegen/test_dense_array_shape_lowering.py::test_dense_array_plan_records_extent_dependencies_flat_storage_and_order` | `tests/fortran/arrays/end_to_end/test_documented_array_journey.py::test_documented_array_source_build_validates_layout_flat_strides_mutation_and_results` | `tests/fortran/arrays/end_to_end/test_documented_array_journey.py::test_documented_array_source_build_validates_layout_flat_strides_mutation_and_results` (`runtime`) | canonical | -| [Arrays: Strided Views](../../docs/user/guide/arrays.md#strided-views) | Supported | dense and positive-stride rank two and three input/output; extent and element-stride handoff | `tests/fortran/arrays/wrapper_codegen/test_strided_array_lowering.py::test_strided_array_plan_names_bounds_and_element_strides_explicitly` | `tests/fortran/arrays/end_to_end/test_layout_and_strided_arrays.py::test_rank2_assumed_shape_accepts_fortran_ordered_strided_views[source]`
`tests/fortran/arrays/end_to_end/test_layout_and_strided_arrays.py::test_rank3_assumed_shape_accepts_fortran_ordered_strided_views[source]` | `tests/fortran/arrays/end_to_end/test_layout_and_strided_arrays.py::test_rank2_assumed_shape_rejects_non_positive_strides[source]` (`runtime`) | canonical | -| [Arrays: Mutation and Results](../../docs/user/guide/arrays.md#mutation-and-results) | Supported | input; caller output; in-place `intent(inout)`; no-`intent`; immutable replacement; optional presence; fixed, automatic, rank 1-15, and zero-size results | `tests/fortran/arrays/wrapper_codegen/test_array_result_lowering.py::test_array_results_record_producer_shape_copy_ownership_and_shared_hidden_slot`
`tests/fortran/arrays/wrapper_codegen/test_array_output_identity.py::test_projected_array_identity_uses_one_completed_in_place_copy_out_action` | `tests/fortran/arrays/end_to_end/test_array_results.py::test_array_results_follow_data_buffer_and_descriptor_handle_contracts[source]`
`tests/fortran/arrays/end_to_end/test_edited_pyi_layout_contract.py::test_edited_pyi_selects_direct_c_storage_or_fortran_copy_semantics` | — | canonical | -| [Arrays: Common Array Contracts](../../docs/user/guide/arrays.md#common-array-contracts) | Supported | every primitive dtype at concrete ranks 1-15; rank zero separately; assumed rank 1-15; dense, C-order, copy, flat, strided, fixed/open extents | `tests/fortran/arrays/semantics/test_array_semantics.py::test_array_constraints`
`tests/fortran/arrays/wrapper_codegen/test_array_buffer_lowering.py::test_required_array_buffer_has_one_printable_editable_handoff_plan` | `tests/fortran/arrays/end_to_end/test_primitive_dtype_rank_matrix.py::test_every_primitive_dtype_at_every_concrete_rank_mutates_exact_storage`
`tests/fortran/arrays/end_to_end/test_assumed_rank_arrays.py::test_assumed_rank_arguments_dispatch_to_runtime_rank[source]`
`tests/fortran/data_types/end_to_end/test_rank_zero_scalar_storage.py::test_scalar_values_and_rank_zero_storage_cross_the_native_boundary` | `tests/fortran/arrays/end_to_end/test_assumed_rank_arrays.py::test_assumed_rank_arguments_dispatch_to_runtime_rank[source]` (`runtime`) | canonical | +| [Arrays: C-order Arrays](../../docs/user/guide/arrays.md#c-order-arrays) | Supported | edited semantic `.pyi`; direct `ORDER_C`; `ORDER_C` plus `COPY_F`; caller-visible copyback semantics | `tests/fortran/arrays/codegen/test_dense_array_shape_lowering.py::test_copy_f_is_one_binding_owned_transformation_lifecycle` | `tests/fortran/arrays/end_to_end/test_edited_pyi_layout_contract.py::test_edited_pyi_selects_direct_c_storage_or_fortran_copy_semantics` | `tests/fortran/arrays/codegen/test_dense_array_shape_lowering.py::test_copy_f_layer_edit_fails_central_validation` (`codegen`) | canonical | +| [Arrays: Flat Storage](../../docs/user/guide/arrays.md#flat-storage) | Supported | ranks 1-15; order-dependent contiguous storage; flat final axis with checked prefix; zero-sized storage | `tests/fortran/arrays/codegen/test_dense_array_shape_lowering.py::test_dense_array_plan_records_extent_dependencies_flat_storage_and_order` | `tests/fortran/arrays/end_to_end/test_documented_array_journey.py::test_documented_array_source_build_validates_layout_flat_strides_mutation_and_results` | `tests/fortran/arrays/end_to_end/test_documented_array_journey.py::test_documented_array_source_build_validates_layout_flat_strides_mutation_and_results` (`runtime`) | canonical | +| [Arrays: Strided Views](../../docs/user/guide/arrays.md#strided-views) | Supported | dense and positive-stride rank two and three input/output; extent and element-stride handoff | `tests/fortran/arrays/codegen/test_strided_array_lowering.py::test_strided_array_plan_names_bounds_and_element_strides_explicitly` | `tests/fortran/arrays/end_to_end/test_layout_and_strided_arrays.py::test_rank2_assumed_shape_accepts_fortran_ordered_strided_views[source]`
`tests/fortran/arrays/end_to_end/test_layout_and_strided_arrays.py::test_rank3_assumed_shape_accepts_fortran_ordered_strided_views[source]` | `tests/fortran/arrays/end_to_end/test_layout_and_strided_arrays.py::test_rank2_assumed_shape_rejects_non_positive_strides[source]` (`runtime`) | canonical | +| [Arrays: Mutation and Results](../../docs/user/guide/arrays.md#mutation-and-results) | Supported | input; caller output; in-place `intent(inout)`; no-`intent`; immutable replacement; optional presence; fixed, automatic, rank 1-15, and zero-size results; exact-kind Boolean copy-in/copy-out | `tests/fortran/arrays/codegen/test_array_result_lowering.py::test_array_results_record_producer_shape_copy_ownership_and_shared_hidden_slot`
`tests/fortran/arrays/codegen/test_array_output_identity.py::test_projected_array_identity_uses_one_completed_in_place_copy_out_action` | `tests/fortran/arrays/end_to_end/test_array_results.py::test_array_results_follow_data_buffer_and_descriptor_handle_contracts[source]`
`tests/fortran/arrays/end_to_end/test_edited_pyi_layout_contract.py::test_edited_pyi_selects_direct_c_storage_or_fortran_copy_semantics`
`tests/fortran/arrays/end_to_end/test_logical_kind_array_conversions.py::test_boolean_arrays_copy_only_in_required_directions_for_every_supported_width` | — | canonical | +| [Arrays: Common Array Contracts](../../docs/user/guide/arrays.md#common-array-contracts) | Supported | every primitive dtype at concrete ranks 1-15; rank zero separately; assumed rank 1-15; dense, C-order, copy, flat, strided, fixed/open extents | `tests/fortran/arrays/semantics/test_array_semantics.py::test_array_constraints`
`tests/fortran/arrays/codegen/test_array_buffer_lowering.py::test_required_array_buffer_has_one_printable_editable_handoff_plan` | `tests/fortran/arrays/end_to_end/test_primitive_dtype_rank_matrix.py::test_every_primitive_dtype_at_every_concrete_rank_mutates_exact_storage`
`tests/fortran/arrays/end_to_end/test_assumed_rank_arrays.py::test_assumed_rank_arguments_dispatch_to_runtime_rank[source]`
`tests/fortran/data_types/end_to_end/test_rank_zero_scalar_storage.py::test_scalar_values_and_rank_zero_storage_cross_the_native_boundary` | `tests/fortran/arrays/end_to_end/test_assumed_rank_arrays.py::test_assumed_rank_arguments_dispatch_to_runtime_rank[source]` (`runtime`) | canonical | | [Strings: Choose A String Boundary](../../docs/user/guide/strings.md#choose-a-string-boundary) | Supported | runtime-length `String`; fixed scalar value/replacement/discard; rank-zero `S8`; rank-one `S8`; raw address deferred to Raw Addresses | `tests/fortran/strings/semantics/test_string_pyi_semantics.py::test_string_length_and_shape_axes_round_trip`
`tests/fortran/strings/policy/test_string_wrapper_policy.py::test_wrapper_policy_completes_fixed_string_replacement_and_discarded_identity` | `tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays`
`tests/fortran/strings/end_to_end/test_character_edge_cases.py::test_fixed_string_replacement_and_identity_use_canonical_plan` | — | canonical | | [Strings: Complete Example](../../docs/user/guide/strings.md#complete-example) | Supported | source compilation; edited semantic `.pyi`; fixed scalar input/result; mutable scalar storage; fixed-width array input/output/inout | `tests/fortran/strings/pipeline/test_generated_string_contracts.py::test_string_generated_pyi_contract_matches_fixture[fstrings_f90]` | `tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays` | — | canonical | -| [Strings: Immutable Values](../../docs/user/guide/strings.md#immutable-values) | Supported | exact encoded width; trailing blanks; replacement identity; discarded mutation; hidden and direct results; allocation failure | `tests/fortran/strings/wrapper_codegen/test_fixed_string_writeback.py::test_fixed_replacement_projects_completed_argument_and_lifecycle_facts`
`tests/fortran/strings/wrapper_codegen/test_fixed_string_result_lowering.py::test_fixed_strings_reuse_ordered_result_plans_with_completed_length_and_copy_facts` | `tests/fortran/strings/end_to_end/test_character_edge_cases.py::test_fortran_character_edge_cases_follow_copy_in_copy_out_policy[source]` | `tests/fortran/strings/end_to_end/test_character_edge_cases.py::test_fortran_character_edge_cases_follow_copy_in_copy_out_policy[source]` (`runtime`) | canonical | +| [Strings: Immutable Values](../../docs/user/guide/strings.md#immutable-values) | Supported | exact encoded width; trailing blanks; replacement identity; discarded mutation; hidden and direct results; allocation failure | `tests/fortran/strings/codegen/test_fixed_string_writeback.py::test_fixed_replacement_projects_completed_argument_and_lifecycle_facts`
`tests/fortran/strings/codegen/test_fixed_string_result_lowering.py::test_fixed_strings_reuse_ordered_result_plans_with_completed_length_and_copy_facts` | `tests/fortran/strings/end_to_end/test_character_edge_cases.py::test_fortran_character_edge_cases_follow_copy_in_copy_out_policy[source]` | `tests/fortran/strings/end_to_end/test_character_edge_cases.py::test_fortran_character_edge_cases_follow_copy_in_copy_out_policy[source]` (`runtime`) | canonical | | [Strings: Mutable Scalar Storage](../../docs/user/guide/strings.md#mutable-scalar-storage) | Supported | rank-zero `S8`; same-object mutation; bytes result; exact itemsize/rank/writeability | `tests/fortran/strings/semantics/test_string_pyi_semantics.py::test_rank_zero_string_storage_round_trips_as_empty_tuple_array` | `tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays` | `tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays` (`runtime`) | canonical | -| [Strings: String Arrays](../../docs/user/guide/strings.md#string-arrays) | Supported | fixed itemsize; input and in-place mutation; fixed array result; rank/shape/dtype/writeability; zero size | `tests/fortran/strings/wrapper_codegen/test_character_array_lowering.py::test_fixed_width_character_array_results_reuse_the_ordinary_array_copy_plan` | `tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays`
`tests/fortran/strings/end_to_end/test_character_boundaries.py::test_modern_fortran_character_arguments_and_results[source]` | `tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays` (`runtime`) | canonical | -| [Strings: Length And Encoding](../../docs/user/guide/strings.md#length-and-encoding) | Supported | length 1, representative width 8, runtime length, Unicode UTF-8 byte length, blanks, empty values, embedded NUL rejection, conservative no-`intent`, ambiguous mutable deferred scalar rejection | `tests/fortran/strings/parsing/test_character_length_parsing.py::test_character_entity_lengths_and_assumed_bounds_are_preserved`
`tests/fortran/strings/wrapper_codegen/test_string_input_lowering.py::test_required_string_values_reuse_argument_plan_with_character_handoff_facts` | `tests/fortran/strings/end_to_end/test_character_boundaries.py::test_modern_fortran_character_arguments_and_results[source]` | `tests/fortran/strings/semantics/test_string_pyi_semantics.py::test_bare_string_slice_is_rejected_as_ambiguous` (`semantics`)
`tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays` (`runtime`) | canonical | +| [Strings: String Arrays](../../docs/user/guide/strings.md#string-arrays) | Supported | fixed itemsize; input and in-place mutation; fixed array result; rank/shape/dtype/writeability; zero size | `tests/fortran/strings/codegen/test_character_array_lowering.py::test_fixed_width_character_array_results_reuse_the_ordinary_array_copy_plan` | `tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays`
`tests/fortran/strings/end_to_end/test_character_boundaries.py::test_modern_fortran_character_arguments_and_results[source]` | `tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays` (`runtime`) | canonical | +| [Strings: Length And Encoding](../../docs/user/guide/strings.md#length-and-encoding) | Supported | length 1, representative width 8, runtime length, Unicode UTF-8 byte length, blanks, empty values, embedded NUL rejection, conservative no-`intent`, ambiguous mutable deferred scalar rejection | `tests/fortran/strings/parsing/test_character_length_parsing.py::test_character_entity_lengths_and_assumed_bounds_are_preserved`
`tests/fortran/strings/codegen/test_string_input_lowering.py::test_required_string_values_reuse_argument_plan_with_character_handoff_facts` | `tests/fortran/strings/end_to_end/test_character_boundaries.py::test_modern_fortran_character_arguments_and_results[source]` | `tests/fortran/strings/semantics/test_string_pyi_semantics.py::test_bare_string_slice_is_rejected_as_ambiguous` (`semantics`)
`tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays` (`runtime`) | canonical | | [Wrapping Functions: Basic Scalar Function](../../docs/user/guide/wrapping-functions.md#basic-scalar-function) | Supported | direct scalar result; exact NumPy inputs; visible value | `tests/fortran/functions/semantics/test_fortran_function_semantics.py::test_function_result` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` (`runtime`) | canonical | | [Wrapping Functions: Python And Native Names](../../docs/user/guide/wrapping-functions.md#python-and-native-names) | Supported | edited `.pyi`; standalone external; `@bind`; changed Python name; unchanged native ABI; exact signature | — | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` (`runtime`) | canonical | -| [Wrapping Functions: Array Return Values](../../docs/user/guide/wrapping-functions.md#array-return-values) | Supported | automatic shape; new NumPy array; Fortran layout; values | `tests/fortran/arrays/wrapper_codegen/test_array_result_lowering.py::test_array_results_record_producer_shape_copy_ownership_and_shared_hidden_slot` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | — | canonical | -| [Wrapping Functions: Functions with Output Arguments](../../docs/user/guide/wrapping-functions.md#functions-with-output-arguments) | Supported | direct result first; hidden scalar output second; caller array excluded from tuple; stable tuple order | `tests/fortran/functions/policy/test_function_result_policy.py::test_multiple_scalar_result_policy_completes_order_and_hidden_address_before_planning`
`tests/fortran/functions/wrapper_codegen/test_multiple_function_results.py::test_multiple_scalar_results_lower_to_binding_tuple_and_one_bridge_function_call` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | `tests/fortran/functions/wrapper_codegen/test_multiple_function_results.py::test_multiple_scalar_result_validation_rejects_position_and_consumer_drift` (`wrapper_codegen`) | canonical | -| [Wrapping Functions: Important Rules](../../docs/user/guide/wrapping-functions.md#important-rules) | Supported | exact dtype; array copy result; projected scalar tuple order; caller array mutation; conservative no-`intent` scalar replacement after direct result | `tests/fortran/functions/semantics/test_fortran_function_semantics.py::test_missing_intent_scalar_uses_conservative_replacement_projection`
`tests/fortran/functions/policy/test_function_result_policy.py::test_scalar_copy_in_out_policy_completes_writeback_before_planning`
`tests/fortran/functions/wrapper_codegen/test_scalar_function_writeback.py::test_scalar_writeback_is_an_explicit_binding_lifecycle_result` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | — | canonical | +| [Wrapping Functions: Array Return Values](../../docs/user/guide/wrapping-functions.md#array-return-values) | Supported | automatic shape; new NumPy array; Fortran layout; values | `tests/fortran/arrays/codegen/test_array_result_lowering.py::test_array_results_record_producer_shape_copy_ownership_and_shared_hidden_slot` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | — | canonical | +| [Wrapping Functions: Functions with Output Arguments](../../docs/user/guide/wrapping-functions.md#functions-with-output-arguments) | Supported | direct result first; hidden scalar output second; caller array excluded from tuple; stable tuple order | `tests/fortran/functions/policy/test_function_result_policy.py::test_multiple_scalar_result_policy_completes_order_and_hidden_address_before_planning`
`tests/fortran/functions/codegen/test_multiple_function_results.py::test_multiple_scalar_results_lower_to_binding_tuple_and_one_bridge_function_call` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | `tests/fortran/functions/codegen/test_multiple_function_results.py::test_multiple_scalar_result_validation_rejects_position_and_consumer_drift` (`codegen`) | canonical | +| [Wrapping Functions: Important Rules](../../docs/user/guide/wrapping-functions.md#important-rules) | Supported | exact dtype; array copy result; projected scalar tuple order; caller array mutation; conservative no-`intent` scalar replacement after direct result | `tests/fortran/functions/semantics/test_fortran_function_semantics.py::test_missing_intent_scalar_uses_conservative_replacement_projection`
`tests/fortran/functions/policy/test_function_result_policy.py::test_scalar_copy_in_out_policy_completes_writeback_before_planning`
`tests/fortran/functions/codegen/test_scalar_function_writeback.py::test_scalar_writeback_is_an_explicit_binding_lifecycle_result` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | — | canonical | | [Wrapping Subroutines: How Arguments Become Python Results](../../docs/user/guide/wrapping-subroutines.md#how-arguments-become-python-results) | Supported | input scalar/array; hidden scalar output; scalar replacement; caller array output/inout; visible derived object; hidden allocatable handle; conservative no-`intent` | `tests/fortran/subroutines/semantics/test_subroutine_argument_projection.py::test_primitive_scalar_inout_stays_visible_and_projects_replacement_return`
`tests/fortran/subroutines/semantics/test_subroutine_argument_projection.py::test_ordinary_array_output_stays_visible_without_result_projection`
`tests/fortran/subroutines/semantics/test_subroutine_argument_projection.py::test_scalar_derived_output_stays_visible_without_result_projection` | `tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py::test_subroutine_outputs_and_caller_storage_follow_documented_projection_rules` | — | canonical | | [Wrapping Subroutines: Complete Example](../../docs/user/guide/wrapping-subroutines.md#complete-example) | Supported | source build; hidden bounds tuple; in-place array scaling; scalar replacement; caller output storage | `tests/fortran/subroutines/policy/test_subroutine_output_policy.py::test_source_hidden_scalar_output_completes_call_local_address_before_planning` | `tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py::test_subroutine_outputs_and_caller_storage_follow_documented_projection_rules` | — | canonical | | [Wrapping Subroutines: Python Usage](../../docs/user/guide/wrapping-subroutines.md#python-usage) | Supported | exact NumPy values; scalar object unchanged; arrays mutated in place; visible outputs | — | `tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py::test_subroutine_outputs_and_caller_storage_follow_documented_projection_rules` | `tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py::test_subroutine_outputs_and_caller_storage_follow_documented_projection_rules` (`runtime`) | canonical | -| [Wrapping Subroutines: Key Rules](../../docs/user/guide/wrapping-subroutines.md#key-rules) | Supported | hidden scalar ordering; explicit scalar writeback lifecycle; ordinary arrays and derived objects excluded from result; native-created allocatable returned; `.pyi` projection authority | `tests/fortran/subroutines/wrapper_codegen/test_hidden_scalar_outputs.py::test_hidden_scalar_result_is_one_bridge_output_and_one_python_result`
`tests/fortran/subroutines/wrapper_codegen/test_scalar_subroutine_writeback_validation.py::test_generator_rejects_writeback_without_python_result_target` | `tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py::test_subroutine_outputs_and_caller_storage_follow_documented_projection_rules` | `tests/fortran/subroutines/wrapper_codegen/test_scalar_subroutine_writeback_validation.py::test_generator_rejects_writeback_from_an_unavailable_handoff` (`wrapper_codegen`) | canonical | +| [Wrapping Subroutines: Key Rules](../../docs/user/guide/wrapping-subroutines.md#key-rules) | Supported | hidden scalar ordering; explicit scalar writeback lifecycle; ordinary arrays and derived objects excluded from result; native-created allocatable returned; `.pyi` projection authority | `tests/fortran/subroutines/codegen/test_hidden_scalar_outputs.py::test_hidden_scalar_result_is_one_bridge_output_and_one_python_result`
`tests/fortran/subroutines/codegen/test_scalar_subroutine_writeback_validation.py::test_generator_rejects_writeback_without_python_result_target` | `tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py::test_subroutine_outputs_and_caller_storage_follow_documented_projection_rules` | `tests/fortran/subroutines/codegen/test_scalar_subroutine_writeback_validation.py::test_generator_rejects_writeback_from_an_unavailable_handoff` (`codegen`) | canonical | | [Wrapping Modules: Basic Usage](../../docs/user/guide/wrapping-modules.md#basic-usage) | Supported | generated package entry; child native-module namespace; isolated import | `tests/fortran/modules/pipeline/test_generated_module_contracts.py::test_generated_module_contract_matches_fixture[module_exports]` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | — | canonical | | [Wrapping Modules: Procedures](../../docs/user/guide/wrapping-modules.md#procedures) | Supported | module functions; standalone external at root; multiple native modules in one source | `tests/fortran/modules/pipeline/test_generated_module_contracts.py::test_generated_module_contract_matches_fixture[module_exports]` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | — | canonical | -| [Wrapping Modules: Public Variables and Constants](../../docs/user/guide/wrapping-modules.md#public-variables-and-constants) | Supported | writable scalar state; true parameter; Python-local constant shadow; native state unchanged | `tests/fortran/modules/policy/test_module_variable_policy.py::test_scalar_module_variable_policy_completes_access_and_storage_before_planning`
`tests/fortran/modules/wrapper_codegen/test_scalar_module_variable_lowering.py::test_module_variable_plan_contains_only_completed_dispatch_facts` | `tests/fortran/modules/end_to_end/test_module_variables_and_state.py::test_scalar_module_variables_use_attributes_and_parameters_have_no_native_setter[source]` | — | canonical | +| [Wrapping Modules: Public Variables and Constants](../../docs/user/guide/wrapping-modules.md#public-variables-and-constants) | Supported | writable scalar state; true parameter; Python-local constant shadow; native state unchanged | `tests/fortran/modules/policy/test_module_variable_policy.py::test_scalar_module_variable_policy_completes_access_and_storage_before_planning`
`tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py::test_module_variable_plan_contains_only_completed_dispatch_facts` | `tests/fortran/modules/end_to_end/test_module_variables_and_state.py::test_scalar_module_variables_use_attributes_and_parameters_have_no_native_setter[source]` | — | canonical | | [Wrapping Modules: Module Arrays and Saved State](../../docs/user/guide/wrapping-modules.md#module-arrays-saved-state) | Supported | allocatable module array; persistent handle; live NumPy view; mutation; deallocation; procedure-local `save`; shared state across imports | `tests/fortran/modules/policy/test_module_variable_policy.py::test_scalar_module_variable_policy_completes_access_and_storage_before_planning` | `tests/fortran/modules/end_to_end/test_scalar_module_variable_plan.py::test_whole_scalar_module_variable_behavior_uses_canonical_plan`
`tests/fortran/modules/end_to_end/test_module_variables_and_state.py::test_scalar_module_variables_use_attributes_and_parameters_have_no_native_setter[source]` | — | canonical | | [Wrapping Modules: Shape the Module API With the Contract](../../docs/user/guide/wrapping-modules.md#shape-the-module-api-with-the-contract) | Supported | mutable literal initializer; hidden variable; private procedure; removed declaration; true `Final` constant | `tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_literal_defaults_are_preserved`
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | `tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_expression_defaults_are_rejected[from prik.contracts import Int32\ncounter: Int32 = f(42)\n]` (`semantics`)
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_unsupported_module_variable_initializer_completes_an_unsupported_policy` (`policy`) | canonical | | [Wrapping Modules: Flatten Module Namespaces](../../docs/user/guide/wrapping-modules.md#flatten-module-namespaces) | Supported | child namespaces; wildcard flattening; selective imports; explicit aliases; unchanged native targets | `tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_rejects_colliding_wildcard_exports` (`pipeline`) | canonical | | [Wrapping Modules: Important Rules](../../docs/user/guide/wrapping-modules.md#important-rules) | Supported | private declarations hidden; common-block storage internal; shared native state; source-derived extension identity | `tests/fortran/modules/semantics/test_module_contract_semantics.py::test_module_common_block_storage_stays_internal` | `tests/fortran/modules/end_to_end/test_common_blocks.py::test_common_block_storage_stays_internal_to_wrapped_fortran[source]`
`tests/fortran/modules/end_to_end/test_module_variables_and_state.py::test_scalar_module_variables_use_attributes_and_parameters_have_no_native_setter[source]` | — | canonical | | [Optional Arguments: Complete Example](../../docs/user/guide/optional-arguments.md#complete-example) | Supported | source generation; reviewed generated `.pyi`; optional scalar input; optional ordinary array output; native `present(...)` | `tests/fortran/optional_arguments/pipeline/test_generated_optional_contracts.py::test_generated_optional_contract_matches_fixture[foptional_f90]` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]`
`tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[generated-pyi]` | — | canonical | -| [Optional Arguments: Usage in Python](../../docs/user/guide/optional-arguments.md#usage-in-python) | Supported | omission; explicit `None`; positional value; keyword value; skipped earlier positions | `tests/fortran/optional_arguments/wrapper_codegen/test_optional_lowering.py::test_optional_scalar_lowering_distinguishes_absent_or_none_from_value` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]` (`runtime`) | canonical | +| [Optional Arguments: Usage in Python](../../docs/user/guide/optional-arguments.md#usage-in-python) | Supported | omission; explicit `None`; positional value; keyword value; skipped earlier positions | `tests/fortran/optional_arguments/codegen/test_optional_lowering.py::test_optional_scalar_lowering_distinguishes_absent_or_none_from_value` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]` (`runtime`) | canonical | | [Optional Arguments: Key Rules](../../docs/user/guide/optional-arguments.md#key-rules) | Supported | scalar; array; string; derived input; output visibility; conservative no-`intent`; presence without result inflation | `tests/fortran/optional_arguments/semantics/test_optional_fortran_semantics.py::test_optional_without_intent_uses_visible_conservative_replacement_projection`
`tests/fortran/optional_arguments/policy/test_optional_policy.py::test_optional_projected_array_keeps_nullable_value_separate_from_descriptor_storage` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]`
`tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_array_buffers_preserve_omission_and_identity` | — | canonical | -| [Optional Arguments: Scalar Allocatables and Pointers](../../docs/user/guide/optional-arguments.md#scalar-allocatables-and-pointers) | Supported | allocatable and pointer; omitted; present-unallocated/unassociated `None`; present concrete value; exact type rejection | `tests/fortran/optional_arguments/policy/test_optional_policy.py::test_optional_descriptor_policy_completes_three_state_boundary_before_planning`
`tests/fortran/optional_arguments/wrapper_codegen/test_optional_lowering.py::test_optional_descriptor_lowering_records_presence_and_nullable_value_handoffs` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_scalar_descriptors_distinguish_omitted_none_and_value` | — | canonical | +| [Optional Arguments: Scalar Allocatables and Pointers](../../docs/user/guide/optional-arguments.md#scalar-allocatables-and-pointers) | Supported | allocatable and pointer; omitted; present-unallocated/unassociated `None`; present concrete value; exact type rejection | `tests/fortran/optional_arguments/policy/test_optional_policy.py::test_optional_descriptor_policy_completes_three_state_boundary_before_planning`
`tests/fortran/optional_arguments/codegen/test_optional_lowering.py::test_optional_descriptor_lowering_records_presence_and_nullable_value_handoffs` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_scalar_descriptors_distinguish_omitted_none_and_value` | — | canonical | | [Optional Arguments: Optional Outputs](../../docs/user/guide/optional-arguments.md#optional-outputs) | Supported | ordinary scalar and array outputs; derived output remains visible; allocatable and pointer output visibility; absent, explicit `None`, caller storage; in-place identity; stable result projection | `tests/fortran/optional_arguments/semantics/test_optional_fortran_semantics.py::test_optional_scalar_output_remains_visible_scalar_storage`
`tests/fortran/optional_arguments/semantics/test_optional_fortran_semantics.py::test_optional_scalar_derived_output_stays_visible_without_result_projection`
`tests/fortran/optional_arguments/semantics/test_optional_fortran_semantics.py::test_optional_allocatable_output_remains_visible`
`tests/fortran/optional_arguments/semantics/test_optional_fortran_semantics.py::test_pointer_array_output_visibility_follows_intent_and_optional_presence` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]`
`tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_array_buffers_preserve_omission_and_identity` | — | canonical | -| [Optional Arguments: Limitations](../../docs/user/guide/optional-arguments.md#limitations) | Blocked | optional passed procedure; no invented native default; optional native literals cannot replace native procedure behavior | — | — | `tests/fortran/optional_arguments/policy/test_optional_policy.py::test_optional_passed_procedure_is_blocked_before_codegen` (`policy`)
`tests/fortran/optional_arguments/wrapper_codegen/test_optional_lowering.py::test_optional_arguments_with_hidden_literals_fail_during_shared_plan_validation` (`wrapper_codegen`) | canonical | +| [Optional Arguments: Limitations](../../docs/user/guide/optional-arguments.md#limitations) | Blocked | optional passed procedure; no invented native default; optional native literals cannot replace native procedure behavior | — | — | `tests/fortran/optional_arguments/policy/test_optional_policy.py::test_optional_passed_procedure_is_blocked_before_codegen` (`policy`)
`tests/fortran/optional_arguments/codegen/test_optional_lowering.py::test_optional_arguments_with_hidden_literals_fail_during_shared_plan_validation` (`codegen`) | canonical | | [Generic Interfaces: Complete Example](../../docs/user/guide/generic-interfaces.md#complete-example) | Supported | free-form source; fixed-form parsing and contract generation; reviewed generated `.pyi`; source build; generated-`.pyi` replay | `tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py::test_fixed_form_generic_interface_preserves_specific_procedures`
`tests/fortran/generic_interfaces/pipeline/test_generated_generic_contracts.py::test_generated_generic_contract_matches_fixture[foverloads_f90]`
`tests/fortran/generic_interfaces/pipeline/test_generated_generic_contracts.py::test_generated_generic_contract_matches_fixture[foverloads_fixed]` | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]`
`tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[generated-pyi]` | — | canonical | | [Generic Interfaces: Generated Contract](../../docs/user/guide/generic-interfaces.md#generated-contract) | Supported | private link targets; one exact overload candidate per declaration; public-generic `@bind`; native target precedence | `tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py::test_convert_pyi_to_ir_resolves_prik_overload_by_explicit_specific_name`
`tests/fortran/generic_interfaces/policy/test_generic_policy.py::test_module_overload_bind_takes_precedence_per_candidate` | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]` | — | canonical | -| [Generic Interfaces: Usage in Python](../../docs/user/guide/generic-interfaces.md#usage-in-python) | Supported | exact `Int32`, `Float64`, and `Complex128`; scalar and rank-one dispatch; generated-class dispatch; no implicit coercion | `tests/fortran/generic_interfaces/wrapper_codegen/test_overload_dispatch_plan.py::test_plan_records_one_exact_numpy_scalar_predicate_per_candidate` | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]` | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]` (`runtime`) | canonical | +| [Generic Interfaces: Usage in Python](../../docs/user/guide/generic-interfaces.md#usage-in-python) | Supported | exact `Int32`, `Float64`, and `Complex128`; scalar and rank-one dispatch; generated-class dispatch; no implicit coercion | `tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py::test_plan_records_one_exact_numpy_scalar_predicate_per_candidate` | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]` | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]` (`runtime`) | canonical | | [Generic Interfaces: Inspect the Overloads](../../docs/user/guide/generic-interfaces.md#inspect-the-overloads) | Supported | one public callable; all accepted signatures; hidden concrete procedures and internal names | — | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]` | — | canonical | | [Generic Interfaces: Extend an Overload Set](../../docs/user/guide/generic-interfaces.md#extend-an-overload-set) | Supported | edited `.pyi`; renamed public binding; added overload group; private-specific routing through public generic; absent candidate rejection | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_private_native_specific_without_overload_bind_fails_at_build[private_module_specifics_without_bind-missing_targets0]` (`compiling`) | canonical | -| [Generic Interfaces: Key Rules](../../docs/user/guide/generic-interfaces.md#key-rules) | Supported | exact dtype/rank/class match; no-match `TypeError`; ambiguous signature rejection; exact-once specific links; `@bind`; private visibility; type-bound generics; defined operators; defined assignment | `tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_module_and_type_bound_generic_overload_sets`
`tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_defined_operators_assignment_and_type_bound_operators`
`tests/fortran/generic_interfaces/wrapper_codegen/test_overload_dispatch_plan.py::test_plan_records_one_exact_numpy_scalar_predicate_per_candidate` | `tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py::test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extension[source]` | `tests/fortran/generic_interfaces/wrapper_codegen/test_overload_dispatch_plan.py::test_generator_rejects_ambiguous_edited_overload_plan_before_emission` (`wrapper_codegen`)
`tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py::test_convert_pyi_to_ir_rejects_invalid_prik_overload_links[@overload("missing")\ndef convert(value: Int32) -> Int32: ...\n-missing specific procedure 'missing']` (`semantics`) | canonical | -| [Generic Interfaces: Limitations](../../docs/user/guide/generic-interfaces.md#limitations) | Blocked | source generic constructor inference; assumed-type `class(*)`; arrays of derived values | — | — | `tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_rejects_generic_constructor_interfaces_during_semantic_conversion` (`semantics`)
`tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py::test_assumed_type_generic_candidate_is_rejected_at_parsing` (`parsing`)
`tests/fortran/generic_interfaces/wrapper_codegen/test_overload_dispatch_plan.py::test_generic_candidate_with_array_of_derived_values_is_blocked_before_lowering` (`wrapper_codegen`) | canonical | +| [Generic Interfaces: Key Rules](../../docs/user/guide/generic-interfaces.md#key-rules) | Supported | exact dtype/rank/class match; no-match `TypeError`; ambiguous signature rejection; exact-once specific links; `@bind`; private visibility; type-bound generics; defined operators; defined assignment | `tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_module_and_type_bound_generic_overload_sets`
`tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_defined_operators_assignment_and_type_bound_operators`
`tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py::test_plan_records_one_exact_numpy_scalar_predicate_per_candidate` | `tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py::test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extension[source]` | `tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py::test_generator_rejects_ambiguous_edited_overload_plan_before_emission` (`codegen`)
`tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py::test_convert_pyi_to_ir_rejects_invalid_prik_overload_links[@overload("missing")\ndef convert(value: Int32) -> Int32: ...\n-missing specific procedure 'missing']` (`semantics`) | canonical | +| [Generic Interfaces: Limitations](../../docs/user/guide/generic-interfaces.md#limitations) | Blocked | source generic constructor inference; assumed-type `class(*)`; arrays of derived values | — | — | `tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_rejects_generic_constructor_interfaces_during_semantic_conversion` (`semantics`)
`tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py::test_assumed_type_generic_candidate_is_rejected_at_parsing` (`parsing`)
`tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py::test_generic_candidate_with_array_of_derived_values_is_blocked_before_lowering` (`codegen`) | canonical | | [Wrapping Derived Types: Complete Example](../../docs/user/guide/wrapping-derived-types.md#complete-example) | Supported | derived declarations; public and nested fields; source generation; reviewed generated `.pyi`; source build; generated-`.pyi` replay | `tests/fortran/derived_types/parsing/test_derived_type_declarations.py::test_derived_type_fields_and_methods_detection`
`tests/fortran/derived_types/pipeline/test_generated_derived_contracts.py::test_generated_derived_contract_matches_fixture[fderived_boundary_f90]` | `tests/fortran/derived_types/end_to_end/test_derived_boundaries.py::test_scalar_derived_types_cross_procedure_boundaries[source]`
`tests/fortran/derived_types/end_to_end/test_derived_boundaries.py::test_scalar_derived_types_cross_procedure_boundaries[generated-pyi]` | — | canonical | | [Wrapping Derived Types: Usage in Python](../../docs/user/guide/wrapping-derived-types.md#usage-in-python) | Supported | keyword construction; public field get/set; `intent(inout)` identity; owned result; nested borrowed component | `tests/fortran/derived_types/policy/test_derived_policy_defaults.py::test_recursive_module_policy_map_includes_nested_fields_and_functions` | `tests/fortran/derived_types/end_to_end/test_derived_boundaries.py::test_scalar_derived_types_cross_procedure_boundaries[source]` | — | canonical | | [Wrapping Derived Types: Inspect the Class](../../docs/user/guide/wrapping-derived-types.md#inspect-the-class) | Supported | class, constructor, field, method, parameter, return, and overload docstrings; no native implementation names | `tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_bound_constructor_and_method_reuse_completed_direct_function_plans`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_edited_overloads_complete_exact_dispatch_and_reject_ambiguous_plan` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | — | canonical | -| [Wrapping Derived Types: Key Concepts](../../docs/user/guide/wrapping-derived-types.md#key-concepts) | Supported | Python-owned construction/result; parent-retained component; in-place output/inout/no-`intent`; primitive writable fields; nested types; keyword defaults; destruction | `tests/fortran/derived_types/policy/test_derived_accessor_policy.py::test_derived_field_setter_policy_uses_value_copy_write_through`
`tests/fortran/derived_types/wrapper_codegen/test_derived_lowering.py::test_projected_derived_argument_returns_the_exact_caller_wrapper_without_release` | `tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py::test_fortran_default_constructor_keywords_and_finalization[source]`
`tests/fortran/derived_types/end_to_end/test_borrowed_components.py::test_borrowed_child_wrapper_never_finalizes_native_component[source]` | — | canonical | +| [Wrapping Derived Types: Key Concepts](../../docs/user/guide/wrapping-derived-types.md#key-concepts) | Supported | Python-owned construction/result; parent-retained component; in-place output/inout/no-`intent`; primitive writable fields; nested types; keyword defaults; destruction | `tests/fortran/derived_types/policy/test_derived_accessor_policy.py::test_derived_field_setter_policy_uses_value_copy_write_through`
`tests/fortran/derived_types/codegen/test_derived_lowering.py::test_projected_derived_argument_returns_the_exact_caller_wrapper_without_release` | `tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py::test_fortran_default_constructor_keywords_and_finalization[source]`
`tests/fortran/derived_types/end_to_end/test_borrowed_components.py::test_borrowed_child_wrapper_never_finalizes_native_component[source]` | — | canonical | | [Wrapping Derived Types: Custom Constructor](../../docs/user/guide/wrapping-derived-types.md#custom-constructor) | Supported | edited `.pyi`; `@bind`; exactly one `Pass()`; reordered `Addr(Arg)` values; replacement of generated keyword initializer; constructor docs | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bound_constructor_uses_explicit_pass_position_and_native_target`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_bound_constructor_pass_disambiguates_same_type_arguments_and_keeps_module_export` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected[\nclass state:\n @bind("init_state")\n @native_call([Addr(Arg(0))])\n def __init__(self, seed: Int32) -> None: ...\n-Bound constructor native_call requires exactly one Pass() entry]` (`semantics`) | canonical | | [Wrapping Derived Types: Type-Bound Methods](../../docs/user/guide/wrapping-derived-types.md#type-bound-methods) | Supported | passed object becomes `self`; mutation preserves Python identity; direct and generated-`.pyi` replay | `tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py::test_converter_covers_derived_dispatch_methods_and_kind_edges` | `tests/fortran/derived_types/end_to_end/test_type_bound_methods.py::test_modern_fortran_derived_type_exposes_class_and_type_bound_methods[source]`
`tests/fortran/derived_types/end_to_end/test_type_bound_methods.py::test_modern_fortran_derived_type_exposes_class_and_type_bound_methods[generated-pyi]` | — | canonical | | [Wrapping Derived Types: Expose a Module Procedure as a Method](../../docs/user/guide/wrapping-derived-types.md#expose-a-module-procedure-as-a-method) | Supported | edited class method; `Pass()` receiver; independent module declaration; same or bound native name; optional private module surface; method docs | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_method_and_module_declarations_keep_native_targets_independent`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_module_procedure_method_visibility_is_completed_independently` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | — | canonical | | [Wrapping Derived Types: Type-Bound Generics](../../docs/user/guide/wrapping-derived-types.md#type-bound-generics) | Supported | private specifics; public generic bind; exact `Int32`/`Float64` dispatch; wrapped receiver fixed by class; no trial calls | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_edited_overloads_complete_exact_dispatch_and_reject_ambiguous_plan` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` (`runtime`) | canonical | | [Wrapping Derived Types: Defined Operators](../../docs/user/guide/wrapping-derived-types.md#defined-operators) | Supported | direct/reflected binary; unary; comparison; logical; named operators; defined assignment; exact wrapped/scalar dispatch; operator docstrings | `tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_defined_operators_assignment_and_type_bound_operators` | `tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py::test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extension[source]` | `tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py::test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extension[source]` (`runtime`) | canonical | -| [Fortran Wrapper: Derived Types Across Procedure Boundaries](../../docs/user/reference/fortran-wrapper.md#derived-types-across-procedure-boundaries) | Supported | complete scalar actual/dummy matrix; module and nonmodule storage; ordinary, target, allocatable, allocatable-target, pointer; six dummy forms; identity, writeback, empty states, rollback, lifetime, and deliberate blockers | `tests/fortran/derived_types/wrapper_codegen/test_scalar_actual_dummy_plan.py::test_every_dummy_form_has_one_exhaustive_completed_matrix[object_dummy-object]` | `tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_all_sixty_actual_dummy_cells[A-module_object]`
`tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_one_call_uses_all_six_dummy_forms_and_optional_arguments_stay_linear`
`tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_later_acquisition_failure_rolls_back_earlier_origins` | `tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_reassociable_pointer_dummy_requires_pointer_storage[module_object]` (`runtime`)
`tests/fortran/derived_types/wrapper_codegen/test_derived_lowering.py::test_unsupported_derived_shapes_fail_on_exact_completed_policy_blockers[\nfrom prik.contracts import Float64\n\nclass point:\n x: Float64\n\ndef consume(value: point[:]) -> None: ...\n-unsupported array of derived values]` (`wrapper_codegen`) | canonical | -| [Fortran Wrapper: Inheritance And Polymorphism](../../docs/user/reference/fortran-wrapper.md#inheritance-and-polymorphism) | Partially supported | scalar extension inheritance; closed `class(base), intent(in)` dispatch; exact extension classes; unsupported polymorphic results, mutation, arrays, descriptor scalars, and assumed type | `tests/fortran/derived_types/wrapper_codegen/test_class_surfaces.py::test_inheritance_and_polymorphism_are_completed_before_planning` | `tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py::test_fortran_extension_types_generate_python_inheritance[source]`
`tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py::test_fortran_extension_types_generate_python_inheritance[generated-pyi]` | `tests/fortran/derived_types/wrapper_codegen/test_class_surfaces.py::test_invalid_class_graph_fails_before_emission` (`wrapper_codegen`)
`tests/fortran/derived_types/policy/test_derived_accessor_policy.py::test_abstract_type_and_deferred_binding_fail_in_completed_derived_policy` (`policy`) | canonical | -| [Fortran Wrapper: Constructors, Initialization, And Finalizers](../../docs/user/reference/fortran-wrapper.md#constructors-initialization-and-finalizers) | Supported | generated keyword constructor; default field values; custom direct constructor; overloaded constructors; commit-on-success; exact finalization; borrowed non-finalization | `tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py::test_derived_type_initializers_and_finalizers_reach_semantic_ir`
`tests/fortran/derived_types/wrapper_codegen/test_derived_lowering.py::test_owned_derived_result_has_explicit_failure_and_release_lifecycle` | `tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py::test_fortran_default_constructor_keywords_and_finalization[source]`
`tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract`
`tests/fortran/derived_types/end_to_end/test_borrowed_components.py::test_borrowed_child_wrapper_never_finalizes_native_component[source]` | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected[\nclass state:\n def __init__(self, seed: Int32) -> None: ...\n-Non-generated __init__ declarations must use @bind("specific_name")]` (`semantics`) | canonical | -| [Fortran Wrapper: Derived-Type Layout And Interoperability](../../docs/user/reference/fortran-wrapper.md#derived-type-layout-and-interoperability) | Supported | opaque accessor storage for ordinary, `bind(C)`, and `sequence`; field get/set; by-value copy; no direct C aggregate access | `tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py::test_bind_c_and_sequence_types_preserve_accessor_layout_metadata`
`tests/fortran/derived_types/wrapper_codegen/test_derived_lowering.py::test_exact_typed_value_lowering_uses_fortran_value_semantics_and_opaque_binding` | `tests/fortran/derived_types/end_to_end/test_opaque_layout.py::test_bind_c_derived_types_use_accessors_and_fortran_value_copy[source]`
`tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_sequence_derived_value_uses_the_same_typed_opaque_call_path` | — | canonical | +| [Fortran Wrapper: Derived Types Across Procedure Boundaries](../../docs/user/reference/fortran-wrapper.md#derived-types-across-procedure-boundaries) | Supported | complete scalar actual/dummy matrix; module and nonmodule storage; ordinary, target, allocatable, allocatable-target, pointer; six dummy forms; identity, writeback, empty states, rollback, lifetime, and deliberate blockers | `tests/fortran/derived_types/codegen/test_scalar_actual_dummy_plan.py::test_every_dummy_form_has_one_exhaustive_completed_matrix[object_dummy-object]` | `tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_all_sixty_actual_dummy_cells[A-module_object]`
`tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_one_call_uses_all_six_dummy_forms_and_optional_arguments_stay_linear`
`tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_later_acquisition_failure_rolls_back_earlier_origins` | `tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_reassociable_pointer_dummy_requires_pointer_storage[module_object]` (`runtime`)
`tests/fortran/derived_types/codegen/test_derived_lowering.py::test_unsupported_derived_shapes_fail_on_exact_completed_policy_blockers[\nfrom prik.contracts import Float64\n\nclass point:\n x: Float64\n\ndef consume(value: point[:]) -> None: ...\n-unsupported array of derived values]` (`codegen`) | canonical | +| [Fortran Wrapper: Inheritance And Polymorphism](../../docs/user/reference/fortran-wrapper.md#inheritance-and-polymorphism) | Partially supported | scalar extension inheritance; closed `class(base), intent(in)` dispatch; exact extension classes; unsupported polymorphic results, mutation, arrays, descriptor scalars, and assumed type | `tests/fortran/derived_types/codegen/test_class_surfaces.py::test_inheritance_and_polymorphism_are_completed_before_planning` | `tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py::test_fortran_extension_types_generate_python_inheritance[source]`
`tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py::test_fortran_extension_types_generate_python_inheritance[generated-pyi]` | `tests/fortran/derived_types/codegen/test_class_surfaces.py::test_invalid_class_graph_fails_before_emission` (`codegen`)
`tests/fortran/derived_types/policy/test_derived_accessor_policy.py::test_abstract_type_and_deferred_binding_fail_in_completed_derived_policy` (`policy`) | canonical | +| [Fortran Wrapper: Constructors, Initialization, And Finalizers](../../docs/user/reference/fortran-wrapper.md#constructors-initialization-and-finalizers) | Supported | generated keyword constructor; default field values; custom direct constructor; overloaded constructors; commit-on-success; exact finalization; borrowed non-finalization | `tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py::test_derived_type_initializers_and_finalizers_reach_semantic_ir`
`tests/fortran/derived_types/codegen/test_derived_lowering.py::test_owned_derived_result_has_explicit_failure_and_release_lifecycle` | `tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py::test_fortran_default_constructor_keywords_and_finalization[source]`
`tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract`
`tests/fortran/derived_types/end_to_end/test_borrowed_components.py::test_borrowed_child_wrapper_never_finalizes_native_component[source]` | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected[\nclass state:\n def __init__(self, seed: Int32) -> None: ...\n-Non-generated __init__ declarations must use @bind("specific_name")]` (`semantics`) | canonical | +| [Fortran Wrapper: Derived-Type Layout And Interoperability](../../docs/user/reference/fortran-wrapper.md#derived-type-layout-and-interoperability) | Supported | opaque accessor storage for ordinary, `bind(C)`, and `sequence`; field get/set; by-value copy; no direct C aggregate access | `tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py::test_bind_c_and_sequence_types_preserve_accessor_layout_metadata`
`tests/fortran/derived_types/codegen/test_derived_lowering.py::test_exact_typed_value_lowering_uses_fortran_value_semantics_and_opaque_binding` | `tests/fortran/derived_types/end_to_end/test_opaque_layout.py::test_bind_c_derived_types_use_accessors_and_fortran_value_copy[source]`
`tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_sequence_derived_value_uses_the_same_typed_opaque_call_path` | — | canonical | | [Allocatables: Key Concepts](../../docs/user/guide/allocatables.md#key-concepts) | Supported | scalar value versus array handle; allocated, unallocated, and zero-sized states; live views; module, field, result, and caller-created descriptor origins | `tests/fortran/allocatables/semantics/test_pyi_allocatable_semantics.py::test_persistent_allocatable_descriptors_preserve_scalar_and_array_kinds`
`tests/fortran/allocatables/policy/test_allocatable_handle_policy.py::test_allocatable_array_field_is_wrapper_owned_borrowed_view` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[source]` | — | canonical | -| [Allocatables: When To Use An Allocatable Handle](../../docs/user/guide/allocatables.md#when-to-use-an-allocatable-handle) | Supported | descriptor arguments versus ordinary arrays; present-empty caller handle; dtype/rank compatibility; plain NumPy rejection | `tests/fortran/allocatables/runtime/test_allocatable_descriptor_abi.py::test_allocatable_descriptor_hook_accepts_unallocated_descriptor_without_numpy_conversion`
`tests/fortran/allocatables/runtime/test_allocatable_array_actual_abi.py::test_array_actual_argument_abi_packer_uses_allocatable_native_array_actual_without_numpy_conversion` | `tests/fortran/allocatables/end_to_end/test_external_allocatable.py::test_external_allocatable_argument_accepts_a_caller_created_handle` | `tests/fortran/allocatables/runtime/test_allocatable_contract_handles.py::test_generated_storage_rejects_incompatible_allocatable_contract_handles[-float64-1-TypeError-fresh contract handle]` (`runtime`) | canonical | +| [Allocatables: When To Use An Allocatable Handle](../../docs/user/guide/allocatables.md#when-to-use-an-allocatable-handle) | Supported | descriptor arguments versus ordinary arrays; present-empty caller handle; dtype/rank compatibility; plain NumPy rejection | `tests/fortran/allocatables/runtime/test_allocatable_descriptor_abi.py::test_allocatable_descriptor_hook_accepts_unallocated_descriptor_without_numpy_conversion`
`tests/fortran/allocatables/runtime/test_allocatable_array_actual_abi.py::test_array_actual_argument_abi_packer_uses_allocatable_native_array_actual_without_numpy_conversion` | `tests/fortran/allocatables/end_to_end/test_external_allocatable.py::test_standalone_allocatable_argument_accepts_a_caller_created_handle` | `tests/fortran/allocatables/runtime/test_allocatable_contract_handles.py::test_generated_storage_rejects_incompatible_allocatable_contract_handles[-float64-1-TypeError-fresh contract handle]` (`runtime`) | canonical | | [Allocatables: Allocatable Array Handle API](../../docs/user/guide/allocatables.md#allocatable-array-handle-api) | Supported | default construction; allocated, shape, dtype, rank, `to_numpy`, resize, deallocate, close, and closed state; unavailable-operation gating | `tests/fortran/allocatables/runtime/test_allocatable_contract_handles.py::test_contract_default_allocatable_constructor_preserves_dtype_rank_and_empty_state`
`tests/fortran/allocatables/runtime/test_allocatable_handle_protocol.py::test_allocatable_handle_reports_absent_state_and_routes_resize_deallocate` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_plain_allocatable_module_array_exposes_current_live_view[source]` | `tests/fortran/allocatables/runtime/test_allocatable_handle_protocol.py::test_allocatable_operations_are_gated_by_the_completed_ops_table` (`runtime`)
`tests/fortran/allocatables/runtime/test_allocatable_contract_handles.py::test_non_array_allocatable_annotations_are_not_factories[-scalar allocatable contracts]` (`runtime`) | canonical | | [Allocatables: Deallocate Versus Close](../../docs/user/guide/allocatables.md#deallocate-versus-close) | Supported | deallocate while open; owned descriptor close; idempotent close; borrowed module/field close no-op; closed-handle rejection | `tests/fortran/allocatables/runtime/test_allocatable_handle_protocol.py::test_close_is_a_noop_for_a_borrowed_allocatable_handle`
`tests/fortran/allocatables/runtime/test_allocatable_contract_handles.py::test_writable_contract_handle_adopts_generated_storage_and_closes_once` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[source]` | `tests/fortran/allocatables/runtime/test_allocatable_contract_handles.py::test_generated_storage_rejects_a_closed_contract_handle` (`runtime`) | canonical | -| [Allocatables: Module Variables And Derived Fields](../../docs/user/guide/allocatables.md#module-variables-and-derived-fields) | Supported | native-owned module descriptor; parent-retained component descriptor; stable handle identity; live allocation changes and mutation | `tests/fortran/allocatables/policy/test_allocatable_handle_policy.py::test_allocatable_array_field_is_wrapper_owned_borrowed_view`
`tests/fortran/allocatables/wrapper_codegen/test_allocatable_lowering.py::test_plain_module_allocatable_uses_standard_descriptor_callback_without_copy` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[generated-pyi]` | — | canonical | -| [Allocatables: Function Results](../../docs/user/guide/allocatables.md#function-results) | Supported | owned result descriptor; allocated and zero-sized results; explicit maybe-unallocated result; rank one, rank two, and high-rank policy | `tests/fortran/allocatables/policy/test_allocatable_result_policy.py::test_direct_high_rank_allocatable_function_result_is_supported_before_codegen`
`tests/fortran/allocatables/wrapper_codegen/test_allocatable_lowering.py::test_allocated_direct_result_assigns_then_moves_into_owned_descriptor` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_maybe_unallocated_direct_allocatable_results_preserve_unallocated_state` | `tests/fortran/allocatables/policy/test_allocatable_result_policy.py::test_direct_allocatable_scalar_function_result_is_blocked_before_codegen` (`policy`)
`tests/fortran/allocatables/wrapper_codegen/test_allocatable_lowering.py::test_maybe_unallocated_is_only_valid_on_direct_allocatable_array_results` (`wrapper_codegen`) | canonical | +| [Allocatables: Module Variables And Derived Fields](../../docs/user/guide/allocatables.md#module-variables-and-derived-fields) | Supported | native-owned module descriptor; parent-retained component descriptor; stable handle identity; live allocation changes and mutation | `tests/fortran/allocatables/policy/test_allocatable_handle_policy.py::test_allocatable_array_field_is_wrapper_owned_borrowed_view`
`tests/fortran/allocatables/codegen/test_allocatable_lowering.py::test_plain_module_allocatable_uses_standard_descriptor_callback_without_copy` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[generated-pyi]` | — | canonical | +| [Allocatables: Function Results](../../docs/user/guide/allocatables.md#function-results) | Supported | owned result descriptor; allocated and zero-sized results; explicit maybe-unallocated result; rank one, rank two, and high-rank policy | `tests/fortran/allocatables/policy/test_allocatable_result_policy.py::test_direct_high_rank_allocatable_function_result_is_supported_before_codegen`
`tests/fortran/allocatables/codegen/test_allocatable_lowering.py::test_allocated_direct_result_assigns_then_moves_into_owned_descriptor` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_maybe_unallocated_direct_allocatable_results_preserve_unallocated_state` | `tests/fortran/allocatables/policy/test_allocatable_result_policy.py::test_direct_allocatable_scalar_function_result_is_blocked_before_codegen` (`policy`)
`tests/fortran/allocatables/codegen/test_allocatable_lowering.py::test_maybe_unallocated_is_only_valid_on_direct_allocatable_array_results` (`codegen`) | canonical | | [Allocatables: Output And Inout Arguments](../../docs/user/guide/allocatables.md#output-and-inout-arguments) | Supported | hidden nonoptional output; visible optional output; visible inout; unallocated, allocated, reallocated, and deallocated states; same-object projection | `tests/fortran/allocatables/semantics/test_fortran_allocatable_semantics.py::test_allocatable_output_semantics_projects_a_hidden_descriptor_handle`
`tests/fortran/allocatables/policy/test_allocatable_handle_policy.py::test_visible_descriptor_writeback_completes_caller_handle_construction_lifecycle`
`tests/fortran/allocatables/pipeline/test_allocatable_output_contract_printing.py::test_emit_optional_allocatable_output_as_visible_argument` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[source]`
`tests/fortran/allocatables/end_to_end/test_allocatable_cross_extension.py::test_caller_created_allocatable_crosses_separately_built_extensions` | — | canonical | | [Allocatables: Complete Example](../../docs/user/guide/allocatables.md#complete-example) | Supported | source build; reviewed generated `.pyi`; generated-`.pyi` replay; owned result; same-handle inout replacement; exact visible values | `tests/fortran/allocatables/pipeline/test_generated_allocatable_contract.py::test_generated_allocatable_contract_matches_fixture[fallocatable_views_f90]` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[source]`
`tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[generated-pyi]` | — | canonical | | [Allocatables: Safety Checklist](../../docs/user/guide/allocatables.md#safety-checklist) | Supported | allocation checks; independent copies; fresh extraction after reallocation; explicit close; owner-only release; borrowed views and owner retention | `tests/fortran/allocatables/runtime/test_allocatable_handle_protocol.py::test_allocatable_to_numpy_explicit_copy_is_independent`
`tests/fortran/allocatables/runtime/test_allocatable_handle_protocol.py::test_allocatable_to_numpy_short_circuits_unallocated_state_before_generated_extraction` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_plain_allocatable_module_array_exposes_current_live_view[source]`
`tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[source]` | `tests/fortran/allocatables/runtime/test_allocatable_contract_handles.py::test_generated_storage_rejects_a_closed_contract_handle` (`runtime`) | canonical | @@ -120,9 +120,9 @@ Authoritative sources: | [Pointers: Pointer Array Handle API](../../docs/user/guide/pointers.md#pointer-array-handle-api) | Supported | default construction; association; shape; dtype; rank; live view; associate; nullify; allocate; deallocate; resize; close; unavailable operations | `tests/fortran/pointers/runtime/test_pointer_contract_handles.py::test_contract_default_handle_constructors_preserve_dtype_rank_and_empty_state`
`tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_pointer_handle_uses_common_base_and_nullify_operation` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_caller_created_pointer_handle_tracks_native_output_association` | `tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_pointer_allocation_operations_are_policy_gated_by_ops_table` (`runtime`)
`tests/fortran/pointers/runtime/test_pointer_contract_handles.py::test_non_array_descriptor_and_ordinary_array_annotations_are_not_factories` (`runtime`) | canonical | | [Pointers: Associate Two Pointers](../../docs/user/guide/pointers.md#associate-two-pointers) | Supported | same dtype/rank; associated and unassociated source; no copy; independent descriptor state; cross-extension ABI | `tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_pointer_associate_accepts_reassociation_and_an_unassociated_source`
`tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_generated_pointer_associate_packs_standard_descriptor_facts` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_caller_created_pointer_crosses_separately_built_extensions` | `tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_pointer_associate_rejects_incompatible_sources[other1-TypeError-dtype]` (`runtime`)
`tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_pointer_associate_rejects_incompatible_sources[other2-ValueError-rank]` (`runtime`) | canonical | | [Pointers: Nullify, Deallocate, And Close](../../docs/user/guide/pointers.md#nullify-deallocate-and-close) | Supported | association release; policy-gated target release; owned descriptor release; borrowed close no-op; closed state | `tests/fortran/pointers/policy/test_pointer_ownership_policy.py::test_pointer_policy_unsafe_deallocate_is_explicit_operation_opt_in`
`tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_pointer_allocation_operations_route_when_policy_ops_exist` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_module_and_derived_pointer_handles_track_native_association[source]` | `tests/fortran/pointers/runtime/test_pointer_contract_handles.py::test_pointer_association_rejects_closed_handles` (`runtime`) | canonical | -| [Pointers: Module Variables And Derived Fields](../../docs/user/guide/pointers.md#module-variables-and-derived-fields) | Supported | native module owner; parent-retained component; stable handle identity; live native reassociation; getter-only field | `tests/fortran/pointers/policy/test_pointer_ownership_policy.py::test_plain_pointer_array_container_policy_completes_default_handle_profile`
`tests/fortran/pointers/wrapper_codegen/test_pointer_lowering.py::test_pointer_plans_complete_descriptor_ownership_and_operations_before_lowering` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_pointer_descriptor_views_preserve_slice_shape_strides_and_parent_lifetime` | — | canonical | -| [Pointers: Function Results](../../docs/user/guide/pointers.md#function-results) | Supported | associated and unassociated result; wrapper-owned persistent descriptor; borrowed target; explicit close | `tests/fortran/pointers/policy/test_pointer_ownership_policy.py::test_hidden_pointer_handle_output_owns_descriptor_but_not_target_policy`
`tests/fortran/pointers/wrapper_codegen/test_pointer_lowering.py::test_pointer_lowering_assigns_descriptors_without_target_deallocation` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_pointer_array_results_use_owned_descriptors_without_owning_targets[source]`
`tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_pointer_array_results_use_owned_descriptors_without_owning_targets[generated-pyi]` | — | canonical | -| [Pointers: Output And Inout Arguments](../../docs/user/guide/pointers.md#output-and-inout-arguments) | Supported | hidden nonoptional output; visible inout; present unassociated caller descriptor; same-object update | `tests/fortran/pointers/wrapper_codegen/test_pointer_lowering.py::test_pointer_plans_complete_descriptor_ownership_and_operations_before_lowering` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_caller_created_pointer_handle_tracks_native_output_association`
`tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_module_and_derived_pointer_handles_track_native_association[source]` | — | canonical | +| [Pointers: Module Variables And Derived Fields](../../docs/user/guide/pointers.md#module-variables-and-derived-fields) | Supported | native module owner; parent-retained component; stable handle identity; live native reassociation; getter-only field | `tests/fortran/pointers/policy/test_pointer_ownership_policy.py::test_plain_pointer_array_container_policy_completes_default_handle_profile`
`tests/fortran/pointers/codegen/test_pointer_lowering.py::test_pointer_plans_complete_descriptor_ownership_and_operations_before_lowering` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_pointer_descriptor_views_preserve_slice_shape_strides_and_parent_lifetime` | — | canonical | +| [Pointers: Function Results](../../docs/user/guide/pointers.md#function-results) | Supported | associated and unassociated result; wrapper-owned persistent descriptor; borrowed target; explicit close | `tests/fortran/pointers/policy/test_pointer_ownership_policy.py::test_hidden_pointer_handle_output_owns_descriptor_but_not_target_policy`
`tests/fortran/pointers/codegen/test_pointer_lowering.py::test_pointer_lowering_assigns_descriptors_without_target_deallocation` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_pointer_array_results_use_owned_descriptors_without_owning_targets[source]`
`tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_pointer_array_results_use_owned_descriptors_without_owning_targets[generated-pyi]` | — | canonical | +| [Pointers: Output And Inout Arguments](../../docs/user/guide/pointers.md#output-and-inout-arguments) | Supported | hidden nonoptional output; visible inout; present unassociated caller descriptor; same-object update | `tests/fortran/pointers/codegen/test_pointer_lowering.py::test_pointer_plans_complete_descriptor_ownership_and_operations_before_lowering` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_caller_created_pointer_handle_tracks_native_output_association`
`tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_module_and_derived_pointer_handles_track_native_association[source]` | — | canonical | | [Pointers: Complete Module Example](../../docs/user/guide/pointers.md#complete-module-example) | Supported | source generation; reviewed generated `.pyi`; source and generated-`.pyi` builds; module handle; descriptor call; nullification | `tests/fortran/pointers/pipeline/test_generated_pointer_contract.py::test_generated_pointer_contract_matches_fixture[fpointers_f90]` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_module_and_derived_pointer_handles_track_native_association[source]`
`tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_module_and_derived_pointer_handles_track_native_association[generated-pyi]` | — | canonical | | [Pointers: Contiguous And Strided Targets](../../docs/user/guide/pointers.md#contiguous-and-strided-targets) | Supported | decoded descriptor shape and strides; negative strides; live mutation; descriptor parameter; ordinary contiguous-array blocker | `tests/fortran/pointers/runtime/test_pointer_descriptor_abi.py::test_pointer_c_descriptor_helper_builds_strided_numpy_view_from_decoded_fields`
`tests/fortran/pointers/runtime/test_pointer_descriptor_abi.py::test_pointer_c_descriptor_helper_builds_negative_stride_numpy_view_from_decoded_fields` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_pointer_descriptor_views_preserve_slice_shape_strides_and_parent_lifetime` | `tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_to_numpy_contiguous_view_policy_rejects_non_contiguous_storage` (`runtime`) | canonical | | [Pointers: Check Association And Lifetime](../../docs/user/guide/pointers.md#check-association-and-lifetime) | Partially supported | unassociated short circuit; owner retention; descriptor state; external target lifetime remains native responsibility | `tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_pointer_to_numpy_short_circuits_unassociated_state_before_unsupported_policy` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_pointer_descriptor_views_preserve_slice_shape_strides_and_parent_lifetime` | — | canonical | @@ -135,26 +135,26 @@ Authoritative sources: | [Pointers: Synchronize Target Changes](../../docs/user/guide/pointers.md#synchronize-target-changes) | Partially supported | live descriptor state and views; no implicit copy or lock; synchronization remains application responsibility | `tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_to_numpy_descriptor_view_policy_never_copies_storage` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_module_and_derived_pointer_handles_track_native_association[source]` | — | canonical | | [Pointers: Scalar Pointers](../../docs/user/guide/pointers.md#scalar-pointers) | Supported | module snapshot; input/inout/output; nullable associated state; copied result; no handle API; source/generated-`.pyi` parity | `tests/fortran/pointers/semantics/test_pointer_semantics.py::test_fortran_pointer_arrays_and_scalars_preserve_descriptor_semantics`
`tests/fortran/pointers/semantics/test_pointer_semantics.py::test_pyi_pointer_handles_preserve_rank_optionality_and_scalar_state` | `tests/fortran/pointers/end_to_end/test_scalar_pointers.py::test_scalar_pointers_project_nullable_copied_values[source]`
`tests/fortran/pointers/end_to_end/test_scalar_pointers.py::test_scalar_pointers_project_nullable_copied_values[generated-pyi]` | `tests/fortran/pointers/semantics/test_pointer_semantics.py::test_scalar_pointer_results_reject_legacy_descriptor_spellings[def produce() -> Pointer[Float64]: ...\n-Procedure scalar descriptor results use a nullable value annotation]` (`semantics`) | canonical | | [Semantic `.pyi`: Pointer Array Handles](../../docs/user/reference/semantic-pyi-format.md#pointer-array-handles) | Supported | `Pointer[T[...]]`; optional absence; ten-fact `PointerPolicy`; operations; build requirements; result descriptor storage; strided extraction | `tests/fortran/pointers/policy/test_pointer_ownership_policy.py::test_complete_pointer_policy_metadata_round_trips_without_overriding_container_ownership`
`tests/fortran/pointers/pipeline/test_pointer_build_manifest.py::test_pyi_manifest_records_pointer_descriptor_interop_requirements` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_pointer_array_results_use_owned_descriptors_without_owning_targets[generated-pyi]` | `tests/fortran/pointers/policy/test_pointer_ownership_policy.py::test_pointer_policy_metadata_requires_every_fact` (`policy`) | canonical | -| [Fortran Wrapper: Pointer Arguments, Results, And Association](../../docs/user/reference/fortran-wrapper.md#pointer-arguments-results-and-association) | Partially supported | scalar and array pointer boundaries; descriptor and target ownership; module/field handles; results; outputs; strided views; explicit policy blockers | `tests/fortran/pointers/wrapper_codegen/test_pointer_lowering.py::test_pointer_lowering_assigns_descriptors_without_target_deallocation`
`tests/fortran/pointers/policy/test_pointer_ownership_policy.py::test_native_array_handle_build_requirements_are_selected_from_completed_policy` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_module_and_derived_pointer_handles_track_native_association[source]`
`tests/fortran/pointers/end_to_end/test_scalar_pointers.py::test_scalar_pointers_project_nullable_copied_values[source]` | `tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_pointer_allocation_operations_are_policy_gated_by_ops_table` (`runtime`) | canonical | +| [Fortran Wrapper: Pointer Arguments, Results, And Association](../../docs/user/reference/fortran-wrapper.md#pointer-arguments-results-and-association) | Partially supported | scalar and array pointer boundaries; descriptor and target ownership; module/field handles; results; outputs; strided views; explicit policy blockers | `tests/fortran/pointers/codegen/test_pointer_lowering.py::test_pointer_lowering_assigns_descriptors_without_target_deallocation`
`tests/fortran/pointers/policy/test_pointer_ownership_policy.py::test_native_array_handle_build_requirements_are_selected_from_completed_policy` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_module_and_derived_pointer_handles_track_native_association[source]`
`tests/fortran/pointers/end_to_end/test_scalar_pointers.py::test_scalar_pointers_project_nullable_copied_values[source]` | `tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_pointer_allocation_operations_are_policy_gated_by_ops_table` (`runtime`) | canonical | | [Memory Management: The Python Object And Its Storage](../../docs/user/guide/memory-management.md#the-python-object-and-its-storage) | Supported | Python object ownership; Python, native, wrapper, parent, descriptor, and target storage owners | `tests/fortran/memory_management/runtime/test_handle_lifecycle.py::test_runtime_handle_classes_are_public_api_exports`
`tests/fortran/memory_management/runtime/test_handle_lifecycle.py::test_generated_handle_factory_adapts_private_operations_to_runtime_protocol` | `tests/fortran/allocatables/end_to_end/test_edited_ownership.py::test_explicit_handle_ownership_uses_native_wrapper_and_result_lifetimes` | — | canonical | | [Memory Management: Live Views And Copies](../../docs/user/guide/memory-management.md#live-views-and-copies) | Supported | live mutation; explicit detached copy; fresh extraction; reallocation and reassociation boundaries | `tests/fortran/allocatables/runtime/test_allocatable_handle_protocol.py::test_allocatable_to_numpy_explicit_copy_is_independent`
`tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_to_numpy_descriptor_view_policy_never_copies_storage` | `tests/fortran/derived_types/end_to_end/test_derived_runtime_mechanisms.py::test_plain_module_derived_proxy_reads_and_writes_live_members`
`tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_pointer_descriptor_views_preserve_slice_shape_strides_and_parent_lifetime` | — | canonical | | [Memory Management: Allocatables And Pointers](../../docs/user/guide/memory-management.md#allocatables-and-pointers) | Supported | allocated versus associated; live view; allocation release; nullification; descriptor close; pointer target separation | `tests/fortran/memory_management/runtime/test_handle_lifecycle.py::test_generated_owned_handle_factory_passes_persistent_owner_to_every_operation` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[source]`
`tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_module_and_derived_pointer_handles_track_native_association[source]` | `tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_pointer_allocation_operations_are_policy_gated_by_ops_table` (`runtime`) | canonical | | [Memory Management: Closing Handles](../../docs/user/guide/memory-management.md#closing-handles) | Supported | owned close; idempotence; finalizer; failed destruction; construction rollback; closed-use rejection; borrowed close no-op | `tests/fortran/memory_management/runtime/test_handle_lifecycle.py::test_owned_handle_close_calls_destroy_once_and_blocks_later_use`
`tests/fortran/memory_management/runtime/test_handle_lifecycle.py::test_owned_handle_finalizer_calls_destroy_once`
`tests/fortran/memory_management/runtime/test_handle_lifecycle.py::test_borrowed_handle_close_and_finalizer_do_not_destroy_native_storage` | `tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_pointer_array_results_use_owned_descriptors_without_owning_targets[source]` | `tests/fortran/memory_management/runtime/test_handle_lifecycle.py::test_owned_handle_construction_requires_generated_destroy_operation` (`runtime`) | canonical | | [Memory Management: Sharing Handles Between Extensions](../../docs/user/guide/memory-management.md#sharing-handles-between-extensions) | Supported | same descriptor kind, dtype, and rank; no-copy handoff; compatible runtime record; target lifetime unchanged | `tests/fortran/pointers/runtime/test_pointer_contract_handles.py::test_generated_storage_rejects_incompatible_contract_handles[-pointer-float64-1-TypeError-cannot attach pointer descriptor storage]` | `tests/fortran/allocatables/end_to_end/test_allocatable_cross_extension.py::test_caller_created_allocatable_crosses_separately_built_extensions`
`tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_caller_created_pointer_crosses_separately_built_extensions` | `tests/fortran/pointers/runtime/test_pointer_handle_protocol.py::test_pointer_associate_rejects_incompatible_sources[other1-TypeError-dtype]` (`runtime`) | canonical | -| [Memory Management: Passing Objects To Functions](../../docs/user/guide/memory-management.md#passing-objects-to-functions) | Supported | caller ownership preserved; same-array mutation; same allocatable handle replacement; same pointer handle reassociation | `tests/fortran/arrays/wrapper_codegen/test_array_output_identity.py::test_projected_array_identity_uses_one_completed_in_place_copy_out_action` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[source]`
`tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_caller_created_pointer_handle_tracks_native_output_association` | — | canonical | +| [Memory Management: Passing Objects To Functions](../../docs/user/guide/memory-management.md#passing-objects-to-functions) | Supported | caller ownership preserved; same-array mutation; same allocatable handle replacement; same pointer handle reassociation | `tests/fortran/arrays/codegen/test_array_output_identity.py::test_projected_array_identity_uses_one_completed_in_place_copy_out_action` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[source]`
`tests/fortran/pointers/end_to_end/test_pointer_handles.py::test_caller_created_pointer_handle_tracks_native_output_association` | — | canonical | | [Memory Management: Derived Objects And Fields](../../docs/user/guide/memory-management.md#derived-objects-and-fields) | Supported | wrapper-owned instance; native-owned module object; parent-retained field; exactly-once finalization; stale storage warning | `tests/fortran/memory_management/semantics/test_memory_contract_semantics.py::test_convert_pyi_to_ir_rejects_immutable_writable_borrowed_view_argument` | `tests/fortran/memory_management/end_to_end/test_explicit_borrowed_owner.py::test_wrapper_owned_borrow_keeps_owner_alive_and_finalizes_exactly_once`
`tests/fortran/derived_types/end_to_end/test_derived_runtime_mechanisms.py::test_borrowed_child_retains_owner_and_finalizes_exactly_once` | — | canonical | | [Fortran Wrapper: Scalar Derived Descriptor Transactions](../../docs/user/reference/fortran-wrapper.md#derived-types-across-procedure-boundaries) | Supported | wrapper and module origins; absent proxies; direct and scoped address; reversible `move_alloc`; pointer transaction; rollback and exactly-once restoration | `tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_generated_artifacts_keep_matrix_dispatch_linear_and_descriptor_free` | `tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_move_alloc_round_trip_preserves_target_association`
`tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_module_descriptor_transactions_preserve_empty_and_recreated_state`
`tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_later_acquisition_failure_rolls_back_earlier_origins` | `tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_reassociable_pointer_dummy_requires_pointer_storage[module_allocatable]` (`runtime`) | canonical | | [Memory Management: Safety Checklist](../../docs/user/guide/memory-management.md#safety-checklist) | Supported | state checks; live views; explicit copies; owner-only deallocation; nullification; closed handles; caller synchronization responsibility | `tests/fortran/memory_management/runtime/test_handle_lifecycle.py::test_owned_handle_close_marks_closed_when_destroy_raises`
`tests/fortran/allocatables/runtime/test_allocatable_handle_protocol.py::test_allocatable_to_numpy_short_circuits_unallocated_state_before_generated_extraction` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_plain_allocatable_module_array_exposes_current_live_view[source]` | `tests/fortran/pointers/runtime/test_pointer_contract_handles.py::test_pointer_association_rejects_closed_handles` (`runtime`) | canonical | | [Semantic `.pyi`: Ownership, Transfer, And Destruction Policies](../../docs/user/reference/semantic-pyi-format.md#ownership-transfer-and-destruction-policies) | Supported | explicit ownership triple; by-value, call-local, in-place, copy-return, snapshot, borrowed-view, wrapper-instance, and blocked policy families; release responsibility | `tests/fortran/memory_management/policy/test_memory_ownership_policy.py::test_scalar_storage_rejects_incompatible_explicit_ownership_metadata`
`tests/fortran/memory_management/policy/test_memory_ownership_policy.py::test_contradictory_ownership_contract_fails_before_lowering` | `tests/fortran/allocatables/end_to_end/test_edited_ownership.py::test_explicit_handle_ownership_uses_native_wrapper_and_result_lifetimes`
`tests/fortran/memory_management/end_to_end/test_explicit_borrowed_owner.py::test_wrapper_owned_borrow_keeps_owner_alive_and_finalizes_exactly_once` | `tests/fortran/memory_management/semantics/test_memory_contract_semantics.py::test_convert_pyi_to_ir_rejects_immutable_writable_borrowed_view_argument` (`semantics`)
`tests/fortran/memory_management/policy/test_memory_ownership_policy.py::test_contradictory_ownership_contract_fails_before_lowering` (`policy`) | canonical | -| [Callbacks: The Short Version](../../docs/user/guide/callbacks.md#the-short-version) | Supported | primitive value and reference scalars; arrays; fixed strings; derived references and values | `tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py::test_convert_pyi_to_ir_uses_value_default_and_explicit_reference_callbacks`
`tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py::test_callback_policy_completes_value_default_and_explicit_reference_before_planning` | `tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[source]`
`tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[generated-pyi]` | — | canonical | +| [Callbacks: The Short Version](../../docs/user/guide/callbacks.md#the-short-version) | Supported | primitive value and reference scalars; arrays; fixed strings; derived references and values | `tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py::test_convert_pyi_to_ir_uses_value_default_and_explicit_reference_callbacks`
`tests/fortran/callbacks/codegen/test_callback_planning.py::test_callback_policy_completes_value_default_and_explicit_reference_before_planning` | `tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[source]`
`tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[generated-pyi]` | — | canonical | | [Callbacks: What The Callable Sees](../../docs/user/guide/callbacks.md#what-the-callable-sees) | Supported | prototype boundary; outer native-call projection; exact NumPy scalar conversion | `tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py::test_dummy_procedure_interfaces_become_complete_callable_contracts` | `tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]` | — | canonical | | [Callbacks: Small Example](../../docs/user/guide/callbacks.md#small-example) | Supported | immediate scalar reference callback; exact scalar result; source and generated-`.pyi` replay | `tests/fortran/callbacks/pipeline/test_generated_callback_contracts.py::test_callback_generated_pyi_contract_matches_fixture[fcallback_scalar_f90]` | `tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]`
`tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[generated-pyi]` | — | canonical | | [Callbacks: Choosing The Prototype Spelling](../../docs/user/guide/callbacks.md#choosing-the-prototype-spelling) | Supported | native value/reference ABI; named arguments; shape dependencies; imported prototypes | `tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py::test_convert_pyi_to_ir_uses_value_default_and_explicit_reference_callbacks`
`tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py::test_convert_pyi_to_ir_preserves_prototype_argument_names_and_dimensions`
`tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py::test_imported_prototype_resolves_as_module_interface_definition` | `tests/fortran/callbacks/end_to_end/test_array_callbacks.py::test_immediate_dummy_procedure_converts_array_arguments_and_results[source]` | `tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py::test_convert_pyi_to_ir_rejects_invalid_prototype_address_wrappers[Addr(String[8])]` (`semantics`)
`tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py::test_convert_pyi_to_ir_rejects_redundant_or_invalid_prototype_value_wrappers[Value(Float64)]` (`semantics`) | canonical | -| [Callbacks: Key Rules](../../docs/user/guide/callbacks.md#key-rules) | Supported | call scope; same-thread nested entry; exact scalar result; reference cleanup; live array and derived storage | `tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py::test_callback_artifacts_use_linear_context_adapter_and_trampoline_paths` | `tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]`
`tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[source]` | — | canonical | -| [Callbacks: Important Limitations](../../docs/user/guide/callbacks.md#important-limitations) | Blocked | persistent, optional, asynchronous, and cross-thread callbacks; optional or descriptor prototype forms; fatal exceptions and invalid returns | `tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py::test_optional_callback_retains_one_exact_policy_blocker` | `tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]` | `tests/fortran/callbacks/policy/test_callback_policy.py::test_callback_descriptor_and_optional_forms_are_blocked_before_codegen[def callback_shape(value: Allocatable[Float64]) -> None: ...-callback argument 'value' uses unsupported allocatable, pointer, polymorphic, or assumed-type storage]` (`policy`)
`tests/fortran/callbacks/policy/test_callback_policy.py::test_callback_descriptor_and_optional_forms_are_blocked_before_codegen[def callback_shape(value: Float64 = ...) -> None: ...-callback argument 'value' cannot be optional]` (`policy`) | canonical | -| [Fortran Wrapper: Immediate Python Callbacks](../../docs/user/reference/fortran-wrapper.md#immediate-python-callbacks) | Supported | completed callback ABI, conversion, lifecycle, adapter symbols, bridge declaration, source/generated parity | `tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py::test_callback_plan_projects_one_explicit_site_and_stable_roles_per_argument`
`tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py::test_callback_declaration_uses_external_unless_prototype_requires_explicit_interface` | `tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[source]`
`tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[generated-pyi]` | `tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py::test_optional_callback_retains_one_exact_policy_blocker` (`wrapper_codegen`) | canonical | -| [Feature Matrix: Immediate Call-Scoped Python Callbacks](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | scalar, array, string, and derived conversions; entering thread; source/generated parity | `tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py::test_callback_artifacts_use_linear_context_adapter_and_trampoline_paths` | `tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[source]` | — | canonical | -| [Feature Matrix: Persistent Callbacks And Procedure Pointers](../../docs/user/language-support/feature-matrix.md#unsupported-or-blocked-forms) | Blocked | stored or post-call invocation; call-scoped context | `tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py::test_callback_plan_projects_one_explicit_site_and_stable_roles_per_argument` | — | `tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py::test_optional_callback_retains_one_exact_policy_blocker` (`wrapper_codegen`) | canonical | +| [Callbacks: Key Rules](../../docs/user/guide/callbacks.md#key-rules) | Supported | call scope; same-thread nested entry; exact scalar result; reference cleanup; live array and derived storage | `tests/fortran/callbacks/codegen/test_callback_planning.py::test_callback_artifacts_use_linear_context_adapter_and_trampoline_paths` | `tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]`
`tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[source]` | — | canonical | +| [Callbacks: Important Limitations](../../docs/user/guide/callbacks.md#important-limitations) | Blocked | persistent, optional, asynchronous, and cross-thread callbacks; optional or descriptor prototype forms; fatal exceptions and invalid returns | `tests/fortran/callbacks/codegen/test_callback_planning.py::test_optional_callback_retains_one_exact_policy_blocker` | `tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]` | `tests/fortran/callbacks/policy/test_callback_policy.py::test_callback_descriptor_and_optional_forms_are_blocked_before_codegen[def callback_shape(value: Allocatable[Float64]) -> None: ...-callback argument 'value' uses unsupported allocatable, pointer, polymorphic, or assumed-type storage]` (`policy`)
`tests/fortran/callbacks/policy/test_callback_policy.py::test_callback_descriptor_and_optional_forms_are_blocked_before_codegen[def callback_shape(value: Float64 = ...) -> None: ...-callback argument 'value' cannot be optional]` (`policy`) | canonical | +| [Fortran Wrapper: Immediate Python Callbacks](../../docs/user/reference/fortran-wrapper.md#immediate-python-callbacks) | Supported | completed callback ABI, conversion, lifecycle, adapter symbols, bridge declaration, source/generated parity | `tests/fortran/callbacks/codegen/test_callback_planning.py::test_callback_plan_projects_one_explicit_site_and_stable_roles_per_argument`
`tests/fortran/callbacks/codegen/test_callback_planning.py::test_every_callback_uses_the_shared_generated_abstract_prototype` | `tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[source]`
`tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[generated-pyi]` | `tests/fortran/callbacks/codegen/test_callback_planning.py::test_optional_callback_retains_one_exact_policy_blocker` (`codegen`) | canonical | +| [Feature Matrix: Immediate Call-Scoped Python Callbacks](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | scalar, array, string, and derived conversions; entering thread; source/generated parity | `tests/fortran/callbacks/codegen/test_callback_planning.py::test_callback_artifacts_use_linear_context_adapter_and_trampoline_paths` | `tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[source]` | — | canonical | +| [Feature Matrix: Persistent Callbacks And Procedure Pointers](../../docs/user/language-support/feature-matrix.md#unsupported-or-blocked-forms) | Blocked | stored or post-call invocation; call-scoped context | `tests/fortran/callbacks/codegen/test_callback_planning.py::test_callback_plan_projects_one_explicit_site_and_stable_roles_per_argument` | — | `tests/fortran/callbacks/codegen/test_callback_planning.py::test_optional_callback_retains_one_exact_policy_blocker` (`codegen`) | canonical | | [Enumerations: Complete Example](../../docs/user/guide/enumerations.md#complete-example) | Supported | `enum, bind(C)`; explicit, implicit, negative, and symbolic values; source generation; reviewed contract; source/generated replay | `tests/fortran/enumerations/parsing/test_enum_syntax.py::test_valid_enum_subunit_accepts_optional_separator_and_multiple_enumerators`
`tests/fortran/enumerations/pipeline/test_generated_enum_contract.py::test_generated_enum_contract_matches_reviewed_package` | `tests/fortran/enumerations/end_to_end/test_enum_runtime.py::test_fortran_enums_preserve_integer_runtime_surface[source]`
`tests/fortran/enumerations/end_to_end/test_enum_runtime.py::test_fortran_enums_preserve_integer_runtime_surface[generated-pyi]` | — | canonical | | [Enumerations: Usage In Python](../../docs/user/guide/enumerations.md#usage-in-python) | Supported | module constants; integer procedure input and result; exact `np.int32` values | `tests/fortran/enumerations/semantics/test_enum_semantics.py::test_fortran_enums_preserve_values_in_generated_pyi_contract` | `tests/fortran/enumerations/end_to_end/test_enum_runtime.py::test_fortran_enums_preserve_integer_runtime_surface[source]` | — | canonical | | [Enumerations: Key Points](../../docs/user/guide/enumerations.md#key-points) | Supported | `Final[Int32]`; native value stability; exact dtype; no closed-domain validation; integer fields and results | `tests/fortran/enumerations/semantics/test_enum_compile_time_values.py::test_resolve_semantic_compile_time_values_handles_enum_like_constants`
`tests/fortran/enumerations/semantics/test_pyi_enum_constants.py::test_convert_pyi_to_ir_round_trips_enum_like_integer_constants` | `tests/fortran/enumerations/end_to_end/test_enum_runtime.py::test_fortran_enums_preserve_integer_runtime_surface[source]` | — | canonical | @@ -162,24 +162,24 @@ Authoritative sources: | [Semantic `.pyi`: Constants And Enums](../../docs/user/reference/semantic-pyi-format.md#constants-and-enums) | Supported | `Final[T]`; literal and symbolic initializers; round trip; ordinary integer arguments/results | `tests/fortran/enumerations/semantics/test_pyi_enum_constants.py::test_convert_pyi_to_ir_round_trips_enum_like_integer_constants` | `tests/fortran/enumerations/end_to_end/test_enum_runtime.py::test_fortran_enums_preserve_integer_runtime_surface[generated-pyi]` | `tests/fortran/enumerations/semantics/test_unsupported_enum_classes.py::test_convert_pyi_to_ir_rejects_enum_classes` (`semantics`) | canonical | | [Fortran Wrapper: Fortran Enums](../../docs/user/reference/fortran-wrapper.md#fortran-enums) | Supported | typed integer constants; `bind(C)` metadata; integer procedure and field surface; no generated enum class | `tests/fortran/enumerations/semantics/test_enum_semantics.py::test_fortran_enums_preserve_values_in_generated_pyi_contract` | `tests/fortran/enumerations/end_to_end/test_enum_runtime.py::test_fortran_enums_preserve_integer_runtime_surface[source]` | — | canonical | | [Feature Matrix: Fortran Enum Constants](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | integer constants, field and procedure values, source/generated parity, malformed syntax diagnostics | `tests/fortran/enumerations/parsing/test_enum_diagnostics.py::test_enum_diagnostic_reports_first_invalid_line_after_valid_enumerator`
`tests/fortran/enumerations/semantics/test_enum_semantics.py::test_fortran_enums_preserve_values_in_generated_pyi_contract` | `tests/fortran/enumerations/end_to_end/test_enum_runtime.py::test_fortran_enums_preserve_integer_runtime_surface[source]` | `tests/fortran/enumerations/parsing/test_enum_syntax.py::test_enum_subunit_rejects_malformed_lines_and_nested_units[interface invalid]` (`parsing`) | canonical | -| [Raw Addresses: Checked Storage Or Raw Address](../../docs/user/guide/raw-addresses.md#checked-storage-or-raw-address) | Supported | checked rank-zero NumPy storage versus integer raw address; exact dtype/rank/itemsize/writeability validation only on checked storage | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_completes_fixed_string_storage_and_raw_address_ownership`
`tests/fortran/raw_addresses/wrapper_codegen/test_scalar_address_lowering.py::test_scalar_storage_and_raw_address_plans_keep_explicit_boundary_facts` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | — | canonical | +| [Raw Addresses: Checked Storage Or Raw Address](../../docs/user/guide/raw-addresses.md#checked-storage-or-raw-address) | Supported | checked rank-zero NumPy storage versus integer raw address; exact dtype/rank/itemsize/writeability validation only on checked storage | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_completes_fixed_string_storage_and_raw_address_ownership`
`tests/fortran/raw_addresses/codegen/test_scalar_address_lowering.py::test_scalar_storage_and_raw_address_plans_keep_explicit_boundary_facts` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | — | canonical | | [Raw Addresses: `Addr(T)` And `Addr(Arg(...))`](../../docs/user/guide/raw-addresses.md#addrt-and-addrarg) | Supported | type-level Python-visible integer address; native-call address projection; primitive-scalar projection restriction; no conflation of the two forms | `tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py::test_public_raw_address_contract_round_trips`
`tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py::test_native_call_addr_arg_rejects_non_primitive_scalar_values[Addr(Float64)]` | — | `tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py::test_native_call_address_projection_rejects_non_argument_storage[Addr(Return(0))-Float64]` (`semantics`) | canonical | -| [Raw Addresses: Complete Example](../../docs/user/guide/raw-addresses.md#complete-example) | Supported | reviewed edited contract; primitive, vector, C-order matrix, Fortran-order matrix, and fixed-string address calls; checked-storage comparison | `tests/fortran/raw_addresses/wrapper_codegen/test_raw_array_lowering.py::test_raw_array_addresses_use_one_shared_transfer_and_shape_plan`
`tests/fortran/raw_addresses/wrapper_codegen/test_string_address_lowering.py::test_string_addresses_dispatch_to_named_binding_and_bridge_lowering` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | — | canonical | -| [Raw Addresses: Primitive Address](../../docs/user/guide/raw-addresses.md#primitive-address) | Supported | integer pointer extraction; direct handoff; mutation; non-integer rejection; address-range overflow | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_completes_primitive_raw_address_handoff`
`tests/fortran/raw_addresses/wrapper_codegen/test_scalar_address_lowering.py::test_scalar_storage_and_raw_address_lower_to_direct_named_paths` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` (`runtime`) | canonical | -| [Raw Addresses: Array Address](../../docs/user/guide/raw-addresses.md#array-address) | Supported | fully resolved rank and extents; C and Fortran orientation; same transfer action; mutation without descriptor or copy lifecycle | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_completes_required_raw_array_address_handoff`
`tests/fortran/raw_addresses/wrapper_codegen/test_raw_array_lowering.py::test_raw_array_addresses_reuse_integer_extraction_and_named_array_bridge_association` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_keeps_optional_raw_array_addresses_blocked` (`policy`)
`tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_keeps_projected_raw_array_addresses_blocked` (`policy`) | canonical | -| [Raw Addresses: Fixed-String Address](../../docs/user/guide/raw-addresses.md#fixed-string-address) | Supported | fixed scalar and rank-one string pointees; exact encoded width; integer address handoff; in-place mutation | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_completes_fixed_string_storage_and_raw_address_ownership`
`tests/fortran/raw_addresses/wrapper_codegen/test_string_address_lowering.py::test_string_address_plans_keep_completed_ownership_length_and_copy_facts` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build`
`tests/fortran/raw_addresses/end_to_end/test_raw_fixed_string_arrays.py::test_raw_fixed_width_character_arrays_use_canonical_plan` | — | canonical | -| [Raw Addresses: Safety Rules](../../docs/user/guide/raw-addresses.md#safety-rules) | Supported | exact owner lifetime remains caller responsibility; no dtype, shape, layout, writeability, or target-lifetime validation; zero, negative, and arbitrary in-range integers are forwarded; unsafe dereference remains deliberately unexecuted | `tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py::test_raw_address_policy_accepts_only_complete_primitive_layouts`
`tests/fortran/raw_addresses/wrapper_codegen/test_raw_array_lowering.py::test_raw_array_addresses_use_one_shared_transfer_and_shape_plan` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | `tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py::test_raw_address_policy_rejects_incomplete_or_wrapped_pointees[Addr(Float64[:])-raw arrays require a fully resolved rank and shape]` (`semantics`) | canonical | -| [Semantic `.pyi`: Python And Native Boundaries](../../docs/user/reference/semantic-pyi-format.md#python-and-native-boundaries) | Supported | Python integer raw address; direct native address; distinct checked-storage and call-local address boundaries | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_completes_primitive_raw_address_handoff`
`tests/fortran/raw_addresses/wrapper_codegen/test_scalar_address_lowering.py::test_scalar_storage_and_raw_address_plans_keep_explicit_boundary_facts` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | — | canonical | +| [Raw Addresses: Complete Example](../../docs/user/guide/raw-addresses.md#complete-example) | Supported | reviewed edited contract; primitive, vector, C-order matrix, Fortran-order matrix, and fixed-string address calls; checked-storage comparison | `tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py::test_raw_array_addresses_use_one_shared_transfer_and_shape_plan`
`tests/fortran/raw_addresses/codegen/test_string_address_lowering.py::test_string_addresses_dispatch_to_named_binding_and_bridge_lowering` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | — | canonical | +| [Raw Addresses: Primitive Address](../../docs/user/guide/raw-addresses.md#primitive-address) | Supported | integer pointer extraction; direct handoff; mutation; non-integer rejection; address-range overflow | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_completes_primitive_raw_address_handoff`
`tests/fortran/raw_addresses/codegen/test_scalar_address_lowering.py::test_scalar_storage_and_raw_address_lower_to_direct_named_paths` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` (`runtime`) | canonical | +| [Raw Addresses: Array Address](../../docs/user/guide/raw-addresses.md#array-address) | Supported | fully resolved rank and extents; C and Fortran orientation; same transfer action; mutation without descriptor or copy lifecycle | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_completes_required_raw_array_address_handoff`
`tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py::test_raw_array_addresses_reuse_integer_extraction_and_named_array_bridge_association` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_keeps_optional_raw_array_addresses_blocked` (`policy`)
`tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_keeps_projected_raw_array_addresses_blocked` (`policy`) | canonical | +| [Raw Addresses: Fixed-String Address](../../docs/user/guide/raw-addresses.md#fixed-string-address) | Supported | fixed scalar and rank-one string pointees; exact encoded width; integer address handoff; in-place mutation | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_completes_fixed_string_storage_and_raw_address_ownership`
`tests/fortran/raw_addresses/codegen/test_string_address_lowering.py::test_string_address_plans_keep_completed_ownership_length_and_copy_facts` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build`
`tests/fortran/raw_addresses/end_to_end/test_raw_fixed_string_arrays.py::test_raw_fixed_width_character_arrays_use_canonical_plan` | — | canonical | +| [Raw Addresses: Safety Rules](../../docs/user/guide/raw-addresses.md#safety-rules) | Supported | exact owner lifetime remains caller responsibility; no dtype, shape, layout, writeability, or target-lifetime validation; zero, negative, and arbitrary in-range integers are forwarded; unsafe dereference remains deliberately unexecuted | `tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py::test_raw_address_policy_accepts_only_complete_primitive_layouts`
`tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py::test_raw_array_addresses_use_one_shared_transfer_and_shape_plan` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | `tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py::test_raw_address_policy_rejects_incomplete_or_wrapped_pointees[Addr(Float64[:])-raw arrays require a fully resolved rank and shape]` (`semantics`) | canonical | +| [Semantic `.pyi`: Python And Native Boundaries](../../docs/user/reference/semantic-pyi-format.md#python-and-native-boundaries) | Supported | Python integer raw address; direct native address; distinct checked-storage and call-local address boundaries | `tests/fortran/raw_addresses/policy/test_raw_address_policy.py::test_wrapper_policy_completes_primitive_raw_address_handoff`
`tests/fortran/raw_addresses/codegen/test_scalar_address_lowering.py::test_scalar_storage_and_raw_address_plans_keep_explicit_boundary_facts` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | — | canonical | | [Semantic `.pyi`: Storage Contracts](../../docs/user/reference/semantic-pyi-format.md#storage-contracts) | Partially supported | `Addr(T)` for primitive scalar, fixed string, and fully resolved arrays; one-level pointer depth; deliberate wrapped, unresolved, optional, projected-array, and callable limits | `tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py::test_public_raw_address_contract_round_trips`
`tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py::test_raw_address_syntax_rejects_multiple_pointees_and_explicit_depth_one[value: Addr[1](Int32)\n-Addr[1](...) is invalid; use Addr(...)]` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | `tests/fortran/raw_addresses/semantics/test_raw_address_semantics.py::test_raw_address_policy_rejects_incomplete_or_wrapped_pointees[Addr(String)-raw strings require a fixed length]` (`semantics`) | canonical | -| [Fortran Wrapper: Scalar Calls And Verified Baseline](../../docs/user/reference/fortran-wrapper.md#scalar-calls-and-verified-baseline) | Supported | raw scalar addresses and checked rank-zero storage use distinct completed plans and named lowering paths | `tests/fortran/raw_addresses/wrapper_codegen/test_scalar_address_lowering.py::test_scalar_storage_and_raw_address_lower_to_direct_named_paths` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | — | canonical | -| [Error Handling: Failure Stages](../../docs/user/guide/error-handling.md#failure-stages) | Supported | parsing, interface conversion, wrapper planning, compilation/linking, import, Python-call validation, native status, and callback-fatal boundaries remain distinct | `tests/fortran/error_handling/parsing/test_fortran_diagnostics.py::test_parse_error_formats_compiler_style_diagnostic`
`tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py::test_planner_records_editable_native_runtime_and_status_error_facts` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | `tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]` (`runtime`) | canonical | +| [Fortran Wrapper: Scalar Calls And Verified Baseline](../../docs/user/reference/fortran-wrapper.md#scalar-calls-and-verified-baseline) | Supported | raw scalar addresses and checked rank-zero storage use distinct completed plans and named lowering paths | `tests/fortran/raw_addresses/codegen/test_scalar_address_lowering.py::test_scalar_storage_and_raw_address_lower_to_direct_named_paths` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build` | — | canonical | +| [Error Handling: Failure Stages](../../docs/user/guide/error-handling.md#failure-stages) | Supported | parsing, interface conversion, wrapper planning, compilation/linking, import, Python-call validation, native status, and callback-fatal boundaries remain distinct | `tests/fortran/error_handling/parsing/test_fortran_diagnostics.py::test_parse_error_formats_compiler_style_diagnostic`
`tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_planner_records_editable_native_runtime_and_status_error_facts` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | `tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]` (`runtime`) | canonical | | [Error Handling: Verbose Output And Tracebacks](../../docs/user/guide/error-handling.md#verbose-output-and-tracebacks) | Supported | concise expected diagnostic; full `--debug` traceback; replayable verbose native command | `tests/fortran/error_handling/compiling/test_verbose_commands.py::test_run_command_verbose_prints_replayable_command` | `tests/fortran/error_handling/pipeline/test_concise_cli_diagnostics.py::test_cli_formats_parse_errors_without_traceback`
`tests/fortran/error_handling/pipeline/test_debug_cli_tracebacks.py::test_cli_debug_flag_reraises_parse_errors` | — | canonical | -| [Error Handling: Status Projection Example](../../docs/user/guide/error-handling.md#status-projection-example) | Supported | edited `@native_call` hidden status/message projection; `@raises`; success value; `None` on success; exact `RuntimeError` message; repeated failure cleanup and recovery | `tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_runtime_policy_decorators_round_trip_through_pyi`
`tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py::test_direct_bridge_lowering_projects_status_and_copies_fixed_message` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | — | canonical | +| [Error Handling: Status Projection Example](../../docs/user/guide/error-handling.md#status-projection-example) | Supported | edited `@native_call` hidden status/message projection; `@raises`; success value; `None` on success; exact `RuntimeError` message; repeated failure cleanup and recovery | `tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_runtime_policy_decorators_round_trip_through_pyi`
`tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_direct_bridge_lowering_projects_status_and_copies_fixed_message` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | — | canonical | | [Error Handling: Common Python Exceptions](../../docs/user/guide/error-handling.md#common-python-exceptions) | Supported | boundary `TypeError`; contract/option and parse `ValueError`; projected native `RuntimeError`; native artifact import/load error taxonomy | `tests/fortran/error_handling/parsing/test_fortran_diagnostics.py::test_parse_error_is_subclass_of_value_error`
`tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_runtime_status_policy_rejects_invalid_output_contracts[@raises(status="status")\ndef solve(status: Int32) -> None: ...-status target must name a hidden output]` | `tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py::test_primitive_array_and_fixed_string_raw_addresses_share_one_native_build`
`tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | — | canonical | | [Error Handling: Best Practices](../../docs/user/guide/error-handling.md#best-practices) | Supported | full diagnostic first; verbose command replay; debug traceback only on demand; edited-contract inspection; risky callback isolation | `tests/fortran/error_handling/parsing/test_fortran_diagnostics.py::test_parse_error_message_includes_filename_and_lineno`
`tests/fortran/error_handling/compiling/test_verbose_commands.py::test_run_command_verbose_prints_replayable_command` | `tests/fortran/error_handling/pipeline/test_debug_cli_tracebacks.py::test_cli_debug_flag_reraises_parse_errors`
`tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]` | — | canonical | -| [Fortran Wrapper: Wrapper Errors And Fortran Errors](../../docs/user/reference/fortran-wrapper.md#wrapper-errors-and-fortran-errors) | Supported | ordinary wrapper exceptions; no inferred application convention; opt-in status/message projection; cleanup after failure; native termination remains unrecoverable | `tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py::test_direct_binding_lowering_places_only_opted_in_native_call_outside_the_gil`
`tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py::test_fixed_message_bridge_copy_requires_its_completed_reason` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | `tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]` (`runtime`) | canonical | -| [Feature Matrix: Runtime Error Projection, GIL Policy, Recursion, OpenMP Path, And GNU ABI Checks](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | status error and message; completed GIL envelope; recursion/OpenMP/ABI remain separately owned; no caller synchronization inference | `tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py::test_planner_records_editable_native_runtime_and_status_error_facts` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | — | canonical | -| [Building The Shared Library: Build](../../docs/user/guide/building-shared-library.md#build) | Supported | source input; default and explicit module names; build directory; generated sources; importable shared library | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_source_build_result_records_structured_native_plan`
`tests/fortran/building_shared_library/pipeline/test_source_generated_contracts.py::test_source_build_generated_pyi_contract_matches_fixture[fruntime_abi_f90]` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_documented_homepage_points_example_builds_and_imports` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_wrapper_build_rejects_empty_source_list` (`pipeline`)
`tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_wrapper_build_rejects_missing_source` (`pipeline`) | canonical | +| [Fortran Wrapper: Wrapper Errors And Fortran Errors](../../docs/user/reference/fortran-wrapper.md#wrapper-errors-and-fortran-errors) | Supported | ordinary wrapper exceptions; no inferred application convention; opt-in status/message projection; cleanup after failure; native termination remains unrecoverable | `tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_direct_binding_lowering_places_only_opted_in_native_call_outside_the_gil`
`tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_fixed_message_bridge_copy_requires_its_completed_reason` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | `tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]` (`runtime`) | canonical | +| [Feature Matrix: Runtime Error Projection, GIL Policy, Recursion, OpenMP Path, And GNU ABI Checks](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | status error and message; completed GIL envelope; recursion/OpenMP/ABI remain separately owned; no caller synchronization inference | `tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_planner_records_editable_native_runtime_and_status_error_facts` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | — | canonical | +| [Building The Shared Library: Build](../../docs/user/guide/building-shared-library.md#build) | Supported | source input; default and explicit module names; build directory; generated sources; importable shared library | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_source_build_result_records_structured_native_plan`
`tests/fortran/building_shared_library/pipeline/test_source_generated_contracts.py::test_source_build_generated_pyi_contract_matches_fixture[fruntime_abi_f90]` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_documented_readme_points_example_builds_and_imports` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_wrapper_build_rejects_empty_source_list` (`pipeline`)
`tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_wrapper_build_rejects_missing_source` (`pipeline`) | canonical | | [Building The Shared Library: Import](../../docs/user/guide/building-shared-library.md#import) | Supported | ABI-suffixed artifact; stable module import name; explicit output name; root-function name collision avoidance | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_dir_separates_abi_artifact_from_cli_alias` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_names_importable_shared_library`
`tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_default_module_name_does_not_collide_with_root_function` | — | canonical | | [Building The Shared Library: Multiple Source Files](../../docs/user/guide/building-shared-library.md#multiple-source-files) | Supported | caller order; contained-module namespaces; standalone externals; one merged extension; generated and edited contract parity | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_multi_source_pyi_out_writes_one_flat_combined_package` | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_multi_file_modules_build_one_merged_extension`
`tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_multi_file_standalone_procedures_build_one_merged_extension` | `tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_missing_module_directory_reports_compile_error` (`compiling`) | canonical | | [Building The Shared Library: Use A Makefile](../../docs/user/guide/building-shared-library.md#use-a-makefile) | Supported | generation without compilation; editable compiler and flags; ordered source dependencies; GNU Make build; manifest regeneration and replay | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_pyi_makefile_manifest_and_replay_workflows` | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_makefile_mode_reproduces_multi_source_build` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_wrapper_build_rejects_generation_verbose_combination[makefile]` (`pipeline`) | canonical | @@ -198,7 +198,7 @@ Authoritative sources: | [Semantic `.pyi`: Imported Derived-Type Identity](../../docs/user/reference/semantic-pyi-format.md#imported-derived-type-identity) | Supported | direct, aliased, relative, qualified, opaque, and edited wrapped external type identity | `tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_pyi_paths_to_semantic_modules_reconciles_opaque_and_edited_external_types`
`tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_pyi_paths_to_semantic_modules_reconciles_relative_namespace_type_refs` | — | `tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_pyi_paths_to_semantic_modules_handles_duplicate_roots_and_ambiguous_module_names` (`semantics`) | canonical | | [Semantic `.pyi`: Contract Files And Native Procedure Placement](../../docs/user/reference/semantic-pyi-format.md#contract-files-and-native-procedure-placement) | Supported | entry contract; native module leaves; standalone root declarations; multiple modules; same-name module collision | `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_multi_module_generation_keeps_each_native_namespace`
`tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_same_named_module_uses_init_entry_and_keeps_externals_at_root` | `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback` | — | canonical | | [Semantic `.pyi`: Contained Module Procedures](../../docs/user/reference/semantic-pyi-format.md#contained-module-procedures) | Supported | filename-selected native module scope; child Python namespace; exact native procedure name | `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_module_generation_writes_explicit_package_entry_and_native_leaf`
`tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_generated_native_scope_comes_from_contract_filename` | `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback` | — | canonical | -| [Semantic `.pyi`: Standalone External Procedures](../../docs/user/reference/semantic-pyi-format.md#standalone-external-procedures) | Supported | `@external`; entry placement; multiple root procedures; no invented module scope | `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_standalone_generation_writes_explicit_package_entry`
`tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_generated_standalone_contract_retains_external_native_placement` | — | — | canonical | +| [Semantic `.pyi`: Standalone Procedures](../../docs/user/reference/semantic-pyi-format.md#standalone-procedures) | Supported | `@standalone`; entry placement; multiple root procedures; no invented module scope | `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_standalone_generation_writes_explicit_package_entry`
`tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_generated_standalone_contract_retains_standalone_native_placement` | — | — | canonical | | [Semantic `.pyi`: Source-To-Contract Layout](../../docs/user/reference/semantic-pyi-format.md#source-to-contract-layout) | Supported | module-only, standalone-only, mixed, multi-module, same-name, and transitive-import source layouts | `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_import_graph_generation_writes_entry_and_native_leaves`
`tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_multi_module_generation_keeps_each_native_namespace` | `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback` | — | canonical | | [Semantic `.pyi`: Root Export Contract](../../docs/user/reference/semantic-pyi-format.md#root-export-contract) | Supported | module import, selective symbol export, alias, support-import exclusion, and collision rejection | `tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_checked_entry_discovers_its_complete_contract_package[contract_import_graph]`
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_rejects_colliding_wildcard_exports` (`pipeline`) | canonical | | [Semantic `.pyi`: Entry Contract And Extension Identity](../../docs/user/reference/semantic-pyi-format.md#entry-contract-and-extension-identity) | Supported | `__init__.pyi` parent identity; explicit output identity; leaf identity; ABI-suffixed shared object | `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_same_named_module_uses_init_entry_and_keeps_externals_at_root` | `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback`
`tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_dir_separates_abi_artifact_from_cli_alias` | — | canonical | @@ -217,18 +217,18 @@ Authoritative sources: | [Semantic `.pyi`: Remaining Format And Runtime Work](../../docs/user/reference/semantic-pyi-format.md#remaining-format-and-runtime-work) | Partially supported | implemented ordered projection and policy dispatch; broader polymorphism, pointer lifetimes, and IDE-only stub separation remain limited | `tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py::test_fortran_to_pyi_and_back_preserves_mixed_input_output_projection` | — | `tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_rejects_generic_constructor_interfaces_during_semantic_conversion` (`semantics`)
`tests/fortran/allocatables/policy/test_allocatable_result_policy.py::test_direct_allocatable_scalar_function_result_is_blocked_before_codegen` (`policy`) | canonical | | [`.pyi` Exports And Modules: Choose The Package Shape](../../docs/user/reference/pyi-contracts/exports-and-modules.md#choose-the-package-shape) | Supported | child namespaces; wildcard flattening; selective imports; symbol and module aliases; nested aliases; support-import exclusion; reachable declarations only | `tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering`
`tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_checked_entry_discovers_its_complete_contract_package[contract_import_graph]` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_rejects_colliding_wildcard_exports` (`pipeline`)
`tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_recursive_graph_reports_missing_relative_contract_before_native_validation` (`pipeline`)
`tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_recursive_graph_reports_cycles_before_codegen` (`pipeline`) | canonical | | [`.pyi` Exports And Modules: Remove Or Hide A Declaration](../../docs/user/reference/pyi-contracts/exports-and-modules.md#remove-or-hide-a-declaration) | Supported | deleted function and variable; `@private`; `private[...]`; class constructor suppression; later class/member/overload runtime owner retained | `tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering`
`tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_removing_constructor_suppresses_generated_keyword_initialization` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | — | canonical | -| [`.pyi` Exports And Modules: Add Or Rename A Native Procedure](../../docs/user/reference/pyi-contracts/exports-and-modules.md#add-or-rename-a-native-procedure) | Supported | added module-leaf declaration; `@bind`; renamed standalone `@external`; unchanged native targets; no invented implementation | `tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py::test_convert_pyi_to_ir_preserves_user_private_bound_function_contract` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | — | canonical | -| [`.pyi` Exports And Modules: Set Module Values At Import](../../docs/user/reference/pyi-contracts/exports-and-modules.md#set-module-values-at-import) | Supported | mutable Boolean, integer, real, and complex literals; import-time write-through; `Final` constant distinction; unsupported setter/storage and expression rejection | `tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_literal_defaults_are_preserved`
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_module_variable_initializer_policy_is_complete_before_ir_lowering`
`tests/fortran/pyi_contracts/exports_and_modules/wrapper_codegen/test_module_initializer_lowering.py::test_module_variable_literal_families_select_their_c_spelling` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | `tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_expression_defaults_are_rejected[from prik.contracts import Int32\ncounter: Int32 = f(42)\n]` (`semantics`)
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_unsupported_module_variable_initializer_completes_an_unsupported_policy` (`policy`) | canonical | +| [`.pyi` Exports And Modules: Add Or Rename A Native Procedure](../../docs/user/reference/pyi-contracts/exports-and-modules.md#add-or-rename-a-native-procedure) | Supported | added module-leaf declaration; `@bind`; renamed standalone `@standalone`; unchanged native targets; no invented implementation | `tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py::test_convert_pyi_to_ir_preserves_user_private_bound_function_contract` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | — | canonical | +| [`.pyi` Exports And Modules: Set Module Values At Import](../../docs/user/reference/pyi-contracts/exports-and-modules.md#set-module-values-at-import) | Supported | mutable Boolean, integer, real, and complex literals; import-time write-through; `Final` constant distinction; unsupported setter/storage and expression rejection | `tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_literal_defaults_are_preserved`
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_module_variable_initializer_policy_is_complete_before_ir_lowering`
`tests/fortran/pyi_contracts/exports_and_modules/codegen/test_module_initializer_lowering.py::test_module_variable_literal_families_select_their_c_spelling` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | `tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_expression_defaults_are_rejected[from prik.contracts import Int32\ncounter: Int32 = f(42)\n]` (`semantics`)
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_unsupported_module_variable_initializer_completes_an_unsupported_policy` (`policy`) | canonical | | [`.pyi` Functions And Classes: Expose A Module Procedure As A Method](../../docs/user/reference/pyi-contracts/functions-and-classes.md#expose-a-module-procedure-as-a-method) | Supported | retained module declaration; `Pass()` receiver placement; public or private module surface; same or bound method target | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_method_and_module_declarations_keep_native_targets_independent`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_module_procedure_method_visibility_is_completed_independently` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | — | canonical | | [`.pyi` Functions And Classes: Edit An Overload Set](../../docs/user/reference/pyi-contracts/functions-and-classes.md#edit-an-overload-set) | Supported | deleted and added candidates; exact dtype dispatch; module and class `@bind`; private-specific routing; native-private accessibility retained | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_edited_overloads_complete_exact_dispatch_and_reject_ambiguous_plan` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract`
`tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_editable_contract_removes_class_method_constructor_member_and_overload` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_private_native_specific_without_overload_bind_fails_at_build[private_module_specifics_without_bind-missing_targets0]` (`compiling`)
`tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_private_native_specific_without_overload_bind_fails_at_build[private_type_bound_specifics_without_bind-missing_targets1]` (`compiling`) | canonical | -| [`.pyi` Functions And Classes: Replace The Constructor](../../docs/user/reference/pyi-contracts/functions-and-classes.md#replace-the-constructor) | Supported | direct native initializer; one explicit `Pass()`; reordered native position; generated constructor replacement or removal; overload constructor | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bound_constructor_uses_explicit_pass_position_and_native_target`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_bound_constructor_and_method_reuse_completed_direct_function_plans`
`tests/fortran/pyi_contracts/functions_and_classes/wrapper_codegen/test_constructor_lowering.py::test_bound_constructor_generates_one_initializer_without_keyword_default` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function`
`tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected[\nclass state:\n @bind("init_state")\n @native_call([Addr(Arg(0))])\n def __init__(self, seed: Int32) -> None: ...\n-Bound constructor native_call requires exactly one Pass() entry]` (`semantics`) | canonical | +| [`.pyi` Functions And Classes: Replace The Constructor](../../docs/user/reference/pyi-contracts/functions-and-classes.md#replace-the-constructor) | Supported | direct native initializer; one explicit `Pass()`; reordered native position; generated constructor replacement or removal; overload constructor | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bound_constructor_uses_explicit_pass_position_and_native_target`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_bound_constructor_and_method_reuse_completed_direct_function_plans`
`tests/fortran/pyi_contracts/functions_and_classes/codegen/test_constructor_lowering.py::test_bound_constructor_generates_one_initializer_without_keyword_default` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function`
`tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected[\nclass state:\n @bind("init_state")\n @native_call([Addr(Arg(0))])\n def __init__(self, seed: Int32) -> None: ...\n-Bound constructor native_call requires exactly one Pass() entry]` (`semantics`) | canonical | | [`.pyi` Functions And Classes: Type-Bound And Magic Methods](../../docs/user/reference/pyi-contracts/functions-and-classes.md#type-bound-and-magic-methods) | Supported | concrete native targets; passed object; bound Python/native names; overloaded type-bound calls; operators and assignment retain exact candidate mapping | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets`
`tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_defined_operators_assignment_and_type_bound_operators` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract`
`tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py::test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extension[source]` | `tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py::test_convert_pyi_to_ir_rejects_invalid_prik_overload_links[\ndef compare(left: item, right: item) -> Bool: ...\nclass item:\n @overload("compare", generic="operator(.eqv.)")\n def __add__(self, right: item) -> Bool: ...\n-generic 'operator\\(\\.eqv\\.\\)' is incompatible with method '__add__']` (`semantics`) | canonical | | [`.pyi` Calls And Results: Expose Native Arguments Directly](../../docs/user/reference/pyi-contracts/calls-and-results.md#expose-native-arguments-directly) | Supported | no `@native_call`; native-order scalar, rank-zero storage, array, fixed string, and derived object arguments; visible caller mutation and discarded string-temporary mutation | `tests/fortran/pyi_contracts/calls_and_results/policy/test_call_and_result_policy.py::test_native_order_and_projected_result_positions_are_completed_before_planning` | `tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_native_order_exposes_writable_slots_without_projection` | — | canonical | -| [`.pyi` Calls And Results: Reorder Arguments And Project Outputs](../../docs/user/reference/pyi-contracts/calls-and-results.md#reorder-arguments-and-project-outputs) | Supported | reordered `Arg`/`Addr(Arg)`; hidden scalar, fixed string, and fixed-array results; caller arrays and derived objects; multiple-result tuple order; typed literals and complete projection grammar | `tests/fortran/pyi_contracts/calls_and_results/policy/test_call_and_result_policy.py::test_native_order_and_projected_result_positions_are_completed_before_planning`
`tests/fortran/pyi_contracts/calls_and_results/wrapper_codegen/test_call_and_result_lowering.py::test_plan_records_reordered_arguments_gil_behavior_and_hidden_result_slots`
`tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py::test_native_call_accepts_hidden_native_values` | `tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_native_call_reorders_arguments_and_projects_mixed_results`
`tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_hidden_fixed_shape_array_output_is_allocated_and_returned` | `tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_pyi_python_api_rejects_invalid_projection_before_codegen` (`pipeline`) | canonical | -| [`.pyi` Calls And Results: Control Mutation](../../docs/user/reference/pyi-contracts/calls-and-results.md#control-mutation) | Supported | immutable scalar, fixed string, array, and derived replacement results; unchanged Python inputs; copy-in/copy-out and identity writeback paths | `tests/fortran/pyi_contracts/calls_and_results/policy/test_call_and_result_policy.py::test_immutable_replacement_policy_is_complete_before_ir_lowering`
`tests/fortran/pyi_contracts/calls_and_results/wrapper_codegen/test_call_and_result_lowering.py::test_replacement_writeback_dispatches_selected_scalar_result_behavior[copy_in_out]` | `tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_immutable_values_return_replacements_without_mutating_inputs` | `tests/fortran/memory_management/policy/test_memory_ownership_policy.py::test_contradictory_ownership_contract_fails_before_lowering` (`policy`) | canonical | -| [`.pyi` Calls And Results: Edit Types Shapes Layout And Optionality](../../docs/user/reference/pyi-contracts/calls-and-results.md#edit-types-shapes-layout-and-optionality) | Supported | fixed/open shapes; exact dtype, rank, layout, writeability, byte order, alignment, and zero-size checks; Fortran-order default; supported nullable/defaulted native optionals | `tests/fortran/arrays/wrapper_codegen/test_dense_array_shape_lowering.py::test_dense_array_lowering_uses_planned_shape_checks_and_bridge_orientation`
`tests/fortran/optional_arguments/policy/test_optional_policy.py::test_optional_scalar_policy_completes_nullable_value_presence_before_planning` | `tests/fortran/arrays/end_to_end/test_array_contract_validation.py::test_remaining_array_contracts_are_validated_before_fortran_calls[source]`
`tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]` | `tests/fortran/optional_arguments/policy/test_optional_policy.py::test_optional_passed_procedure_is_blocked_before_codegen` (`policy`) | canonical | -| [`.pyi` Calls And Results: Translate Status Results Into Exceptions](../../docs/user/reference/pyi-contracts/calls-and-results.md#translate-status-results-into-exceptions) | Supported | named hidden scalar integer status; optional hidden string message; configurable success value; consumed projected outputs | `tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_status_projection_accepts_an_optional_missing_message_target`
`tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py::test_runtime_plan_edits_dispatch_to_named_lowering_and_validate_roles` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | `tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_runtime_status_policy_rejects_invalid_output_contracts[@raises(status="status", message="message")\ndef solve() -> tuple[Returns["status", Int32], Returns["message", Int32]]: ...-must be a scalar string hidden output]` (`policy`) | canonical | -| [`.pyi` Calls And Results: Release The GIL For A Native Call](../../docs/user/reference/pyi-contracts/calls-and-results.md#release-the-gil-for-a-native-call) | Supported | ordinary held call; explicit released call; status conversion after reacquisition; callback trampoline reacquisition | `tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_runtime_policy_decorators_round_trip_through_pyi`
`tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py::test_direct_binding_lowering_places_only_opted_in_native_call_outside_the_gil` | `tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[source]` | — | canonical | +| [`.pyi` Calls And Results: Reorder Arguments And Project Outputs](../../docs/user/reference/pyi-contracts/calls-and-results.md#reorder-arguments-and-project-outputs) | Supported | reordered `Arg`/`Addr(Arg)`; hidden scalar, fixed string, and fixed-array results; caller arrays and derived objects; multiple-result tuple order; typed literals and complete projection grammar | `tests/fortran/pyi_contracts/calls_and_results/policy/test_call_and_result_policy.py::test_native_order_and_projected_result_positions_are_completed_before_planning`
`tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py::test_plan_records_reordered_arguments_gil_behavior_and_hidden_result_slots`
`tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py::test_native_call_accepts_hidden_native_values` | `tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_native_call_reorders_arguments_and_projects_mixed_results`
`tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_hidden_fixed_shape_array_output_is_allocated_and_returned` | `tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_pyi_python_api_rejects_invalid_projection_before_codegen` (`pipeline`) | canonical | +| [`.pyi` Calls And Results: Control Mutation](../../docs/user/reference/pyi-contracts/calls-and-results.md#control-mutation) | Supported | immutable scalar, fixed string, array, and derived replacement results; unchanged Python inputs; copy-in/copy-out and identity writeback paths | `tests/fortran/pyi_contracts/calls_and_results/policy/test_call_and_result_policy.py::test_immutable_replacement_policy_is_complete_before_ir_lowering`
`tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py::test_replacement_writeback_dispatches_selected_scalar_result_behavior[copy_in_out]` | `tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_immutable_values_return_replacements_without_mutating_inputs` | `tests/fortran/memory_management/policy/test_memory_ownership_policy.py::test_contradictory_ownership_contract_fails_before_lowering` (`policy`) | canonical | +| [`.pyi` Calls And Results: Edit Types Shapes Layout And Optionality](../../docs/user/reference/pyi-contracts/calls-and-results.md#edit-types-shapes-layout-and-optionality) | Supported | fixed/open shapes; exact dtype, rank, layout, writeability, byte order, alignment, and zero-size checks; Fortran-order default; supported nullable/defaulted native optionals | `tests/fortran/arrays/codegen/test_dense_array_shape_lowering.py::test_dense_array_lowering_uses_planned_shape_checks_and_bridge_orientation`
`tests/fortran/optional_arguments/policy/test_optional_policy.py::test_optional_scalar_policy_completes_nullable_value_presence_before_planning` | `tests/fortran/arrays/end_to_end/test_array_contract_validation.py::test_remaining_array_contracts_are_validated_before_fortran_calls[source]`
`tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]` | `tests/fortran/optional_arguments/policy/test_optional_policy.py::test_optional_passed_procedure_is_blocked_before_codegen` (`policy`) | canonical | +| [`.pyi` Calls And Results: Translate Status Results Into Exceptions](../../docs/user/reference/pyi-contracts/calls-and-results.md#translate-status-results-into-exceptions) | Supported | named hidden scalar integer status; optional hidden string message; configurable success value; consumed projected outputs | `tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_status_projection_accepts_an_optional_missing_message_target`
`tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_runtime_plan_edits_dispatch_to_named_lowering_and_validate_roles` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | `tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_runtime_status_policy_rejects_invalid_output_contracts[@raises(status="status", message="message")\ndef solve() -> tuple[Returns["status", Int32], Returns["message", Int32]]: ...-must be a scalar string hidden output]` (`policy`) | canonical | +| [`.pyi` Calls And Results: Release The GIL For A Native Call](../../docs/user/reference/pyi-contracts/calls-and-results.md#release-the-gil-for-a-native-call) | Supported | ordinary held call; explicit released call; status conversion after reacquisition; callback trampoline reacquisition | `tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_runtime_policy_decorators_round_trip_through_pyi`
`tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_direct_binding_lowering_places_only_opted_in_native_call_outside_the_gil` | `tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[source]` | — | canonical | | [Feature Matrix: Caller-Ordered Multi-Source Builds, Makefiles, Verbose Mode, And Output Placement](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | caller order; direct and Makefile builds; replayable verbose commands; ABI artifact and stable alias placement | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_verbose_mode_prints_full_direct_build_commands` | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_makefile_mode_reproduces_multi_source_build`
`tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_dir_separates_abi_artifact_from_cli_alias` | — | canonical | | [Feature Matrix: Fortran Source Wrapper Builds](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | ordered Fortran source inputs; generated contracts; structured native plan; ABI-compatible import | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_source_build_result_records_structured_native_plan`
`tests/fortran/building_shared_library/pipeline/test_source_generated_contracts.py::test_source_build_generated_pyi_contract_matches_fixture[fdefault_output]` | `tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py::test_debug_and_optimized_wrapper_builds_preserve_runtime_abi` | — | canonical | | [Feature Matrix: Semantic `.pyi` Wrapper Builds From Explicit Native Artifacts](../../docs/user/language-support/feature-matrix.md#supported-inspection-features) | Partially supported | exactly one entry contract; explicit native input; source-free object build; ordered link items; current runtime subset | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_pyi_python_api_accepts_exactly_one_entry_contract`
`tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_generated_pyi_fixture_builds_from_native_object_without_source_reparse` | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_scale_runtime_contract[generated-pyi]` | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_pyi_python_api_rejects_a_missing_native_artifact` (`pipeline`) | canonical | diff --git a/tests/fortran/README.md b/tests/fortran/README.md index e113c5250..6a6e5fa28 100644 --- a/tests/fortran/README.md +++ b/tests/fortran/README.md @@ -49,9 +49,16 @@ owns a real test or fixture. | [Calls and Results](../../docs/user/reference/pyi-contracts/calls-and-results.md) | `pyi_contracts/calls_and_results/` | `python3 -m pytest -q tests/fortran/pyi_contracts/calls_and_results` | Each feature uses only the stages it needs: `parsing`, `probes`, -`preprocessing`, `semantics`, `policy`, `wrapper_codegen`, `compiling`, +`preprocessing`, `semantics`, `policy`, `codegen`, `compiling`, `pipeline`, `runtime`, and `end_to_end`. +Array declaration-expression coverage is intentionally split by evidence: +`arrays/semantics/` preserves expression and native-call provenance, +`arrays/policy/` proves completed dependency roles and named blockers, and +`arrays/end_to_end/` compiles supported dimensions and logical array kinds. +Cross-module editable-contract reconciliation remains under +the semantic `.pyi` format stage, not under a code-generation test. + ## Infrastructure owners Infrastructure contains only internal cross-feature frameworks with no honest @@ -65,7 +72,7 @@ evidence, not the ownership rule. | Final directory | Owner | | --- | --- | | `infrastructure/policy/` | Internal completed-policy dispatch and validation framework | -| `infrastructure/wrapper_codegen/` | Shared typed-plan and generator mechanics | +| `infrastructure/codegen/` | Shared typed-plan and generator mechanics | Minimized real-source parser regressions live in `source_parsing/parsing/test_real_world_interaction_regressions.py`. A diff --git a/tests/fortran/_support/ownership_policy.py b/tests/fortran/_support/ownership_policy.py index e9b93cc84..b39287618 100644 --- a/tests/fortran/_support/ownership_policy.py +++ b/tests/fortran/_support/ownership_policy.py @@ -12,7 +12,7 @@ SCALAR_STORAGE_CATEGORY, ) -from prik.wrapper_codegen.printers import PyiPrinter +from prik.codegen.printers import PyiPrinter from prik.semantics.ownership import ( AssignmentMode, diff --git a/tests/fortran/_support/parser_properties.py b/tests/fortran/_support/parser_properties.py index 886aeab9f..27d35d43a 100644 --- a/tests/fortran/_support/parser_properties.py +++ b/tests/fortran/_support/parser_properties.py @@ -24,7 +24,7 @@ from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text -from prik.wrapper_codegen.printers import emit_module_stubs +from prik.codegen.printers import emit_module_stubs from prik import FortranParseError, parse_fortran_file diff --git a/tests/fortran/_support/printer_models.py b/tests/fortran/_support/printer_models.py index 46748c244..6a5e98fba 100644 --- a/tests/fortran/_support/printer_models.py +++ b/tests/fortran/_support/printer_models.py @@ -15,13 +15,13 @@ from prik.pipeline.pyi import pyi_text_to_semantic_module as _parse_pyi_text -from prik.wrapper_codegen.printers import ( +from prik.codegen.printers import ( emit_module, emit_module_stubs, opaque_dependency_modules, PyiPrinter, ) -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner from prik.semantics.models import ( PROTOTYPE_REF_METADATA, diff --git a/tests/fortran/_support/pyi_conversion.py b/tests/fortran/_support/pyi_conversion.py index d72c6f985..42b903371 100644 --- a/tests/fortran/_support/pyi_conversion.py +++ b/tests/fortran/_support/pyi_conversion.py @@ -63,7 +63,7 @@ from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen.printers import emit_module +from prik.codegen.printers import emit_module from prik import parse_fortran_file diff --git a/tests/fortran/_support/semantic_conversion.py b/tests/fortran/_support/semantic_conversion.py index c08e73ca6..02a2ef980 100644 --- a/tests/fortran/_support/semantic_conversion.py +++ b/tests/fortran/_support/semantic_conversion.py @@ -48,7 +48,7 @@ from prik.semantics.metadata import SCALAR_STORAGE_CATEGORY -from prik.wrapper_codegen.printers import emit_module +from prik.codegen.printers import emit_module from prik.semantics.models import ( ProjectionMapping, diff --git a/tests/fortran/_support/semantic_properties.py b/tests/fortran/_support/semantic_properties.py index 3f21610b1..d43960b06 100644 --- a/tests/fortran/_support/semantic_properties.py +++ b/tests/fortran/_support/semantic_properties.py @@ -27,7 +27,7 @@ from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text -from prik.wrapper_codegen.printers import emit_module +from prik.codegen.printers import emit_module from prik import parse_fortran_file diff --git a/tests/fortran/_support/wrapper_build.py b/tests/fortran/_support/wrapper_build.py index 8e8876bbc..547e3cbf3 100644 --- a/tests/fortran/_support/wrapper_build.py +++ b/tests/fortran/_support/wrapper_build.py @@ -35,7 +35,7 @@ from prik.runtime.handles import AllocatableArray from prik.semantics.fortran2ir import fortran_project_to_semantic_modules from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner REPO_ROOT = Path(__file__).resolve().parents[3] WRAPPER_TEST_ROOT = Path(__file__).resolve().parent diff --git a/tests/fortran/allocatables/wrapper_codegen/test_allocatable_lowering.py b/tests/fortran/allocatables/codegen/test_allocatable_lowering.py similarity index 98% rename from tests/fortran/allocatables/wrapper_codegen/test_allocatable_lowering.py rename to tests/fortran/allocatables/codegen/test_allocatable_lowering.py index 492701f8e..f4b2e6eba 100644 --- a/tests/fortran/allocatables/wrapper_codegen/test_allocatable_lowering.py +++ b/tests/fortran/allocatables/codegen/test_allocatable_lowering.py @@ -4,7 +4,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _allocatable_plan(): diff --git a/tests/fortran/allocatables/end_to_end/test_external_allocatable.py b/tests/fortran/allocatables/end_to_end/test_external_allocatable.py index edc271457..704645bec 100644 --- a/tests/fortran/allocatables/end_to_end/test_external_allocatable.py +++ b/tests/fortran/allocatables/end_to_end/test_external_allocatable.py @@ -15,7 +15,7 @@ pytestmark = pytest.mark.fortran_end_to_end -def test_external_allocatable_argument_accepts_a_caller_created_handle(tmp_path: Path): +def test_standalone_allocatable_argument_accepts_a_caller_created_handle(tmp_path: Path): source = tmp_path / "external_allocatable.f90" source.write_text( """ @@ -30,9 +30,9 @@ def test_external_allocatable_argument_accepts_a_caller_created_handle(tmp_path: ) contract = tmp_path / "external_allocatable.pyi" contract.write_text( - """from prik.contracts import Allocatable, Float64, Returns, external + """from prik.contracts import Allocatable, Float64, Returns, standalone -@external +@standalone def replace_external( values: Allocatable[Float64[:]], ) -> Returns["values", Allocatable[Float64[:]]]: ... diff --git a/tests/fortran/arrays/wrapper_codegen/test_array_buffer_lowering.py b/tests/fortran/arrays/codegen/test_array_buffer_lowering.py similarity index 97% rename from tests/fortran/arrays/wrapper_codegen/test_array_buffer_lowering.py rename to tests/fortran/arrays/codegen/test_array_buffer_lowering.py index 5567b2ffc..abee9561c 100644 --- a/tests/fortran/arrays/wrapper_codegen/test_array_buffer_lowering.py +++ b/tests/fortran/arrays/codegen/test_array_buffer_lowering.py @@ -17,8 +17,8 @@ ) from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import ArgumentHandoffMode, BridgeDataAction -from prik.wrapper_codegen import ArrayHandoffPlan, WrapperCodeGenerator, WrapperPlanner -from prik.wrapper_codegen.plan import DatatypeFamily +from prik.codegen import ArrayHandoffPlan, WrapperCodeGenerator, WrapperPlanner +from prik.codegen.plan import DatatypeFamily def _array_module(): diff --git a/tests/fortran/arrays/wrapper_codegen/test_array_output_identity.py b/tests/fortran/arrays/codegen/test_array_output_identity.py similarity index 97% rename from tests/fortran/arrays/wrapper_codegen/test_array_output_identity.py rename to tests/fortran/arrays/codegen/test_array_output_identity.py index f4e90b4e6..d2c6f5b1d 100644 --- a/tests/fortran/arrays/wrapper_codegen/test_array_output_identity.py +++ b/tests/fortran/arrays/codegen/test_array_output_identity.py @@ -8,8 +8,8 @@ from prik.semantics.ownership import CodegenAction, ObjectKind, OwnershipOwner, TransferMode from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import ArrayWritebackABI -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner -from prik.wrapper_codegen.plan import WritebackPhase +from prik.codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen.plan import WritebackPhase def _output_plan(): diff --git a/tests/fortran/arrays/wrapper_codegen/test_array_result_lowering.py b/tests/fortran/arrays/codegen/test_array_result_lowering.py similarity index 70% rename from tests/fortran/arrays/wrapper_codegen/test_array_result_lowering.py rename to tests/fortran/arrays/codegen/test_array_result_lowering.py index c45221912..b534f32c8 100644 --- a/tests/fortran/arrays/wrapper_codegen/test_array_result_lowering.py +++ b/tests/fortran/arrays/codegen/test_array_result_lowering.py @@ -8,7 +8,7 @@ from prik.semantics.ownership import CodegenAction, NativeBarrierAction, ObjectKind, OwnershipOwner, TransferMode from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import BridgeDataAction, ORDINARY_ARRAY_RESULT_COPY_REASON -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _result_plan(): @@ -27,6 +27,21 @@ def hidden() -> Float64[3]: ... return WrapperPlanner().build(module) +def _array_property_result_plan(): + module = parse_pyi_text( + """ +from prik.contracts import Float64 + +def vector(values: Float64[:]) -> Float64[values.size]: ... +def flattened(values: Float64[:, :]) -> Float64[values.size]: ... +def columns(values: Float64[:, :]) -> Float64[values.shape[1]]: ... +""", + module_name="size_intrinsic_results", + ) + complete_semantic_policies(module) + return WrapperPlanner().build(module) + + def test_array_results_record_producer_shape_copy_ownership_and_shared_hidden_slot(): direct_function, hidden_function = _result_plan().namespaces[0].functions direct = direct_function.results[0] @@ -73,12 +88,35 @@ def test_array_result_lowering_transfers_bridge_copy_to_capsule_owned_numpy_stor assert "void bind_c_hidden(void ** out);" in c_source assert "PyCapsule_New(out, NULL, prik_release_owned_memory)" in c_source assert "real(c_double), dimension(n) :: result_value" in bridge_source - assert "result = c_malloc(max(1_c_size_t, c_sizeof(result_value)))" in bridge_source + assert "result = c_malloc(" in bridge_source + assert "size(result_value," in bridge_source + assert "storage_size(result_value," in bridge_source assert "result_copy = reshape(result_value, [size(result_value)])" in bridge_source assert "real(c_double), dimension(3) :: out_value" in bridge_source assert "call native_hidden(out_value)" in bridge_source +def test_array_property_results_reuse_input_array_extent_roles_in_both_backends(): + plan = _array_property_result_plan() + vector, flattened, columns = plan.namespaces[0].functions + + assert vector.results[0].array.shape == ("__prik_extent_values_0",) + assert vector.results[0].array.extent_reference_tokens == (("__prik_extent_values_0",),) + assert vector.results[0].array.extent_reference_roles == (("size_intrinsic_results.vector.values:extent:0",),) + assert flattened.results[0].array.shape == ("__prik_extent_values_0 * __prik_extent_values_1",) + assert columns.results[0].array.shape == ("__prik_extent_values_1",) + + artifacts = WrapperCodeGenerator().generate(plan) + c_source = next(source.text for source in artifacts.sources if source.path.suffix == ".c") + bridge_source = next(source.text for source in artifacts.sources if source.path.suffix == ".f90") + + assert "npy_intp result_obj_dims[] = {bound_values_extent_0};" in c_source + assert "npy_intp result_obj_dims[] = {bound_values_extent_0 * bound_values_extent_1};" in c_source + assert "real(c_double), dimension(values_extent_0) :: result_value" in bridge_source + assert "dimension(values_extent_0 * values_extent_1) :: result_value" in bridge_source + assert "real(c_double), dimension(values_extent_1) :: result_value" in bridge_source + + @pytest.mark.parametrize( ("edit", "diagnostic"), [ diff --git a/tests/fortran/arrays/wrapper_codegen/test_dense_array_shape_lowering.py b/tests/fortran/arrays/codegen/test_dense_array_shape_lowering.py similarity index 98% rename from tests/fortran/arrays/wrapper_codegen/test_dense_array_shape_lowering.py rename to tests/fortran/arrays/codegen/test_dense_array_shape_lowering.py index a66ca3e16..ae23fc4f4 100644 --- a/tests/fortran/arrays/wrapper_codegen/test_dense_array_shape_lowering.py +++ b/tests/fortran/arrays/codegen/test_dense_array_shape_lowering.py @@ -11,31 +11,31 @@ TransformationLayer, WritebackPhase, ) -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _dense_plan(): module = parse_pyi_text( """ -from prik.contracts import Annotated, Flat, Float64, Int32, ORDER_C, external +from prik.contracts import Annotated, Flat, Float64, Int32, ORDER_C, standalone def dense_f(rows: Int32, cols: Int32, values: Float64[rows, cols]) -> None: ... def dense_c(rows: Int32, cols: Int32, values: Annotated[Float64[rows, cols], ORDER_C]) -> None: ... def flat(n: Int32, values: Float64[Flat]) -> None: ... -@external +@standalone def flat_rank2_runtime(values: Float64[:, Flat]) -> None: ... -@external +@standalone def flat_rank2_fixed(values: Float64[3, Flat]) -> None: ... -@external +@standalone def c_flat_rank2_runtime(values: Annotated[Float64[Flat, :], ORDER_C]) -> None: ... -@external +@standalone def c_flat_rank2_fixed(values: Annotated[Float64[Flat, 3], ORDER_C]) -> None: ... -@external +@standalone def bounded_flat( ldb: Int32, values: Float64[ldb, Flat], @@ -80,9 +80,9 @@ def projected( def _late_extent_external_plan(): module = parse_pyi_text( """ -from prik.contracts import Annotated, Float64, Immutable, Int32, external +from prik.contracts import Annotated, Float64, Immutable, Int32, standalone -@external +@standalone def late_extent(values: Float64[n], n: Annotated[Int32, Immutable] | None = ...) -> None: ... """, module_name="late_extent_external", diff --git a/tests/fortran/arrays/wrapper_codegen/test_specialized_array_roles.py b/tests/fortran/arrays/codegen/test_specialized_array_roles.py similarity index 97% rename from tests/fortran/arrays/wrapper_codegen/test_specialized_array_roles.py rename to tests/fortran/arrays/codegen/test_specialized_array_roles.py index 79b8d7614..18be9a817 100644 --- a/tests/fortran/arrays/wrapper_codegen/test_specialized_array_roles.py +++ b/tests/fortran/arrays/codegen/test_specialized_array_roles.py @@ -6,7 +6,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import OptionalMode -from prik.wrapper_codegen import CBindingGenerator, WrapperCodeGenerator, WrapperPlanner +from prik.codegen import CBindingGenerator, WrapperCodeGenerator, WrapperPlanner def _later_array_plan(): diff --git a/tests/fortran/arrays/wrapper_codegen/test_strided_array_lowering.py b/tests/fortran/arrays/codegen/test_strided_array_lowering.py similarity index 98% rename from tests/fortran/arrays/wrapper_codegen/test_strided_array_lowering.py rename to tests/fortran/arrays/codegen/test_strided_array_lowering.py index 66be6aee1..0a2c763e3 100644 --- a/tests/fortran/arrays/wrapper_codegen/test_strided_array_lowering.py +++ b/tests/fortran/arrays/codegen/test_strided_array_lowering.py @@ -6,7 +6,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _strided_plan(rank: int = 2): diff --git a/tests/fortran/arrays/end_to_end/fixtures/baseline/contracts/fmath_arrays/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/baseline/contracts/fmath_arrays/__init__.pyi index ff0c626ce..1746077b5 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/baseline/contracts/fmath_arrays/__init__.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/baseline/contracts/fmath_arrays/__init__.pyi @@ -1,7 +1,7 @@ -from prik.contracts import Addr, Arg, Bool, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, external, native_call +from prik.contracts import Addr, Arg, Bool8, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call, standalone @bind("SQUARE_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r4( N: Int32, @@ -10,7 +10,7 @@ def square_r4( ) -> Returns["N", Int32]: ... @bind("SQUARE_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_r8( N: Int32, @@ -19,7 +19,7 @@ def square_r8( ) -> Returns["N", Int32]: ... @bind("SQUARE_I4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_i4( N: Int32, @@ -28,7 +28,7 @@ def square_i4( ) -> Returns["N", Int32]: ... @bind("SQUARE_C4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c4( N: Int32, @@ -37,7 +37,7 @@ def square_c4( ) -> Returns["N", Int32]: ... @bind("SQUARE_C8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def square_c8( N: Int32, @@ -46,7 +46,7 @@ def square_c8( ) -> Returns["N", Int32]: ... @bind("CUBE_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r4( N: Int32, @@ -55,7 +55,7 @@ def cube_r4( ) -> Returns["N", Int32]: ... @bind("CUBE_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_r8( N: Int32, @@ -64,7 +64,7 @@ def cube_r8( ) -> Returns["N", Int32]: ... @bind("CUBE_I4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cube_i4( N: Int32, @@ -73,7 +73,7 @@ def cube_i4( ) -> Returns["N", Int32]: ... @bind("ADD_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r4( N: Int32, @@ -83,7 +83,7 @@ def add_r4( ) -> Returns["N", Int32]: ... @bind("ADD_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_r8( N: Int32, @@ -93,7 +93,7 @@ def add_r8( ) -> Returns["N", Int32]: ... @bind("ADD_I4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_i4( N: Int32, @@ -103,7 +103,7 @@ def add_i4( ) -> Returns["N", Int32]: ... @bind("ADD_C4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c4( N: Int32, @@ -113,7 +113,7 @@ def add_c4( ) -> Returns["N", Int32]: ... @bind("ADD_C8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def add_c8( N: Int32, @@ -123,7 +123,7 @@ def add_c8( ) -> Returns["N", Int32]: ... @bind("SUB_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r4( N: Int32, @@ -133,7 +133,7 @@ def sub_r4( ) -> Returns["N", Int32]: ... @bind("SUB_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_r8( N: Int32, @@ -143,7 +143,7 @@ def sub_r8( ) -> Returns["N", Int32]: ... @bind("SUB_I4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sub_i4( N: Int32, @@ -153,7 +153,7 @@ def sub_i4( ) -> Returns["N", Int32]: ... @bind("MUL_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r4( N: Int32, @@ -163,7 +163,7 @@ def mul_r4( ) -> Returns["N", Int32]: ... @bind("MUL_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_r8( N: Int32, @@ -173,7 +173,7 @@ def mul_r8( ) -> Returns["N", Int32]: ... @bind("MUL_I4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mul_i4( N: Int32, @@ -183,7 +183,7 @@ def mul_i4( ) -> Returns["N", Int32]: ... @bind("DIV_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r4( N: Int32, @@ -193,7 +193,7 @@ def div_r4( ) -> Returns["N", Int32]: ... @bind("DIV_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def div_r8( N: Int32, @@ -203,7 +203,7 @@ def div_r8( ) -> Returns["N", Int32]: ... @bind("POW_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r4( N: Int32, @@ -213,7 +213,7 @@ def pow_r4( ) -> Returns["N", Int32]: ... @bind("POW_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def pow_r8( N: Int32, @@ -223,7 +223,7 @@ def pow_r8( ) -> Returns["N", Int32]: ... @bind("ABS_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r4( N: Int32, @@ -232,7 +232,7 @@ def abs_r4( ) -> Returns["N", Int32]: ... @bind("ABS_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_r8( N: Int32, @@ -241,7 +241,7 @@ def abs_r8( ) -> Returns["N", Int32]: ... @bind("ABS_I4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_i4( N: Int32, @@ -250,7 +250,7 @@ def abs_i4( ) -> Returns["N", Int32]: ... @bind("NEG_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r4( N: Int32, @@ -259,7 +259,7 @@ def neg_r4( ) -> Returns["N", Int32]: ... @bind("NEG_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_r8( N: Int32, @@ -268,7 +268,7 @@ def neg_r8( ) -> Returns["N", Int32]: ... @bind("NEG_I4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def neg_i4( N: Int32, @@ -277,7 +277,7 @@ def neg_i4( ) -> Returns["N", Int32]: ... @bind("SIN_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r4( N: Int32, @@ -286,7 +286,7 @@ def sin_r4( ) -> Returns["N", Int32]: ... @bind("SIN_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sin_r8( N: Int32, @@ -295,7 +295,7 @@ def sin_r8( ) -> Returns["N", Int32]: ... @bind("COS_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r4( N: Int32, @@ -304,7 +304,7 @@ def cos_r4( ) -> Returns["N", Int32]: ... @bind("COS_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def cos_r8( N: Int32, @@ -313,7 +313,7 @@ def cos_r8( ) -> Returns["N", Int32]: ... @bind("TAN_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r4( N: Int32, @@ -322,7 +322,7 @@ def tan_r4( ) -> Returns["N", Int32]: ... @bind("TAN_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def tan_r8( N: Int32, @@ -331,7 +331,7 @@ def tan_r8( ) -> Returns["N", Int32]: ... @bind("ASIN_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r4( N: Int32, @@ -340,7 +340,7 @@ def asin_r4( ) -> Returns["N", Int32]: ... @bind("ASIN_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def asin_r8( N: Int32, @@ -349,7 +349,7 @@ def asin_r8( ) -> Returns["N", Int32]: ... @bind("ACOS_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r4( N: Int32, @@ -358,7 +358,7 @@ def acos_r4( ) -> Returns["N", Int32]: ... @bind("ACOS_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def acos_r8( N: Int32, @@ -367,7 +367,7 @@ def acos_r8( ) -> Returns["N", Int32]: ... @bind("ATAN_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r4( N: Int32, @@ -376,7 +376,7 @@ def atan_r4( ) -> Returns["N", Int32]: ... @bind("ATAN_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def atan_r8( N: Int32, @@ -385,7 +385,7 @@ def atan_r8( ) -> Returns["N", Int32]: ... @bind("ATAN2_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r4( N: Int32, @@ -395,7 +395,7 @@ def atan2_r4( ) -> Returns["N", Int32]: ... @bind("ATAN2_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def atan2_r8( N: Int32, @@ -405,7 +405,7 @@ def atan2_r8( ) -> Returns["N", Int32]: ... @bind("EXP_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r4( N: Int32, @@ -414,7 +414,7 @@ def exp_r4( ) -> Returns["N", Int32]: ... @bind("EXP_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def exp_r8( N: Int32, @@ -423,7 +423,7 @@ def exp_r8( ) -> Returns["N", Int32]: ... @bind("LOG_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r4( N: Int32, @@ -432,7 +432,7 @@ def log_r4( ) -> Returns["N", Int32]: ... @bind("LOG_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log_r8( N: Int32, @@ -441,7 +441,7 @@ def log_r8( ) -> Returns["N", Int32]: ... @bind("LOG10_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r4( N: Int32, @@ -450,7 +450,7 @@ def log10_r4( ) -> Returns["N", Int32]: ... @bind("LOG10_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def log10_r8( N: Int32, @@ -459,7 +459,7 @@ def log10_r8( ) -> Returns["N", Int32]: ... @bind("SQRT_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r4( N: Int32, @@ -468,7 +468,7 @@ def sqrt_r4( ) -> Returns["N", Int32]: ... @bind("SQRT_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def sqrt_r8( N: Int32, @@ -477,7 +477,7 @@ def sqrt_r8( ) -> Returns["N", Int32]: ... @bind("HYPOT_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r4( N: Int32, @@ -487,7 +487,7 @@ def hypot_r4( ) -> Returns["N", Int32]: ... @bind("HYPOT_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def hypot_r8( N: Int32, @@ -497,7 +497,7 @@ def hypot_r8( ) -> Returns["N", Int32]: ... @bind("MIN_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r4( N: Int32, @@ -507,7 +507,7 @@ def min_r4( ) -> Returns["N", Int32]: ... @bind("MIN_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_r8( N: Int32, @@ -517,7 +517,7 @@ def min_r8( ) -> Returns["N", Int32]: ... @bind("MIN_I4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def min_i4( N: Int32, @@ -527,7 +527,7 @@ def min_i4( ) -> Returns["N", Int32]: ... @bind("MAX_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r4( N: Int32, @@ -537,7 +537,7 @@ def max_r4( ) -> Returns["N", Int32]: ... @bind("MAX_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_r8( N: Int32, @@ -547,7 +547,7 @@ def max_r8( ) -> Returns["N", Int32]: ... @bind("MAX_I4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def max_i4( N: Int32, @@ -557,7 +557,7 @@ def max_i4( ) -> Returns["N", Int32]: ... @bind("SIGN_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r4( N: Int32, @@ -567,7 +567,7 @@ def sign_r4( ) -> Returns["N", Int32]: ... @bind("SIGN_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def sign_r8( N: Int32, @@ -577,7 +577,7 @@ def sign_r8( ) -> Returns["N", Int32]: ... @bind("MOD_I4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_i4( N: Int32, @@ -587,7 +587,7 @@ def mod_i4( ) -> Returns["N", Int32]: ... @bind("MOD_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r4( N: Int32, @@ -597,7 +597,7 @@ def mod_r4( ) -> Returns["N", Int32]: ... @bind("MOD_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def mod_r8( N: Int32, @@ -607,7 +607,7 @@ def mod_r8( ) -> Returns["N", Int32]: ... @bind("DEG2RAD_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r4( N: Int32, @@ -616,7 +616,7 @@ def deg2rad_r4( ) -> Returns["N", Int32]: ... @bind("DEG2RAD_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def deg2rad_r8( N: Int32, @@ -625,7 +625,7 @@ def deg2rad_r8( ) -> Returns["N", Int32]: ... @bind("RAD2DEG_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r4( N: Int32, @@ -634,7 +634,7 @@ def rad2deg_r4( ) -> Returns["N", Int32]: ... @bind("RAD2DEG_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def rad2deg_r8( N: Int32, @@ -643,7 +643,7 @@ def rad2deg_r8( ) -> Returns["N", Int32]: ... @bind("DIST2_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r4( N: Int32, @@ -653,7 +653,7 @@ def dist2_r4( ) -> Returns["N", Int32]: ... @bind("DIST2_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3)]) def dist2_r8( N: Int32, @@ -663,7 +663,7 @@ def dist2_r8( ) -> Returns["N", Int32]: ... @bind("DOT2_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r4( N: Int32, @@ -675,7 +675,7 @@ def dot2_r4( ) -> Returns["N", Int32]: ... @bind("DOT2_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5)]) def dot2_r8( N: Int32, @@ -687,7 +687,7 @@ def dot2_r8( ) -> Returns["N", Int32]: ... @bind("DOT3_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r4( N: Int32, @@ -701,7 +701,7 @@ def dot3_r4( ) -> Returns["N", Int32]: ... @bind("DOT3_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)]) def dot3_r8( N: Int32, @@ -715,7 +715,7 @@ def dot3_r8( ) -> Returns["N", Int32]: ... @bind("CONJ_C4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c4( N: Int32, @@ -724,7 +724,7 @@ def conj_c4( ) -> Returns["N", Int32]: ... @bind("CONJ_C8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def conj_c8( N: Int32, @@ -733,7 +733,7 @@ def conj_c8( ) -> Returns["N", Int32]: ... @bind("REAL_C4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c4( N: Int32, @@ -742,7 +742,7 @@ def real_c4( ) -> Returns["N", Int32]: ... @bind("REAL_C8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def real_c8( N: Int32, @@ -751,7 +751,7 @@ def real_c8( ) -> Returns["N", Int32]: ... @bind("AIMAG_C4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c4( N: Int32, @@ -760,7 +760,7 @@ def aimag_c4( ) -> Returns["N", Int32]: ... @bind("AIMAG_C8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def aimag_c8( N: Int32, @@ -769,7 +769,7 @@ def aimag_c8( ) -> Returns["N", Int32]: ... @bind("ABS_C4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c4( N: Int32, @@ -778,7 +778,7 @@ def abs_c4( ) -> Returns["N", Int32]: ... @bind("ABS_C8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def abs_c8( N: Int32, @@ -787,28 +787,28 @@ def abs_c8( ) -> Returns["N", Int32]: ... @bind("IS_POSITIVE_R4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r4( N: Int32, X: Float32[N], - R: Bool[N] + R: Bool8[N] ) -> Returns["N", Int32]: ... @bind("IS_POSITIVE_R8") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_positive_r8( N: Int32, X: Float64[N], - R: Bool[N] + R: Bool8[N] ) -> Returns["N", Int32]: ... @bind("IS_EVEN_I4") -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def is_even_i4( N: Int32, X: Int32[N], - R: Bool[N] + R: Bool8[N] ) -> Returns["N", Int32]: ... diff --git a/tests/fortran/arrays/end_to_end/fixtures/baseline/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi b/tests/fortran/arrays/end_to_end/fixtures/baseline/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi index ae08236a5..2c5a7a922 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/baseline/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/baseline/contracts/fmath_arrays_f90/fmath_arrays_f90.pyi @@ -1,4 +1,4 @@ -from prik.contracts import Addr, Arg, Bool, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call +from prik.contracts import Addr, Arg, Bool8, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call @bind("SQUARE_R4_CONTIGUOUS") @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) @@ -709,7 +709,7 @@ def abs_c8_contiguous( def is_positive_r4_contiguous( N: Int32, X: Float32[:], - R: Bool[:] + R: Bool8[:] ) -> Returns["N", Int32]: ... @bind("IS_POSITIVE_R8_CONTIGUOUS") @@ -717,7 +717,7 @@ def is_positive_r4_contiguous( def is_positive_r8_contiguous( N: Int32, X: Float64[:], - R: Bool[:] + R: Bool8[:] ) -> Returns["N", Int32]: ... @bind("IS_EVEN_I4_CONTIGUOUS") @@ -725,7 +725,7 @@ def is_positive_r8_contiguous( def is_even_i4_contiguous( N: Int32, X: Int32[:], - R: Bool[:] + R: Bool8[:] ) -> Returns["N", Int32]: ... @bind("SQUARE_R4_STRIDED") @@ -1437,7 +1437,7 @@ def abs_c8_strided( def is_positive_r4_strided( N: Int32, X: Float32[::], - R: Bool[::] + R: Bool8[::] ) -> Returns["N", Int32]: ... @bind("IS_POSITIVE_R8_STRIDED") @@ -1445,7 +1445,7 @@ def is_positive_r4_strided( def is_positive_r8_strided( N: Int32, X: Float64[::], - R: Bool[::] + R: Bool8[::] ) -> Returns["N", Int32]: ... @bind("IS_EVEN_I4_STRIDED") @@ -1453,5 +1453,5 @@ def is_positive_r8_strided( def is_even_i4_strided( N: Int32, X: Int32[::], - R: Bool[::] + R: Bool8[::] ) -> Returns["N", Int32]: ... diff --git a/tests/fortran/arrays/end_to_end/fixtures/baseline/edited_contracts/c_order_flat_buffer/c_order_flat_buffer.pyi b/tests/fortran/arrays/end_to_end/fixtures/baseline/edited_contracts/c_order_flat_buffer/c_order_flat_buffer.pyi index 0611a93c2..e899000ea 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/baseline/edited_contracts/c_order_flat_buffer/c_order_flat_buffer.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/baseline/edited_contracts/c_order_flat_buffer/c_order_flat_buffer.pyi @@ -1,6 +1,6 @@ -from prik.contracts import Addr, Annotated, Arg, Flat, Float64, Int32, ORDER_C, external, native_call +from prik.contracts import Addr, Annotated, Arg, Flat, Float64, Int32, ORDER_C, native_call, standalone -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def row_sums_c( n: Int32, diff --git a/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/farray_results_f90.pyi b/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/farray_results_f90.pyi index 8a5bf2a04..dac619931 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/farray_results_f90.pyi +++ b/tests/fortran/arrays/end_to_end/fixtures/contracts/farray_results_f90/farray_results_f90.pyi @@ -7,6 +7,10 @@ def automatic_vector( n: Int32 ) -> Float64[n]: ... +def size_intrinsic_vector( + x: Float64[::] +) -> Float64[x.size]: ... + @native_call([Addr(Arg(0)), Addr(Arg(1))]) def automatic_matrix( rows: Int32, diff --git a/tests/fortran/arrays/end_to_end/fixtures/farray_results_f90.f90 b/tests/fortran/arrays/end_to_end/fixtures/farray_results_f90.f90 index f8350226a..38d437e15 100644 --- a/tests/fortran/arrays/end_to_end/fixtures/farray_results_f90.f90 +++ b/tests/fortran/arrays/end_to_end/fixtures/farray_results_f90.f90 @@ -17,6 +17,13 @@ function automatic_vector(n) result(values) end do end function automatic_vector + function size_intrinsic_vector(x) result(values) + real(8), intent(in) :: x(:) + real(8), dimension(size(x)) :: values + + values = 3.0_8 * x + end function size_intrinsic_vector + function automatic_matrix(rows, cols) result(values) integer, intent(in) :: rows integer, intent(in) :: cols diff --git a/tests/fortran/arrays/end_to_end/test_array_results.py b/tests/fortran/arrays/end_to_end/test_array_results.py index 33b45f318..3841c7d54 100644 --- a/tests/fortran/arrays/end_to_end/test_array_results.py +++ b/tests/fortran/arrays/end_to_end/test_array_results.py @@ -43,6 +43,11 @@ def test_array_results_follow_data_buffer_and_descriptor_handle_contracts( np.testing.assert_allclose(automatic, np.array([2.0, 4.0, 6.0, 8.0], dtype=np.float64)) assert automatic.base is not None + intrinsic_source = np.arange(1.0, 6.0, dtype=np.float64) + intrinsic = module.size_intrinsic_vector(intrinsic_source) + np.testing.assert_allclose(intrinsic, 3.0 * intrinsic_source) + assert intrinsic.base is not None + matrix = module.automatic_matrix(np.int32(2), np.int32(3)) np.testing.assert_allclose( matrix, diff --git a/tests/fortran/arrays/end_to_end/test_declaration_extent_expressions.py b/tests/fortran/arrays/end_to_end/test_declaration_extent_expressions.py new file mode 100644 index 000000000..3a8236c63 --- /dev/null +++ b/tests/fortran/arrays/end_to_end/test_declaration_extent_expressions.py @@ -0,0 +1,371 @@ +"""End-to-end declaration expressions across every array declaration owner.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_sources_and_import, + _build_text_and_import, +) + + +pytestmark = pytest.mark.fortran_end_to_end + + +SOURCE = """ +module declaration_extent_expressions + use, intrinsic :: iso_c_binding, only: c_double + implicit none + + integer, parameter :: base_extent = 2 + integer, parameter :: field_extent = max(3, base_extent + 1) + + type :: record + real(c_double) :: values(field_extent) + end type record + + real(c_double), target :: module_values(base_extent ** 2) + type(record), target :: current + +contains + + pure integer function local_extent(n) result(extent) + integer, intent(in) :: n + extent = max(0, n) + end function local_extent + + subroutine initialize_state() + module_values = [1.0_c_double, 2.0_c_double, 3.0_c_double, 4.0_c_double] + current%values = [5.0_c_double, 6.0_c_double, 7.0_c_double] + end subroutine initialize_state + + subroutine exercise_inquiries( & + source, destination, total, rank_values, reduced, constructed, conditional, lower_bound, upper_bound) + real(c_double), intent(in) :: source(2:, 2:) + real(c_double), intent(out) :: destination( & + ubound(source, 1) - lbound(source, 1) + 1, & + max(1, size(source, dim=2, kind=8))) + real(c_double), intent(out) :: total(size(source, kind=8)) + real(c_double), intent(out) :: rank_values(2 ** rank(source)) + real(c_double), intent(out) :: reduced(sum(shape(source, kind=8))) + real(c_double), intent(out) :: constructed(product((/ size(source, 1), 1 /))) + real(c_double), intent(out) :: conditional(merge(size(source, 1), 1, size(source, 2) > 0)) + real(c_double), intent(out) :: lower_bound(lbound(source, 1)) + real(c_double), intent(out) :: upper_bound(ubound(source, 1)) + + destination = source + total = reshape(source, [size(source)]) + rank_values = 8.0_c_double + reduced = 9.0_c_double + constructed = 10.0_c_double + conditional = 11.0_c_double + lower_bound = 12.0_c_double + upper_bound = 13.0_c_double + end subroutine exercise_inquiries + + function copied(source) result(values) + real(c_double), intent(in) :: source(:, :) + real(c_double) :: values(size(source, 1), size(source, 2)) + values = source + end function copied + + function local_extent_values(n) result(values) + integer, intent(in) :: n + real(c_double) :: values(local_extent(n)) + values = 14.0_c_double + end function local_extent_values + + subroutine fill_local_extent(n, values) + integer, intent(in) :: n + real(c_double), intent(out) :: values(local_extent(n)) + values = 15.0_c_double + end subroutine fill_local_extent + +end module declaration_extent_expressions +""" + + +def test_all_array_declaration_owners_and_supported_extent_forms_execute(tmp_path: Path): + module = _build_text_and_import( + SOURCE, + "declaration_extent_expressions.f90", + tmp_path, + { + "bind_c_declaration_extent_expressions_wrapper.f90", + "declaration_extent_expressions_wrapper.c", + "declaration_extent_expressions_wrapper.h", + }, + ) + + assert module.initialize_state() is None + np.testing.assert_array_equal(module.module_values, np.arange(1.0, 5.0, dtype=np.float64)) + np.testing.assert_array_equal(module.current.values, np.arange(5.0, 8.0, dtype=np.float64)) + + source = np.asfortranarray(np.arange(1.0, 7.0, dtype=np.float64).reshape((2, 3), order="F")) + destination = np.empty(source.shape, dtype=np.float64, order="F") + total = np.empty(source.size, dtype=np.float64) + rank_values = np.empty(2**source.ndim, dtype=np.float64) + reduced = np.empty(sum(source.shape), dtype=np.float64) + constructed = np.empty(source.shape[0], dtype=np.float64) + conditional = np.empty(source.shape[0], dtype=np.float64) + lower_bound = np.empty(2, dtype=np.float64) + upper_bound = np.empty(source.shape[0] + 1, dtype=np.float64) + + assert ( + module.exercise_inquiries( + source, + destination, + total, + rank_values, + reduced, + constructed, + conditional, + lower_bound, + upper_bound, + ) + is None + ) + np.testing.assert_array_equal(destination, source) + np.testing.assert_array_equal(total, source.reshape(-1, order="F")) + np.testing.assert_array_equal(rank_values, np.full(2**source.ndim, 8.0)) + np.testing.assert_array_equal(reduced, np.full(sum(source.shape), 9.0)) + np.testing.assert_array_equal(constructed, np.full(source.shape[0], 10.0)) + np.testing.assert_array_equal(conditional, np.full(source.shape[0], 11.0)) + np.testing.assert_array_equal(lower_bound, np.full(2, 12.0)) + np.testing.assert_array_equal(upper_bound, np.full(source.shape[0] + 1, 13.0)) + + empty_source = np.empty((0, 3), dtype=np.float64, order="F") + empty_destination = np.empty(empty_source.shape, dtype=np.float64, order="F") + empty_total = np.empty(0, dtype=np.float64) + empty_rank_values = np.empty(2**empty_source.ndim, dtype=np.float64) + empty_reduced = np.empty(sum(empty_source.shape), dtype=np.float64) + empty_constructed = np.empty(empty_source.shape[0], dtype=np.float64) + empty_conditional = np.empty(0, dtype=np.float64) + empty_lower_bound = np.empty(1, dtype=np.float64) + empty_upper_bound = np.empty(0, dtype=np.float64) + + assert ( + module.exercise_inquiries( + empty_source, + empty_destination, + empty_total, + empty_rank_values, + empty_reduced, + empty_constructed, + empty_conditional, + empty_lower_bound, + empty_upper_bound, + ) + is None + ) + np.testing.assert_array_equal(empty_destination, empty_source) + np.testing.assert_array_equal(empty_total, np.empty(0, dtype=np.float64)) + np.testing.assert_array_equal(empty_rank_values, np.full(2**empty_source.ndim, 8.0)) + np.testing.assert_array_equal(empty_reduced, np.full(sum(empty_source.shape), 9.0)) + np.testing.assert_array_equal(empty_constructed, np.full(empty_source.shape[0], 10.0)) + np.testing.assert_array_equal(empty_conditional, np.empty(0, dtype=np.float64)) + np.testing.assert_array_equal(empty_lower_bound, np.full(1, 12.0)) + np.testing.assert_array_equal(empty_upper_bound, np.empty(0, dtype=np.float64)) + + copied = module.copied(source) + assert copied.flags.f_contiguous + np.testing.assert_array_equal(copied, source) + + local_values = module.local_extent_values(np.int32(4)) + np.testing.assert_array_equal(local_values, np.full(4, 14.0)) + caller_values = np.empty(4, dtype=np.float64) + assert module.fill_local_extent(np.int32(4), caller_values) is None + np.testing.assert_array_equal(caller_values, np.full(4, 15.0)) + + +IMPORTED_EXTENT_PROVIDER = """ +module imported_extent_provider + implicit none +contains + pure integer function extent_for(n) result(extent) + integer, intent(in) :: n + extent = max(0, n + 1) + end function extent_for +end module imported_extent_provider +""" + + +IMPORTED_EXTENT_OWNER = """ +module imported_extent_owner + use, intrinsic :: iso_c_binding, only: c_double + use imported_extent_provider, only: imported_extent => extent_for + implicit none +contains + function values(n) result(output) + integer, intent(in) :: n + real(c_double) :: output(imported_extent(n)) + output = 16.0_c_double + end function values +end module imported_extent_owner +""" + + +def test_imported_module_specification_function_uses_its_mod_interface(tmp_path: Path): + module, _payload = _build_sources_and_import( + [ + ("imported_extent_provider.f90", IMPORTED_EXTENT_PROVIDER), + ("imported_extent_owner.f90", IMPORTED_EXTENT_OWNER), + ], + tmp_path, + ) + + values = module.imported_extent_owner.values(np.int32(3)) + np.testing.assert_array_equal(values, np.full(4, 16.0)) + bridge = (tmp_path / "bind_c_imported_extent_provider_wrapper.f90").read_text(encoding="utf-8").lower() + assert "use imported_extent_provider, only:" in bridge + assert "=> extent_for" in bridge + assert "pure function extent_for(" not in bridge + + +STANDALONE_EXTENT_SOURCE = """ +pure integer function standalone_extent(n) result(extent) + implicit none + integer, intent(in) :: n + extent = max(0, n + 2) +end function standalone_extent + +pure integer function standalone_value_extent(n) result(extent) + implicit none + integer, value, intent(in) :: n + extent = max(0, n + 3) +end function standalone_value_extent + +module standalone_extent_contract + use, intrinsic :: iso_c_binding, only: c_double + implicit none + interface + pure integer function standalone_extent(n) result(extent) + integer, intent(in) :: n + end function standalone_extent + pure integer function standalone_value_extent(n) result(extent) + integer, value, intent(in) :: n + end function standalone_value_extent + end interface +contains + function values(n) result(output) + integer, intent(in) :: n + real(c_double) :: output(standalone_extent(n)) + output = 17.0_c_double + end function values + function value_values(n) result(output) + integer, intent(in) :: n + real(c_double) :: output(standalone_value_extent(n)) + output = 18.0_c_double + end function value_values +end module standalone_extent_contract +""" + + +STANDALONE_EXTENT_CONTRACT = """ +from prik.contracts import Addr, Float64, In, Int32, prototype, pure + +@pure +@prototype +def standalone_extent(n: In(Addr(Int32))) -> Int32: ... + +@pure +@prototype +def standalone_value_extent(n: In(Int32)) -> Int32: ... + +def values(n: Int32) -> Float64[standalone_extent(n)]: ... + +def value_values(n: Int32) -> Float64[standalone_value_extent(n)]: ... +""" + + +def test_prototype_emits_standalone_extent_entity(tmp_path: Path): + module, result = _build_inline_pyi_contract_module( + tmp_path, + module_name="standalone_extent_contract", + source_text=STANDALONE_EXTENT_SOURCE, + contract_text=STANDALONE_EXTENT_CONTRACT, + ) + + values = module.values(np.int32(3)) + np.testing.assert_array_equal(values, np.full(5, 17.0)) + value_values = module.value_values(np.int32(3)) + np.testing.assert_array_equal(value_values, np.full(6, 18.0)) + + bridge = (result.output_dir / "bind_c_standalone_extent_contract_wrapper.f90").read_text(encoding="utf-8").lower() + binding = (result.output_dir / "standalone_extent_contract_wrapper.c").read_text(encoding="utf-8").lower() + interface_line = next( + line.strip() for line in bridge.splitlines() if line.strip().startswith("pure function prik_standalone_extent_") + ) + interface_symbol = interface_line.split("(", maxsplit=1)[0].split()[-1] + assert bridge.count(f"pure function {interface_symbol}(") == 1 + assert f"procedure({interface_symbol}) :: standalone_extent" in bridge + assert "integer(c_int32_t), intent(in) :: n" in bridge + assert "integer(c_int32_t), value, intent(in) :: n" in bridge + assert "external :: standalone_extent" not in bridge + assert "prik_decl_extent_0_0 = int(standalone_extent(n), c_int64_t)" in bridge + assert "real(c_double), allocatable, dimension(:) :: result_value" in bridge + assert "allocate(result_value(prik_decl_extent_0_0))" in bridge + executable_binding = binding.split("static pyobject * wrap_values", maxsplit=1)[1] + assert "standalone_extent(" not in executable_binding + assert "&prik_decl_extent_0_0" in binding + assert "shape: (standalone_extent(n))" in binding + assert "__prik_callable_" not in binding + + +STANDALONE_TARGET_SOURCE = """ +pure integer function external_extent(n) result(extent) + implicit none + integer, intent(in) :: n + extent = max(0, n + 4) +end function external_extent + +function external_values(n) result(output) + use, intrinsic :: iso_c_binding, only: c_double + implicit none + interface + pure integer function external_extent(n) result(extent) + integer, intent(in) :: n + end function external_extent + end interface + integer, intent(in) :: n + real(c_double) :: output(external_extent(n)) + output = 19.0_c_double +end function external_values +""" + + +STANDALONE_TARGET_CONTRACT = """ +from prik.contracts import Addr, Float64, In, Int32, standalone, prototype, pure + +@pure +@prototype +def external_extent(n: In(Addr(Int32))) -> Int32: ... + +@standalone +def external_values(n: Int32) -> Float64[external_extent(n)]: ... +""" + + +def test_prototype_entity_is_visible_inside_a_standalone_target_interface(tmp_path: Path): + module, result = _build_inline_pyi_contract_module( + tmp_path, + module_name="standalone_target_extent", + source_text=STANDALONE_TARGET_SOURCE, + contract_text=STANDALONE_TARGET_CONTRACT, + ) + + values = module.external_values(np.int32(3)) + np.testing.assert_array_equal(values, np.full(7, 19.0)) + + bridge = (result.output_dir / "bind_c_standalone_target_extent_wrapper.f90").read_text(encoding="utf-8").lower() + interface_line = next( + line.strip() for line in bridge.splitlines() if line.strip().startswith("pure function prik_external_extent_") + ) + interface_symbol = interface_line.split("(", maxsplit=1)[0].split()[-1] + assert bridge.index(f"pure function {interface_symbol}(") < bridge.index("function external_values(") + assert f"procedure({interface_symbol}) :: external_extent" in bridge + assert "import :: c_int32_t, external_extent, c_double" in bridge + assert "real(c_double), dimension(external_extent(n)) :: native_result" in bridge diff --git a/tests/fortran/arrays/end_to_end/test_logical_kind_array_conversions.py b/tests/fortran/arrays/end_to_end/test_logical_kind_array_conversions.py new file mode 100644 index 000000000..c306cbeec --- /dev/null +++ b/tests/fortran/arrays/end_to_end/test_logical_kind_array_conversions.py @@ -0,0 +1,176 @@ +"""End-to-end Boolean array conversion across supported native storage widths.""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_inline_pyi_contract_module, _build_text_and_import + + +pytestmark = pytest.mark.fortran_end_to_end + + +_LOGICAL_KIND_ARRAY_SOURCE = """ +module logical_kind_arrays + use, intrinsic :: iso_c_binding, only: c_bool, c_int32_t + implicit none +contains + + subroutine exercise_c_bool(n, input_values, output_values, inout_values) + integer(c_int32_t), intent(in) :: n + logical(kind=c_bool), intent(in) :: input_values(n) + logical(kind=c_bool), intent(out) :: output_values(n) + logical(kind=c_bool), intent(inout) :: inout_values(n) + output_values = .not. input_values + inout_values = input_values .neqv. inout_values + end subroutine exercise_c_bool + + subroutine exercise_8(n, input_values, output_values, inout_values) + integer(c_int32_t), intent(in) :: n + logical(kind=1), intent(in) :: input_values(n) + logical(kind=1), intent(out) :: output_values(n) + logical(kind=1), intent(inout) :: inout_values(n) + output_values = .not. input_values + inout_values = input_values .neqv. inout_values + end subroutine exercise_8 + + subroutine exercise_16(n, input_values, output_values, inout_values) + integer(c_int32_t), intent(in) :: n + logical(kind=2), intent(in) :: input_values(n) + logical(kind=2), intent(out) :: output_values(n) + logical(kind=2), intent(inout) :: inout_values(n) + output_values = .not. input_values + inout_values = input_values .neqv. inout_values + end subroutine exercise_16 + + subroutine exercise_32(n, input_values, output_values, inout_values) + integer(c_int32_t), intent(in) :: n + logical(kind=4), intent(in) :: input_values(n) + logical(kind=4), intent(out) :: output_values(n) + logical(kind=4), intent(inout) :: inout_values(n) + output_values = .not. input_values + inout_values = input_values .neqv. inout_values + end subroutine exercise_32 + + subroutine exercise_64(n, input_values, output_values, inout_values) + integer(c_int32_t), intent(in) :: n + logical(kind=8), intent(in) :: input_values(n) + logical(kind=8), intent(out) :: output_values(n) + logical(kind=8), intent(inout) :: inout_values(n) + output_values = .not. input_values + inout_values = input_values .neqv. inout_values + end subroutine exercise_64 + +end module logical_kind_arrays +""" + + +def test_boolean_arrays_copy_only_in_required_directions_for_every_supported_width(tmp_path: Path): + module = _build_text_and_import( + _LOGICAL_KIND_ARRAY_SOURCE, + "logical_kind_arrays.f90", + tmp_path, + { + "bind_c_logical_kind_arrays_wrapper.f90", + "logical_kind_arrays_wrapper.c", + "logical_kind_arrays_wrapper.h", + }, + ) + bridge_source = (tmp_path / "bind_c_logical_kind_arrays_wrapper.f90").read_text(encoding="utf-8") + assert bridge_source.count("input_values_native = input_values") == 4 + assert bridge_source.count("inout_values_native = inout_values") == 4 + assert "output_values_native = output_values" not in bridge_source + assert bridge_source.count("output_values = merge(.true._c_bool, .false._c_bool, output_values_native)") == 4 + assert bridge_source.count("inout_values = merge(.true._c_bool, .false._c_bool, inout_values_native)") == 4 + assert "call native_exercise_c_bool(n, input_values, output_values, inout_values)" in bridge_source + input_values = np.array([True, False, True, False], dtype=np.bool_) + initial_inout = np.array([False, False, True, True], dtype=np.bool_) + expected_output = np.logical_not(input_values) + expected_inout = np.logical_xor(input_values, initial_inout) + + for suffix in ("c_bool", "8", "16", "32", "64"): + output_values = np.empty(input_values.shape, dtype=np.bool_) + inout_values = initial_inout.copy() + + result = getattr(module, f"exercise_{suffix}")( + np.int32(input_values.size), + input_values, + output_values, + inout_values, + ) + + assert result is None + assert input_values.dtype == output_values.dtype == inout_values.dtype == np.dtype(np.bool_) + np.testing.assert_array_equal(output_values, expected_output) + np.testing.assert_array_equal(inout_values, expected_inout) + + +def test_numbered_boolean_pyi_contracts_probe_and_call_every_supported_width(tmp_path: Path): + contract = """ +from prik.contracts import Bool8, Bool16, Bool32, Bool64, Int32 + +def exercise_c_bool( + n: Int32, + input_values: Bool8[n], + output_values: Bool8[n], + inout_values: Bool8[n], +) -> None: ... + +def exercise_8( + n: Int32, + input_values: Bool8[n], + output_values: Bool8[n], + inout_values: Bool8[n], +) -> None: ... + +def exercise_16( + n: Int32, + input_values: Bool16[n], + output_values: Bool16[n], + inout_values: Bool16[n], +) -> None: ... + +def exercise_32( + n: Int32, + input_values: Bool32[n], + output_values: Bool32[n], + inout_values: Bool32[n], +) -> None: ... + +def exercise_64( + n: Int32, + input_values: Bool64[n], + output_values: Bool64[n], + inout_values: Bool64[n], +) -> None: ... +""" + module, result = _build_inline_pyi_contract_module( + tmp_path, + module_name="logical_kind_arrays", + source_text=_LOGICAL_KIND_ARRAY_SOURCE, + contract_text=contract, + ) + bridge_source = next(path for path in result.generated_sources if path.suffix == ".f90").read_text(encoding="utf-8") + assert "call native_exercise_8(n, input_values, output_values, inout_values)" in bridge_source + assert "logical(kind=2), dimension(input_values_extent_0) :: input_values_native" in bridge_source + assert "logical(kind=4), dimension(input_values_extent_0) :: input_values_native" in bridge_source + assert "logical(kind=8), dimension(input_values_extent_0) :: input_values_native" in bridge_source + + input_values = np.array([True, False, True, False], dtype=np.bool_) + initial_inout = np.array([False, False, True, True], dtype=np.bool_) + for suffix in ("c_bool", "8", "16", "32", "64"): + output_values = np.empty(input_values.shape, dtype=np.bool_) + inout_values = initial_inout.copy() + + getattr(module, f"exercise_{suffix}")( + np.int32(input_values.size), + input_values, + output_values, + inout_values, + ) + + np.testing.assert_array_equal(output_values, np.logical_not(input_values)) + np.testing.assert_array_equal(inout_values, np.logical_xor(input_values, initial_inout)) diff --git a/tests/fortran/arrays/parsing/test_array_declarations_and_shapes.py b/tests/fortran/arrays/parsing/test_array_declarations_and_shapes.py index e75ec754d..970232dfa 100644 --- a/tests/fortran/arrays/parsing/test_array_declarations_and_shapes.py +++ b/tests/fortran/arrays/parsing/test_array_declarations_and_shapes.py @@ -127,3 +127,29 @@ def test_symbolic_shape_symbols_can_be_collected_and_later_evaluated(): evaluated = evaluate_signature_shapes(sig, {"nx": 6, "ny": 4}) assert evaluated.arguments[0].shape == ["0:6-1", "1:4*2"] + + +def test_balanced_extent_expressions_are_preserved_for_every_declaration_owner(): + code = """ +module declaration_owners + integer, parameter :: n = 3 + real, target :: module_values(max(2, n)) + type :: record + real :: values(product([n, 1])) + end type record +contains + function transform(source, output) result(values) + real, intent(in) :: source(0:, 2:) + real, intent(out) :: output(size(source, dim=2, kind=8)) + real :: values(lbound(source, 1):ubound(source, 1)) + end function transform +end module declaration_owners +""" + module = parse_fortran_file(code).modules[0] + procedure = module.procedures[0] + + assert module.variables[-1].shape == ["3"] + assert module.derived_types[0].fields[0].shape == ["3"] + assert procedure.arguments[0].shape == ["0:", "2:"] + assert procedure.arguments[1].shape == ["size(source, dim=2, kind=8)"] + assert procedure.result.shape == ["lbound(source, 1):ubound(source, 1)"] diff --git a/tests/fortran/arrays/policy/test_array_shape_policy.py b/tests/fortran/arrays/policy/test_array_shape_policy.py index ec2e4e723..65a6c1170 100644 --- a/tests/fortran/arrays/policy/test_array_shape_policy.py +++ b/tests/fortran/arrays/policy/test_array_shape_policy.py @@ -1,8 +1,20 @@ """Completed array extent-reference policy.""" +import pytest + +from tests.fortran._support.semantic_conversion import ( + fortran_module_to_semantic_module, + get_function, + parse_fortran_source, +) from tests.fortran._support.ownership_policy import parse_pyi_text -from prik.semantics.models import RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA +from prik.semantics.models import ( + RESOLVED_DERIVED_TYPE_POLICY_METADATA, + RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA, + RESOLVED_MODULE_VARIABLE_POLICY_METADATA, +) from prik.semantics.policy_completion import complete_semantic_policies +from prik.semantics.wrapper_policy import DeclarationCallableAction def test_array_extent_reference_requires_a_visible_scalar_argument(): @@ -22,3 +34,207 @@ def values() -> Float64[missing]: ... "array owner 'missing_extent.values.return' extent axis 0 has unavailable scalar references ('missing',)" in policy.blockers ) + + +def test_python_array_properties_and_integer_helpers_resolve_to_extent_roles(): + module = parse_pyi_text( + """ +from prik.contracts import Float64 + +def values(source: Float64[:, :]) -> Float64[ + source.size, + source.shape[1], + source.ndim, + len(source), + max(1, source.shape[0] - 1), + 2 ** source.shape[1], +]: ... +""", + module_name="property_extents", + ) + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + assert policy.supported is True + assert policy.results[0].array.shape == ( + "__prik_extent_source_0 * __prik_extent_source_1", + "__prik_extent_source_1", + "2", + "__prik_extent_source_0", + "max(1, __prik_extent_source_0 - 1)", + "2 ** __prik_extent_source_1", + ) + + +def test_persistent_array_extents_reject_unavailable_runtime_values(): + module = parse_pyi_text( + """ +from prik.contracts import Aliased, Annotated, Float64 + +values: Annotated[Float64[missing], Aliased] + +class record: + values: Float64[missing] +""", + module_name="persistent_extents", + ) + complete_semantic_policies(module) + + variable_policy = module.variables[0].metadata[RESOLVED_MODULE_VARIABLE_POLICY_METADATA] + class_policy = module.classes[0].metadata[RESOLVED_DERIVED_TYPE_POLICY_METADATA] + + assert variable_policy.supported is False + assert ( + "module variable 'values' extent axis 0 depends on unavailable declaration values ('missing',)" + in variable_policy.blockers + ) + assert class_policy.supported is False + assert any( + "field 'values' extent axis 0 depends on unavailable declaration values ('missing',)" in blocker + for blocker in class_policy.blockers + ) + + +def test_source_specification_function_completes_as_a_module_bridge_dependency(): + module = fortran_module_to_semantic_module( + parse_fortran_source( + """ +module pure_extent_mod +contains +pure integer function extent_for(n) result(extent) + integer, intent(in) :: n + extent = max(1, n) +end function extent_for + +function values(n) result(output) + integer, intent(in) :: n + real(8) :: output(extent_for(n)) +end function values +end module pure_extent_mod +""" + ) + ) + function = get_function(module, "values") + + assert function.return_type.storage.array.source_shape == ["extent_for(n)"] + assert function.return_type.storage.array.shape == ["extent_for(n)"] + + complete_semantic_policies(module) + + policy = function.metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + assert policy.supported is True + assert policy.results[0].array.extent_blockers == ((),) + assert policy.results[0].array.extent_evaluation == ("bridge",) + assert len(policy.declaration_callables) == 1 + declaration = policy.declaration_callables[0] + assert declaration.action is DeclarationCallableAction.MODULE_IMPORT + assert declaration.native_scope == "pure_extent_mod" + assert declaration.native_name == "extent_for" + assert declaration.prototype is None + assert declaration.blockers == () + + +def test_called_pure_prototype_completes_as_an_exact_standalone_procedure(): + module = parse_pyi_text( + """ +@pure +@prototype +def extent_for(n: In(Addr(Int32))) -> Int32: ... + +def values(n: Int32) -> Float64[extent_for(n)]: ... +""", + module_name="external_extent", + ) + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + declaration = policy.declaration_callables[0] + + assert policy.supported is True + assert declaration.action is DeclarationCallableAction.STANDALONE_PROCEDURE + assert declaration.native_scope is None + assert declaration.prototype is not None + assert declaration.prototype.result.semantic_type_name == "Int32" + assert [(argument.intent, argument.passed_by_value) for argument in declaration.prototype.arguments] == [ + ("in", False) + ] + + +def test_pure_prototype_used_as_callback_and_dummy_extent_is_blocked_before_planning(): + module = parse_pyi_text( + """ +@pure +@prototype +def extent_for(n: In(Addr(Int32))) -> Int32: ... + +def apply( + callback: extent_for, + n: Int32, + values: Float64[extent_for(n)], +) -> None: ... +""", + module_name="mixed_prototype_roles", + ) + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + callback = policy.arguments[0].callback + declaration = policy.declaration_callables[0] + + assert policy.supported is False + assert callback is not None + assert callback.prototype.identity == declaration.prototype.identity + assert callback.prototype.pure is True + assert declaration.prototype.pure is True + assert any("cannot be used as a Python callback" in item for item in policy.blockers) + + +@pytest.mark.parametrize( + ("prototype_decorators", "argument", "result", "call", "blocker"), + [ + ("@prototype", "n: In(Addr(Int32))", "Int32", "extent_for(n)", "must be @pure"), + ( + "@pure\n@prototype", + "n: Addr(Int32)", + "Int32", + "extent_for(n)", + "requires exact In(...) direction", + ), + ( + "@pure\n@prototype", + "n: In(Addr(Int32))", + "Float64", + "extent_for(n)", + "must return one scalar integer", + ), + ( + "@pure\n@prototype", + "n: In(Addr(Int32))", + "Int32", + "extent_for(n, n)", + "expects 1 arguments", + ), + ], +) +def test_invalid_declaration_callable_contracts_block_before_planning( + prototype_decorators: str, + argument: str, + result: str, + call: str, + blocker: str, +): + module = parse_pyi_text( + f""" +{prototype_decorators} +def extent_for({argument}) -> {result}: ... + +def values(n: Int32) -> Float64[{call}]: ... +""", + module_name="invalid_external_extent", + ) + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + + assert policy.supported is False + assert any(blocker in item for item in policy.blockers) diff --git a/tests/fortran/arrays/semantics/test_array_semantics.py b/tests/fortran/arrays/semantics/test_array_semantics.py index 44171becf..5c403389e 100644 --- a/tests/fortran/arrays/semantics/test_array_semantics.py +++ b/tests/fortran/arrays/semantics/test_array_semantics.py @@ -2,10 +2,14 @@ from tests.fortran._support.semantic_conversion import ( array_contract, + fortran_file_to_semantic_modules, fortran_module_to_semantic_module, get_function, + parse_pyi_text, parse_fortran_source, ) +from prik.semantics.models import SemanticExpressionCallable +from prik.codegen.printers import PyiPrinter def test_array_constraints(): @@ -128,3 +132,191 @@ def test_explicit_shape(): A = func.arguments[0] assert A.semantic_type.shape == ["10", "20"] + + +def test_fortran_inquiries_become_python_array_expressions_and_keep_source_bounds(): + source = """ +module inquiry_mod +contains +function transformed(source) result(values) + real(8), intent(in) :: source(0:, 2:) + real(8) :: values( & + ubound(source, 1) - lbound(source, 1) + 1, & + max(1, size(source, dim=2)), & + size(source), & + 2 ** rank(source), & + lbound(source, 2), & + ubound(source, 2)) +end function transformed +end module inquiry_mod +""" + module = fortran_module_to_semantic_module(parse_fortran_source(source)) + result = get_function(module, "transformed").return_type.storage.array + + assert result.source_shape == [ + "ubound(source, 1) - lbound(source, 1) + 1", + "max(1, size(source, dim=2))", + "size(source)", + "2 ** rank(source)", + "lbound(source, 2)", + "ubound(source, 2)", + ] + assert result.shape == [ + "source.shape[0]", + "max(1, source.shape[1])", + "source.size", + "2 ** source.ndim", + "2 if source.shape[1] > 0 else 1", + "2 + source.shape[1] - 1 if source.shape[1] > 0 else 0", + ] + generated = PyiPrinter().emit(module) + assert "source.shape[0], max(1, source.shape[1]), source.size, 2 ** source.ndim" in generated + assert "2 if source.shape[1] > 0 else 1" in generated + assert "2 + source.shape[1] - 1 if source.shape[1] > 0 else 0" in generated + + +def test_specification_function_calls_keep_local_and_imported_native_identity(): + source = """ +module extent_helpers +contains +pure integer function extent_for(n) result(extent) + integer, intent(in) :: n + extent = max(1, n) +end function extent_for +end module extent_helpers + +module expression_owner + use extent_helpers, only: imported_extent => extent_for +contains +pure integer function local_extent(n) result(extent) + integer, intent(in) :: n + extent = max(1, n) +end function local_extent + +function values(n) result(output) + integer, intent(in) :: n + real(8) :: output(imported_extent(n), local_extent(n)) +end function values +end module expression_owner +""" + modules = fortran_file_to_semantic_modules(parse_fortran_source(source)) + module = next(item for item in modules if item.name == "expression_owner") + array = get_function(module, "values").return_type.storage.array + + assert array.expression_callables == [ + [ + SemanticExpressionCallable( + name="imported_extent", + native_name="extent_for", + native_scope="extent_helpers", + source_language="fortran", + placement="module", + ) + ], + [ + SemanticExpressionCallable( + name="local_extent", + native_name="local_extent", + native_scope="expression_owner", + source_language="fortran", + placement="module", + ) + ], + ] + + generated = PyiPrinter().emit(module) + 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 "Float64[imported_extent(n), local_extent(n)]" in generated + assert reloaded_array.expression_callables == array.expression_callables + + +def test_wildcard_specification_function_origin_round_trips_unambiguously(): + source = """ +module extent_helpers +contains +pure integer function extent_for(n) result(extent) + integer, intent(in) :: n + extent = max(1, n) +end function extent_for +end module extent_helpers + +module unrelated_helpers +contains +subroutine unrelated() +end subroutine unrelated +end module unrelated_helpers + +module expression_owner + use extent_helpers + use unrelated_helpers +contains +function values(n) result(output) + integer, intent(in) :: n + real(8) :: output(extent_for(n)) +end function values +end module expression_owner +""" + modules = fortran_file_to_semantic_modules(parse_fortran_source(source)) + module = next(item for item in modules if item.name == "expression_owner") + array = get_function(module, "values").return_type.storage.array + + assert array.expression_callables[0][0].native_scope == "extent_helpers" + + generated = PyiPrinter().emit(module) + 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 reloaded_array.expression_callables == array.expression_callables + + +def test_unindexed_wildcard_specification_function_origin_is_not_guessed(): + source = """ +module expression_owner + use unavailable_helpers +contains +function values(n) result(output) + integer, intent(in) :: n + real(8) :: output(extent_for(n)) +end function values +end module expression_owner +""" + module = fortran_module_to_semantic_module(parse_fortran_source(source)) + reference = get_function(module, "values").return_type.storage.array.expression_callables[0][0] + + assert reference.name == "extent_for" + assert reference.native_name == "extent_for" + assert reference.native_scope is None + + +def test_standalone_specification_interface_round_trips_as_one_pure_prototype_signature(): + source = """ +module expression_owner + interface + pure integer function extent_for(n) result(extent) + integer, intent(in) :: n + end function extent_for + end interface +contains + function values(n) result(output) + integer, intent(in) :: n + real(8) :: output(extent_for(n)) + end function values +end module expression_owner +""" + module = fortran_module_to_semantic_module(parse_fortran_source(source)) + generated = PyiPrinter().emit(module) + reloaded = parse_pyi_text(generated, module_name="expression_owner") + + assert "@pure\n@prototype\ndef extent_for(" in generated + assert "n: In(Addr(Int32))" in generated + assert "@standalone" not in generated + prototype = reloaded.prototypes[0] + assert prototype.pure is True + assert prototype.origin.native_scope == "expression_owner" + assert ( + get_function(reloaded, "values").return_type.storage.array.expression_callables[0][0].placement == "standalone" + ) diff --git a/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py b/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py new file mode 100644 index 000000000..104b0fdac --- /dev/null +++ b/tests/fortran/arrays/semantics/test_declaration_expression_utilities.py @@ -0,0 +1,321 @@ +"""Direct public-API coverage for declaration-expression utility stages.""" + +from __future__ import annotations + +import pytest + +from prik.utilities.declaration_expressions import ( + ArrayExpressionSource, + DeclarationExpressionCall, + ResolvedDeclarationExtent, + canonicalize_declaration_extent, + declaration_expression_call_sites, + declaration_expression_calls, + declaration_extent_references, + declaration_extent_uses_power, + evaluate_integer_expression, + fortran_extent_to_python, + is_declaration_expression_helper, + is_public_declaration_expression, + render_declaration_extent, + resolve_declaration_extent, + split_declaration_assignment, + split_dimension_bounds, + split_top_level_expression, +) + + +def test_source_helpers_keep_nested_syntax_intact() -> None: + """Split only outer declaration delimiters and assignments.""" + assert split_top_level_expression("first, call('a,b'), [third, fourth]", ",") == [ + "first", + "call('a,b')", + "[third, fourth]", + ] + assert split_top_level_expression("'first''part', second", ",") == ["'first''part'", "second"] + assert split_top_level_expression("first::Strided:upper", ":") == ["first", "", "Strided", "upper"] + with pytest.raises(ValueError, match="one character"): + split_top_level_expression("value", "::") + + assert split_dimension_bounds("") == (None, None) + assert split_dimension_bounds("upper") == ("1", "upper") + assert split_dimension_bounds("lower:upper") == ("lower", "upper") + assert split_dimension_bounds(":upper") == (None, "upper") + assert split_dimension_bounds("lower:") == ("lower", None) + assert split_dimension_bounds("lower:max(first:second, upper)") == ("lower", "max(first:second, upper)") + + assert split_declaration_assignment("value = merge(first, second, mask)") == ( + "value", + "merge(first, second, mask)", + ) + assert split_declaration_assignment("pointer => target") == ("pointer", "target") + assert split_declaration_assignment("value == other") == ("value == other", None) + assert split_declaration_assignment("value = size(array, dim=1)") == ("value", "size(array, dim=1)") + assert split_declaration_assignment("character = 'a''b'") == ("character", "'a''b'") + + +def test_normalization_and_inspection_preserve_expression_provenance() -> None: + """Translate known inquiries while leaving unknown calls available to policy.""" + arrays = {"source": ArrayExpressionSource(rank=2, lower_bounds=("0", "2"))} + + assert fortran_extent_to_python("size(source)", arrays) == "source.size" + assert fortran_extent_to_python("size(source, dim=2)", arrays) == "source.shape[1]" + assert fortran_extent_to_python("shape(source)", arrays) == "source.shape" + assert fortran_extent_to_python("rank(source)", arrays) == "source.ndim" + assert fortran_extent_to_python("lbound(source, 2)", arrays) == "2 if source.shape[1] > 0 else 1" + assert ( + fortran_extent_to_python("ubound(source, 2)", arrays) == "2 + source.shape[1] - 1 if source.shape[1] > 0 else 0" + ) + assert fortran_extent_to_python("ubound(source, 1) - lbound(source, 1) + 1", arrays) == "source.shape[0]" + assert fortran_extent_to_python("mod(n, 3)", arrays) == "n % 3" + assert fortran_extent_to_python("merge(first, second, mask)", arrays) == "first if mask else second" + assert fortran_extent_to_python("product((/ 2, 3 /))", arrays) == "2 * 3" + assert fortran_extent_to_python("product(shape(source))", arrays) == "source.size" + assert fortran_extent_to_python("sum((/ 2, 3 /))", arrays) == "sum([2, 3])" + assert fortran_extent_to_python("maxval((/ 2, 3 /))", arrays) == "max([2, 3])" + assert fortran_extent_to_python("minval((/ 2, 3 /))", arrays) == "min([2, 3])" + assert fortran_extent_to_python("extent_for(n, kind=4)", arrays) == "extent_for(n)" + assert fortran_extent_to_python("size(unknown, dim=1)", arrays) == "size(unknown, dim=1)" + assert fortran_extent_to_python("size(source, dim=3)", arrays) == "source.shape[2]" + assert fortran_extent_to_python("size()", arrays) == "size()" + assert fortran_extent_to_python("size(source, dim=index)", arrays) == "size(source, dim=index)" + assert fortran_extent_to_python("size(source, dim=1, DIM=2)", arrays) == "size(source, dim=1, DIM=2)" + assert fortran_extent_to_python("shape(source, 1, 2)", arrays) == "shape(source, 1, 2)" + assert fortran_extent_to_python("size(source + 1)", arrays) == "size(source + 1)" + assert ( + fortran_extent_to_python("lbound(source)", arrays) + == "(0 if source.shape[0] > 0 else 1, 2 if source.shape[1] > 0 else 1)" + ) + assert fortran_extent_to_python("ubound(source)", arrays) == ( + "(0 + source.shape[0] - 1 if source.shape[0] > 0 else 0, 2 + source.shape[1] - 1 if source.shape[1] > 0 else 0)" + ) + assert fortran_extent_to_python("lbound(source, 3)", arrays) == "lbound(source, 3)" + assert fortran_extent_to_python("ubound(source, 1) - lbound(source, 2) + 1", arrays) == ( + "(0 + source.shape[0] - 1 if source.shape[0] > 0 else 0) - (2 if source.shape[1] > 0 else 1) + 1" + ) + assert fortran_extent_to_python("len('a''b')", arrays) == "len('ab')" + assert fortran_extent_to_python("rank(source, 1)", arrays) == "rank(source, 1)" + assert fortran_extent_to_python("not valid (") == "not valid (" + + assert canonicalize_declaration_extent("n + 3 - n") == "3" + assert canonicalize_declaration_extent("n + n - n") == "n" + assert canonicalize_declaration_extent("not valid (") == "not valid (" + assert declaration_expression_calls("extent_for(n) + helpers.other(m)") == ("extent_for", "helpers.other") + assert declaration_expression_call_sites("extent_for(n) + helpers.other(m, 1)") == ( + DeclarationExpressionCall("extent_for", 1), + DeclarationExpressionCall("helpers.other", 2), + ) + assert declaration_expression_calls("not valid (") == ("",) + assert declaration_expression_call_sites("not valid (") == (DeclarationExpressionCall("", 0),) + 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_uses_power("n ** 2") + assert not declaration_extent_uses_power("not valid (") + assert is_declaration_expression_helper("SUM") + assert not is_declaration_expression_helper("helpers.sum") + + +@pytest.mark.parametrize( + ("expression", "expected"), + [ + ("n + values.shape[1]", True), + ("max(1, n)", True), + ("helpers.extent_for(n)", True), + ("n if flag else m", True), + ("sum([n, m])", True), + ("values.shape[index]", False), + ("values.unknown", False), + ("len(values, default=0)", False), + ("factory()(n)", False), + ("1.5", False), + ("not valid (", False), + ], +) +def test_public_expression_grammar_rejects_unsupported_syntax(expression: str, expected: bool) -> None: + """Keep public grammar validation separate from producer-role binding.""" + assert is_public_declaration_expression(expression) is expected + + +@pytest.mark.parametrize( + ("expression", "expected"), + [ + ("abs(-3)", 3), + ("max(3, 7, 4)", 7), + ("min(3, 7, 4)", 3), + ("modulo(8, 3)", 2), + ("product((/ 2, 3, 4 /))", 24), + ("sum((/ 2, 3, 4 /))", 9), + ("maxval((/ 2, 3, 4 /))", 4), + ("minval((/ 2, 3, 4 /))", 2), + ("merge(4, 2, .true.)", 4), + ("int(2.9)", 2), + ("len('abc')", 3), + ("len_trim('a ')", 1), + ("iachar('A')", 65), + ("2 ** 3", 8), + ("3 if 1 < 2 else 4", 3), + ("True and not False", 1), + ("False or True", 1), + ("1 == 1 == 1", 1), + ("1 != 2", 1), + ("1 >= 1", 1), + ("1 <= 1", 1), + ("+3", 3), + ("1 // 1", 1), + ("1 << 1", None), + ("~1", None), + ("int(2.9, kind=4)", 2), + ("int(2, base=10)", None), + ("abs(1, 2)", None), + ("iachar('')", None), + ("len(1)", None), + ("sum((/ /))", None), + ("1 / 0", None), + ("max()", None), + ("unknown(3)", None), + ("'not an integer result'", None), + ], +) +def test_compile_time_evaluator_only_executes_supported_integer_expressions( + expression: str, expected: int | None +) -> None: + """Exercise the intrinsic subset and its no-exception failure sentinel.""" + assert evaluate_integer_expression(expression) == expected + + +def test_role_resolution_reuses_completed_roles_and_names_blockers() -> None: + """Bind scalars, array inquiries, and native calls without guessing missing roles.""" + scalar_roles = {"n": ("number", "number_role"), "m": ("count", "count_role")} + 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("n + values.shape[1]", scalar_roles, array_roles) == ResolvedDeclarationExtent( + "n + __prik_extent_values_1", + ("n", "__prik_extent_values_1"), + ("number_role", "value_role_1"), + ) + assert resolve_declaration_extent("values.size", scalar_roles, array_roles) == ResolvedDeclarationExtent( + "__prik_extent_values_0 * __prik_extent_values_1", + ("__prik_extent_values_0", "__prik_extent_values_1"), + ("value_role_0", "value_role_1"), + ) + assert resolve_declaration_extent("values.ndim", scalar_roles, array_roles) == ResolvedDeclarationExtent("2") + assert resolve_declaration_extent("len(values)", scalar_roles, array_roles) == ResolvedDeclarationExtent( + "__prik_extent_values_0", ("__prik_extent_values_0",), ("value_role_0",) + ) + assert resolve_declaration_extent("sum(values.shape)", scalar_roles, array_roles) == ResolvedDeclarationExtent( + "__prik_extent_values_0 + __prik_extent_values_1", + ("__prik_extent_values_0", "__prik_extent_values_1"), + ("value_role_0", "value_role_1"), + ) + assert resolve_declaration_extent("max(values.shape)", scalar_roles, array_roles) == ResolvedDeclarationExtent( + "max(__prik_extent_values_0, __prik_extent_values_1)", + ("__prik_extent_values_0", "__prik_extent_values_1"), + ("value_role_0", "value_role_1"), + ) + assert resolve_declaration_extent( + "extent_for(n)", scalar_roles, array_roles, callable_roles + ) == ResolvedDeclarationExtent( + "prik_extent_for(n)", + ("n",), + ("number_role",), + ("prik_extent_for",), + ("extent_role",), + ) + assert resolve_declaration_extent("other(n)", scalar_roles, array_roles) == ResolvedDeclarationExtent( + "other(n)", ("other()",), blockers=("other()",) + ) + assert resolve_declaration_extent("len(n)", scalar_roles, array_roles) == ResolvedDeclarationExtent( + "len(n)", ("n",), blockers=("n",) + ) + assert resolve_declaration_extent("values.shape[3]", scalar_roles, array_roles) == ResolvedDeclarationExtent( + "values.shape[3]", ("",), blockers=("",) + ) + assert resolve_declaration_extent("missing", scalar_roles, array_roles).blockers == ("missing",) + assert resolve_declaration_extent("missing.size", scalar_roles, array_roles).blockers == ("missing",) + assert resolve_declaration_extent("values.shape[index]", scalar_roles, array_roles).blockers == ("",) + assert resolve_declaration_extent("missing.shape[0]", scalar_roles, array_roles).blockers == ("missing",) + assert resolve_declaration_extent("max(n, other=m)", scalar_roles, array_roles).blockers == ("max()",) + assert resolve_declaration_extent("helper.other(n)", scalar_roles, array_roles).blockers == ("helper.other()",) + assert resolve_declaration_extent("len()", scalar_roles, array_roles).blockers == ("",) + assert resolve_declaration_extent("abs(n, m)", scalar_roles, array_roles).blockers == ("abs()",) + assert resolve_declaration_extent("sum(missing.shape)", scalar_roles, array_roles).blockers == ("missing",) + assert resolve_declaration_extent("sum([n, m])", scalar_roles, array_roles) == ResolvedDeclarationExtent( + "n + m", ("n", "m"), ("number_role", "count_role") + ) + assert resolve_declaration_extent("max([n, m])", scalar_roles, array_roles) == ResolvedDeclarationExtent( + "max(n, m)", ("n", "m"), ("number_role", "count_role") + ) + + +@pytest.mark.parametrize( + ("expression", "target", "expected"), + [ + ("n + m * 2", "c", "native_n + native_m * 2"), + ("n ** 2", "c", "prik_extent_power((native_n), (2))"), + ("n ** 2", "fortran", "native_n ** 2"), + ("n % 3", "fortran", "mod((native_n), (3))"), + ("n % 3", "c", "native_n % 3"), + ("n * (m + limit)", "c", "native_n * (native_m + native_limit)"), + ("n - (m - limit)", "fortran", "native_n - (native_m - native_limit)"), + ("(n ** m) ** limit", "fortran", "(native_n ** native_m) ** native_limit"), + ("n ** (m ** limit)", "c", "prik_extent_power((native_n), (prik_extent_power((native_m), (native_limit))))"), + ("-(n + m)", "c", "-(native_n + native_m)"), + ("+n", "fortran", "+native_n"), + ("not flag", "fortran", ".not. (native_flag)"), + ("not flag", "c", "! (native_flag)"), + ("n and m or flag", "c", "((((native_n) && (native_m))) || (native_flag))"), + ("n and m", "fortran", "((native_n) .and. (native_m))"), + ("n or m", "fortran", "((native_n) .or. (native_m))"), + ("n < m <= limit", "fortran", "(((native_n) .lt. (native_m)) .and. ((native_m) .le. (native_limit)))"), + ("n == m", "c", "(((native_n) == (native_m)))"), + ("n != m", "fortran", "(((native_n) .ne. (native_m)))"), + ("n > m", "c", "(((native_n) > (native_m)))"), + ("n >= m", "fortran", "(((native_n) .ge. (native_m)))"), + ("n if flag else m", "c", "((native_flag) ? (native_n) : (native_m))"), + ("n if flag else m", "fortran", "merge((native_n), (native_m), (native_flag))"), + ("int(n)", "c", "((npy_intp)(native_n))"), + ("int(n)", "fortran", "int(native_n)"), + ("abs(n)", "fortran", "abs(native_n)"), + ("abs(n)", "c", "((native_n) < 0 ? -(native_n) : (native_n))"), + ("max(n, m, limit)", "fortran", "max(native_n, native_m, native_limit)"), + ( + "max(n, m)", + "c", + "((native_n) > (native_m) ? (native_n) : (native_m))", + ), + ("min(n, m)", "c", "((native_n) < (native_m) ? (native_n) : (native_m))"), + ("min(n, m)", "fortran", "min(native_n, native_m)"), + ("extent_for(n)", "c", "native_extent_for(native_n)"), + ("True", "c", "1"), + ("False", "fortran", ".false."), + ("...", "c", "..."), + ], +) +def test_backend_renderer_preserves_completed_expression_semantics( + expression: str, + target: str, + expected: str, +) -> None: + """Render completed token expressions for each backend without re-planning policy.""" + substitutions = { + "n": "native_n", + "m": "native_m", + "limit": "native_limit", + "flag": "native_flag", + "extent_for": "native_extent_for", + } + assert render_declaration_extent(expression, substitutions, target=target) == expected + + +def test_backend_renderer_rejects_invalid_target_and_unrenderable_syntax() -> None: + """Expose API errors instead of inventing output for invalid completed input.""" + with pytest.raises(ValueError, match="unsupported declaration-expression target"): + render_declaration_extent("n", {}, target="python") + with pytest.raises(ValueError, match="invalid completed declaration expression"): + render_declaration_extent("not valid (", {}, target="c") + with pytest.raises(ValueError, match="unsupported completed declaration-expression node"): + render_declaration_extent("[n]", {}, target="c") diff --git a/tests/fortran/building_shared_library/end_to_end/real_libraries/__init__.py b/tests/fortran/building_shared_library/end_to_end/real_libraries/__init__.py new file mode 100644 index 000000000..0c1967436 --- /dev/null +++ b/tests/fortran/building_shared_library/end_to_end/real_libraries/__init__.py @@ -0,0 +1 @@ +"""Compiled numerical journeys for actual third-party library sources.""" diff --git a/tests/fortran/building_shared_library/end_to_end/real_libraries/_support.py b/tests/fortran/building_shared_library/end_to_end/real_libraries/_support.py new file mode 100644 index 000000000..480089724 --- /dev/null +++ b/tests/fortran/building_shared_library/end_to_end/real_libraries/_support.py @@ -0,0 +1,47 @@ +"""Shared source discovery and build helpers for real-library showcases.""" + +from __future__ import annotations + +import os +from pathlib import Path +import shutil + +import pytest + +from prik import build_fortran_extension +from tests.fortran._support.wrapper_build import _import_from_build_dir + + +REPOSITORY_ROOT = Path(__file__).resolve().parents[5] + + +def real_library_source_dir(library: str) -> Path: + """Locate one configured or sibling real-library source directory.""" + environment_name = f"PRIK_{library.upper()}_SOURCE_DIR" + configured = os.environ.get(environment_name) + source_dir = Path(configured).expanduser() if configured else REPOSITORY_ROOT.parent / library / "src" + if not source_dir.is_dir(): + pytest.skip(f"{library} source directory is unavailable: set {environment_name}") + return source_dir + + +def build_real_fortran_library( + library: str, + sources: list[Path], + build_dir: Path, + *, + native_fortran_sources: list[Path] | None = None, +): + """Build and import one actual third-party source set through the public API.""" + if shutil.which("gfortran") is None: + pytest.skip("gfortran is required for real-library showcases") + if not sources or any(not source.is_file() for source in sources): + pytest.skip(f"{library} checkout does not contain the expected source files") + result = build_fortran_extension( + sources, + native_fortran_sources=native_fortran_sources, + output_dir=build_dir, + output_name=f"prik_{library}_showcase", + ) + assert result.shared_library.exists() + return _import_from_build_dir(result.module_name, result.output_dir) diff --git a/tests/fortran/building_shared_library/end_to_end/real_libraries/test_fftpack_routines.py b/tests/fortran/building_shared_library/end_to_end/real_libraries/test_fftpack_routines.py new file mode 100644 index 000000000..ad91c46b3 --- /dev/null +++ b/tests/fortran/building_shared_library/end_to_end/real_libraries/test_fftpack_routines.py @@ -0,0 +1,60 @@ +"""Build actual FFTPACK sources and verify representative transform answers.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from tests.fortran.building_shared_library.end_to_end.real_libraries._support import ( + build_real_fortran_library, + real_library_source_dir, +) + + +pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] + + +@pytest.fixture(scope="module") +def fftpack(tmp_path_factory: pytest.TempPathFactory): + source_dir = real_library_source_dir("fftpack") + public_sources = [ + source_dir / "rk.f90", + source_dir / "fftpack.f90", + *sorted(source_dir.glob("fftpack_*.f90")), + ] + public_source_names = {source.name for source in public_sources} + link_only_sources = [ + source for source in sorted(source_dir.glob("*.f90")) if source.name not in public_source_names + ] + extension = build_real_fortran_library( + "fftpack", + public_sources, + tmp_path_factory.mktemp("fftpack-showcase"), + native_fortran_sources=link_only_sources, + ) + return extension.fftpack + + +def test_fft_and_ifft_return_known_impulse_transforms(fftpack): + impulse = np.array([1.0 + 0.0j, 0.0j, 0.0j, 0.0j], dtype=np.complex128) + spectrum = fftpack.fft(impulse) + inverse = fftpack.ifft(np.ones(4, dtype=np.complex128)) + try: + np.testing.assert_allclose(spectrum.to_numpy(), np.ones(4, dtype=np.complex128)) + np.testing.assert_allclose( + inverse.to_numpy(), + np.array([4.0 + 0.0j, 0.0j, 0.0j, 0.0j], dtype=np.complex128), + atol=1.0e-12, + ) + finally: + spectrum.close() + inverse.close() + + +def test_fftshift_and_ifftshift_are_inverse_permutations(fftpack): + values = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64) + + shifted = fftpack.fftshift(values) + + np.testing.assert_array_equal(shifted, np.array([3.0, 4.0, 1.0, 2.0])) + np.testing.assert_array_equal(fftpack.ifftshift(shifted), values) diff --git a/tests/fortran/building_shared_library/end_to_end/real_libraries/test_minpack_routines.py b/tests/fortran/building_shared_library/end_to_end/real_libraries/test_minpack_routines.py new file mode 100644 index 000000000..89dfc06c4 --- /dev/null +++ b/tests/fortran/building_shared_library/end_to_end/real_libraries/test_minpack_routines.py @@ -0,0 +1,56 @@ +"""Build actual MINPACK sources and verify representative numerical answers.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from tests.fortran.building_shared_library.end_to_end.real_libraries._support import ( + build_real_fortran_library, + real_library_source_dir, +) + + +pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] + + +@pytest.fixture(scope="module") +def minpack(tmp_path_factory: pytest.TempPathFactory): + source_dir = real_library_source_dir("minpack") + extension = build_real_fortran_library( + "minpack", + [source_dir / "minpack.f90"], + tmp_path_factory.mktemp("minpack-showcase"), + ) + return extension.minpack_module + + +def test_enorm_returns_the_known_euclidean_norm(minpack): + values = np.array([3.0, 4.0, 12.0], dtype=np.float64) + + assert minpack.enorm(np.int32(values.size), values) == pytest.approx(13.0) + + +def test_qrfac_returns_known_column_norms_and_r_diagonal(minpack): + matrix = np.asfortranarray([[3.0, 0.0], [4.0, 5.0]], dtype=np.float64) + pivots = np.zeros(2, dtype=np.int32) + diagonal = np.zeros(2, dtype=np.float64) + column_norms = np.zeros(2, dtype=np.float64) + workspace = np.zeros(2, dtype=np.float64) + + minpack.qrfac( + np.int32(2), + np.int32(2), + matrix, + np.int32(2), + True, + pivots, + np.int32(2), + diagonal, + column_norms, + workspace, + ) + + np.testing.assert_array_equal(pivots, np.array([1, 2], dtype=np.int32)) + np.testing.assert_allclose(column_norms, np.array([5.0, 5.0])) + np.testing.assert_allclose(diagonal, np.array([-5.0, -3.0]), atol=1.0e-12) diff --git a/tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py b/tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py index 889850671..9142b41c7 100644 --- a/tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py +++ b/tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py @@ -249,6 +249,36 @@ def test_multi_file_standalone_procedures_build_one_merged_extension(tmp_path: P assert module.double_value(np.int32(4)) == (8, 4) +def test_multi_file_build_resolves_reexported_intrinsic_kind_alias(tmp_path: Path): + module, payload = _build_sources_and_import( + [ + ( + "kind_consumer.f90", + """function twice(value) result(output) + use fftpack_kind, only: dp => rk + implicit none + real(dp), intent(in) :: value + real(dp) :: output + output = 2.0_dp * value +end function twice +""", + ), + ( + "fftpack_kind.f90", + """module fftpack_kind + use, intrinsic :: iso_fortran_env, only: rk => real64 + implicit none +end module fftpack_kind +""", + ), + ], + tmp_path, + ) + + assert payload["module_name"] == "kind_consumer" + assert module.twice(np.float64(1.25)) == np.float64(2.5) + + def test_multi_source_pyi_out_writes_one_flat_combined_package(tmp_path: Path): sources = _write_combined_sources(tmp_path) package = tmp_path / "contracts" diff --git a/tests/fortran/building_shared_library/end_to_end/test_native_bundles.py b/tests/fortran/building_shared_library/end_to_end/test_native_bundles.py index 65f275e48..04c7e11d9 100644 --- a/tests/fortran/building_shared_library/end_to_end/test_native_bundles.py +++ b/tests/fortran/building_shared_library/end_to_end/test_native_bundles.py @@ -21,7 +21,7 @@ ) -CONTRACT_IMPORT = "from prik.contracts import Addr, Arg, Int32, external, native_call\n\n" +CONTRACT_IMPORT = "from prik.contracts import Addr, Arg, Int32, native_call, standalone\n\n" NATIVE_CALL_IMPORT = "from prik.contracts import Addr, Arg, Int32, native_call\n\n" pytestmark = pytest.mark.fortran_end_to_end @@ -114,7 +114,7 @@ def _simple_external_contract(name: str) -> str: def _simple_external_declaration(name: str) -> str: - return f"@external\n@native_call([Addr(Arg(0))])\ndef {name}(value: Int32) -> Int32: ...\n" + return f"@standalone\n@native_call([Addr(Arg(0))])\ndef {name}(value: Int32) -> Int32: ...\n" def _simple_external_source(name: str, expression: str) -> str: @@ -195,7 +195,7 @@ def test_mixed_module_external_bundle_resolves_all_native_input_kinds(tmp_path: entry = _write_contract_package( tmp_path / "contracts" / "mixed_native_bundle", entry=( - "from prik.contracts import Addr, Arg, Int32, external, native_call\n" + "from prik.contracts import Addr, Arg, Int32, native_call, standalone\n" "from . import artifact_mod\n\n" f"{_simple_external_declaration('ext_object')}\n" f"{_simple_external_declaration('ext_archive')}\n" diff --git a/tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py b/tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py index fa96b35de..06678d65b 100644 --- a/tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py +++ b/tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py @@ -21,9 +21,22 @@ SCALE_SOURCE = NATIVE_FIXTURES / "scale.f90" SCALAR_SOURCE = SCALE_SOURCE HOME_POINTS_SOURCE = NATIVE_FIXTURES / "home_points.f90" +BUILD_MODULE = Path(__file__).resolve().parents[4] / "prik" / "pipeline" / "build.py" pytestmark = pytest.mark.fortran_end_to_end +def test_build_module_direct_execution_runs_the_public_api_example(): + result = subprocess.run( + [sys.executable, str(BUILD_MODULE)], + capture_output=True, + text=True, + check=True, + cwd=BUILD_MODULE.parents[2], + ) + + assert result.stdout == "scale(3.0, 2.5) = 7.5\n" + + def test_verbose_mode_prints_full_direct_build_commands(tmp_path: Path): source = tmp_path / "verbose_api.f90" shutil.copyfile(VERBOSE_SOURCE, source) @@ -260,7 +273,7 @@ def test_fortran_wrapper_out_names_importable_shared_library(tmp_path: Path): assert module.scale(np.float64(3.0), np.float64(2.5)) == np.float64(7.5) -def test_documented_homepage_points_example_builds_and_imports(tmp_path: Path): +def test_documented_readme_points_example_builds_and_imports(tmp_path: Path): source = tmp_path / "points.f90" build_dir = tmp_path / "build" / "geometry" shutil.copyfile(HOME_POINTS_SOURCE, source) diff --git a/tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi b/tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi index e71d7fad2..e191cf56a 100644 --- a/tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi +++ b/tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi @@ -1,6 +1,6 @@ -from prik.contracts import Addr, Arg, Int32, Returns, external, native_call +from prik.contracts import Addr, Arg, Int32, Returns, native_call, standalone -@external +@standalone @native_call([Addr(Arg(0))]) def add_one( value: Int32 diff --git a/tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py b/tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py index d91a443be..ab357bec1 100644 --- a/tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py +++ b/tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py @@ -12,7 +12,7 @@ import pytest from prik import build_pyi_extension -from prik.pipeline.build import build_fortran_extension +from prik.pipeline.build import WrapperBuildResult, build_fortran_extension from tests.fortran._support.pyi_fixtures import assert_generated_pyi_package_matches_fixture FEATURE_ROOT = Path(__file__).resolve().parents[1] @@ -111,6 +111,36 @@ def _assert_scale_runtime_contract(module) -> None: assert module.scale(np.float64(2.0), np.float64(4.0)) == np.float64(8.0) +def test_wrapper_build_result_import_module_loads_and_caches_a_built_extension(tmp_path: Path): + result = build_fortran_extension(SOURCE, output_dir=tmp_path / "source_build") + + sys.modules.pop(result.module_name, None) + try: + module = result.import_module() + assert module.__file__ == str(result.shared_library) + native_module = _sole_native_module(module) + assert native_module.scale(np.float64(3.0), np.float64(2.5)) == np.float64(7.5) + assert result.import_module() is module + finally: + sys.modules.pop(result.module_name, None) + + +def test_wrapper_build_result_import_module_requires_a_built_artifact(tmp_path: Path): + result = WrapperBuildResult( + sources=(), + module_name="missing_extension", + output_dir=tmp_path, + shared_library=tmp_path / "missing_extension.so", + build_makefile=None, + compiled=False, + generated_sources=(), + generated_files=(), + ) + + with pytest.raises(FileNotFoundError, match=r"Built extension not found: .+missing_extension\.so"): + result.import_module() + + @pytest.fixture def scale_runtime_module(pyi_parity_build_mode: str, tmp_path: Path): if pyi_parity_build_mode == "source": diff --git a/tests/fortran/building_shared_library/pipeline/test_rendered_wrapper_artifact_build.py b/tests/fortran/building_shared_library/pipeline/test_rendered_wrapper_artifact_build.py index 2fa312124..4ee9eb402 100644 --- a/tests/fortran/building_shared_library/pipeline/test_rendered_wrapper_artifact_build.py +++ b/tests/fortran/building_shared_library/pipeline/test_rendered_wrapper_artifact_build.py @@ -21,7 +21,7 @@ ) from prik.semantics.policy_completion import complete_semantic_policies from prik.stage_values import FrozenStageRecordError -from prik.wrapper_codegen import ( +from prik.codegen import ( ModulePlan, WrapperCodeGenerator, WrapperPlanner, diff --git a/tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py b/tests/fortran/callbacks/codegen/test_callback_planning.py similarity index 91% rename from tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py rename to tests/fortran/callbacks/codegen/test_callback_planning.py index b82377ff9..8d3106269 100644 --- a/tests/fortran/callbacks/wrapper_codegen/test_callback_planning.py +++ b/tests/fortran/callbacks/codegen/test_callback_planning.py @@ -15,10 +15,9 @@ CallbackResultAction, CallbackThreadAction, CallbackTransferAction, - ExternalDeclarationMode, ) -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner -from prik.wrapper_codegen.plan import DatatypeFamily +from prik.codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen.plan import DatatypeFamily CONTRACT_ROOT = Path(__file__).parents[1] / "end_to_end" / "fixtures" / "contracts" CONTRACT = CONTRACT_ROOT / "fcallback_all_f90" / "fcallback_all_f90.pyi" @@ -64,7 +63,11 @@ 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 - assert tuple(transfer.adapter_action for transfer in scalar.arguments) == (CallbackTransferAction.COPY_IN,) * 3 + assert tuple(transfer.adapter_action for transfer in scalar.arguments) == ( + CallbackTransferAction.COPY_IN_OUT, + CallbackTransferAction.COPY_OUT, + CallbackTransferAction.COPY_IN, + ) assert tuple(transfer.python_action for transfer in scalar.arguments) == (PythonBarrierAction.SCALAR_VALUE,) * 3 array = policies["apply_array_storage_callback"].arguments[0].callback @@ -164,7 +167,7 @@ def test_callback_artifacts_use_linear_context_adapter_and_trampoline_paths(): assert "integer(c_int32_t), value :: value" in bridge assert "integer(c_int32_t) :: count" in bridge - assert "external :: prik_callback_adapter" in bridge + assert "procedure(prik_" in bridge assert 'bind(c, name="prik_callback_trampoline' in bridge assert "size(values_callback_storage, dim=1, kind=c_int64_t)" in bridge assert "int(len(read_label_callback_storage), kind=c_int64_t)" in bridge @@ -198,20 +201,21 @@ def test_nogil_callback_call_releases_outer_envelope_and_reacquires_in_trampolin assert "PyGILState_Release(" in c_source -def test_callback_declaration_uses_external_unless_prototype_requires_explicit_interface(): +def test_every_callback_uses_the_shared_generated_abstract_prototype(): module = pyi_file_to_semantic_module(ARRAY_CONTRACT, module_name="fcallback_array_f90") complete_semantic_policies(module) plan = WrapperPlanner().build(module) reduce = _callback_argument(plan, "apply_reduce").callback transform = _callback_argument(plan, "apply_transform").callback - assert reduce.declaration_mode is ExternalDeclarationMode.IMPLICIT_EXTERNAL - assert transform.declaration_mode is ExternalDeclarationMode.EXPLICIT_INTERFACE + assert reduce.prototype.interface_symbol.startswith("prik_reduce_callback_") + assert transform.prototype.interface_symbol.startswith("prik_transform_callback_") _, bridge = _sources(plan) - assert f"real(c_double), external :: {reduce.adapter_symbol}" in bridge - assert f"procedure({transform.adapter_symbol}_prototype) :: {transform.adapter_symbol}" in bridge - assert f"{transform.adapter_symbol}_prototype => transform_callback" in bridge + assert f"procedure({reduce.prototype.interface_symbol}) :: {reduce.adapter_symbol}" in bridge + assert f"procedure({transform.prototype.interface_symbol}) :: {transform.adapter_symbol}" in bridge + assert "abstract interface" in bridge + assert "=> transform_callback" not in bridge def test_optional_callback_retains_one_exact_policy_blocker(): 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 63af67388..b6895f49a 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 @@ -1,4 +1,4 @@ -from prik.contracts import Addr, Arg, Float64, Int32, Return, Returns, String, native_call, prototype +from prik.contracts import Addr, Arg, Float64, In, InOut, Int32, Out, Return, Returns, String, native_call, prototype class point_t: def __init__( @@ -13,33 +13,33 @@ class point_t: @prototype def value_callback( - value: Int32 + value: In(Int32) ) -> Int32: ... @prototype def scalar_storage_callback( - value: Addr(Float64), - output: Addr(Float64), + value: InOut(Addr(Float64)), + output: Out(Addr(Float64)), missing: Addr(Float64) ) -> None: ... @prototype def array_storage_callback( - count: Addr(Int32), - values: Float64[count], - output: Float64[count] + count: In(Addr(Int32)), + values: In(Float64[count]), + output: Out(Float64[count]) ) -> None: ... @prototype def string_storage_callback( - read_label: String[8], - write_label: String[8], - update_label: String[8] + read_label: In(String[8]), + write_label: Out(String[8]), + update_label: InOut(String[8]) ) -> None: ... @prototype def point_callback( - value: point_t + value: In(point_t) ) -> point_t: ... @native_call([Arg(0), Addr(Arg(1))]) 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 3efffa977..5623f4aad 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,15 +1,15 @@ -from prik.contracts import Addr, Arg, Float64, Int32, native_call, prototype +from prik.contracts import Addr, Arg, Float64, In, Int32, native_call, prototype @prototype def reduce_callback( - count: Addr(Int32), - values: Float64[count] + count: In(Addr(Int32)), + values: In(Float64[count]) ) -> Float64: ... @prototype def transform_callback( - count: Addr(Int32), - values: Float64[count] + count: In(Addr(Int32)), + values: In(Float64[count]) ) -> Float64[count]: ... @native_call([Arg(0), Addr(Arg(1)), Arg(2)]) diff --git a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi index 5592e7783..1e9b2d239 100644 --- a/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi +++ b/tests/fortran/callbacks/end_to_end/fixtures/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi @@ -1,18 +1,18 @@ -from prik.contracts import Addr, Arg, Float64, native_call, prototype +from prik.contracts import Addr, Arg, Float64, In, native_call, prototype @prototype def scalar_callback( - value: Addr(Float64) + value: In(Addr(Float64)) ) -> Float64: ... @prototype def notify_callback( - value: Addr(Float64) + value: In(Addr(Float64)) ) -> None: ... @prototype def callback( - value: Addr(Float64) + value: In(Addr(Float64)) ) -> Float64: ... @native_call([Arg(0), Addr(Arg(1))]) diff --git a/tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py b/tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py index a14e1178b..e698e0804 100644 --- a/tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py +++ b/tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py @@ -41,7 +41,7 @@ def add_five(value): def array_callback(count, input_values, output_values): assert isinstance(count, np.int32) assert input_values.flags.f_contiguous - assert input_values.flags.writeable + assert not input_values.flags.writeable assert output_values.flags.writeable output_values[:count] = input_values[:count] + 1.5 @@ -50,13 +50,11 @@ def array_callback(count, input_values, output_values): np.testing.assert_allclose(output, np.array([2.5, 3.5, 4.5], dtype=np.float64)) def string_callback(read_label, write_label, update_label): - assert read_label.shape == () + assert read_label == "READONLY" assert write_label.shape == () assert update_label.shape == () - assert read_label.dtype.itemsize == 8 assert write_label.dtype.itemsize == 8 assert update_label.dtype.itemsize == 8 - assert read_label[()] == b"READONLY" assert update_label[()] == b"OLD " write_label[...] = b"WRITTEN!" update_label[...] = b"UPDATED!" diff --git a/tests/fortran/callbacks/pipeline/test_callback_contract_printing.py b/tests/fortran/callbacks/pipeline/test_callback_contract_printing.py index 06ddbbdd3..302e2e295 100644 --- a/tests/fortran/callbacks/pipeline/test_callback_contract_printing.py +++ b/tests/fortran/callbacks/pipeline/test_callback_contract_printing.py @@ -60,6 +60,7 @@ def test_callback_contract_prints_descriptor_and_derived_value_transports(): contract = emit_module(module) + assert "@prototype\ndef callback(" in contract assert "values: Annotated[Addr(Float64[:]), FortranAllocatable]" in contract assert "poly: Annotated[item, Polymorphic]" in contract assert "value: Value(item)" in contract diff --git a/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py b/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py index bafa39dd3..f313f29f7 100644 --- a/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py +++ b/tests/fortran/callbacks/semantics/test_fortran_callback_semantics.py @@ -119,10 +119,10 @@ def test_dummy_procedure_interfaces_become_complete_callable_contracts(): assert "@prototype\ndef transform_iface(" in emitted assert "callback: transform_iface" in emitted assert "@prototype\ndef value_iface(" in emitted - assert "value: Int32" in emitted + assert "value: In(Int32)" in emitted assert "ref: Addr(Float64)" in emitted assert "@prototype\ndef string_iface(" in emitted - assert "read_label: String[8]" in emitted + assert "read_label: In(String[8])" in emitted assert native_contract_issues(parse_pyi_text(emitted, module_name=module.name)) == [] project = parse_fortran_project( diff --git a/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py b/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py index 44f007dfb..38b95e435 100644 --- a/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py +++ b/tests/fortran/callbacks/semantics/test_pyi_callback_semantics.py @@ -105,9 +105,46 @@ def apply_transform( assert callback_type.metadata["prototype_ref"]["name"] == "transform_callback" +def test_prototype_is_one_exact_nonexported_signature_declaration(): + module = parse_pyi_text( + """ +@pure +@prototype +def extent_for(n: In(Addr(Int32))) -> Int32: ... + +def values(n: Int32) -> Float64[extent_for(n)]: ... +""", + module_name="prototype_signature", + ) + + prototype = module.prototypes[0] + assert prototype.pure is True + assert prototype.origin.native_scope == "prototype_signature" + assert prototype.arguments[0].origin.metadata == {"value": False, "prototype_intent": "in"} + assert [function.name for function in module.functions] == ["values"] + + +@pytest.mark.parametrize( + ("decorators", "message"), + [ + ("@pure", "pure requires prototype"), + ("@standalone\n@prototype", "prototype cannot be combined with standalone"), + ], +) +def test_exact_interface_decorators_reject_ambiguous_combinations(decorators: str, message: str): + with pytest.raises(ValueError, match=message): + parse_pyi_text( + f""" +{decorators} +def declared(value: Int32) -> Int32: ... +""", + module_name="invalid_prototype_decorators", + ) + + def test_imported_prototype_resolves_as_module_interface_definition(tmp_path): from prik.pipeline.pyi import pyi_paths_to_semantic_modules - from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner + from prik.codegen import WrapperCodeGenerator, WrapperPlanner (tmp_path / "callback_shapes.pyi").write_text( """from prik.contracts import Float64, Int32, prototype @@ -138,9 +175,10 @@ def apply(callback: transform, count: Int32, values: Float64[count]) -> None: .. complete_semantic_policies(api) artifacts = WrapperCodeGenerator().generate(WrapperPlanner().build(api)) bridge = next(source.text for source in artifacts.sources if source.path.suffix == ".f90") - assert "use callback_shapes, only:" in bridge - assert "_prototype => transform" in bridge - assert "procedure(prik_callback_adapter_callback_" in bridge + assert "abstract interface" in bridge + assert "function prik_transform_" in bridge + assert "procedure(prik_transform_" in bridge + assert "use callback_shapes, only:" not in bridge @pytest.mark.parametrize( diff --git a/tests/fortran/command_line_interface/pipeline/test_output_contract.py b/tests/fortran/command_line_interface/pipeline/test_output_contract.py index 91dc822e1..5967b5383 100644 --- a/tests/fortran/command_line_interface/pipeline/test_output_contract.py +++ b/tests/fortran/command_line_interface/pipeline/test_output_contract.py @@ -1,6 +1,7 @@ """Tests split by stable CLI output-contract ownership.""" from importlib import metadata +import shutil import prik @@ -29,9 +30,11 @@ def test_cli_and_python_api_report_installed_distribution_version(): expected = metadata.version("prik") + installed_script = shutil.which("prik") + assert installed_script is not None commands = ( [sys.executable, "-m", "prik", "--version"], - [str(Path(sys.executable).with_name("prik")), "--version"], + [installed_script, "--version"], ) assert prik.__version__ == expected diff --git a/tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py b/tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py index 2377bf5a1..ce4db7e94 100644 --- a/tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py +++ b/tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py @@ -3,6 +3,7 @@ from tests.fortran.command_line_interface.pipeline._support import ( FortranParseError, Path, + PreprocessingConfig, PreprocessingDiagnostic, PreprocessingError, TEST_FILE, @@ -19,6 +20,7 @@ types, prik_cli, ) +from prik.semantics.fortran2ir import collect_semantic_compile_time_requirements def test_cli_keeps_free_procedure_when_module_has_same_name(tmp_path: Path): @@ -235,6 +237,33 @@ def test_prik_semantics_marks_explicit_cross_file_derived_type_as_wrapped(tmp_pa assert "class particle" not in payload[str(physics)]["pyi"] +def test_single_file_cli_resolves_direct_intrinsic_kind_rename_before_probing(tmp_path: Path): + source = tmp_path / "direct_intrinsic_kind.f90" + source.write_text( + """ +module direct_intrinsic_kind + use iso_fortran_env, only: wp => real64 + real(wp), parameter :: scale = 2.0_wp +contains + real(wp) function twice(value) result(output) + real(wp), intent(in) :: value + output = scale*value + end function twice +end module direct_intrinsic_kind +""", + encoding="utf-8", + ) + + parsed_files = prik_cli._parse_fortran_source_files([source], PreprocessingConfig()) + parsed = parsed_files[0][1] + module = parsed.modules[0] + + assert module.variables[0].kind == "real64" + assert module.procedures[0].arguments[0].kind == "real64" + assert module.procedures[0].result.kind == "real64" + assert collect_semantic_compile_time_requirements(parsed) == [] + + def test_prik_pyi_report_writes_opaque_dependency_stub_for_external_type(tmp_path: Path, monkeypatch): physics = tmp_path / "physics.f90" physics.write_text( diff --git a/tests/fortran/conftest.py b/tests/fortran/conftest.py index 5f1606c39..8d9dc9960 100644 --- a/tests/fortran/conftest.py +++ b/tests/fortran/conftest.py @@ -82,22 +82,6 @@ def pyi_parity_build_mode(request: pytest.FixtureRequest) -> str: return request.param -def pytest_addoption(parser: pytest.Parser) -> None: - group = parser.getgroup("prik Fortran") - group.addoption( - COMPILER_OPTION, - action="store", - default=os.environ.get(COMPILER_ENV, "gfortran"), - metavar="EXECUTABLE", - help="Fortran compiler executable used by compiled Fortran tests.", - ) - group.addoption( - "--require-toolchain-smoke", - action="store_true", - help="Require a nonempty, skip-free selection containing only toolchain smoke nodes.", - ) - - def _compiler_was_requested_explicitly(config: pytest.Config) -> bool: if COMPILER_ENV in os.environ: return True diff --git a/tests/fortran/data_types/codegen/test_default_logical_scalar_lowering.py b/tests/fortran/data_types/codegen/test_default_logical_scalar_lowering.py new file mode 100644 index 000000000..064c1364c --- /dev/null +++ b/tests/fortran/data_types/codegen/test_default_logical_scalar_lowering.py @@ -0,0 +1,56 @@ +"""Default-logical scalar kind adaptation through completed wrapper plans.""" + +from prik import parse_fortran_file +from prik.semantics.fortran2ir import fortran_module_to_semantic_module +from prik.semantics.policy_completion import complete_semantic_policies +from prik.semantics.wrapper_policy import BridgeDataAction, ScalarLogicalABI +from prik.codegen import WrapperCodeGenerator, WrapperPlanner + + +SOURCE = """ +module logical_args +contains +subroutine use_flags(input, output) + logical, intent(in) :: input + logical, intent(out) :: output + output = .not. input +end subroutine use_flags +end module logical_args +""" + + +def _logical_function_plan(): + parsed_module = parse_fortran_file(SOURCE).modules[0] + semantic_module = fortran_module_to_semantic_module(parsed_module) + complete_semantic_policies(semantic_module) + module_plan = WrapperPlanner().build(semantic_module) + return module_plan, module_plan.namespaces[0].functions[0] + + +def test_policy_completes_default_logical_input_and_output_kind_copies(): + _module_plan, function = _logical_function_plan() + input_plan = function.arguments[0] + output_slot = function.results[0].native_call_slot + + assert input_plan.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY + assert input_plan.scalar_native_type == "logical" + assert input_plan.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION + assert output_slot.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY + assert output_slot.scalar_native_type == "logical" + assert output_slot.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION + + +def test_bridge_mechanically_lowers_completed_default_logical_kind_copies(): + module_plan, _function = _logical_function_plan() + + bridge_source = next( + source.text for source in WrapperCodeGenerator().generate(module_plan).sources if source.path.suffix == ".f90" + ) + + assert "logical(c_bool), value :: input" in bridge_source + assert "logical :: input_native" in bridge_source + assert "input_native = input" in bridge_source + assert "logical(c_bool) :: output" in bridge_source + assert "logical :: output_value" in bridge_source + assert "call native_use_flags(input_native, output_value)" in bridge_source + assert "output = output_value" in bridge_source diff --git a/tests/fortran/data_types/wrapper_codegen/test_primitive_scalar_input_lowering.py b/tests/fortran/data_types/codegen/test_primitive_scalar_input_lowering.py similarity index 96% rename from tests/fortran/data_types/wrapper_codegen/test_primitive_scalar_input_lowering.py rename to tests/fortran/data_types/codegen/test_primitive_scalar_input_lowering.py index 94ff79147..0531ec7df 100644 --- a/tests/fortran/data_types/wrapper_codegen/test_primitive_scalar_input_lowering.py +++ b/tests/fortran/data_types/codegen/test_primitive_scalar_input_lowering.py @@ -6,7 +6,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner @pytest.mark.parametrize( diff --git a/tests/fortran/data_types/wrapper_codegen/test_primitive_scalar_result_lowering.py b/tests/fortran/data_types/codegen/test_primitive_scalar_result_lowering.py similarity index 97% rename from tests/fortran/data_types/wrapper_codegen/test_primitive_scalar_result_lowering.py rename to tests/fortran/data_types/codegen/test_primitive_scalar_result_lowering.py index 803ad3780..b3c0a389b 100644 --- a/tests/fortran/data_types/wrapper_codegen/test_primitive_scalar_result_lowering.py +++ b/tests/fortran/data_types/codegen/test_primitive_scalar_result_lowering.py @@ -7,7 +7,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import DirectResultABI -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner @pytest.mark.parametrize( diff --git a/tests/fortran/data_types/wrapper_codegen/test_scalar_boundary_plan.py b/tests/fortran/data_types/codegen/test_scalar_boundary_plan.py similarity index 100% rename from tests/fortran/data_types/wrapper_codegen/test_scalar_boundary_plan.py rename to tests/fortran/data_types/codegen/test_scalar_boundary_plan.py diff --git a/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fbind_value_f90/fbind_value_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fbind_value_f90/fbind_value_f90.pyi index f13590f9e..9406a243c 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fbind_value_f90/fbind_value_f90.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fbind_value_f90/fbind_value_f90.pyi @@ -1,4 +1,4 @@ -from prik.contracts import Addr, Arg, Bool, Complex128, Float64, Int32, String, native_call +from prik.contracts import Addr, Arg, Bool8, Complex128, Float64, Int32, String, native_call def plus_value( n: Int32 @@ -22,8 +22,8 @@ def conjugate_value( ) -> Complex128: ... def invert_flag( - flag: Bool -) -> Bool: ... + flag: Bool8 +) -> Bool8: ... def char_code( ch: String[1] diff --git a/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fmath/__init__.pyi b/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fmath/__init__.pyi index 8df08ac05..d7a7d8642 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fmath/__init__.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fmath/__init__.pyi @@ -1,63 +1,63 @@ -from prik.contracts import Addr, Arg, Bool, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, external, native_call +from prik.contracts import Addr, Arg, Bool32, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call, standalone @bind("SQUARE_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def square_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("SQUARE_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def square_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("SQUARE_I4") -@external +@standalone @native_call([Addr(Arg(0))]) def square_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... @bind("SQUARE_C4") -@external +@standalone @native_call([Addr(Arg(0))]) def square_c4( Z: Complex64 ) -> tuple[Complex64, Returns["Z", Complex64]]: ... @bind("SQUARE_C8") -@external +@standalone @native_call([Addr(Arg(0))]) def square_c8( Z: Complex128 ) -> tuple[Complex128, Returns["Z", Complex128]]: ... @bind("CUBE_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def cube_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("CUBE_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def cube_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("CUBE_I4") -@external +@standalone @native_call([Addr(Arg(0))]) def cube_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... @bind("ADD_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_r4( X: Float32, @@ -65,7 +65,7 @@ def add_r4( ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... @bind("ADD_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_r8( X: Float64, @@ -73,7 +73,7 @@ def add_r8( ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... @bind("ADD_I4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_i4( X: Int32, @@ -81,7 +81,7 @@ def add_i4( ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... @bind("ADD_C4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_c4( X: Complex64, @@ -89,7 +89,7 @@ def add_c4( ) -> tuple[Complex64, Returns["X", Complex64], Returns["Y", Complex64]]: ... @bind("ADD_C8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def add_c8( X: Complex128, @@ -97,7 +97,7 @@ def add_c8( ) -> tuple[Complex128, Returns["X", Complex128], Returns["Y", Complex128]]: ... @bind("SUB_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_r4( X: Float32, @@ -105,7 +105,7 @@ def sub_r4( ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... @bind("SUB_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_r8( X: Float64, @@ -113,7 +113,7 @@ def sub_r8( ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... @bind("SUB_I4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sub_i4( X: Int32, @@ -121,7 +121,7 @@ def sub_i4( ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... @bind("MUL_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_r4( X: Float32, @@ -129,7 +129,7 @@ def mul_r4( ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... @bind("MUL_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_r8( X: Float64, @@ -137,7 +137,7 @@ def mul_r8( ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... @bind("MUL_I4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mul_i4( X: Int32, @@ -145,7 +145,7 @@ def mul_i4( ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... @bind("DIV_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def div_r4( X: Float32, @@ -153,7 +153,7 @@ def div_r4( ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... @bind("DIV_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def div_r8( X: Float64, @@ -161,7 +161,7 @@ def div_r8( ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... @bind("POW_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def pow_r4( X: Float32, @@ -169,7 +169,7 @@ def pow_r4( ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... @bind("POW_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def pow_r8( X: Float64, @@ -177,133 +177,133 @@ def pow_r8( ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... @bind("ABS_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def abs_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("ABS_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def abs_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("ABS_I4") -@external +@standalone @native_call([Addr(Arg(0))]) def abs_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... @bind("NEG_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def neg_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("NEG_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def neg_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("NEG_I4") -@external +@standalone @native_call([Addr(Arg(0))]) def neg_i4( X: Int32 ) -> tuple[Int32, Returns["X", Int32]]: ... @bind("SIN_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def sin_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("SIN_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def sin_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("COS_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def cos_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("COS_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def cos_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("TAN_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def tan_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("TAN_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def tan_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("ASIN_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def asin_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("ASIN_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def asin_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("ACOS_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def acos_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("ACOS_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def acos_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("ATAN_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def atan_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("ATAN_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def atan_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("ATAN2_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def atan2_r4( Y: Float32, @@ -311,7 +311,7 @@ def atan2_r4( ) -> tuple[Float32, Returns["Y", Float32], Returns["X", Float32]]: ... @bind("ATAN2_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def atan2_r8( Y: Float64, @@ -319,63 +319,63 @@ def atan2_r8( ) -> tuple[Float64, Returns["Y", Float64], Returns["X", Float64]]: ... @bind("EXP_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def exp_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("EXP_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def exp_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("LOG_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def log_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("LOG_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def log_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("LOG10_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def log10_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("LOG10_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def log10_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("SQRT_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def sqrt_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("SQRT_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def sqrt_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("HYPOT_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def hypot_r4( X: Float32, @@ -383,7 +383,7 @@ def hypot_r4( ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... @bind("HYPOT_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def hypot_r8( X: Float64, @@ -391,7 +391,7 @@ def hypot_r8( ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... @bind("MIN_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_r4( X: Float32, @@ -399,7 +399,7 @@ def min_r4( ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... @bind("MIN_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_r8( X: Float64, @@ -407,7 +407,7 @@ def min_r8( ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... @bind("MIN_I4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def min_i4( X: Int32, @@ -415,7 +415,7 @@ def min_i4( ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... @bind("MAX_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_r4( X: Float32, @@ -423,7 +423,7 @@ def max_r4( ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... @bind("MAX_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_r8( X: Float64, @@ -431,7 +431,7 @@ def max_r8( ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... @bind("MAX_I4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def max_i4( X: Int32, @@ -439,7 +439,7 @@ def max_i4( ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... @bind("SIGN_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sign_r4( X: Float32, @@ -447,7 +447,7 @@ def sign_r4( ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... @bind("SIGN_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def sign_r8( X: Float64, @@ -455,7 +455,7 @@ def sign_r8( ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... @bind("MOD_I4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_i4( X: Int32, @@ -463,7 +463,7 @@ def mod_i4( ) -> tuple[Int32, Returns["X", Int32], Returns["Y", Int32]]: ... @bind("MOD_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_r4( X: Float32, @@ -471,7 +471,7 @@ def mod_r4( ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... @bind("MOD_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def mod_r8( X: Float64, @@ -479,35 +479,35 @@ def mod_r8( ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... @bind("DEG2RAD_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def deg2rad_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("DEG2RAD_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def deg2rad_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("RAD2DEG_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def rad2deg_r4( X: Float32 ) -> tuple[Float32, Returns["X", Float32]]: ... @bind("RAD2DEG_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def rad2deg_r8( X: Float64 ) -> tuple[Float64, Returns["X", Float64]]: ... @bind("DIST2_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def dist2_r4( X: Float32, @@ -515,7 +515,7 @@ def dist2_r4( ) -> tuple[Float32, Returns["X", Float32], Returns["Y", Float32]]: ... @bind("DIST2_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def dist2_r8( X: Float64, @@ -523,7 +523,7 @@ def dist2_r8( ) -> tuple[Float64, Returns["X", Float64], Returns["Y", Float64]]: ... @bind("DOT2_R4") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3))]) def dot2_r4( X1: Float32, @@ -533,7 +533,7 @@ def dot2_r4( ) -> tuple[Float32, Returns["X1", Float32], Returns["X2", Float32], Returns["Y1", Float32], Returns["Y2", Float32]]: ... @bind("DOT2_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Addr(Arg(2)), Addr(Arg(3))]) def dot2_r8( X1: Float64, @@ -543,7 +543,7 @@ def dot2_r8( ) -> tuple[Float64, Returns["X1", Float64], Returns["X2", Float64], Returns["Y1", Float64], Returns["Y2", Float64]]: ... @bind("DOT3_R4") -@external +@standalone @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, @@ -555,7 +555,7 @@ def dot3_r4( ) -> tuple[Float32, Returns["X1", Float32], Returns["X2", Float32], Returns["X3", Float32], Returns["Y1", Float32], Returns["Y2", Float32], Returns["Y3", Float32]]: ... @bind("DOT3_R8") -@external +@standalone @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, @@ -567,78 +567,78 @@ def dot3_r8( ) -> tuple[Float64, Returns["X1", Float64], Returns["X2", Float64], Returns["X3", Float64], Returns["Y1", Float64], Returns["Y2", Float64], Returns["Y3", Float64]]: ... @bind("CONJ_C4") -@external +@standalone @native_call([Addr(Arg(0))]) def conj_c4( Z: Complex64 ) -> tuple[Complex64, Returns["Z", Complex64]]: ... @bind("CONJ_C8") -@external +@standalone @native_call([Addr(Arg(0))]) def conj_c8( Z: Complex128 ) -> tuple[Complex128, Returns["Z", Complex128]]: ... @bind("REAL_C4") -@external +@standalone @native_call([Addr(Arg(0))]) def real_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... @bind("REAL_C8") -@external +@standalone @native_call([Addr(Arg(0))]) def real_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... @bind("AIMAG_C4") -@external +@standalone @native_call([Addr(Arg(0))]) def aimag_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... @bind("AIMAG_C8") -@external +@standalone @native_call([Addr(Arg(0))]) def aimag_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... @bind("ABS_C4") -@external +@standalone @native_call([Addr(Arg(0))]) def abs_c4( Z: Complex64 ) -> tuple[Float32, Returns["Z", Complex64]]: ... @bind("ABS_C8") -@external +@standalone @native_call([Addr(Arg(0))]) def abs_c8( Z: Complex128 ) -> tuple[Float64, Returns["Z", Complex128]]: ... @bind("IS_POSITIVE_R4") -@external +@standalone @native_call([Addr(Arg(0))]) def is_positive_r4( X: Float32 -) -> tuple[Bool, Returns["X", Float32]]: ... +) -> tuple[Bool32, Returns["X", Float32]]: ... @bind("IS_POSITIVE_R8") -@external +@standalone @native_call([Addr(Arg(0))]) def is_positive_r8( X: Float64 -) -> tuple[Bool, Returns["X", Float64]]: ... +) -> tuple[Bool32, Returns["X", Float64]]: ... @bind("IS_EVEN_I4") -@external +@standalone @native_call([Addr(Arg(0))]) def is_even_i4( X: Int32 -) -> tuple[Bool, Returns["X", Int32]]: ... +) -> tuple[Bool32, Returns["X", Int32]]: ... diff --git a/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fmath_f90/fmath_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fmath_f90/fmath_f90.pyi index c8a7ea8c4..6b8daf07d 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fmath_f90/fmath_f90.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fmath_f90/fmath_f90.pyi @@ -1,4 +1,4 @@ -from prik.contracts import Addr, Arg, Bool, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call +from prik.contracts import Addr, Arg, Bool32, Complex128, Complex64, Float32, Float64, Int32, Returns, bind, native_call @bind("SQUARE_R4") @native_call([Addr(Arg(0))]) @@ -544,16 +544,16 @@ def abs_c8( @native_call([Addr(Arg(0))]) def is_positive_r4( X: Float32 -) -> tuple[Bool, Returns["X", Float32]]: ... +) -> tuple[Bool32, Returns["X", Float32]]: ... @bind("IS_POSITIVE_R8") @native_call([Addr(Arg(0))]) def is_positive_r8( X: Float64 -) -> tuple[Bool, Returns["X", Float64]]: ... +) -> tuple[Bool32, Returns["X", Float64]]: ... @bind("IS_EVEN_I4") @native_call([Addr(Arg(0))]) def is_even_i4( X: Int32 -) -> tuple[Bool, Returns["X", Int32]]: ... +) -> tuple[Bool32, Returns["X", Int32]]: ... diff --git a/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi index 56ec8abbc..957e79c19 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi @@ -1,4 +1,4 @@ -from prik.contracts import Addr, Arg, Bool, Complex128, Complex64, Float32, Float64, Int16, Int32, Int64, Int8, native_call +from prik.contracts import Addr, Arg, Bool8, Complex128, Complex64, Float32, Float64, Int16, Int32, Int64, Int8, native_call @native_call([Addr(Arg(0))]) def id_i8( @@ -33,14 +33,14 @@ def copy_i16( @native_call([Addr(Arg(0))]) def not_flag( - value: Bool -) -> Bool: ... + value: Bool8 +) -> Bool8: ... @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def invert_flags( n: Int32, - values: Bool[n], - out: Bool[n] + values: Bool8[n], + out: Bool8[n] ) -> None: ... @native_call([Addr(Arg(0))]) diff --git a/tests/fortran/data_types/parsing/test_declarations_and_shapes.py b/tests/fortran/data_types/parsing/test_declarations_and_shapes.py index b3be62e11..a9f24bafd 100644 --- a/tests/fortran/data_types/parsing/test_declarations_and_shapes.py +++ b/tests/fortran/data_types/parsing/test_declarations_and_shapes.py @@ -1,5 +1,9 @@ """Tests split by stable ownership concept from `test_procedures_and_interfaces.py`.""" +import subprocess +import sys +from pathlib import Path + from tests.fortran._support.parser_procedures import ( COMPILE_TIME_EXPRESSION_SOURCE, collect_project_procedure_signatures, @@ -300,6 +304,24 @@ def test_extract_kind_from_type_spec_contract(base_type, type_spec, expected): assert extract_kind_from_type_spec(base_type, type_spec) == expected +def test_type_resolver_module_direct_execution_example(): + repository_root = Path(__file__).parents[4] + + result = subprocess.run( + [sys.executable, "prik/parsers/fortran/type_resolver.py"], + cwd=repository_root, + capture_output=True, + text=True, + check=True, + ) + + assert result.stdout == ( + "integer(4) -> 4\n" + "real(kind=selected_real_kind(15, 307)) -> selected_real_kind(15, 307)\n" + "character(len=16, kind=c_char) -> len=16, kind=c_char\n" + ) + + def test_compiler_dependent_parameter_expressions_remain_symbolic_with_value_at_module_level(): files = { "kinds.f90": """ diff --git a/tests/fortran/data_types/probes/test_fortran_type_probes.py b/tests/fortran/data_types/probes/test_fortran_type_probes.py index 7ee21cfa4..a3eb1254d 100644 --- a/tests/fortran/data_types/probes/test_fortran_type_probes.py +++ b/tests/fortran/data_types/probes/test_fortran_type_probes.py @@ -4,6 +4,7 @@ import shutil import subprocess import sys +from pathlib import Path from types import SimpleNamespace import pytest @@ -14,6 +15,7 @@ fortran_module_to_semantic_module, ) from prik import parse_fortran_file as parse_fortran_source +from prik import parse_fortran_project from prik.probes.fortran_types import ( FortranTypeProbeRecipe, FortranTypeProbeReport, @@ -27,6 +29,7 @@ load_fortran_type_probe_report, probe_fortran_type_expressions, probe_fortran_type_expressions_cached, + resolve_fortran_logical_storage_types, ) from prik.pipeline.preprocessing import PreprocessingConfig @@ -350,6 +353,20 @@ def test_fortran_type_probe_reports_values_from_native_compiler(): assert "selected_real_kind(12)" in report.source_text +def test_fortran_type_probe_resolves_supported_logical_storage_widths(tmp_path): + compiler = _required_fortran_compiler() + + resolved = resolve_fortran_logical_storage_types( + PreprocessingConfig(mode="compiler", compiler=compiler), + [8, 16, 32, 64], + cache_dir=tmp_path, + ) + + assert resolved[8] == "logical(kind=c_bool)" + assert set(resolved) == {8, 16, 32, 64} + assert all(spelling.startswith("logical(kind=") for spelling in resolved.values()) + + def test_fortran_type_probe_carries_target_relevant_user_flags(tmp_path): compiler = _required_fortran_compiler() include_dir = tmp_path / "include" @@ -435,6 +452,34 @@ def test_fortran_type_probe_evaluates_collected_semantic_requirements(): assert module.functions[0].arguments[0].semantic_type.name == "Float64" +def test_collected_probe_requirements_resolve_submodule_host_kind(): + project = parse_fortran_project( + { + "implementation.f90": """ +submodule(transform_api) transform_impl +contains + module function twice(value) result(output) + real(rk), intent(in) :: value + real(rk) :: output + end function twice +end submodule transform_impl +""", + "parent.f90": """ +module transform_api + use precision +end module transform_api +""", + "kind.f90": """ +module precision + use, intrinsic :: iso_fortran_env, only: rk => real64 +end module precision +""", + } + ) + + assert collect_semantic_compile_time_requirements(project) == [] + + def test_fortran_type_probe_module_cli_emits_json_for_semantic_input(tmp_path): compiler = _required_fortran_compiler() completed = subprocess.run( @@ -464,6 +509,21 @@ def test_fortran_type_probe_module_cli_emits_json_for_semantic_input(tmp_path): assert payload["source_text"].startswith("program prik_fortran_type_probe") +def test_fortran_type_probe_direct_script_runs_its_no_argument_example(): + completed = subprocess.run( + [sys.executable, "prik/probes/fortran_types.py"], + cwd=Path(__file__).resolve().parents[4], + capture_output=True, + text=True, + check=True, + ) + + label, separator, raw_value = completed.stdout.strip().partition(" = ") + assert label == "selected_int_kind(9)" + assert separator == " = " + assert int(raw_value) > 0 + + def test_prik_semantics_cli_evaluates_collected_fortran_type_requirements(tmp_path): compiler = _required_fortran_compiler() source = tmp_path / "solver.f90" diff --git a/tests/fortran/data_types/runtime/test_contract_scalar_constructors.py b/tests/fortran/data_types/runtime/test_contract_scalar_constructors.py index bfbcace7d..97853af81 100644 --- a/tests/fortran/data_types/runtime/test_contract_scalar_constructors.py +++ b/tests/fortran/data_types/runtime/test_contract_scalar_constructors.py @@ -9,6 +9,10 @@ def test_concrete_primitive_default_constructors_return_zero_numpy_scalars(): cases = ( (contracts.Bool, np.bool_), + (contracts.Bool8, np.bool_), + (contracts.Bool16, np.bool_), + (contracts.Bool32, np.bool_), + (contracts.Bool64, np.bool_), (contracts.Int8, np.int8), (contracts.Int16, np.int16), (contracts.Int32, np.int32), 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 f35a53f88..12cc02d6e 100644 --- a/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py +++ b/tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py @@ -105,10 +105,30 @@ def test_fortran2ir_uses_compiler_probed_storage_facts_and_preserves_provenance( FortranVariable(name="flag", base_type="logical", kind="1") ) - assert logical_type.name == "Bool" + assert logical_type.name == "Bool8" assert logical_type.metadata["fortran_type_fact"] == logical_fact +@pytest.mark.parametrize( + ("bits", "expected"), + [(8, "Bool8"), (16, "Bool16"), (32, "Bool32"), (64, "Bool64")], +) +def test_fortran2ir_maps_probed_logical_storage_to_language_neutral_boolean_widths(bits, expected): + fact = { + "base_type": "logical", + "kind": str(bits // 8), + "bits": bits, + "expression": f"storage_size(logical(.false.,kind={bits // 8}))", + } + + semantic_type = FortranToIRConverter(type_facts={("logical", str(bits // 8)): fact}).visit( + FortranVariable(name="flag", base_type="logical", kind=str(bits // 8)) + ) + + assert semantic_type.name == expected + assert semantic_type.dtype == expected + + def test_fortran2ir_rejects_compiler_storage_without_semantic_dtype(): fact = { "base_type": "integer", diff --git a/tests/fortran/derived_types/wrapper_codegen/test_class_surfaces.py b/tests/fortran/derived_types/codegen/test_class_surfaces.py similarity index 96% rename from tests/fortran/derived_types/wrapper_codegen/test_class_surfaces.py rename to tests/fortran/derived_types/codegen/test_class_surfaces.py index dec8dad38..6c430e2ef 100644 --- a/tests/fortran/derived_types/wrapper_codegen/test_class_surfaces.py +++ b/tests/fortran/derived_types/codegen/test_class_surfaces.py @@ -6,7 +6,7 @@ from prik.pipeline.pyi import pyi_file_to_semantic_module from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner FIXTURES = Path(__file__).parents[1] / "end_to_end" / "fixtures" INHERITANCE = FIXTURES / "contracts" / "finheritance_f90" / "finheritance_f90.pyi" diff --git a/tests/fortran/derived_types/wrapper_codegen/test_derived_lowering.py b/tests/fortran/derived_types/codegen/test_derived_lowering.py similarity index 99% rename from tests/fortran/derived_types/wrapper_codegen/test_derived_lowering.py rename to tests/fortran/derived_types/codegen/test_derived_lowering.py index 72fefb46e..f707829da 100644 --- a/tests/fortran/derived_types/wrapper_codegen/test_derived_lowering.py +++ b/tests/fortran/derived_types/codegen/test_derived_lowering.py @@ -17,7 +17,7 @@ DerivedRelease, LifecycleOperation, ) -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _value_module(*, attributes: tuple[str, ...] = ()): diff --git a/tests/fortran/derived_types/wrapper_codegen/test_derived_plan_completion.py b/tests/fortran/derived_types/codegen/test_derived_plan_completion.py similarity index 100% rename from tests/fortran/derived_types/wrapper_codegen/test_derived_plan_completion.py rename to tests/fortran/derived_types/codegen/test_derived_plan_completion.py diff --git a/tests/fortran/derived_types/wrapper_codegen/test_scalar_actual_dummy_plan.py b/tests/fortran/derived_types/codegen/test_scalar_actual_dummy_plan.py similarity index 99% rename from tests/fortran/derived_types/wrapper_codegen/test_scalar_actual_dummy_plan.py rename to tests/fortran/derived_types/codegen/test_scalar_actual_dummy_plan.py index d8973dd42..85508fa9e 100644 --- a/tests/fortran/derived_types/wrapper_codegen/test_scalar_actual_dummy_plan.py +++ b/tests/fortran/derived_types/codegen/test_scalar_actual_dummy_plan.py @@ -16,7 +16,7 @@ DerivedOwnerRetention, DerivedRelease, ) -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner CONTRACT = """ diff --git a/tests/fortran/enumerations/semantics/test_enum_semantics.py b/tests/fortran/enumerations/semantics/test_enum_semantics.py index d75607408..97fba37b0 100644 --- a/tests/fortran/enumerations/semantics/test_enum_semantics.py +++ b/tests/fortran/enumerations/semantics/test_enum_semantics.py @@ -4,7 +4,7 @@ from prik import parse_fortran_file as parse_fortran_source -from prik.wrapper_codegen.printers import emit_module +from prik.codegen.printers import emit_module from prik.semantics.fortran2ir import fortran_module_to_semantic_module ENUM_SOURCE = Path(__file__).parents[1] / "end_to_end" / "fixtures" / "fenums_f90.f90" diff --git a/tests/fortran/error_handling/wrapper_codegen/test_runtime_envelope_lowering.py b/tests/fortran/error_handling/codegen/test_runtime_envelope_lowering.py similarity index 94% rename from tests/fortran/error_handling/wrapper_codegen/test_runtime_envelope_lowering.py rename to tests/fortran/error_handling/codegen/test_runtime_envelope_lowering.py index 6de3bc4f1..a9b87fea5 100644 --- a/tests/fortran/error_handling/wrapper_codegen/test_runtime_envelope_lowering.py +++ b/tests/fortran/error_handling/codegen/test_runtime_envelope_lowering.py @@ -6,7 +6,7 @@ from prik.pipeline.pyi import pyi_file_to_semantic_module from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner RECURSION_CONTRACT = ( diff --git a/tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py b/tests/fortran/error_handling/codegen/test_status_error_lowering.py similarity index 98% rename from tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py rename to tests/fortran/error_handling/codegen/test_status_error_lowering.py index 5ef535fa8..15aa2deba 100644 --- a/tests/fortran/error_handling/wrapper_codegen/test_status_error_lowering.py +++ b/tests/fortran/error_handling/codegen/test_status_error_lowering.py @@ -10,7 +10,7 @@ from prik.pipeline.pyi import pyi_file_to_semantic_module from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import BridgeDataAction, PythonExceptionKind -from prik.wrapper_codegen import DatatypeFamily, WrapperCodeGenerator, WrapperPlanner +from prik.codegen import DatatypeFamily, WrapperCodeGenerator, WrapperPlanner RUNTIME_POLICY_CONTRACT = ( diff --git a/tests/fortran/error_handling/semantics/test_status_contract_semantics.py b/tests/fortran/error_handling/semantics/test_status_contract_semantics.py index 88c9bf810..4a257804c 100644 --- a/tests/fortran/error_handling/semantics/test_status_contract_semantics.py +++ b/tests/fortran/error_handling/semantics/test_status_contract_semantics.py @@ -5,7 +5,7 @@ from prik.pipeline.pyi import pyi_text_to_semantic_module from prik.semantics.models import RUNTIME_RELEASE_GIL_METADATA, RUNTIME_STATUS_ERROR_METADATA from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen.printers import emit_module +from prik.codegen.printers import emit_module _CONTRACT_IMPORTS = """from prik.contracts import ( diff --git a/tests/fortran/functions/wrapper_codegen/test_multiple_function_results.py b/tests/fortran/functions/codegen/test_multiple_function_results.py similarity index 98% rename from tests/fortran/functions/wrapper_codegen/test_multiple_function_results.py rename to tests/fortran/functions/codegen/test_multiple_function_results.py index 3834cf3b8..643f1cfc6 100644 --- a/tests/fortran/functions/wrapper_codegen/test_multiple_function_results.py +++ b/tests/fortran/functions/codegen/test_multiple_function_results.py @@ -9,7 +9,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.ownership import CodegenAction, NativeBarrierAction from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _multiple_result_plan(): diff --git a/tests/fortran/functions/wrapper_codegen/test_scalar_function_writeback.py b/tests/fortran/functions/codegen/test_scalar_function_writeback.py similarity index 96% rename from tests/fortran/functions/wrapper_codegen/test_scalar_function_writeback.py rename to tests/fortran/functions/codegen/test_scalar_function_writeback.py index 69fd56f33..0e8fe1f6e 100644 --- a/tests/fortran/functions/wrapper_codegen/test_scalar_function_writeback.py +++ b/tests/fortran/functions/codegen/test_scalar_function_writeback.py @@ -6,7 +6,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import WritebackPhase -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _artifacts(module): diff --git a/tests/fortran/functions/end_to_end/fixtures/external/contracts/blas_like/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/external/contracts/blas_like/__init__.pyi index 816e09895..6f3ea33a8 100644 --- a/tests/fortran/functions/end_to_end/fixtures/external/contracts/blas_like/__init__.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/external/contracts/blas_like/__init__.pyi @@ -1,6 +1,6 @@ -from prik.contracts import Addr, Arg, Float64, Int32, external, native_call +from prik.contracts import Addr, Arg, Float64, Int32, native_call, standalone -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Arg(2), Arg(3)]) def daxpy_like( n: Int32, @@ -9,7 +9,7 @@ def daxpy_like( y: Float64[n] ) -> None: ... -@external +@standalone @native_call([Addr(Arg(0)), Arg(1), Arg(2)]) def ddot_like( n: Int32, diff --git a/tests/fortran/functions/end_to_end/fixtures/external/contracts/external_bundle/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/external/contracts/external_bundle/__init__.pyi index 1494e0b0e..e9a67a5d4 100644 --- a/tests/fortran/functions/end_to_end/fixtures/external/contracts/external_bundle/__init__.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/external/contracts/external_bundle/__init__.pyi @@ -1,12 +1,12 @@ -from prik.contracts import Addr, Arg, Int32, external, native_call +from prik.contracts import Addr, Arg, Int32, native_call, standalone -@external +@standalone @native_call([Addr(Arg(0))]) def triple_value( value: Int32 ) -> Int32: ... -@external +@standalone @native_call([Addr(Arg(0))]) def offset_value( value: Int32 diff --git a/tests/fortran/functions/end_to_end/fixtures/external/contracts/fixed_external/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/external/contracts/fixed_external/__init__.pyi index 80cd63530..ad7eef082 100644 --- a/tests/fortran/functions/end_to_end/fixtures/external/contracts/fixed_external/__init__.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/external/contracts/fixed_external/__init__.pyi @@ -1,6 +1,6 @@ -from prik.contracts import Addr, Arg, Int32, Returns, external, native_call +from prik.contracts import Addr, Arg, Int32, Returns, native_call, standalone -@external +@standalone @native_call([Addr(Arg(0))]) def fixed_add( value: Int32 diff --git a/tests/fortran/functions/end_to_end/fixtures/external/contracts/free_external/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/external/contracts/free_external/__init__.pyi index d74b2eb0f..3468b30b6 100644 --- a/tests/fortran/functions/end_to_end/fixtures/external/contracts/free_external/__init__.pyi +++ b/tests/fortran/functions/end_to_end/fixtures/external/contracts/free_external/__init__.pyi @@ -1,6 +1,6 @@ -from prik.contracts import Addr, Arg, Int32, external, native_call +from prik.contracts import Addr, Arg, Int32, native_call, standalone -@external +@standalone @native_call([Addr(Arg(0))]) def free_square( value: Int32 diff --git a/tests/fortran/functions/end_to_end/test_external_procedures.py b/tests/fortran/functions/end_to_end/test_external_procedures.py index 62bcb717e..93d95abac 100644 --- a/tests/fortran/functions/end_to_end/test_external_procedures.py +++ b/tests/fortran/functions/end_to_end/test_external_procedures.py @@ -180,20 +180,20 @@ def bundled_external_module(pyi_parity_build_mode: str, tmp_path: Path): return module -def test_fixed_form_standalone_external_runtime_parity(fixed_external_module): +def test_fixed_form_standalone_procedure_runtime_parity(fixed_external_module): assert fixed_external_module.fixed_add(np.int32(4)) == (np.int32(5), np.int32(4)) -def test_free_form_standalone_external_runtime_parity(free_external_module): +def test_free_form_standalone_procedure_runtime_parity(free_external_module): assert free_external_module.free_square(np.int32(5)) == np.int32(25) -def test_one_source_with_several_standalone_externals_exports_each_at_root(bundled_external_module): +def test_one_source_with_several_standalone_procedures_exports_each_at_root(bundled_external_module): assert bundled_external_module.triple_value(np.int32(4)) == np.int32(12) assert bundled_external_module.offset_value(np.int32(4)) == np.int32(14) -def test_generated_external_contracts_are_non_empty_root_fragments(tmp_path: Path): +def test_generated_standalone_contracts_are_non_empty_root_fragments(tmp_path: Path): for source in (FIXED_EXTERNAL, FREE_EXTERNAL, EXTERNAL_BUNDLE): copied = _copy_sources((source,), tmp_path / source.stem) entry = _generate_contract( @@ -205,7 +205,7 @@ def test_generated_external_contracts_are_non_empty_root_fragments(tmp_path: Pat assert entry.name == "__init__.pyi" assert text.strip() - assert text.count("@external") == len([line for line in text.splitlines() if line.startswith("def ")]) + assert text.count("@standalone") == len([line for line in text.splitlines() if line.startswith("def ")]) assert sorted(path.name for path in entry.parent.glob("*.pyi")) == ["__init__.pyi"] @@ -221,7 +221,7 @@ def test_classic_external_bridge_uses_implicit_declaration_and_no_module_use(tmp bridge = (result.output_dir / f"bind_c_{result.module_name}_wrapper.f90").read_text(encoding="utf-8").lower() assert module.free_square(np.int32(3)) == np.int32(9) assert entry.read_text(encoding="utf-8").startswith( - "from prik.contracts import Addr, Arg, Int32, external, native_call\n\n@external\n" + "from prik.contracts import Addr, Arg, Int32, native_call, standalone\n\n@standalone\n" ) assert "integer(c_int32_t), external :: free_square" in bridge assert "function free_square(" not in bridge @@ -314,9 +314,9 @@ def test_handwritten_fortran_order_flat_contract_flattens_the_final_python_axes( ) contract = tmp_path / "column_sums_f.pyi" contract.write_text( - """from prik.contracts import Addr, Arg, Flat, Float64, Int32, Return, external, native_call + """from prik.contracts import Addr, Arg, Flat, Float64, Int32, Return, native_call, standalone -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1)), Arg(2), Arg(3)]) def column_sums_f( rows: Int32, @@ -325,14 +325,14 @@ def column_sums_f( result: Float64[Flat], ) -> None: ... -@external +@standalone def bump_storage(value: Int32[()]) -> None: ... -@external +@standalone @native_call([Return("value", 0)]) def make_storage() -> Int32[()]: ... -@external +@standalone def storage_value() -> Int32[()]: ... """, encoding="utf-8", @@ -433,7 +433,7 @@ def maybe_scale_c( assert module.maybe_scale_c(np.int32(6), np.int32(4)) is None -def test_compact_blas_like_folder_generates_one_external_entry_and_preserves_separate_objects(tmp_path: Path): +def test_compact_blas_like_folder_generates_one_standalone_entry_and_preserves_separate_objects(tmp_path: Path): copied_sources = _copy_sources(BLAS_LIKE_SOURCES, tmp_path / "sources") source_module, source_result = _build_source(copied_sources, tmp_path / "source_build") @@ -453,8 +453,8 @@ def test_compact_blas_like_folder_generates_one_external_entry_and_preserves_sep assert sorted(path.relative_to(entry.parent).as_posix() for path in entry.parent.rglob("*.pyi")) == ["__init__.pyi"] text = entry.read_text(encoding="utf-8") - assert "@external\n@native_call([Addr(Arg(0)), Addr(Arg(1)), Arg(2), Arg(3)])\ndef daxpy_like(" in text - assert "@external\n@native_call([Addr(Arg(0)), Arg(1), Arg(2)])\ndef ddot_like(" in text + assert "@standalone\n@native_call([Addr(Arg(0)), Addr(Arg(1)), Arg(2), Arg(3)])\ndef daxpy_like(" in text + assert "@standalone\n@native_call([Addr(Arg(0)), Arg(1), Arg(2)])\ndef ddot_like(" in text assert generated_result.native_build_plan.to_dict()["link_items"] == [ {"kind": "object", "path": str(tmp_path / "native" / "daxpy_like.o")}, {"kind": "object", "path": str(tmp_path / "native" / "ddot_like.o")}, @@ -466,10 +466,10 @@ def test_compact_blas_like_folder_generates_one_external_entry_and_preserves_sep assert source_dot == generated_module.ddot_like(np.int32(x.size), x, y_generated) -def test_package_entry_rejects_non_external_root_declaration_before_codegen(tmp_path: Path): +def test_package_entry_rejects_non_standalone_root_declaration_before_codegen(tmp_path: Path): source = _copy_sources((FREE_EXTERNAL,), tmp_path / "sources") entry = _generate_contract(source, tmp_path / "contracts", _generated_contract_fixture(FREE_EXTERNAL.stem)) - entry.write_text(entry.read_text(encoding="utf-8").replace("@external\n", ""), encoding="utf-8") + entry.write_text(entry.read_text(encoding="utf-8").replace("@standalone\n", ""), encoding="utf-8") native_objects = _compile_native_objects(source, tmp_path / "native") build_dir = tmp_path / "pyi_build" @@ -479,20 +479,20 @@ def test_package_entry_rejects_non_external_root_declaration_before_codegen(tmp_ assert not build_dir.exists() -def test_namespace_imported_module_rejects_external_marker_before_codegen(tmp_path: Path): +def test_namespace_imported_module_rejects_standalone_marker_before_codegen(tmp_path: Path): source = _copy_sources((BASIC_SOURCE,), tmp_path / "sources") entry = _generate_contract(source, tmp_path / "contracts", _generated_contract_fixture(BASIC_SOURCE.stem)) leaf = entry.parent / "m1.pyi" leaf_text = leaf.read_text(encoding="utf-8").replace( "from prik.contracts import ", - "from prik.contracts import external, ", + "from prik.contracts import standalone, ", 1, ) - leaf.write_text(leaf_text.replace("def add1", "@external\ndef add1"), encoding="utf-8") + leaf.write_text(leaf_text.replace("def add1", "@standalone\ndef add1"), encoding="utf-8") native_objects = _compile_native_objects(source, tmp_path / "native") build_dir = tmp_path / "pyi_build" - with pytest.raises(ValueError, match="cannot contain @external declarations"): + with pytest.raises(ValueError, match="cannot contain @standalone declarations"): build_pyi_extension( entry, native_objects=native_objects, diff --git a/tests/fortran/generic_interfaces/wrapper_codegen/test_overload_dispatch_plan.py b/tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py similarity index 97% rename from tests/fortran/generic_interfaces/wrapper_codegen/test_overload_dispatch_plan.py rename to tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py index 36f31fe06..574ed3ee0 100644 --- a/tests/fortran/generic_interfaces/wrapper_codegen/test_overload_dispatch_plan.py +++ b/tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py @@ -7,7 +7,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import OverloadMatchKind -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _plan(): diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/foperators_f90.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/foperators_f90.pyi index 968825d54..24c5606e0 100644 --- a/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/foperators_f90.pyi +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/contracts/foperators_f90/foperators_f90.pyi @@ -1,4 +1,4 @@ -from prik.contracts import Addr, Annotated, Arg, Bool, Float64, Int32, Pass, Polymorphic, Returns, bind, native_call, overload, private +from prik.contracts import Addr, Annotated, Arg, Bool32, Float64, Int32, Pass, Polymorphic, Returns, bind, native_call, overload, private class vector: def __init__( @@ -85,76 +85,76 @@ class vector: def __eq__( self, right: vector - ) -> Bool: ... + ) -> Bool32: ... @overload("equivalent_vector_offset", generic="operator(.eqv.)") def __eq__( self, right: offset - ) -> Bool: ... + ) -> Bool32: ... @overload("not_equal_vectors") def __ne__( self, right: vector - ) -> Bool: ... + ) -> Bool32: ... @overload("not_equivalent_vector_integer", generic="operator(.neqv.)") def __ne__( self, right: Int32 - ) -> Bool: ... + ) -> Bool32: ... @overload("less_vectors") def __lt__( self, right: vector - ) -> Bool: ... + ) -> Bool32: ... @overload("less_vector_real") def __lt__( self, right: Float64 - ) -> Bool: ... + ) -> Bool32: ... @overload("less_real_vector") def __gt__( self, left: Float64 - ) -> Bool: ... + ) -> Bool32: ... @overload("greater_vectors") def __gt__( self, right: vector - ) -> Bool: ... + ) -> Bool32: ... @overload("less_equal_vectors") def __le__( self, right: vector - ) -> Bool: ... + ) -> Bool32: ... @overload("greater_equal_vectors") def __ge__( self, right: vector - ) -> Bool: ... + ) -> Bool32: ... @overload("and_vectors") def __and__( self, right: vector - ) -> Bool: ... + ) -> Bool32: ... @overload("or_vectors") def __or__( self, right: vector - ) -> Bool: ... + ) -> Bool32: ... @overload("not_vector") - def __invert__(self) -> Bool: ... + def __invert__(self) -> Bool32: ... @overload("dot_vectors") def operator_dot( @@ -199,7 +199,7 @@ class offset: def __eq__( self, left: vector - ) -> Bool: ... + ) -> Bool32: ... class counter: def __init__( @@ -324,81 +324,81 @@ def power_vector_integer( def equal_vectors( left: vector, right: vector -) -> Bool: ... +) -> Bool32: ... @private def not_equal_vectors( left: vector, right: vector -) -> Bool: ... +) -> Bool32: ... @private def less_vectors( left: vector, right: vector -) -> Bool: ... +) -> Bool32: ... @private @native_call([Arg(0), Addr(Arg(1))]) def less_vector_real( left: vector, right: Float64 -) -> Bool: ... +) -> Bool32: ... @private @native_call([Addr(Arg(0)), Arg(1)]) def less_real_vector( left: Float64, right: vector -) -> Bool: ... +) -> Bool32: ... @private def less_equal_vectors( left: vector, right: vector -) -> Bool: ... +) -> Bool32: ... @private def greater_vectors( left: vector, right: vector -) -> Bool: ... +) -> Bool32: ... @private def greater_equal_vectors( left: vector, right: vector -) -> Bool: ... +) -> Bool32: ... @private def and_vectors( left: vector, right: vector -) -> Bool: ... +) -> Bool32: ... @private def or_vectors( left: vector, right: vector -) -> Bool: ... +) -> Bool32: ... @private def not_vector( value: vector -) -> Bool: ... +) -> Bool32: ... @private def equivalent_vector_offset( left: vector, right: offset -) -> Bool: ... +) -> Bool32: ... @private @native_call([Arg(0), Addr(Arg(1))]) def not_equivalent_vector_integer( left: vector, right: Int32 -) -> Bool: ... +) -> Bool32: ... @private def dot_vectors( 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 5e9078a20..29c44aca5 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 @@ -7,6 +7,7 @@ from tests.fortran._support.wrapper_build import ( _build_source_or_generated_pyi_and_import, + _build_sources_and_import, ) FIXTURES = Path(__file__).parent / "fixtures" @@ -14,6 +15,38 @@ CONTRACT_FIXTURES = FIXTURES / "contracts" pytestmark = pytest.mark.fortran_end_to_end +PRIVATE_INLINE_GENERIC_MODULE = """\ +module private_inline_generic + implicit none + private + public :: shift + + interface shift + module function shift_integer(value) result(output) + integer, intent(in) :: value + integer :: output + end function shift_integer + module function shift_real(value) result(output) + real(8), intent(in) :: value + real(8) :: output + end function shift_real + end interface shift +end module private_inline_generic +""" + +PRIVATE_INLINE_GENERIC_SUBMODULE = """\ +submodule(private_inline_generic) private_inline_generic_impl +contains + module procedure shift_integer + output = value + 1 + end procedure shift_integer + + module procedure shift_real + output = value + 0.5_8 + end procedure shift_real +end submodule private_inline_generic_impl +""" + @pytest.fixture def compiled_generic_module( @@ -69,3 +102,21 @@ def test_fortran_generic_interfaces_dispatch_in_generated_c_extension( module.convert("not numeric") with pytest.raises(TypeError): value.add(np.complex128(1.0 + 0.0j)) + + +def test_public_generic_dispatches_to_private_inline_submodule_specifics(tmp_path: Path): + module, _payload = _build_sources_and_import( + [ + ("private_inline_generic.f90", PRIVATE_INLINE_GENERIC_MODULE), + ("private_inline_generic_impl.f90", PRIVATE_INLINE_GENERIC_SUBMODULE), + ], + tmp_path, + ) + + assert module.private_inline_generic.shift(np.int32(4)) == np.int32(5) + assert module.private_inline_generic.shift(np.float64(4.0)) == np.float64(4.5) + bridge = (tmp_path / "bind_c_private_inline_generic_wrapper.f90").read_text(encoding="utf-8").lower() + assert "native__prik_overload_shift_0 => shift" in bridge + assert "native__prik_overload_shift_1 => shift" in bridge + assert "=> shift_integer" not in bridge + assert "=> shift_real" not in bridge 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 365829b7d..13b0f0635 100644 --- a/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py +++ b/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py @@ -8,6 +8,7 @@ parse_fortran_source, pytest, ) +from prik.semantics.metadata import BIND_TARGET_METADATA OPERATOR_F90_SOURCE = Path(__file__).parents[1] / "end_to_end" / "fixtures" / "foperators_f90.f90" @@ -58,6 +59,33 @@ def test_converter_preserves_module_and_type_bound_generic_overload_sets(): assert all(proc.visibility == "public" for proc in box.overload_sets[0].procedures) +def test_public_generic_binds_private_inline_module_function_specifics_to_the_generic_name(): + source = """ +module generic_mod + implicit none + private + public :: shift + interface shift + module function shift_integer(value) result(output) + integer, intent(in) :: value + integer :: output + end function shift_integer + module function shift_real(value) result(output) + real, intent(in) :: value + real :: output + end function shift_real + end interface shift +end module generic_mod +""" + + module = FortranToIRConverter().visit(parse_fortran_source(source).modules[0]) + candidates = module.overload_sets[0].procedures + + assert [candidate.name for candidate in candidates] == ["shift_integer", "shift_real"] + assert [candidate.native_name for candidate in candidates] == ["shift", "shift"] + assert [candidate.metadata[BIND_TARGET_METADATA] for candidate in candidates] == ["shift", "shift"] + + def test_converter_rejects_generic_constructor_interfaces_during_semantic_conversion(): source = """ module constructor_generic_mod diff --git a/tests/fortran/infrastructure/wrapper_codegen/test_backend_foundations.py b/tests/fortran/infrastructure/codegen/test_backend_foundations.py similarity index 97% rename from tests/fortran/infrastructure/wrapper_codegen/test_backend_foundations.py rename to tests/fortran/infrastructure/codegen/test_backend_foundations.py index 35bc6cc37..a8ad12da5 100644 --- a/tests/fortran/infrastructure/wrapper_codegen/test_backend_foundations.py +++ b/tests/fortran/infrastructure/codegen/test_backend_foundations.py @@ -9,7 +9,7 @@ import pytest from tests.fortran._support.wrapper_build import REPO_ROOT -from prik.wrapper_codegen import ( +from prik.codegen import ( BackendScalarType, BindingModulePlan, BridgeModulePlan, @@ -37,7 +37,7 @@ NamespacePlan, UnsupportedWrapperCodegenNodeError, ) -from prik.wrapper_codegen.c.binding import CBindingGenerator +from prik.codegen.c.binding import CBindingGenerator def test_source_printers_render_complete_c_header_and_fortran_modules(): @@ -221,11 +221,11 @@ def test_fortran_source_printer_rejects_an_overlong_token_without_a_safe_break() def test_source_printers_do_not_import_wrapper_plan_models(): - path = REPO_ROOT / "prik" / "wrapper_codegen" / "printers" / "source_printers.py" + path = REPO_ROOT / "prik" / "codegen" / "printers" / "source_printers.py" imports = { node.module for node in ast.walk(ast.parse(Path(path).read_text(encoding="utf-8"))) if isinstance(node, ast.ImportFrom) and node.module is not None } - assert "prik.wrapper_codegen.plan" not in imports + assert "prik.codegen.plan" not in imports diff --git a/tests/fortran/infrastructure/wrapper_codegen/test_codegen_package_boundaries.py b/tests/fortran/infrastructure/codegen/test_codegen_package_boundaries.py similarity index 82% rename from tests/fortran/infrastructure/wrapper_codegen/test_codegen_package_boundaries.py rename to tests/fortran/infrastructure/codegen/test_codegen_package_boundaries.py index ee975400c..738c3e2ab 100644 --- a/tests/fortran/infrastructure/wrapper_codegen/test_codegen_package_boundaries.py +++ b/tests/fortran/infrastructure/codegen/test_codegen_package_boundaries.py @@ -10,14 +10,14 @@ from tests.fortran._support.wrapper_build import REPO_ROOT from prik.pipeline.wrapper_artifacts import GeneratedWrapperArtifacts -from prik.wrapper_codegen.checks import ( +from prik.codegen.checks import ( WrapperCodegenCheckConfig, - check_wrapper_codegen_package, - check_wrapper_codegen_paths, + check_codegen_package, + check_codegen_paths, ) SOURCE_ROOT = REPO_ROOT / "prik" -WRAPPER_CODEGEN_ROOT = SOURCE_ROOT / "wrapper_codegen" +CODEGEN_ROOT = SOURCE_ROOT / "codegen" def _imported_modules(path: Path) -> set[str]: @@ -44,7 +44,7 @@ def _write_module(root: Path, relative_path: str, source: str) -> Path: def _check_source(tmp_path: Path, source: str, *, filename: str = "bad.py") -> set[str]: path = _write_module(tmp_path, filename, source) - violations = check_wrapper_codegen_paths( + violations = check_codegen_paths( [path], config=WrapperCodegenCheckConfig(max_complexity=3, max_statements=4, max_nesting=2), ) @@ -52,33 +52,33 @@ def _check_source(tmp_path: Path, source: str, *, filename: str = "bad.py") -> s def test_canonical_printers_share_one_package(): - printers = WRAPPER_CODEGEN_ROOT / "printers" + printers = CODEGEN_ROOT / "printers" assert (printers / "pyi_printer.py").is_file() assert (printers / "source_printers.py").is_file() def test_backend_generators_do_not_import_each_other(): - binding_imports = _imported_modules(WRAPPER_CODEGEN_ROOT / "c" / "binding.py") - bridge_imports = _imported_modules(WRAPPER_CODEGEN_ROOT / "fortran" / "bridge.py") + binding_imports = _imported_modules(CODEGEN_ROOT / "c" / "binding.py") + bridge_imports = _imported_modules(CODEGEN_ROOT / "fortran" / "bridge.py") - assert not _imports_under(binding_imports, "prik.wrapper_codegen.fortran") - assert not _imports_under(bridge_imports, "prik.wrapper_codegen.c") + assert not _imports_under(binding_imports, "prik.codegen.fortran") + assert not _imports_under(bridge_imports, "prik.codegen.c") def test_wrapper_build_pipeline_imports_canonical_generator(): imports = _imported_modules(SOURCE_ROOT / "pipeline" / "build.py") - assert _imports_under(imports, "prik.wrapper_codegen") + assert _imports_under(imports, "prik.codegen") -def test_wrapper_codegen_package_static_contracts_pass(): - assert check_wrapper_codegen_package(WRAPPER_CODEGEN_ROOT) == () +def test_codegen_package_static_contracts_pass(): + assert check_codegen_package(CODEGEN_ROOT) == () -def test_wrapper_codegen_checker_command_runs_the_package_checker(): +def test_codegen_checker_command_runs_the_package_checker(): result = subprocess.run( - [sys.executable, "tools/check_wrapper_codegen_complexity.py"], + [sys.executable, "tools/check_codegen_complexity.py"], cwd=REPO_ROOT, capture_output=True, text=True, @@ -131,7 +131,7 @@ def test_checker_uses_strict_default_limits_for_emitter_handlers(tmp_path: Path) tmp_path, "strict.py", """ -from prik.wrapper_codegen import ClassVisitor +from prik.codegen import ClassVisitor class DemoEmitter(ClassVisitor): def _convert_item(self, value): @@ -149,7 +149,7 @@ def _convert_item(self, value): """, ) - violations = check_wrapper_codegen_paths([path]) + violations = check_codegen_paths([path]) assert "complexity" in {violation.code for violation in violations} @@ -158,7 +158,7 @@ def test_checker_rejects_missing_primary_and_secondary_registry_handlers(tmp_pat codes = _check_source( tmp_path, """ -from prik.wrapper_codegen import ClassVisitor +from prik.codegen import ClassVisitor class DemoEmitter(ClassVisitor): PRIMARY_REGISTRY = {"item": "_emit_item"} @@ -173,7 +173,7 @@ def test_checker_rejects_printer_calls_from_handlers(tmp_path: Path): codes = _check_source( tmp_path, """ -from prik.wrapper_codegen import ClassVisitor +from prik.codegen import ClassVisitor class DemoEmitter(ClassVisitor): HANDLER_REGISTRY = {"item": "_emit_item"} diff --git a/tests/fortran/infrastructure/wrapper_codegen/test_native_binding_support.py b/tests/fortran/infrastructure/codegen/test_native_binding_support.py similarity index 100% rename from tests/fortran/infrastructure/wrapper_codegen/test_native_binding_support.py rename to tests/fortran/infrastructure/codegen/test_native_binding_support.py diff --git a/tests/fortran/infrastructure/wrapper_codegen/test_scalar_boundary_validation.py b/tests/fortran/infrastructure/codegen/test_scalar_boundary_validation.py similarity index 98% rename from tests/fortran/infrastructure/wrapper_codegen/test_scalar_boundary_validation.py rename to tests/fortran/infrastructure/codegen/test_scalar_boundary_validation.py index ccfb4eadf..b40c6823d 100644 --- a/tests/fortran/infrastructure/wrapper_codegen/test_scalar_boundary_validation.py +++ b/tests/fortran/infrastructure/codegen/test_scalar_boundary_validation.py @@ -8,7 +8,7 @@ from prik.semantics.ownership import CodegenAction, NativeBarrierAction from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import ArgumentHandoffMode, BridgeDataAction -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _scalar_boundary_plan(): diff --git a/tests/fortran/infrastructure/wrapper_codegen/test_visitor.py b/tests/fortran/infrastructure/codegen/test_visitor.py similarity index 94% rename from tests/fortran/infrastructure/wrapper_codegen/test_visitor.py rename to tests/fortran/infrastructure/codegen/test_visitor.py index 0ded11791..38b0f2341 100644 --- a/tests/fortran/infrastructure/wrapper_codegen/test_visitor.py +++ b/tests/fortran/infrastructure/codegen/test_visitor.py @@ -4,7 +4,7 @@ import pytest -from prik.wrapper_codegen import ClassVisitor, UnsupportedWrapperCodegenNodeError +from prik.codegen import ClassVisitor, UnsupportedWrapperCodegenNodeError class BaseNode: diff --git a/tests/fortran/infrastructure/wrapper_codegen/test_wrapper_assembly.py b/tests/fortran/infrastructure/codegen/test_wrapper_assembly.py similarity index 99% rename from tests/fortran/infrastructure/wrapper_codegen/test_wrapper_assembly.py rename to tests/fortran/infrastructure/codegen/test_wrapper_assembly.py index 1e4e85f30..03d2d0c99 100644 --- a/tests/fortran/infrastructure/wrapper_codegen/test_wrapper_assembly.py +++ b/tests/fortran/infrastructure/codegen/test_wrapper_assembly.py @@ -10,7 +10,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.policy_completion import complete_semantic_policies from prik.stage_values import FrozenStageRecordError -from prik.wrapper_codegen import ( +from prik.codegen import ( CBindingGenerator, CSourcePrinter, FortranBridgeGenerator, @@ -35,7 +35,7 @@ def test_public_generator_directly_returns_complete_rendered_artifacts(): """ @nogil @bind("SWAP_ARGS") -@external +@standalone @native_call([Addr(Arg(1)), Addr(Arg(0))]) def swap_args(x: Float64, y: Float64) -> Float64: ... """, @@ -160,7 +160,7 @@ def test_direct_plan_edits_change_binding_and_bridge_generation_then_freeze_plan plan = _plan( """ @bind("ADD_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def calculate(x: Float64, y: Float64) -> Float64: ... """, diff --git a/tests/fortran/infrastructure/wrapper_codegen/test_wrapper_plan_validation.py b/tests/fortran/infrastructure/codegen/test_wrapper_plan_validation.py similarity index 99% rename from tests/fortran/infrastructure/wrapper_codegen/test_wrapper_plan_validation.py rename to tests/fortran/infrastructure/codegen/test_wrapper_plan_validation.py index 50c48fd9d..16b12f88c 100644 --- a/tests/fortran/infrastructure/wrapper_codegen/test_wrapper_plan_validation.py +++ b/tests/fortran/infrastructure/codegen/test_wrapper_plan_validation.py @@ -10,7 +10,7 @@ from prik.semantics.models import PYTHON_EXPORTS_METADATA from prik.semantics.ownership import CodegenAction, NativeBarrierAction, ObjectKind from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import ( +from prik.codegen import ( NamespacePlan, WrapperCodeGenerator, WrapperPlanner, @@ -28,7 +28,7 @@ def _scalar_plan(): """ @nogil @bind("SWAP_ARGS") -@external +@standalone @native_call([Addr(Arg(1)), Addr(Arg(0))]) def swap_args(x: Float64, y: Float64) -> Float64: ... """, diff --git a/tests/fortran/infrastructure/policy/test_policy_defaults_and_validation.py b/tests/fortran/infrastructure/policy/test_policy_defaults_and_validation.py index 42ba13fcf..d313dd5be 100644 --- a/tests/fortran/infrastructure/policy/test_policy_defaults_and_validation.py +++ b/tests/fortran/infrastructure/policy/test_policy_defaults_and_validation.py @@ -1,5 +1,9 @@ """Tests split by stable ownership concept from `test_handle_policy_dispatch.py`.""" +from pathlib import Path +import subprocess +import sys + from tests.fortran._support.ownership_policy import ( ADDRESS_ROLE_PROJECTION, ArrayInteropPolicy, @@ -437,3 +441,35 @@ def test_policy_completion_attaches_decisions_before_ir_lowering(): module.functions[0].arguments[0].metadata[RESOLVED_OWNERSHIP_POLICY_METADATA].transfer is TransferMode.IN_PLACE ) assert RESOLVED_RETURN_OWNERSHIP_POLICY_METADATA not in module.functions[0].metadata + + +def test_policy_completion_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + + result = subprocess.run( + [sys.executable, "prik/semantics/policy_completion.py"], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert result.stdout == ( + "before: math.scale(value): Float64 semantic IR\nafter: math.scale(value): scalar_value -> pass_value\n" + ) + + +def test_ownership_policy_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + + result = subprocess.run( + [sys.executable, "prik/semantics/ownership.py"], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert result.stdout == ( + "before: math.scale(value): Float64 semantic IR\nafter: scalar/caller/call_local; scalar_value -> pass_value\n" + ) diff --git a/tests/fortran/infrastructure/policy/test_wrapper_policy.py b/tests/fortran/infrastructure/policy/test_wrapper_policy.py index 3064f5e6e..c51920bc3 100644 --- a/tests/fortran/infrastructure/policy/test_wrapper_policy.py +++ b/tests/fortran/infrastructure/policy/test_wrapper_policy.py @@ -1,4 +1,6 @@ from pathlib import Path +import subprocess +import sys import pytest @@ -107,10 +109,10 @@ def hidden_storage_result() -> Float64[()]: ... def test_external_declaration_mode_is_completed_from_native_abi_requirements(): module = parse_pyi_text( """ -@external +@standalone def classic(n: Int32, values: Float64[n]) -> Float64: ... -@external +@standalone def optional(value: Annotated[Float64, Immutable] | None = ...) -> None: ... """, module_name="external_modes", @@ -135,7 +137,7 @@ def test_source_fmath_scalar_policy_projects_conservative_replacements(): assert policy.blockers == () assert [(export.namespace, export.name) for export in policy.python_exports] == [((), "add_r8")] assert policy.native_name == "ADD_R8" - assert policy.external is True + assert policy.standalone is True assert [argument.name for argument in policy.arguments] == ["X", "Y"] assert [argument.codegen_action for argument in policy.arguments] == [ CodegenAction.COPY_IN_OUT, @@ -204,7 +206,7 @@ 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" - assert policy.external is True + assert policy.standalone is True assert [argument.name for argument in policy.arguments] == ["X", "Y"] assert [argument.python_position for argument in policy.arguments] == [0, 1] @@ -252,7 +254,7 @@ def test_wrapper_policy_records_runtime_and_native_order_metadata(): """ @nogil @bind("SWAP_ARGS") -@external +@standalone @native_call([Addr(Arg(1)), Addr(Arg(0))]) def swap_args(x: Float64, y: Float64) -> Float64: ... """, @@ -263,7 +265,7 @@ def swap_args(x: Float64, y: Float64) -> Float64: ... policy = completed_function_wrapper_policy(module.functions[0]) assert policy.release_gil is True - assert policy.external is True + assert policy.standalone is True assert [argument.python_position for argument in policy.arguments] == [0, 1] assert [argument.native_position for argument in policy.arguments] == [1, 0] assert [(slot.native_position, slot.python_position, slot.value_kind) for slot in policy.native_call_slots] == [ @@ -578,3 +580,20 @@ def test_missing_wrapper_policy_fails_before_planning(): with pytest.raises(ValueError, match="missing completed wrapper policy"): completed_function_wrapper_policy(function) + + +def test_wrapper_policy_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + + result = subprocess.run( + [sys.executable, "prik/semantics/wrapper_policy.py"], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert result.stdout == ( + "before: math.scale(value): Float64 semantic IR\n" + "after: direct_transfer; result=native_scalar; native=pass_value\n" + ) diff --git a/tests/fortran/memory_management/wrapper_codegen/test_native_handle_planning.py b/tests/fortran/memory_management/codegen/test_native_handle_planning.py similarity index 98% rename from tests/fortran/memory_management/wrapper_codegen/test_native_handle_planning.py rename to tests/fortran/memory_management/codegen/test_native_handle_planning.py index c2d8495c5..dce87c938 100644 --- a/tests/fortran/memory_management/wrapper_codegen/test_native_handle_planning.py +++ b/tests/fortran/memory_management/codegen/test_native_handle_planning.py @@ -21,7 +21,7 @@ NativeArraySourceKind, NativeDescriptorHandoffABI, ) -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _native_handle_plan(): @@ -390,6 +390,10 @@ def test_generated_native_handle_artifacts_follow_one_typed_action_vocabulary(): assert "result_itemsize" in c_source assert "CFI_type_char" in c_source assert "character(kind=c_char, len=:), allocatable, dimension(:) :: names" in bridge_source + assert "result_owner_status = CFI_establish(result, NULL, CFI_attribute_pointer" in c_source + assert ( + "PRIK_NATIVE_ARRAY_KIND_POINTER, 1, CFI_type_double, sizeof(double), sizeof(CFI_CDESC_T(1)), result" in c_source + ) def test_constant_owned_handle_operations_do_not_emit_unused_descriptor_locals(): diff --git a/tests/fortran/modules/wrapper_codegen/test_scalar_module_variable_lowering.py b/tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py similarity index 82% rename from tests/fortran/modules/wrapper_codegen/test_scalar_module_variable_lowering.py rename to tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py index 9043e2904..c1c01ccf1 100644 --- a/tests/fortran/modules/wrapper_codegen/test_scalar_module_variable_lowering.py +++ b/tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py @@ -14,9 +14,9 @@ from prik.semantics.ownership import AssignmentMode, SetterAction from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import ModuleGetterAction -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner -from prik.wrapper_codegen.c.binding import CBindingGenerator -from prik.wrapper_codegen.fortran.bridge import FortranBridgeGenerator +from prik.codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen.c.binding import CBindingGenerator +from prik.codegen.fortran.bridge import FortranBridgeGenerator SCALAR_MODULE_CONTRACT = """ @@ -54,6 +54,24 @@ def _computed_constant_plan(): return WrapperPlanner().build(module) +def _parameter_array_plan(): + parsed = parse_fortran_project( + { + "parameter_array.f90": """ +module parameter_array + use iso_fortran_env, only: real64 + real(real64), parameter :: dpmpar(3) = [epsilon(1.0_real64), tiny(1.0_real64), huge(1.0_real64)] +end module parameter_array +""" + } + ) + modules = fortran_project_to_semantic_modules(parsed) + _apply_source_python_exports(modules) + module = _merge_wrapper_modules(modules, name="parameter_array_wrapper") + complete_semantic_policies(module) + return WrapperPlanner().build(module) + + def _source(artifacts, suffix: str) -> str: return next(item.text for item in artifacts.sources if item.path.name.endswith(suffix)) @@ -109,6 +127,33 @@ def test_symbolic_source_parameter_reuses_scalar_bridge_getter_for_module_initia assert "bind_c_set_computed" not in fortran_source +def test_parameter_array_uses_one_immutable_python_owned_import_snapshot(): + plan = _parameter_array_plan() + variable = next( + variable + for namespace in plan.namespaces + for variable in namespace.variables + if variable.binding.python_names == ("dpmpar",) + ) + assert variable.binding.getter_action is ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE + assert variable.binding.setter_action is SetterAction.OMIT + assert variable.binding.constant_value is None + assert variable.array is not None + assert variable.array.shape == ("3",) + + artifacts = WrapperCodeGenerator().generate(plan) + c_source = _source(artifacts, ".c") + fortran_source = _source(artifacts, ".f90") + assert "void * bind_c_get_dpmpar(int64_t * extent_0);" in c_source + assert "PyArray_EMPTY(1, constant_dpmpar_value_0_dimensions, NPY_FLOAT64, 1)" in c_source + assert "memcpy(PyArray_DATA((PyArrayObject *)constant_dpmpar_object_0)" in c_source + assert "PyArray_CLEARFLAGS((PyArrayObject *)constant_dpmpar_object_0, NPY_ARRAY_WRITEABLE)" in c_source + assert 'PyModule_AddObject(namespace_parameter_array, "dpmpar", constant_dpmpar_object_0)' in c_source + assert "real(c_double), allocatable, target, save, dimension(:) :: parameter_snapshot" in fortran_source + assert "parameter_snapshot = native_dpmpar" in fortran_source + assert "result = c_loc(parameter_snapshot)" in fortran_source + + def test_module_variable_visitors_consume_their_backend_owned_actions(): plan = _plan() counter = next( diff --git a/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/__init__.pyi b/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/__init__.pyi index 8f7db8aa0..c74019363 100644 --- a/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/__init__.pyi +++ b/tests/fortran/modules/end_to_end/fixtures/contracts/module_exports/__init__.pyi @@ -1,6 +1,6 @@ -from prik.contracts import Int32, external +from prik.contracts import Int32, standalone as prik_standalone from . import module1 from . import module2 -@external +@prik_standalone def standalone() -> Int32: ... diff --git a/tests/fortran/modules/end_to_end/test_parameter_array_constants.py b/tests/fortran/modules/end_to_end/test_parameter_array_constants.py new file mode 100644 index 000000000..90f694eb0 --- /dev/null +++ b/tests/fortran/modules/end_to_end/test_parameter_array_constants.py @@ -0,0 +1,67 @@ +"""Runtime contract for immutable snapshots of Fortran parameter arrays.""" + +from __future__ import annotations + +import importlib +import sys +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_wrapper_plan_and_import, _sole_native_module + +pytestmark = pytest.mark.fortran_end_to_end + +PARAMETER_ARRAY_SOURCE = """\ +module parameter_array_constants_f90 + use iso_fortran_env, only: real64 + implicit none + real(real64), parameter :: dpmpar(3) = [epsilon(1.0_real64), tiny(1.0_real64), huge(1.0_real64)] +contains + function parameter_sum() result(value) + real(real64) :: value + + value = sum(dpmpar) + end function parameter_sum +end module parameter_array_constants_f90 +""" + + +def _write_source(root: Path) -> Path: + root.mkdir(parents=True) + source = root / "parameter_array_constants_f90.f90" + source.write_text(PARAMETER_ARRAY_SOURCE, encoding="utf-8") + return source + + +def test_fortran_parameter_array_is_a_read_only_python_owned_import_snapshot(tmp_path: Path): + source = _write_source(tmp_path / "fixture") + module, result = _build_source_wrapper_plan_and_import(source, tmp_path / "build") + + values = module.dpmpar + expected = np.array( + [np.finfo(np.float64).eps, np.finfo(np.float64).tiny, np.finfo(np.float64).max], + dtype=np.float64, + ) + assert isinstance(values, np.ndarray) + assert values.dtype == np.dtype(np.float64) + assert values.shape == (3,) + assert values.flags.f_contiguous + assert values.flags.writeable is False + np.testing.assert_array_equal(values, expected) + with pytest.raises(ValueError, match="read-only"): + values[0] = 1.0 + assert module.parameter_sum() == np.float64(expected.sum()) + + module.dpmpar = np.array([1.0, 2.0, 3.0], dtype=np.float64) + assert module.parameter_sum() == np.float64(expected.sum()) + + sys.modules.pop("parameter_array_constants_f90", None) + sys.path.insert(0, str(result.output_dir)) + try: + reloaded = _sole_native_module(importlib.import_module("parameter_array_constants_f90")) + finally: + sys.path.remove(str(result.output_dir)) + np.testing.assert_array_equal(reloaded.dpmpar, expected) + assert reloaded.dpmpar.flags.writeable is False diff --git a/tests/fortran/modules/parsing/test_project_scope_models.py b/tests/fortran/modules/parsing/test_project_scope_models.py index 138168885..73546b7bc 100644 --- a/tests/fortran/modules/parsing/test_project_scope_models.py +++ b/tests/fortran/modules/parsing/test_project_scope_models.py @@ -32,6 +32,25 @@ def test_module_visibility_public_and_private_spec_lines_are_applied(): assert modules["public_mod"].variables[0].visibility == "public" +def test_declaration_level_private_attribute_overrides_public_module_default(): + parsed = parse_fortran_file( + """ +module constants + real, parameter, private :: epsilon = 1.0 + real, parameter :: visible = 2.0 +end module constants +""" + ) + + module = parsed.modules[0] + + assert {variable.name: variable.visibility for variable in module.variables} == { + "epsilon": "private", + "visible": "public", + } + assert module.private_symbols == ["epsilon"] + + def test_submodule_types_interfaces_and_project_dependencies_attach_to_public_models(): code = """ submodule (ancestor_mod:parent_mod) child_mod @@ -308,6 +327,88 @@ def test_directory_project_tracks_renamed_kind_imports_from_other_files(tmp_path assert project.dependencies["solver_mod"] == {"precision_mod"} +def test_project_resolves_reexported_intrinsic_kind_renames(): + project = parse_fortran_project( + { + "consumer.f90": """ +subroutine consume(x) + use fftpack_kind, only: dp => rk + real(dp), intent(inout) :: x +end subroutine consume +""", + "kind.f90": """ +module fftpack_kind + use, intrinsic :: iso_fortran_env, only: rk => real64 +end module fftpack_kind +""", + } + ) + + assert project.procedures["consume"].arguments[0].kind == "real64" + + +def test_single_file_project_resolves_intrinsic_kind_rename_for_module_variables(): + project = parse_fortran_project( + { + "minpack.f90": """ +module minpack_module + use iso_fortran_env, only: wp => real64 + real(wp), parameter :: dpmpar(3) = 0.0_wp +contains + real(wp) function enorm(value) result(output) + real(wp), intent(in) :: value + output = value + end function enorm +end module minpack_module +""" + } + ) + + module = project.modules["minpack_module"] + assert module.variables[0].kind == "real64" + assert module.procedures[0].arguments[0].kind == "real64" + assert module.procedures[0].result.kind == "real64" + + +def test_project_resolves_submodule_host_associated_kind(): + project = parse_fortran_project( + { + "implementation.f90": """ +submodule(transform_api) transform_impl +contains + module function twice(value) result(output) + real(rk), intent(in) :: value + real(rk) :: output + end function twice +end submodule transform_impl +""", + "parent.f90": """ +module transform_api + use precision + interface + module function twice(value) result(output) + real(rk), intent(in) :: value + real(rk) :: output + end function twice + end interface +end module transform_api +""", + "kind.f90": """ +module precision + use, intrinsic :: iso_fortran_env, only: rk => real64 +end module precision +""", + } + ) + + procedure = project.submodules["transform_impl"].procedures[0] + assert procedure.arguments[0].kind == "real64" + assert procedure.result.kind == "real64" + prototype = project.modules["transform_api"].interfaces[0].procedures[0] + assert prototype.arguments[0].kind == "real64" + assert prototype.result.kind == "real64" + + def test_directory_namespace_records_missing_and_parent_only_submodule_dependencies(tmp_path): (tmp_path / "parent.f90").write_text( """ @@ -377,7 +478,7 @@ def test_program_and_block_data_scope_errors_use_public_parse_paths(): ) -def test_project_resolution_keeps_relevant_local_parameter_variables(): +def test_project_resolution_folds_fortran_real_literal_integer_parameters(): project = parse_fortran_project( { "local_params.f90": """ @@ -392,8 +493,7 @@ def test_project_resolution_keeps_relevant_local_parameter_variables(): proc = project.procedures["use_relevant_local_param"] - assert proc.arguments[0].shape == ["1:+(8)-one"] - assert proc.variables["one"].value == "1" + assert proc.arguments[0].shape == ["1:7"] def test_project_resolution_uses_file_level_use_only_and_local_parameters(tmp_path): diff --git a/tests/fortran/modules/policy/test_module_variable_policy.py b/tests/fortran/modules/policy/test_module_variable_policy.py index bbd9bd6b8..1662e875e 100644 --- a/tests/fortran/modules/policy/test_module_variable_policy.py +++ b/tests/fortran/modules/policy/test_module_variable_policy.py @@ -7,6 +7,7 @@ ) from prik.parsers.fortran.parser import parse_fortran_project from prik.pipeline.build import _apply_source_python_exports, _merge_wrapper_modules +from prik.codegen.printers.pyi_printer import PyiPrinter from prik.semantics.fortran2ir import fortran_project_to_semantic_modules from prik.semantics.models import RESOLVED_MODULE_VARIABLE_POLICY_METADATA from prik.semantics.ownership import AssignmentMode @@ -87,6 +88,40 @@ def test_symbolic_source_parameters_use_native_getters_while_literals_stay_in_bi assert all(policy.supported for policy in policies.values()) +def test_parameter_arrays_complete_as_immutable_native_snapshots(): + parsed = parse_fortran_project( + { + "parameter_array.f90": """ +module parameter_array + use iso_fortran_env, only: real64 + real(real64), parameter :: dpmpar(3) = [epsilon(1.0_real64), tiny(1.0_real64), huge(1.0_real64)] +end module parameter_array +""" + } + ) + modules = fortran_project_to_semantic_modules(parsed) + _apply_source_python_exports(modules) + module = _merge_wrapper_modules(modules, name="parameter_array_wrapper") + + complete_semantic_policies(module) + + policy = next( + variable.metadata[RESOLVED_MODULE_VARIABLE_POLICY_METADATA] + for variable in module.variables + if variable.name == "dpmpar" + ) + assert policy.supported is True + assert policy.getter_action is ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE + assert policy.getter is not None + assert policy.getter.owner.value == "python" + assert policy.setter_action is SetterAction.OMIT + assert policy.native_assignment is AssignmentMode.NONE + assert policy.constant_value is None + assert policy.array is not None + assert policy.array.shape == ("3",) + assert "dpmpar: Final[Float64[3]]" in PyiPrinter().emit(module) + + def test_fixed_module_array_requires_explicit_addressable_alias_storage(): module = parse_pyi_text( """ diff --git a/tests/fortran/modules/semantics/test_modules_and_imports.py b/tests/fortran/modules/semantics/test_modules_and_imports.py index 742f351be..b8d2c2789 100644 --- a/tests/fortran/modules/semantics/test_modules_and_imports.py +++ b/tests/fortran/modules/semantics/test_modules_and_imports.py @@ -14,6 +14,8 @@ has_constraint, parse_fortran_source, ) +from prik import parse_fortran_project +from prik.semantics.fortran2ir import fortran_project_to_semantic_modules def test_converter_normalizes_wrapped_types_and_resolves_wildcard_imports(): @@ -79,6 +81,86 @@ def test_iso_c_module_variable_kinds_map_to_semantic_types(): assert variables["origin"].shape == ["3"] +def test_parameter_array_is_retained_as_a_public_semantic_constant(): + source = """ +module constants_mod + real, parameter :: machine_values(3) = [1.0, 2.0, 3.0] + integer, parameter :: count = 3 +contains + real function second_value() result(value) + value = machine_values(2) + end function second_value +end module constants_mod +""" + + module = fortran_module_to_semantic_module(parse_fortran_source(source)) + + variables = {variable.name: variable.semantic_type for variable in module.variables} + + assert list(variables) == ["machine_values", "count"] + assert variables["machine_values"].name == "Float32" + assert variables["machine_values"].shape == ["3"] + assert has_constraint(variables["machine_values"], "Constant") + assert [function.name for function in module.functions] == ["second_value"] + + +def test_explicit_public_unnamed_interface_procedure_uses_the_declared_signature(): + source = """ +module api + private + public :: scale + interface + subroutine scale(values) + real(8), intent(inout) :: values(*) + end subroutine scale + end interface +end module api +""" + + module = fortran_module_to_semantic_module(parse_fortran_source(source)) + + assert [function.name for function in module.functions] == ["scale"] + assert module.prototypes == [] + argument = module.functions[0].arguments[0] + assert argument.semantic_type.name == "Float64" + assert argument.semantic_type.rank == 1 + + +def test_explicit_public_submodule_interface_is_callable_from_parent_contract(): + project = parse_fortran_project( + { + "api.f90": """ +module api + private + public :: values + interface + module function values(n) result(output) + integer, intent(in) :: n + integer :: output(n) + end function values + end interface +end module api +""", + "implementation.f90": """ +submodule(api) implementation +contains + module procedure values + do n = 1, n + output(n) = n + end do + end procedure values +end submodule implementation +""", + } + ) + + module = next(item for item in fortran_project_to_semantic_modules(project) if item.name == "api") + + assert [function.name for function in module.functions] == ["values"] + assert module.prototypes == [] + assert module.functions[0].return_type.shape == ["n"] + + def test_complex_module(): source = """ module fem_mod @@ -218,3 +300,22 @@ def test_fortran_to_ir_preserves_module_semantics_from_inline_source(): assert semantic_dtype.visibility == "private" assert semantic_proc.visibility == "public" assert semantic_file_modules[0].name == "m" + + +def test_declaration_level_private_module_constant_is_not_exported(): + parsed = parse_fortran_source( + """ +module constants + real, parameter, private :: epsilon = 1.0 + real, parameter :: visible = 2.0 +end module constants +""", + filename="constants.f90", + ) + + semantic_module = fortran_module_to_semantic_module(parsed) + + assert [(variable.name, variable.visibility) for variable in semantic_module.variables] == [ + ("epsilon", "private"), + ("visible", "public"), + ] diff --git a/tests/fortran/optional_arguments/wrapper_codegen/test_optional_lowering.py b/tests/fortran/optional_arguments/codegen/test_optional_lowering.py similarity index 98% rename from tests/fortran/optional_arguments/wrapper_codegen/test_optional_lowering.py rename to tests/fortran/optional_arguments/codegen/test_optional_lowering.py index e4e3a86b5..51e3fb2f3 100644 --- a/tests/fortran/optional_arguments/wrapper_codegen/test_optional_lowering.py +++ b/tests/fortran/optional_arguments/codegen/test_optional_lowering.py @@ -11,7 +11,7 @@ from prik.pipeline.pyi import pyi_file_to_semantic_module from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import BridgeDataAction, OptionalMode -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner OPTIONAL_FIXED_CONTRACT = ( diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_fixed/__init__.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_fixed/__init__.pyi index 99f71fd4b..bc769ae74 100644 --- a/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_fixed/__init__.pyi +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/contracts/foptional_fixed/__init__.pyi @@ -1,6 +1,6 @@ -from prik.contracts import Addr, Arg, Int32, external, native_call +from prik.contracts import Addr, Arg, Int32, native_call, standalone -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def optional_scale( base: Int32, diff --git a/tests/fortran/pointers/wrapper_codegen/test_pointer_lowering.py b/tests/fortran/pointers/codegen/test_pointer_lowering.py similarity index 98% rename from tests/fortran/pointers/wrapper_codegen/test_pointer_lowering.py rename to tests/fortran/pointers/codegen/test_pointer_lowering.py index 7b6063523..df98a2a2d 100644 --- a/tests/fortran/pointers/wrapper_codegen/test_pointer_lowering.py +++ b/tests/fortran/pointers/codegen/test_pointer_lowering.py @@ -9,7 +9,7 @@ NativeArrayResultAllocation, NativeDescriptorHandoffABI, ) -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _pointer_plan(): diff --git a/tests/fortran/pyi_contracts/calls_and_results/README.md b/tests/fortran/pyi_contracts/calls_and_results/README.md index db0e813c1..e8963fb2a 100644 --- a/tests/fortran/pyi_contracts/calls_and_results/README.md +++ b/tests/fortran/pyi_contracts/calls_and_results/README.md @@ -10,7 +10,7 @@ Evidence is split by the stage that establishes it: - `policy/` checks that projection, mutation, storage, and GIL decisions are complete before wrapper planning; -- `wrapper_codegen/` checks that reordered slots, hidden results, and +- `codegen/` checks that reordered slots, hidden results, and replacement writeback dispatch through their selected plan paths; and - `end_to_end/` builds source-free edited contracts against explicit native objects and verifies native-order calls, projections, immutable diff --git a/tests/fortran/pyi_contracts/calls_and_results/wrapper_codegen/test_call_and_result_lowering.py b/tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py similarity index 96% rename from tests/fortran/pyi_contracts/calls_and_results/wrapper_codegen/test_call_and_result_lowering.py rename to tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py index 6357e33e8..d8ccfcdf4 100644 --- a/tests/fortran/pyi_contracts/calls_and_results/wrapper_codegen/test_call_and_result_lowering.py +++ b/tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py @@ -8,7 +8,7 @@ from prik.semantics.ownership import CodegenAction, NativeBarrierAction, ObjectKind from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import WritebackPhase -from prik.wrapper_codegen import DatatypeFamily, WrapperCodeGenerator, WrapperPlanner +from prik.codegen import DatatypeFamily, WrapperCodeGenerator, WrapperPlanner def _plan(source: str, *, module_name: str): @@ -28,7 +28,7 @@ def test_plan_records_reordered_arguments_gil_behavior_and_hidden_result_slots() """ @nogil @bind("SWAP_ARGS") -@external +@standalone @native_call([Addr(Arg(1)), Addr(Arg(0))]) def swap_args(x: Float64, y: Float64) -> Float64: ... """, @@ -40,7 +40,7 @@ def swap_args(x: Float64, y: Float64) -> Float64: ... assert reordered.binding.release_gil is True assert reordered.bridge.native_name == "SWAP_ARGS" - assert reordered.bridge.external is True + assert reordered.bridge.standalone is True assert [argument.native_position for argument in reordered.arguments] == [1, 0] assert [argument.datatype_family for argument in reordered.arguments] == [ DatatypeFamily.REAL, diff --git a/tests/fortran/pyi_contracts/exports_and_modules/README.md b/tests/fortran/pyi_contracts/exports_and_modules/README.md index 431dd7e7d..3ef1e7700 100644 --- a/tests/fortran/pyi_contracts/exports_and_modules/README.md +++ b/tests/fortran/pyi_contracts/exports_and_modules/README.md @@ -10,7 +10,7 @@ Evidence is split by the stage that establishes it: - `semantics/` checks accepted literal values and rejects expression defaults; - `policy/` completes export pruning and initializer/write-through decisions; -- `wrapper_codegen/` checks literal spelling selected by the completed plan; +- `codegen/` checks literal spelling selected by the completed plan; and - `end_to_end/` builds child, flattened, aliased/bound, hidden, removed, and initialized public surfaces and checks export-collision diagnostics. diff --git a/tests/fortran/pyi_contracts/exports_and_modules/wrapper_codegen/test_module_initializer_lowering.py b/tests/fortran/pyi_contracts/exports_and_modules/codegen/test_module_initializer_lowering.py similarity index 92% rename from tests/fortran/pyi_contracts/exports_and_modules/wrapper_codegen/test_module_initializer_lowering.py rename to tests/fortran/pyi_contracts/exports_and_modules/codegen/test_module_initializer_lowering.py index edb987ace..08d0aac07 100644 --- a/tests/fortran/pyi_contracts/exports_and_modules/wrapper_codegen/test_module_initializer_lowering.py +++ b/tests/fortran/pyi_contracts/exports_and_modules/codegen/test_module_initializer_lowering.py @@ -2,7 +2,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def test_module_variable_literal_families_select_their_c_spelling(): diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/aliases.pyi b/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/aliases.pyi index 756b7cfbb..2f09862f2 100644 --- a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/aliases.pyi +++ b/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/aliases.pyi @@ -1,9 +1,9 @@ -# Intentional difference: select an added binding, alias two exports, and rename an external. -from prik.contracts import Int32, bind, external +# Intentional difference: select an added binding, alias two exports, and rename a standalone procedure. +from prik.contracts import Int32, bind, standalone from . import facade as m2 from .module1 import solve, update as update_module1 from .module2 import update as update_module2 -@external +@standalone @bind("standalone") def renamed_standalone() -> Int32: ... diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/collision.pyi b/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/collision.pyi index 5c8634dbe..e44f0b517 100644 --- a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/collision.pyi +++ b/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/collision.pyi @@ -1,7 +1,7 @@ # Intentional invalid difference: both wildcard imports export update. -from prik.contracts import Int32, external +from prik.contracts import Int32, standalone from .module1 import * from .module2 import * -@external +@standalone def standalone() -> Int32: ... diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/flatten.pyi b/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/flatten.pyi index 7173450dd..62ac33b4a 100644 --- a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/flatten.pyi +++ b/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/flatten.pyi @@ -1,7 +1,7 @@ # Intentional difference: flatten the non-colliding public names. -from prik.contracts import Int32, external +from prik.contracts import Int32, standalone from .module1 import * from .module2 import * -@external +@standalone def standalone() -> Int32: ... diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py b/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py index 1219a4b04..e00f95425 100644 --- a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py +++ b/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py @@ -93,7 +93,6 @@ def test_entry_contract_selects_child_flattened_aliased_and_bound_exports(tmp_pa assert aliased.m2.branch.func2() == np.int32(2) assert not hasattr(aliased, "Int32") assert not hasattr(aliased, "bind") - assert not hasattr(aliased, "external") assert not hasattr(aliased, "facade") assert not hasattr(aliased, "func1") assert not hasattr(aliased, "standalone") diff --git a/tests/fortran/pyi_contracts/functions_and_classes/README.md b/tests/fortran/pyi_contracts/functions_and_classes/README.md index 535dbc000..b41a3d2b2 100644 --- a/tests/fortran/pyi_contracts/functions_and_classes/README.md +++ b/tests/fortran/pyi_contracts/functions_and_classes/README.md @@ -12,7 +12,7 @@ Evidence is split by the stage that establishes it: rejects contradictory constructor declarations; - `policy/` checks completed class invocation, visibility, overload dispatch, and constructor plans; -- `wrapper_codegen/` checks the selected direct-constructor emission path; and +- `codegen/` checks the selected direct-constructor emission path; and - `end_to_end/` calls edited methods, constructors, and overloads and checks removal and native-accessibility failures. diff --git a/tests/fortran/pyi_contracts/functions_and_classes/wrapper_codegen/test_constructor_lowering.py b/tests/fortran/pyi_contracts/functions_and_classes/codegen/test_constructor_lowering.py similarity index 95% rename from tests/fortran/pyi_contracts/functions_and_classes/wrapper_codegen/test_constructor_lowering.py rename to tests/fortran/pyi_contracts/functions_and_classes/codegen/test_constructor_lowering.py index e103acdb5..10a2e7a5f 100644 --- a/tests/fortran/pyi_contracts/functions_and_classes/wrapper_codegen/test_constructor_lowering.py +++ b/tests/fortran/pyi_contracts/functions_and_classes/codegen/test_constructor_lowering.py @@ -2,7 +2,7 @@ from prik.pipeline.pyi import pyi_text_to_semantic_module from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def test_bound_constructor_generates_one_initializer_without_keyword_default(): diff --git a/tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py b/tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py index e00253333..ed91d4aa4 100644 --- a/tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py +++ b/tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py @@ -8,7 +8,7 @@ from prik.pipeline.pyi import pyi_file_to_semantic_module from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import ClassInvocationKind, OverloadMatchKind -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner FIXTURES = Path(__file__).parents[1] / "end_to_end" / "fixtures" / "edited_contracts" METHOD_AND_CONSTRUCTOR = FIXTURES / "method_and_constructor" / "fclasses_f90.pyi" diff --git a/tests/fortran/raw_addresses/wrapper_codegen/test_raw_array_lowering.py b/tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py similarity index 98% rename from tests/fortran/raw_addresses/wrapper_codegen/test_raw_array_lowering.py rename to tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py index c6a40b938..3f77dc670 100644 --- a/tests/fortran/raw_addresses/wrapper_codegen/test_raw_array_lowering.py +++ b/tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py @@ -17,8 +17,8 @@ ) from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import ArgumentHandoffMode, BridgeDataAction -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner -from prik.wrapper_codegen.plan import DatatypeFamily +from prik.codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen.plan import DatatypeFamily def _raw_array_module(): diff --git a/tests/fortran/raw_addresses/wrapper_codegen/test_scalar_address_lowering.py b/tests/fortran/raw_addresses/codegen/test_scalar_address_lowering.py similarity index 98% rename from tests/fortran/raw_addresses/wrapper_codegen/test_scalar_address_lowering.py rename to tests/fortran/raw_addresses/codegen/test_scalar_address_lowering.py index 01b91754d..d55451234 100644 --- a/tests/fortran/raw_addresses/wrapper_codegen/test_scalar_address_lowering.py +++ b/tests/fortran/raw_addresses/codegen/test_scalar_address_lowering.py @@ -7,7 +7,7 @@ from prik.semantics.ownership import CodegenAction, NativeBarrierAction, ObjectKind, PythonBarrierAction from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import ArgumentHandoffMode, BridgeDataAction, DirectResultABI -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _scalar_boundary_plan(): diff --git a/tests/fortran/raw_addresses/wrapper_codegen/test_string_address_lowering.py b/tests/fortran/raw_addresses/codegen/test_string_address_lowering.py similarity index 98% rename from tests/fortran/raw_addresses/wrapper_codegen/test_string_address_lowering.py rename to tests/fortran/raw_addresses/codegen/test_string_address_lowering.py index 064b45ed2..1011bf5d6 100644 --- a/tests/fortran/raw_addresses/wrapper_codegen/test_string_address_lowering.py +++ b/tests/fortran/raw_addresses/codegen/test_string_address_lowering.py @@ -22,7 +22,7 @@ RAW_STRING_ADDRESS_COPY_REASON, STRING_STORAGE_COPY_REASON, ) -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _string_address_module(): diff --git a/tests/fortran/semantic_ir/semantics/test_compile_time_values.py b/tests/fortran/semantic_ir/semantics/test_compile_time_values.py index 667e9200d..a70f65c09 100644 --- a/tests/fortran/semantic_ir/semantics/test_compile_time_values.py +++ b/tests/fortran/semantic_ir/semantics/test_compile_time_values.py @@ -1,5 +1,9 @@ """Tests split by stable ownership concept from `test_compile_time_values.py`.""" +from pathlib import Path +import subprocess +import sys + from tests.fortran._support.semantic_conversion import ( FortranArgument, FortranBlockData, @@ -32,6 +36,18 @@ ) +def test_fortran_to_ir_direct_script_runs_its_no_argument_example(): + completed = subprocess.run( + [sys.executable, "prik/semantics/fortran2ir.py"], + cwd=Path(__file__).resolve().parents[4], + capture_output=True, + text=True, + check=True, + ) + + assert completed.stdout.strip() == "math.scale(value): Float64 via reference storage" + + def test_semantic_compile_time_requirements_can_be_supplied_for_kind_selection(): source = """ module solver_mod diff --git a/tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py b/tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py index c7aee6e05..29a890726 100644 --- a/tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py +++ b/tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py @@ -1,5 +1,9 @@ """Tests split by stable ownership concept from `test_python_ast_contracts.py`.""" +from pathlib import Path +import subprocess +import sys + from tests.fortran._support.pyi_conversion import ( CONTRACT_IMPORT, CONTRACT_SYMBOLS, @@ -38,6 +42,20 @@ def test_convert_pyi_to_ir_accepts_parsed_pyi_ast_only(): convert_pyi_to_ir(source) +def test_pyi2ir_direct_example_is_runnable(): + repository_root = Path(__file__).resolve().parents[4] + + result = subprocess.run( + [sys.executable, "prik/semantics/pyi2ir.py"], + cwd=repository_root, + capture_output=True, + check=True, + text=True, + ) + + assert result.stdout == "math.scale(value): Float64 -> Float64\n" + + def test_pyi_parser_reports_unsupported_lines_and_invalid_helpers(): with pytest.raises(ValueError, match=r"Unsupported .pyi node"): parse_pyi_text("bare_name\n", module_name="edited") diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi b/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi index c31d5b238..b7dc4f895 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi +++ b/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi @@ -1,7 +1,7 @@ -from prik.contracts import Addr, Arg, Int32, external, native_call +from prik.contracts import Addr, Arg, Int32, native_call, standalone from . import contract_math_mod -@external +@standalone @native_call([Addr(Arg(0))]) def external_double( value: Int32 diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi b/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi index 8d95f69bc..d2ff52ec9 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi +++ b/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi @@ -1,5 +1,5 @@ -from prik.contracts import external +from prik.contracts import standalone from . import contract_same_name -@external +@standalone def external_ping() -> None: ... diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi b/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi index 12171c6c6..af3318765 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi +++ b/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi @@ -1,9 +1,9 @@ -from prik.contracts import Addr, Arg, Int32, external, native_call +from prik.contracts import Addr, Arg, Int32, native_call, standalone -@external +@standalone def standalone_ping() -> None: ... -@external +@standalone @native_call([Addr(Arg(0))]) def standalone_double( value: Int32 diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py b/tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py index 6319df427..bcf768a9d 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py +++ b/tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py @@ -9,7 +9,7 @@ from prik.pipeline import build as build_pipeline from prik.pipeline.build import _discover_pyi_imports, _pyi_contract_bundle, _pyi_dependency_path from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text -from prik.wrapper_codegen.printers import emit_module +from prik.codegen.printers import emit_module FIXTURES = Path(__file__).parent / "fixtures" CONTRACT_FIXTURES = FIXTURES / "contracts" diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py b/tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py index e79da01ad..4dda9227a 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py +++ b/tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py @@ -50,7 +50,7 @@ def test_standalone_generation_writes_explicit_package_entry(tmp_path: Path): assert entry == tmp_path / "contracts" / "contract_standalone_only" / "__init__.pyi" assert {path.name for path in entry.parent.iterdir()} == {"__init__.pyi"} text = entry.read_text(encoding="utf-8") - assert text.count("@external") == 2 + assert text.count("@standalone") == 2 assert "def standalone_ping() -> None: ..." in text assert "def standalone_double(" in text @@ -67,9 +67,9 @@ def test_module_generation_writes_explicit_package_entry_and_native_leaf(tmp_pat "contract_math_mod.pyi", } assert entry.read_text(encoding="utf-8").startswith( - "from prik.contracts import Addr, Arg, Int32, external, native_call\n" + "from prik.contracts import Addr, Arg, Int32, native_call, standalone\n" "from . import contract_math_mod\n\n" - "@external\n" + "@standalone\n" ) @@ -82,9 +82,9 @@ def test_same_named_module_uses_init_entry_and_keeps_externals_at_root(tmp_path: assert entry == tmp_path / "contracts" / "contract_same_name" / "__init__.pyi" assert {path.name for path in entry.parent.iterdir()} == {"__init__.pyi", "contract_same_name.pyi"} assert entry.read_text(encoding="utf-8") == ( - "from prik.contracts import external\n" + "from prik.contracts import standalone\n" "from . import contract_same_name\n\n" - "@external\n" + "@standalone\n" "def external_ping() -> None: ...\n" ) assert "def module_ping() -> None: ..." in (entry.parent / "contract_same_name.pyi").read_text(encoding="utf-8") diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_modern_example.py b/tests/fortran/semantic_pyi_format/pipeline/test_modern_example.py index 7be1ad2a4..09e02b1cb 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_modern_example.py +++ b/tests/fortran/semantic_pyi_format/pipeline/test_modern_example.py @@ -2,7 +2,7 @@ from prik import parse_fortran_file from prik.semantics.fortran2ir import fortran_module_to_semantic_module -from prik.wrapper_codegen.printers import emit_module +from prik.codegen.printers import emit_module def test_modern_fortran_example_pyi_snapshot(): diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py b/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py index 14b749b08..3c1ed2b13 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py +++ b/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py @@ -3,7 +3,7 @@ import pytest from prik.semantics.fortran2ir import fortran_module_to_semantic_module -from prik.wrapper_codegen.printers import emit_module +from prik.codegen.printers import emit_module from tests.fortran._support.fixture_conversion import FORTRAN_FIXTURES, TESTS_DIR, parse_fixture diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_imports_and_packages.py b/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_imports_and_packages.py index d9754283a..f9fa026a5 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_imports_and_packages.py +++ b/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_imports_and_packages.py @@ -427,7 +427,45 @@ def test_emit_module_aliases_contract_import_when_user_name_collides(): code = emit_module(module) - assert "Flat as " in code.splitlines()[0] + assert "Flat as prik_Flat" in code.splitlines()[0] reparsed = _parse_pyi_text(code, module_name="alias_mod") assert reparsed.variables[0].name == "Flat" assert reparsed.functions[0].arguments[0].semantic_type.storage.array.category == "assumed_size" + + +def test_emit_module_aliases_standalone_only_for_actual_name_collisions(): + int32_type = SemanticType("Int32") + standalone_origin = SemanticOrigin(source_language="fortran", native_scope=None) + + ordinary = emit_module( + SemanticModule( + name="ordinary", + functions=[SemanticFunction("calculate", return_type=int32_type, origin=standalone_origin)], + ) + ) + colliding = emit_module( + SemanticModule( + name="colliding", + functions=[SemanticFunction("standalone", return_type=int32_type, origin=standalone_origin)], + ) + ) + twice_colliding = emit_module( + SemanticModule( + name="twice_colliding", + variables=[ + SemanticVariable( + "prik_standalone", + SemanticType("Int32", constraints=[SemanticConstraint("Constant")]), + default_value="1", + ) + ], + functions=[SemanticFunction("standalone", return_type=int32_type, origin=standalone_origin)], + ) + ) + + assert "from prik.contracts import Int32, standalone\n" in ordinary + assert "@standalone\ndef calculate() -> Int32: ..." in ordinary + assert "from prik.contracts import Int32, standalone as prik_standalone\n" in colliding + 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 diff --git a/tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py b/tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py index d20daf092..9cb7e82ac 100644 --- a/tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py +++ b/tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py @@ -740,7 +740,7 @@ def test_convert_pyi_to_ir_handles_pointer_and_array_storage_variants(): deep: Addr[3](Float64) rank_any: Float64[...] strided: Float64[0:n:] -computed: Float64[size(xl)] +computed: Float64[xl.size] bounded_answer: Final[Annotated[Int32, Bounded(1, 8)]] nested_answer: Final[Final[Int32]] """, @@ -759,7 +759,7 @@ def test_convert_pyi_to_ir_handles_pointer_and_array_storage_variants(): assert rank_any.rank == 1 assert strided.shape == ["0:n:Strided"] assert strided.storage.array.contiguous is False - assert computed.shape == ["size(xl)"] + assert computed.shape == ["xl.size"] assert bounded.constraints == [ SemanticConstraint("Bounded", [1, 8]), SemanticConstraint("Constant"), diff --git a/tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py b/tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py index c321c379c..7fc54b5be 100644 --- a/tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py +++ b/tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py @@ -352,7 +352,7 @@ def test_generated_native_scope_comes_from_contract_filename(): assert loaded.functions[0].origin.native_scope == "renamed_contract" -def test_generated_standalone_contract_retains_external_native_placement(): +def test_generated_standalone_contract_retains_standalone_native_placement(): parsed = parse_fortran_file( """ subroutine solve(value) @@ -364,7 +364,7 @@ def test_generated_standalone_contract_retains_external_native_placement(): generated = emit_module(module) loaded = parse_pyi_text(generated, module_name="renamed_root_contract") - assert "@external" in generated + assert "@standalone" in generated assert loaded.functions[0].origin.native_scope is None assert native_contract_issues(loaded) == [] assert loaded.origin.native_name == "renamed_root_contract" diff --git a/tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py b/tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py index 942df625d..128a6cbc8 100644 --- a/tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py +++ b/tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py @@ -50,6 +50,33 @@ def test_convert_pyi_to_ir_dispatches_nested_and_qualified_semantic_types(): assert raw_pointer.semantic_type.storage.read_only is False +def test_boolean_storage_widths_round_trip_as_one_semantic_type_family(): + module = pyi_text_to_semantic_module( + """ +from prik.contracts import Bool, Bool8, Bool16, Bool32, Bool64 + +def inspect( + default: Bool, + byte: Bool8[:], + short: Bool16[:], + word: Bool32[:], + wide: Bool64[:], +) -> None: ... +""", + module_name="boolean_widths", + ) + + assert [argument.semantic_type.name for argument in module.functions[0].arguments] == [ + "Bool", + "Bool8", + "Bool16", + "Bool32", + "Bool64", + ] + emitted = emit_module(module) + assert "from prik.contracts import Bool, Bool16, Bool32, Bool64, Bool8" in emitted + + def test_value_projection_round_trips_as_argument_specific_native_transport(): module = parse_pyi_text( """ diff --git a/tests/fortran/source_parsing/parsing/test_declaration_and_interface_edges.py b/tests/fortran/source_parsing/parsing/test_declaration_and_interface_edges.py index 5a2017dfb..700d36157 100644 --- a/tests/fortran/source_parsing/parsing/test_declaration_and_interface_edges.py +++ b/tests/fortran/source_parsing/parsing/test_declaration_and_interface_edges.py @@ -264,10 +264,9 @@ def test_local_compile_time_arithmetic_is_folded_for_shapes_and_parameters(): "1:24", "1:(8)/(3)", "1:9", - "1:+(8)-one", + "1:7", "1:8", ] - assert sig.variables["one"].value == "1" def test_type_contains_accepts_bindings_and_rejects_other_lines(): diff --git a/tests/fortran/source_parsing/parsing/test_declaration_and_scope_regressions.py b/tests/fortran/source_parsing/parsing/test_declaration_and_scope_regressions.py index 02b4cffbe..630786a00 100644 --- a/tests/fortran/source_parsing/parsing/test_declaration_and_scope_regressions.py +++ b/tests/fortran/source_parsing/parsing/test_declaration_and_scope_regressions.py @@ -66,7 +66,9 @@ def test_compile_time_resolution_helpers_preserve_kind_shape_values_and_literal_ assert parser._resolve_kind_expression("len=n + 1", {"n": "3"}) == "len=4" assert parser._resolve_symbol_reference("alias", {"alias": "target", "target": "8"}) == "8" - assert parser._resolve_module_parameter_values({"M": {"a": "4", "b": "a + 2"}}) == {"m": {"a": "4", "b": "6"}} + assert parser._resolve_module_parameter_values( + {"M": {"a": "4", "b": "a + 2", "rk": "selected_real_kind(12)", "dp": "rk"}} + ) == {"m": {"a": "4", "b": "6", "rk": "selected_real_kind(12)", "dp": "selected_real_kind(12)"}} assert parser._collect_relevant_local_params( FortranProcedureSignature( "shape", diff --git a/tests/fortran/source_parsing/parsing/test_parser_benchmarks.py b/tests/fortran/source_parsing/parsing/test_parser_benchmarks.py index 3e85a35ef..1a6b106f4 100644 --- a/tests/fortran/source_parsing/parsing/test_parser_benchmarks.py +++ b/tests/fortran/source_parsing/parsing/test_parser_benchmarks.py @@ -7,7 +7,7 @@ import pytest from prik.semantics.fortran2ir import fortran_file_to_semantic_modules -from prik.wrapper_codegen.printers import emit_module_stubs +from prik.codegen.printers import emit_module_stubs from prik import parse_fortran_file pytestmark = pytest.mark.skip(reason="Benchmarks are parked until benchmark adoption resumes.") diff --git a/tests/fortran/source_parsing/parsing/test_public_entrypoints.py b/tests/fortran/source_parsing/parsing/test_public_entrypoints.py index 7291250a9..06e00d758 100644 --- a/tests/fortran/source_parsing/parsing/test_public_entrypoints.py +++ b/tests/fortran/source_parsing/parsing/test_public_entrypoints.py @@ -1,5 +1,9 @@ """Public parser entrypoints and source/path input contracts.""" +import subprocess +import sys +from pathlib import Path + import pytest from prik.parsers.fortran.parser import FortranParser @@ -8,6 +12,21 @@ from prik.semantics.fortran2ir import fortran_file_to_semantic_modules +def test_fortran_parser_module_direct_execution_example(): + """Run the parser's documented source-to-model example from the repository root.""" + repository_root = Path(__file__).parents[4] + + result = subprocess.run( + [sys.executable, "prik/parsers/fortran/parser.py"], + cwd=repository_root, + capture_output=True, + text=True, + check=True, + ) + + assert result.stdout == "Module: metrics\nParameter: n = 4\nProcedure: scale(values: real[1])\n" + + def test_parser_public_entrypoint_aliases_and_singular_contracts_use_inline_sources(): parser = FortranParser() module_code = """ diff --git a/tests/fortran/strings/wrapper_codegen/test_character_array_lowering.py b/tests/fortran/strings/codegen/test_character_array_lowering.py similarity index 97% rename from tests/fortran/strings/wrapper_codegen/test_character_array_lowering.py rename to tests/fortran/strings/codegen/test_character_array_lowering.py index 14d966189..10670fb2c 100644 --- a/tests/fortran/strings/wrapper_codegen/test_character_array_lowering.py +++ b/tests/fortran/strings/codegen/test_character_array_lowering.py @@ -8,8 +8,8 @@ from prik.semantics.ownership import CodegenAction, NativeBarrierAction, ObjectKind from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import BridgeDataAction -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner -from prik.wrapper_codegen.plan import DatatypeFamily +from prik.codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen.plan import DatatypeFamily def _later_array_plan(): diff --git a/tests/fortran/strings/wrapper_codegen/test_fixed_string_result_lowering.py b/tests/fortran/strings/codegen/test_fixed_string_result_lowering.py similarity index 98% rename from tests/fortran/strings/wrapper_codegen/test_fixed_string_result_lowering.py rename to tests/fortran/strings/codegen/test_fixed_string_result_lowering.py index 38ccefcb0..a9cf63f1c 100644 --- a/tests/fortran/strings/wrapper_codegen/test_fixed_string_result_lowering.py +++ b/tests/fortran/strings/codegen/test_fixed_string_result_lowering.py @@ -18,8 +18,8 @@ ) from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import BridgeDataAction -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner -from prik.wrapper_codegen.plan import DatatypeFamily +from prik.codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen.plan import DatatypeFamily _COPY_REASON = "copy fixed-length Fortran character output into C-owned null-terminated storage" diff --git a/tests/fortran/strings/wrapper_codegen/test_fixed_string_writeback.py b/tests/fortran/strings/codegen/test_fixed_string_writeback.py similarity index 98% rename from tests/fortran/strings/wrapper_codegen/test_fixed_string_writeback.py rename to tests/fortran/strings/codegen/test_fixed_string_writeback.py index 52c182882..10f590254 100644 --- a/tests/fortran/strings/wrapper_codegen/test_fixed_string_writeback.py +++ b/tests/fortran/strings/codegen/test_fixed_string_writeback.py @@ -23,8 +23,8 @@ STRING_REPLACEMENT_COPY_REASON, WritebackPhase, ) -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner -from prik.wrapper_codegen.plan import BindingStatusErrorPlan, DatatypeFamily +from prik.codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen.plan import BindingStatusErrorPlan, DatatypeFamily def _fixed_writeback_module(): diff --git a/tests/fortran/strings/wrapper_codegen/test_string_input_lowering.py b/tests/fortran/strings/codegen/test_string_input_lowering.py similarity index 97% rename from tests/fortran/strings/wrapper_codegen/test_string_input_lowering.py rename to tests/fortran/strings/codegen/test_string_input_lowering.py index 4013d1137..b758737c0 100644 --- a/tests/fortran/strings/wrapper_codegen/test_string_input_lowering.py +++ b/tests/fortran/strings/codegen/test_string_input_lowering.py @@ -8,8 +8,8 @@ from prik.semantics.ownership import CodegenAction, NativeBarrierAction, PythonBarrierAction from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import ArgumentHandoffMode, BridgeDataAction -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner -from prik.wrapper_codegen.plan import DatatypeFamily +from prik.codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen.plan import DatatypeFamily def _string_input_module(): 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 e20c6b2d7..954d1a526 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,47 @@ -from prik.contracts import Int32, String, bind, external +from prik.contracts import Int32, String, bind, standalone @bind("CHAR_CODE_DEFAULT") -@external +@standalone def char_code_default( C: String[1] ) -> Int32: ... @bind("CHAR_CODE_STAR1") -@external +@standalone def char_code_star1( C: String[1] ) -> Int32: ... @bind("STRING_LEN_STAR8") -@external +@standalone def string_len_star8( TEXT: String[8] ) -> Int32: ... @bind("STRING_LEN_ASSUMED") -@external +@standalone def string_len_assumed( TEXT: String ) -> Int32: ... @bind("STRING_LEN_ENTITY") -@external +@standalone def string_len_entity( TEXT: String[6] ) -> Int32: ... @bind("CHAR_RESULT_DEFAULT") -@external +@standalone def char_result_default() -> String[1]: ... @bind("STRING_RESULT_STAR8") -@external +@standalone def string_result_star8() -> String[8]: ... @bind("STRING_RESULT_PADDED") -@external +@standalone def string_result_padded() -> String[8]: ... @bind("STRING_RESULT_DECLARED") -@external +@standalone def string_result_declared() -> String[6]: ... diff --git a/tests/fortran/subroutines/wrapper_codegen/test_hidden_scalar_outputs.py b/tests/fortran/subroutines/codegen/test_hidden_scalar_outputs.py similarity index 95% rename from tests/fortran/subroutines/wrapper_codegen/test_hidden_scalar_outputs.py rename to tests/fortran/subroutines/codegen/test_hidden_scalar_outputs.py index 9bf481f5b..33fd498e9 100644 --- a/tests/fortran/subroutines/wrapper_codegen/test_hidden_scalar_outputs.py +++ b/tests/fortran/subroutines/codegen/test_hidden_scalar_outputs.py @@ -4,14 +4,14 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def test_hidden_scalar_result_is_one_bridge_output_and_one_python_result(): module = parse_pyi_text( """ @bind("SCALE_OUT") -@external +@standalone @native_call([Addr(Arg(0)), Return("result", 0)]) def scale(x: Float64) -> Float64: ... """, @@ -40,10 +40,10 @@ def scale(x: Float64) -> Float64: ... def test_required_explicit_interface_declares_hidden_result_in_native_order(): module = parse_pyi_text( """ -from prik.contracts import Addr, Annotated, Arg, Float64, Immutable, Int32, Return, bind, external, native_call +from prik.contracts import Addr, Annotated, Arg, Float64, Immutable, Int32, Return, bind, native_call, standalone @bind("SCALE_OUT") -@external +@standalone @native_call([Addr(Arg(0)), Return("result", 0), Addr(Arg(1))]) def scale( x: Float64, diff --git a/tests/fortran/subroutines/wrapper_codegen/test_scalar_subroutine_writeback_validation.py b/tests/fortran/subroutines/codegen/test_scalar_subroutine_writeback_validation.py similarity index 97% rename from tests/fortran/subroutines/wrapper_codegen/test_scalar_subroutine_writeback_validation.py rename to tests/fortran/subroutines/codegen/test_scalar_subroutine_writeback_validation.py index 18aff5ebe..93680ce1c 100644 --- a/tests/fortran/subroutines/wrapper_codegen/test_scalar_subroutine_writeback_validation.py +++ b/tests/fortran/subroutines/codegen/test_scalar_subroutine_writeback_validation.py @@ -8,7 +8,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.semantics.policy_completion import complete_semantic_policies from prik.semantics.wrapper_policy import WritebackPhase -from prik.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner +from prik.codegen import WrapperCodeGenerator, WrapperPlanner def _artifacts(module): diff --git a/tests/shared/architecture/test_dependency_boundaries.py b/tests/shared/architecture/test_dependency_boundaries.py index cfb567dae..57e0924e9 100644 --- a/tests/shared/architecture/test_dependency_boundaries.py +++ b/tests/shared/architecture/test_dependency_boundaries.py @@ -6,12 +6,12 @@ from tests.fortran._support.wrapper_build import REPO_ROOT -WRAPPER_CODEGEN_ROOT = REPO_ROOT / "prik" / "wrapper_codegen" +CODEGEN_ROOT = REPO_ROOT / "prik" / "codegen" BOUNDARY_MODULES = ( - ("c", WRAPPER_CODEGEN_ROOT / "c" / "binding.py"), - ("fortran", WRAPPER_CODEGEN_ROOT / "fortran" / "bridge.py"), - ("printers", WRAPPER_CODEGEN_ROOT / "printers" / "pyi_printer.py"), - ("printers", WRAPPER_CODEGEN_ROOT / "printers" / "source_printers.py"), + ("c", CODEGEN_ROOT / "c" / "binding.py"), + ("fortran", CODEGEN_ROOT / "fortran" / "bridge.py"), + ("printers", CODEGEN_ROOT / "printers" / "pyi_printer.py"), + ("printers", CODEGEN_ROOT / "printers" / "source_printers.py"), ) PUBLIC_MODULE_FUNCTIONS = { ("printers", "pyi_printer.py", "emit_module"), @@ -20,7 +20,7 @@ } -def test_wrapper_codegen_boundary_entrypoints_and_visitors_are_documented(): +def test_codegen_boundary_entrypoints_and_visitors_are_documented(): """Require public entrypoints and dispatched model visitors to state their contract.""" missing = [] for _, path in BOUNDARY_MODULES: @@ -39,7 +39,7 @@ def test_wrapper_codegen_boundary_entrypoints_and_visitors_are_documented(): assert not missing, "Undocumented wrapper-codegen callables:\n" + "\n".join(missing) -def test_wrapper_codegen_uses_one_model_visitor_protocol(): +def test_codegen_uses_one_model_visitor_protocol(): """Prevent alternate printer and extractor dispatch protocols from appearing.""" invalid = [] lowercase_model_names = {"int", "str", "tuple"} diff --git a/tests/shared/architecture/test_test_suite_layout.py b/tests/shared/architecture/test_test_suite_layout.py index 75e2a1376..7c2840400 100644 --- a/tests/shared/architecture/test_test_suite_layout.py +++ b/tests/shared/architecture/test_test_suite_layout.py @@ -12,7 +12,7 @@ EXAMPLES_ROOT = REPO_ROOT / "examples" TEST_INDEX = TEST_ROOT / "README.md" WORKFLOW_ROOT = REPO_ROOT / ".github/workflows" -BLAS_LAPACK_WORKFLOW = REPO_ROOT / ".github/workflows/blas-lapack.yml" +REAL_LIBRARIES_WORKFLOW = REPO_ROOT / ".github/workflows/real-libraries.yml" CLAUDE_WORKFLOW = REPO_ROOT / ".github/workflows/claude.yml" COVERAGE_WORKFLOW = REPO_ROOT / ".github/workflows/coverage.yml" CODECOV_CONFIG = REPO_ROOT / "codecov.yml" @@ -46,7 +46,7 @@ "types", "utilities", "wrapper", - "wrapper_codegen", + "codegen", } LANGUAGE_DIRECTORIES = {"c", "fortran", "shared"} PRIMARY_OWNER_DIRECTORIES = {"architecture", *LANGUAGE_DIRECTORIES} @@ -282,7 +282,7 @@ def test_maintained_docs_do_not_name_deprecated_pytest_locations() -> None: def test_real_library_examples_have_one_dedicated_workflow() -> None: ordinary_jobs = TESTS_WORKFLOW.read_text(encoding="utf-8") - dedicated_job = BLAS_LAPACK_WORKFLOW.read_text(encoding="utf-8") + dedicated_job = REAL_LIBRARIES_WORKFLOW.read_text(encoding="utf-8") assert '-m "not real_library and not toolchain_smoke"' in ordinary_jobs assert ordinary_jobs.count('-m "not real_library and not toolchain_smoke"') == 2 @@ -290,8 +290,12 @@ def test_real_library_examples_have_one_dedicated_workflow() -> None: assert "examples/lapack" not in ordinary_jobs assert "source examples/blas/build_all.sh" in dedicated_job assert "source examples/lapack/build_all.sh" in dedicated_job + assert "source examples/fftpack/build_all.sh" in dedicated_job + assert "source examples/minpack/build_all.sh" in dedicated_job assert f"python -m pytest -q examples/blas/tests {BLAS_CI_FULL_SURFACE}" in dedicated_job assert f"python -m pytest -q examples/lapack/tests {LAPACK_CI_FULL_SURFACE}" in dedicated_job + assert "python -m pytest -q examples/fftpack/tests" in dedicated_job + assert "python -m pytest -q examples/minpack/tests" in dedicated_job assert BLAS_CI_FULL_SURFACE in dedicated_job assert LAPACK_CI_FULL_SURFACE in dedicated_job assert '"meson==1.11.2"' in dedicated_job @@ -340,7 +344,7 @@ def test_codecov_keeps_project_coverage_blocking_and_patch_coverage_informationa def test_active_github_action_checks_use_distinct_workflow_scopes_and_job_names() -> None: workflow_names = { - BLAS_LAPACK_WORKFLOW: "Native Libraries", + REAL_LIBRARIES_WORKFLOW: "Real Libraries", CLAUDE_WORKFLOW: "Repository Automation", COVERAGE_WORKFLOW: "Quality Metrics", DOCS_WORKFLOW: "Documentation", @@ -351,7 +355,9 @@ def test_active_github_action_checks_use_distinct_workflow_scopes_and_job_names( TESTS_WORKFLOW: "Test Matrix", } expected = { - BLAS_LAPACK_WORKFLOW: {"real-library-wrappers": "BLAS + LAPACK · Ubuntu 24.04 · Python 3.12"}, + REAL_LIBRARIES_WORKFLOW: { + "real-library-wrappers": "BLAS + LAPACK + FFTPACK + MINPACK · Ubuntu 24.04 · Python 3.12" + }, CLAUDE_WORKFLOW: {"claude": "Claude Code response to mention"}, COVERAGE_WORKFLOW: {"coverage": "Project coverage · Ubuntu 24.04 · Python 3.12"}, DOCS_WORKFLOW: { @@ -369,7 +375,7 @@ def test_active_github_action_checks_use_distinct_workflow_scopes_and_job_names( "compiler-smoke-macos": "Compiler smoke · macOS 15 ARM64 · LLVM Flang · Python 3.12", "unit-tests": "${{ matrix.display_name }}", "unit-tests-macos": "Unit tests · macOS 15 ARM64 · Python 3.12", - "native-libraries": "BLAS + LAPACK · Ubuntu 24.04 · Python 3.12", + "native-libraries": "BLAS + LAPACK + FFTPACK + MINPACK · Ubuntu 24.04 · Python 3.12", "documentation-benchmark": "Documentation performance benchmark · Ubuntu 24.04 ARM64 · Python 3.12", "documentation-build": "Documentation site build · Ubuntu 24.04 · Python 3.12", "merge-gate": "Validation · all required checks", @@ -482,7 +488,7 @@ def test_purpose_specific_workflows_do_not_create_duplicate_pull_request_runs() for workflow in ( TESTS_WORKFLOW, STATIC_ANALYSIS_WORKFLOW, - BLAS_LAPACK_WORKFLOW, + REAL_LIBRARIES_WORKFLOW, FORTRAN_SMOKE_WORKFLOW, COVERAGE_WORKFLOW, DOCS_WORKFLOW, @@ -500,7 +506,7 @@ def test_pull_request_jobs_share_the_reviewed_component_workflow_steps() -> None ("compiler-smoke", FORTRAN_SMOKE_WORKFLOW, "toolchain-smoke"), ("compiler-smoke-macos", FORTRAN_SMOKE_WORKFLOW, "macos-flang-smoke"), ("unit-tests-macos", TESTS_WORKFLOW, "macos"), - ("native-libraries", BLAS_LAPACK_WORKFLOW, "real-library-wrappers"), + ("native-libraries", REAL_LIBRARIES_WORKFLOW, "real-library-wrappers"), ("documentation-benchmark", DOCS_WORKFLOW, "benchmark"), ): assert _github_action_job_steps(MERGE_VALIDATION_WORKFLOW, pull_request_job) == _github_action_job_steps( @@ -594,6 +600,10 @@ def test_pull_request_and_main_generate_equivalent_documentation_performance_sna "name: performance-snapshot", "benchmarks/results/f2py.json", "benchmarks/results/prik.json", + "benchmarks/results/f2py-prik-first.json", + "benchmarks/results/f2py-f2py-first.json", + "benchmarks/results/prik-prik-first.json", + "benchmarks/results/prik-f2py-first.json", "benchmarks/results/f2py-build.json", "benchmarks/results/prik-build.json", "uses: actions/download-artifact@v4", diff --git a/tests/shared/architecture/test_visitor_protocol.py b/tests/shared/architecture/test_visitor_protocol.py index 825f6e10b..34a10a3b1 100644 --- a/tests/shared/architecture/test_visitor_protocol.py +++ b/tests/shared/architecture/test_visitor_protocol.py @@ -12,11 +12,11 @@ from prik.semantics.fortran2ir import FortranToIRConverter, _FortranVariableContextVisitor from prik.semantics.pyi2ir import _ClassBodyVisitor, _ModuleVisitor from prik.utilities.visitor import ClassVisitor as SemanticClassVisitor -from prik.wrapper_codegen.c.binding import CBindingGenerator -from prik.wrapper_codegen.fortran.bridge import FortranBridgeGenerator -from prik.wrapper_codegen.planner import WrapperPlanner -from prik.wrapper_codegen.printers import PyiPrinter -from prik.wrapper_codegen.visitor import ClassVisitor as WrapperClassVisitor +from prik.codegen.c.binding import CBindingGenerator +from prik.codegen.fortran.bridge import FortranBridgeGenerator +from prik.codegen.planner import WrapperPlanner +from prik.codegen.printers import PyiPrinter +from prik.codegen.visitor import ClassVisitor as WrapperClassVisitor SEMANTIC_VISITORS = ( @@ -38,10 +38,10 @@ REPO_ROOT / "prik" / "semantics" / "c2ir.py", REPO_ROOT / "prik" / "semantics" / "fortran2ir.py", REPO_ROOT / "prik" / "semantics" / "pyi2ir.py", - REPO_ROOT / "prik" / "wrapper_codegen" / "planner.py", - REPO_ROOT / "prik" / "wrapper_codegen" / "c" / "binding.py", - REPO_ROOT / "prik" / "wrapper_codegen" / "fortran" / "bridge.py", - REPO_ROOT / "prik" / "wrapper_codegen" / "printers" / "pyi_printer.py", + REPO_ROOT / "prik" / "codegen" / "planner.py", + REPO_ROOT / "prik" / "codegen" / "c" / "binding.py", + REPO_ROOT / "prik" / "codegen" / "fortran" / "bridge.py", + REPO_ROOT / "prik" / "codegen" / "printers" / "pyi_printer.py", ) diff --git a/tests/shared/docs/test_examples.py b/tests/shared/docs/test_examples.py index da6b1278d..140ea7c63 100644 --- a/tests/shared/docs/test_examples.py +++ b/tests/shared/docs/test_examples.py @@ -21,7 +21,9 @@ DOC_PATHS = [ ROOT / "README.md", ROOT / "examples/blas/README.md", + ROOT / "examples/fftpack/README.md", ROOT / "examples/lapack/README.md", + ROOT / "examples/minpack/README.md", *sorted(path for path in (ROOT / "docs").rglob("*.md") if "old_docs" not in path.parts), ] AUDITED_PYTHON_DOC_PATHS = [ diff --git a/tests/shared/docs/test_structure.py b/tests/shared/docs/test_structure.py index 55b13f21c..62d3043de 100644 --- a/tests/shared/docs/test_structure.py +++ b/tests/shared/docs/test_structure.py @@ -282,13 +282,13 @@ "prik/semantics/pyi2ir.py", "prik/pipeline/pyi.py", "prik/semantics/policy_completion.py", - "prik/wrapper_codegen/plan.py", - "prik/wrapper_codegen/planner.py", - "prik/wrapper_codegen/generator.py", - "prik/wrapper_codegen/c/binding.py", - "prik/wrapper_codegen/fortran/bridge.py", - "prik/wrapper_codegen/printers/pyi_printer.py", - "prik/wrapper_codegen/printers/source_printers.py", + "prik/codegen/plan.py", + "prik/codegen/planner.py", + "prik/codegen/generator.py", + "prik/codegen/c/binding.py", + "prik/codegen/fortran/bridge.py", + "prik/codegen/printers/pyi_printer.py", + "prik/codegen/printers/source_printers.py", "prik/compiling/objects.py", "prik/compiling/compilers.py", "prik/compiling/native_support.py", @@ -411,10 +411,16 @@ for path in sorted((DOCS_ROOT / "user/examples").rglob("*.md")) if path.name != "index.md" ] +REAL_LIBRARY_EXAMPLE_PAGES = [ + "user/examples/blas-wrapper.md", + "user/examples/lapack-wrapper.md", + "user/examples/fftpack-wrapper.md", + "user/examples/minpack-wrapper.md", +] MAJOR_SOURCE_PACKAGES = [ "prik/parsers/", "prik/semantics/", - "prik/wrapper_codegen/", + "prik/codegen/", "prik/compiling/", ] PACKAGE_READMES = [ @@ -1015,6 +1021,32 @@ def test_reviewed_user_pages_do_not_contain_editorial_notes(relative_path: str) assert phrase not in page +@pytest.mark.parametrize("relative_path", REAL_LIBRARY_EXAMPLE_PAGES) +def test_real_library_examples_share_a_user_facing_structure(relative_path: str) -> None: + page = _visible_documentation_source(DOCS_ROOT / relative_path) + common_sections = [ + "### What this example shows", + "## Versions used", + "## 1. Prepare the repository and toolchain", + "## 4. Run the complete test suite", + "## 5. See how results are validated", + "## 6. Run focused examples", + "## Troubleshooting", + "## Source provenance", + ] + + positions = [page.index(section) for section in common_sections] + assert positions == sorted(positions) + for internal_phrase in ( + "stopping after a successful import", + "fail-closed", + "authoritative public classification", + "complete maintained suite", + "machine constants", + ): + assert internal_phrase not in page + + def test_getting_started_overview_uses_standalone_example() -> None: overview = (DOCS_ROOT / "user/getting-started/index.md").read_text(encoding="utf-8") introduction_index = overview.index("you will create\n`scale.f90`") @@ -1027,53 +1059,164 @@ def test_getting_started_overview_uses_standalone_example() -> None: def test_documentation_homepage_demonstrates_prik_before_getting_started() -> None: page = (DOCS_ROOT / "index.md").read_text(encoding="utf-8") - introduction_index = page.index("Turn Fortran into natural Python APIs") - build_index = page.index("python3 -m prik points.f90 --out geometry") - example_heading_index = page.index("## See it in action") + introduction_index = page.index("Generate native Python bindings for Fortran") + example_heading_index = page.index("## From Fortran to Python in one command") source_index = page.index("```fortran", example_heading_index) - generated_api_index = page.index("**Generated Python API:**", source_index) - constructor_index = page.index("item = points.point(", generated_api_index) - mutation_index = page.index("points.move(item", constructor_index) - result_index = page.index("# 4.0 2.0", mutation_index) - contract_index = page.index("Edit the generated `.pyi` contract", result_index) - features_index = page.index("## Key Features", contract_index) - getting_started_index = page.index("Getting Started Guide →", features_index) - - assert introduction_index < build_index < example_heading_index < source_index - assert source_index < generated_api_index < constructor_index < mutation_index - assert mutation_index < result_index < contract_index < features_index < getting_started_index - assert "print(points.norm_squared(item))" in page - assert "# 20.0" in page - assert "[contract guide](user/reference/pyi-contracts/index.md)" in page - assert "{ .prik-primary-cta }" in page + build_index = page.index("python3 -m prik scale.f90", source_index) + import_index = page.index("import scale", build_index) + call_index = page.index("scale.scale(np.float64(3.0), np.float64(2.5))", import_index) + result_index = page.index("# 7.5", call_index) + advantages_index = page.index("## Why PRIK", result_index) + evidence_index = page.index("## Proven on real Fortran libraries", advantages_index) + performance_index = page.index("## Measured against NumPy's f2py", evidence_index) + runtime_chart_index = page.index("user/assets/performance-comparison.svg", performance_index) + build_chart_index = page.index("user/assets/build-time-comparison.svg", runtime_chart_index) + methodology_index = page.index("[See the benchmark machine, full results, and methodology →]", build_chart_index) + install_index = page.index("[Install PRIK →]", methodology_index) + getting_started_index = page.index("[Read Getting Started →]", install_index) + + assert introduction_index < example_heading_index < source_index < build_index + assert build_index < import_index < call_index < result_index + assert result_index < advantages_index < evidence_index < performance_index + assert performance_index < runtime_chart_index < build_chart_index + assert build_chart_index < methodology_index < install_index < getting_started_index + assert "python3 -m pip install prik" in page + assert "No manual binding code is required." in page + assert "[BLAS](user/examples/blas-wrapper.md)" in page + assert "[LAPACK](user/examples/lapack-wrapper.md)" in page + assert "[FFTPACK](user/examples/fftpack-wrapper.md)" in page + assert "[MINPACK](user/examples/minpack-wrapper.md)" in page + assert "The charts show the current published snapshot" in page + assert "specific to its machine and toolchain" in page + assert "PRIK was faster in" not in page + assert page.count("{ .prik-performance-chart }") == 2 + assert page.count("{ .prik-primary-cta }") == 2 assert "developer/index.md" not in page assert "maintainer/README.md" not in page assert "user/guide/" not in page -def test_readme_opening_uses_the_homepage_message_and_showcase() -> None: +def test_public_entrypoints_use_the_canonical_identity_and_description() -> None: homepage = _visible_documentation_source(DOCS_ROOT / "index.md") readme = _visible_documentation_source(ROOT / "README.md") - readme_opening = readme.split("## Installation & Quick Start", maxsplit=1)[0] - shared_content = ( - "python3 -m prik points.f90 --out geometry", - "", - "import geometry.points as points", - "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(points.norm_squared(item)) # 20.0", - "No manual bindings are required.", - "## Key Features", - "- Editable `.pyi` contracts and readable generated docstrings", + mkdocs = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8") + title = "PRIK — Python Runtime Interop Kit" + subtitle = "Generate native Python bindings for Fortran, with editable `.pyi` contracts\nand Pythonic APIs." + description = ( + "PRIK generates native Python bindings from Fortran projects, producing\n" + "importable extensions and editable `.pyi` contracts for Pythonic APIs." ) + language_direction = ( + "**PRIK starts with Fortran-to-Python.** Its semantic contract model is designed\n" + "to support more native languages over time." + ) + metadata_description = description.replace("\n", " ").replace("`", "") + + assert readme.startswith(f"# {title}\n\n**{subtitle}**\n\n{description}\n") + assert f"# {title}\n\n**{subtitle}**\n\n{description}\n" in homepage + assert language_direction in readme + assert language_direction in homepage + assert f"site_name: {title}" in mkdocs + assert f"site_description: {metadata_description}" in mkdocs + assert f'description = "{metadata_description}"' in pyproject + + +def test_faq_routes_search_questions_to_authoritative_pages() -> None: + metadata, page = _front_matter(DOCS_ROOT / "user/faq/index.md") + question_targets = [ + ( + "How do I call Fortran from Python?", + "how-do-i-call-fortran-from-python", + "../getting-started/first-wrapped-function.md", + ), + ( + "How do I generate Python bindings for a Fortran module?", + "how-do-i-generate-python-bindings-for-a-fortran-module", + "../getting-started/first-wrapped-module.md", + ), + ( + "How do I wrap an existing Fortran library for Python?", + "how-do-i-wrap-an-existing-fortran-library-for-python", + "../guide/building-shared-library.md", + ), + ( + "How do I expose Fortran derived types as Python classes?", + "how-do-i-expose-fortran-derived-types-as-python-classes", + "../guide/wrapping-derived-types.md", + ), + ( + "How do I pass NumPy arrays to Fortran without unnecessary copies?", + "how-do-i-pass-numpy-arrays-to-fortran-without-unnecessary-copies", + "../guide/arrays.md", + ), + ("Should I use PRIK or f2py?", "should-i-use-prik-or-f2py", "../performance.md"), + ] - for content in shared_content: - assert content in homepage - assert content in readme_opening - - assert readme.count("\nmodule points\n") == 1 - assert readme_opening.count("python3 -m prik points.f90 --out geometry") == 1 - assert readme_opening.count("import geometry.points as points") == 1 + assert metadata["status"] == "maintained" + assert metadata["publication"] == "reviewed" + previous_start = -1 + for question, anchor_id, target in question_targets: + start_marker = f'
' + start = page.index(start_marker) + end = page.index("
", start) + answer = page[start:end] + assert previous_start < start + assert f"{question}" in answer + assert target in answer + previous_start = start + + assert page.count('
None: + configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + stylesheet = (DOCS_ROOT / "stylesheets/site.css").read_text(encoding="utf-8") + script = (DOCS_ROOT / "javascripts/faq.js").read_text(encoding="utf-8") + + assert "- md_in_html" in configuration + assert "- javascripts/faq.js" in configuration + assert ".prik-faq-item summary" in stylesheet + assert ".prik-faq-item:target" in stylesheet + assert 'target.matches("details.prik-faq-item")' in script + assert "target.open = true" in script + assert 'window.addEventListener("hashchange", openLinkedQuestion)' in script + + +def test_performance_page_bounds_the_prik_f2py_decision() -> None: + page = _visible_documentation_source(DOCS_ROOT / "user/performance.md") + start = page.index("## Should I use PRIK or f2py?") + end = page.index("## Fair, Like-for-Like Setup", start) + comparison = " ".join(page[start:end].split()) + + assert "runtime-call overhead and clean build time" in comparison + assert "They do not rank feature coverage" in comparison + assert "https://numpy.org/doc/stable/f2py/" in comparison + assert "https://numpy.org/doc/stable/f2py/signature-file.html" in comparison + assert "design the Python API, not just generate a wrapper" in comparison + assert "simpler, more Pythonic place to rename or hide exports, flatten modules" in comparison + assert "reorder or hide native arguments, and return native outputs as Python results" in comparison + assert "[NumPy arrays](guide/arrays.md) as complete API contracts" in comparison + assert "dtype, rank, shape, memory layout, contiguity, strides, mutation, and copy behavior" in comparison + assert "[supported positive-stride views](guide/arrays.md#strided-views) without copying" in comparison + assert "[derived types](guide/wrapping-derived-types.md) as Python classes" in comparison + assert "[allocatables](guide/allocatables.md)" in comparison + assert "[pointer forms](guide/pointers.md)" in comparison + assert "native errors as [Python exceptions](guide/error-handling.md)" in comparison + assert "[overloaded procedures](guide/generic-interfaces.md)" in comparison + assert "reference/pyi-contracts/index.md" in comparison + assert "PRIK is currently alpha" in comparison def test_documentation_links_to_documentation_stay_on_the_website() -> None: @@ -1120,7 +1263,7 @@ def test_first_wrapped_function_shows_contract_and_mentions_later_support_bounda build_index = page.index("python3 -m prik scale.f90") command_index = page.index("python3 -m prik generate --pyi scale.f90") contract_index = page.index( - "@external\n@native_call([Addr(Arg(0)), Addr(Arg(1))])\ndef scale(\n" + "@standalone\n@native_call([Addr(Arg(0)), Addr(Arg(1))])\ndef scale(\n" " value: Float64,\n factor: Float64\n) -> Float64: ..." ) docstring_index = page.index("## Inspect the Generated Docstring") @@ -1209,6 +1352,21 @@ def test_user_guide_teaches_small_contract_edits_in_context() -> None: assert '@raises(status="status", message="message", success=0)' in errors +def test_array_guide_routes_advanced_shape_expressions_to_reference() -> None: + guide = _visible_documentation_source(DOCS_ROOT / "user/guide/arrays.md") + reference = _visible_documentation_source(DOCS_ROOT / "user/reference/pyi-contracts/calls-and-results.md") + + assert "Generated contracts may describe a shape with visible arguments" in guide + assert "../reference/pyi-contracts/calls-and-results.md#advanced-array-shape-expressions" in guide + assert "## Advanced Array Shape Expressions" in reference + assert "Native relationship" in reference + assert "extent_for(n)" in reference + + assert "The bridge emits the signature as an abstract Fortran interface" not in guide + assert "A specification function must be pure" not in guide + assert "compiler-produced `.mod` interfaces" not in guide + + def test_user_guide_keeps_generated_docstrings_with_new_overload_and_class_features() -> None: modules = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-modules.md") generics = _visible_documentation_source(DOCS_ROOT / "user/guide/generic-interfaces.md") @@ -1271,11 +1429,11 @@ def test_user_compiler_docs_distinguish_runtime_evidence_from_configured_profile assert "keeps both compilers in the same family" in shared_library -def test_cli_reference_reuses_the_homepage_points_example() -> None: +def test_cli_reference_reuses_the_derived_type_points_example() -> None: content = _visible_documentation_source(CLI_REFERENCE_PATH) - assert "`points.f90` source and naming" in content - assert "../../index.md#see-it-in-action" in content + assert "`points.f90` and `geometry` naming" in content + assert "../guide/wrapping-derived-types.md#complete-example" in content assert "python3 -m prik parse points.f90" in content assert "python3 -m prik generate --pyi points.f90 --out contracts" in content assert "scale.f90" not in content @@ -1318,6 +1476,19 @@ def test_array_handle_docs_keep_views_copies_and_handles_distinct() -> None: assert "Fortran module owns their storage" in memory +def test_parameter_array_references_document_read_only_snapshots() -> None: + references = [ + _visible_documentation_source(DOCS_ROOT / "user/reference/fortran-wrapper.md"), + _visible_documentation_source(DOCS_ROOT / "user/reference/generated-modules.md"), + ] + + for reference in references: + normalized_reference = " ".join(reference.split()) + assert "Python-owned" in normalized_reference + assert "read-only NumPy snapshots" in normalized_reference + assert "no native setter" in normalized_reference + + @pytest.mark.parametrize("heading", CLI_HELP_GROUP_HEADINGS) def test_cli_help_uses_documented_option_groups(heading: str) -> None: assert heading in _prik_cli_help() diff --git a/tests/shared/tools/test_build_time_benchmark.py b/tests/shared/tools/test_build_time_benchmark.py index 9f0a3b558..946fccc5c 100644 --- a/tests/shared/tools/test_build_time_benchmark.py +++ b/tests/shared/tools/test_build_time_benchmark.py @@ -72,6 +72,13 @@ def test_tool_order_alternates_between_rounds() -> None: assert build_time.tool_order("f2py", 1) == ("prik", "f2py") +def test_build_benchmark_defaults_to_four_measured_rounds() -> None: + args = build_time.parse_args([]) + + assert args.runs == 4 + assert args.warmups == 1 + + def test_timed_build_excludes_post_build_import_verification(tmp_path: Path, monkeypatch) -> None: workload = build_time.BuildWorkload("test", (), (), ()) case = build_time.BuildCase(build_time.BUILD_PROFILES[0], workload) diff --git a/tests/shared/tools/test_generate_performance_docs.py b/tests/shared/tools/test_generate_performance_docs.py index 4024459d0..4d202ab4d 100644 --- a/tests/shared/tools/test_generate_performance_docs.py +++ b/tests/shared/tools/test_generate_performance_docs.py @@ -27,6 +27,7 @@ "perf_version": "2.10.0", "platform_details": "Linux-test-x86_64", "python_version": "3.12.11 (test build)", + "runtime_order_protocol": "balanced_ab_ba", "unit": "second", } TEST_OS = "Test Linux 1.0" @@ -81,7 +82,7 @@ def _paired_suites(tmp_path: Path) -> tuple[Path, Path]: def _paired_build_suites(tmp_path: Path) -> tuple[Path, Path]: metadata = { "build_profiles": "development:-O0;optimized:-O3 -march=native -mtune=native", - "build_runs": 6, + "build_runs": 4, "build_scope": "clean source-to-extension generation, compilation, and linking", "build_warmups": 1, "compiler": "/usr/bin/gfortran", @@ -191,7 +192,8 @@ def test_render_page_updates_only_marked_blocks(tmp_path: Path) -> None: assert "Development (`-O0`) · full reference BLAS (155 sources)" in rendered assert "Optimized (`-O3 -march=native -mtune=native`) · small module" in rendered assert "Optimized (`-O3 -march=native -mtune=native`) · full reference BLAS" in rendered - assert "mean of 6 clean builds after 1 untimed warm-up" in rendered + assert "mean of 4 clean builds after 1 untimed warm-up" in rendered + assert "equal PRIK-first and f2py-first process budgets" in rendered assert "up to 4 concurrent compiler" in rendered assert "f2py uses its normal Meson/Ninja scheduler" in rendered assert "private-runner-name" not in rendered diff --git a/tests/shared/tools/test_runtime_benchmark.py b/tests/shared/tools/test_runtime_benchmark.py index c74020596..835c4a4c7 100644 --- a/tests/shared/tools/test_runtime_benchmark.py +++ b/tests/shared/tools/test_runtime_benchmark.py @@ -3,6 +3,8 @@ import importlib from pathlib import Path import runpy +import subprocess +import sys from types import SimpleNamespace import pyperf @@ -15,18 +17,18 @@ @pytest.mark.parametrize( ("group", "processes", "values", "expected_names"), [ - ("calls", 64, 4, ("call.noop", "call.add_scalars")), - ("vector-latency", 64, 4, ("array.increment_vector.n=1", "array.increment_vector.n=16")), - ("vector-bulk", 16, 3, ("array.increment_vector.n=1024", "array.increment_vector.n=1000000")), - ("matrix-sum-latency", 64, 4, ("matrix.sum.4x4.order=F",)), + ("calls", 16, 4, ("call.noop", "call.add_scalars")), + ("vector-latency", 16, 4, ("array.increment_vector.n=1", "array.increment_vector.n=16")), + ("vector-bulk", 4, 3, ("array.increment_vector.n=1024", "array.increment_vector.n=1000000")), + ("matrix-sum-latency", 16, 4, ("matrix.sum.4x4.order=F",)), ( "matrix-sum-bulk", - 4, + 2, 3, ("matrix.sum.32x32.order=F", "matrix.sum.256x256.order=F", "matrix.sum.1024x1024.order=F"), ), - ("matrix-update-latency", 64, 4, ("matrix.update.4x4.order=F", "matrix.update.256x256.order=F")), - ("matrix-update-bulk", 32, 3, ("matrix.update.1024x1024.order=F",)), + ("matrix-update-latency", 16, 4, ("matrix.update.4x4.order=F", "matrix.update.256x256.order=F")), + ("matrix-update-bulk", 8, 3, ("matrix.update.1024x1024.order=F",)), ], ) def test_runtime_groups_assign_more_samples_only_to_noisy_cases( @@ -54,6 +56,7 @@ def timeit(self, name: str, **_kwargs) -> None: ) monkeypatch.setenv("BINDING_TOOL", "prik") monkeypatch.setenv("PRIK_RUNTIME_BENCHMARK_GROUP", group) + monkeypatch.setenv("PRIK_RUNTIME_ORDER_PASS", "prik-first") monkeypatch.setenv("PRIK_BENCHMARK_CPU_MODEL", "Published Benchmark CPU") monkeypatch.setattr(importlib, "import_module", lambda _name: SimpleNamespace(kernels=kernels)) monkeypatch.setattr(pyperf, "Runner", FakeRunner) @@ -63,10 +66,12 @@ def timeit(self, name: str, **_kwargs) -> None: assert observed["processes"] == processes assert observed["values"] == values assert observed["metadata"]["cpu_model_name"] == "Published Benchmark CPU" + assert observed["metadata"]["runtime_order_pass"] == "prik-first" + assert observed["metadata"]["runtime_order_protocol"] == "balanced_ab_ba" assert observed["names"] == list(expected_names) -def test_run_script_appends_runtime_groups_in_public_table_order() -> None: +def test_run_script_balances_reduced_runtime_budget_in_public_table_order() -> None: source = Path("benchmarks/run.sh").read_text(encoding="utf-8") positions = [ @@ -82,5 +87,63 @@ def test_run_script_appends_runtime_groups_in_public_table_order() -> None: ) ] assert positions == sorted(positions) - assert 'result_args=(--append "results/$binding_tool.json")' in source + assert source.index('for runtime_group in "${runtime_groups[@]}"') < source.index( + 'for runtime_pass in "${runtime_passes[@]}"' + ) + assert "runtime_passes=(prik-first f2py-first)" in source + assert "binding_tools=(prik f2py)" in source + assert "binding_tools=(f2py prik)" in source + assert 'PRIK_RUNTIME_ORDER_PASS="$runtime_pass"' in source + assert "results/$binding_tool-$runtime_pass.json" in source + assert '--add "results/$binding_tool-f2py-first.json"' in source + assert '--output "results/$binding_tool.json"' in source + assert "PRIK_BUILD_BENCHMARK_RUNS:-4" in source assert "PRIK_BENCHMARK_CPU_MODEL" in source + + +def test_pyperf_merge_preserves_both_runtime_order_passes(tmp_path: Path) -> None: + pass_paths = [] + for index, order_pass in enumerate(("prik-first", "f2py-first"), 1): + run = pyperf.Run( + [float(index), float(index) + 0.1], + warmups=[(1, float(index))], + metadata={ + "name": "call.noop", + "unit": "second", + "loops": 1, + "binding_tool": "prik", + "runtime_order_pass": order_pass, + "runtime_order_protocol": "balanced_ab_ba", + }, + ) + suite = pyperf.BenchmarkSuite([pyperf.Benchmark([run])]) + pass_path = tmp_path / f"{order_pass}.json" + suite.dump(str(pass_path), compact=False) + pass_paths.append(pass_path) + + merged_path = tmp_path / "merged.json" + subprocess.run( + [ + sys.executable, + "-m", + "pyperf", + "convert", + str(pass_paths[0]), + "--add", + str(pass_paths[1]), + "--output", + str(merged_path), + ], + check=True, + capture_output=True, + text=True, + ) + + merged = pyperf.BenchmarkSuite.load(str(merged_path)).get_benchmark("call.noop") + assert len(merged.get_runs()) == 2 + assert merged.get_nvalue() == 4 + assert merged.get_metadata()["runtime_order_protocol"] == "balanced_ab_ba" + assert {run.get_metadata()["runtime_order_pass"] for run in merged.get_runs()} == { + "prik-first", + "f2py-first", + } diff --git a/tests/shared/types/test_mapping_report.py b/tests/shared/types/test_mapping_report.py index c4943dbfc..14d291a67 100644 --- a/tests/shared/types/test_mapping_report.py +++ b/tests/shared/types/test_mapping_report.py @@ -1,6 +1,9 @@ """Target-specific datatype mapping report tests.""" import shutil +import subprocess +import sys +from pathlib import Path import pytest @@ -99,3 +102,21 @@ def test_character_mapping_fact_is_modeled_without_compiler_probe_metadata(): semantic_type = type("SemanticType", (), {"metadata": {}})() assert type_mapping_report._fortran_fact_text(semantic_type, ("character", "c_char")) == "8-bit storage" + + +def test_type_mapping_report_direct_script_runs_its_no_argument_example(): + if shutil.which("cc") is None: + pytest.skip("cc is required for the direct type-mapping example") + + completed = subprocess.run( + [sys.executable, "prik/probes/report.py"], + cwd=Path(__file__).resolve().parents[3], + capture_output=True, + text=True, + check=True, + ) + + row = completed.stdout.strip() + assert row.startswith("| `int` | ") + assert "signed" in row + assert "numpy." in row diff --git a/tests/shared/types/test_numpy.py b/tests/shared/types/test_numpy.py index 76dca5dcf..3888add3a 100644 --- a/tests/shared/types/test_numpy.py +++ b/tests/shared/types/test_numpy.py @@ -5,6 +5,8 @@ from prik.semantics.models import SemanticType from prik.types.numpy import ( SEMANTIC_DTYPE_TO_NUMPY_DTYPE, + boolean_storage_bits, + is_boolean_semantic_type_name, numpy_dtype_expression, semantic_dtype_to_numpy_dtype, semantic_dtype_to_numpy_dtype_map, @@ -15,6 +17,10 @@ def test_semantic_dtype_to_numpy_dtype_dictionary_uses_resolved_widths(): assert SEMANTIC_DTYPE_TO_NUMPY_DTYPE == { "Bool": "numpy.bool_", + "Bool8": "numpy.bool_", + "Bool16": "numpy.bool_", + "Bool32": "numpy.bool_", + "Bool64": "numpy.bool_", "Int8": "numpy.int8", "Int16": "numpy.int16", "Int32": "numpy.int32", @@ -34,6 +40,14 @@ def test_semantic_dtype_to_numpy_dtype_dictionary_uses_resolved_widths(): "SizeT": "numpy.uintp", } assert "Int" not in SEMANTIC_DTYPE_TO_NUMPY_DTYPE + assert all(is_boolean_semantic_type_name(name) for name in ("Bool", "Bool8", "Bool16", "Bool32", "Bool64")) + assert [boolean_storage_bits(name) for name in ("Bool", "Bool8", "Bool16", "Bool32", "Bool64")] == [ + 8, + 8, + 16, + 32, + 64, + ] def test_numpy_dtype_expression_rejects_unresolved_or_unknown_semantic_dtypes(): diff --git a/tools/check_wrapper_codegen_complexity.py b/tools/check_codegen_complexity.py similarity index 75% rename from tools/check_wrapper_codegen_complexity.py rename to tools/check_codegen_complexity.py index 68d7e3741..2f683bc70 100644 --- a/tools/check_wrapper_codegen_complexity.py +++ b/tools/check_codegen_complexity.py @@ -4,13 +4,13 @@ from pathlib import Path -from prik.wrapper_codegen.checks import check_wrapper_codegen_package +from prik.codegen.checks import check_codegen_package def main() -> int: """Print checker violations and return a process status for automation.""" - package_root = Path(__file__).resolve().parents[1] / "prik" / "wrapper_codegen" - violations = check_wrapper_codegen_package(package_root) + package_root = Path(__file__).resolve().parents[1] / "prik" / "codegen" + violations = check_codegen_package(package_root) for violation in violations: print(violation.label) return int(bool(violations)) diff --git a/tools/generate_performance_docs.py b/tools/generate_performance_docs.py index 41b781b9b..5684e2014 100644 --- a/tools/generate_performance_docs.py +++ b/tools/generate_performance_docs.py @@ -39,6 +39,7 @@ "perf_version", "platform_details", "python_version", + "runtime_order_protocol", ) BUILD_SHARED_METADATA = ( "build_profiles", @@ -401,6 +402,7 @@ def _environment_markdown(snapshot: PerformanceSnapshot, build_snapshot: Perform "- Both interfaces keep the GIL held.", "- OpenMP, OpenBLAS, and MKL are limited to one thread.", f"- `pyperf --rigorous` pins each benchmark to logical CPU `{affinity}`.", + "- Runtime samples combine equal PRIK-first and f2py-first process budgets.", f"- PRIK build timings use up to {int(build_snapshot.metadata['prik_build_jobs'])} concurrent compiler", " processes; f2py uses its normal Meson/Ninja scheduler.", "- Build timings alternate tool order, use clean output directories, and exclude", diff --git a/tools/wrapper_plan_staged_walkthrough.py b/tools/wrapper_plan_staged_walkthrough.py index a6e94a57e..bbb8ccffe 100644 --- a/tools/wrapper_plan_staged_walkthrough.py +++ b/tools/wrapper_plan_staged_walkthrough.py @@ -16,7 +16,7 @@ from prik.pipeline.preprocessing import PreprocessingConfig from prik.semantics.fortran2ir import fortran_project_to_semantic_modules from prik.semantics.policy_completion import complete_semantic_policies -from prik.wrapper_codegen import ( +from prik.codegen import ( CBindingGenerator, FortranBridgeGenerator, WrapperCodeGenerator, @@ -42,10 +42,10 @@ """ DEMO_PYI_CONTRACT = """\ -from prik.contracts import Addr, Arg, Float64, bind, external, native_call +from prik.contracts import Addr, Arg, Float64, bind, native_call, standalone @bind("ADD_R8") -@external +@standalone @native_call([Addr(Arg(0)), Addr(Arg(1))]) def calculate(x: Float64, y: Float64) -> Float64: ... """ @@ -104,13 +104,14 @@ def calculate(x: Float64, y: Float64) -> Float64: ... f"binding action={argument.binding.optional_mode!r}, " f"bridge action={argument.bridge.optional_mode!r}" ) - if planned_function.result is None: + if not planned_function.results: print(" result: binding=, bridge=") else: - print( - f" result: binding action={planned_function.result.binding.codegen_action!r}, " - f"bridge action={planned_function.result.bridge.codegen_action!r}" - ) + for result in planned_function.results: + print( + f" result {result.result_position}: binding action={result.binding.codegen_action!r}, " + f"bridge action={result.bridge.codegen_action!r}" + ) for variable in item.variables: print( f" variable {variable.binding.python_names}: " @@ -119,7 +120,7 @@ def calculate(x: Float64, y: Float64) -> Float64: ... f"assignment={variable.bridge.native_assignment!r}" ) print("native slots:", function.native_call_slots) -print("result plan:", function.result) +print("result plans:", function.results) # 3. Editable plan -> generated C binding and Fortran bridge sources. @@ -137,10 +138,14 @@ def calculate(x: Float64, y: Float64) -> Float64: ... print("\n== BUILD AND RUN ==") build_dir = workdir / "build" build_dir.mkdir() -compiler = pipeline._new_gnu_compiler() +compiler = pipeline._new_compiler() native_object = pipeline._source_compile_object(source, build_dir, object_stem="native") compiler.compile_object(native_object, verbose=False) -native_build_plan = pipeline._source_native_build_plan((source,), (native_object,), module_dir=build_dir) +native_build_plan = pipeline.NativeBuildPlan( + produced_objects=(native_object.object_path,), + module_dirs=(build_dir,), + include_dirs=(build_dir,), +) build = pipeline._build_rendered_wrapper_extension( artifacts, output_dir=build_dir,