From dea57fec681476c1fe264401652cfd23cee72dea Mon Sep 17 00:00:00 2001 From: said Date: Mon, 20 Jul 2026 00:30:04 +0100 Subject: [PATCH 01/35] prepare for website --- .github/workflows/docs.yml | 81 +++++++ .gitignore | 1 + README.md | 4 +- .../adding-a-code-generation-backend.md | 1 + docs/developer/adding-a-feature.md | 1 + docs/developer/adding-a-fortran-construct.md | 1 + docs/developer/build-system.md | 1 + docs/developer/c-parser-reference.md | 1 + docs/developer/coding-standards.md | 1 + .../contributing/coding-standards.md | 1 + .../contributing/contribution-guide.md | 1 + docs/developer/contributing/index.md | 1 + .../contributing/pull-request-workflow.md | 1 + docs/developer/contributing/review-process.md | 1 + docs/developer/development-workflow.md | 1 + docs/developer/feature-to-code-map.md | 1 + docs/developer/fortran-parser-reference.md | 1 + docs/developer/index.md | 1 + docs/developer/quality-assurance.md | 1 + docs/developer/repository-structure.md | 1 + docs/developer/source-map.md | 1 + docs/developer/testing-strategy.md | 1 + docs/index.md | 15 +- docs/maintainer/README.md | 1 + docs/maintainer/ci-cd.md | 30 ++- docs/maintainer/design/code-generation.md | 1 + docs/maintainer/design/cpython-integration.md | 1 + .../design/error-propagation-model.md | 1 + docs/maintainer/design/index.md | 1 + .../design/memory-ownership-model.md | 1 + .../maintainer/design/overall-architecture.md | 1 + docs/maintainer/design/parser-architecture.md | 1 + docs/maintainer/design/runtime-model.md | 1 + docs/maintainer/design/semantic-analysis.md | 1 + ...tilanguage-wrapper-runtime-architecture.md | 1 + .../maintainer/design/wrapper-design-notes.md | 1 + docs/maintainer/documentation-architecture.md | 69 ++++-- .../internal-architecture/ast-design.md | 1 + .../dependency-analysis.md | 1 + .../error-handling-pipeline.md | 1 + .../maintainer/internal-architecture/index.md | 1 + .../ownership-tracking.md | 1 + .../internal-architecture/pipeline-map.md | 1 + .../internal-architecture/runtime-layer.md | 1 + .../internal-architecture/semantic-passes.md | 1 + .../internal-architecture/symbol-tables.md | 1 + .../internal-architecture/type-system.md | 1 + .../wrapper-generation-pipeline.md | 1 + docs/maintainer/release-process.md | 1 + .../documentation-content-checklist.md | 24 ++- docs/maintainer/roadmap/index.md | 1 + .../roadmap/native-array-handle-checklist.md | 1 + .../roadmap/semantic-pyi-wrapper-checklist.md | 1 + .../test-suite-organization-checklist.md | 1 + .../wrapper-plan-migration-checklist.md | 1 + docs/user/changelog/index.md | 1 + docs/user/examples/blas-wrapper.md | 1 + docs/user/examples/cfd-mini-example.md | 1 + docs/user/examples/index.md | 1 + docs/user/examples/lapack-wrapper.md | 1 + docs/user/examples/mpi-example.md | 1 + docs/user/examples/object-oriented-fortran.md | 1 + docs/user/examples/ode-solver.md | 1 + docs/user/examples/openmp-example.md | 1 + .../examples/recipes/build-and-import-cli.md | 1 + .../recipes/build-and-import-python-api.md | 1 + .../recipes/build-multiple-fortran-sources.md | 1 + .../recipes/compiler-preprocessing.md | 1 + .../examples/recipes/control-cli-output.md | 1 + .../recipes/generate-editable-makefile.md | 1 + docs/user/examples/recipes/inspect-c-api.md | 1 + .../examples/recipes/inspect-fortran-api.md | 1 + .../recipes/semantic-pyi-contracts.md | 1 + .../recipes/use-python-inspection-apis.md | 1 + docs/user/examples/verified-cookbook.md | 1 + docs/user/faq/index.md | 1 + .../user/getting-started/beginner-workflow.md | 3 +- .../getting-started/first-wrapped-function.md | 3 +- .../getting-started/first-wrapped-module.md | 1 + docs/user/getting-started/index.md | 1 + docs/user/getting-started/installation.md | 1 + docs/user/getting-started/verification.md | 3 +- docs/user/guide/allocatables.md | 1 + docs/user/guide/arrays.md | 1 + docs/user/guide/callbacks.md | 1 + docs/user/guide/data-types.md | 1 + docs/user/guide/distribution.md | 1 + .../guide/editing-semantic-pyi-contracts.md | 1 + docs/user/guide/enumerations.md | 1 + docs/user/guide/error-handling.md | 1 + docs/user/guide/fortran-wrapper.md | 1 + docs/user/guide/generic-interfaces.md | 1 + docs/user/guide/index.md | 1 + docs/user/guide/memory-management.md | 1 + docs/user/guide/optional-arguments.md | 1 + docs/user/guide/packaging.md | 3 +- docs/user/guide/pointers.md | 1 + docs/user/guide/wrapping-derived-types.md | 1 + docs/user/guide/wrapping-functions.md | 3 +- docs/user/guide/wrapping-modules.md | 1 + docs/user/guide/wrapping-subroutines.md | 1 + docs/user/index.md | 1 + docs/user/language-support/feature-matrix.md | 1 + docs/user/language-support/index.md | 1 + .../partially-supported-features.md | 1 + .../user/language-support/planned-features.md | 1 + .../language-support/supported-features.md | 1 + .../language-support/unsupported-features.md | 1 + docs/user/reference/callbacks.md | 1 + docs/user/reference/cli-commands.md | 5 +- docs/user/reference/configuration-files.md | 38 +++- docs/user/reference/diagnostic-codes.md | 1 + docs/user/reference/generated-classes.md | 1 + docs/user/reference/generated-functions.md | 1 + docs/user/reference/generated-modules.md | 1 + docs/user/reference/index.md | 1 + docs/user/reference/python-api.md | 1 + docs/user/reference/semantic-ir.md | 1 + docs/user/reference/semantic-pyi-format.md | 1 + docs/user/troubleshooting/build-issues.md | 1 + docs/user/troubleshooting/compiler-issues.md | 1 + docs/user/troubleshooting/index.md | 1 + .../troubleshooting/installation-issues.md | 1 + .../platform-specific-issues.md | 1 + docs/user/troubleshooting/runtime-issues.md | 1 + docs/user/tutorials/basic-wrapper.md | 1 + docs/user/tutorials/index.md | 1 + docs/user/tutorials/large-fortran-codebase.md | 1 + docs/user/tutorials/modern-fortran-project.md | 1 + docs/user/tutorials/numerical-solver.md | 1 + docs/user/tutorials/packaging.md | 1 + docs/user/tutorials/scientific-library.md | 1 + mkdocs.yml | 83 +++++++- pyproject.toml | 4 + tests/docs/test_publication.py | 73 +++++++ tests/docs/test_structure.py | 54 +++-- tools/mkdocs_publication.py | 199 ++++++++++++++++++ 137 files changed, 749 insertions(+), 64 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 tests/docs/test_publication.py create mode 100644 tools/mkdocs_publication.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..a54524e05 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,81 @@ +name: Documentation + +on: + push: + branches: [main] + paths: + - ".github/workflows/docs.yml" + - "README.md" + - "docs/**" + - "mkdocs.yml" + - "pyproject.toml" + - "tests/docs/**" + - "tools/mkdocs_publication.py" + pull_request: + paths: + - ".github/workflows/docs.yml" + - "README.md" + - "docs/**" + - "mkdocs.yml" + - "pyproject.toml" + - "tests/docs/**" + - "tools/mkdocs_publication.py" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + pages: write + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install documentation dependencies + run: python -m pip install -e ".[docs,qa]" + + - name: Run documentation tests + run: python -m pytest -q tests/docs + + - name: Build reviewed documentation + run: python -m mkdocs build --strict + + - name: Configure GitHub Pages + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + uses: actions/configure-pages@v5 + + - name: Upload GitHub Pages artifact + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + uses: actions/upload-pages-artifact@v4 + with: + path: site + + deploy: + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + permissions: + pages: write + id-token: write + steps: + - name: Deploy GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index 87a5d500f..1d057da14 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ mutants/ .ruff_cache/ .benchmarks/ htmlcov/ +site/ *.pyc *.pyo diff --git a/README.md b/README.md index e216c8f63..f818dd316 100644 --- a/README.md +++ b/README.md @@ -557,9 +557,9 @@ ownership, callback lifetime, ABI shims, or Python-visible projections. - **[Getting Started](docs/user/getting-started/index.md)** — Installation, verification, standalone procedures, modules, and rebuild workflow - **[User Guide](docs/user/guide/index.md)** — Detailed guidance on data types, functions, subroutines, modules, arrays, derived types, callbacks, ownership, runtime behavior, and packaging - [ ] Public documentation site publication gate: deploy the existing MkDocs documentation as the project website only after all of the following are true; do not create a separate marketing-content system for this milestone. + - [x] Material for MkDocs, fail-closed `publication` metadata filtering, + local draft preview, strict production builds, and the GitHub Pages Actions + workflow are configured. - [ ] The landing page states the current project promise, supported workflow, and limitations without relying on planned behavior. - [ ] Installation and the first-wrapper workflow are complete and verified @@ -233,10 +237,14 @@ X2PY_C_DOCS_END --> management have maintained user-facing explanations. - [ ] The architecture overview explains the parser, semantic-policy, lowering, bridge, and binding boundaries. - - [ ] Empty, placeholder-only, and TODO-only pages are removed from public - navigation until their content is ready. - - [ ] An unlisted development preview has validated navigation, links, search, - rendering, and the static site build before public deployment. + - [ ] Each page has been reviewed explicitly; change `publication: draft` to + `publication: reviewed` only after that review. + - [ ] Each lane index is reviewed last, after the lane pages intended for its + initial publication are ready. A draft lane index keeps the complete lane + out of production. + - [ ] A local draft preview and the Pages workflow artifact have validated + navigation, links, search, rendering, and the static site build before + enabling GitHub Pages. ## Completed Content Evidence @@ -244,13 +252,13 @@ These pages already carry maintained content or active implementation roadmap evidence. Keep them current as behavior changes, but do not treat them as the primary placeholder queue. -- [x] `docs/index.md`: maintained website entry point for User and Developer - documentation. +- [x] `docs/index.md`: maintained website entry point for all reviewed + documentation lanes. - [x] `docs/user/index.md`: maintained User documentation lane entry point. - [x] `docs/developer/index.md`: maintained Developer documentation lane entry point. -- [x] `docs/maintainer/README.md`: maintained GitHub-only Maintainer - documentation entry point. +- [x] `docs/maintainer/README.md`: maintained Maintainer documentation entry + point, publication-gated like the User and Developer indexes. - [x] `docs/maintainer/documentation-architecture.md`: maintained three-lane documentation and publication contract. - [x] `docs/user/getting-started/index.md`: maintained beginner route from diff --git a/docs/maintainer/roadmap/index.md b/docs/maintainer/roadmap/index.md index 40f84e712..0db6a14be 100644 --- a/docs/maintainer/roadmap/index.md +++ b/docs/maintainer/roadmap/index.md @@ -4,6 +4,7 @@ audience: maintainers prerequisites: user language support, developer documentation related: ../../user/language-support/planned-features.md, wrapper-plan-migration-checklist.md, semantic-pyi-wrapper-checklist.md, native-array-handle-checklist.md, documentation-content-checklist.md status: active-roadmap +publication: draft --- # Roadmap diff --git a/docs/maintainer/roadmap/native-array-handle-checklist.md b/docs/maintainer/roadmap/native-array-handle-checklist.md index 4e9e43793..f49e6d463 100644 --- a/docs/maintainer/roadmap/native-array-handle-checklist.md +++ b/docs/maintainer/roadmap/native-array-handle-checklist.md @@ -4,6 +4,7 @@ audience: maintainers prerequisites: semantic .pyi format, ownership policy, allocatables, pointers related: index.md, ../../user/reference/semantic-pyi-format.md, ../../user/guide/allocatables.md, ../../user/guide/pointers.md status: active-roadmap +publication: draft --- # Native Array Handle Checklist diff --git a/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md b/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md index b26474ca9..123d5b18f 100644 --- a/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md +++ b/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md @@ -4,6 +4,7 @@ audience: maintainers prerequisites: semantic .pyi format, Fortran wrapper guide related: ../../user/reference/semantic-pyi-format.md, index.md status: active-roadmap +publication: draft --- # Semantic `.pyi` Wrapper Checklist diff --git a/docs/maintainer/roadmap/test-suite-organization-checklist.md b/docs/maintainer/roadmap/test-suite-organization-checklist.md index 9499df682..2e803522d 100644 --- a/docs/maintainer/roadmap/test-suite-organization-checklist.md +++ b/docs/maintainer/roadmap/test-suite-organization-checklist.md @@ -4,6 +4,7 @@ audience: maintainers prerequisites: testing strategy, repository structure related: ../../../developer/testing-strategy.md, ../../../../tests/README.md status: active-roadmap +publication: draft --- # Test Suite Organization Checklist diff --git a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md index c5a05fc52..56613114f 100644 --- a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md +++ b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md @@ -4,6 +4,7 @@ audience: maintainers prerequisites: pipeline map, semantic IR, ownership policy related: ../internal-architecture/pipeline-map.md, ../../user/reference/semantic-ir.md, semantic-pyi-wrapper-checklist.md, index.md status: active-roadmap +publication: draft --- # Wrapper Plan Migration Checklist diff --git a/docs/user/changelog/index.md b/docs/user/changelog/index.md index 5e09befba..13370029b 100644 --- a/docs/user/changelog/index.md +++ b/docs/user/changelog/index.md @@ -4,6 +4,7 @@ audience: users, developers prerequisites: none related: ../language-support/index.md, ../guide/distribution.md status: planned-documentation +publication: draft --- # Changelog diff --git a/docs/user/examples/blas-wrapper.md b/docs/user/examples/blas-wrapper.md index bccaf63ef..a9220969e 100644 --- a/docs/user/examples/blas-wrapper.md +++ b/docs/user/examples/blas-wrapper.md @@ -4,6 +4,7 @@ audience: users, advanced users prerequisites: arrays, packaging related: lapack-wrapper.md, ../guide/arrays.md status: planned-documentation +publication: draft --- # BLAS Wrapper Example diff --git a/docs/user/examples/cfd-mini-example.md b/docs/user/examples/cfd-mini-example.md index 983db008b..b8d6bf871 100644 --- a/docs/user/examples/cfd-mini-example.md +++ b/docs/user/examples/cfd-mini-example.md @@ -4,6 +4,7 @@ audience: advanced users prerequisites: arrays, large Fortran codebase tutorial related: ../tutorials/large-fortran-codebase.md, ../guide/arrays.md status: planned-documentation +publication: draft --- # CFD Mini-Example diff --git a/docs/user/examples/index.md b/docs/user/examples/index.md index 6a3dd04af..2476b076c 100644 --- a/docs/user/examples/index.md +++ b/docs/user/examples/index.md @@ -4,6 +4,7 @@ audience: users prerequisites: getting started related: ../tutorials/index.md, verified-cookbook.md status: planned-documentation +publication: draft --- # Examples Gallery diff --git a/docs/user/examples/lapack-wrapper.md b/docs/user/examples/lapack-wrapper.md index 829935808..66c76fe6e 100644 --- a/docs/user/examples/lapack-wrapper.md +++ b/docs/user/examples/lapack-wrapper.md @@ -4,6 +4,7 @@ audience: users, advanced users prerequisites: arrays, BLAS wrapper example related: blas-wrapper.md, ../guide/error-handling.md status: planned-documentation +publication: draft --- # LAPACK Wrapper Example diff --git a/docs/user/examples/mpi-example.md b/docs/user/examples/mpi-example.md index 36f14d94f..b4fc6eb5e 100644 --- a/docs/user/examples/mpi-example.md +++ b/docs/user/examples/mpi-example.md @@ -4,6 +4,7 @@ audience: advanced users prerequisites: packaging, platform-specific troubleshooting related: openmp-example.md, ../troubleshooting/platform-specific-issues.md status: not-yet-implemented +publication: draft --- # MPI Example diff --git a/docs/user/examples/object-oriented-fortran.md b/docs/user/examples/object-oriented-fortran.md index 29daa3955..01cace948 100644 --- a/docs/user/examples/object-oriented-fortran.md +++ b/docs/user/examples/object-oriented-fortran.md @@ -4,6 +4,7 @@ audience: advanced users prerequisites: wrapping derived types, memory management related: ../guide/wrapping-derived-types.md, ../guide/memory-management.md status: planned-documentation +publication: draft --- # Object-Oriented Fortran Example diff --git a/docs/user/examples/ode-solver.md b/docs/user/examples/ode-solver.md index df33a1fbe..1947e2470 100644 --- a/docs/user/examples/ode-solver.md +++ b/docs/user/examples/ode-solver.md @@ -4,6 +4,7 @@ audience: users, advanced users prerequisites: callbacks, arrays related: ../tutorials/numerical-solver.md, ../guide/callbacks.md status: planned-documentation +publication: draft --- # ODE Solver Example diff --git a/docs/user/examples/openmp-example.md b/docs/user/examples/openmp-example.md index fb0bc0538..75084bb49 100644 --- a/docs/user/examples/openmp-example.md +++ b/docs/user/examples/openmp-example.md @@ -4,6 +4,7 @@ audience: advanced users prerequisites: runtime troubleshooting, platform-specific troubleshooting related: mpi-example.md, ../guide/error-handling.md status: planned-documentation +publication: draft --- # OpenMP Example diff --git a/docs/user/examples/recipes/build-and-import-cli.md b/docs/user/examples/recipes/build-and-import-cli.md index c1f5cd927..02560230d 100644 --- a/docs/user/examples/recipes/build-and-import-cli.md +++ b/docs/user/examples/recipes/build-and-import-cli.md @@ -4,6 +4,7 @@ audience: users prerequisites: basic wrapper tutorial, supported compiler toolchain related: ../verified-cookbook.md, ../../guide/fortran-wrapper.md status: maintained +publication: draft --- # Build And Import With The CLI 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 ac758e63d..79edc7a56 100644 --- a/docs/user/examples/recipes/build-and-import-python-api.md +++ b/docs/user/examples/recipes/build-and-import-python-api.md @@ -4,6 +4,7 @@ audience: users, developers prerequisites: basic wrapper tutorial, supported compiler toolchain related: ../verified-cookbook.md, ../../reference/python-api.md status: maintained +publication: draft --- # Build And Import With The Python API diff --git a/docs/user/examples/recipes/build-multiple-fortran-sources.md b/docs/user/examples/recipes/build-multiple-fortran-sources.md index 0e0cf3ede..aff1023ff 100644 --- a/docs/user/examples/recipes/build-multiple-fortran-sources.md +++ b/docs/user/examples/recipes/build-multiple-fortran-sources.md @@ -4,6 +4,7 @@ audience: users, developers prerequisites: basic wrapper tutorial, supported compiler toolchain related: ../verified-cookbook.md, ../../guide/fortran-wrapper.md status: maintained +publication: draft --- # Build Multiple Fortran Sources diff --git a/docs/user/examples/recipes/compiler-preprocessing.md b/docs/user/examples/recipes/compiler-preprocessing.md index c291cd52b..afb69d350 100644 --- a/docs/user/examples/recipes/compiler-preprocessing.md +++ b/docs/user/examples/recipes/compiler-preprocessing.md @@ -4,6 +4,7 @@ audience: users, developers prerequisites: installation, native project compiler flags related: ../verified-cookbook.md, ../../../developer/c-parser-reference.md, ../../../developer/fortran-parser-reference.md status: maintained +publication: draft --- # Use Compiler Preprocessing Options diff --git a/docs/user/examples/recipes/control-cli-output.md b/docs/user/examples/recipes/control-cli-output.md index 72aeb6e10..10cd11343 100644 --- a/docs/user/examples/recipes/control-cli-output.md +++ b/docs/user/examples/recipes/control-cli-output.md @@ -4,6 +4,7 @@ audience: users, developers prerequisites: installation related: ../verified-cookbook.md, ../../reference/cli-commands.md status: maintained +publication: draft --- # Control CLI Output diff --git a/docs/user/examples/recipes/generate-editable-makefile.md b/docs/user/examples/recipes/generate-editable-makefile.md index 22ca9d482..1177ea0fc 100644 --- a/docs/user/examples/recipes/generate-editable-makefile.md +++ b/docs/user/examples/recipes/generate-editable-makefile.md @@ -4,6 +4,7 @@ audience: users, developers prerequisites: basic wrapper tutorial, GNU Make, supported compiler toolchain related: ../verified-cookbook.md, ../../guide/fortran-wrapper.md status: maintained +publication: draft --- # Generate An Editable Makefile diff --git a/docs/user/examples/recipes/inspect-c-api.md b/docs/user/examples/recipes/inspect-c-api.md index d01afb9b9..1e59d5b3f 100644 --- a/docs/user/examples/recipes/inspect-c-api.md +++ b/docs/user/examples/recipes/inspect-c-api.md @@ -5,6 +5,7 @@ audience: users, developers prerequisites: installation related: ../verified-cookbook.md, ../../../developer/c-parser-reference.md status: maintained +publication: draft --- " and hidden == "ordinary": + hidden = None + elif hidden == "deferred-c": assert "--" not in line, f"{path.relative_to(ROOT)}: invalid double hyphen in deferred comment" + elif hidden == "ordinary": + continue elif not line.lstrip().startswith(C_DOCS_DISABLED): visible.append(line) assert not hidden, f"{path.relative_to(ROOT)}: unclosed deferred documentation comment" @@ -539,12 +551,22 @@ def test_documentation_page_metadata(path: Path) -> None: assert metadata[key], f"{path.relative_to(ROOT)}: metadata field {key!r} is empty" assert metadata["status"] in ALLOWED_STATUSES, f"{path.relative_to(ROOT)}: unknown status {metadata['status']!r}" + assert metadata["publication"] in ALLOWED_PUBLICATION_STATES, ( + f"{path.relative_to(ROOT)}: unknown publication state {metadata['publication']!r}" + ) if metadata["status"] in TODO_STATUSES: assert "## TODO" in body, f"{path.relative_to(ROOT)}: unfinished pages must include a TODO section" assert "TODO:" in body, f"{path.relative_to(ROOT)}: TODO section must contain explicit TODO markers" -@pytest.mark.parametrize("path", PUBLISHED_DOCUMENTATION_PATHS, ids=lambda path: str(path.relative_to(ROOT))) +@pytest.mark.parametrize( + "path", + [ + ROOT / "README.md", + *(path for path in WEBSITE_DOCUMENTATION_PATHS if _front_matter(path)[0].get("publication") == "reviewed"), + ], + ids=lambda path: str(path.relative_to(ROOT)), +) def test_deferred_c_documentation_is_not_visible(path: Path) -> None: visible = _visible_documentation_source(path) for allowed_text in VISIBLE_C_DOCUMENTATION_EXCEPTIONS.get(str(path.relative_to(ROOT)), ()): @@ -569,7 +591,7 @@ def test_deferred_c_pages_are_not_in_site_navigation() -> None: def test_readme_quick_start_shows_input_source_before_wrapper_build() -> None: readme = _visible_documentation_source(ROOT / "README.md") - quick_start = readme.split("## Quick Start", maxsplit=1)[1].split( + quick_start = readme.split("## Installation & Quick Start", maxsplit=1)[1].split( "The runtime wrapper mechanism is:", maxsplit=1, )[0] @@ -727,7 +749,7 @@ def test_documentation_lane_has_consistent_audience(lane: str, audience_terms: t assert metadata["audience"] == "maintainers" -@pytest.mark.parametrize("path", WEBSITE_DOCUMENTATION_PATHS, ids=lambda path: str(path.relative_to(ROOT))) +@pytest.mark.parametrize("path", LEARNING_DOCUMENTATION_PATHS, ids=lambda path: str(path.relative_to(ROOT))) def test_website_documentation_does_not_link_to_maintainer_lane(path: Path) -> None: maintainer_root = (DOCS_ROOT / "maintainer").resolve() for target in MARKDOWN_LINK.findall(_visible_documentation_source(path)): @@ -770,11 +792,13 @@ def test_required_roadmap_page_exists(relative_path: str) -> None: assert (DOCS_ROOT / relative_path).is_file() -def test_maintainer_documentation_is_excluded_from_site_build() -> None: +def test_site_navigation_includes_all_publishable_lanes_and_excludes_archive() -> None: site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") - assert "maintainer/**" in site_configuration assert "old_docs/**" in site_configuration - assert not any(path.startswith("maintainer/") for path in _site_navigation_positions()) + positions = _site_navigation_positions() + assert "user/index.md" in positions + assert "developer/index.md" in positions + assert "maintainer/README.md" in positions @pytest.mark.parametrize("relative_path", REQUIRED_GETTING_STARTED_PAGES) @@ -849,7 +873,7 @@ def test_getting_started_overview_uses_standalone_example_and_current_evidence() def test_first_wrapped_function_shows_contract_and_mentions_later_support_boundaries() -> None: page = (DOCS_ROOT / "user/getting-started/first-wrapped-function.md").read_text(encoding="utf-8") - source_index = page.index("[README Quick Start](../../../README.md#quick-start)") + source_index = page.index("[README Quick Start](../../../README.md#installation--quick-start)") build_index = page.index("python3 -m x2py scale.f90 \\") command_index = page.index("python3 -m x2py generate --pyi scale.f90") contract_index = page.index( @@ -878,7 +902,7 @@ def test_first_wrapped_module_shows_local_input_and_generated_contract() -> None def test_beginner_workflow_reuses_scale_example_without_renaming_it() -> None: page = (DOCS_ROOT / "user/getting-started/beginner-workflow.md").read_text(encoding="utf-8") - source_reference_index = page.index("[README Quick Start](../../../README.md#quick-start)") + source_reference_index = page.index("[README Quick Start](../../../README.md#installation--quick-start)") layout_index = page.index("src/\n scale.f90") contract_index = page.index("python3 -m x2py generate --pyi src/scale.f90") build_index = page.index("python3 -m x2py src/scale.f90 \\\n --out-dir build/scale") diff --git a/tools/mkdocs_publication.py b/tools/mkdocs_publication.py new file mode 100644 index 000000000..3bd00cbb8 --- /dev/null +++ b/tools/mkdocs_publication.py @@ -0,0 +1,199 @@ +"""Fail-closed page publication for the x2py MkDocs website.""" + +from __future__ import annotations + +import os +import posixpath +import re +from pathlib import Path, PurePosixPath +from urllib.parse import quote, unquote, urlsplit + + +_PUBLICATION_KEY = "publication" +_REVIEWED = "reviewed" +_TRUE_VALUES = {"1", "true", "yes", "on"} +_MARKDOWN_SUFFIXES = {".md", ".markdown", ".mdown", ".mkdn", ".mkd"} +_LANE_INDEXES = { + "user": "user/index.md", + "developer": "developer/index.md", + "maintainer": "maintainer/README.md", +} +_MARKDOWN_LINK = re.compile(r"(? str | None: + lines = path.read_text(encoding="utf-8").splitlines() + if not lines or lines[0] != "---": + return None + + try: + end = lines.index("---", 1) + except ValueError: + return None + + for line in lines[1:end]: + name, separator, value = line.partition(":") + if separator and name.strip() == key: + return value.strip() + return None + + +def _publication_states(docs_dir: Path) -> tuple[dict[str, str | None], set[str]]: + states: dict[str, str | None] = {} + known_paths: set[str] = set() + for path in docs_dir.rglob("*"): + if not path.is_file() or path.suffix.lower() not in _MARKDOWN_SUFFIXES: + continue + relative_path = path.relative_to(docs_dir).as_posix() + known_paths.add(relative_path) + if relative_path.startswith("old_docs/"): + continue + states[relative_path] = _front_matter_value(path, _PUBLICATION_KEY) + return states, known_paths + + +def _reviewed_paths(states: dict[str, str | None]) -> set[str]: + if states.get("index.md") != _REVIEWED: + return set() + + reviewed = {"index.md"} + for lane, lane_index in _LANE_INDEXES.items(): + if states.get(lane_index) != _REVIEWED: + continue + reviewed.update( + path + for path, state in states.items() + if (path == lane_index or path.startswith(f"{lane}/")) and state == _REVIEWED + ) + return reviewed + + +def _filter_navigation(value, published_paths: set[str]): + if isinstance(value, str): + if PurePosixPath(value).suffix.lower() not in _MARKDOWN_SUFFIXES: + return value + return value if value in published_paths else None + + if isinstance(value, list): + filtered = [] + for item in value: + kept = _filter_navigation(item, published_paths) + if kept is not None: + filtered.append(kept) + return filtered or None + + if isinstance(value, dict): + filtered = {} + for title, item in value.items(): + kept = _filter_navigation(item, published_paths) + if kept is not None: + filtered[title] = kept + return filtered or None + + return value + + +def _relative_document_target(source_uri: str, raw_target: str) -> str | None: + target = raw_target.strip().split(maxsplit=1)[0] + parsed = urlsplit(target) + if parsed.scheme or parsed.netloc or not parsed.path or parsed.path.startswith("/"): + return None + if PurePosixPath(parsed.path).suffix.lower() not in _MARKDOWN_SUFFIXES: + return None + source_parent = PurePosixPath(source_uri).parent.as_posix() + return posixpath.normpath(posixpath.join(source_parent, unquote(parsed.path))) + + +def _unlink_unpublished_targets(markdown: str, source_uri: str) -> str: + def replace_link(match: re.Match[str]) -> str: + label, target = match.groups() + resolved = _relative_document_target(source_uri, target) + if resolved in _known_document_paths and resolved not in _published_paths: + return label + return match.group(0) + + return _MARKDOWN_LINK.sub(replace_link, markdown) + + +def _repository_target(source_uri: str, raw_target: str) -> str | None: + target_parts = raw_target.strip().split(maxsplit=1) + parsed = urlsplit(target_parts[0]) + if parsed.scheme or parsed.netloc or not parsed.path or parsed.path.startswith("/"): + return None + + source_path = _docs_dir / source_uri + resolved = (source_path.parent / unquote(parsed.path)).resolve() + repository_root = _docs_dir.parent.resolve() + if not resolved.is_relative_to(repository_root) or not resolved.exists(): + return None + if resolved.is_relative_to(_docs_dir.resolve()) and resolved.is_file(): + return None + + route = "tree" if resolved.is_dir() else "blob" + relative_path = resolved.relative_to(repository_root).as_posix() + rewritten = f"{_repository_url}/{route}/main/{quote(relative_path)}" + if parsed.query: + rewritten += f"?{parsed.query}" + if parsed.fragment: + rewritten += f"#{parsed.fragment}" + if len(target_parts) == 2: + rewritten += f" {target_parts[1]}" + return rewritten + + +def _rewrite_repository_targets(markdown: str, source_uri: str) -> str: + def replace_link(match: re.Match[str]) -> str: + label, target = match.groups() + rewritten = _repository_target(source_uri, target) + if rewritten is None: + return match.group(0) + return f"[{label}]({rewritten})" + + return _MARKDOWN_LINK.sub(replace_link, markdown) + + +def on_config(config, **_kwargs): + """Load publication state and filter production navigation.""" + global _docs_dir, _include_drafts, _known_document_paths, _published_paths, _repository_url + + _include_drafts = os.getenv("X2PY_DOCS_INCLUDE_DRAFTS", "").strip().lower() in _TRUE_VALUES + _docs_dir = Path(config["docs_dir"]) + _repository_url = str(config["repo_url"]).rstrip("/") + states, _known_document_paths = _publication_states(_docs_dir) + _published_paths = _reviewed_paths(states) + + if not _include_drafts: + config["nav"] = _filter_navigation(config["nav"], _published_paths) or [] + return config + + +def on_files(files, **_kwargs): + """Remove unpublished Markdown files from production output and search.""" + if _include_drafts: + return files + + for file in list(files): + if PurePosixPath(file.src_uri).suffix.lower() in _MARKDOWN_SUFFIXES and file.src_uri not in _published_paths: + files.remove(file) + return files + + +def on_page_markdown(markdown: str, page, **_kwargs) -> str: + """Label local drafts and remove production links to unpublished pages.""" + source_uri = page.file.src_uri + markdown = _rewrite_repository_targets(markdown, source_uri) + if _include_drafts: + if source_uri not in _published_paths: + warning = ( + '!!! warning "Unpublished documentation draft"\n' + " This page is available only in the local draft preview.\n\n" + ) + return warning + markdown + return markdown + return _unlink_unpublished_targets(markdown, source_uri) From b5ccf2733e89b81538d7841d051958535a7a4aa4 Mon Sep 17 00:00:00 2001 From: said Date: Mon, 20 Jul 2026 00:56:57 +0100 Subject: [PATCH 02/35] fix error --- docs/developer/repository-structure.md | 4 +++- tests/README.md | 2 +- tests/architecture/test_test_suite_layout.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/developer/repository-structure.md b/docs/developer/repository-structure.md index 95b85cb0b..4c0f640f5 100644 --- a/docs/developer/repository-structure.md +++ b/docs/developer/repository-structure.md @@ -52,7 +52,7 @@ through `x2py/__init__.py`. | `tests/wrapper_codegen/` | Typed planning, direct bridge/binding generation, and source-printer tests. | | `tests/utilities/` | Shared Python utility tests. | | `tests/wrapper/fortran/` | Runtime wrapper tests that compile, import, call, and check failure paths. | -| `tests/docs/` | Documentation example and structure checks. | +| `tests/docs/` | Documentation example, structure, and website-publication checks. | | `tests/tools/` | Repository tooling tests. | +```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 +``` -[Developer documentation](developer/index.md) explains how to change x2py, -locate implementation ownership, add features, run focused tests, and prepare a -contribution. Start here when you are modifying the codebase. +Build the Python extension from the directory containing that file: -## Maintainer Documentation +```bash +python3 -m x2py scale.f90 +``` -[Maintainer documentation](maintainer/README.md) records project governance, -accepted design decisions, internal architecture, release policy, and active -roadmaps. +The command creates an importable `scale` extension beside the source and keeps +its generated wrapper and build artifacts under `__x2py__/`. Call the native +function from Python with the exact NumPy scalar types required by its +contract: + +```python +import numpy as np + +import scale + +result = scale.scale(np.float64(3.0), np.float64(2.5)) +print(result) +``` + +The call prints: + +```text +7.5 +``` + +The generated function is inspectable from Python: + +```python +print(scale.scale.__doc__) +``` + +Its docstring describes the public signature, accepted dtypes, result, and +call-time type error: + +```text +scale(value, factor) -> float64 + +Parameters +---------- +value : float64 +factor : float64 + +Returns +------- +result : float64 + +Raises +------ +TypeError + If an argument has an incompatible Python type or dtype. +``` + +That is the basic x2py workflow: provide native source, build an extension, +import it, and call the generated Python surface. + +## Continue With Getting Started + +This preview assumes x2py, NumPy, and a supported native compiler are already +available. [Getting Started](user/getting-started/index.md) walks through +installation and verification first, then rebuilds this function and explains +its generated contract and artifacts. diff --git a/docs/javascripts/code-copy.js b/docs/javascripts/code-copy.js new file mode 100644 index 000000000..f592a606a --- /dev/null +++ b/docs/javascripts/code-copy.js @@ -0,0 +1,98 @@ +(function () { + "use strict"; + + const copyIcon = ` + `; + const copiedIcon = ` + `; + + function fallbackCopy(text) { + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.setAttribute("readonly", ""); + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + + try { + if (!document.execCommand("copy")) { + throw new Error("The browser rejected the copy command."); + } + } finally { + textarea.remove(); + } + } + + async function copyText(text) { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text); + return; + } + fallbackCopy(text); + } + + function addCopyButton(code) { + const pre = code.closest("pre"); + if (!pre) { + return; + } + + const parent = pre.parentElement; + const host = + parent && (parent.classList.contains("highlight") || parent.classList.contains("codehilite")) + ? parent + : pre; + if (host.classList.contains("x2py-copy-host")) { + return; + } + + host.classList.add("x2py-copy-host"); + const button = document.createElement("button"); + button.type = "button"; + button.className = "x2py-code-copy"; + button.setAttribute("aria-label", "Copy code to clipboard"); + button.title = "Copy"; + button.innerHTML = copyIcon + copiedIcon; + + let resetTimer; + button.addEventListener("click", async function () { + window.clearTimeout(resetTimer); + button.disabled = true; + try { + await copyText(code.textContent); + button.classList.add("is-copied"); + button.setAttribute("aria-label", "Copied to clipboard"); + button.title = "Copied"; + } catch (_error) { + button.classList.add("is-error"); + button.setAttribute("aria-label", "Could not copy to clipboard"); + button.title = "Copy failed"; + } finally { + button.disabled = false; + resetTimer = window.setTimeout(function () { + button.classList.remove("is-copied", "is-error"); + button.setAttribute("aria-label", "Copy code to clipboard"); + button.title = "Copy"; + }, 2000); + } + }); + + host.appendChild(button); + } + + function addCopyButtons() { + document.querySelectorAll("pre code").forEach(addCopyButton); + } + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", addCopyButtons); + } else { + addCopyButtons(); + } +})(); diff --git a/docs/maintainer/README.md b/docs/maintainer/README.md index b290aa769..6ec1c8a33 100644 --- a/docs/maintainer/README.md +++ b/docs/maintainer/README.md @@ -35,5 +35,5 @@ Implementation orientation, source maps, feature workflows, tests, and contribution requirements remain in the separate [Developer documentation](../developer/index.md) lane. -The historical [old documentation archive](../old_docs) is retained for -comparison only and is excluded from active navigation. +The historical `docs/old_docs/` archive is retained for comparison only and is +excluded from the website and active navigation. diff --git a/docs/maintainer/documentation-architecture.md b/docs/maintainer/documentation-architecture.md index 629b5eb39..f6e9f6b40 100644 --- a/docs/maintainer/documentation-architecture.md +++ b/docs/maintainer/documentation-architecture.md @@ -31,6 +31,22 @@ contains only pages explicitly marked as reviewed. 7. User-facing source-driven examples show the complete input source before the command that consumes it. Generated paths must come from an immediately preceding command, and commands show their expected result. +8. The website keeps its documentation navigation expanded and renders an + accessible copy control on every code block, including command-output and + result blocks. +9. On desktop-sized viewports, the page body starts beside the navigation and + uses a `1200px` maximum width: wider than the theme default for code and + tables, but still bounded for readable prose. Any unused space remains on + the far right rather than separating the sidebar from the content. +10. Code and result blocks use a consistent responsive width capped at `56rem`. + 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 x2py, +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. ## Audience Lanes @@ -92,7 +108,10 @@ HTML, search, or the sitemap. When a reviewed index or overview mentions a draft page, the production build renders that page name as plain text until the target becomes publishable. Links to existing repository evidence outside the `docs/` tree are rewritten to the matching file or directory on GitHub; links -to missing targets remain unchanged so the strict build can reject them. +to missing targets remain unchanged so the strict build can reject them. Links +to another active documentation page or directory must stay relative to +`docs/` and resolve inside the website. The hook never rewrites a target inside +`docs/` to GitHub. Use the normal local server to preview exactly what GitHub Pages will publish: @@ -139,13 +158,19 @@ docs/ internal-architecture/ roadmap/ CI and release policy + javascripts/ + code-copy.js + stylesheets/ + site.css + code-copy.css old_docs/ ``` -New active pages must be created in one of the three lanes. Do not restore -top-level topic directories or place maintainer rules beside the website -landing page. Historical `old_docs/` material is never eligible for website -publication. +New active pages must be created in one of the three lanes. Website-only static +behavior and presentation assets live in `javascripts/` and `stylesheets/`. +Do not restore top-level topic directories or place maintainer rules beside the +website landing page. Historical `old_docs/` material is never eligible for +website publication. ## Continuous Documentation Quality diff --git a/docs/stylesheets/code-copy.css b/docs/stylesheets/code-copy.css new file mode 100644 index 000000000..d7e00bad1 --- /dev/null +++ b/docs/stylesheets/code-copy.css @@ -0,0 +1,78 @@ +.x2py-copy-host { + position: relative; +} + +.x2py-code-copy { + position: absolute; + z-index: 2; + top: 0.45rem; + right: 0.45rem; + display: inline-flex; + width: 2rem; + height: 2rem; + align-items: center; + justify-content: center; + padding: 0; + color: #404040; + background: rgb(255 255 255 / 92%); + border: 1px solid #d6d6d6; + border-radius: 0.2rem; + cursor: pointer; + opacity: 0.35; + transition: color 0.15s ease, background-color 0.15s ease, opacity 0.15s ease; +} + +.x2py-copy-host:hover > .x2py-code-copy, +.x2py-code-copy:focus-visible, +.x2py-code-copy.is-copied, +.x2py-code-copy.is-error { + opacity: 1; +} + +.x2py-code-copy:hover { + color: #ffffff; + background: #2980b9; + border-color: #2980b9; +} + +.x2py-code-copy:focus-visible { + outline: 3px solid #f1c40f; + outline-offset: 2px; +} + +.x2py-code-copy.is-copied { + color: #ffffff; + background: #27ae60; + border-color: #27ae60; +} + +.x2py-code-copy.is-error { + color: #ffffff; + background: #c0392b; + border-color: #c0392b; +} + +.x2py-code-copy svg { + width: 1.1rem; + height: 1.1rem; + fill: none; + stroke: currentcolor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} + +.x2py-copied-icon, +.x2py-code-copy.is-copied .x2py-copy-icon { + display: none; +} + +.x2py-code-copy.is-copied .x2py-copied-icon { + display: block; +} + +@media (hover: none) { + .x2py-code-copy { + opacity: 0.8; + } +} diff --git a/docs/stylesheets/site.css b/docs/stylesheets/site.css new file mode 100644 index 000000000..ffc207fe5 --- /dev/null +++ b/docs/stylesheets/site.css @@ -0,0 +1,10 @@ +.wy-nav-content { + max-width: 1200px; + margin: 0; +} + +.rst-content pre { + width: 100%; + max-width: 56rem; + padding-right: 3.25rem; +} diff --git a/docs/user/getting-started/beginner-workflow.md b/docs/user/getting-started/beginner-workflow.md index 7d3aae450..722c6641f 100644 --- a/docs/user/getting-started/beginner-workflow.md +++ b/docs/user/getting-started/beginner-workflow.md @@ -4,7 +4,7 @@ audience: users prerequisites: first wrapped module related: ../tutorials/basic-wrapper.md, ../examples/verified-cookbook.md, ../reference/cli-commands.md status: maintained -publication: draft +publication: reviewed --- # Common Beginner Workflow @@ -15,7 +15,7 @@ into `build/`, run a small Python check, and cleanly rebuild when the native contract changes. Use the `scale.f90` input from the -[README Quick Start](../../../README.md#installation--quick-start). Keep the same filename when +[homepage example](../../index.md#try-x2py). Keep the same filename when you move it into a project layout. ## 1. Create A Small Project Layout diff --git a/docs/user/getting-started/first-wrapped-function.md b/docs/user/getting-started/first-wrapped-function.md index 7f64ccd95..3d76ef6ca 100644 --- a/docs/user/getting-started/first-wrapped-function.md +++ b/docs/user/getting-started/first-wrapped-function.md @@ -4,7 +4,7 @@ audience: users prerequisites: installation, verification related: first-wrapped-module.md, ../guide/wrapping-functions.md, ../reference/semantic-pyi-format.md status: maintained -publication: draft +publication: reviewed --- # First Wrapped Function @@ -15,7 +15,7 @@ NumPy dtypes required by its native contract. ## Source Reuse the same `scale.f90` input from the -[README Quick Start](../../../README.md#installation--quick-start). +[homepage example](../../index.md#try-x2py). The generated Python call accepts two `numpy.float64` values and returns a Python `float` result. diff --git a/docs/user/getting-started/first-wrapped-module.md b/docs/user/getting-started/first-wrapped-module.md index 804d39aac..874c392e0 100644 --- a/docs/user/getting-started/first-wrapped-module.md +++ b/docs/user/getting-started/first-wrapped-module.md @@ -4,7 +4,7 @@ audience: users prerequisites: first wrapped function related: beginner-workflow.md, ../guide/wrapping-modules.md, ../language-support/feature-matrix.md status: maintained -publication: draft +publication: reviewed --- # First Wrapped Module diff --git a/docs/user/getting-started/index.md b/docs/user/getting-started/index.md index 03eda9417..d8b1d9e25 100644 --- a/docs/user/getting-started/index.md +++ b/docs/user/getting-started/index.md @@ -4,7 +4,7 @@ audience: users prerequisites: repository checkout related: installation.md, verification.md status: maintained -publication: draft +publication: reviewed --- # Getting Started diff --git a/docs/user/getting-started/installation.md b/docs/user/getting-started/installation.md index 95aac078a..d8b540482 100644 --- a/docs/user/getting-started/installation.md +++ b/docs/user/getting-started/installation.md @@ -4,7 +4,7 @@ audience: users, contributors prerequisites: Python 3.10 or newer, repository checkout related: verification.md, ../troubleshooting/installation-issues.md, ../../developer/quality-assurance.md status: maintained -publication: draft +publication: reviewed --- # Installation diff --git a/docs/user/getting-started/verification.md b/docs/user/getting-started/verification.md index 4a509ec7a..d73c5dba3 100644 --- a/docs/user/getting-started/verification.md +++ b/docs/user/getting-started/verification.md @@ -4,7 +4,7 @@ audience: users, contributors prerequisites: installation related: first-wrapped-function.md, ../troubleshooting/index.md, ../reference/cli-commands.md status: maintained -publication: draft +publication: reviewed --- # Verification @@ -28,7 +28,7 @@ interpreter. The third proves that the module entrypoint is installed. ## 2. Verify The Contract Path Use the `scale.f90` input created in the -[README Quick Start](../../../README.md#installation--quick-start). +[homepage example](../../index.md#try-x2py). From the directory containing `scale.f90`, print the semantic `.pyi` contract without compiling a wrapper: diff --git a/docs/user/guide/allocatables.md b/docs/user/guide/allocatables.md index 2fdd9efb5..f65171510 100644 --- a/docs/user/guide/allocatables.md +++ b/docs/user/guide/allocatables.md @@ -4,7 +4,7 @@ audience: users, advanced users prerequisites: arrays related: arrays.md, pointers.md, memory-management.md status: maintained -publication: draft +publication: reviewed --- # Allocatables diff --git a/docs/user/guide/arrays.md b/docs/user/guide/arrays.md index 07b442693..ddbfa09b3 100644 --- a/docs/user/guide/arrays.md +++ b/docs/user/guide/arrays.md @@ -4,7 +4,7 @@ audience: users prerequisites: data types, wrapping functions related: allocatables.md, pointers.md, wrapping-subroutines.md status: maintained -publication: draft +publication: reviewed --- # Arrays diff --git a/docs/user/guide/callbacks.md b/docs/user/guide/callbacks.md index 3c7e02b99..91ac5570e 100644 --- a/docs/user/guide/callbacks.md +++ b/docs/user/guide/callbacks.md @@ -4,7 +4,7 @@ audience: advanced users prerequisites: wrapping functions, error handling, data types related: error-handling.md, memory-management.md, ../reference/semantic-pyi-format.md status: maintained -publication: draft +publication: reviewed --- # Callbacks diff --git a/docs/user/guide/data-types.md b/docs/user/guide/data-types.md index 14dd5b624..39d3b7995 100644 --- a/docs/user/guide/data-types.md +++ b/docs/user/guide/data-types.md @@ -4,7 +4,7 @@ audience: users prerequisites: common beginner workflow related: arrays.md, wrapping-derived-types.md, ../reference/semantic-pyi-format.md status: maintained -publication: draft +publication: reviewed --- # Data Types diff --git a/docs/user/guide/distribution.md b/docs/user/guide/distribution.md index b0efc959f..b82537ec4 100644 --- a/docs/user/guide/distribution.md +++ b/docs/user/guide/distribution.md @@ -4,7 +4,7 @@ audience: users, packagers prerequisites: packaging related: packaging.md, ../troubleshooting/platform-specific-issues.md, ../getting-started/installation.md status: maintained -publication: draft +publication: reviewed --- # Distribution diff --git a/docs/user/guide/editing-semantic-pyi-contracts.md b/docs/user/guide/editing-semantic-pyi-contracts.md index 1f8a65277..6137d3bc0 100644 --- a/docs/user/guide/editing-semantic-pyi-contracts.md +++ b/docs/user/guide/editing-semantic-pyi-contracts.md @@ -4,7 +4,7 @@ audience: users, advanced users prerequisites: Fortran wrapper guide, semantic .pyi format related: fortran-wrapper.md, ../reference/semantic-pyi-format.md status: maintained -publication: draft +publication: reviewed --- # Editing Semantic `.pyi` Contracts diff --git a/docs/user/guide/enumerations.md b/docs/user/guide/enumerations.md index 2d056cd6f..9ee80efb0 100644 --- a/docs/user/guide/enumerations.md +++ b/docs/user/guide/enumerations.md @@ -4,7 +4,7 @@ audience: users prerequisites: wrapping modules, data types related: wrapping-modules.md, generic-interfaces.md, ../language-support/feature-matrix.md status: maintained -publication: draft +publication: reviewed --- # Enumerations diff --git a/docs/user/guide/error-handling.md b/docs/user/guide/error-handling.md index b3ba4796b..c741f88a0 100644 --- a/docs/user/guide/error-handling.md +++ b/docs/user/guide/error-handling.md @@ -4,7 +4,7 @@ audience: users, advanced users prerequisites: common beginner workflow, data types related: ../reference/diagnostic-codes.md, ../troubleshooting/index.md, callbacks.md status: maintained -publication: draft +publication: reviewed --- # Error Handling diff --git a/docs/user/guide/fortran-wrapper.md b/docs/user/guide/fortran-wrapper.md index 52395f9f1..e27d0d6da 100644 --- a/docs/user/guide/fortran-wrapper.md +++ b/docs/user/guide/fortran-wrapper.md @@ -4,7 +4,7 @@ audience: users, advanced users prerequisites: first wrapped module, NumPy basics related: index.md, editing-semantic-pyi-contracts.md, ../language-support/index.md status: maintained -publication: draft +publication: reviewed --- # Fortran Wrapper Guide diff --git a/docs/user/guide/generic-interfaces.md b/docs/user/guide/generic-interfaces.md index 0f9358bb7..2013b324c 100644 --- a/docs/user/guide/generic-interfaces.md +++ b/docs/user/guide/generic-interfaces.md @@ -4,7 +4,7 @@ audience: users, advanced users prerequisites: wrapping functions, wrapping subroutines, data types related: optional-arguments.md, wrapping-derived-types.md, error-handling.md status: maintained -publication: draft +publication: reviewed --- # Generic Interfaces diff --git a/docs/user/guide/index.md b/docs/user/guide/index.md index 1a1672aad..961dffa1a 100644 --- a/docs/user/guide/index.md +++ b/docs/user/guide/index.md @@ -4,7 +4,7 @@ audience: users prerequisites: getting started related: data-types.md, fortran-wrapper.md, ../language-support/index.md status: maintained -publication: draft +publication: reviewed --- # User Guide diff --git a/docs/user/guide/memory-management.md b/docs/user/guide/memory-management.md index 930dec80d..412671cd6 100644 --- a/docs/user/guide/memory-management.md +++ b/docs/user/guide/memory-management.md @@ -4,7 +4,7 @@ audience: users, advanced users prerequisites: arrays, wrapping derived types related: allocatables.md, pointers.md, editing-semantic-pyi-contracts.md status: maintained -publication: draft +publication: reviewed --- # Memory Management diff --git a/docs/user/guide/optional-arguments.md b/docs/user/guide/optional-arguments.md index 0fbfafeeb..c20ea45ec 100644 --- a/docs/user/guide/optional-arguments.md +++ b/docs/user/guide/optional-arguments.md @@ -4,7 +4,7 @@ audience: users prerequisites: wrapping subroutines, data types related: generic-interfaces.md, arrays.md, error-handling.md status: maintained -publication: draft +publication: reviewed --- # Optional Arguments diff --git a/docs/user/guide/packaging.md b/docs/user/guide/packaging.md index 82d536df3..f52bfccc3 100644 --- a/docs/user/guide/packaging.md +++ b/docs/user/guide/packaging.md @@ -4,7 +4,7 @@ audience: users, packagers prerequisites: common beginner workflow related: distribution.md, ../reference/cli-commands.md, ../tutorials/packaging.md status: maintained -publication: draft +publication: reviewed --- # Packaging @@ -18,7 +18,7 @@ directory, and treat generated native artifacts as replaceable build output. ## Complete Local Project Example Reuse `scale.f90`, whose complete source is first shown in the -[README Quick Start](../../../README.md#installation--quick-start). Place that file in this +[homepage example](../../index.md#try-x2py). Place that file in this simple project: ```text diff --git a/docs/user/guide/pointers.md b/docs/user/guide/pointers.md index afc6cbe23..5096f81b4 100644 --- a/docs/user/guide/pointers.md +++ b/docs/user/guide/pointers.md @@ -4,7 +4,7 @@ audience: advanced users prerequisites: arrays, memory management related: allocatables.md, memory-management.md, ../reference/semantic-pyi-format.md status: maintained -publication: draft +publication: reviewed --- # Pointers diff --git a/docs/user/guide/wrapping-derived-types.md b/docs/user/guide/wrapping-derived-types.md index 0378e8616..830540be3 100644 --- a/docs/user/guide/wrapping-derived-types.md +++ b/docs/user/guide/wrapping-derived-types.md @@ -4,7 +4,7 @@ audience: users, advanced users prerequisites: wrapping modules, data types related: memory-management.md, generic-interfaces.md, fortran-wrapper.md status: maintained -publication: draft +publication: reviewed --- # Wrapping Derived Types diff --git a/docs/user/guide/wrapping-functions.md b/docs/user/guide/wrapping-functions.md index 7806cc39e..99993ece7 100644 --- a/docs/user/guide/wrapping-functions.md +++ b/docs/user/guide/wrapping-functions.md @@ -4,7 +4,7 @@ audience: users prerequisites: data types, first wrapped function related: wrapping-subroutines.md, arrays.md, fortran-wrapper.md status: maintained -publication: draft +publication: reviewed --- # Wrapping Functions @@ -14,7 +14,8 @@ All arguments follow the exact semantic types shown in the generated `.pyi` file See [Data Types](data-types.md) for details on how Fortran types are mapped to Python/NumPy. -For this example, we'll use `scale.f90` (from [README Quick Start](../../../README.md#installation--quick-start)). +For this example, we'll use `scale.f90` from the +[homepage example](../../index.md#try-x2py). ```bash python3 -m x2py generate --pyi scale.f90 diff --git a/docs/user/guide/wrapping-modules.md b/docs/user/guide/wrapping-modules.md index c252bee53..7fb5dd846 100644 --- a/docs/user/guide/wrapping-modules.md +++ b/docs/user/guide/wrapping-modules.md @@ -4,7 +4,7 @@ audience: users prerequisites: data types, first wrapped module related: wrapping-functions.md, memory-management.md, packaging.md status: maintained -publication: draft +publication: reviewed --- # Wrapping Modules diff --git a/docs/user/guide/wrapping-subroutines.md b/docs/user/guide/wrapping-subroutines.md index 7af0dd373..ddcf248ad 100644 --- a/docs/user/guide/wrapping-subroutines.md +++ b/docs/user/guide/wrapping-subroutines.md @@ -4,7 +4,7 @@ audience: users prerequisites: data types, first wrapped function related: wrapping-functions.md, arrays.md, optional-arguments.md status: maintained -publication: draft +publication: reviewed --- # Wrapping Subroutines diff --git a/docs/user/index.md b/docs/user/index.md index c181074e6..c57670ba3 100644 --- a/docs/user/index.md +++ b/docs/user/index.md @@ -4,7 +4,7 @@ audience: users prerequisites: none related: getting-started/index.md, guide/index.md, reference/index.md status: maintained -publication: draft +publication: reviewed --- # User Documentation diff --git a/docs/user/reference/cli-commands.md b/docs/user/reference/cli-commands.md index da1df8575..26819e3b6 100644 --- a/docs/user/reference/cli-commands.md +++ b/docs/user/reference/cli-commands.md @@ -82,8 +82,8 @@ 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 `scale.f90` source and exact commands from the -[README Quick Start](../../../README.md#installation--quick-start), which contains the -complete source, expected artifacts, contract, and import flow. +[homepage example](../../index.md#try-x2py), which contains the +complete source, basic build, import flow, and expected result. The full build help uses the following two forms: @@ -183,7 +183,7 @@ python3 -m x2py generate --makefile scale.f90 --out-dir build ``` These examples reuse `scale.f90` from the -[README Quick Start](../../../README.md#installation--quick-start). +[homepage example](../../index.md#try-x2py). 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/configuration-files.md b/docs/user/reference/configuration-files.md index 23fee69ce..09f0be25f 100644 --- a/docs/user/reference/configuration-files.md +++ b/docs/user/reference/configuration-files.md @@ -116,13 +116,21 @@ Wrapper users select inputs through CLI flags, Python API arguments, semantic ## `mkdocs.yml` `mkdocs.yml` is the documentation-site configuration. It sets `docs_dir: docs`, -selects Material for MkDocs, owns the complete intended navigation tree, and -loads the publication hook. The production hook includes only pages whose -front matter says `publication: reviewed`. A draft lane index suppresses its -complete User, Developer, or Maintainer lane. Links from documentation pages to -existing source, tests, configuration, and other repository evidence are -rendered as GitHub links because those files are outside the MkDocs source -tree. +selects MkDocs' built-in Read the Docs theme, owns the complete intended +navigation tree, and loads the publication hook. The theme configuration keeps +the sidebar expanded through four navigation levels. A local stylesheet keeps +the page body adjacent to the sidebar with a `1200px` maximum width, balancing +readable prose with room for code and tables. Code and result blocks use the +available page width up to a consistent `56rem` cap; long lines scroll inside +the block. Local JavaScript and CSS add an accessible copy control to every +rendered code, command-output, and result block, with separate space reserved +beside the text. The production hook includes only pages whose front matter says +`publication: reviewed`. A draft lane index suppresses its complete User, +Developer, or Maintainer lane. Links from documentation pages to existing +source, tests, configuration, and other repository evidence are rendered as +GitHub links because those files are outside the MkDocs source tree. Links +between documentation pages remain site-relative and are never rewritten to +GitHub. Preview exactly what GitHub Pages will publish with: diff --git a/mkdocs.yml b/mkdocs.yml index 1bfe51390..9942347fa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,12 +4,17 @@ repo_url: https://github.com/PyNumLab/x2py repo_name: PyNumLab/x2py docs_dir: docs theme: - name: material - features: - - navigation.sections - - navigation.top - - search.highlight - - search.suggest + name: readthedocs + collapse_navigation: false + include_homepage_in_sidebar: true + navigation_depth: 4 + sticky_navigation: true + titles_only: false +extra_css: + - stylesheets/site.css + - stylesheets/code-copy.css +extra_javascript: + - javascripts/code-copy.js plugins: - search hooks: diff --git a/scale.f90 b/scale.f90 new file mode 100644 index 000000000..f3236af5e --- /dev/null +++ b/scale.f90 @@ -0,0 +1,5 @@ +real(8) function scale(value, factor) result(output) + real(8), intent(in) :: value + real(8), intent(in) :: factor + output = value * factor +end function scale diff --git a/tests/docs/test_publication.py b/tests/docs/test_publication.py index 137a0fd28..fda61c0d5 100644 --- a/tests/docs/test_publication.py +++ b/tests/docs/test_publication.py @@ -58,16 +58,20 @@ def test_repository_evidence_links_are_rewritten_to_github(tmp_path: Path, monke page_dir = docs_dir / "user" page_dir.mkdir(parents=True) (page_dir / "index.md").write_text("# User\n", encoding="utf-8") + documentation_section = page_dir / "guide" + documentation_section.mkdir() source_file = tmp_path / "tests" / "evidence.py" source_file.parent.mkdir() source_file.write_text("# evidence\n", encoding="utf-8") monkeypatch.setattr(mkdocs_publication, "_docs_dir", docs_dir) monkeypatch.setattr(mkdocs_publication, "_repository_url", "https://github.com/PyNumLab/x2py") - markdown = "[Page](index.md) [Evidence](../../tests/evidence.py#proof) [Missing](../../missing.py)" + markdown = ( + "[Page](index.md) [Section](guide/) [Evidence](../../tests/evidence.py#proof) [Missing](../../missing.py)" + ) assert mkdocs_publication._rewrite_repository_targets(markdown, "user/index.md") == ( - "[Page](index.md) " + "[Page](index.md) [Section](guide/) " "[Evidence](https://github.com/PyNumLab/x2py/blob/main/tests/evidence.py#proof) " "[Missing](../../missing.py)" ) diff --git a/tests/docs/test_structure.py b/tests/docs/test_structure.py index 015b61dc8..3c84ee476 100644 --- a/tests/docs/test_structure.py +++ b/tests/docs/test_structure.py @@ -727,7 +727,14 @@ def test_required_documentation_area_exists(relative_path: str) -> None: def test_documentation_root_uses_three_audience_lanes() -> None: directories = {path.name for path in DOCS_ROOT.iterdir() if path.is_dir()} root_pages = {path.name for path in DOCS_ROOT.glob("*.md")} - assert directories == {"user", "developer", "maintainer", "old_docs"} + assert directories == { + "user", + "developer", + "maintainer", + "javascripts", + "stylesheets", + "old_docs", + } assert root_pages == {"index.md"} @@ -871,9 +878,67 @@ def test_getting_started_overview_uses_standalone_example_and_current_evidence() assert "build_from_source/test_build_modes.py" in overview +def test_documentation_homepage_demonstrates_x2py_before_getting_started() -> None: + page = (DOCS_ROOT / "index.md").read_text(encoding="utf-8") + introduction_index = page.index("x2py turns supported Fortran source into an importable Python extension") + source_index = page.index("") + build_index = page.index("python3 -m x2py scale.f90") + call_index = page.index("result = scale.scale(np.float64(3.0), np.float64(2.5))") + output_index = page.index("7.5", call_index) + docstring_index = page.index("scale(value, factor) -> float64", output_index) + getting_started_index = page.index("[Getting Started](user/getting-started/index.md)") + + assert introduction_index < source_index < build_index < call_index < output_index + assert output_index < docstring_index < getting_started_index + assert "value : float64\nfactor : float64" in page + assert "result : float64" in page + assert "If an argument has an incompatible Python type or dtype." in page + assert "developer/index.md" not in page + assert "maintainer/README.md" not in page + assert "user/guide/" not in page + + +def test_documentation_links_to_documentation_stay_on_the_website() -> None: + github_documentation_prefixes = ( + "https://github.com/PyNumLab/x2py/blob/main/docs/", + "https://github.com/PyNumLab/x2py/tree/main/docs/", + ) + + for path in DOC_PATHS: + prose_lines: list[str] = [] + fence: str | None = None + for line in _visible_documentation_source(path).splitlines(): + marker = re.match(r"^\s*(`{3,}|~{3,})", line) + if fence is not None: + if line.strip() == fence: + fence = None + continue + if marker is not None: + fence = marker.group(1) + continue + prose_lines.append(line) + + for target in MARKDOWN_LINK.findall("\n".join(prose_lines)): + if "/" not in target and "." not in target: + continue + assert not target.startswith(github_documentation_prefixes), ( + f"{path.relative_to(ROOT)}: documentation link points to GitHub: {target}" + ) + if target.startswith(("http://", "https://", "mailto:")): + continue + resolved = (path.parent / target).resolve() + assert resolved != (ROOT / "README.md").resolve(), ( + f"{path.relative_to(ROOT)}: documentation workflow points to the repository README" + ) + if resolved.is_relative_to(DOCS_ROOT.resolve()): + assert resolved.is_file(), ( + f"{path.relative_to(ROOT)}: documentation link must target a website page or asset: {target}" + ) + + def test_first_wrapped_function_shows_contract_and_mentions_later_support_boundaries() -> None: page = (DOCS_ROOT / "user/getting-started/first-wrapped-function.md").read_text(encoding="utf-8") - source_index = page.index("[README Quick Start](../../../README.md#installation--quick-start)") + source_index = page.index("[homepage example](../../index.md#try-x2py)") build_index = page.index("python3 -m x2py scale.f90 \\") command_index = page.index("python3 -m x2py generate --pyi scale.f90") contract_index = page.index( @@ -902,7 +967,7 @@ def test_first_wrapped_module_shows_local_input_and_generated_contract() -> None def test_beginner_workflow_reuses_scale_example_without_renaming_it() -> None: page = (DOCS_ROOT / "user/getting-started/beginner-workflow.md").read_text(encoding="utf-8") - source_reference_index = page.index("[README Quick Start](../../../README.md#installation--quick-start)") + source_reference_index = page.index("[homepage example](../../index.md#try-x2py)") layout_index = page.index("src/\n scale.f90") contract_index = page.index("python3 -m x2py generate --pyi src/scale.f90") build_index = page.index("python3 -m x2py src/scale.f90 \\\n --out-dir build/scale") @@ -1116,3 +1181,28 @@ def test_old_top_level_documentation_was_moved(relative_path: str) -> None: def test_static_site_seed_configuration_exists() -> None: assert (ROOT / "mkdocs.yml").is_file() + + +def test_site_theme_keeps_sidebar_open_and_code_blocks_copyable() -> None: + site_configuration = (ROOT / "mkdocs.yml").read_text(encoding="utf-8") + assert "name: readthedocs" in site_configuration + assert "collapse_navigation: false" in site_configuration + assert "navigation_depth: 4" in site_configuration + assert "stylesheets/site.css" in site_configuration + assert "stylesheets/code-copy.css" in site_configuration + assert "javascripts/code-copy.js" in site_configuration + + script = (DOCS_ROOT / "javascripts" / "code-copy.js").read_text(encoding="utf-8") + layout_stylesheet = (DOCS_ROOT / "stylesheets" / "site.css").read_text(encoding="utf-8") + stylesheet = (DOCS_ROOT / "stylesheets" / "code-copy.css").read_text(encoding="utf-8") + assert ".wy-nav-content" in layout_stylesheet + assert "max-width: 1200px" in layout_stylesheet + assert "margin: 0" in layout_stylesheet + assert ".rst-content pre" in layout_stylesheet + assert "width: 100%" in layout_stylesheet + assert "max-width: 56rem" in layout_stylesheet + assert "padding-right: 3.25rem" in layout_stylesheet + assert 'document.querySelectorAll("pre code")' in script + assert "navigator.clipboard.writeText" in script + assert 'button.setAttribute("aria-label", "Copy code to clipboard")' in script + assert ".x2py-code-copy" in stylesheet diff --git a/tools/mkdocs_publication.py b/tools/mkdocs_publication.py index 3bd00cbb8..9ecf0ec67 100644 --- a/tools/mkdocs_publication.py +++ b/tools/mkdocs_publication.py @@ -132,7 +132,7 @@ def _repository_target(source_uri: str, raw_target: str) -> str | None: repository_root = _docs_dir.parent.resolve() if not resolved.is_relative_to(repository_root) or not resolved.exists(): return None - if resolved.is_relative_to(_docs_dir.resolve()) and resolved.is_file(): + if resolved.is_relative_to(_docs_dir.resolve()): return None route = "tree" if resolved.is_dir() else "blob" From 54b8dcba4c441591a7b758be6d9960ec0a50ffcb Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 00:03:05 +0100 Subject: [PATCH 04/35] improve docs --- docs/index.md | 73 ++- docs/maintainer/documentation-architecture.md | 31 +- .../documentation-content-checklist.md | 8 +- .../roadmap/native-array-handle-checklist.md | 14 +- .../wrapper-plan-migration-checklist.md | 56 +- docs/user/examples/verified-cookbook.md | 10 +- .../user/getting-started/beginner-workflow.md | 189 +++---- .../getting-started/first-wrapped-function.md | 96 ++-- .../getting-started/first-wrapped-module.md | 141 ++--- docs/user/getting-started/index.md | 52 +- docs/user/getting-started/installation.md | 109 ++-- docs/user/getting-started/verification.md | 146 ++--- docs/user/guide/allocatables.md | 514 ++---------------- docs/user/guide/arrays.md | 213 ++------ docs/user/guide/callbacks.md | 302 +++++----- docs/user/guide/data-types.md | 273 +++------- docs/user/guide/distribution.md | 140 ++--- .../guide/editing-semantic-pyi-contracts.md | 48 +- docs/user/guide/enumerations.md | 59 +- docs/user/guide/error-handling.md | 190 ++----- docs/user/guide/fortran-wrapper.md | 218 ++++---- docs/user/guide/generic-interfaces.md | 129 ++--- docs/user/guide/index.md | 71 ++- docs/user/guide/memory-management.md | 355 ++++++++---- docs/user/guide/optional-arguments.md | 107 ++-- docs/user/guide/packaging.md | 144 ++--- docs/user/guide/pointers.md | 314 ++--------- docs/user/guide/wrapping-derived-types.md | 306 +++-------- docs/user/guide/wrapping-functions.md | 173 ++---- docs/user/guide/wrapping-modules.md | 178 ++---- docs/user/guide/wrapping-subroutines.md | 182 ++----- docs/user/index.md | 23 +- docs/user/reference/callbacks.md | 40 +- docs/user/reference/semantic-pyi-format.md | 61 ++- mkdocs.yml | 3 +- .../wrapper/fallocatable_views_f90.f90 | 6 +- .../fortran/wrapper/farray_results_f90.f90 | 24 + tests/data/fortran/wrapper/fstrings_f90.f90 | 6 +- tests/docs/test_publication.py | 6 +- tests/docs/test_structure.py | 156 +++++- .../parser/c/fixtures/general/name_reuse.json | 64 +-- tests/pipeline/preprocessing/test_cli.py | 2 +- ...an_conversion_procedures_and_interfaces.py | 3 +- .../conversion/pyi/test_types_and_values.py | 51 +- tests/semantics/policy/test_wrapper_policy.py | 130 ++++- .../farray_results_f90/farray_results_f90.pyi | 11 + .../fortran/arrays/test_array_results.py | 52 +- .../test_compiler_verbose.py | 74 +++ .../fcallback_all_f90/fcallback_all_f90.pyi | 12 +- .../fcallback_array_f90.pyi | 4 +- .../fcallback_scalar_f90.pyi | 6 +- .../fallocatable_views_f90.pyi | 2 +- .../module_state/test_allocatable_views.py | 6 +- .../scalars/test_scalar_boundary_plan.py | 27 + .../contracts/fstrings_f90/fstrings_f90.pyi | 2 +- .../strings/test_character_arguments.py | 8 +- .../test_calls_and_policy_metadata.py | 37 +- .../wrapper_codegen/test_phase10_callbacks.py | 2 +- .../test_phase2e_scalar_boundaries.py | 44 +- .../test_phase7_native_array_handles.py | 29 +- tools/mkdocs_publication.py | 33 +- x2py/compiling/compiler_profiles.py | 3 + x2py/compiling/compilers.py | 46 +- x2py/semantics/ownership.py | 47 +- x2py/semantics/pyi2ir.py | 54 +- x2py/semantics/wrapper_policy.py | 147 ++++- x2py/wrapper_codegen/c/binding.py | 41 +- x2py/wrapper_codegen/fortran/bridge.py | 314 +++++------ x2py/wrapper_codegen/generator.py | 55 +- x2py/wrapper_codegen/printers/pyi_printer.py | 53 +- 70 files changed, 2906 insertions(+), 3619 deletions(-) diff --git a/docs/index.md b/docs/index.md index a922012bd..5a3dc88ed 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,6 @@ --- title: x2py +description: Turn Fortran into importable Python extensions with zero boilerplate audience: users prerequisites: none related: user/getting-started/index.md, user/getting-started/installation.md @@ -9,14 +10,16 @@ publication: reviewed # x2py -x2py turns supported Fortran source into an importable Python extension. It -also exposes the parsed interface as language-neutral semantic IR and editable -`.pyi` contracts, so unsupported boundaries are reported before wrapper code is -compiled. +**x2py turns supported Fortran source into fast, importable Python extensions.** -## Try x2py +It also generates a language-neutral semantic IR and editable `.pyi` +contracts, so unsupported boundaries are reported before wrapper compilation. -This first example wraps a scalar Fortran function. Create `scale.f90`: +--- + +## Try it in 30 seconds {#try-x2py} + +Create a file `scale.f90`: ```fortran @@ -27,41 +30,28 @@ real(8) function scale(value, factor) result(output) end function scale ``` -Build the Python extension from the directory containing that file: +Build the Python extension: ```bash python3 -m x2py scale.f90 ``` -The command creates an importable `scale` extension beside the source and keeps -its generated wrapper and build artifacts under `__x2py__/`. Call the native -function from Python with the exact NumPy scalar types required by its -contract: +Use it from Python: ```python import numpy as np - import scale result = scale.scale(np.float64(3.0), np.float64(2.5)) -print(result) +print(result) # 7.5 ``` -The call prints: - -```text -7.5 -``` - -The generated function is inspectable from Python: +Inspect the generated contract: ```python print(scale.scale.__doc__) ``` -Its docstring describes the public signature, accepted dtypes, result, and -call-time type error: - ```text scale(value, factor) -> float64 @@ -80,12 +70,35 @@ TypeError If an argument has an incompatible Python type or dtype. ``` -That is the basic x2py workflow: provide native source, build an extension, -import it, and call the generated Python surface. +--- + +## How it works -## Continue With Getting Started +1. You write standard Fortran +2. `x2py` parses the interface and generates a compact native wrapper +3. It produces a Python extension module and editable semantic `.pyi` contracts +4. You get full NumPy scalar dtype safety and clear error messages + +No manual `f2py` signatures. No low-level boilerplate. + +## Next steps + +[Getting Started](user/getting-started/index.md){ .md-button .md-button--primary } + +This guide walks you through installation, compiler setup, and a deeper look at the generated artifacts. + +--- + +## Features + +- Automatic generation of Python extensions from Fortran +- Language-neutral semantic IR +- Editable `.pyi` type stubs +- Strict NumPy dtype checking at call time +- Clean, readable `__doc__` strings +- Build artifacts isolated in `__x2py__/` + +--- -This preview assumes x2py, NumPy, and a supported native compiler are already -available. [Getting Started](user/getting-started/index.md) walks through -installation and verification first, then rebuilds this function and explains -its generated contract and artifacts. +**Ready to wrap your Fortran code?** +Start with the [Getting Started](user/getting-started/index.md) guide. diff --git a/docs/maintainer/documentation-architecture.md b/docs/maintainer/documentation-architecture.md index f6e9f6b40..9b6ddfc40 100644 --- a/docs/maintainer/documentation-architecture.md +++ b/docs/maintainer/documentation-architecture.md @@ -23,14 +23,16 @@ contains only pages explicitly marked as reviewed. lane: a draft lane index prevents every page below that lane from entering the production site, even when an individual child page is marked reviewed. 3. Implemented behavior is documented as supported only when current code and - tests prove it. + tests prove it. Public user pages describe behavior and limits without + exposing internal test-evidence ledgers. 4. Planned behavior is marked explicitly and never presented as an implemented user contract. 5. Maintainer policy and volatile internals do not appear in user workflows. 6. Historical material remains under `old_docs/` and outside active navigation. 7. User-facing source-driven examples show the complete input source before the command that consumes it. Generated paths must come from an immediately - preceding command, and commands show their expected result. + preceding command, and commands show their expected result. Fixture-backed + examples stay synchronized with their checked source. 8. The website keeps its documentation navigation expanded and renders an accessible copy control on every code block, including command-output and result blocks. @@ -66,8 +68,14 @@ the reader is leaving the current lane. The `nav` sequence in `mkdocs.yml` is the canonical reading order. Sequential User documentation pages may link back to pages the reader has already completed. They must not link from instructional prose to a later page in that -sequence. Name the later topic in plain text and say that it is covered later -instead of asking the reader to leave the current task. +sequence. Explicit terminal navigation blocks headed `Next` may link forward +because choosing a next destination is their purpose. Outside those blocks, +name the later topic in plain text and say that it is covered later instead of +asking the reader to leave the current task. +`Next` blocks list destinations as bullets, and each bullet includes at least +one Markdown link. If an intended destination page does not exist yet, either +remove the destination until it is useful or create the draft page with +metadata and a TODO section. Each page includes the behavior, warning, ownership fact, or limitation needed for its current task. A forward reference never defers a fact needed now. @@ -104,14 +112,13 @@ as `draft`. Production builds include a Markdown page only when: The publication hook removes every other Markdown page from the MkDocs file collection and navigation before rendering, so drafts do not enter generated -HTML, search, or the sitemap. When a reviewed index or overview mentions a -draft page, the production build renders that page name as plain text until the -target becomes publishable. Links to existing repository evidence outside the -`docs/` tree are rewritten to the matching file or directory on GitHub; links -to missing targets remain unchanged so the strict build can reject them. Links -to another active documentation page or directory must stay relative to -`docs/` and resolve inside the website. The hook never rewrites a target inside -`docs/` to GitHub. +HTML, search, or the sitemap. When a reviewed page mentions an unpublished +documentation page, the production build keeps the link visible with its +expected website route even though the target page itself is not published. +Links to existing repository evidence outside the `docs/` tree are rewritten to +the matching file or directory on GitHub; links to missing targets remain +unchanged so the strict build can reject them. Links to another active +documentation page or directory must stay relative to `docs/`. Use the normal local server to preview exactly what GitHub Pages will publish: diff --git a/docs/maintainer/roadmap/documentation-content-checklist.md b/docs/maintainer/roadmap/documentation-content-checklist.md index 78ea508bb..6db2b5ef4 100644 --- a/docs/maintainer/roadmap/documentation-content-checklist.md +++ b/docs/maintainer/roadmap/documentation-content-checklist.md @@ -25,14 +25,14 @@ these are true: - [ ] The page status is accurate: `maintained` for current public behavior, `design` for accepted architecture, or `not-yet-implemented` for explicit future behavior. -- [ ] The page explains what is supported now, what is unsupported, and where to - find the supporting tests, fixtures, examples, or source owner. +- [ ] The page explains what is supported now and what is unsupported without + exposing internal test-evidence ledgers in public user-facing prose. - [ ] User-facing pages include a task-oriented workflow, expected output or API shape, limitations, and troubleshooting links. - [ ] Developer-facing pages include ownership boundaries, source routes, focused verification commands, and rules for updating related docs. -- [ ] Examples are either executable documentation examples, checked fixtures, - or clearly labeled illustrative snippets. +- [ ] Examples are polished, copyable, and internally coherent. Executable + examples and fixture-backed source stay synchronized with their checks. - [ ] Reuse earlier examples by reference instead of reprinting them, unless the page must be self-contained for a first-time user task. - [ ] User-facing examples use clean copyable filenames and module names; keep diff --git a/docs/maintainer/roadmap/native-array-handle-checklist.md b/docs/maintainer/roadmap/native-array-handle-checklist.md index f49e6d463..6432f984a 100644 --- a/docs/maintainer/roadmap/native-array-handle-checklist.md +++ b/docs/maintainer/roadmap/native-array-handle-checklist.md @@ -649,16 +649,14 @@ specialize operation bodies by descriptor kind. stable owner storage. - [x] Use wrapper-owned standard C descriptor storage for allocatable results: allocate persistent rank-specific `CFI_CDESC_T(rank)` storage and establish - it with allocatable attribute. Numeric function results populate local - allocatable storage whose allocation is transferred with `move_alloc`; - generated shape-changing operations use `CFI_allocate`. -- [x] Assign a numeric direct allocatable function result once into a + it with allocatable attribute. Numeric function results populate a local + allocatable once, then transfer that allocation with `move_alloc`; generated + shape-changing operations use `CFI_allocate`. +- [x] Assign a supported numeric direct allocatable function result once into a bridge-local allocatable, then `move_alloc` that allocation into the allocatable `intent(out)` dummy backed by persistent CFI storage. Do not - generate a collector, an `allocated(...)` guard, or a second intrinsic - assignment. The native function must return an allocated, defined result; an - unallocated nonpointer result is a nonconforming native procedure and remains - the user's responsibility. + generate a collector or a second intrinsic assignment. Rank-one, matrix, and + higher-rank results preserve allocated, zero-sized, and unallocated state. - [x] Return a native pointer to owner storage for owned allocatable handles. - [x] Generate destroy routines called by the Python handle finalizer for owned allocatable handles. diff --git a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md index 56613114f..70f29ffd0 100644 --- a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md +++ b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md @@ -594,7 +594,7 @@ summary, the exhaustive matrix, and the test tree disagree. | Status | Collected nodes | | --- | ---: | -| `wrapper-plan` | 353 | +| `wrapper-plan` | 357 | | `dual-route` | 0 | | `legacy` | 0 | | `not-applicable` | 76 | @@ -2610,30 +2610,27 @@ Legacy oracle: ### Phase 7E — Owned Allocatable Results And Hidden Outputs -Included: rank-positive allocatable direct function results and hidden output -descriptors whose completed policy selects `owned_result_descriptor`. A valid -allocatable function result is allocated when returned; an unallocated -nonpointer function result is a nonconforming native procedure and the wrapper -does not compensate for it. An allocatable output dummy may validly remain -unallocated and still returns a present `AllocatableArray` handle whose state -lives inside that handle. Pointer handle results remain blocked until stable -owner storage and target lifetime are explicit. - -For a numeric direct allocatable function result, the bridge assigns the native -function expression once into a procedure-local allocatable and then uses -`move_alloc` to transfer that allocation into the allocatable `intent(out)` -dummy backed by persistent wrapper-owned `CFI_CDESC_T(rank)` storage. The move -does not copy the array payload. Do not insert a collector helper, an -`allocated(...)` guard, or a second intrinsic assignment. The native function -must return an allocated, defined result; an unallocated result is a -nonconforming native procedure and remains the user's responsibility rather -than a wrapper fallback. Other procedure-local storage remains permitted only -when representation conversion genuinely requires it, such as -deferred-character byte materialization. The binding constructs the complete -generated operation table and Python handle only after owner storage is valid. -Ownership transfers to the handle exactly once; every earlier failure path -releases the persistent allocation and any genuinely required bridge-local -allocation. +Included: allocatable array direct function results and hidden output +descriptors whose completed policy selects `owned_result_descriptor`. +Direct array results preserve allocated, zero-sized, and unallocated state, +including matrices and higher-rank arrays. An allocatable output dummy may +validly remain unallocated and still returns a present `AllocatableArray` handle +whose state lives inside that handle. Pointer handle results remain blocked +until stable owner storage and target lifetime are +explicit. + +For a supported numeric direct allocatable function result, the bridge assigns +the native function expression once into a procedure-local allocatable and then +uses `move_alloc` to transfer its state into the allocatable `intent(out)` dummy +backed by persistent wrapper-owned `CFI_CDESC_T(rank)` storage. The move does +not copy the array payload and preserves an unallocated rank-one result. Do not +insert a collector helper or a second intrinsic assignment. Other +procedure-local storage remains permitted only when representation conversion +genuinely requires it, such as deferred-character byte materialization. The +binding constructs the complete generated operation table and Python handle +only after owner storage is valid. Ownership transfers to the handle exactly +once; every earlier failure path releases persistent storage and any genuinely +required bridge-local allocation. Character-element handles carry runtime `elem_len` and declared element-length policy in the same descriptor record. Because a deferred character width is @@ -4431,8 +4428,8 @@ existing source/generated-`.pyi` runtime assertions are the behavioral oracle. | `callbacks/test_array_callbacks.py::test_immediate_dummy_procedure_converts_array_arguments_and_results[*]` | writable array view, shaped array result, outer-output identity, and reference writeback | array argument/result slice | | `callbacks/test_all_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[*]` | scalar values, fixed strings, arrays, derived values, non-scalar reference writeback, and one combined call envelope | cross-kind closure slice | | `callbacks/test_derived_callbacks.py::test_immediate_dummy_procedure_converts_derived_arguments_and_results[*]` | callback-local borrowed derived input plus wrapper-owned derived result conversion | derived slice after Phase 9 construction | -| `callbacks/test_callback_generated_pyi_contracts.py` | named prototypes, reference-default and `Value(T)` transport, shape, character storage, cross-module identity, and result annotations round-trip exactly | semantic-contract parity slice | -| `semantics/conversion/pyi/test_types_and_values.py` callback cases | prototype declarations and references, `Value(T)`, exact argument names used by shapes, and unnecessary `Addr(...)` prototype forms | policy completion before planner work | +| `callbacks/test_callback_generated_pyi_contracts.py` | named prototypes, primitive value defaults, explicit primitive `Addr(T)` references, non-primitive `Value(T)` transport, shape, character storage, cross-module identity, and result annotations round-trip exactly | semantic-contract parity slice | +| `semantics/conversion/pyi/test_types_and_values.py` callback cases | prototype declarations and references, primitive `Addr(T)`, non-primitive `Value(T)`, exact argument names used by shapes, and invalid prototype transport forms | policy completion before planner work | ### Phase 10 Plan Shape And Action Vocabulary @@ -4504,12 +4501,13 @@ For every dependency-closed sub-lane: call scope, context lifetime, same-thread rule, GIL rule, cleanup, and fatal-error behavior. - [x] Preserve generated and edited named prototypes exactly. Reject an - incomplete prototype reference, unnecessary `Addr`, optional procedure, + incomplete prototype reference, invalid prototype `Addr`, optional procedure, stored/procedure-pointer lifetime, unavailable mandatory native interface, or unsupported result with the owner path and one exact reason. - [x] Complete callback signature/result/ownership policy before wrapper planning; lowering may only project the completed callback record. -- [x] Add policy/planning tests for reference-default transport, `Value(T)`, +- [x] Add policy/planning tests for primitive value defaults, explicit + primitive `Addr(T)` references, non-primitive `Value(T)`, and retained unsupported forms before planner changes. ### Phase 10B — Typed Callback Plan And Validation diff --git a/docs/user/examples/verified-cookbook.md b/docs/user/examples/verified-cookbook.md index 4438809da..325543669 100644 --- a/docs/user/examples/verified-cookbook.md +++ b/docs/user/examples/verified-cookbook.md @@ -15,8 +15,8 @@ uses checked repository fixtures where the command output is stable. Start with the [basic wrapper tutorial](../tutorials/basic-wrapper.md) if this is your first x2py workflow. Use the [Fortran wrapper guide](../guide/fortran-wrapper.md) for the full runtime -contract and [Semantic .pyi Format](../reference/semantic-pyi-format.md) for -editable wrapper contracts. +contract. The Semantic .pyi Format reference gives the complete editable +wrapper contract syntax. ## Choose A Recipe @@ -65,6 +65,6 @@ X2PY_C_DOCS_END --> - [Basic wrapper tutorial](../tutorials/basic-wrapper.md) - [Fortran wrapper guide](../guide/fortran-wrapper.md) -- [Semantic .pyi Format](../reference/semantic-pyi-format.md) -- [Semantic IR Reference](../reference/semantic-ir.md) -- [Diagnostic Codes](../reference/diagnostic-codes.md) +- Semantic .pyi Format +- Semantic IR Reference +- Diagnostic Codes diff --git a/docs/user/getting-started/beginner-workflow.md b/docs/user/getting-started/beginner-workflow.md index 722c6641f..c01ff4db1 100644 --- a/docs/user/getting-started/beginner-workflow.md +++ b/docs/user/getting-started/beginner-workflow.md @@ -1,5 +1,6 @@ --- title: Common Beginner Workflow +description: Recommended development loop — edit, review contract, build, test, and rebuild audience: users prerequisites: first wrapped module related: ../tutorials/basic-wrapper.md, ../examples/verified-cookbook.md, ../reference/cli-commands.md @@ -9,169 +10,121 @@ publication: reviewed # Common Beginner Workflow -You have already built and called the `scale.f90` example. This page turns that -same file into a repeatable project workflow: keep source under `src/`, build -into `build/`, run a small Python check, and cleanly rebuild when the native -contract changes. +Now that you have built individual examples, here is a clean, repeatable workflow you can use for your own projects. +The example project continues to use `scale.f90`. -Use the `scale.f90` input from the -[homepage example](../../index.md#try-x2py). Keep the same filename when -you move it into a project layout. - -## 1. Create A Small Project Layout - -Keep native sources under `src/` and Python tests under `tests/`. Treat every -file under `build/` as generated output that the next build may replace. +--- -Start with this layout: +## Recommended Project Layout -```text -scale-project/ - src/ - scale.f90 - build/ - tests/ - test_scale.py +``` +my-project/ +├── src/ +│ └── scale.f90 +├── build/ # ← Generated, do not commit +├── tests/ +│ └── test_scale.py +└── contracts/ # Optional: edited semantic contracts ``` -Run the remaining commands from `scale-project/`. Keep `src/` and `tests/` -under version control. Do not commit `build/`. +Keep `src/` and `tests/` under version control. Never commit the `build/` folder. - +--- -## 2. Review The Contract Before Compiling +## 1. Review the Contract First -Before building, print the semantic `.pyi` contract for the same source: +Before building, always inspect the generated contract: ```bash python3 -m x2py generate --pyi src/scale.f90 ``` -The output should match the wrapper contract from the -[First Wrapped Function](first-wrapped-function.md) page: - -```python -from x2py.contracts import Addr, Arg, Float64, external, native_call - -@external -@native_call([Addr(Arg(0)), Addr(Arg(1))]) -def scale( - value: Float64, - factor: Float64 -) -> Float64: ... -``` +This shows you exactly what Python signatures and dtypes x2py expects. -This confirms the Python-facing dtype contract and the native scalar-address -projection that code generation will follow. It does not prove that the -compiler, linker, native dependency set, or runtime environment is valid; the -build and smoke test still need to run. +--- -## 3. Build Into An Explicit Directory +## 2. Build the Extension ```bash -python3 -m x2py src/scale.f90 \ - --out-dir build/scale +python3 -m x2py src/scale.f90 --out-dir build/scale ``` -Build output goes under `build/scale`, leaving `src/scale.f90` untouched. Use -`--verbose` when you need exact compiler and linker commands in build logs. +Use `--verbose` if you want to see the exact compiler and linker commands. + +--- -## 4. Run A Python Smoke Test +## 3. Write a Small Smoke Test -Put this in `tests/test_scale.py`, or run it directly while learning the flow: +Create `tests/test_scale.py`: ```python import sys - import numpy as np sys.path.insert(0, "build/scale") import scale -result = scale.scale(np.float64(3.0), np.float64(2.5)) -assert result == np.float64(7.5) -``` - -Do not stop at “the extension imports.” For each wrapped routine, keep at least -one asserted result. For real projects, also add failure checks that matter to -the contract: wrong dtype, wrong rank or shape, non-writable outputs, or -unsupported optional arguments. The generated `.pyi` defines the current call -contract; the language feature matrix later collects support boundaries and -their focused evidence. - -## 5. Review Generated Artifacts +def test_scale_function(): + result = scale.scale(np.float64(3.0), np.float64(2.5)) + assert result == 7.5 + assert isinstance(result, float) -You normally do not need to open generated files. When debugging, expect -`build/scale` to contain: +if __name__ == "__main__": + test_scale_function() + print("✅ Test passed") +``` -| Artifact | Purpose | -| --- | --- | -| `binding_support/` | header-only native binding support | -| `.o` and `.mod` files | native intermediates | -| `.` | importable extension | +Run it with: - +```bash +python3 -m pytest tests/test_scale.py -q +# or simply: +python3 tests/test_scale.py +``` -Treat these as diagnostic evidence, not editable API definitions. Change the -native source or an intentional semantic `.pyi` contract instead. +--- -## 6. Rebuild Cleanly When The Contract Changes +## 4. Clean Rebuild When Needed -For a normal rerun, execute the same build command. After changing source order, -compiler flags, native dependencies, or the wrapper contract, remove the -selected output directory first: +When you change the Fortran source or want a completely clean build: ```bash rm -rf build/scale python3 -m x2py src/scale.f90 --out-dir build/scale ``` -The advanced Makefile workflow is available when you intentionally want -inspectable commands and manual rebuild control. Makefile generation and -`--verbose` are separate modes and cannot be combined. +--- -## Advanced Next Step: Edit The Semantic Contract +## 5. Advanced: Editing the Semantic Contract (Optional) -Stay with source-driven builds until the normal loop is clear. When you need to -review or intentionally edit the semantic `.pyi` contract, generate it -separately: +Only do this after you are comfortable with the basic workflow: ```bash python3 -m x2py generate --pyi src/scale.f90 --out contracts ``` -Do not treat this as the beginner default. A runtime build from an edited `.pyi` -must also receive the native implementation explicitly through options such as -`--native-fortran-sources`, `--native-objects`, or native libraries. Editing -Semantic `.pyi` Contracts later provides the complete workflow; do not use that -path until its ownership and native-artifact requirements are understood. - -## Failure Routing - -1. If `.pyi` generation fails, fix preprocessing, parsing, or semantic diagnostics. -2. If compilation or linking fails, rebuild with `--verbose` and inspect - the emitted native commands. Build Issues is covered later. -3. If import or runtime behavior fails, compare the artifact and call with the - generated contract. Runtime Issues is covered later. -4. If a documented supported behavior fails, reproduce it with the focused - wrapper test linked by the feature matrix before escalating to full CI. - -Support boundaries are collected later in the language feature matrix. Platform -and toolchain requirements are established in [Installation](installation.md), -and Distribution later explains artifact portability. - -## Evidence - -CLI build modes, output placement, and clean artifact expectations are checked -by [`test_build_modes.py`](../../../tests/wrapper/fortran/build_from_source/test_build_modes.py). -The source-driven runtime call is checked by -[`test_runtime_abi.py`](../../../tests/wrapper/fortran/build_from_source/test_runtime_abi.py), -and semantic `.pyi` build requirements by -[`test_pyi_wrapper_builds.py`](../../../tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py). +Editing contracts is powerful but adds complexity. See **Editing Semantic .pyi Contracts** in the User Guide when you're ready. + +--- + +## Summary of the Workflow + +1. Edit Fortran source in `src/` +2. Review contract with `generate --pyi` +3. Build with explicit `--out-dir` +4. Test with a Python smoke test +5. Clean rebuild when necessary (`rm -rf build/...`) + +--- + +## Next + +- Explore the full [User Guide](../guide/index.md) +- Check the [Language Feature Matrix](../language-support/feature-matrix.md) +- Look at the [Verified Cookbook](../examples/verified-cookbook.md) for more examples + +--- + +**Troubleshooting** +Use `--verbose` on build failures. +Always compare failing calls with the generated contract. diff --git a/docs/user/getting-started/first-wrapped-function.md b/docs/user/getting-started/first-wrapped-function.md index 3d76ef6ca..971aed5a6 100644 --- a/docs/user/getting-started/first-wrapped-function.md +++ b/docs/user/getting-started/first-wrapped-function.md @@ -1,5 +1,6 @@ --- title: First Wrapped Function +description: Build and call your first Fortran function as a Python extension audience: users prerequisites: installation, verification related: first-wrapped-module.md, ../guide/wrapping-functions.md, ../reference/semantic-pyi-format.md @@ -9,56 +10,63 @@ publication: reviewed # First Wrapped Function -This example builds one checked scalar function and calls it with the exact -NumPy dtypes required by its native contract. +This example shows how to build a simple scalar Fortran function and call it from Python using the exact NumPy dtypes required by its contract. -## Source +--- + +## Source Code -Reuse the same `scale.f90` input from the -[homepage example](../../index.md#try-x2py). +Use the same `scale.f90` from the homepage: -The generated Python call accepts two `numpy.float64` values and returns a -Python `float` result. +```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 +``` + +--- -## Build +## Build the Extension -From the directory containing `scale.f90`: +From the directory containing `scale.f90`, run: ```bash -python3 -m x2py scale.f90 \ - --out-dir build/first-function +python3 -m x2py scale.f90 --out-dir build/first-function ``` -The extension is named after the source stem: `scale`. The standalone native -function is exposed directly at that extension's root. +This creates an importable `scale` extension module in the `build/first-function` directory. -## Import And Call +--- + +## Import and Call ```python import sys - import numpy as np sys.path.insert(0, "build/first-function") import scale result = scale.scale(np.float64(3.0), np.float64(2.5)) +print(result) # → 7.5 assert isinstance(result, float) assert result == 7.5 ``` -The checked call returns the Python value `7.5`. +--- -## Inspect The Generated Signature +## Inspect the Generated Contract -Before compiling, print the semantic contract: +You can preview the semantic interface without building: ```bash python3 -m x2py generate --pyi scale.f90 ``` -The generated declaration is: +The generated contract has this shape: ```python from x2py.contracts import Addr, Arg, Float64, external, native_call @@ -71,46 +79,34 @@ def scale( ) -> Float64: ... ``` -The contract describes `value` and `factor` as read-only `Float64` values at -the Python boundary. The `@native_call` decorator records that the native call -receives the address of each converted native scalar slot. It does not mean the -caller passes references. The semantic `.pyi` is a native contract, not an -ordinary pure-Python type stub. Do not edit it during this first workflow; the -Semantic `.pyi` Format reference explains the complete grammar later. +This contract is the source of truth for the generated wrapper. -Fortran `intent` is not printed into the semantic `.pyi`. It helps generate the -initial Python argument/result projection, but the visible signature, -`Returns[...]`, and ordered `@native_call` list are the wrapper authority after -the contract is loaded. The compiled Fortran procedure retains its own `intent`. +--- -## Failure Mode: Wrong Scalar Type +## Common Pitfall: Wrong Scalar Type -Native scalar arguments use exact NumPy dtypes. A plain Python `float` is not a -replacement for `numpy.float64` at this boundary: +You **must** pass the exact NumPy scalar types: ```python -scale.scale(3.0, 2.5) # raises TypeError -``` +# This will raise TypeError +scale.scale(3.0, 2.5) -Do not fix this by adding an implicit conversion inside generated code. Convert -at the Python call site so the selected ABI is explicit: - -```python +# Correct way scale.scale(np.float64(3.0), np.float64(2.5)) ``` -For array functions, rank, dtype, shape, order, contiguity, and allowed stride -patterns can also be contract requirements. Wrapping Functions and Arrays -expand those rules later in the User Guide. The language feature matrix later -records supported, partial, and unsupported wrapper forms. +Always convert at the call site for scalar arguments. -Build Issues and Runtime Issues are covered later in Troubleshooting. For now, -rerun failed builds with `--verbose`, and compare rejected calls with the -generated `.pyi` contract. +--- + +## Next -## Evidence +- Learn how to wrap [Fortran modules](first-wrapped-module.md) +- Read more about [wrapping functions](../guide/wrapping-functions.md) +- Understand the [semantic .pyi format](../reference/semantic-pyi-format.md) + +--- -The linked `scale.f90` input is checked against the repository fixture by -[`test_examples.py`](../../../tests/docs/test_examples.py). -The default extension name and `7.5` runtime result are checked by -[`test_build_modes.py`](../../../tests/wrapper/fortran/build_from_source/test_build_modes.py). +**Troubleshooting** +If the build fails, rerun with `--verbose`. +If the call fails, compare your arguments with the generated contract. diff --git a/docs/user/getting-started/first-wrapped-module.md b/docs/user/getting-started/first-wrapped-module.md index 874c392e0..3812b8580 100644 --- a/docs/user/getting-started/first-wrapped-module.md +++ b/docs/user/getting-started/first-wrapped-module.md @@ -1,5 +1,6 @@ --- title: First Wrapped Module +description: Wrap a Fortran module with public procedures and state variables audience: users prerequisites: first wrapped function related: beginner-workflow.md, ../guide/wrapping-modules.md, ../language-support/feature-matrix.md @@ -9,18 +10,19 @@ publication: reviewed # First Wrapped Module -A Fortran module becomes a child Python module inside the extension. Public -procedures and supported public state appear on that child; private native -names and internal getter/setter hooks do not. +A Fortran `module` becomes a **child namespace** inside the generated Python extension. Public procedures and supported public variables are exposed under that namespace. -## Source +--- + +## Source Code -Create `module_state.f90` with this module: +Create a file named `module_state.f90`: ```fortran module module_state implicit none private + public :: nmax, counter, scale, saved_counter public :: summarize, scaled_counter, next_local @@ -31,6 +33,7 @@ module module_state integer(4) :: hidden_counter = 17 contains + integer(4) function summarize() result(value) value = counter + nmax end function summarize @@ -41,135 +44,89 @@ contains integer(4) function next_local() result(value) integer(4), save :: local_counter = 0 - local_counter = local_counter + 1 value = local_counter end function next_local + end module module_state ``` -## Build +--- + +## Build the Extension -From the directory containing `module_state.f90`: +Run the following command: ```bash -python3 -m x2py module_state.f90 \ - --out-dir build/first-module +python3 -m x2py module_state.f90 --out-dir build/first-module ``` -The source stem creates extension `module_state`. Its contained module is -available as `module_state.module_state`. +The extension will be named `module_state`, and the Fortran module will be available as `module_state.module_state`. -## Read Procedures And State +--- + +## Usage Example ```python import sys - import numpy as np sys.path.insert(0, "build/first-module") import module_state -module = module_state.module_state +mod = module_state.module_state -assert module.nmax == np.int32(12) -assert module.counter == np.int32(3) -assert module.scale == np.float64(1.5) -assert module.summarize() == np.int32(15) -``` +print(mod.nmax) # 12 +print(mod.counter) # 3 +print(mod.scale) # 1.5 -The generated surface exposes public variable names directly. Internal native -helpers such as `get_counter` and `set_counter`, and private names such as -`hidden_counter`, are not part of the Python API. +print(mod.summarize()) # 15 +print(mod.scaled_counter()) # 4.5 +``` -## Mutate Module State +--- -Writable state is assigned through the public attribute with its exact NumPy -dtype: +## Mutating Module State ```python -module.counter = np.int32(9) -assert module.counter == np.int32(9) -assert module.summarize() == np.int32(21) +mod.counter = np.int32(9) +print(mod.summarize()) # 21 -module.scale = np.float64(2.0) -assert module.scaled_counter() == np.float64(18.0) +mod.scale = np.float64(2.0) +print(mod.scaled_counter()) # 18.0 ``` -Supported saved state is native process state. Importing a second extension -module object does not create a second copy of the underlying writable Fortran -state; updates are visible through both wrappers. Python-side values that are -not backed by a native setter can differ between module objects, so do not infer -native mutability from assignment success alone. - -Procedure-local saved state also persists across calls: +Procedure-local `save` variables also persist across calls: ```python -assert module.next_local() == np.int32(1) -assert module.next_local() == np.int32(2) +print(mod.next_local()) # 1 +print(mod.next_local()) # 2 ``` -## Public Surface Rules +--- -- The extension name comes from the first source filename. -- Each contained Fortran module is a Python child namespace. -- Public procedures use their generated Python names under that namespace. -- Supported writable module variables use direct attributes; generated native - accessors stay hidden. -- Constants and parameters may be readable without a native setter. -- Private Fortran declarations remain absent from the public wrapper. +## Inspect the Contract -Use `--pyi` to inspect names and types before building: +Preview the generated interface without building: ```bash python3 -m x2py generate --pyi module_state.f90 ``` -The generated package entry preserves the module namespace: - -```python -from . import module_state -``` - -The generated module leaf remains the native contract: - -```python -from x2py.contracts import Final, Float64, Int32 - -nmax: Final[Int32] = 12 - -counter: Int32 - -scale: Float64 - -saved_counter: Int32 - -def summarize() -> Int32: ... - -def scaled_counter() -> Float64: ... - -def next_local() -> Int32: ... -``` +--- -The entry file is Python export policy. Advanced contract editing can flatten -the child module, select only some declarations, or expose one native declaration -under multiple Python names. Those edits reshape Python exports only; they are -not native ABI changes. Keep the leaf contract as the source of native facts and -wait for Editing Semantic `.pyi` Contracts later in the User Guide before -changing the generated contract package. +## Key Rules -The language feature matrix later collects support boundaries for module state -and other Fortran constructs. +- The extension name is derived from the source filename. +- Each Fortran `module` becomes a child Python namespace. +- Only **public** entities are exposed. +- Private variables (like `hidden_counter`) are hidden. +- Use exact NumPy dtypes when reading/writing variables. -If the extension imports but a name is absent, inspect the generated `.pyi`, -then check native visibility. Runtime Issues later expands this diagnosis. +--- -## Evidence +## Next -The same module-state behavior, hidden accessors, mutation, saved state, and -repeated import behavior are checked by the internal fixture tests in -[`test_module_state.py`](../../../tests/wrapper/fortran/module_state/test_module_state.py). -Generated module contracts are checked by -[`test_module_state_generated_pyi_contracts.py`](../../../tests/wrapper/fortran/module_state/test_module_state_generated_pyi_contracts.py). -The advanced entry export policy linked from this page is checked by -[`test_pyi_wrapper_builds.py`](../../../tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py). +- Continue with the [Beginner Workflow](beginner-workflow.md) to learn the full development cycle. +- For module details, see [Wrapping Modules](../guide/wrapping-modules.md). +- For support boundaries, see the [Language Feature Matrix](../language-support/feature-matrix.md). diff --git a/docs/user/getting-started/index.md b/docs/user/getting-started/index.md index d8b1d9e25..c345f90e7 100644 --- a/docs/user/getting-started/index.md +++ b/docs/user/getting-started/index.md @@ -1,5 +1,6 @@ --- title: Getting Started +description: Install x2py, set up compilers, and build your first Fortran-to-Python extension audience: users prerequisites: repository checkout related: installation.md, verification.md @@ -9,35 +10,27 @@ publication: reviewed # Getting Started -This section takes you from a source checkout to an imported Python extension. -The supported beginner path wraps Fortran source with the GNU native toolchain. +This guide takes you from a fresh clone to your first working Python extension built from Fortran code. - +The recommended beginner path uses the **GNU toolchain**, which offers the best compatibility right now. + +--- ## Beginner Path Follow these pages in order: -1. [Install x2py and its native prerequisites](installation.md). -2. [Verify Python, NumPy, the CLI, and the compilers](verification.md). -3. [Build and call a scalar function](first-wrapped-function.md). -4. [Work with a Fortran module and its saved state](first-wrapped-module.md). -5. [Use the normal edit, inspect, build, test, and rebuild loop](beginner-workflow.md). +1. **[Installation](installation.md)** — Install x2py and the required native compilers. +2. **[Verification](verification.md)** — Make sure everything works together. +3. **[Your First Function](first-wrapped-function.md)** — Wrap a simple scalar Fortran function. +4. **[Your First Module](first-wrapped-module.md)** — Work with Fortran modules and saved state. +5. **[Development Workflow](beginner-workflow.md)** — Learn the edit → build → test loop. -## What You Will Build +--- -The checked beginner example exposes a Fortran function through an importable -Python extension: +## What You Will Build - +By the end of this section you will be able to write Fortran and call it cleanly from Python: ```python import numpy as np @@ -45,21 +38,14 @@ import numpy as np import scale result = scale.scale(np.float64(3.0), np.float64(2.5)) -assert result == np.float64(7.5) +print(result) # → 7.5 ``` -The first example is a standalone procedure exposed directly at the extension -root. The next module example introduces contained Fortran modules as Python -child namespaces. +The first example exposes a standalone Fortran function directly on the extension. +Later examples show how Fortran modules become Python namespaces. -Check the [language feature matrix](../language-support/feature-matrix.md) before -depending on an advanced construct. Installation, compiler, build, and import -failures are routed through [Troubleshooting](../troubleshooting/index.md). +--- -## Evidence +## Next -The standalone example used throughout this section is checked against its -fixture by -[`test_examples.py`](../../../tests/docs/test_examples.py), -and its `7.5` runtime result is checked by -[`test_build_modes.py`](../../../tests/wrapper/fortran/build_from_source/test_build_modes.py). +- Start with [Installation](installation.md). diff --git a/docs/user/getting-started/installation.md b/docs/user/getting-started/installation.md index d8b540482..aa8a00a78 100644 --- a/docs/user/getting-started/installation.md +++ b/docs/user/getting-started/installation.md @@ -1,5 +1,6 @@ --- title: Installation +description: Install x2py from source and set up the native GNU toolchain audience: users, contributors prerequisites: Python 3.10 or newer, repository checkout related: verification.md, ../troubleshooting/installation-issues.md, ../../developer/quality-assurance.md @@ -9,60 +10,44 @@ publication: reviewed # Installation -x2py is currently installed from a source checkout. A runtime wrapper build -needs both the Python package and a native GNU toolchain. +x2py is currently installed from a local source checkout. Building Python extensions also requires a native GNU toolchain. + +--- ## Supported Python Versions -The package metadata requires Python 3.10 or newer. GitHub Actions currently -tests Python 3.10, 3.11, and 3.12 on Ubuntu 24.04. A newer Python may satisfy -the package constraint but is not part of the current CI matrix. +x2py requires **Python 3.10 or newer**. +The project is regularly tested on Python 3.10, 3.11, and 3.12. -Check the interpreter before creating the environment: +Check your Python version first: ```bash python3 --version ``` -## Native Prerequisites - -Install these before attempting a wrapper build: +--- -- GNU Fortran (`gfortran`) for preprocessing, type probes, and native builds; -- Python development headers matching the active interpreter; -- NumPy, whose installed package supplies the required development files; -- a native linker supplied by the compiler toolchain. +## Native Prerequisites - +Install these packages before building wrappers: -GNU Make is optional. Direct builds do not require it. The generated Makefile -workflow is an advanced build mode that expects GNU Make and a POSIX-style -shell. +- `gfortran` (GNU Fortran compiler) +- `python3-dev` (Python development headers) +- NumPy (includes required development files) +- `build-essential` (linker and build tools) -On Ubuntu or Debian, the prerequisite packages normally come from: +On **Ubuntu / Debian**: ```bash sudo apt-get update sudo apt-get install build-essential gfortran python3-dev ``` - - -The checked CI target uses Ubuntu 24.04 and `gfortran-13`. Package names and -compiler locations differ on other Linux distributions. +--- ## User Installation -Create an isolated environment from the repository root and install the -checkout in editable mode: +From the root of the cloned repository, run: ```bash python3 -m venv .venv @@ -71,65 +56,49 @@ python3 -m pip install --upgrade pip python3 -m pip install -e . ``` -The installation pulls the runtime Python dependencies declared by the -project, including NumPy, `filelock`, and `immutabledict`. +This installs x2py in editable mode along with its runtime dependencies (including NumPy). + +--- ## Contributor Installation -Contributors should install the optional QA dependencies as well: +If you are contributing code or running tests, also install the QA tools: ```bash python3 -m pip install -e ".[qa]" ``` -The `qa` extra includes pytest, coverage, Hypothesis, Ruff, Bandit, Vulture, -and Radon. These tools are not required merely to import x2py or build a wrapper. +--- -## Header And Compiler Checks +## Header and Compiler Checks -Verify that the active environment can locate its development headers: +Run these commands to verify your setup: ```bash +# Python and NumPy headers python3 -c "import sysconfig; print(sysconfig.get_path('include'))" python3 -c "import numpy; print(numpy.get_include())" -``` - -Verify the compiler executables independently: -```bash +# Compiler gfortran --version ``` - - -Continue with the Verification page only after these commands succeed and the -printed header directories exist. +Make sure the printed header directories exist. -## Platform Caveats +--- -| Platform | Current status | -| --- | --- | -| Ubuntu Linux | CI-verified with Ubuntu 24.04, Python 3.10-3.12, and `gfortran-13`. | -| Other Linux distributions | Expected to require equivalent GNU compilers and development headers; package names and ABI details are not CI-verified. | -| macOS | Not in the current wrapper CI matrix. Compiler discovery, extension suffixes, linker flags, and runtime library paths need platform validation. | -| Windows | Not in the current wrapper CI matrix. The direct GNU/POSIX build assumptions and generated Makefile workflow are not established as supported. | +## Platform Support -Do not interpret successful contract-generation or diagnostic commands as proof -that the native wrapper toolchain works on an unverified platform. +| Platform | Current Status | +|--------------------|-----------------------------------------| +| Ubuntu Linux | CI-verified (Ubuntu 24.04 + gfortran-13) | +| Other Linux | Expected to work with GNU tools | +| macOS | Not yet in CI matrix | +| Windows | Not yet supported | -## Evidence And Troubleshooting +--- -Dependency and version declarations live in -[`pyproject.toml`](../../../pyproject.toml). The current CI environment is defined -in [`.github/workflows/quality.yml`](../../../.github/workflows/quality.yml), and -the compiler/header configuration is exercised by -[`test_runtime_abi.py`](../../../tests/wrapper/fortran/build_from_source/test_runtime_abi.py). +## Next -For missing packages, headers, or virtual-environment problems, the later -Installation Issues page provides focused checks. Compiler discovery and -linking problems are covered later under Compiler Issues. +- Go to the [Verification](verification.md) page after the checks above pass. +- If setup fails, see [Installation Issues](../troubleshooting/installation-issues.md). diff --git a/docs/user/getting-started/verification.md b/docs/user/getting-started/verification.md index d73c5dba3..efcead28e 100644 --- a/docs/user/getting-started/verification.md +++ b/docs/user/getting-started/verification.md @@ -1,5 +1,6 @@ --- title: Verification +description: Verify that x2py, NumPy, and the native toolchain are working correctly audience: users, contributors prerequisites: installation related: first-wrapped-function.md, ../troubleshooting/index.md, ../reference/cli-commands.md @@ -9,139 +10,104 @@ publication: reviewed # Verification -Verify the Python environment, contract-generation path, and native build path -separately. This makes a failure easier to route. +After installation, verify the Python package, contract generation, and native build toolchain separately. This makes debugging much easier. -## 1. Verify The Installed Package +--- + +## 1. Verify the Installed Package -Run these commands from the activated environment: +Run these commands in your activated virtual environment: ```bash -python3 -c "from importlib.metadata import version; import x2py; print(version('x2py'))" -python3 -c "import numpy; print(numpy.__version__)" +# Check x2py and NumPy +python3 -c "from importlib.metadata import version; import x2py, numpy; print('x2py:', version('x2py')); print('NumPy:', numpy.__version__)" + +# Check CLI entrypoint python3 -m x2py --help ``` -The first two commands prove that x2py and NumPy import from the selected -interpreter. The third proves that the module entrypoint is installed. +--- -## 2. Verify The Contract Path +## 2. Verify Contract Generation -Use the `scale.f90` input created in the -[homepage example](../../index.md#try-x2py). +Use the `scale.f90` file from the homepage example. -From the directory containing `scale.f90`, print the semantic `.pyi` contract -without compiling a wrapper: +Run this command in the directory containing `scale.f90`: ```bash python3 -m x2py generate --pyi scale.f90 ``` -The generated declaration should look like: - -```python -from x2py.contracts import Addr, Arg, Float64, external, native_call - -@external -@native_call([Addr(Arg(0)), Addr(Arg(1))]) -def scale( - value: Float64, - factor: Float64 -) -> Float64: ... -``` +You should see a clean `.pyi`-style contract. This confirms parsing, semantic analysis, and type probing work correctly. -This verifies preprocessing, parsing, semantic lowering, type probing, and -contract printing. It does not compile or import an extension. +--- -## 3. Verify The Native Toolchain +## 3. Verify Native Build Toolchain -Check compiler discovery before running a build: +First, check the compiler: ```bash gfortran --version ``` - - -From the same directory, build `scale.f90` into a dedicated directory: +Then build the extension: ```bash -python3 -m x2py scale.f90 \ - --out-dir build/verify +python3 -m x2py scale.f90 --out-dir build/verify ``` -The command must create: +This should create an importable `scale` module inside `build/verify`. -- an importable `scale` extension under `build/verify`; and -- generated native bridge, object, native-support, and extension files. - - - -Import the extension from that build directory: +Test it: ```python import sys - import numpy as np sys.path.insert(0, "build/verify") import scale -assert scale.scale(np.float64(3.0), np.float64(2.5)) == np.float64(7.5) +result = scale.scale(np.float64(3.0), np.float64(2.5)) +print(result) # Should print 7.5 ``` -## 4. Inspect Generated Files +--- -`WrapperBuildResult` is the stable way to inspect a Python API build: +## 4. Inspect Generated Files (Optional) -```python -from pathlib import Path +You can also inspect the build programmatically: +```python from x2py import build_fortran_extension -build = build_fortran_extension( - "scale.f90", - output_dir="build/verify", -) +build = build_fortran_extension("scale.f90", output_dir="build/verify") -assert build.compiled -assert build.shared_library.is_file() -assert all(Path(path).exists() for path in build.generated_files) -print(build.output_dir) -print(build.shared_library) +print("Compiled:", build.compiled) +print("Shared library:", build.shared_library) +print("Output directory:", build.output_dir) +``` + +For detailed output when something fails, add `--verbose`: + +```bash +python3 -m x2py scale.f90 --out-dir build/verify --verbose ``` -For CLI builds, add `--verbose` when a compiler or linker command fails; it -prints the exact native commands and stage timings. - -## Escalation Path - -| Failure | Next action | -| --- | --- | -| `import x2py` or `import numpy` fails | Recheck the active interpreter; Installation Issues is covered later. | -| `--help` works but `.pyi` generation fails | Read the diagnostic; diagnostic codes are catalogued later in the reference section. | -| Compiler executable is missing | Recheck `PATH` and the compiler command; Compiler Issues is covered later. | -| Native compilation or linking fails | Rebuild with `--verbose`; Build Issues is covered later. | -| Build succeeds but import or call fails | Compare the call with the generated contract; Runtime Issues is covered later. | -| The checked smoke test passes but an advanced construct fails | The language feature matrix later records its support status and focused evidence. | - -Contributors changing documentation should run -`python3 -m pytest -q tests/docs/test_examples.py tests/docs/test_structure.py`. -Wrapper behavior changes require the focused `tests/wrapper/fortran/...` path; -the full GitHub Actions matrix is the final cross-version evidence. - -## Evidence - -The linked `scale.f90` input is checked against the repository fixture by -[`test_examples.py`](../../../tests/docs/test_examples.py). -Native artifact placement and runtime calls are checked by -[`test_build_modes.py`](../../../tests/wrapper/fortran/build_from_source/test_build_modes.py) -and -[`test_runtime_abi.py`](../../../tests/wrapper/fortran/build_from_source/test_runtime_abi.py). +--- + +## Troubleshooting Guide + +| Failure Type | Recommended Action | +|--------------------------------|---------------------------------------------| +| Cannot import x2py / NumPy | Check active virtual environment | +| `x2py --help` works but `.pyi` fails | Check diagnostics and reference section | +| Compiler not found | Fix `PATH` or reinstall gfortran | +| Build / linking fails | Run with `--verbose` | +| Builds but import/call fails | Compare against generated contract | + +--- + +## Next + +- Proceed to [Your First Wrapped Function](first-wrapped-function.md). +- For detailed help, use [Troubleshooting](../troubleshooting/index.md). diff --git a/docs/user/guide/allocatables.md b/docs/user/guide/allocatables.md index f65171510..fd85232f4 100644 --- a/docs/user/guide/allocatables.md +++ b/docs/user/guide/allocatables.md @@ -1,5 +1,6 @@ --- title: Allocatables +description: How x2py handles Fortran `allocatable` variables, arrays, and descriptors audience: users, advanced users prerequisites: arrays related: arrays.md, pointers.md, memory-management.md @@ -9,33 +10,16 @@ publication: reviewed # Allocatables -Allocatable behavior depends on whether the contract describes a scalar -descriptor projection or an array descriptor handle. Scalar allocatables cross -procedure boundaries as ordinary nullable Python values. Array allocatables use -`Allocatable[T[...]]`, which is a Python handle to a native allocatable -descriptor, not a NumPy array. +x2py treats `allocatable` entities differently depending on context: -| Case | Python sees | Owner and lifetime | -| --- | --- | --- | -| Scalar allocatable projection | `T | None` | a call-local native descriptor is created or read back by the bridge | -| Allocatable descriptor argument | `Allocatable[T[...]]` handle | the handle passes the native allocatable descriptor | -| Module allocatable array | `Allocatable[T[...]]` handle | the Fortran module owns allocation and release | -| Derived allocatable field | `Allocatable[T[...]]` handle | the containing generated wrapper owns the native instance | -| Owned allocatable result | `Allocatable[T[...]]` handle | x2py-owned descriptor storage releases the native allocation with the handle | +- **Scalar allocatables** appear as `T | None` values. +- **Array allocatables** appear as `Allocatable[T[...]]` handles. -`Allocatable` is the dynamic-storage fact shared by all rows. It does not by -itself choose copy, replacement, borrowed-view, or owned-handle behavior. The -declaration context and completed ownership policy choose that behavior before -wrapper lowering. - -At runtime, every allocatable array handle described below is an -`AllocatableArray`. Scalar allocatables never produce an `AllocatableArray`; -they remain ordinary `T | None` values at the Python boundary. +--- -## Array Handles +## Allocatable Array Handles -`Allocatable[T[...]]` is the active allocatable-array spelling in semantic -`.pyi` contracts: +Use `Allocatable[T[...]]` for array allocatables in the semantic contract: ```python from x2py.contracts import Allocatable, Float64, Int32 @@ -43,479 +27,93 @@ from x2py.contracts import Allocatable, Float64, Int32 values: Allocatable[Float64[:]] def resize(values: Allocatable[Float64[:]], n: Int32) -> None: ... -def scale(values: Float64[:]) -> None: ... ``` -The handle owns the allocation state, so an unallocated descriptor is still a -present handle: `h.allocated is False`, `h.shape is None`, and -`h.to_numpy() is None`. `| None` means the handle object itself may be absent -for an optional native dummy, making native `present(values)` false: - -```python -def maybe_resize(values: Allocatable[Float64[:]] | None = ...) -> None: ... -``` +### Important Properties -That spelling is valid only for optional callable arguments. Do not use -`Allocatable[T[...]] | None` for module variables, derived-type fields, or -function results; those surfaces return a present handle. Module variables, -fields, allocatable output dummies, function results, and handles changed by -later operations may be unallocated. The handle then reports -`allocated is False` and `to_numpy() is None`; this is descriptor state, not an -absent optional argument. - -Passing a handle to `Allocatable[T[...]]` passes the native descriptor. Passing -the same allocated handle to a normal `T[...]` argument uses ordinary Fortran -array-actual semantics by handing off the handle's native array data facet. It -is not an implicit call to `.to_numpy()`. A normal `T[...]` argument rejects an -unallocated handle because there is no valid array actual to pass; an allocated -zero-length array remains valid. - -Plain NumPy arrays are accepted by normal `T[...]` array parameters. They are -rejected for `Allocatable[T[...]]` descriptor parameters because a NumPy array -does not carry a native allocatable descriptor. - -`h.to_numpy()` is the explicit extraction operation. It returns `None` when the -handle is unallocated. Otherwise, it returns a live mutable NumPy view of the -current native allocation. It never creates an automatic detached snapshot or -copy. Users who need independent storage must explicitly call `.copy()` on the -returned array. - -A borrowed view is a NumPy array that points at storage Python does not own. -Mutating the view mutates the owner. Deallocating or reallocating the owner can -make an existing view stale. Accessing a stale view is unsupported and may -crash the process; discard it and call `to_numpy()` again after the native state -changes. Each fresh extraction inspects the current descriptor and starts at -the current native lower bounds. Changing lower bounds during native -reallocation must not offset the first element exposed to NumPy. - -An allocatable array returned by a function or hidden output is different from -a borrowed module or field handle. x2py transfers the result into persistent -descriptor storage owned by the returned handle. The handle remains -usable after the native call returns, and `close()` or finalization releases the -native allocation. A NumPy view extracted from that handle retains the handle; -as with every live view, explicitly closing or resizing the handle makes older -views stale. - -## Scalar Allocatable Projections - -Scalar allocatables cross a procedure boundary as ordinary nullable Python -values. The semantic `.pyi` keeps the Python annotation as `T | None` and uses -`Allocatable(...)` inside `@native_call` to describe native descriptor -construction and readback. - -For example: - -```fortran -function maybe_scale(enabled) result(scale) - integer(4), intent(in) :: enabled - real(8), allocatable :: scale - - if (enabled /= 0) then - allocate(scale) - scale = 2.5_8 - end if -end function maybe_scale - -subroutine update_scale(scale) - real(8), allocatable, intent(inout) :: scale - - if (allocated(scale)) then - scale = scale + 1.0_8 - else - allocate(scale) - scale = 1.0_8 - end if -end subroutine update_scale -``` - -The corresponding semantic contract is: +- A handle is **always present**, even when unallocated. +- Reading the Python attribute returns an `Allocatable[T[...]]` handle, not `ndarray | None`. +- `handle.allocated` tells you the current state. +- `handle.to_numpy()` returns a live NumPy view **or** `None` if unallocated; it never creates an automatic detached snapshot. +- The view is **borrowed**. Do not keep it after the native storage may change. +- A borrowed view is a NumPy array that points at storage Python does not own. ```python -from x2py.contracts import Addr, Allocatable, Arg, Float64, Int32, Return, Returns, native_call - -@native_call([Addr(Arg(0))], result=Allocatable(Return(0))) -def maybe_scale(enabled: Int32) -> Float64 | None: ... - -@native_call([Allocatable(Arg(0))]) -def update_scale( - scale: Float64 | None, -) -> Returns["scale", Float64] | None: ... +h = api.some_allocatable +if h.allocated: + view = h.to_numpy() # live mutable view + view[0] = 42.0 +else: + print("Not allocated") ``` -Passing `None` creates a present but unallocated call-local descriptor. Omitting -a defaulted scalar descriptor argument creates native optional absence, so -`present(scale)` is false. Passing a value creates a present allocated -call-local descriptor. A projected output becomes `None` when its descriptor is -unallocated, including an allocatable scalar function result. Ordinary scalar -projection rules still apply: -`intent(out)` uses `Allocatable(Return("name", j))`, while `intent(inout)` uses -`Allocatable(Arg(i))` plus a matching `Returns["name", T] | None` readback. The -singular `result=Allocatable(Return(j))` mapping describes the native function -result and places it among any other Python results. - -Use a default only when the native scalar dummy is optional: - -```python -@native_call([Allocatable(Arg(0))]) -def update_scale(scale: Float64 | None = ...) -> None: ... -``` - -This scalar rule is separate from array allocatable handles. Array arguments use -`Allocatable[T[...]] | None` only for an optional absent handle; unallocated array -state stays inside a present handle. +--- -## Complete Allocatable Example +## Complete Example -Create `allocations.f90`: +Create `storage.f90`: ```fortran module storage implicit none - real(8), allocatable, target :: shared_values(:) - real(8), allocatable :: plain_values(:) + real(8), allocatable :: values(:) contains - function make_values(count) result(values) - integer(4), intent(in) :: count - real(8), allocatable :: values(:) - integer(4) :: index - - if (count <= 0) return - allocate(values(count)) - values = [(2.0_8 * index, index = 1, count)] - end function make_values - subroutine replace_values(values) - real(8), allocatable, intent(inout) :: values(:) + function make_values(n) result(arr) + integer(4), intent(in) :: n + integer(4) :: i + real(8), allocatable :: arr(:) + if (n > 0) then + allocate(arr(n)) + arr = [(real(i, 8)*2, i = 1, n)] + end if + end function make_values - if (allocated(values)) deallocate(values) - allocate(values(2)) - values = [10.0_8, 20.0_8] + subroutine replace_values(arr) + real(8), allocatable, intent(inout) :: arr(:) + if (allocated(arr)) deallocate(arr) + allocate(arr(2)) + arr = [10.0_8, 20.0_8] end subroutine replace_values - subroutine allocate_shared(count) - integer(4), intent(in) :: count - integer(4) :: index - - if (allocated(shared_values)) deallocate(shared_values) - allocate(shared_values(count)) - shared_values = [(1.0_8 * index, index = 1, count)] - end subroutine allocate_shared - - subroutine allocate_plain(count) - integer(4), intent(in) :: count - integer(4) :: index - - if (allocated(plain_values)) deallocate(plain_values) - allocate(plain_values(count)) - plain_values = [(3.0_8 * index, index = 1, count)] - end subroutine allocate_plain - - subroutine release_shared() - if (allocated(shared_values)) deallocate(shared_values) - end subroutine release_shared - - subroutine scale_plain(scale) - real(8), intent(in) :: scale - plain_values = scale * plain_values - end subroutine scale_plain - - subroutine release_plain() - if (allocated(plain_values)) deallocate(plain_values) - end subroutine release_plain - - real(8) function shared_sum() result(total) - total = sum(shared_values) - end function shared_sum end module storage ``` -Inspecting `allocations.f90` prints allocatable array handles for module -storage, descriptor results, and descriptor arguments. `Aliased` remains a -language-neutral fact that native storage may be externally aliased or -addressed. It does not change `to_numpy()` extraction semantics: - -```python -from x2py.contracts import Addr, Aliased, Allocatable, Annotated, Arg, Float64, Int32, Returns, native_call - -shared_values: Annotated[Allocatable[Float64[:]], Aliased] -plain_values: Allocatable[Float64[:]] - -@native_call([Addr(Arg(0))]) -def make_values( - count: Int32 -) -> Allocatable[Float64[:]]: ... - -def replace_values( - values: Allocatable[Float64[:]] -) -> Returns["values", Allocatable[Float64[:]]]: ... - -@native_call([Addr(Arg(0))]) -def allocate_shared( - count: Int32 -) -> None: ... - -@native_call([Addr(Arg(0))]) -def allocate_plain( - count: Int32 -) -> None: ... - -def release_shared() -> None: ... - -@native_call([Addr(Arg(0))]) -def scale_plain( - scale: Float64 -) -> None: ... - -def release_plain() -> None: ... - -def shared_sum() -> Float64: ... -``` - -`plain_values` and `shared_values` have the same extraction behavior: a fresh -`to_numpy()` call returns a live view of the current allocation or `None`. -`Aliased` remains present on `shared_values` because the native declaration -supplies the corresponding addressability fact. It does not change the -allocatable-array extraction mode. - Build it: ```bash -python3 -m x2py allocations.f90 --out-dir build/allocations +python3 -m x2py storage.f90 --out-dir build/storage ``` -Then exercise owned-result, descriptor-argument, and module-handle behavior: +Use the generated module: ```python import sys + import numpy as np -sys.path.insert(0, "build/allocations") -import allocations +sys.path.insert(0, "build/storage") +import storage -api = allocations.storage +api = storage.storage values = api.make_values(np.int32(3)) -np.testing.assert_array_equal(values.to_numpy(), np.array([2.0, 4.0, 6.0], dtype=np.float64)) -assert api.make_values(np.int32(0)).allocated is False +print(values.to_numpy()) # [2. 4. 6.] returned = api.replace_values(values) -assert returned is values -np.testing.assert_array_equal(values.to_numpy(), np.array([10.0, 20.0], dtype=np.float64)) - -api.allocate_shared(np.int32(3)) -shared = api.shared_values -view = shared.to_numpy() -view[0] = np.float64(10.0) -assert api.shared_sum() == np.float64(15.0) - -api.allocate_plain(np.int32(3)) -plain_view = api.plain_values.to_numpy() -plain_copy = plain_view.copy() -plain_view[0] = np.float64(12.0) - -api.scale_plain(np.float64(2.0)) -np.testing.assert_array_equal(plain_copy, np.array([3.0, 6.0, 9.0], dtype=np.float64)) -np.testing.assert_array_equal( - api.plain_values.to_numpy(), - np.array([24.0, 12.0, 18.0], dtype=np.float64), -) +assert returned is values # same handle +print(values.to_numpy()) # [10. 20.] ``` -Do not access `view` after `api.release_shared()`, or `plain_view` after -`api.release_plain()` or another reallocation. Native storage changes make the -previous views stale, and accessing a stale view is unsupported and may crash. - -## Output And Function Results - -Allocated top-level results and non-optional hidden allocatable outputs return -wrapper-owned `AllocatableArray` objects. The generated binding transfers the -result into persistent descriptor storage; the handle releases that storage on -`close()` or finalization. Unallocated storage is represented by a present -handle whose `allocated` property is false and whose `to_numpy()` result is -`None`. Optional allocatable outputs remain visible so the caller can omit them -and make native `present(...)` false. - -A NumPy view returned by `to_numpy()` retains its handle owner. Changing that -view changes the handle's current allocation, but does not affect later, -independent result handles. - -## Inout Replacement - -An allocatable `intent(inout)` descriptor argument accepts an -`AllocatableArray`, not a plain NumPy array. A matching `Returns[...]` -projection records that the same caller handle is the Python result. Policy -completion marks that descriptor boundary read-write before lowering; generated -binding code does not manufacture a replacement ndarray or a second handle. - -```python -assert api.replace_values(values) is values -``` - -The source for this call is already shown in the complete example above. - -## Character Array Replacement - -Allocatable character arrays use fixed-width NumPy bytes storage. Create -`character_allocatables.f90`: - -```fortran -module character_names - implicit none -contains - function make_names() result(names) - character(len=:), allocatable :: names(:) - - allocate(character(len=3) :: names(2)) - names = [character(len=3) :: "red", "sky"] - end function make_names - - subroutine replace_names(names) - character(len=:), allocatable, intent(inout) :: names(:) - integer :: count - - if (allocated(names)) then - count = size(names) - else - count = 2 - end if - - if (allocated(names)) deallocate(names) - allocate(character(len=5) :: names(count)) - names = " " - if (count >= 1) names(1) = "red" - if (count >= 2) names(2) = "blue" - end subroutine replace_names -end module character_names -``` - -The generated `.pyi` represents a fixed-length rank-one character array as -`String[n][::]`, where `n` is its fixed element length. A deferred-length -allocatable rank-one array uses the two-axis -handle spelling `Allocatable[String[:][:]]`, so the element width can come from -the native allocation at runtime: - -```python -from x2py.contracts import Allocatable, Returns, String - -def make_names() -> Allocatable[String[:][:]]: ... - -def replace_names( - names: Allocatable[String[:][:]] -) -> Returns[ - "names", Allocatable[String[:][:]] -]: ... -``` - -Build the example: - -```bash -python3 -m x2py character_allocatables.f90 --out-dir build/character_allocatables -``` - -Pass an existing compatible allocatable character handle. The projected result -is that same handle, and extraction remains explicit: - -```python -import sys -sys.path.insert(0, "build/character_allocatables") -import character_allocatables - -api = character_allocatables.character_names -names = api.make_names() -assert api.replace_names(names) is names -assert names.to_numpy().dtype.itemsize == 5 -assert names.to_numpy().tolist() == [b"red ", b"blue "] -``` +--- -The `S5` itemsize comes from `allocate(character(len=5) :: names(count))`. -Plain NumPy arrays are not allocatable descriptors and are rejected for this -handle-typed parameter. When extracting character storage, x2py uses NumPy -bytes dtype `S`; Unicode (`U`) and object (`O`) arrays are not descriptor-handle -substitutes. - -Projected writable descriptor mutation requires a handle with persistent -wrapper-owned standard-descriptor storage, such as the owned result returned by -`make_names()`. A borrowed module handle can be passed to a read-only descriptor -argument through descriptor facts, but it cannot be passed to a projected -writable descriptor argument: native mutation of a call-local reconstructed -descriptor would not update the module handle reliably. - -## Module Handles And Views - -An allocatable module array is native-owned. Reading the Python attribute -returns an `Allocatable[T[...]]` handle, not `ndarray | None`. The module's -allocation routines create and release the storage. `h.to_numpy()` returns the -current live view or `None` according to the current allocation state. When the -Fortran declaration has `target`, the generated `.pyi` marks the handle with -`Aliased`. `Aliased` is not an ownership mode or an extraction selector; it -records native addressability for pointer association, raw-address, foreign-pointer, -and related policy. - -A plain allocatable module array has the same extraction contract as an -`Aliased` one. The wrapper uses the completed descriptor mechanism to inspect -the current allocation without copying. If the backend cannot expose a live -view through a supported mechanism, wrapper planning stops with a clear -diagnostic. Call `.copy()` explicitly when independent Python-owned storage is -required. - -A supported allocatable component belongs to its containing native derived-type -instance. The generated wrapper owns that native instance. The field exposes an -`Allocatable[T[...]]` handle that retains the parent wrapper. Any borrowed NumPy -view produced by `to_numpy()` retains the field handle, and the field handle -retains the parent wrapper. Assigning a replacement array directly to such a -field is rejected when native reallocation must go through an explicit method. - -Neither owner model can invalidate an already-created NumPy object safely after -native reallocation. Copy before any operation that may reallocate or -deallocate: +## Key Rules -```python -independent = view.copy() -``` +- Use `Allocatable[T[...]]` when the Python API needs a native descriptor. +- Use normal `T[...]` when you just want to pass array **data**. +- Call `.to_numpy()` to get a view; the handle itself is not an array. +- Copy data (`view.copy()`) if you need it to survive possible reallocation/deallocation. +- Direct allocatable array function results preserve allocated, zero-sized, and + unallocated handle state. This includes matrices and higher-rank arrays. -## Limitations - -- A wrapper-owned allocatable scalar derived result can be passed to a - compatible ordinary, target, allocatable, allocatable-target, input-only - pointer, or value dummy. The generated typed holder preserves the same Python - object and writes allocation changes back to that holder. -- An allocatable scalar derived module variable is a live nullable field proxy, - and it can satisfy a compatible allocatable dummy through a scoped - `move_alloc` transaction. The allocation is moved into an exact typed local - holder, passed to the native procedure, and restored exactly once; no object - address substitutes for the module descriptor and no descriptor crosses the - interoperable boundary. -- The complete ordinary, `TARGET`, `ALLOCATABLE`, `ALLOCATABLE,TARGET`, - `POINTER`, and `VALUE` compatibility rules—including empty state, - multi-argument cleanup, and deliberate errors—are in the later Wrapping - Derived Types guide under “Scalar Actuals And Native Dummies.” -- Mutable scalar deferred-length character storage is blocked. -- Plain derived module objects use typed module-specific member access; - `Aliased` is needed only for policies that require a direct native address. - Allocatable module-array handles use their standard descriptor path and have - the same live-view extraction contract with or without `Aliased`. -- Borrowed module handles do not provide the persistent direct descriptor - handoff required by projected writable descriptor arguments; use an owned - result handle for that operation. -- An edited `.pyi` cannot relabel a native-owned descriptor as Python-owned. - Use an implemented owned-result handle or copy an extracted NumPy value when - Python needs independent storage. -- `Annotated[T[...], Allocatable]` is no longer the active public spelling for - allocatable array descriptors; use `Allocatable[T[...]]`. - -## Evidence And Troubleshooting - -Owned results, module and component handles, unallocated state, extraction, and -owner retention are exercised by -[`test_allocatable_views.py`](../../../tests/wrapper/fortran/module_state/test_allocatable_views.py). -Allocatable descriptor `intent(inout)` mutation and same-handle projection are -exercised by -[`test_allocatable_replacement.py`](../../../tests/wrapper/fortran/module_state/test_allocatable_replacement.py). -Character descriptor generation in source and generated-`.pyi` modes is -exercised by -[`test_character_arguments.py`](../../../tests/wrapper/fortran/strings/test_character_arguments.py). - -A borrowed view can become stale after its native owner reallocates or -deallocates storage, so copy any data that needs an independent lifetime. -Memory Management expands this rule later, and Runtime Issues later covers -dtype, rank, and stale-storage symptoms. +--- diff --git a/docs/user/guide/arrays.md b/docs/user/guide/arrays.md index ddbfa09b3..5da278209 100644 --- a/docs/user/guide/arrays.md +++ b/docs/user/guide/arrays.md @@ -1,5 +1,6 @@ --- title: Arrays +description: How to pass NumPy arrays to Fortran routines with x2py — shape, layout, strides, and validation rules audience: users prerequisites: data types, wrapping functions related: allocatables.md, pointers.md, wrapping-subroutines.md @@ -9,14 +10,11 @@ publication: reviewed # Arrays -Ordinary numeric Fortran arrays cross the Python boundary as NumPy arrays. -Native allocatable and pointer array descriptors instead cross as -`Allocatable[T[...]]` and `Pointer[T[...]]` handles. In both cases, the -semantic contract records element dtype, rank, known extents, layout, allowed -strides, mutability, and storage category. The wrapper validates these facts -before the native call and does not silently repair an incompatible value. +x2py passes ordinary numeric Fortran arrays as **NumPy arrays**. Allocatable and pointer arrays use special handle types (`Allocatable[T[...]]` and `Pointer[T[...]]`). The semantic contract clearly specifies element type, rank, shape, layout, strides, and mutability. -## Complete Array Example +--- + +## Complete Example Create `arrays.f90`: @@ -24,6 +22,7 @@ Create `arrays.f90`: module array_ops implicit none contains + subroutine scale_matrix(rows, columns, values) integer(4), intent(in) :: rows, columns real(8), intent(inout) :: values(rows, columns) @@ -39,35 +38,12 @@ contains function automatic_vector(count) result(values) integer(4), intent(in) :: count real(8) :: values(count) - integer(4) :: index + integer(4) :: i - values = [(2.0_8 * index, index = 1, count)] + values = [(2.0_8 * i, i = 1, count)] end function automatic_vector -end module array_ops -``` - -Inspecting `arrays.f90` prints these array contracts: -```python -from x2py.contracts import Addr, Arg, Float64, Int32, native_call - -@native_call([Addr(Arg(0)), Addr(Arg(1)), Arg(2)]) -def scale_matrix( - rows: Int32, - columns: Int32, - values: Float64[rows, columns] -) -> None: ... - -@native_call([Addr(Arg(0)), Arg(1)]) -def shift( - size: Int32, - values: Float64[size] -) -> None: ... - -@native_call([Addr(Arg(0))]) -def automatic_vector( - count: Int32 -) -> Float64[count]: ... +end module array_ops ``` Build it: @@ -76,173 +52,60 @@ Build it: python3 -m x2py arrays.f90 --out-dir build/arrays ``` -Then assert in-place mutation, lower-bound handling, and an array result: +--- + +## Python Usage ```python import sys - import numpy as np sys.path.insert(0, "build/arrays") import arrays api = arrays.array_ops + +# In-place modification (Fortran order) matrix = np.ones((2, 3), dtype=np.float64, order="F") api.scale_matrix(np.int32(2), np.int32(3), matrix) np.testing.assert_array_equal(matrix, np.full((2, 3), 2.0, order="F")) +# Lower-bound aware array shifted = np.zeros(4, dtype=np.float64) api.shift(np.int32(4), shifted) np.testing.assert_array_equal(shifted, np.ones(4, dtype=np.float64)) +# Array return value result = api.automatic_vector(np.int32(4)) -np.testing.assert_array_equal( - result, - np.array([2.0, 4.0, 6.0, 8.0], dtype=np.float64), -) +np.testing.assert_array_equal(result, np.array([2.0, 4.0, 6.0, 8.0], dtype=np.float64)) ``` -## Read The Contract - -For the complete example, generated annotations record a rank-two matrix whose -extents depend on `rows` and `columns`, a rank-one lower-bound-aware array, and -an automatic rank-one result. Other supported contracts can use `Float64[:]`, -`Float64[3]`, `Float64[::]`, `Float64[Flat]`, or `Float64[...]`. - -The element name maps to an exact NumPy dtype; see [Data Types](data-types.md). -Dimension expressions constrain extents, not source lower/upper-bound -spellings. The native dimension `0:size-1` therefore becomes the public extent -`size`. Python remains zero-indexed even when the native declaration has -non-default lower bounds. - -## Validation - -Before entering native code, x2py checks: +--- -- exact NumPy dtype without implicit casts; -- native byte order and alignment; -- required rank and every expressible extent; -- contract-required contiguity, orientation, and stride pattern; and -- writeability for output and inout storage. +## Key Concepts -Read-only arrays are valid for input-only arguments. x2py does not byte-swap, -realign, de-alias overlapping arrays, or make a hidden contiguous copy for an -ordinary in-place contract. A violation raises `TypeError` before native code -runs. +- Use **exact NumPy dtypes** (`np.float64`, `np.int32`, etc.). +- For multidimensional arrays intended for Fortran, use `order="F"` or `np.asfortranarray()`. +- The wrapper validates dtype, rank, shape, contiguity, and writeability **before** calling native code. +- No silent casting, copying, or layout conversion happens by default. -## Layout And Strides +--- -Use `numpy.asfortranarray` or `order="F"` for a multidimensional contract that -requires Fortran orientation, as shown by `matrix` in the complete example. +## Common Array Contracts -Layout annotations describe a deliberate non-default storage representation; -they do not request an automatic conversion. Plain multidimensional -Fortran-facing arrays already pass Fortran-contiguous storage with their logical -axes unchanged. `ORDER_C` passes the same C-contiguous data address without -copying and constructs the Fortran bridge view with reversed axes. For example, -a C-order Python shape `(2, 3)` is a Fortran bridge shape `(3, 2)` over the -same six elements. Use `ORDER_C` only when the native operation intentionally -accepts that transposed storage view. +| Contract | Meaning | +|-----------------------|----------------------------------------------| +| `Float64[:]` | 1D contiguous array | +| `Float64[:, :]` | 2D Fortran-contiguous array | +| `Float64[::]` | 1D strided array | +| `Float64[rows, columns]` | Shape depends on other arguments | +| `Float64[Flat]` | Assumed-size (flat) contiguous storage | +| `Float64[...]` | Assumed-rank (rank 1–15) | -Add `COPY_F` when Python should accept C-contiguous storage but native Fortran -must observe the same logical axes in Fortran order: +--- -```python -values: Annotated[Float64[:, :], ORDER_C, COPY_F] -``` +## Next -The binding owns this complete representation lifecycle. It creates an -F-contiguous NumPy temporary before the call, passes that ordinary F-order -buffer through the unchanged bridge path, copies values back into the original -C-order array after the call, and releases the temporary. Projected results -return the original C-order object. Native `intent(in)` remains a property of -the native procedure call; neither the semantic `.pyi` nor the bridge temporary -needs a separate direction annotation for `COPY_F`. The bridge performs neither -half of this argument conversion. - -Rank-one contiguous arrays can satisfy their documented contiguous contract -without a meaningful row/column distinction. Legacy fixed-form array contracts -are contiguous-only. A modern Fortran dummy is stride-aware only when its -generated contract explicitly permits strides. Inspect `.pyi` output instead -of assuming every slice is accepted. - -Zero-sized dimensions are supported when dtype, rank, writeability, and known -extent rules still match. Degenerate strides on axes with no addressable -movement do not by themselves make the layout invalid. - -## Inputs, Outputs, And Inout Arrays - -- Input arrays remain caller-owned and may be read-only. -- Ordinary output arrays remain visible; the caller allocates writable storage. -- Inout arrays remain visible and mutate in place. -- Ordinary array function results are Python-owned NumPy copies. -- Non-optional hidden allocatable outputs are wrapper-owned - `Allocatable[T[...]]` handles. Unallocated state remains inside the present - handle. -- Optional allocatable outputs remain visible so the caller controls native - `present(...)`. -- Pointer-array handle results remain blocked until owner storage, target - lifetime, descriptor extraction, and destroy behavior are implemented. -- Allocatable and pointer module variables and supported components are handle - objects. NumPy views are obtained explicitly with `to_numpy()` and require - lifetime care after native descriptor changes. - -Caller-provided output storage is demonstrated with complete source in -[Wrapping Subroutines](wrapping-subroutines.md#complete-output-example). - -## Assumed Size And Lower Bounds - -`Float64[Flat]` records supported flat assumed-size storage. Python supplies -the actual allocation, and the caller must ensure it is large enough for the -native routine. x2py validates explicit dimensions it can express but cannot -infer an omitted final extent from an unrelated argument. - -Non-default native lower bounds change how extents are computed internally, -but they do not alter Python indexing. Even if a Fortran argument is declared -with custom bounds like values(3:size+2), -the wrapped NumPy array in Python remains strictly zero-indexed. - -## Assumed Rank - -Supported numeric assumed-rank arguments accept NumPy ranks 1 through 15 through -a generated native rank dispatcher. Each assumed-rank argument dispatches at -its own runtime rank. Rank-zero values and ranks above 15 are rejected. - -## Array Results - -Supported ordinary numeric and fixed-width character array results preserve -dtype, rank, and Fortran-oriented multidimensional data as NumPy arrays. -Character arrays use NumPy bytes dtypes such as `S5`, where the dtype itemsize -is the Fortran element length. Ordinary zero-sized results remain zero-sized -arrays. - -An allocatable array result instead returns an `Allocatable[T[...]]` handle. -An allocated zero-sized result is a handle whose shape contains a zero extent; -an unallocated result is a present handle with `allocated is False` and -`to_numpy() is None`. Pointer-array results remain blocked until their owner -and target lifetime can be represented safely. - -## Unsupported Forms - -- assumed type `type(*)`; -- character arrays that cannot be represented as fixed-width NumPy bytes - storage; -- arrays of derived types; -- pointer-array results and reassociation without completed owner, lifetime, - and operation policy; and -- any kind or rank whose portable NumPy storage contract cannot be proved. - -## Evidence And Troubleshooting - -Validation and layout behavior are exercised by -[`test_array_contracts.py`](../../../tests/wrapper/fortran/arrays/test_array_contracts.py), -assumed-rank behavior by -[`test_assumed_rank_arrays.py`](../../../tests/wrapper/fortran/arrays/test_assumed_rank_arrays.py), -multidimensional behavior by -[`test_multidimensional_arrays.py`](../../../tests/wrapper/fortran/arrays/test_multidimensional_arrays.py), -and results by -[`test_array_results.py`](../../../tests/wrapper/fortran/arrays/test_array_results.py). - -When a call fails, compare `value.dtype`, `value.shape`, `value.strides`, -`value.flags`, and writeability with the generated annotation. Runtime Issues -later covers calls that still fail when those properties match. +- Learn about [Allocatables](allocatables.md) and [Pointers](pointers.md) +- See more examples in [Wrapping Subroutines](wrapping-subroutines.md) +- Check the [Language Feature Matrix](../language-support/feature-matrix.md) for supported and unsupported array forms. diff --git a/docs/user/guide/callbacks.md b/docs/user/guide/callbacks.md index 91ac5570e..fd038c069 100644 --- a/docs/user/guide/callbacks.md +++ b/docs/user/guide/callbacks.md @@ -1,5 +1,6 @@ --- title: Callbacks +description: How to pass Python callables to Fortran as callbacks with x2py audience: advanced users prerequisites: wrapping functions, error handling, data types related: error-handling.md, memory-management.md, ../reference/semantic-pyi-format.md @@ -9,36 +10,98 @@ publication: reviewed # Callbacks -x2py supports Python callbacks invoked immediately during one wrapped native -call. A semantic `.pyi` declares each native callback shape once as a named -prototype, then callback-taking procedures refer to that prototype by name. +Callbacks let wrapped Fortran call a Python function while an x2py call is +running. They are useful for objective functions, progress hooks, custom +transforms, and small pieces of user-defined numerical logic. -This is the opposite direction from a normal `.pyi` function signature. A -normal function signature describes how Python calls the wrapper; x2py may -lower that call into any compatible native bridge shape. A `@prototype` -declaration describes how native code calls the callback adapter, so argument -order, value/reference passing, rank, shape, character length, and result shape -are part of the callback contract. Native callback direction is deliberately -not repeated in semantic `.pyi`. +Declare the callback shape once with `@prototype`, then use that prototype name +as the type of the procedure argument that accepts the callback. -## Complete Callback Example +--- + +## The Short Version + +| Native callback argument | Prototype spelling | Python callable receives | +| --- | --- | --- | +| Primitive scalar dummy declared with Fortran `value` | `value: Float64` | Owned `np.float64` scalar | +| Primitive scalar reference dummy | `value: Addr(Float64)` | Owned `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 | + +!!! tip "Rule of thumb" + Bare primitive callback arguments are native values: + `value: Float64`, `count: Int32`, and so on. + + Use `Addr(T)` only when a primitive callback 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 +dummies. Use `Value(point_t)` only for a supported derived-type callback dummy +declared with the Fortran `value` attribute. + +--- + +## What The Callable Sees + +Two declarations can appear around callbacks, and they control different calls: + +| Declaration | Controls | +| --- | --- | +| `@prototype` | How 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 x2py.contracts import Addr, Arg, Float64, native_call, prototype + +@prototype +def scalar_callback(value: 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 +`@native_call(...)` describes how Python calls the wrapped function. + +At runtime, pass an ordinary Python callable: + +```python +import numpy as np + +api.apply(lambda value: np.float64(3.0 * value), np.float64(2.5)) +``` + +The lambda receives converted Python objects, not `Addr(...)` markers. + +--- + +## Small Example Create `callbacks.f90`: ```fortran module callbacks_api implicit none + abstract interface real(8) function scalar_callback(value) result(output) real(8), intent(in) :: value end function scalar_callback end interface + contains + real(8) function apply(callback, value) result(output) procedure(scalar_callback) :: callback real(8), intent(in) :: value output = callback(value) end function apply + end module callbacks_api ``` @@ -48,7 +111,7 @@ Build it: python3 -m x2py callbacks.f90 --out-dir build/callbacks ``` -Then pass a Python callable and assert the converted result: +**Python usage:** ```python import sys @@ -58,163 +121,98 @@ sys.path.insert(0, "build/callbacks") import callbacks api = callbacks.callbacks_api -result = api.apply(lambda value: np.float64(3.0 * value), np.float64(2.5)) + +result = api.apply( + lambda value: np.float64(3.0 * value), + np.float64(2.5) +) assert result == np.float64(7.5) ``` -## Lifetime - -The generated wrapper keeps a strong reference to the Python callable only -until the wrapped call returns. Native code must not store the callback or call -it later. Nested callback-taking calls on the same entering Python thread are -supported. +--- -Primitive scalar callback arguments are materialized as independent NumPy -scalar values, so retaining them is safe. Temporary NumPy array views and -borrowed derived wrappers are valid only for that callback invocation. -Retaining either afterward is unsupported unless the value is explicitly -copied. +## Choosing The Prototype Spelling -## Callback Values +Prototype declarations describe the **native callback signature**. They are not +Python runtime functions and they are not exported from the generated module. -Callback arguments use ordinary semantic types. Native code passes them by -reference unless the prototype applies the one ABI override, `Value(T)`: +For ordinary scalar and array callback arguments, use the same contract spellings +you use elsewhere: ```python -from x2py.contracts import Float64, Int32, Value, prototype +from x2py.contracts import Addr, Float64, Int32, prototype @prototype def update_values( - count: Int32, - scale: Value(Float64), - values: Float64[count], + count: Addr(Int32), + scale: Float64, + values: Float64[count] ) -> None: ... - -def apply_update(callback: update_values, count: Int32) -> None: ... ``` -The spellings mean: +Here `count` is a primitive reference dummy, while `scale` is a primitive value +dummy. Python receives both as NumPy scalar values. -| Callback spelling | Fortran callback dummy | Python callback object | -| --- | --- | --- | -| `Int32` | scalar reference dummy | owned `np.int32` scalar value | -| `Value(Float64)` | scalar `value` dummy | owned `np.float64` scalar value | -| `Float64[n]` | array reference dummy | writable NumPy array view | -| `point_t` | derived reference dummy | generated wrapper object | -| `Value(point_t)` | derived `value` dummy | generated wrapper over the call-local value copy | - -Array callback arguments require exact dtype, rank, declared shape, alignment, -and required Fortran contiguity. Derived values use the generated wrapper class. -Reference arrays, characters, and derived objects are exposed permissively and -written back before the callback adapter returns. Primitive scalar arguments -are immutable NumPy scalar values even when their native ABI uses a reference. -Scalar reference writeback is unsupported: model a scalar value delivered back -to native code as the declared callback result. A `Value(...)` argument changes -only the native ABI to Fortran `value`; it is also received as an independent -NumPy scalar. - -## Character Callback Arguments - -Fixed-length character callback arguments use their ordinary semantic spelling: +For scalar arguments, choose the spelling from the Fortran callback dummy: -```python -from x2py.contracts import String, prototype +| Fortran callback dummy | Matching prototype | +| --- | --- | +| `real(8), intent(in) :: value` | `value: Addr(Float64)` | +| `real(8), value :: value` | `value: Float64` | -@prototype -def label_callback(label: String[8]) -> None: ... -``` +Both forms call Python with an owned `np.float64` scalar. The difference is the +native calling convention x2py must match. -Because reference callbacks are permissive, the callback receives mutable -rank-zero NumPy bytes storage with shape `()` and dtype `S8`. The Python -callback reads or writes through that storage: +`Value(T)` is only for supported non-primitive scalar value dummies, such as a +derived-type callback dummy declared with the Fortran `value` attribute. -```python -def rewrite_label(label): - label[...] = b"done " -``` +--- + +## Key Rules + +- The callback is only valid **during** the wrapped native call. +- Native code must not store the callback for later use. +- Return the exact NumPy scalar type when x2py expects a scalar callback result. +- Primitive scalar callback arguments arrive as independent NumPy scalar values, + whether the native dummy is `value` or reference. +- Primitive scalar reference writeback is unsupported; return a scalar result + instead. +- Arrays and derived-type arguments can expose live native state; copy data you + need after the wrapped call returns. + +--- + +## Important Limitations + +Supported callbacks are immediate, same-thread adapters. The native routine may +call the Python callable while the wrapped call is active, and x2py tears down +the callback context when that wrapped call returns. + +The current callback contract does not support: + +- Stored callbacks, persistent callbacks, procedure pointers, or callbacks + invoked after the wrapped call returns. Pass the callable into each wrapped + call that needs it. +- Optional callback procedure arguments. Expose a separate native entry point + for the no-callback path, or require the callback argument. +- Optional arguments inside a `@prototype`. Pass an explicit value, sentinel, or + presence flag instead. +- 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. +- Arrays passed by Fortran `value`, arrays of derived values, and array callback + results without a complete fixed shape. Pass arrays by reference and give array + results an exact primitive shape. +- Variable-length callback strings. Use a fixed positive `String[n]` length. +- Callback execution on a different Python thread. The callback must run on the + same thread that entered the wrapper. + +Callback exceptions and invalid return conversions are fatal at the callback +boundary: x2py prints the Python traceback and aborts the host process. + +--- + +## Next -The semantic callback annotation remains `String[n]`; the callback adapter -chooses mutable scalar storage without encoding native direction in `.pyi`. - -A non-callable argument raises `TypeError` before native execution. - -## Threads And The GIL - -The callback trampoline acquires the Python GIL for the callback and releases -the matching state afterward. The callback must execute on the same Python -thread that entered the wrapped routine. Cross-thread native invocation is not -supported. - -Callback-taking calls keep the GIL policy required by the callback bridge. -Do not use callback execution as synchronization for unrelated native state. - -## Callback Failures - -A callback exception, invalid callback result, or cross-thread invocation -cannot be safely unwound through arbitrary native frames. The wrapper prints -the Python traceback and aborts the host process. It never invents a fallback -return value and continues native execution. - -Run untrusted callback behavior in a subprocess if the host application must -survive such failures. - -## Unsupported Forms - -- stored callback registration and unregistration; -- callbacks invoked after the wrapped call; -- optional dummy procedure arguments; -- procedure pointers and null procedure pointers; -- asynchronous or cross-thread callback invocation; and -- persistent callback ownership during object or library teardown. - -## Adapter Policy - -The post-IR policy stage completes how the adapter crosses the -Fortran-to-Python-to-Fortran boundary before wrapper generation begins. It -records value versus reference ABI, permissive reference writeback, array shape, -fixed character length, exact derived type identity, call-scoped context -lifetime, entering-thread enforcement, GIL entry, cleanup, and the fatal error -action. The Python binding and Fortran bridge only lower those completed actions. - -A `@prototype` declaration is a compile-time semantic declaration, not a -Python runtime export. Its declaration order, value/reference transport, -storage shape, character length, and result type define the callback transport -contract. The Python callable may adapt argument names itself, so prototypes do -not use normal wrapper projection such as `@native_call`. - -Post-IR policy selects the weakest correct native declaration from the complete -prototype. Classic scalar, explicit-shape, and assumed-size signatures use an -implicit external declaration. Signatures with optional or descriptor -arguments, polymorphism, or non-scalar/descriptor results use the -named native prototype through an explicit declaration. That path imports the -real interface, including direction facts deliberately omitted from semantic -`.pyi`, so its compiler module file must be available. `Value(...)` alone does -not force the explicit path when a typed external declaration is sufficient. - -Future work may add user-selectable policy for: - -- borrowed-view, detached-copy, and zero-copy choices; -- dtype conversion, overflow checks, and result coercion; -- fixed-length character encoding, padding, truncation, and writeback rules; -- ownership and lifetime rules for arrays, scalar storage, derived wrappers, - and temporary callback values; -- writeback protocols for values that cannot be mutated - directly by the Python object currently passed to the callback; and -- callback-specific error/result policy beyond the current fatal native - callback boundary. - -These choices are not currently user-selectable; unsupported forms remain -blocked instead of selecting a different backend behavior. - -## Evidence And Troubleshooting - -Scalar lifetime, nesting, GIL behavior, invalid callbacks, and fatal exception -behavior are exercised by -[`test_scalar_callbacks.py`](../../../tests/wrapper/fortran/callbacks/test_scalar_callbacks.py). -Array conversion is exercised by -[`test_array_callbacks.py`](../../../tests/wrapper/fortran/callbacks/test_array_callbacks.py) -and derived values by -[`test_derived_callbacks.py`](../../../tests/wrapper/fortran/callbacks/test_derived_callbacks.py). - -Error Handling later distinguishes ordinary wrapper exceptions from fatal -callback-boundary failures. +- Use the [Callbacks Reference](../reference/callbacks.md) for the full prototype contract. +- Review [Error Handling](error-handling.md) when callback failure behavior matters. diff --git a/docs/user/guide/data-types.md b/docs/user/guide/data-types.md index 39d3b7995..226044169 100644 --- a/docs/user/guide/data-types.md +++ b/docs/user/guide/data-types.md @@ -1,5 +1,6 @@ --- title: Data Types +description: How x2py maps Fortran types to Python, NumPy dtypes, and semantic contracts audience: users prerequisites: common beginner workflow related: arrays.md, wrapping-derived-types.md, ../reference/semantic-pyi-format.md @@ -9,20 +10,22 @@ publication: reviewed # Data Types -x2py resolves native Fortran types into explicit semantic types before wrapper -generation. The generated `.pyi` contract shows the resolved type, and Python -callers use the matching NumPy dtype or generated wrapper class. Do not infer a -mapping from a Fortran kind number alone: kind values are compiler-dependent, -so wrapper builds probe the selected compiler. +x2py resolves Fortran types using the selected compiler, then generates an +explicit semantic contract (`.pyi`). Inspect that contract before calling the +wrapper because kind numbers are compiler-dependent. -The first example uses a small file named `numeric_types.f90`. Create it with -the complete source below: +--- + +## Example + +Create `numeric_types.f90`: ```fortran module numeric_types use iso_fortran_env, only: int32, real64 implicit none contains + integer(int32) function add_one(value) result(output) integer(int32), intent(in) :: value output = value + 1 @@ -42,22 +45,25 @@ contains logical(kind=1), intent(in) :: flag output = .not. flag end function invert + end module numeric_types ``` -It is highly recommended to generate the type contract first for inspection: +Generate the contract: ```bash python3 -m x2py generate --pyi numeric_types.f90 ``` -Build the wrapper with: +Build it: ```bash python3 -m x2py numeric_types.f90 --out-dir build/numeric-types ``` -Here is how to call the generated module from Python: +--- + +## Calling from Python ```python import sys @@ -74,195 +80,66 @@ assert api.conjugate_value(np.complex128(1.0 + 2.0j)) == np.complex128(1.0 - 2.0 assert bool(api.invert(True)) is False ``` -The tables below summarize the currently verified Fortran-to-Python type mappings. - -## Scalar Mapping - -| Fortran storage resolved by the compiler | Semantic `.pyi` type | Python input to prefer | NumPy array dtype | -| --- | --- | --- | --- | -| signed integer, 8 bits | `Int8` | `numpy.int8` | `numpy.int8` | -| signed integer, 16 bits | `Int16` | `numpy.int16` | `numpy.int16` | -| signed integer, 32 bits | `Int32` | `numpy.int32` | `numpy.int32` | -| signed integer, 64 bits | `Int64` | `numpy.int64` | `numpy.int64` | -| real, 32 bits | `Float32` | `numpy.float32` | `numpy.float32` | -| real, 64 bits | `Float64` | `numpy.float64` | `numpy.float64` | -| complex, 64 total bits | `Complex64` | `numpy.complex64` | `numpy.complex64` | -| complex, 128 total bits | `Complex128` | `numpy.complex128` | `numpy.complex128` | -| supported logical storage | `Bool` | `bool` or `numpy.bool_` as documented by the generated contract | `numpy.bool_` | -| scalar character | `String` or `String[n]` | `str` | fixed-width NumPy bytes, such as `S8` | -| derived type | generated class name | instance of that generated class | arrays of derived types are unsupported | -| dummy procedure | named `@prototype` reference | Python callable matching the named prototype | not applicable | - -## Source Kind Names - -Source spellings such as default `integer`, `integer(8)`, -`integer(kind=int64)`, or a selected-kind expression do not define a portable -NumPy dtype by themselves. x2py resolves the expression with the selected -compiler and then emits `Int8`, `Int16`, `Int32`, or `Int64`. Real, complex, -and logical kinds follow the same rule. - -Common `iso_fortran_env` and compiler-supported kind expressions are resolved -during the build. Inspect `--pyi` output whenever compiler flags, the compiler, -or target architecture changes. x2py blocks a mapping that cannot preserve the -native storage instead of silently narrowing it. - -## Scalar Values And Native Storage - -A bare numeric semantic type is a Python-visible value. `T` is a value -contract, not a copy of Fortran `intent`. Its default native boundary is -pass-by-value. -`Addr(Arg(...))` in `@native_call` means x2py converts that Python-visible value -to call-local native scalar storage and passes the storage address to native -code. - -Use `T[()]` when the Python API itself exposes safe caller-provided scalar -storage as a rank-0 NumPy array. Use `Addr(T)` only when the caller passes a raw -address such as `array.ctypes.data`. For raw array addresses such as -`Addr(Float64[n])`, every extent must be a fixed literal or a visible argument; -the address value itself does not carry shape. x2py does not reject zero or -negative integer addresses or validate that raw-address extents are positive. -It reports integer-to-pointer overflow, but otherwise the caller is responsible -for supplying a live address and a valid pointee shape before native code uses -either value. - -Arrays and strings are storage-like at the native boundary. `Float64[n]` already -passes the NumPy data address, and `String[8]` already passes the address of -x2py's temporary fixed-width character storage. Do not add `Addr(Arg(...))` for -those arguments. - -These annotations describe the native contract, not implicit Python -conversions. Use exact NumPy scalar types where the generated call requires -them. Scalar `intent(out)` values are normally hidden and returned as values; -caller-provided writable scalar storage is an explicit advanced `.pyi` -contract, not the default source-generated interface. - -Internally x2py treats the Python-extension extraction step and the native handoff step -as two different completed policies: - -| `.pyi` contract shape | Python barrier | Native barrier | -| --- | --- | --- | -| `T` | read a Python scalar value | pass the value or call-local scalar storage selected by policy | -| `@native_call([Addr(Arg(i))])` on bare `T` | read a Python scalar value | pass the address of call-local scalar storage | -| `T[()]` | validate rank-0 NumPy scalar storage | pass the caller-provided storage address | -| `T[:]`, `T[n]`, `T[:, :]` | validate NumPy array storage | pass the packed array descriptor/data contract | -| `String[n]` | read a Python `str` | pass x2py's call-local fixed-width character storage | -| `String[n][:]`, `String[:][:]` | validate NumPy bytes array storage | pass the character array descriptor/data contract | -| `Addr(T)` or `Addr(T[n])` | read a raw address value | pass that raw address unchanged | - -Those barrier actions are completed after semantic IR is loaded and before -wrapper lowering. Generated bridges and bindings dispatch from the completed -actions; they do not reinterpret datatype, `intent`, addressability, or local -memory details to choose a different behavior. - -## Arrays - -Array annotations combine an element dtype with rank and shape: - -| Semantic type | Python value | -| --- | --- | -| `Float64[:]` | rank-one dense array with `dtype=numpy.float64` | -| `Float64[:, :]` | rank-two dense array with `dtype=numpy.float64`; the wrapper requires the documented contiguous layout | -| `Float64[::]` | rank-one array whose axis may be a strided NumPy view | -| `Float64[::, ::]` | rank-two array whose axes may be strided NumPy views | -| `Float64[3, 4]` | exact shape `(3, 4)` | -| `Float64[n, :]` | first extent constrained by semantic constant or argument `n` | -| `Float64[Flat]` | flat contiguous storage for a supported assumed-size contract | -| `Float64[...]` | supported assumed-rank numeric storage, ranks 1 through 15 | - -Plain `:` records a dense axis. `::` records an axis where the contract allows -runtime strides; x2py reads that exact slice spelling from the `.pyi` source -before Python AST normalization. Multidimensional dense arrays in a -Fortran-facing contract use Fortran orientation by default; generated contracts -omit `ORDER_F`. Explicit order metadata appears only when a contract -deliberately requests a non-default representation. - -The wrapper validates exact dtype, native byte order, rank, known extents, -alignment, layout, and writeability before entering native code. It does not -silently cast, byte-swap, realign, or repair an incompatible array. Arrays later -expands layout, stride, output-storage, and zero-size rules. - -## Strings - -Scalar Fortran character values use Python `str`. `String[8]` records fixed -native length eight; plain `String` records assumed, deferred, or otherwise -non-fixed scalar length. The first `String[...]` subscription is character -length, not array shape. Bare `String[:]` is invalid; write `String` for a -scalar non-fixed string or add a second shape axis for an array. - -| Contract | Meaning | -| --- | --- | -| `String` | scalar string with unknown, assumed, deferred, or otherwise non-fixed length | -| `String[8]` | scalar fixed-length string with length 8 | -| `String[:][:]` | rank-one array of strings whose element length is not fixed in the public contract | -| `String[8][:]` | rank-one array of fixed-length strings | -| `String[8][()]` | mutable scalar fixed-length string storage | - -For `String[8]`, the encoded Python string length must be exactly eight; pass -`"aa "` when the native argument is an eight-character value. - -Returned strings are Python-owned copies. Fixed-length results retain trailing -Fortran blanks. For mutable scalar character input/output, x2py creates -call-local character storage, passes its address to native code, and returns a -replacement only when the `.pyi` signature includes `Returns["name", String[n]]`. -Without that return contract, native mutation is discarded because the original -Python `str` is immutable. - -Mutable scalar character storage uses a rank-zero fixed-width NumPy bytes array: +--- + +## Scalar Type Mapping + +| Fortran Type | Semantic Type | Preferred Python / NumPy Type | +|-------------------------------|-----------------|------------------------------------| +| `integer(1)` | `Int8` | `np.int8` | +| `integer(2)` | `Int16` | `np.int16` | +| `integer(4)` / `int32` | `Int32` | `np.int32` | +| `integer(8)` / `int64` | `Int64` | `np.int64` | +| `real(4)` | `Float32` | `np.float32` | +| `real(8)` / `real64` | `Float64` | `np.float64` | +| `complex(4)` | `Complex64` | `np.complex64` | +| `complex(8)` | `Complex128` | `np.complex128` | +| `logical` | `Bool` | `bool` or `np.bool_` | +| `character` | `String` / `String[n]` | `str` or fixed `np.bytes_` | +| Derived Type | Generated Class | Instance of that class | + +--- + +## Important Rules + +- Always use **exact NumPy scalar dtypes** (`np.float64`, `np.int32`, etc.). +- Plain Python `float` / `int` will raise `TypeError` for scalar arguments. +- x2py resolves kinds using the selected compiler (`gfortran` by default). +- Inspect the contract with `generate --pyi` whenever you change compiler flags or architecture. + +--- + +## Values And Native Storage + +A bare primitive type is a Python-visible value: + +```python +def scale(value: Float64) -> Float64: ... +``` + +If the native routine expects that scalar by reference, the generated +`@native_call` records the address handoff while the Python call stays simple: ```python -label = np.array("abcdefgh", dtype="S8") +@native_call([Addr(Arg(0))]) +def scale(value: Float64) -> Float64: ... ``` -The matching `.pyi` annotation is `String[8][()]`. Native mutation writes back -into the NumPy array, and Python reads the scalar storage as bytes, for example -`label[()]`. - -Character arrays use fixed-width NumPy bytes dtypes. `String[8][:]` is a rank-one -array of eight-character elements. `String[:][:]` is a rank-one array whose -element length is not fixed in the public contract. For `character(len=5) :: -names(:)`, Python passes and receives arrays with `dtype="S5"`. For an -allocatable deferred-length character array, the runtime allocation length -becomes the returned dtype itemsize. x2py treats these arrays as raw fixed-width -bytes storage; Python Unicode arrays, object arrays, and mutable scalar -deferred-length character storage remain blocked. - -## Derived Types - -A supported Fortran derived type becomes a generated Python extension class. -Scalar inputs accept that exact generated class or a supported descendant where -polymorphic dispatch is documented. Scalar outputs and function results become -wrapper-owned instances. Nested derived components are borrowed child wrappers -whose parent remains their owner. Do not treat a nested child as independently -owned native storage. Wrapping Derived Types later expands nested-object and -finalization behavior. - -## Unsupported Widths And Forms - -The semantic format can represent names such as `Float128` and `Complex256`, -but representation in a `.pyi` file is not a native binding support claim. Current -Fortran wrapper generation blocks: - -- real storage wider than 64 bits; -- complex storage wider than 128 total bits; -- wider explicit logical storage without a portable NumPy round trip; -- unsigned Fortran integer assumptions without a proved native mapping; -- character arrays that cannot be represented as fixed-width NumPy bytes - storage; and -- arrays of derived types. - -The language feature matrix later records the support status and evidence for a -wrapper-planning error. - -## Evidence And Troubleshooting - -Scalar integer, logical, real, and complex mappings are exercised by -[`test_scalar_kinds.py`](../../../tests/wrapper/fortran/scalars/test_scalar_kinds.py). -String behavior is exercised by -[`test_character_arguments.py`](../../../tests/wrapper/fortran/strings/test_character_arguments.py), -and array dtype validation by -[`test_array_contracts.py`](../../../tests/wrapper/fortran/arrays/test_array_contracts.py). - -For a wrong scalar or array dtype, compare the value with `--pyi` output and -convert explicitly at the Python call site. Runtime Issues later covers a -successful build that rejects a call, while Compiler Issues covers kind probing -and compiler-selection failures. +Use `T[()]` when the Python boundary is rank-zero NumPy storage. Arguments +accept a 0-D NumPy array, and results return a 0-D NumPy array. +Use type-level `Addr(T)` only for an API whose caller supplies a raw integer +address. Arrays and strings already carry storage and do not need an address +override. + +The semantic format can represent wider types such as `Float128` and +`Complex256`, but the current Fortran wrapper blocks real storage wider than 64 +bits and complex storage wider than 128 total bits instead of narrowing it. + +--- + +## Next + +- **[Arrays](arrays.md)** - Rank, shape, strides, and contiguity rules +- [Wrapping Derived Types](wrapping-derived-types.md) +- [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) +- Check the [Language Feature Matrix](../language-support/feature-matrix.md) for current type support status. diff --git a/docs/user/guide/distribution.md b/docs/user/guide/distribution.md index b82537ec4..a382ff24d 100644 --- a/docs/user/guide/distribution.md +++ b/docs/user/guide/distribution.md @@ -1,5 +1,6 @@ --- title: Distribution +description: How to share x2py-based extensions with other users and environments audience: users, packagers prerequisites: packaging related: packaging.md, ../troubleshooting/platform-specific-issues.md, ../getting-started/installation.md @@ -9,112 +10,79 @@ publication: reviewed # Distribution -The portable distribution unit today is the project source plus a reproducible -native build recipe, not a universal prebuilt wheel. A generated extension may -be shared only with environments that match its Python, NumPy, operating-system, -architecture, compiler ABI, and native-library assumptions. - -## Source Distribution Workflow - -Reuse the `scale-project` and `scale.f90` source first presented in -[Packaging](packaging.md#complete-local-project-example). Distribute these -inputs: - -```text -scale-project/ - src/ - scale.f90 - python/ - check_scale.py - requirements.txt - BUILDING.md -``` +x2py currently produces **platform-specific native extensions**. There is no stable, universal wheel format yet. The most reliable way to distribute your code is to share the **source + build recipe**. -`BUILDING.md` should record the exact supported build command: +--- -```bash -python3 -m x2py src/scale.f90 --out-dir build/scale -python3 python/check_scale.py -``` +## Recommended Distribution Approach: Source + Build Instructions -The asserted result remains the Python value `7.5`, as shown with the original -source in the packaging example. +Distribute your project like this: + +``` +my-project/ +├── src/ +│ └── mycode.f90 +├── tests/ +│ └── test_mycode.py +├── BUILDING.md # ← Important! +├── requirements.txt +└── README.md +``` -Record the required Python and NumPy versions, compiler family, compiler flags, -native libraries, library search paths, source order, and platform assumptions. -The receiving environment rebuilds the extension and runs the same smoke test. +**`BUILDING.md`** should clearly document: -## Sharing A Prebuilt Extension +```bash +# Build the extension +python3 -m x2py src/mycode.f90 --out-dir build/mycode -A prebuilt extension is a platform-specific artifact. Before sharing it, the -producer and consumer must match at least: +# Run tests +python3 -m pytest tests/ +``` -- operating system and architecture; -- Python implementation, major/minor version, and extension suffix; -- compatible NumPy runtime ABI; -- native compiler ABI and runtime libraries; -- linked native library versions and load paths; and -- extension module name and expected package namespace. +Include: +- Required Python and NumPy versions +- Compiler (e.g. gfortran) and version +- Any extra compiler flags or libraries +- Supported platforms -Use the produced extension file as-is; do not rename it without also preserving -its Python initialization symbol. +--- -Even when these facts appear to match, import and runtime smoke tests on the -target environment are required. Current CI evidence does not establish a -general portability matrix. +## Sharing Prebuilt Extensions -## Native Dependencies +You **can** share a built extension, but it is **highly platform-specific**. -x2py can link caller-supplied objects, archives, shared libraries, named -libraries, and library directories for supported builds. It does not bundle, -relocate, or discover those dependencies for distribution. The application or -platform packaging system remains responsible for: +Consumers must match: +- Operating system and architecture +- Python version and implementation +- NumPy ABI +- Compiler ABI and runtime libraries -- shipping redistributable native libraries; -- setting runtime loader paths; -- preserving compiler runtime dependencies; -- respecting library licenses; and -- validating symbols and ABI on the target platform. +Even small differences (e.g. different Python patch version or NumPy build) can cause import or runtime failures. -## Wheels And Source Archives +**Best practice**: Always include the source and build instructions even if you also provide a prebuilt binary. -x2py does not currently claim a stable automated wheel workflow, manylinux or -equivalent compliance, macOS universal binaries, Windows wheel support, or -automatic source-archive build hooks. A project may build custom packaging -around x2py, but that project owns the resulting portability and installation -contract. +--- -Do not label a wheel or source archive as generally supported merely because it -worked on the machine that produced it. +## Key Limitations -## Platform Boundaries +- No official wheel-building support yet +- No automatic bundling of native dependencies +- No cross-platform guarantees +- Extensions are tied to the exact Python + NumPy + compiler combination used to build them -The verified wrapper path uses a GNU native toolchain on the tested Linux -environment. Other platforms and compilers require their own build, ABI, -import, runtime, ownership, and cleanup evidence. See -[Installation](../getting-started/installation.md) for current prerequisites. +--- ## Release Checklist -Before distributing a wrapper project: - -1. Generate and review semantic `.pyi` output. -2. Build from a clean output directory with recorded source order and flags; - resolve any wrapper-plan errors reported by the default build. -3. Preserve the exact build command, source order, flags, and Makefile manifest - when one is generated. -4. Run asserted calls for every public routine used by the application. -5. Test expected invalid dtype, rank, shape, and ownership cases. -6. Rebuild and rerun on every claimed target environment. -7. State unsupported platforms and external dependencies explicitly. +Before releasing: +1. Generate and review the semantic `.pyi` contract +2. Build from a clean directory +3. Run all tests with realistic inputs +4. Document supported platforms and dependencies clearly +5. Test on target environments when possible -## Evidence And Troubleshooting +--- -Local output placement and importable artifact creation are exercised by -[`test_build_modes.py`](../../../tests/wrapper/fortran/build_from_source/test_build_modes.py). -Caller-ordered multi-source and external-library builds are exercised by the -focused wrapper suites recorded later in the language feature matrix. +## Next -No repository evidence currently proves universal wheel portability. -Platform-Specific Issues and Build Issues later cover target-environment -limitations and failures while rebuilding from source. +- See [Platform-Specific Issues](../troubleshooting/platform-specific-issues.md) for common distribution problems. diff --git a/docs/user/guide/editing-semantic-pyi-contracts.md b/docs/user/guide/editing-semantic-pyi-contracts.md index 6137d3bc0..52010ac32 100644 --- a/docs/user/guide/editing-semantic-pyi-contracts.md +++ b/docs/user/guide/editing-semantic-pyi-contracts.md @@ -48,6 +48,13 @@ python3 -m x2py contracts/edited_solver/__init__.pyi \ The semantic `.pyi` entry contract selects the wrapper build automatically; the native artifact options provide the implementation to compile or link. +When the implementation should be compiled from source during the edited +contract build, pass the native sources instead: + +```bash +python3 -m x2py contracts/solver/__init__.pyi \ + --native-fortran-sources solver.f90 +``` The entry `.pyi` is the sole semantic input to wrapper generation. x2py does not reparse the native source to restore a removed declaration, projection, or @@ -331,12 +338,10 @@ assert module.scalar_status(base, status) is None assert status[()] == np.int32(15) ``` -This exact edit is compiled and exercised by -[`test_native_order_contracts.py`](../../../tests/wrapper/fortran/edit_pyi_contracts/test_native_order_contracts.py). -It covers scalar, array, matrix, string, mixed-result, and derived-type native -order calls. An ordinary Python `str` cannot observe mutation of the temporary -native character buffer; use a projected replacement when Python must see the -new string. +This style covers scalar, array, matrix, string, mixed-result, and derived-type +native-order calls. An ordinary Python `str` cannot observe mutation of the +temporary native character buffer; use a projected replacement when Python must +see the new string. For a native-order scalar character dummy, keep the Python boundary as `String[n]`: @@ -401,9 +406,7 @@ def scale_with_status( ``` At runtime, x2py copies `values` into mutable native storage, calls native code, -and returns a different NumPy array. The original remains unchanged. The -compiled evidence is -[`test_policy_dispatch_contracts.py`](../../../tests/wrapper/fortran/edit_pyi_contracts/test_policy_dispatch_contracts.py). +and returns a different NumPy array. The original remains unchanged. Mutability is a property of this argument boundary, not of `Float64`, `String[n]`, or another datatype in isolation. The same datatype may be an @@ -713,24 +716,9 @@ Do not fix a policy blocker by deleting metadata until the wrapper happens to build. The corrected contract must explicitly describe the intended boundary behavior and its owner. -## Runtime Evidence - -Editable-contract runtime fixtures live under -[`tests/wrapper/fortran/edit_pyi_contracts`](../../../tests/wrapper/fortran/edit_pyi_contracts/README.md): - -- `test_native_order_contracts.py` removes `@native_call` and exposes native - argument order; -- `test_ownership_contracts.py` applies explicit native-owned module, - wrapper-owned field, and wrapper-owned result-handle lifetime policies to the - same descriptor concept; -- `test_visibility_contracts.py` removes and hides declarations while checking - unaffected runtime behavior; -- `test_surface_edit_contracts.py` removes classes, methods, constructors, - fields, and overload candidates and adds renamed bindings and overloads; and -- `test_policy_dispatch_contracts.py` proves immutable replacement through the - completed ownership/action policy. - -Broader handle ownership and lifetime evidence is in -[`test_allocatable_views.py`](../../../tests/wrapper/fortran/module_state/test_allocatable_views.py). -The Semantic `.pyi` Wrapper Checklist later provides the active completion -ledger. +## Troubleshooting + +When an edited contract fails, reduce the edit to one surface change at a time: +visibility, call ordering, ownership, mutability, or type shape. Keep the +generated contract nearby and compare each intentional edit against the native +artifact you are asking x2py to call. diff --git a/docs/user/guide/enumerations.md b/docs/user/guide/enumerations.md index 9ee80efb0..d7158c5d3 100644 --- a/docs/user/guide/enumerations.md +++ b/docs/user/guide/enumerations.md @@ -1,5 +1,6 @@ --- title: Enumerations +description: How x2py handles Fortran `enum` and enumerators audience: users prerequisites: wrapping modules, data types related: wrapping-modules.md, generic-interfaces.md, ../language-support/feature-matrix.md @@ -9,28 +10,32 @@ publication: reviewed # Enumerations -Supported Fortran enumerators become typed integer constants. x2py does not -generate Python `Enum` or `IntEnum` classes, and values passed through -procedures or fields remain the resolved integer dtype. +x2py turns supported Fortran `enum` declarations into **typed integer constants**. It does **not** generate Python `Enum` or `IntEnum` classes — values remain plain integers with the resolved dtype. -## Complete Enumeration Example +--- + +## Complete Example Create `colors.f90`: ```fortran module colors_api implicit none + enum, bind(C) enumerator :: red = -1 enumerator :: blue enumerator :: green = 10 enumerator :: yellow end enum + contains + integer(4) function round_trip_color(value) result(output) integer(4), intent(in) :: value output = value end function round_trip_color + end module colors_api ``` @@ -40,7 +45,9 @@ Build it: python3 -m x2py colors.f90 --out-dir build/colors ``` -The generated constants retain explicit and implicit values: +--- + +## Usage in Python ```python import sys @@ -50,41 +57,37 @@ sys.path.insert(0, "build/colors") import colors api = colors.colors_api + assert api.red == np.int32(-1) assert api.blue == np.int32(0) assert api.green == np.int32(10) assert api.yellow == np.int32(11) -assert api.round_trip_color(np.int32(api.green)) == np.int32(10) -``` -## Generated Contract +# Pass enumerator values to procedures +result = api.round_trip_color(api.green) +assert result == np.int32(10) +``` -The semantic `.pyi` exposes constants as `Final[Int32]` values for this -resolved representation. A variable or field holding one of these values still -uses `Int32`; it does not acquire a distinct Python enum type. +--- -The constants are read-only native facts. Python assignment can only shadow a -module attribute; it cannot mutate the native enumerator. +## Key Points -## Naming And Type Checking +- Enumerators become **read-only** constants on the module. +- They use the resolved integer dtype (usually `Int32`). +- Assigning to them in Python only creates a local shadow — it does **not** change the native value. +- No automatic runtime validation — passing any integer of the correct dtype works. +- Static type checkers see them as integer constants. -Generated names follow the normal visibility, keyword escaping, and collision -policy. Static type checkers see integer constants and integer parameters. Code -that needs a project-specific Python `Enum` may define one in application code -and pass `numpy.int32(member.value)` to the wrapper. +--- ## Limitations -- No runtime validation restricting an integer parameter to declared - enumerator values unless the native routine performs that validation. -- Unsupported source enum forms stop at parsing, semantic conversion, or wrapper - planning instead of being converted into unrelated constants. +- No native `Enum` class is generated in Python. +- If you want a proper Python `Enum`, define one in your application code and pass `.value` (as `np.int32`). -## Evidence And Troubleshooting +--- -Value preservation, `Final[Int32]` emission, absence of Python enum classes, -field behavior, and runtime round trip are exercised by -[`test_fortran_enums.py`](../../../tests/wrapper/fortran/scalars/test_fortran_enums.py). +## Next -Use [Data Types](data-types.md) for integer width and -[Wrapping Modules](wrapping-modules.md) for constant attribute behavior. +- Continue with [Callbacks](callbacks.md). +- Check the [Language Feature Matrix](../language-support/feature-matrix.md) for current enum support. diff --git a/docs/user/guide/error-handling.md b/docs/user/guide/error-handling.md index c741f88a0..2466f016f 100644 --- a/docs/user/guide/error-handling.md +++ b/docs/user/guide/error-handling.md @@ -1,5 +1,6 @@ --- title: Error Handling +description: How x2py reports errors at different stages and how to diagnose them audience: users, advanced users prerequisites: common beginner workflow, data types related: ../reference/diagnostic-codes.md, ../troubleshooting/index.md, callbacks.md @@ -9,170 +10,77 @@ publication: reviewed # Error Handling -Failures occur at distinct stages. Parsing rejects syntax that x2py cannot -model; semantic conversion records contract facts; post-IR policy completion -records every wrapper decision and its precise unsupported reason; wrapper -planning retrieves those completed decisions and raises at the owning -declaration; compilation and linking diagnose native-language and build failures; -Python calls validate values at runtime. Some native termination and callback -failures terminate the process. +x2py reports failures at several distinct stages. Understanding which stage failed helps you know where to look and what to fix. -This is the general error guide for x2py. The Diagnostic Codes reference lists -stable parser and preprocessing categories; this page explains what to do at -each stage. +--- -## Complete Status-Projection Example +## Failure Stages -Create `solver.f90`: +| Stage | Typical Cause | What to do | +|-----------------------------|----------------------------------------------------|----------| +| Preprocessing / Parsing | Syntax x2py can't model, missing include | Check the diagnostic code and source location | +| Semantic Conversion | Unresolved types, missing contract facts | Fix source or edit the generated `.pyi` | +| Policy Completion & Planning| Unsupported ownership, layout, callback, etc. | Read the full error message — it points to the problematic declaration | +| Compilation / Linking | Compiler issues, missing modules/libraries | Run with `--verbose` to see native commands | +| Import | Missing shared library, ABI mismatch | Check paths and environment | +| Python Call | Wrong dtype, shape, layout, class, etc. | Match the generated contract | +| Native Execution | Application-level status (e.g. error code) | Use `@raises` projection or handle manually | +| Callback / Fatal | Exception in callback, `stop`, `error stop` | Process usually aborts | -```fortran -module solver - implicit none -contains - subroutine solve(value, status, message) - integer(4), intent(in) :: value - integer(4), intent(out) :: status - character(len=32), intent(out) :: message - - if (value < 0) then - status = 1 - message = "negative input" - else - status = 0 - message = "" - end if - end subroutine solve -end module solver -``` - -Generate an editable contract package: +--- -```bash -python3 -m x2py generate --pyi solver.f90 --out contracts/solver -``` +## Status Projection Example -In `contracts/solver/solver.pyi`, keep the generated native types and add -the explicit status policy: +You can turn Fortran status codes into Python exceptions using the `@raises` decorator in an edited contract. -```python -from x2py.contracts import Addr, Arg, Int32, Return, String, native_call, raises +**Example:** -@raises(status="status", message="message", success=0) -@native_call([Addr(Arg(0)), Return("status", 0), Return("message", 1)]) -def solve( - value: Int32, -) -> tuple[Int32, String[32]]: ... +```fortran +subroutine solve(value, status, message) + integer(4), intent(in) :: value + integer(4), intent(out) :: status + character(len=32), intent(out) :: message + ... +end subroutine ``` -Build that contract against the same simple native source: +In your edited `.pyi`: -```bash -python3 -m x2py contracts/solver/__init__.pyi \ - --native-fortran-sources solver.f90 \ - --out-dir build/solver +```python +@raises(status="status", message="message", success=0) +def solve(value: Int32) -> None: ... ``` -The success outputs are consumed, while a nonzero status becomes -`RuntimeError` with the native message: +Then: ```python -import sys -import numpy as np - -sys.path.insert(0, "build/solver") -import solver - -api = solver.solver -assert api.solve(np.int32(1)) is None - try: api.solve(np.int32(-1)) -except RuntimeError as error: - assert "negative input" in str(error) -else: - raise AssertionError("expected RuntimeError") +except RuntimeError as e: + print(e) # "negative input" ``` -Status projection is opt-in. Without `@raises`, status and message remain -ordinary outputs. Editing Semantic `.pyi` Contracts later defines the supported -workflow for changing a generated contract. +--- -## Failure Layers +## Common Python Exceptions -| Layer | Typical failure | User action | -| --- | --- | --- | -| preprocessing or parsing | missing include, unsupported parser syntax, or a declaration x2py cannot model | read the diagnostic code and source location | -| semantic conversion | unresolved contract type, missing compile-time fact, or incomplete imported contract data | correct the source facts or edit the generated `.pyi` contract | -| post-IR policy completion and wrapper planning | unsupported ownership, ABI, pointer, array, callback, overload, or storage decision; inconsistent completed policy | read the owner path and reason in the wrapper-build error; planning does not retry another route | -| compilation or linking | missing compiler, module, object, symbol, or library | rerun with `--verbose`; inspect the native build plan | -| import | missing shared dependency, wrong ABI, wrong output path | inspect the shared library and runtime environment | -| Python call | wrong dtype, rank, shape, layout, writeability, class, or callable | pass a value matching the generated contract | -| native execution | application status output | return it normally or opt into documented `@raises` policy | -| native termination | `stop`, `error stop`, abort, fatal finalizer | isolate risky calls; Python cannot recover | -| callback boundary | callback exception or invalid result | traceback is printed and the host process aborts | +- `TypeError` — Wrong dtype, rank, shape, layout, class, or callback +- `ValueError` — Invalid options or contract values +- `RuntimeError` — Native status projected as exception +- `ImportError` / `OSError` — Extension loading problems -## Wrapper Build Errors +--- -The default wrapper build is the only compiled-wrapper decision path. With no -subcommand, it completes semantic policy, projects the typed wrapper plan, -generates source, and invokes the native build. There is no separate wrapper -selector, preflight report, or support-analysis command to run. +## Best Practices -An unsupported completed policy stops at its owner while the wrapper build -follows that path. For example: +- Always start with the **full error message** — it usually tells you exactly what went wrong. +- Use `--verbose` when investigating build failures. +- For complex contracts, generate the `.pyi` first and inspect it. +- Run risky or untrusted callbacks in a subprocess if you need the main process to survive failures. -```text -x2py: error: Semantic class 'shapes.shape' has unsupported derived-type policy: abstract derived types need a non-instantiable Python class policy -``` +--- + +## Next -The owner path points at the declaration whose completed decision cannot be -implemented. Fix that contract in the source or editable `.pyi`, then rerun -the same wrapper-build command. - -Examples include a missing completed policy, an unsupported derived-type shape, -an incomplete callback contract, or an unsafe ownership/array layout. These are -ordinary errors from the real policy-completion or planning call, not entries in -a separate report. - -Do not duplicate native-language validation in these stages. If x2py has enough -information to build the semantic contract and plan, errors such as an invalid -defined operator or invalid native source syntax remain the native compiler's -responsibility. The compiler command and diagnostic are shown by `--verbose`. - -## Python Exception Types - -- `TypeError` covers wrong Python object type, scalar dtype, array dtype/rank/ - shape/layout/writeability, wrong generated class, non-callable callback, and - failed result conversion. -- `ValueError` covers invalid wrapper options and contract values where a Python - value is structurally wrong rather than the wrong object category. -- `MemoryError` reports failure to allocate a required Python result copy. -- `RuntimeError` is used by explicit native status projection. -- `ImportError` or loader-specific `OSError` can report extension or shared - dependency loading failures. - -Exact wording is not a substitute for the stable category. Diagnostic codes -for inspection stages are catalogued later in the Diagnostic Codes reference. - -## Cleanup Guarantees - -Validation that fails before native entry releases generated temporaries and -does not call the routine. Successful and exceptional conversion paths release -call-local storage according to completed ownership policy. Wrapper-owned -objects use their generated deallocator; borrowed views do not free native -storage. - -No cleanup promise can recover from process termination, native memory -corruption, or a fatal callback boundary. - -## Evidence And Troubleshooting - -Status-to-exception projection and GIL policy are exercised by -[`test_runtime_policies.py`](../../../tests/wrapper/fortran/runtime_behavior/test_runtime_policies.py). -Validation failures are exercised throughout the focused wrapper suites, and -fatal callback behavior by -[`test_scalar_callbacks.py`](../../../tests/wrapper/fortran/callbacks/test_scalar_callbacks.py). - -Troubleshooting later provides focused routes for environmental failures. Use -`--debug` only when an x2py traceback is needed; ordinary user diagnostics -should remain concise. +- [Editing Semantic `.pyi` Contracts](editing-semantic-pyi-contracts.md) +- Check the [Diagnostic Codes](../reference/diagnostic-codes.md) reference for detailed error categories diff --git a/docs/user/guide/fortran-wrapper.md b/docs/user/guide/fortran-wrapper.md index e27d0d6da..ea0356384 100644 --- a/docs/user/guide/fortran-wrapper.md +++ b/docs/user/guide/fortran-wrapper.md @@ -17,20 +17,84 @@ The guide follows the wrapper by subject. Each subject includes a small example showing the Fortran interface and the corresponding Python use. Examples omit unrelated module scaffolding when that makes the contract easier to see. -Runtime evidence lives in -[`tests/wrapper`](../../../tests/wrapper/fortran/README.md). A behavior is supported -only when generated native sources compile, the extension imports, and Python -tests exercise successful calls, mutation, lifetime, and relevant failures. - This guide covers the implemented wrapper for Fortran source inputs. + + ## Contents - Foundations: [building a wrapper](#building-and-importing-a-wrapper), - [support evidence](#how-support-claims-are-established), and + [support boundaries](#how-support-claims-are-established), and [ownership and lifetime](#ownership-and-lifetime) -- Arrays and pointers: [allocatables](#allocatable-arguments-results-and-views), - [pointers](#pointer-arguments-results-and-association), - [array results](#array-valued-function-results), and - [NumPy argument contracts](#numpy-array-argument-contracts) +- Arrays and pointers: [allocatables](allocatables.md), + [pointers](pointers.md), [array results](arrays.md), and + [NumPy argument contracts](arrays.md) - Objects and state: [derived types](#derived-types-across-procedure-boundaries), [inheritance](#inheritance-and-polymorphism), [constructors/finalizers](#constructors-initialization-and-finalizers), @@ -310,13 +373,6 @@ Format reference. The Semantic `.pyi` Wrapper Checklist later records parity completion. -Runtime tests: [`test_pyi_wrapper_builds.py`](../../../tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py), -[`test_contract_package_runtime.py`](../../../tests/wrapper/fortran/build_from_pyi/test_contract_package_runtime.py), -[`test_native_order_contracts.py`](../../../tests/wrapper/fortran/edit_pyi_contracts/test_native_order_contracts.py), -[`test_ownership_contracts.py`](../../../tests/wrapper/fortran/edit_pyi_contracts/test_ownership_contracts.py), -[`test_surface_edit_contracts.py`](../../../tests/wrapper/fortran/edit_pyi_contracts/test_surface_edit_contracts.py), -[`test_visibility_contracts.py`](../../../tests/wrapper/fortran/edit_pyi_contracts/test_visibility_contracts.py), and -[`test_policy_dispatch_contracts.py`](../../../tests/wrapper/fortran/edit_pyi_contracts/test_policy_dispatch_contracts.py). Use `--verbose` to execute a build while printing every exact, shell-escaped compiler and linker command. It first announces binding, bridge, and header @@ -511,9 +567,9 @@ A wrapper feature is considered supported only when all applicable layers agree: - the default wrapper build emits a precise error when a declaration is unsupported or lacks policy; - semantic lowering preserves the contract without reconstructing source text; -- runtime tests import the extension and verify results, mutation, lifetime, - ownership, and invalid calls; and -- fixed-form and free-form behavior are both tested when the source feature +- runtime behavior is covered by the project verification policy before it is + presented as supported; and +- fixed-form and free-form behavior are both considered when the source feature exists in both forms. ## Allocatable Arguments, Results, And Views @@ -996,9 +1041,10 @@ Allocatable behavior depends on where the allocation lives. Top-level allocatable array function results and non-optional hidden allocatable array outputs return wrapper-owned `AllocatableArray` objects. -Allocated and unallocated native states both return a present handle. The -handle owns persistent descriptor storage and releases it on `close()` or -finalization. +Allocated, zero-sized, and unallocated native states all return a present +handle. The handle owns persistent descriptor storage and releases it on +`close()` or finalization. Use a hidden allocatable output dummy when the native +API already expresses the result as an `intent(out)` argument. ```fortran function make_vector(n) result(values) @@ -1076,12 +1122,6 @@ state, pointer `INTENT(IN)`, deliberate incompatibilities, and calls with many derived objects, is maintained in [Scalar Actuals And Native Dummies](wrapping-derived-types.md#scalar-actuals-and-native-dummies). -Runtime tests: -[`test_allocatable_views.py`](../../../tests/wrapper/fortran/module_state/test_allocatable_views.py) -and -[`test_scalar_derived_actual_dummy_matrix.py`](../../../tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py) -and -[`test_allocatable_replacement.py`](../../../tests/wrapper/fortran/module_state/test_allocatable_replacement.py). ## Pointer Arguments, Results, And Association @@ -1152,15 +1192,14 @@ Metadata can select implemented descriptor extraction and policy-gated operations. It cannot invent stable owner storage for a pointer-array result or make an unproved persistent reassociation safe. -Runtime tests: [`test_pointers.py`](../../../tests/wrapper/fortran/derived_types/test_pointers.py). ## Array-Valued Function Results Numeric explicit-shape and automatic-shape array function results are returned as new Python-owned NumPy arrays. Allocatable array results use owned -`AllocatableArray` objects instead. Pointer-array results remain blocked -because a returned pointer association does not establish stable owner storage -or target lifetime. +`AllocatableArray` objects instead, including matrices and higher-rank arrays. +Pointer-array results remain blocked because a returned pointer association does +not establish stable owner storage or target lifetime. ```fortran function spectrum(n) result(values) @@ -1187,7 +1226,6 @@ result is `None`. Arrays of derived types are blocked because their element layout, construction, destruction, aliasing, and copy policy are not defined. -Runtime tests: [`test_array_results.py`](../../../tests/wrapper/fortran/arrays/test_array_results.py). ## NumPy Array Argument Contracts @@ -1225,6 +1263,8 @@ subroutine scale_matrix(n, m, values) end subroutine scale_matrix ``` +X2PY_C_DOCS_END --> + -Runtime tests: [`test_fortran_enums.py`](../../../tests/wrapper/fortran/scalars/test_fortran_enums.py). ## Character Arguments, Results, And Fields @@ -1694,8 +1716,6 @@ Python Unicode arrays, object arrays, mutable scalar deferred-length character storage, deferred-length character fields, and mutable character-buffer fields remain blocked until an explicit field and encoding policy exists. -Runtime tests: [`test_character_arguments.py`](../../../tests/wrapper/fortran/strings/test_character_arguments.py) -and [`test_character_edge_cases.py`](../../../tests/wrapper/fortran/strings/test_character_edge_cases.py). ## Scalar Types And Kind Coverage @@ -1742,7 +1762,6 @@ than 64 bits and complex storage wider than 128 bits are blocked rather than silently down-converted. Wider explicit logical kinds are blocked because they lack a portable Python/NumPy Boolean round-trip contract. -Runtime tests: [`test_scalar_kinds.py`](../../../tests/wrapper/fortran/scalars/test_scalar_kinds.py). ## Derived-Type Layout And Interoperability @@ -1785,7 +1804,6 @@ compiler-validated size, alignment, padding, component offsets, and nested layout, with accessor fallback whenever proof is unavailable. X2PY_C_DOCS_END --> -Runtime tests: [`test_derived_layout.py`](../../../tests/wrapper/fortran/derived_types/test_derived_layout.py). ## Multiple Sources And Build Modes @@ -1930,12 +1948,6 @@ python3 -m x2py generate --makefile contracts/solver.pyi \ python3 -m x2py --build-manifest build/solver/x2py-build.json ``` -Runtime tests: [`test_multi_source_builds.py`](../../../tests/wrapper/fortran/multiple_files/test_multi_source_builds.py), -[`test_external_procedures.py`](../../../tests/wrapper/fortran/external_routines/test_external_procedures.py), -[`test_real_blas_lapack.py`](../../../tests/wrapper/fortran/real_libraries/test_real_blas_lapack.py), -[`test_stage7_native_bundles.py`](../../../tests/wrapper/fortran/real_libraries/test_stage7_native_bundles.py), -[`test_build_modes.py`](../../../tests/wrapper/fortran/build_from_source/test_build_modes.py), and -[`test_compiler_verbose.py`](../../../tests/wrapper/fortran/build_from_source/test_compiler_verbose.py). ## Visibility, Naming, And The Python Surface @@ -2015,7 +2027,6 @@ With `--strict-wrapper-names`, x2py applies no fixes. Any name requiring keyword or identifier escaping, or any collision after normalization, raises a generation error before native compilation. -Runtime tests: [`test_visibility_naming.py`](../../../tests/wrapper/fortran/naming/test_visibility_naming.py). ## Immediate Python Callbacks @@ -2101,10 +2112,6 @@ X2PY_C_DOCS_END --> Stored callbacks, callback registration, optional dummy procedures, procedure pointers, and invocation after the wrapped call are not supported. -Runtime tests: [`test_all_callback_shapes.py`](../../../tests/wrapper/fortran/callbacks/test_all_callback_shapes.py), -[`test_scalar_callbacks.py`](../../../tests/wrapper/fortran/callbacks/test_scalar_callbacks.py), -[`test_array_callbacks.py`](../../../tests/wrapper/fortran/callbacks/test_array_callbacks.py), and -[`test_derived_callbacks.py`](../../../tests/wrapper/fortran/callbacks/test_derived_callbacks.py). ## Runtime Errors, The GIL, OpenMP, And Concurrency @@ -2190,10 +2197,6 @@ The verified compiler path includes GNU Fortran and debug/optimized ABI builds. Other compilers and platforms require their own ABI validation; support is not inferred from GNU results. -Runtime tests: [`test_runtime_policies.py`](../../../tests/wrapper/fortran/runtime_behavior/test_runtime_policies.py), -[`test_runtime_recursion.py`](../../../tests/wrapper/fortran/runtime_behavior/test_runtime_recursion.py), -[`test_openmp_runtime.py`](../../../tests/wrapper/fortran/runtime_behavior/test_openmp_runtime.py), and -[`test_runtime_abi.py`](../../../tests/wrapper/fortran/build_from_source/test_runtime_abi.py). ## Not Handled Or Not Yet Settled @@ -2265,30 +2268,9 @@ wrappers: | Layout | Direct C struct views of Fortran derived types | Compiler-validated size, alignment, padding, offsets, and nested layout. | X2PY_C_DOCS_END --> -## Finding The Runtime Tests - -The subject index in [`tests/wrapper/fortran/README.md`](../../../tests/wrapper/fortran/README.md) -maps each feature to its Python runtime tests and fixture routes. Native source -fixtures are being consolidated under the shared `tests/data/fortran/` corpus so -the same valid source can exercise parser, semantic IR, `.pyi`, and -wrapper stages. Runtime semantic `.pyi` contracts remain with the wrapper tests -that consume them. Subject modules use descriptive test names, and builds that -wrap several related sources together use the -[`multiple_files`](../../../tests/wrapper/fortran/multiple_files) directory. - -Generated `.pyi` package fixtures for source-driven wrapper subjects are checked -by [`test_source_generated_pyi_contracts.py`](../../../tests/wrapper/fortran/build_from_source/test_source_generated_pyi_contracts.py), -[`test_array_generated_pyi_contracts.py`](../../../tests/wrapper/fortran/arrays/test_array_generated_pyi_contracts.py), -[`test_scalar_generated_pyi_contracts.py`](../../../tests/wrapper/fortran/scalars/test_scalar_generated_pyi_contracts.py), -[`test_function_call_generated_pyi_contracts.py`](../../../tests/wrapper/fortran/function_calls/test_function_call_generated_pyi_contracts.py), -[`test_native_call_examples.py`](../../../tests/wrapper/fortran/function_calls/test_native_call_examples.py), -[`test_string_generated_pyi_contracts.py`](../../../tests/wrapper/fortran/strings/test_string_generated_pyi_contracts.py), -[`test_derived_type_generated_pyi_contracts.py`](../../../tests/wrapper/fortran/derived_types/test_derived_type_generated_pyi_contracts.py), -[`test_callback_generated_pyi_contracts.py`](../../../tests/wrapper/fortran/callbacks/test_callback_generated_pyi_contracts.py), -[`test_module_state_generated_pyi_contracts.py`](../../../tests/wrapper/fortran/module_state/test_module_state_generated_pyi_contracts.py), -[`test_runtime_behavior_generated_pyi_contracts.py`](../../../tests/wrapper/fortran/runtime_behavior/test_runtime_behavior_generated_pyi_contracts.py), -and [`test_naming_generated_pyi_contracts.py`](../../../tests/wrapper/fortran/naming/test_naming_generated_pyi_contracts.py). - -Semantic-only details, edited `.pyi` round trips, and wrapper-planning diagnostics also -have narrower tests outside `tests/wrapper`, but those tests do not replace -compiled runtime evidence. +## Troubleshooting + +If a documented wrapper behavior does not match the generated extension, first +compare the native source, generated `.pyi`, Python call, dtype, shape, and +ownership expectations. Use `--verbose` for build failures and reduce runtime +failures to the smallest source and call that still reproduces the mismatch. diff --git a/docs/user/guide/generic-interfaces.md b/docs/user/guide/generic-interfaces.md index 2013b324c..c23325c35 100644 --- a/docs/user/guide/generic-interfaces.md +++ b/docs/user/guide/generic-interfaces.md @@ -1,5 +1,6 @@ --- title: Generic Interfaces +description: How x2py supports Fortran named generics, type-bound generics, operators, and defined assignment audience: users, advanced users prerequisites: wrapping functions, wrapping subroutines, data types related: optional-arguments.md, wrapping-derived-types.md, error-handling.md @@ -9,12 +10,11 @@ publication: reviewed # Generic Interfaces -Named module and type-bound generic interfaces become one Python-visible -callable backed by a checked overload set. Dispatch uses exact scalar or array -dtype, rank, and generated extension class; it does not perform broad numeric -coercion. +x2py turns Fortran named generic interfaces (and type-bound generics) into a single Python callable backed by an overload set. Dispatch is based on **exact** dtype, rank, and generated class — no implicit numeric coercion is performed. -## Complete Generic Example +--- + +## Complete Example Create `generic.f90`: @@ -23,11 +23,14 @@ module conversions implicit none private public :: convert + interface convert module procedure convert_integer module procedure convert_real end interface convert + contains + integer(4) function convert_integer(value) result(output) integer(4), intent(in) :: value output = value + 10 @@ -37,36 +40,8 @@ contains real(8), intent(in) :: value output = value + 0.5_8 end function convert_real -end module conversions -``` - -Inspecting `generic.f90` prints the private specifics and public overload -contracts: -```python -from x2py.contracts import Addr, Arg, Float64, Int32, native_call, overload, private - -@private -@native_call([Addr(Arg(0))]) -def convert_integer( - value: Int32 -) -> Int32: ... - -@private -@native_call([Addr(Arg(0))]) -def convert_real( - value: Float64 -) -> Float64: ... - -@overload("convert_integer") -def convert( - value: Int32 -) -> Int32: ... - -@overload("convert_real") -def convert( - value: Float64 -) -> Float64: ... +end module conversions ``` Build it: @@ -75,7 +50,9 @@ Build it: python3 -m x2py generic.f90 --out-dir build/generic ``` -The public generic dispatches by exact dtype: +--- + +## Usage in Python ```python import sys @@ -85,79 +62,41 @@ sys.path.insert(0, "build/generic") import generic api = generic.conversions + assert api.convert(np.int32(4)) == np.int32(14) assert api.convert(np.float64(4.0)) == np.float64(4.5) ``` -## Calling A Generic - -The complete example covers integer and real overloads. Complex, array, and -generated-class overloads follow the same exact dtype/rank/class rule when -their specifics are supported. - -The generated `.pyi` contains overload declarations associated with concrete -native targets. The public generic name remains one callable. `@native_call` -belongs on the concrete specific procedure, not on the `@overload(...)` -declaration that links the public Python signature to that target. +The correct specific procedure is chosen automatically based on the argument type. -## Type-Bound Generics - -Type-bound overloads dispatch after accounting for the implicit passed object. -For example, one documented generated method may provide distinct `Int32` and -`Float64` call shapes under the same public name. - -Supported scalar polymorphic input dispatch uses the generated base and -descendant wrapper classes. Descendants are checked before the base class so a -concrete descendant selects its concrete bridge. +--- -## No Match And Ambiguity +## Key Rules -A value with no matching specific raises `TypeError`. If two native specifics -collapse to the same Python dtype/rank/class signature, wrapper generation -rejects the overload set deterministically. Declaration order is never used as -an ambiguity tiebreaker. +- Dispatch uses **exact** match on dtype, rank, and generated class. +- If no overload matches, a `TypeError` is raised. +- If two specifics collapse to the same Python signature, wrapper generation fails (ambiguity is rejected). +- Type-bound generics also work and dispatch after accounting for the passed object. -Changing an overload set in an edited semantic `.pyi` must preserve distinct -supported signatures and valid native targets. Removing an overload removes -that Python call shape; it does not remove the native implementation. +--- -## Defined Operators +## Defined Operators and Assignment -Defined operators use Python data-model slots only where Python has equivalent -syntax. Supported arithmetic, unary, comparison, reverse, and safe in-place -forms can therefore appear as normal Python operations such as `left + right` -or a reverse operation when the native specifics define that operand order. +- Supported operators (`+`, `-`, `*`, `==`, etc.) can be used with normal Python syntax when the native generic defines them. +- Defined assignment (`=`) is exposed as an explicit `.assign(...)` method because Python `=` only rebinds names. -Named native operators without Python syntax become documented methods rather -than invented operators. +--- -## Defined Assignment +## Limitations -Python `=` rebinds a name and cannot invoke native defined assignment. x2py -exposes supported native assignment as an explicit mutating `assign(...)` -method that returns the same receiver object. +- Generic constructors and initialization overloads are not yet supported. +- Polymorphic (`class(*)`) arguments and results are blocked. +- Arrays of derived types and complex polymorphic cases are not supported yet. -Named generics and operator/assignment lowering are separate contracts even -though both use overload dispatch. +--- -## Limitations +## Next -- Generic constructor interfaces and overloaded runtime initialization are - blocked. -- Polymorphic results, mutable polymorphic arguments, arrays, pointer/allocatable - polymorphic scalars, and `class(*)` are blocked. -- Unsupported operands raise deterministic Python errors; x2py does not fall - back to a different specific. - -## Evidence And Troubleshooting - -Named and type-bound generic dispatch is exercised by -[`test_generic_interfaces.py`](../../../tests/wrapper/fortran/naming/test_generic_interfaces.py), -operator and assignment behavior by -[`test_defined_operators.py`](../../../tests/wrapper/fortran/naming/test_defined_operators.py), -and scalar inheritance dispatch by -[`test_inheritance.py`](../../../tests/wrapper/fortran/derived_types/test_inheritance.py). - -For `TypeError`, compare the argument dtype, rank, and class with generated -overloads. For generation-time ambiguity, rename or redesign the native call -shapes; declaration reordering is not a fix. +- Continue with [Wrapping Derived Types](wrapping-derived-types.md) +- See [Error Handling](error-handling.md) for dispatch errors +- For current generic and operator support, refer to the [Language Feature Matrix](../language-support/feature-matrix.md). diff --git a/docs/user/guide/index.md b/docs/user/guide/index.md index 961dffa1a..5ee7a9169 100644 --- a/docs/user/guide/index.md +++ b/docs/user/guide/index.md @@ -1,5 +1,6 @@ --- title: User Guide +description: Detailed guides for wrapping Fortran code with x2py audience: users prerequisites: getting started related: data-types.md, fortran-wrapper.md, ../language-support/index.md @@ -9,52 +10,62 @@ publication: reviewed # User Guide -The user guide continues from the completed -[Getting Started](../getting-started/index.md) workflow. Start with the datatype -mapping, then follow the workflow group that matches the native API you are -wrapping. Each page states the current supported subset, Python API shape, -limitations, troubleshooting route, and runtime evidence. +This section builds on the [Getting Started](../getting-started/index.md) +workflow. It explains data type mapping, supported Fortran constructs, runtime +behavior, packaging, and distribution. + +--- ## Start Here -- [Data types](data-types.md): Fortran storage, semantic `.pyi` names, exact - NumPy dtypes, strings, arrays, and generated classes. -- [Wrapping functions](wrapping-functions.md) -- [Wrapping subroutines](wrapping-subroutines.md) -- [Wrapping modules](wrapping-modules.md) -- [Arrays](arrays.md) -- [Optional arguments](optional-arguments.md) -- [Generic interfaces](generic-interfaces.md) +- [Data Types](data-types.md) — Fortran types, semantic `.pyi` names, exact NumPy dtypes, strings, and arrays +- [Arrays](arrays.md) — Rank, shape, strides, contiguity, and layout rules +- [Wrapping Functions](wrapping-functions.md) +- [Wrapping Subroutines](wrapping-subroutines.md) +- [Wrapping Modules](wrapping-modules.md) +- [Optional Arguments](optional-arguments.md) +- [Generic Interfaces](generic-interfaces.md) +- [Wrapping Derived Types](wrapping-derived-types.md) + +--- -## Storage And Objects +## Storage and Objects - [Allocatables](allocatables.md) - [Pointers](pointers.md) -- [Wrapping derived types](wrapping-derived-types.md) -- [Memory management](memory-management.md) +- [Memory Management](memory-management.md) + +--- ## Runtime Behavior - [Callbacks](callbacks.md) - [Enumerations](enumerations.md) -- [Error handling](error-handling.md) +- [Error Handling](error-handling.md) + +--- -## Build And Deployment +## Build and Deployment - [Packaging](packaging.md) - [Distribution](distribution.md) -## Contract References +--- + +## Contract & Reference + +- **[Fortran Wrapper Guide](fortran-wrapper.md)** — Complete reference for the generated runtime surface +- [Editing Semantic `.pyi` Contracts](editing-semantic-pyi-contracts.md) — When and how to customize contracts +- [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) — Full annotation reference +- [Language Feature Matrix](../language-support/feature-matrix.md) — Current support status (supported / partial / unsupported) + +--- + +**Important Note** -- [Fortran wrapper guide](fortran-wrapper.md): complete contract and evidence - ledger for the generated runtime surface. -- [Editing semantic `.pyi` contracts](editing-semantic-pyi-contracts.md): - intentional changes to generated wrapper policy. -- [Semantic `.pyi` format](../reference/semantic-pyi-format.md): annotation and - metadata reference. -- [Language feature matrix](../language-support/feature-matrix.md): central - supported, partial, unsupported, and planned status. +The recommended workflow is **source-driven** (starting from `.f90` files). +Only move to editing semantic `.pyi` contracts after you understand the default generated behavior and are ready to manage native artifacts manually. + +--- -The workflow pages explain the normal source-driven wrapper. Edit a semantic -`.pyi` only after the generated behavior is understood and the native artifacts -needed by a `.pyi`-driven build are available. +Start with **[Data Types](data-types.md)**. diff --git a/docs/user/guide/memory-management.md b/docs/user/guide/memory-management.md index 412671cd6..c5c78a171 100644 --- a/docs/user/guide/memory-management.md +++ b/docs/user/guide/memory-management.md @@ -1,5 +1,6 @@ --- title: Memory Management +description: Clear ownership rules, views vs copies, lifetimes, and destruction responsibility in x2py audience: users, advanced users prerequisites: arrays, wrapping derived types related: allocatables.md, pointers.md, editing-semantic-pyi-contracts.md @@ -9,121 +10,259 @@ publication: reviewed # Memory Management -Ownership determines whether Python sees a value, copy, live view, or generated -native object; whether mutation reaches native storage; and which runtime is -responsible for destruction. x2py completes these decisions before wrapper -generation. Bridge and binding code consume the completed policy and do not -guess from datatype or intent. +x2py lets Python work directly with native Fortran storage when that is safe. +That gives you fast wrappers without surprise copies, but it also means +ownership has to be explicit. -## Ownership Vocabulary +The guiding question is: -| Owner or transfer | Meaning | First complete example | -| --- | --- | --- | -| Python-owned value or copy | Python or NumPy releases detached storage after references are gone. | an ordinary array function result or `.copy()` of an extracted view | -| Caller-owned storage | The Python caller retains the exact object supplied to the call. | [`outputs.f90` output array](wrapping-subroutines.md#complete-output-example) | -| Wrapper-owned instance | A generated Python extension object owns one native derived instance. | [`points.f90` result](wrapping-derived-types.md#complete-derived-type-example) | -| Native-owned storage | Native module state or another native owner controls allocation and release. | [`allocations.f90` module handle](allocatables.md#complete-allocatable-example) | -| Borrowed view or child | Python refers to storage owned by a module or containing wrapper. | [`points.f90` nested child](wrapping-derived-types.md#complete-derived-type-example) | -| Detached copy (`snapshot_copy` policy) | Python receives copied current native state where an explicit value-copy policy requires it. Native-array-handle `to_numpy()` and derived module-object reads do not select this behavior. | Explicit copy-result contracts | -| Call-local association | Native code may refer to Python storage only during one wrapped call. | [`pointers.f90` input](pointers.md#complete-pointer-example) | - -Those linked pages contain the full source, build commands, and asserted -results. The examples are not repeated here so ownership differences remain -attached to one canonical source listing. - -## Core Invariants - -1. Exactly one owner destroys each owned native allocation. -2. Python-owned copies are independent of later native mutation. -3. Caller-owned arrays are never freed by x2py. -4. A borrowed child or component view retains its generated wrapper owner. -5. Owner retention does not protect a view from explicit native reallocation or deallocation. -6. A pointer declaration never proves ownership of its target. -7. Missing owner, lifetime, release, shape, dtype, mutability, nullability, or aliasing facts block generation. -8. Addressability is an object-origin fact: generated constructors allocate - pointer-backed instances, while pre-existing derived module objects need - either proved `Aliased` addressability or typed module-specific bridge - operations. A backend must not fabricate an address. -9. Native array descriptor state lives in `Allocatable[T[...]]` and - `Pointer[T[...]]` handles; borrowed NumPy views and detached NumPy copies are - explicit extraction results from `to_numpy()`. - -## Destruction Responsibilities - -| Value | Release responsibility | +> Who is allowed to release this storage, and how long is the Python object allowed to use it? + +x2py answers that question **before** wrapper code is generated. The generated +Python extension then follows the completed policy exactly, so ownership is a +documented contract rather than a runtime guess. + +--- + +## The Short Version + +Most memory-management questions in x2py reduce to three cases: + +| Situation | What to expect | | --- | --- | -| scalar, string, copy-return array, scalar pointer copied value | Python, NumPy, or its generated base capsule | -| caller-supplied NumPy array | Python caller | -| wrapper-owned derived instance | generated wrapper deallocator and native finalization | -| borrowed nested component | containing wrapper owner | -| allocatable or pointer array handle | containing wrapper, native module, or explicit x2py owner storage | -| borrowed view extracted from a handle | the handle's completed owner policy | -| call-local temporary | generated bridge before return | -| pointer target | explicit proved owner, never the pointer declaration alone | +| Python receives a normal value or NumPy array result | Python owns an independent value. | +| Python passes an object into a wrapped call | The same object remains with the caller; native code may mutate through it only when the contract allows it. | +| Python receives a handle, wrapper object, component, or view into native state | The Python object may be live, borrowed, or wrapper-owned; check the ownership kind before keeping views across native calls. | + +!!! tip "A reliable habit" + Before keeping a view or passing an object back into native code, ask two + separate questions: who owns the Python object, and who owns the storage + behind it? + +--- + +## Two Ownership Layers + +Ownership can describe two different things: + +| Layer | Question | Example | +| --- | --- | --- | +| **Boundary object** | Who supplied and keeps the Python object used in this call? | A NumPy array, handle, or wrapper instance passed as an argument | +| **Target storage** | Who owns the native allocation that object exposes or points at? | Python array data, Fortran module storage, or a wrapper-owned native instance | + +Those layers can have different owners. A caller-owned handle can point at a +native-owned module allocation. A caller-owned child wrapper can borrow storage +owned by its parent wrapper. There is still exactly one owner for each real +allocation. + +--- + +## Ownership Terms + +| Term | Plain meaning | Typical example | +| --- | --- | --- | +| **Python-owned** | Python owns an independent value or array. | Function array result, `view.copy()` | +| **Caller-owned** | Your Python code supplied the boundary object and keeps it after the call. | Writable NumPy array, native-backed handle, wrapper instance | +| **Wrapper-owned** | A generated x2py Python object owns native storage. | Derived-type result, owned allocatable result handle | +| **Native-owned** | Fortran module state or another native owner controls release. | Module variable, module allocatable | +| **Borrowed view** | Python sees storage owned by someone else. | `handle.to_numpy()`, nested component | +| **Call-local** | x2py creates temporary storage for one native call only. | Scalar address slot, fixed string buffer | + +### Python-owned + +Python-owned values are ordinary Python or NumPy objects with independent +lifetime. If native storage changes later, a Python-owned copy does not change. -Users do not call a generated `destroy()` method for ordinary wrapper-owned -objects. Explicit native allocation and deallocation routines remain normal -wrapped calls, but using one can invalidate previously borrowed storage. +```python +view = handle.to_numpy() +copy = None if view is None else view.copy() +``` + +Use this when the data must survive native reallocation, deallocation, or +reassociation. + +### Caller-owned + +"Caller" means your Python code: the code that calls the wrapped function. This +label says x2py must preserve the Python object you supplied. It does not always +say who owns every allocation reachable through that object. + +For an ordinary NumPy array, the boundary object and the target storage are both +Python-owned: + +```python +values = np.ones(4, dtype=np.float64) +api.scale(values) + +# Same array object, possibly mutated in place. +assert values[0] == 2.0 +``` + +The wrapper may mutate through a caller-owned object when the contract allows +it, but it must not free, reallocate, or secretly replace that object. -## Copies Versus Views +For a native-backed handle or borrowed wrapper, the caller owns the Python +object it passes, but native code or a parent wrapper may still own the target +storage behind it: -Use a copy when Python needs an independent lifetime: +```python +handle = api.values # Python holds the handle object. +view = handle.to_numpy() # Native module storage may be behind the view. +api.update_values(handle) # The call keeps using the same handle object. +``` + +!!! note "Ownership is not transferred" + Passing an object to native code does not transfer the owner of that object + or its target storage. Mutation is allowed only when the completed contract + says that specific object can be written through. + +### Wrapper-owned + +A generated Python class is an extension class produced by x2py and imported +from the generated module. You construct and pass it like a normal Python +object, but internally it controls one native instance. + +For Fortran users, wrapper-owned is mostly visible with supported +`type :: ...` derived types: ```python -independent = borrowed_view.copy() +point = geometry.points.point(x=np.float64(1.0), y=np.float64(2.0)) +made = geometry.points.make_point(np.float64(3.0), np.float64(4.0)) ``` -This operation is ordinary NumPy behavior applied after obtaining the view from -the complete `allocations.f90` example. It is the safe boundary before a native -operation that may reallocate or deallocate the authoritative storage. - -Do not use `del view` as a native deallocation mechanism. Releasing a borrowed -Python object only releases the view and any owner-retaining Python reference; -it does not transfer native release responsibility. - -## Mutability And Replacement - -- Ordinary caller-owned arrays **mutate in place**; -- Python strings use **replacement** because `str` is immutable; -- Allocatable array descriptors use **handles** because native allocation identity may change; -- Ordinary non-descriptor array/function results use **copy-return**; -- Allocatable array results use wrapper-owned handles whose finalizer releases - x2py-owned descriptor storage; -- Pointer-array handle results stop wrapper planning until owner storage, target - lifetime, descriptor extraction, and destroy behavior are implemented; -- plain and `Aliased` derived module variables remain live native-owned objects - through module-specific or address-backed mechanisms respectively; and -- Borrowed views extracted from handles **share native storage** until native - invalidation. - -Native-array-handle extraction remains live-view-or-`None`; callers use -`.copy()` on an extracted NumPy view for independent array storage. - -Return projection and ownership are one contract. An edited `.pyi` cannot ask -for copy-return without a projected replacement, or combine immutable storage -with a writable borrowed view. - -## Policy Source Of Truth - -Generated source facts enter semantic IR, then post-IR policy completion chooses -object kind, ownership, transfer, destruction, mutability, nullability, output -projection, release responsibility, storage mode, getter behavior, native -setter assignment, and Python setter exposure. Unsupported or contradictory -combinations stop before wrapper lowering. - -Advanced users can inspect or edit explicit `Ownership(...)`, `Transfer(...)`, -and `Destruction(...)` metadata. Editing Semantic `.pyi` Contracts and the -semantic format reference explain the editable forms later. Metadata can select -an implemented policy; it cannot invent a backend path. - -## Evidence And Troubleshooting - -The same array concept under native-owned, wrapper-owned, and Python-owned -lifetimes is exercised by -[`test_ownership_contracts.py`](../../../tests/wrapper/fortran/edit_pyi_contracts/test_ownership_contracts.py). -Exactly-once wrapper finalization is exercised by -[`test_borrowed_finalizers.py`](../../../tests/wrapper/fortran/derived_types/test_borrowed_finalizers.py). - -Treat use-after-deallocation risk as an application lifetime bug, not a signal -to guess ownership. Copy before native reallocation. Runtime Issues later -covers reproducible lifetime or cleanup symptoms. +Here `point` and `made` are Python objects whose generated x2py deallocator is +responsible for finalizing and releasing their native instances exactly once. + +Owned allocatable array results are another wrapper-owned case: their generated +handle owns persistent descriptor storage and releases it on `close()` or +finalization. Module allocatable handles are different because the Fortran +module still owns their target allocation. + +Ordinary NumPy arrays, module variables, and borrowed components are not +wrapper-owned. + +Nested derived-type components are different: `container.origin` may be a +borrowed child wrapper. It gives Python a convenient object for the component, +but the parent object still owns the native storage. + +### Native-owned + +Native-owned storage belongs to Fortran module state or another native owner. +Python can read it, write it through supported setters, or view it through a +handle, but Python is not responsible for releasing it. + +In short, plain and `Aliased` derived module variables remain live native-owned objects. +An `Aliased` module object may use an address-backed borrow; a plain module +object uses generated module-specific access. In both cases, the owning module +state decides when the native storage is valid. + +### Borrowed views + +A borrowed view is a Python object that points at storage owned somewhere else. +The view itself may keep the parent wrapper or module object alive when x2py can +do so, but it cannot stop native code from reallocating or deallocating the +target storage. + +```python +view = api.values.to_numpy() +snapshot = None if view is None else view.copy() + +api.resize_values() + +# view may now be stale; snapshot remains independent Python-owned data. +``` + +### Call-local + +Call-local storage is temporary workspace created only for one wrapped call. +x2py may create a native scalar slot, a fixed-width string buffer, or a +descriptor adapter, pass it to native code, and release it before returning to +Python. + +If the contract says native changes should be returned to Python, policy +completion must also say how that value is projected back. Otherwise, mutation +of call-local storage is intentionally not a persistent Python-visible update. + +--- + +## Handles And Target Storage + +`Allocatable[T[...]]` and `Pointer[T[...]]` handles are control objects. They +tell Python whether native storage is allocated or associated, and they provide +`.to_numpy()` when a live view is available. + +Owning a handle is not the same thing as owning the target array. A module +allocatable handle can be a stable Python object while the Fortran module still +owns the allocation behind it. A pointer handle can describe an association +without owning the target at all. + +!!! warning "Views are live, not snapshots" + `handle.to_numpy()` returns a view of current native storage. It never + creates an automatic detached snapshot. If native code may reallocate or + deallocate that storage, copy the view first. + +--- + +## Core Rules + +1. **Exactly one owner** is responsible for destroying each allocation. +2. Passing an object to native code does not automatically transfer ownership of + the object or its target storage. +3. **Borrowed views** can become stale if the owner reallocates or deallocates. +4. **Copies** are safe but more expensive. +5. Pointers **do not** imply ownership of their target. +6. A view from `.to_numpy()` is live: changes affect the native storage. + +--- + +## Views vs Copies + +```python +view = handle.to_numpy() # borrowed live view +copy = view.copy() # independent Python-owned copy +``` + +Use a **view** when performance matters and you know the owner will keep the +storage valid. Use a **copy** when the data must survive later native +reallocation, deallocation, or reassociation. + +| Need | Use | +| --- | --- | +| Fast access to current native storage | `view = handle.to_numpy()` | +| Data that survives native changes | `copy = view.copy()` | +| In-place mutation of Python input | Caller-owned boundary object; target owner depends on that object | +| Native object lifetime managed by Python wrapper | Wrapper-owned generated class | + +--- + +## Common Situations + +| Situation | Ownership shape | +| --- | --- | +| Ordinary NumPy array passed to a writable argument | Caller-owned boundary object with Python-owned array storage. | +| Native-backed handle passed into a call | Caller-owned boundary object; target storage can remain native-owned. | +| Function returning an ordinary array | Usually a Python-owned NumPy array. | +| Derived-type constructor or derived-type function result | Wrapper-owned generated class instance. | +| Nested derived-type component | Borrowed child wrapper retained through the parent. | +| Module variable | Native-owned state exposed through a getter, setter, handle, or proxy. | +| `Allocatable[T[...]]` handle | Control object for allocation state and live views; target ownership depends on origin. | +| `Pointer[T[...]]` handle | Control object for association state; the pointer does not own the target. | +| Call-local adapter | Internal temporary storage used only during one wrapped call. | + +--- + +## Practical Guidelines + +- Always call `.copy()` on a view before a native operation that might reallocate storage. +- Do not use `del` as a native deallocation API. +- Check `.allocated` / `.associated` before using handles. +- Review the generated `.pyi` contract to understand ownership. +- When a value crosses a boundary, identify both the boundary object owner and + the target storage owner. + +--- + +## Next + +- Use the [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) for policy metadata details. +- Review [Callbacks](callbacks.md) and [Error Handling](error-handling.md) when ownership affects callable lifetimes or cleanup after failures. diff --git a/docs/user/guide/optional-arguments.md b/docs/user/guide/optional-arguments.md index c20ea45ec..3d468214f 100644 --- a/docs/user/guide/optional-arguments.md +++ b/docs/user/guide/optional-arguments.md @@ -1,5 +1,6 @@ --- title: Optional Arguments +description: How x2py handles Fortran `optional` arguments — inputs, outputs, arrays, and None behavior audience: users prerequisites: wrapping subroutines, data types related: generic-interfaces.md, arrays.md, error-handling.md @@ -9,12 +10,11 @@ publication: reviewed # Optional Arguments -Supported optional scalars, arrays, strings, derived types, outputs, and inout -arguments preserve native `present(...)` behavior. The generated Python -signature places required parameters before optional parameters without -changing native argument positions. +x2py supports optional scalars, arrays, strings, derived types, and outputs while preserving native `present(...)` semantics. -## Complete Optional Example +--- + +## Complete Example Create `optional.f90`: @@ -22,6 +22,7 @@ Create `optional.f90`: module adjustments implicit none contains + integer(4) function adjust(value, offset) result(output) integer(4), intent(in) :: value integer(4), intent(in), optional :: offset @@ -29,19 +30,8 @@ contains output = value if (present(offset)) output = output + offset end function adjust -end module adjustments -``` -Inspecting `optional.f90` prints this optional-input contract: - -```python -from x2py.contracts import Addr, Arg, Int32, native_call - -@native_call([Addr(Arg(0)), Addr(Arg(1))]) -def adjust( - value: Int32, - offset: Int32 = ... -) -> Int32: ... +end module adjustments ``` Build it: @@ -50,76 +40,63 @@ Build it: python3 -m x2py optional.f90 --out-dir build/optional ``` -Omission and explicit `None` both make `offset` absent: +--- + +## Usage in Python ```python import sys - import numpy as np sys.path.insert(0, "build/optional") import optional api = optional.adjustments -assert api.adjust(np.int32(5)) == np.int32(5) -assert api.adjust(np.int32(5), None) == np.int32(5) -assert api.adjust(np.int32(5), offset=np.int32(3)) == np.int32(8) -``` -## Omission And `None` +assert api.adjust(np.int32(5)) == np.int32(5) # omitted +assert api.adjust(np.int32(5), None) == np.int32(5) # explicit None +assert api.adjust(np.int32(5), np.int32(3)) == np.int32(8) # provided +assert api.adjust(np.int32(5), offset=np.int32(10)) == np.int32(15) # keyword +``` -For a Python-visible optional input, omission and explicit `None` both mean the -native actual argument is absent. The `adjust` calls above show omission, -explicit `None`, and a concrete keyword value. +--- -A concrete value means the native argument is present. Use keywords when skipping -an earlier optional argument; do not depend on native declaration order after -required and optional Python parameters have been normalized. +## Key Rules -## Optional Arrays And Objects +- For ordinary optional inputs, **omission** and `None` both mean the argument + is **not present** to Fortran. +- Providing a concrete value makes the argument **present**. +- Use **keyword arguments** when skipping earlier optional parameters. +- Optional arrays and derived types also accept `None` to indicate absence. +- Optional `intent(out)` / `intent(inout)` arguments remain visible in Python so you can control `present(...)`. -An optional array still requires exact dtype, rank, shape, layout, alignment, -and writeability when supplied. `None` means no native argument; it does not -mean a zero-sized array. An optional derived-type argument accepts `None` or an -instance of the required generated class. +Scalar allocatable and pointer descriptors are the three-state exception: +omission means absent, `None` means present but unallocated or unassociated, +and a concrete value means present storage. -## Optional Native Outputs +--- -Optional `intent(out)` and `intent(inout)` dummies stay Python-visible so the -caller controls native `present(...)`: +## Optional Outputs -- a supplied optional scalar output uses mutable rank-zero storage such as - `Int32[()]`, mutates that storage, and returns it when projected; -- a supplied optional output array is mutated and returned as documented; -- an absent optional output contributes `None` to its result position; -- an optional inout argument mutates normally when supplied and does nothing - when absent; and -- a hidden `Return(...)` output is not caller-optional because the wrapper - requests it with generated temporary storage on every call. +When an output argument is optional: -Always review the generated return annotation when optional outputs are mixed -with required outputs. +- Supplying writable storage uses the normal mutation and return behavior. +- Passing `None` or omitting it makes the native dummy absent. +- A result tuple uses `None` at that output position when other results are + still returned; a routine with only that absent output returns `None`. -## Defaults +Always check the generated `.pyi` contract to see the exact return shape when mixing required and optional outputs. -The generated Python default is normally `None`, meaning native absence. x2py -does not invent a native default value from a Python literal unless the semantic -contract explicitly defines that behavior. A native procedure remains -responsible for its own `present(...)` branch. +--- -## Unsupported Combinations +## Limitations -Optional passed procedures, procedure pointers, and combinations without a -complete native presence and ownership contract make wrapper planning fail. x2py -does not convert an unsupported optional form into an always-present argument -or silently drop it. +- Optional procedure pointers and passed procedures are not yet supported. +- x2py does not invent default values — the Fortran procedure is responsible for handling missing arguments. -## Evidence And Troubleshooting +--- -Optional scalar, array, string, derived, output, and inout behavior is exercised -by -[`test_optional_arguments.py`](../../../tests/wrapper/fortran/function_calls/test_optional_arguments.py). +## Next -Use [Wrapping Subroutines](wrapping-subroutines.md) for result projection and -the later Error Handling page when an unsupported optional combination stops at -wrapper planning. +- Continue with [Generic Interfaces](generic-interfaces.md). +- For optional outputs and memory, see [Error Handling](error-handling.md) and [Memory Management](memory-management.md). diff --git a/docs/user/guide/packaging.md b/docs/user/guide/packaging.md index f52bfccc3..d952998d3 100644 --- a/docs/user/guide/packaging.md +++ b/docs/user/guide/packaging.md @@ -1,5 +1,6 @@ --- title: Packaging +description: How to package and distribute x2py-based extensions in your projects audience: users, packagers prerequisites: common beginner workflow related: distribution.md, ../reference/cli-commands.md, ../tutorials/packaging.md @@ -9,34 +10,35 @@ publication: reviewed # Packaging -x2py currently produces an importable native extension and its build artifacts; -it does not provide a stable Python wheel backend or project template. The -supported packaging workflow is therefore local project integration: keep the -native source and Python tests under version control, rebuild into an explicit -directory, and treat generated native artifacts as replaceable build output. - -## Complete Local Project Example - -Reuse `scale.f90`, whose complete source is first shown in the -[homepage example](../../index.md#try-x2py). Place that file in this -simple project: - -```text -scale-project/ - src/ - scale.f90 - build/ - python/ - check_scale.py +x2py currently focuses on **building importable native extensions**. It does not yet provide a full wheel-building backend or project template. The recommended approach is a clean local project workflow. + +--- + +## Recommended Project Layout + ``` +my-project/ +├── src/ # Fortran source +│ └── scale.f90 +├── build/ # Generated (do not commit) +├── tests/ # Python tests +│ └── test_scale.py +└── pyproject.toml # (optional) +``` + +--- -Build from the project root: +## Basic Workflow + +1. **Build the extension** ```bash python3 -m x2py src/scale.f90 --out-dir build/scale ``` -Put the following result check in `python/check_scale.py`: +2. **Test it** + +Create `tests/test_scale.py`: ```python import sys @@ -45,95 +47,57 @@ import numpy as np sys.path.insert(0, "build/scale") import scale -assert scale.scale(np.float64(3.0), np.float64(2.5)) == np.float64(7.5) +def test_scale(): + result = scale.scale(np.float64(3.0), np.float64(2.5)) + assert result == 7.5 + +if __name__ == "__main__": + test_scale() + print("✅ All tests passed") ``` -Run it from the project root: +Run with: ```bash -python3 python/check_scale.py +python3 -m pytest tests/ -q ``` -No output means the assertion passed. - -## Generated Package Shape - -The extension module name normally comes from the first source filename. -Contained native modules become child Python modules; standalone procedures -remain at the extension root. `--out NAME` selects a different extension name. - -Use the selected output directory as the import location during development. -The shared-library filename is platform-specific, so avoid hard-coding a suffix -outside project-specific build scripts. - -## Generated Artifacts - -An output directory can contain native object and module files, generated -wrapper sources, header-only native binding support, build metadata, and the importable extension. -These files are build products. Do not edit them as the source of the public -API; change the native source or an intentional semantic `.pyi` contract. - -The extension is tied to its Python implementation, NumPy ABI, platform, -architecture, compiler ABI, and linked native dependencies. Merely copying it -into another project is not a portable packaging guarantee. +--- -## Editable Makefile +## Rebuilding -Generate a Makefile when a local build needs inspectable commands or controlled -flags: +When you change source code, compiler flags, or the contract: ```bash -python3 -m x2py generate --makefile src/scale.f90 \ - --out-dir build/scale - -make -f build/scale/Makefile.x2py X2PY_FFLAGS=-O3 X2PY_CFLAGS=-O3 +rm -rf build/scale # Clean previous build +python3 -m x2py src/scale.f90 --out-dir build/scale ``` -`generate --makefile` selects the editable wrapper-build mode directly. Makefile mode -and verbose direct compilation are separate modes. The generated -Makefile expects GNU Make and a POSIX-style shell. Semantic `.pyi` Makefile -builds also write `x2py-build.json`, which can regenerate or replay the build. +--- -## Rebuild Policy +## Makefile Mode (Advanced) -Rebuild when source, source order, compiler, flags, Python, NumPy, native -dependencies, or the semantic contract changes. For a contract-changing build, -remove the selected output directory first so stale objects and modules cannot -mask the new build: +For inspectable builds and custom flags: ```bash -rm -rf build/scale -python3 -m x2py src/scale.f90 --out-dir build/scale -``` - -Keep sources, explicit contracts, build commands, and Python assertions under -version control. Keep `build/` out of version control unless a release process -deliberately captures platform-specific artifacts. +python3 -m x2py generate --makefile src/scale.f90 --out-dir build/scale -## Import Paths +make -f build/scale/Makefile.x2py +``` -During local development, add the build directory to `sys.path`, set -`PYTHONPATH`, or run Python from a location where the extension is importable. -x2py does not currently install the extension into a project package or manage -editable Python installs automatically. +--- -## Limitations +## Important Notes -- No stable wheel-building backend or generated `pyproject.toml` integration. -- No automatic repair or bundling of external native shared libraries. -- No cross-platform artifact promise. -- No automatic native dependency discovery or source reordering. -- No guarantee that a copied extension imports under another Python or NumPy ABI. +- The extension name is usually taken from the first source file (you can override with `--out`). +- Generated artifacts in `build/` are **not** portable across Python versions, NumPy ABIs, or platforms. +- Keep `src/`, tests, and build commands under version control. +- Do **not** commit the `build/` directory (except in special release processes). -## Evidence And Troubleshooting +--- -Output names, directories, native build plans, verbose mode, and Makefile option -validation are exercised by -[`test_build_modes.py`](../../../tests/wrapper/fortran/build_from_source/test_build_modes.py). -Multi-source package shape is exercised by -[`test_multi_source_builds.py`](../../../tests/wrapper/fortran/multiple_files/test_multi_source_builds.py). +## Next -For compile or link failures, rerun with `--verbose` and inspect the emitted -native commands; Build Issues expands that diagnosis later. Distribution later -explains the requirements for sharing an artifact with another machine or -environment. +- See [Distribution](distribution.md) for sharing built extensions +- Check the [Packaging Tutorial](../tutorials/packaging.md) for more advanced setups +- For build or linking problems, see [Troubleshooting](../troubleshooting/index.md) and rerun with `--verbose`. diff --git a/docs/user/guide/pointers.md b/docs/user/guide/pointers.md index 5096f81b4..4bbe91a81 100644 --- a/docs/user/guide/pointers.md +++ b/docs/user/guide/pointers.md @@ -1,5 +1,6 @@ --- title: Pointers +description: How x2py handles Fortran `pointer` variables and descriptors audience: advanced users prerequisites: arrays, memory management related: allocatables.md, memory-management.md, ../reference/semantic-pyi-format.md @@ -9,140 +10,38 @@ publication: reviewed # Pointers -A Fortran pointer does not identify the target owner. Scalar pointers cross -procedure boundaries as ordinary nullable Python values plus bridge descriptor -metadata. Pointer arrays use `Pointer[T[...]]`, which is a Python handle to -native pointer association state, not a NumPy array. +Fortran pointers are handled differently from allocatables. An allocatable +describes owned allocation state; a pointer describes an association to storage +whose owner may be elsewhere. -## Array Handles +- **Scalar pointers** appear as `T | None` values. +- **Array pointers** appear as `Pointer[T[...]]` handles. -`Pointer[T[...]]` is the active pointer-array spelling in semantic `.pyi` -contracts: - -```python -from x2py.contracts import Float64, Int32, Pointer - -values: Pointer[Float64[:]] - -def reassociate(values: Pointer[Float64[:]], target: Pointer[Float64[:]]) -> None: ... -def scale(values: Float64[:], factor: Int32) -> None: ... -``` - -The handle owns association state. An unassociated descriptor is still a -present handle: `p.associated is False`, `p.shape is None`, and -`p.to_numpy() is None`. `| None` means the handle object itself may be absent -for an optional native dummy, making native `present(values)` false: - -```python -def maybe_use(values: Pointer[Float64[:]] | None = ...) -> None: ... -``` - -That spelling is valid only for optional callable arguments. Do not use -`Pointer[T[...]] | None` for module variables, derived-type fields, or function -results; those surfaces return a present handle, and unassociated state is -represented inside that handle. - -Passing a handle to `Pointer[T[...]]` passes the native pointer descriptor. -Passing an associated handle to a normal `T[...]` parameter uses ordinary -Fortran array-actual semantics by handing off the handle's native array data -facet. It is not an implicit call to `.to_numpy()`. A normal `T[...]` parameter -rejects an unassociated pointer handle because there is no valid array actual -to pass; an associated zero-length target remains valid. The pointer/shape -handoff accepts only targets proved contiguous. A noncontiguous target is -rejected until descriptor-backed stride handoff is selected; x2py never treats -such a target as contiguous. - -Plain NumPy arrays are accepted by normal `T[...]` array parameters. They are -rejected for `Pointer[T[...]]` descriptor parameters because a NumPy array does -not carry a native pointer descriptor. - -`p.to_numpy()` is the explicit extraction operation. It returns `None` when the -handle is unassociated. When descriptor extraction is supported, it returns the -current target view and may expose strided targets. It never creates an -automatic detached snapshot or copy. If no supported live-view mechanism can -expose the current target, policy completion or wrapper planning fails explicitly; -x2py does not guess compiler-specific descriptor layout or fall back to a copy. - -Any NumPy view returned by `p.to_numpy()` is tied to the pointer target at the -time of extraction. After native code nullifies, reassociates, deallocates, or -otherwise changes that target, discard older views and call `p.to_numpy()` -again. Accessing a stale view is unsupported and may crash. Users who need -independent storage must call `.copy()` before the target-changing operation. -Each fresh extraction inspects the current descriptor and starts at the -target's current native lower bounds rather than assuming a fixed Fortran lower -bound. - -`p.nullify()` is the default pointer descriptor operation. `allocate(shape)`, -`deallocate()`, and `resize(shape)` are exposed only when completed pointer -policy explicitly allows those operations. A pointer handle does not imply -target ownership. - -## Scalar Pointer Projections - -Supported scalar pointer dummies use ordinary nullable Python values. The -semantic `.pyi` uses `Pointer(...)` inside `@native_call` to construct or read -the native pointer descriptor without exposing a Python pointer handle. - -For example: - -```fortran -real(8), target :: target_scale - -subroutine update_pointer(scale) - real(8), pointer, intent(inout) :: scale - - if (associated(scale)) then - scale = scale + 1.0_8 - else - scale => target_scale - end if -end subroutine update_pointer - -function maybe_pointer(enabled) result(scale) - integer(4), intent(in) :: enabled - real(8), pointer :: scale +--- - nullify(scale) - if (enabled /= 0) scale => target_scale -end function maybe_pointer -``` +## Pointer Array Handles -The corresponding semantic contract keeps scalar pointer values nullable and -uses `Pointer(...)` only for native descriptor projection: +Use `Pointer[T[...]]` for pointer arrays in the semantic contract: ```python -from x2py.contracts import Addr, Arg, Float64, Int32, Pointer, Return, Returns, native_call +from x2py.contracts import Pointer, Float64 -@native_call([Pointer(Arg(0))]) -def update_pointer( - scale: Float64 | None, -) -> Returns["scale", Float64] | None: ... - -@native_call([Addr(Arg(0))], result=Pointer(Return(0))) -def maybe_pointer(enabled: Int32) -> Float64 | None: ... +values: Pointer[Float64[:]] ``` -Passing `None` creates a present but unassociated call-local descriptor. -Omitting a defaulted scalar descriptor argument creates native optional absence, -so `present(scale)` is false. Passing a value creates a present associated -call-local descriptor. An unassociated function result or projected output -becomes `None`. Ordinary scalar projection rules remain unchanged: `intent(out)` -uses `Pointer(Return("name", j))`, and `intent(inout)` uses `Pointer(Arg(i))` -plus a matching `Returns["name", T] | None` readback. Scalar pointer values do -not expose a handle API. - -Use a default only when the native scalar dummy is optional: +### Key Properties -```python -@native_call([Pointer(Arg(0))]) -def update_pointer(scale: Float64 | None = ...) -> None: ... -``` +- A handle is always present, even when unassociated. +- `handle.associated` tells you the state. +- `handle.to_numpy()` returns a live view or `None`. +- The view is **borrowed** and can become stale if the target changes. +- Any NumPy view returned by `p.to_numpy()` is tied to the pointer target. +- Pointer-array handle results remain blocked until ownership and target + lifetime are explicit. -This scalar rule is separate from array pointer handles. Array arguments use -`Pointer[T[...]] | None` only for an optional absent handle; unassociated array -state stays inside a present handle. +--- -## Complete Pointer Example +## Complete Example Create `pointers.f90`: @@ -152,56 +51,21 @@ module pointers_api real(8), target :: storage(3) = [1.0_8, 2.0_8, 3.0_8] real(8), pointer :: values(:) => null() contains + subroutine associate_values() values => storage end subroutine associate_values - real(8) function sum_array(actual) result(total) - real(8), intent(in) :: actual(:) - total = sum(actual) - end function sum_array - - real(8) function sum_pointer(actual) result(total) - real(8), pointer, intent(in) :: actual(:) - - if (associated(actual)) then - total = sum(actual) + real(8) function sum_pointer(p) result(total) + real(8), pointer, intent(in) :: p(:) + if (associated(p)) then + total = sum(p) else total = -1.0_8 end if end function sum_pointer -end module pointers_api -``` - -The generated semantic contract distinguishes the module descriptor, an -ordinary array parameter, and a pointer-descriptor parameter: -```python -from x2py.contracts import ( - Aliased, - Annotated, - Destruction, - Float64, - Ownership, - Pointer, - PointerAssociation, - Transfer, -) - -storage: Annotated[Float64[3], Aliased] -values: Annotated[Pointer[Float64[:]], PointerAssociation("runtime")] - -def associate_values() -> None: ... -def sum_array(actual: Float64[::]) -> Float64: ... -def sum_pointer( - actual: Annotated[ - Pointer[Float64[:]], - PointerAssociation("runtime"), - Ownership("caller"), - Transfer("call_local"), - Destruction("none"), - ] -) -> Float64: ... +end module pointers_api ``` Build it: @@ -210,120 +74,52 @@ Build it: python3 -m x2py pointers.f90 --out-dir build/pointers ``` -Then verify descriptor state, descriptor passing, and normal array-actual -handoff from the same handle: +Use the generated module: ```python import sys - import numpy as np sys.path.insert(0, "build/pointers") import pointers api = pointers.pointers_api -handle = api.values +handle = api.values assert handle.associated is False -assert handle.shape is None assert api.sum_pointer(handle) == np.float64(-1.0) api.associate_values() assert handle.associated is True -assert handle.shape == (3,) assert api.sum_pointer(handle) == np.float64(6.0) -assert api.sum_array(handle) == np.float64(6.0) handle.nullify() assert handle.associated is False ``` -The ordinary `sum_array` call uses the handle's valid contiguous array actual; -it does not call `to_numpy()`. The descriptor-typed `sum_pointer` call requires -the pointer handle and can observe unassociated state. Add explicit pointer -policy when public NumPy extraction or ownership-changing operations are -required. - -## Call Compatibility - -A normal `T[...]` array parameter may accept a plain NumPy array or an -associated `Pointer[T[...]]` handle. The NumPy path passes caller-owned array -storage. The handle path validates pointer association, dtype, rank, shape, -layout, and mutability, then passes the handle's native array actual to the -normal native array dummy. The two paths share validation policy but remain -separate implementation methods. - -If the user writes `api.scale(p.to_numpy())`, that is an explicit ndarray path. -The returned value from `to_numpy()` follows ordinary ndarray validation, -including rejection of `None` and read-only arrays when writable native storage -is required. - -## Pointer Results - -An associated pointer scalar result becomes a copied Python value. An -unassociated scalar result becomes `None`. - -Pointer-array handle results remain blocked until x2py has stable owner storage, -target lifetime, descriptor extraction, and generated destroy behavior for the -returned handle. The wrapper does not silently fall back to a detached NumPy -copy for `Pointer[T[...]]` results. - -## Pointer Fields And Module Variables - -Pointer-backed array fields and module variables expose `Pointer[T[...]]` -handles. Their runtime Python class is `PointerArray`. A scalar pointer to a -derived object instead returns its generated live wrapper or `None`. -The containing object or module does not automatically own the pointer target. -Derived-field handles keep the parent wrapper alive for descriptor access, but -that retention is not target ownership. Plain `Pointer[T[...]]` has a default -conservative handle policy for association inspection and legal descriptor -operations. Generated field operations address the component through its parent -wrapper, and generated module operations address the native module variable. -Neither path invents target ownership or enables ownership-changing operations -that completed pointer policy did not allow. - -An associated scalar derived module pointer can be passed to an ordinary, -target, input-only pointer, or value dummy through its current target. For a -reassociable pointer dummy, x2py uses a typed local pointer holder and restores -the final association—associated, reassociated, allocated, disassociated, or -deallocated—to the module pointer exactly once. A wrapper-owned pointer result -uses the same persistent holder component directly. The holder owns its -association variable, not an unknown target, and its destructor never -deallocates native-owned target storage. Nullification or reassociation makes an -older payload proxy stale, and later field access raises `ReferenceError`. - -The later Wrapping Derived Types guide gives the canonical “Scalar Actuals And -Native Dummies” matrix, including the `INTENT(IN)` exception for nonpointer -actuals, empty-state behavior, module transactions, and multi-argument cleanup. - -## Unsupported Forms - -- pointer array `intent(out)` and `intent(inout)` reassociation without a - completed descriptor policy; -- pointer `allocate()`, `deallocate()`, or `resize()` without explicit policy; -- unknown target owners or release responsibility; -- persistent associations to Python storage after return; and -- stale-view invalidation after target reassociation, nullification, or - deallocation; and -- scalar-derived pointer targets whose lifetime or release responsibility is - neither native nor tied to a retained known owner. - -Semantic `.pyi` metadata can record these policy facts, but metadata does not -implement a missing runtime path. - -## Evidence And Troubleshooting - -Scalar pointer inputs, outputs, inout readback, nullable results, array pointer -handles, descriptor views, normal array-actual handoff, and dtype rejection are exercised by -[`test_pointers.py`](../../../tests/wrapper/fortran/derived_types/test_pointers.py). -Scalar derived module-pointer state, reassociation writeback, wrapper pointer -holders, stale proxy rejection, and multi-argument cleanup are exercised by -[`test_scalar_derived_actual_dummy_matrix.py`](../../../tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py). -The scalar `out` and `inout` parity cases are exercised by -[`test_allocatable_views.py`](../../../tests/wrapper/fortran/module_state/test_allocatable_views.py). - -If wrapper planning rejects a pointer, do not replace the diagnostic with guessed -ownership metadata. Detached pointer result behavior is expressible only when shape, -nullability, target owner, lifetime, and release facts are complete. Memory -Management and the semantic `.pyi` ownership reference expand those decisions -later. +--- + +## Important Rules + +- Use `Pointer[T[...]]` when a callable argument needs a pointer descriptor. +- Use normal `T[...]` when passing the **target data**. +- `to_numpy()` gives a live view. Copy explicitly if you need independent storage. +- Discard old views after nullification, reassociation, or target deallocation. +- `handle.nullify()` is available by default. Other operations (`allocate`, `deallocate`, etc.) require explicit policy. + +--- + +## Scalar Pointers + +Scalar pointers are handled as `T | None` values at the Python boundary. The +wrapper manages the call-local native pointer descriptor. An unassociated +projected result becomes `None`. + +--- + +## Next + +- Continue with **[Memory Management](memory-management.md)** after allocatables and pointers. +- See the [Language Feature Matrix](../language-support/feature-matrix.md) for current pointer support status + +--- diff --git a/docs/user/guide/wrapping-derived-types.md b/docs/user/guide/wrapping-derived-types.md index 830540be3..aded3b3a8 100644 --- a/docs/user/guide/wrapping-derived-types.md +++ b/docs/user/guide/wrapping-derived-types.md @@ -1,5 +1,6 @@ --- title: Wrapping Derived Types +description: How x2py wraps Fortran derived types as Python classes with methods, fields, constructors, and ownership rules audience: users, advanced users prerequisites: wrapping modules, data types related: memory-management.md, generic-interfaces.md, fortran-wrapper.md @@ -9,17 +10,22 @@ publication: reviewed # Wrapping Derived Types -A supported Fortran-derived type becomes a generated Python extension class. -The wrapper owns an opaque native instance; Python field access and methods use -generated native operations rather than assuming a public memory layout. +A supported Fortran `type` becomes a **generated Python extension class**. +Constructor calls and ordinary function results own an opaque native instance. +Nested components and module-owned objects use borrowed or native-owned +instances of the same generated class. Field access and method calls go through +generated native operations; Python never depends on the native memory layout. -## Complete Derived-Type Example +--- + +## Complete Example Create `points.f90`: ```fortran module points implicit none + type :: point real(8) :: x = 0.0_8 real(8) :: y = 0.0_8 @@ -28,7 +34,9 @@ module points type :: holder type(point) :: origin end type holder + contains + subroutine move(item, dx, dy) type(point), intent(inout) :: item real(8), intent(in) :: dx, dy @@ -48,6 +56,7 @@ contains type(point), intent(in) :: item container%origin = item end subroutine set_origin + end module points ``` @@ -57,7 +66,9 @@ Build it: python3 -m x2py points.f90 --out geometry --out-dir build/geometry ``` -Then construct, mutate, return, and borrow generated objects: +--- + +## Usage in Python ```python import sys @@ -66,251 +77,66 @@ import numpy as np sys.path.insert(0, "build/geometry") import geometry -points = geometry.points -item = points.point(x=np.float64(1.0), y=np.float64(2.0)) -points.move(item, np.float64(3.0), np.float64(4.0)) +p = geometry.points + +# Create new object +item = p.point(x=np.float64(1.0), y=np.float64(2.0)) + +# Call method (inout mutation) +p.move(item, np.float64(3.0), np.float64(4.0)) assert item.x == np.float64(4.0) assert item.y == np.float64(6.0) -made = points.make_point(np.float64(8.0), np.float64(9.0)) -assert isinstance(made, points.point) +# Function returning derived type +made = p.make_point(np.float64(8.0), np.float64(9.0)) -container = points.holder() -points.set_origin(container, made) -origin = container.origin -origin.x = np.float64(12.0) +# Nested component +container = p.holder() +p.set_origin(container, made) +container.origin.x = np.float64(12.0) assert container.origin.x == np.float64(12.0) ``` -## Arguments And Results - -- `intent(in)` passes an existing wrapper instance without transferring ownership. -- `intent(inout)` mutates the same native instance. -- hidden `intent(out)` returns a new wrapper-owned instance. -- a function result is copied into a new wrapper-owned native instance before - the native temporary expires. +--- -When an edited semantic contract keeps a writable derived argument visible and -projects it with `Returns["name", T]`, the return value is the exact same Python -wrapper that the caller supplied. Native code mutates that wrapper's existing -storage; the binding does not construct a replacement object, copy the derived -value, or assume destruction responsibility. Omitting the projection leaves the -same mutation in place and returns only the other declared results (or `None`). +## Key Concepts -The complete example shows inout mutation and a wrapper-owned function result. +- **Ownership**: Wrapper-owned objects are finalized when the Python object is garbage-collected. +- **Mutation**: `intent(inout)` modifies the same native instance. +- **Fields**: Public scalar numeric/logical/complex fields become Python attributes. +- **Nested types**: Appear as borrowed child wrappers (they don’t own the memory). +- **Constructors**: Generated for public scalar fields (keyword-only). -A native by-value argument is preserved in generated semantic contracts as -`@native_call([Value(Arg(0)), ...])`. Python still passes an existing `point` -wrapper. -The generated Fortran bridge imports the exact native type and performs the -typed by-value call. The foreign boundary never lays out or byte-copies the -aggregate, -so the same opaque mechanism applies to exact ordinary, `sequence`, and -`bind(C)` types. Polymorphic or unresolved types remain blocked. +--- ## Scalar Actuals And Native Dummies -This section is the canonical compatibility reference for rank-zero, -monomorphic derived objects. It applies when a wrapper object—including a live -module attribute—is passed to another wrapped Fortran procedure. Arrays and -polymorphic objects follow different rules. - -An actual object has one of five relevant Fortran declaration forms: - -| Key | Actual declaration | -| --- | --- | -| `O` | `type(item) :: value` | -| `T` | `type(item), target :: value` | -| `A` | `type(item), allocatable :: value` | -| `AT` | `type(item), allocatable, target :: value` | -| `P` | `type(item), pointer :: value` | - -A native dummy has six forms: - -| Key | Dummy declaration | -| --- | --- | -| `O` | `type(item) :: arg` | -| `T` | `type(item), target :: arg` | -| `A` | `type(item), allocatable :: arg` | -| `AT` | `type(item), allocatable, target :: arg` | -| `P` | `type(item), pointer :: arg` | -| `V` | `type(item), value :: arg` | - -`OPTIONAL`, `INTENT`, rank, and the qualified native type are additional facts; -they are not extra declaration rows. In the table, “payload” requires an -allocated allocatable or associated pointer. “Holder” means persistent -wrapper-owned Fortran storage. “Scoped” means the originating module exposes an -address only for the duration of the synchronous native call. “Allocation -transaction” and “pointer transaction” write descriptor or association changes -back to the originating module variable before control returns to Python. - -| Actual and origin | `O` dummy | `T` dummy | `A` dummy | `AT` dummy | `P` dummy | `V` dummy | -| --- | --- | --- | --- | --- | --- | --- | -| `O`, wrapper-owned | direct reference | call-scoped target | incompatible | incompatible | input-only pointer adapter | typed value | -| `O`, module | scoped reference | scoped target | incompatible | incompatible | scoped input-only pointer adapter | scoped typed value | -| `T`, wrapper-owned | direct reference | direct target | incompatible | incompatible | input-only pointer adapter | typed value | -| `T`, module | module address | module target | incompatible | incompatible | input-only pointer adapter | typed value | -| `A`, wrapper holder | payload | payload as holder target | allocatable holder | allocatable holder | payload input-only pointer adapter | payload typed value | -| `A`, module | scoped payload | scoped payload target | allocation transaction | allocation transaction with call target | scoped payload input-only pointer adapter | scoped payload typed value | -| `AT`, wrapper holder | payload | payload as holder target | allocatable holder | allocatable holder | payload input-only pointer adapter | payload typed value | -| `AT`, module | module payload address | module payload target | target-preserving allocation transaction | target-preserving allocation transaction | payload input-only pointer adapter | payload typed value | -| `P`, wrapper holder | pointee | pointee target | incompatible | incompatible | pointer holder | pointee typed value | -| `P`, module | module pointee | module pointee target | incompatible | incompatible | pointer transaction | pointee typed value | - -“Incompatible” is a deliberate `TypeError` before native entry, not an -unimplemented Phase 8 fallback. A nonpointer actual can satisfy `P` only when -the pointer dummy is proved `INTENT(IN)`. A pointer dummy with no `INTENT`, or -with `INTENT(OUT)`/`INTENT(INOUT)`, may change association and therefore -requires a pointer actual. If the edited contract omits `INTENT` but an imported -Fortran interface is authoritative, x2py emits the target adapter and lets the -Fortran compiler enforce this rule. Without either source of authority, wrapper -generation reports an interface error. - -For payload calls, an unallocated `A`/`AT` actual or disassociated `P` actual -raises `ValueError` before native entry. Descriptor dummies `A`, `AT`, and `P` -instead accept empty state so the native procedure can allocate, deallocate, -nullify, or reassociate it. Empty state is still a present argument; only an -omitted optional argument or explicit optional `None` means absence. - -### Module Transactions And Multiple Arguments - -Module allocation and pointer state stays in Fortran. x2py uses one shared -typed allocatable holder and pointer holder per qualified native type. The -interoperable boundary carries only an opaque holder address and typed operation -pointers; no native descriptor crosses that boundary. - -For an allocatable module actual passed to `A` or `AT`, a module operation uses -`move_alloc` to place its allocation in a bridge-local typed holder. The native -procedure receives that holder component, and a cleanup operation moves the -final allocation back exactly once. For a module pointer passed to `P`, a local -pointer holder starts with the current association; cleanup restores its final -association to the module pointer exactly once. A module target needs neither -transaction: its durable native address is sufficient. - -A procedure may take any number of scalar-derived arguments. x2py validates all -slots first, acquires module origins in deterministic order, nests scoped -address producers, invokes the native procedure once, and restores transactions -in reverse order. It does not generate `2**N` call variants. Repeated read-only -use of the same object shares one acquisition; ambiguous writable aliasing is -rejected before any module state moves. - -This also applies when arguments are module variables from different Fortran -modules and have different qualified derived types. Each module variable owns a -separate bridge operation table and scoped callback; the binding validates the -table's qualified native type before the callback is invoked. There is no -shared type-specific callback slot, so one module variable cannot overwrite -another argument's transport. - -If a later acquisition or the native call reports a normal ABI error, cleanup -continues for every acquired origin and Python raises only after the Fortran -frames have returned. Concurrent or recursive use of the same active module -transaction raises `RuntimeError`. A restoration failure also raises -`RuntimeError` and poisons that module proxy rather than pretending its state is -usable. Process termination, `error stop`, signals, and invalid native pointers -cannot be converted into recoverable Python exceptions. - -A pointer holder owns its association variable, not its target. Native storage -remains native-owned unless completed policy identifies and retains a known -module, parent, or wrapper target. Destroying the Python holder nullifies its -component and releases only the holder; it never deallocates an unknown target. - -## Fields And Nested Components - -Public supported scalar fields become Python descriptors. Private fields are -omitted. A nested scalar derived component is a borrowed child wrapper: it -retains its parent owner and never destroys the component independently. -The same readable and, when policy permits, writable descriptor surface is used -for wrapper-owned instances, borrowed objects, and live module objects. A -target/addressable module object may use a direct native address; a plain module -object uses typed member getter and setter operations instead. Neither path -creates a detached whole-object copy. - -Allocatable fields expose `Allocatable[T[...]]` handles, and pointer-array -fields expose `Pointer[T[...]]` handles. Each field handle retains the parent -wrapper for descriptor access. Call `to_numpy()` to extract the current NumPy -view or `None`; extraction never copies. Discard old views after native -deallocation, reallocation, nullification, or reassociation because accessing a -stale view is unsupported and may crash. Call `.copy()` explicitly for -independent NumPy storage. Pointer fields use a conservative default operation -policy, while ownership-changing operations require explicit pointer policy. -Arrays of derived types remain blocked because element construction, -destruction, layout, aliasing, and copy policy are incomplete. -Rank-zero allocatable or pointer components whose value is itself a derived -object use the same completed holder and ownership policy when their origin is -supported. Rank-zero allocatable and pointer module variables expose persistent -live descriptor proxies, including while the allocatable is unallocated or the -pointer is disassociated. Payload-field access then raises `ReferenceError`, -but the same proxy can still be passed to an `A`, `AT`, or `P` dummy so native -code can establish new state. Wrapper-owned allocatable and pointer results use -persistent typed holders with the same empty-state rule. -Their complete call compatibility and transaction rules are defined in -[Scalar Actuals And Native Dummies](#scalar-actuals-and-native-dummies). x2py -does not silently turn an unsupported origin into an owned object or detached -copy. - -## Constructors - -Native allocation runs native default component initialization. x2py generates -a keyword-only Python initializer for public rank-zero numeric, logical, and -complex fields. Omitted keywords preserve the native initialized values. - -Private components, arrays, allocatables, pointers, strings, and nested derived -components are not automatic constructor keywords. A type with fields but no -eligible keywords still receives explicit default construction when supported. - -An edited semantic `.pyi` may remove the generated constructor or bind one -concrete initializer. x2py does not regenerate a constructor that the edited -contract intentionally removed. - -## Finalizers - -An owned wrapper invokes native finalization exactly once when its owning Python -wrapper is collected. Failed initialization still releases the allocated native -instance. Borrowed child wrappers do not finalize their component; finalization -belongs to the containing owner. - -A native finalizer has no recoverable Python status channel during object -deallocation. Native termination from a finalizer terminates the process. - -## Inheritance And Polymorphism - -Supported extension types form a matching Python inheritance hierarchy. A -scalar polymorphic input over a known hierarchy dispatches descendant-first. - -Ordinary native `class(T)` arguments retain `Annotated[T, Polymorphic]` in the -semantic `.pyi` because that source fact selects the accepted dynamic-type -dispatch. The passed-object dummy of a type-bound procedure is different: its -class binding already proves that it is polymorphic, so generated contracts use -the plain declared type for that one argument and restore the fact when loading -the binding. - -Polymorphic results, mutable polymorphic arguments, arrays, allocatable or pointer -polymorphic scalars, `class(*)`, abstract instantiation, and deferred bindings -are blocked. - -## Opaque Layout - -Generated wrappers do not expose a direct binary-layout promise for ordinary -derived types. Component order and native facts remain in semantic IR, but -Python access follows generated accessors. Do not use `ctypes` offsets or assume -that Python-visible fields imply a stable binary layout. - -## Evidence And Troubleshooting - -Scalar boundaries and nested lifetime are exercised by -[`test_derived_type_boundaries.py`](../../../tests/wrapper/fortran/derived_types/test_derived_type_boundaries.py), -direct live object and field behavior by -[`test_phase8_derived_plan.py`](../../../tests/wrapper/fortran/derived_types/test_phase8_derived_plan.py), -the complete scalar actual/dummy matrix and multi-argument transactions by -[`test_scalar_derived_actual_dummy_matrix.py`](../../../tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py), -methods by -[`test_derived_type_methods.py`](../../../tests/wrapper/fortran/derived_types/test_derived_type_methods.py), -constructors/finalizers by -[`test_constructors_and_finalizers.py`](../../../tests/wrapper/fortran/derived_types/test_constructors_and_finalizers.py), -and borrowed finalization by -[`test_borrowed_finalizers.py`](../../../tests/wrapper/fortran/derived_types/test_borrowed_finalizers.py). - -Treat nested child wrappers as borrowed from their containing wrapper rather -than independently owned native objects. Memory Management later expands -ownership, and Error Handling later covers constructor, type, and wrapper-planning -failures. +The generated class can represent several native origins. Compatibility depends +on both the object passed from Python and the native dummy declaration: + +| Python object origin | Ordinary or `target` dummy | `allocatable` dummy | `pointer` dummy | `value` dummy | +| --- | --- | --- | --- | --- | +| Wrapper-owned ordinary object | Direct object reference | Incompatible | Input-only pointer adapter | Typed value copy | +| Native module object | Scoped live reference | Incompatible | Scoped input-only pointer adapter | Scoped typed value | +| Wrapper-owned allocatable holder | Current payload | Persistent holder | Payload input-only adapter | Payload value copy | +| Module allocatable | Scoped payload | Allocation transaction | Scoped payload input-only adapter | Scoped payload copy | +| Wrapper-owned pointer holder | Current target | Incompatible | Persistent pointer holder | Target value copy | +| Module pointer | Module target | Incompatible | Association transaction | Target value copy | + +An unallocated allocatable or unassociated pointer cannot supply a payload to an +ordinary, `target`, or `value` dummy. Descriptor dummies accept empty state so +native code can establish it. A nonpointer actual can satisfy a pointer dummy +only when that dummy is known to be `intent(in)`. + +Module allocation and association transactions are restored before the wrapped +call returns. Pointer holders own their association variable, not an unknown +target. + +--- + +## Next + +- Learn about [Memory Management](memory-management.md) before keeping borrowed objects or views. +- See [Allocatables](allocatables.md) and [Pointers](pointers.md) for advanced storage + +--- diff --git a/docs/user/guide/wrapping-functions.md b/docs/user/guide/wrapping-functions.md index 99993ece7..f82b3425b 100644 --- a/docs/user/guide/wrapping-functions.md +++ b/docs/user/guide/wrapping-functions.md @@ -1,5 +1,6 @@ --- title: Wrapping Functions +description: How x2py wraps Fortran `function` procedures — return values, output arguments, arrays, and contracts audience: users prerequisites: data types, first wrapped function related: wrapping-subroutines.md, arrays.md, fortran-wrapper.md @@ -9,22 +10,20 @@ publication: reviewed # Wrapping Functions -A Fortran `function` becomes a Python callable. The direct result of the function becomes the first returned value in Python. -All arguments follow the exact semantic types shown in the generated `.pyi` file. +A Fortran `function` becomes a Python callable. The function’s return value becomes the first item in Python, followed by any `intent(out)` or `intent(inout)` arguments (if present). -See [Data Types](data-types.md) for details on how Fortran types are mapped to Python/NumPy. +--- + +## Basic Scalar Function -For this example, we'll use `scale.f90` from the -[homepage example](../../index.md#try-x2py). +Using the `scale.f90` example: ```bash python3 -m x2py generate --pyi scale.f90 python3 -m x2py scale.f90 --out-dir build/scale ``` -## Scalar Functions - -The generated contract for the scale function is: +**Generated contract:** ```python from x2py.contracts import Addr, Arg, Float64, external, native_call @@ -33,68 +32,56 @@ from x2py.contracts import Addr, Arg, Float64, external, native_call @native_call([Addr(Arg(0)), Addr(Arg(1))]) def scale( value: Float64, - factor: Float64, + factor: Float64 ) -> Float64: ... ``` -Call it like this: +**Python call:** ```python +import sys +import numpy as np + +sys.path.insert(0, "build/scale") +import scale + result = scale.scale(np.float64(3.0), np.float64(2.5)) -assert result == np.float64(7.5) +assert result == 7.5 ``` -Contained module functions appear on their generated child module instead of -the extension root. Standalone procedures carry `@external` in the semantic -contract. These placement details do not change the Python argument types. - -Supported scalar function results include resolved signed integer, real, -complex, logical, scalar character, and supported derived-type values. Read -[Data Types](data-types.md) for the complete mapping. Scalar character results -are Python-owned `str` values; derived results are wrapper-owned generated -class instances. +--- -## Array Results +## Array Return Values -Functions can return numeric arrays. These are returned as new NumPy arrays with Fortran (column-major) ordering. +Functions can return arrays. These are returned as new NumPy arrays (Fortran-ordered by default). -Example (`function_results.f90`): +**Example** (`function_results.f90`): ```fortran module results implicit none contains + function squares(count) result(values) integer(4), intent(in) :: count real(8) :: values(count) - integer(4) :: index + integer(4) :: i - values = [(real(index, 8) * real(index, 8), index = 1, count)] + values = [(real(i, 8)**2, i = 1, count)] end function squares -end module results -``` -Generated contract: - -```python -from x2py.contracts import Addr, Arg, Float64, Int32, native_call - -@native_call([Addr(Arg(0))]) -def squares( - count: Int32 -) -> Float64[count]: ... +end module results ``` -Build it: +Build and import this example: ```bash python3 -m x2py function_results.f90 --out-dir build/function-results ``` -Usage: - ```python import sys + import numpy as np sys.path.insert(0, "build/function-results") @@ -102,107 +89,39 @@ import function_results api = function_results.results result = api.squares(np.int32(4)) - -np.testing.assert_array_equal( - result, - np.array([1.0, 4.0, 9.0, 16.0], dtype=np.float64), -) +np.testing.assert_array_equal(result, np.array([1.0, 4.0, 9.0, 16.0], dtype=np.float64)) ``` -The `squares` result is an ordinary array contract, so allocated zero-sized -results remain zero-sized NumPy arrays. Multidimensional results retain -Fortran-oriented element ordering, and returned ordinary arrays are independent -Python-owned copies. +--- -An allocatable array function result has a different public shape: it returns a -present `AllocatableArray`, including when the native result is unallocated. -Check `handle.allocated` and call `handle.to_numpy()` for explicit extraction. -Pointer-array function results remain blocked until x2py can prove stable owner -storage and target lifetime. +## Functions with Output Arguments -## Functions With Output Arguments +When a function has `intent(out)` or `intent(inout)` arguments, Python returns a **tuple**: -If a function also has output arguments, Python returns a tuple: **first the direct function result, then the output arguments** in their native -argument order. +> `(function_result, out_arg1, out_arg2, ...)` -Create `function_outputs.f90`: +**Example:** ```fortran -module outputs - implicit none -contains - function sum_with_count(values, count) result(total) - real(8), intent(in) :: values(:) - integer(4), intent(out) :: count - real(8) :: total - - total = sum(values) - count = size(values) - end function sum_with_count -end module outputs +function sum_with_count(values, count) result(total) + real(8), intent(in) :: values(:) + integer(4), intent(out) :: count + real(8) :: total + total = sum(values) + count = size(values) +end function ``` -Inspecting `function_outputs.f90` prints this function contract: +**Python call:** ```python -from x2py.contracts import Arg, Float64, Int32, Return, native_call - -@native_call([Arg(0), Return('count', 1)]) -def sum_with_count( - values: Float64[::] -) -> tuple[Float64, Int32]: ... -``` - -Build it: - -```bash -python3 -m x2py function_outputs.f90 --out-dir build/function-outputs +total, count = api.sum_with_count(data_array) ``` -Then assert the tuple order: - -```python -import sys - -import numpy as np - -sys.path.insert(0, "build/function-outputs") -import function_outputs +--- -api = function_outputs.outputs -source = np.array([4.0, -2.0, 7.0], dtype=np.float64) -total, count = api.sum_with_count(source) -assert total == np.float64(9.0) -assert count == np.int32(3) -``` +## Important Rules -Caller-provided output arrays remain arguments because the caller must allocate -their storage. Their return projection, when present, refers to that same -object. The same `intent(out)` and `intent(inout)` projection rules apply to -subroutines that have no direct function result. - -## Call Limits - -- Exact input dtype is required where the generated contract names one; x2py - does not silently narrow or widen a native scalar or array. -- Numeric and fixed-width bytes character array results support ranks 1 through - 15. Arrays of derived types are blocked. -- Wider-than-supported real, complex, or explicit logical storage is blocked - rather than narrowed. -- A function result never creates an unproven borrowed pointer view. -- Native `stop`, `error stop`, or process abort cannot be converted into a - normal Python return. - -## Evidence And Troubleshooting - -Scalar calls are exercised by -[`test_verified_baseline.py`](../../../tests/wrapper/fortran/scalars/test_verified_baseline.py), -array results by -[`test_array_results.py`](../../../tests/wrapper/fortran/arrays/test_array_results.py), -and mixed result projection by -[`test_output_arguments.py`](../../../tests/wrapper/fortran/function_calls/test_output_arguments.py). - -For a rejected Python value, compare it with generated `.pyi` output and use -the reported dtype, rank, shape, and layout facts. Runtime Issues later provides -additional diagnosis. For an unsupported wrapper plan, the language feature matrix -later records whether that form is supported before compilation. +- Always pass **exact NumPy dtypes** (`np.float64`, `np.int32`, etc.). +- Array results are returned as new NumPy arrays (copies). +- `intent(out)` and `intent(inout)` values are handled by the generated contract. diff --git a/docs/user/guide/wrapping-modules.md b/docs/user/guide/wrapping-modules.md index 7fb5dd846..4bebe2b97 100644 --- a/docs/user/guide/wrapping-modules.md +++ b/docs/user/guide/wrapping-modules.md @@ -1,5 +1,6 @@ --- title: Wrapping Modules +description: How x2py exposes Fortran modules as Python namespaces with procedures, variables, and state audience: users prerequisites: data types, first wrapped module related: wrapping-functions.md, memory-management.md, packaging.md @@ -9,169 +10,76 @@ publication: reviewed # Wrapping Modules -A contained Fortran module becomes a child Python module inside the generated -extension. Standalone procedures stay at the extension root. x2py preserves -this namespace instead of flattening native module membership implicitly. +A Fortran `module` becomes a **child Python module** (namespace) inside the generated extension. This preserves the original structure instead of flattening everything to the extension root. -As seen in the introductory example, building the source file `module_state.f90` -creates an extension named module_state, allowing you to import its contained module: - -```python -import module_state - -module = module_state.module_state -``` - -See [First Wrapped Module](../getting-started/first-wrapped-module.md) for the -complete source, build command, generated contract, and checked calls. +--- -## Procedures And Package Shape +## Basic Usage -Module functions and subroutines are attributes of the child module: +After building `module_state.f90`: ```python -assert module.summarize() == np.int32(15) -``` - -When compiling multiple ordered source files, a single generated extension can contain multiple child modules. -Each native module retains its own child namespace, while standalone -procedures remain on the extension root. The first source determines the -default extension name unless `--out` selects another name. +import sys -## Public Variables - -Supported public scalar integer, real, complex, and logical module variables -are direct Python attributes. Reading fetches its current native state, and assigning -an exact matching value writes through to native storage: +sys.path.insert(0, "build/first-module") +import module_state -```python -module.counter = np.int32(9) -assert module.counter == np.int32(9) -assert module.summarize() == np.int32(21) +mod = module_state.module_state # ← child module ``` -Generated getter and setter bridge routines are internal and do not appear as -Python callables. Private variables are omitted. +See [First Wrapped Module](../getting-started/first-wrapped-module.md) for the full example. -## Constants And Saved State +--- -Representable native parameters become `Final[...]` constants in the generated -contract: +## Procedures -```python -from x2py.contracts import Final, Int32 +Module procedures become methods on the child module: -nmax: Final[Int32] = 12 +```python +print(mod.summarize()) # 15 +print(mod.scaled_counter()) # 4.5 ``` -No native setter exists for a parameter. Assigning `module.nmax` in Python can -only shadow the attribute on that Python module object; it does not mutate the -native parameter. - -Derived-type parameters follow the same constant rule. A copy-safe -`Final[DerivedType]` is materialized as a wrapper-owned value copy and has no -native setter; it is not mutable native module storage and does not require an -`Aliased` annotation. +Standalone procedures (not inside any module) remain at the extension root. -Public module variables already have module lifetime, whether or not `save` is -written explicitly. Procedure-local saved variables remain internal, but their -state persists across calls. Multiple imported Python module objects backed by -the same extension observe the same native module storage. +--- -## Module Arrays +## Public Variables & Constants -An allocatable module array is exposed as a persistent -`Allocatable[T[...]]` handle. Plain and `Aliased` declarations have the same -extraction behavior: a fresh `to_numpy()` call returns a live view of the -current allocation or `None`. `Aliased` preserves the native addressability -fact for other policy; it does not select view versus copy extraction. The -module attribute remains a handle even when native storage is unallocated: +Supported public scalar variables are exposed as direct attributes: ```python -module.allocate_values(np.int32(3)) -handle = module.values -assert handle.allocated is True -view = handle.to_numpy() -view[0] = np.float64(5.0) -``` +mod.counter = np.int32(9) +assert mod.counter == np.int32(9) -Mutation through either kind of view reaches native module storage. A later -native deallocation or reallocation may make old views stale; accessing stale -views is unsupported and may crash. Use `view.copy()` first when Python needs -an independent lifetime. The same handle object then reports the new allocation -state. - -Pointer-array module variables expose `Pointer[T[...]]` handles with a default -conservative operation policy. Association inspection and `nullify()` are -available by default. `to_numpy()` requires a completed extraction path, and -ownership-changing operations require explicit pointer policy. - -## Derived Module Objects +assert mod.nmax == np.int32(12) # parameters are read-only +``` -A derived-type module variable is not automatically addressable just because -the same type can be constructed from Python. Python construction asks x2py to -allocate a new pointer-backed native instance. A pre-existing module variable -has its own source attributes. Both plain and `Aliased` declarations are read -as live generated objects, but their bridge mechanisms differ: +- `parameter` declarations become `Final[...]` constants. +- Writing to a constant in Python only shadows the attribute locally — it does **not** change native storage. -```python -from x2py.contracts import Aliased, Allocatable, Annotated, Float64 +--- -class box: - values: Allocatable[Float64[:]] +## Module Arrays & State -current: Annotated[box, Aliased] -plain_current: box -``` +- Allocatable module arrays appear as `Allocatable[T[...]]` handles. +- You can read the current state with `.to_numpy()` and mutate through the view. +- `save` variables (including procedure-local `save`) persist across calls. +- Multiple Python imports of the same extension share the same native module state. -Reading either attribute returns a native-owned live object. The wrapper never -copies or destroys module storage. `current` may use its proved native address; -`plain_current` uses typed module-specific bridge operations instead of -fabricating an address. Supported component access such as `.values` returns an -`Allocatable[T[...]]` handle retaining the object; call `to_numpy()` to obtain -its current view. +--- -Whole object replacement through `module.current = other` is not exposed. -Mutate live native module state through its completed module-object policy or a -wrapped native procedure. Use `.copy()` on an extracted array view when -independent NumPy storage is required. +## Important Notes -## Common Blocks +- Private declarations are hidden. +- Common blocks are **not** exposed as Python variables (only through procedures that access them). +- Module state is **shared** — changes are visible across all references to the same extension. -Common-block storage is not exported as Python variables. Wrapped procedures -may still read and write common-block state, so the supported surface is the -native procedure API: +--- -```python -module.write_shared(np.int32(17)) -assert module.read_shared() == np.int32(17) -``` +## Next -x2py does not add locking around module state. The caller remains responsible -for synchronization across Python threads, OpenMP workers, or external native -code. - -## Limitations - -- Private module declarations remain hidden. -- Common-block variables have no generated attribute surface. -- Pointer state is exposed only when association, target lifetime, and a live - descriptor-view mechanism are complete; there is no detached-copy fallback. -- Plain and `Aliased` derived-type module variables both use the live generated - object surface. Plain objects require complete typed member operations; - `Aliased` objects may use a direct native address. Unsupported members block - generation instead of changing the public representation. -- Source ordering and external dependency discovery remain the caller's job. - -## Evidence And Troubleshooting - -Module variables, constants, saved state, visibility, and shared native state -are exercised by -[`test_module_state.py`](../../../tests/wrapper/fortran/module_state/test_module_state.py). -Common-block procedure behavior is exercised by -[`test_common_blocks.py`](../../../tests/wrapper/fortran/module_state/test_common_blocks.py). - -Module array views remain native-owned and can become stale after native -reallocation or deallocation; copy data that needs an independent lifetime. -Memory Management later expands that ownership rule, and Runtime Issues later -covers import, attribute, and shared-state problems. +- Learn about [Memory Management](memory-management.md) +- See [Optional Arguments](optional-arguments.md) +- Explore [Packaging](packaging.md) for distribution +- For supported module features, check the [Language Feature Matrix](../language-support/feature-matrix.md). diff --git a/docs/user/guide/wrapping-subroutines.md b/docs/user/guide/wrapping-subroutines.md index ddcf248ad..27097fc4e 100644 --- a/docs/user/guide/wrapping-subroutines.md +++ b/docs/user/guide/wrapping-subroutines.md @@ -1,5 +1,6 @@ --- title: Wrapping Subroutines +description: How x2py wraps Fortran `subroutine` procedures — output arguments, in-place mutation, and result projection audience: users prerequisites: data types, first wrapped function related: wrapping-functions.md, arrays.md, optional-arguments.md @@ -9,35 +10,24 @@ publication: reviewed # Wrapping Subroutines -A subroutine has no direct native function result, but its output arguments may -become Python return values. The generated signature separates hidden values -from the storage that the Python caller must allocate. +A Fortran `subroutine` has no direct return value. Instead, its `intent(out)` and `intent(inout)` arguments are projected into the Python result. -## Argument Projection +--- -| Native role | Python call shape | Python result shape | -| --- | --- | --- | -| scalar `intent(in)` | visible exact-type argument | no result | -| scalar `intent(out)` | hidden | returned value | -| immutable scalar replacement | visible input when required | returned replacement | -| array `intent(in)` | visible NumPy array | no result | -| array `intent(out)` | visible writable NumPy array | same array when projected | -| array `intent(inout)` | visible writable NumPy array | mutated in place; not duplicated unless explicitly projected | -| allocatable array `intent(out)` | hidden unless optional | wrapper-owned `AllocatableArray` | -| allocatable array `intent(inout)` | visible `AllocatableArray` argument | same handle when projected; allocation state changes in place | -| supported derived `intent(out)` | hidden | new wrapper-owned instance | +## Argument Projection Rules -`optional, intent(out)` is the visibility exception for normally hidden -outputs. The argument remains visible so the caller can omit it and make native -`present(...)` false. Optional scalar outputs use mutable rank-zero storage such -as `Int32[()]`. An optional allocatable array argument uses -`Allocatable[T[...]] | None = ...`: omission means native absence, while a -present handle carries allocated or unallocated descriptor state. +| Native Argument | Python Call | Python Result | +|-----------------------------|------------------------------|-----------------------------------| +| `intent(in)` scalar/array | Visible argument | Not returned | +| `intent(out)` scalar | Hidden | Returned as value | +| `intent(inout)` scalar | Hidden (normal case) | Returned as new value | +| `intent(out)` array | Visible writable NumPy array | Same array, filled and returned | +| `intent(inout)` array | Visible writable NumPy array | Mutated in place; normally no extra result | +| `intent(out)` allocatable | Hidden (or optional) | `Allocatable[...]` handle | -The generated `.pyi` is authoritative when a procedure combines several of -these forms. +--- -## Complete Output Example +## Complete Example Create `outputs.f90`: @@ -45,10 +35,10 @@ Create `outputs.f90`: module outputs implicit none contains + subroutine bounds(values, smallest, largest) real(8), intent(in) :: values(:) real(8), intent(out) :: smallest, largest - smallest = minval(values) largest = maxval(values) end subroutine bounds @@ -56,7 +46,6 @@ contains subroutine scale_in_place(values, factor) real(8), intent(inout) :: values(:) real(8), intent(in) :: factor - values = factor * values end subroutine scale_in_place @@ -64,37 +53,19 @@ contains real(8), intent(out) :: values(:) values = 1.0_8 end subroutine fill -end module outputs -``` - -Inspecting `outputs.f90` prints these subroutine contracts: -```python -from x2py.contracts import Addr, Arg, Float64, Return, Returns, native_call - -@native_call([Arg(0), Return('smallest', 0), Return('largest', 1)]) -def bounds( - values: Float64[::] -) -> tuple[Float64, Float64]: ... - -@native_call([Arg(0), Addr(Arg(1))]) -def scale_in_place( - values: Float64[::], - factor: Float64 -) -> None: ... - -def fill( - values: Float64[::] -) -> Returns["values", Float64[::]]: ... +end module outputs ``` -Build the extension: +Build it: ```bash python3 -m x2py outputs.f90 --out-dir build/outputs ``` -Then assert scalar projection, in-place mutation, and output-array projection: +--- + +## Python Usage ```python import sys @@ -104,94 +75,39 @@ sys.path.insert(0, "build/outputs") import outputs api = outputs.outputs -source = np.array([4.0, -2.0, 7.0], dtype=np.float64) -smallest, largest = api.bounds(source) -assert smallest == np.float64(-2.0) -assert largest == np.float64(7.0) -mutable = np.array([1.0, 2.0, 3.0], dtype=np.float64) -assert api.scale_in_place(mutable, np.float64(3.0)) is None -np.testing.assert_array_equal(mutable, np.array([3.0, 6.0, 9.0], dtype=np.float64)) +# Hidden scalar outputs → returned as tuple +data = np.array([4.0, -2.0, 7.0], dtype=np.float64) +smallest, largest = api.bounds(data) +print(smallest, largest) # -2.0 7.0 + +# In-place mutation +arr = np.array([1.0, 2.0, 3.0], dtype=np.float64) +api.scale_in_place(arr, np.float64(3.0)) +print(arr) # [3. 6. 9.] +# Caller-provided output array target = np.empty(4, dtype=np.float64) returned = api.fill(target) assert returned is target -np.testing.assert_array_equal(target, np.ones(4, dtype=np.float64)) +print(target) # [1. 1. 1. 1.] ``` -## Hidden Scalar Outputs - -A non-allocatable scalar output does not require caller storage in the normal -source-generated API. The `bounds` call above returns `smallest` and `largest` -without corresponding Python arguments. - -Hidden outputs are returned in native argument order. A hidden scalar character -output becomes a new `str`, and a hidden scalar derived output becomes a new -wrapper-owned object. - -In `@native_call`, `Return(...)` always names hidden writable native output -storage. The wrapper passes that storage to the native procedure by address, -because an output argument cannot be written by value. Do not write -`Addr(Return(...))`; `Return(...)` already carries the output-storage contract. - -## Caller-Provided Arrays - -Array output and inout storage remains visible. Allocate it with the exact -dtype, shape, layout, alignment, and writeability required by the contract. The -`fill` call above returns the same `target` object after native mutation, while -`scale_in_place` mutates `mutable` in place and returns `None`. - -The initial contents of an `intent(out)` array are ignored by Fortran, but the array must still be pre-allocated on the Python side. -An `intent(inout)` array is read and written in place. x2py does not create a hidden replacement -for ordinary array storage merely because the supplied array is inconvenient; -an incompatible array is rejected before the native call. - -## Multiple Results - -For a subroutine, projected results follow the native output argument order. For a function, -the function result comes first, followed by output arguments in native argument -order. A caller-provided output can remain visible and also be named in return -metadata; hidden outputs use ordinary result annotations. - -Do not infer tuple order from Python assignment names. Review the generated -`.pyi` and its `Returns[...]` entries when several outputs are present. - -## Scalar Mutation - -Python numbers and strings are immutable. They cannot expose native in-place -mutation. Source-generated output scalars are returned as values, and supported -character `intent(inout)` uses replacement projection: the original `str` -remains unchanged and Python receives a new string. - -An edited semantic contract can deliberately require writable zero-dimensional -NumPy storage for a visible scalar output. That is an advanced native-order -contract, not the normal source-generated subroutine API. Editing Semantic -`.pyi` Contracts explains that advanced form later. - -## Limitations - -- Pointer scalar output and inout use nullable copied-value projection. Pointer - array descriptor arguments use `Pointer[T[...]]` handles; pointer results and - reassociation without completed policy remain blocked. -- Character arrays require fixed-width NumPy bytes dtype storage. Arrays of - derived types are blocked. -- Wrapper-owned allocatable scalar derived results may be passed to compatible - dummies with same-object allocation-state writeback. Module allocatable and - pointer scalar objects use reversible typed transactions for compatible - descriptor dummies. The later Wrapping Derived Types guide gives the complete - “Scalar Actuals And Native Dummies” matrix. -- Unsupported output combinations stop during wrapper planning; code generation does not - silently select another projection. - -## Evidence And Troubleshooting - -Output projection, tuple ordering, caller-provided arrays, allocatable outputs, -and character/derived outputs are exercised by -[`test_output_arguments.py`](../../../tests/wrapper/fortran/function_calls/test_output_arguments.py) -and -[`test_native_call_examples.py`](../../../tests/wrapper/fortran/function_calls/test_native_call_examples.py). - -For array validation failures, compare the value with the exact generated -dtype, rank, shape, layout, and writeability contract. Arrays, Memory -Management, and Error Handling expand validation, ownership, and projected -status exceptions later in the guide. +--- + +## Key Rules + +- Scalar `intent(out)` / `intent(inout)` values are **hidden** in the call and returned. +- Array `intent(out/inout)` arguments must be pre-allocated by the caller and + are mutated in place. +- Source-generated `intent(out)` arrays return the same supplied array; + `intent(inout)` arrays normally communicate through in-place mutation only. +- The generated `.pyi` contract is the source of truth for what is returned. +- For functions with both a return value **and** outputs, the function result comes first in the tuple. + +--- + +## Next + +- Continue with [Optional Arguments](optional-arguments.md) or [Wrapping Modules](wrapping-modules.md). +- For advanced memory management, see [Allocatables](allocatables.md) and [Pointers](pointers.md). diff --git a/docs/user/index.md b/docs/user/index.md index c57670ba3..e815cc055 100644 --- a/docs/user/index.md +++ b/docs/user/index.md @@ -2,28 +2,23 @@ title: User Documentation audience: users prerequisites: none -related: getting-started/index.md, guide/index.md, reference/index.md +related: getting-started/index.md, guide/index.md status: maintained publication: reviewed --- # User Documentation -This lane explains how to use x2py. Follow the learning material in navigation -order; instructional pages link back to established concepts and name later -topics without interrupting the current task. +Use these pages to install x2py, verify your environment, build your first +Fortran wrappers, and understand the supported behavior of generated Python +extensions. -## Learn And Build +## Start Here 1. [Getting Started](getting-started/index.md) 2. [User Guide](guide/index.md) -3. [Tutorials](tutorials/index.md) -4. [Examples](examples/index.md) -## Look Up Behavior - -- [Reference](reference/index.md) -- [Language Support](language-support/index.md) -- [FAQ](faq/index.md) -- [Troubleshooting](troubleshooting/index.md) -- [Changelog](changelog/index.md) +Getting Started covers installation, environment verification, the first +standalone wrapper, the first module wrapper, and the beginner edit-build-test +loop. The User Guide covers supported Fortran wrapper features, runtime +behavior, packaging, and distribution. diff --git a/docs/user/reference/callbacks.md b/docs/user/reference/callbacks.md index fe0e75b0d..a781c423c 100644 --- a/docs/user/reference/callbacks.md +++ b/docs/user/reference/callbacks.md @@ -13,6 +13,10 @@ Callback contracts are native-facing. A normal generated function signature describes how Python calls the wrapper; a `@prototype` declaration describes the native callback signature invoked through the generated adapter. +The wrapper call and the callback call are separate boundaries. `@native_call` +on a generated function controls how Python-visible arguments are lowered into +the native routine. `@prototype` controls how native callback dummy arguments +are received by the adapter and converted before invoking the Python callable. The callable argument list therefore preserves callback argument order, value/reference calling, rank, shape, character length, and result shape. It does not repeat native callback direction. @@ -40,28 +44,36 @@ callback invocation are unsupported. Declare a named prototype once and use its name as the callback argument type: ```python -from x2py.contracts import Float64, Int32, prototype +from x2py.contracts import Addr, Float64, Int32, prototype @prototype -def transform(count: Int32, values: Float64[count]) -> Float64[count]: ... +def transform(count: Addr(Int32), values: Float64[count]) -> Float64[count]: ... -def apply_transform(callback: transform, ...) -> ...: ... +def apply_transform(callback: transform) -> None: ... ``` -Prototype arguments use ordinary semantic types. Reference passing is the -default; `Value(T)` is the only callback argument ABI override. +Prototype arguments use ordinary semantic types. Primitive callback scalars +follow the same default as normal wrapper signatures: a bare primitive is a +native value, and `Addr(T)` marks a primitive reference dummy. Arrays, strings, +and derived-type references carry storage or wrapper objects without `Addr(T)`. | Spelling | Fortran callback dummy | Python callback object | | --- | --- | --- | -| `Int32` | scalar reference dummy | owned `np.int32` scalar value | -| `Value(Float64)` | scalar `value` dummy | owned `np.float64` scalar value | +| `Int32` | scalar `value` dummy | owned `np.int32` scalar value | +| `Addr(Float64)` | scalar reference dummy | owned `np.float64` scalar value | | `Float64[n]` | array reference dummy | writable NumPy array view | | `point_t` | derived reference dummy | generated wrapper object | | `Value(point_t)` | derived `value` dummy | wrapper over the call-local value copy | -`Addr(...)` is unnecessary inside callback signatures because reference is the -default. `Value(...)` is required only when the native callback dummy has the -Fortran `value` attribute. +Do not add `Value(T)` just to make Python receive a primitive scalar: bare +primitive callback arguments already arrive as owned NumPy scalar values. +`Value(T)` is reserved for supported non-primitive scalar value dummies. +`Addr(T)` inside `@prototype` marks a primitive reference dummy; it does not +make the Python callback receive an integer raw address. + +Optional callback dummies and allocatable, pointer, or polymorphic callback +dummies and results are unsupported. Their descriptor and presence semantics +cannot be represented by the current callback transfer plan. Prototypes are semantic declarations and never become Python runtime exports. A prototype defined in another contract module is referenced through a normal @@ -97,10 +109,10 @@ adding direction metadata to the annotation. ## Results And Copy-Back Scalar callback results are converted from the Python return value. Primitive -scalar arguments are always owned NumPy scalar values and never scalar-storage arrays, so -scalar reference writeback is unsupported. Array, derived, and -character-storage reference arguments are copied back before the Fortran -adapter returns to native code. +scalar arguments are always owned NumPy scalar values and never scalar-storage +arrays, so scalar reference writeback is unsupported. Array, derived, and +character-storage reference arguments are copied back before the Fortran adapter +returns to native code. Callback exceptions, invalid callback return conversion, and unsupported cross-thread callback execution are fatal at the callback boundary: x2py prints diff --git a/docs/user/reference/semantic-pyi-format.md b/docs/user/reference/semantic-pyi-format.md index ce5059d6e..06d9a240c 100644 --- a/docs/user/reference/semantic-pyi-format.md +++ b/docs/user/reference/semantic-pyi-format.md @@ -782,7 +782,8 @@ X2PY_C_DOCS_END --> | Text | `String` | | User types | class names and imported type names | | Named callable prototypes | `@prototype` function declarations referenced by name | -| Prototype value override | `Value(T)` inside a `@prototype` declaration only | +| Prototype primitive reference | `Addr(T)` inside a `@prototype` declaration | +| Prototype non-primitive value override | `Value(T)` inside a `@prototype` declaration | " C_DOCS_DISABLED = "") - fortran_block_index = quick_start.index("```fortran", source_index) + fortran_block_index = quick_start.index("```fortran") source_build_command_index = quick_start.index( "python3 -m x2py scale.f90", fortran_block_index, @@ -688,7 +748,7 @@ def test_readme_quick_start_shows_input_source_before_wrapper_build() -> None: verbose_output_index = quick_start.index("generated Python binding", verbose_c_flag_index) module_lesson_index = quick_start.index("first wrapped module", verbose_output_index) - assert help_index < source_index < fortran_block_index < source_build_command_index + assert help_index < fortran_block_index < source_build_command_index assert source_build_command_index < default_source_build_tree_index < named_source_build_command_index assert named_source_build_command_index < source_build_tree_index < explicit_source_build_command_index assert explicit_source_build_command_index < explicit_source_build_tree_index < pyi_generation_command_index @@ -848,12 +908,15 @@ def test_user_guide_page_is_completed_in_documentation_checklist(relative_path: @pytest.mark.parametrize( "relative_path", - [*REQUIRED_GETTING_STARTED_PAGES[1:], *REQUIRED_USER_GUIDE_PAGES[1:]], + [*REQUIRED_GETTING_STARTED_PAGES[1:], *REQUIRED_USER_GUIDE_PAGES[1:], *EXAMPLE_DOCUMENTATION_PAGES], ) -def test_sequential_user_page_does_not_link_forward(relative_path: str) -> None: +def test_sequential_user_pages_do_not_link_forward_from_instructional_prose(relative_path: str) -> None: path = DOCS_ROOT / relative_path _, body = _front_matter(path) + body = _instructional_body_without_next(body) positions = _site_navigation_positions() + if relative_path not in positions: + pytest.skip(f"{relative_path}: not active in site navigation") source_position = positions[relative_path] for target in MARKDOWN_LINK.findall(body): target_path = (path.parent / target).resolve() @@ -865,30 +928,80 @@ def test_sequential_user_page_does_not_link_forward(relative_path: str) -> None: assert positions[target_relative] <= source_position, f"{relative_path}: forward link to {target_relative}" +@pytest.mark.parametrize( + "relative_path", + [*REQUIRED_GETTING_STARTED_PAGES[1:], *REQUIRED_USER_GUIDE_PAGES[1:]], +) +def test_next_sections_use_linked_bullet_destinations(relative_path: str) -> None: + _, body = _front_matter(DOCS_ROOT / relative_path) + for line_number, item, is_bullet in _next_navigation_items(body): + assert is_bullet, f"{relative_path}:{line_number}: Next content must be a bullet item" + assert MARKDOWN_LINK.search(item), f"{relative_path}:{line_number}: Next item must include a Markdown link" + + @pytest.mark.parametrize("relative_path", REQUIRED_USER_GUIDE_PAGES[:-2]) def test_user_guide_commands_do_not_expose_fixture_paths(relative_path: str) -> None: page = (DOCS_ROOT / relative_path).read_text(encoding="utf-8") assert "python3 -m x2py tests/" not in page -def test_getting_started_overview_uses_standalone_example_and_current_evidence() -> None: +@pytest.mark.parametrize( + "relative_path", + [ + "index.md", + "user/index.md", + *REQUIRED_GETTING_STARTED_PAGES, + *REQUIRED_USER_GUIDE_PAGES, + ], +) +def test_reviewed_user_pages_do_not_expose_internal_evidence(relative_path: str) -> None: + page = _visible_documentation_source(DOCS_ROOT / relative_path) + assert "## Evidence" not in page + assert "## Runtime Evidence" not in page + assert "Runtime tests:" not in page + assert "../../../tests/" not in page + assert "../../tests/" not in page + assert "../tests/" not in page + + +@pytest.mark.parametrize( + "relative_path", + [ + "index.md", + "user/index.md", + *REQUIRED_GETTING_STARTED_PAGES, + *REQUIRED_USER_GUIDE_PAGES, + ], +) +def test_reviewed_user_pages_do_not_contain_editorial_notes(relative_path: str) -> None: + page = _visible_documentation_source(DOCS_ROOT / relative_path).casefold() + for phrase in ( + "i kept your", + "let me know if you want", + "original content had", + "restore/polish", + ): + assert 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") assert "scale.scale(np.float64(3.0), np.float64(2.5))" in overview - assert "build_from_source/test_build_modes.py" in overview def test_documentation_homepage_demonstrates_x2py_before_getting_started() -> None: page = (DOCS_ROOT / "index.md").read_text(encoding="utf-8") - introduction_index = page.index("x2py turns supported Fortran source into an importable Python extension") - source_index = page.index("") + introduction_index = page.index("x2py turns supported Fortran source") + try_heading_index = page.index("## Try it in 30 seconds {#try-x2py}") + source_index = page.index("```fortran", try_heading_index) build_index = page.index("python3 -m x2py scale.f90") call_index = page.index("result = scale.scale(np.float64(3.0), np.float64(2.5))") output_index = page.index("7.5", call_index) docstring_index = page.index("scale(value, factor) -> float64", output_index) - getting_started_index = page.index("[Getting Started](user/getting-started/index.md)") + getting_started_index = page.index("Getting Started](user/getting-started/index.md)") - assert introduction_index < source_index < build_index < call_index < output_index + assert introduction_index < try_heading_index < source_index < build_index < call_index < output_index assert output_index < docstring_index < getting_started_index assert "value : float64\nfactor : float64" in page assert "result : float64" in page @@ -938,8 +1051,8 @@ def test_documentation_links_to_documentation_stay_on_the_website() -> None: def test_first_wrapped_function_shows_contract_and_mentions_later_support_boundaries() -> None: page = (DOCS_ROOT / "user/getting-started/first-wrapped-function.md").read_text(encoding="utf-8") - source_index = page.index("[homepage example](../../index.md#try-x2py)") - build_index = page.index("python3 -m x2py scale.f90 \\") + source_index = page.index("scale.f90") + build_index = page.index("python3 -m x2py scale.f90") command_index = page.index("python3 -m x2py generate --pyi scale.f90") contract_index = page.index( "@external\n@native_call([Addr(Arg(0)), Addr(Arg(1))])\ndef scale(\n" @@ -947,32 +1060,29 @@ def test_first_wrapped_function_shows_contract_and_mentions_later_support_bounda ) assert source_index < build_index < command_index < contract_index - assert "" not in page assert "## Current Limitations" not in page - assert "language feature matrix later" in page def test_first_wrapped_module_shows_local_input_and_generated_contract() -> None: page = (DOCS_ROOT / "user/getting-started/first-wrapped-module.md").read_text(encoding="utf-8") - source_index = page.index("Create `module_state.f90` with this module:") - build_index = page.index("python3 -m x2py module_state.f90 \\") + source_index = page.index("module_state.f90") + build_index = page.index("python3 -m x2py module_state.f90") inspect_index = page.index("python3 -m x2py generate --pyi module_state.f90") - contract_index = page.index("nmax: Final[Int32] = 12") + contract_index = page.index("## Key Rules") assert source_index < build_index < inspect_index < contract_index assert "fmodule_vars_f90" not in page assert "## Current Limitations" not in page - assert "language feature matrix later collects support boundaries" in page def test_beginner_workflow_reuses_scale_example_without_renaming_it() -> None: page = (DOCS_ROOT / "user/getting-started/beginner-workflow.md").read_text(encoding="utf-8") - source_reference_index = page.index("[homepage example](../../index.md#try-x2py)") - layout_index = page.index("src/\n scale.f90") + source_reference_index = page.index("scale.f90") + layout_index = page.index("src/") contract_index = page.index("python3 -m x2py generate --pyi src/scale.f90") - build_index = page.index("python3 -m x2py src/scale.f90 \\\n --out-dir build/scale") + build_index = page.index("python3 -m x2py src/scale.f90") smoke_index = page.index("result = scale.scale(np.float64(3.0), np.float64(2.5))") - advanced_index = page.index("## Advanced Next Step: Edit The Semantic Contract") + advanced_index = page.index("Editing the Semantic Contract") assert source_reference_index < layout_index < contract_index < build_index < smoke_index < advanced_index assert "scale_api" not in page diff --git a/tests/parser/c/fixtures/general/name_reuse.json b/tests/parser/c/fixtures/general/name_reuse.json index 4813368b3..a9799ea22 100644 --- a/tests/parser/c/fixtures/general/name_reuse.json +++ b/tests/parser/c/fixtures/general/name_reuse.json @@ -131,14 +131,16 @@ { "name": "same_name", "type": { - "model": "CBool", + "model": "CTypedef", "qualifiers": [], - "source_text": "_Bool same_name" + "source_text": "bool same_name", + "name": "bool", + "type": null, + "source_location": null, + "declaration_locations": [] }, "declared_type": { - "model": "CBool", - "qualifiers": [], - "source_text": "_Bool same_name" + "reference": "bool" }, "source_location": null, "callback_policy": null @@ -220,13 +222,13 @@ "filename": "general/name_reuse.h", "line": 18, "column": 1, - "source_line": "void do_work_l(" + "source_line": "void do_work_l(bool same_name, struct same_name *shared);" }, "start": { "filename": "general/name_reuse.h", "line": 18, "column": 1, - "source_line": "void do_work_l(" + "source_line": "void do_work_l(bool same_name, struct same_name *shared);" }, "end": null, "declaration_locations": [] @@ -365,9 +367,13 @@ { "name": "convert_to_logical", "result_type": { - "model": "CBool", + "model": "CTypedef", "qualifiers": [], - "source_text": "_Bool" + "source_text": "bool", + "name": "bool", + "type": null, + "source_location": null, + "declaration_locations": [] }, "parameters": [ { @@ -423,13 +429,13 @@ "filename": "general/name_reuse.h", "line": 21, "column": 1, - "source_line": "_Bool " + "source_line": "bool convert_to_logical(const char *same_name);" }, "start": { "filename": "general/name_reuse.h", "line": 21, "column": 1, - "source_line": "_Bool " + "source_line": "bool convert_to_logical(const char *same_name);" }, "end": null, "declaration_locations": [] @@ -485,9 +491,13 @@ { "name": "same_name_l", "type": { - "model": "CBool", + "model": "CTypedef", "qualifiers": [], - "source_text": "extern _Bool same_name_l" + "source_text": "extern bool same_name_l", + "name": "bool", + "type": null, + "source_location": null, + "declaration_locations": [] }, "storage": [ "extern" @@ -498,7 +508,7 @@ "filename": "general/name_reuse.h", "line": 12, "column": 1, - "source_line": "extern " + "source_line": "extern bool same_name_l;" }, "callback_policy": null, "declaration_locations": [] @@ -693,14 +703,10 @@ { "name": "same_name", "type": { - "model": "CBool", - "qualifiers": [], - "source_text": "_Bool same_name" + "reference": "bool" }, "declared_type": { - "model": "CBool", - "qualifiers": [], - "source_text": "_Bool same_name" + "reference": "bool" }, "source_location": null, "callback_policy": null @@ -750,13 +756,13 @@ "filename": "general/name_reuse.h", "line": 18, "column": 1, - "source_line": "void do_work_l(" + "source_line": "void do_work_l(bool same_name, struct same_name *shared);" }, "start": { "filename": "general/name_reuse.h", "line": 18, "column": 1, - "source_line": "void do_work_l(" + "source_line": "void do_work_l(bool same_name, struct same_name *shared);" }, "end": null, "declaration_locations": [] @@ -895,9 +901,7 @@ "convert_to_logical": { "name": "convert_to_logical", "result_type": { - "model": "CBool", - "qualifiers": [], - "source_text": "_Bool" + "reference": "bool" }, "parameters": [ { @@ -953,13 +957,13 @@ "filename": "general/name_reuse.h", "line": 21, "column": 1, - "source_line": "_Bool " + "source_line": "bool convert_to_logical(const char *same_name);" }, "start": { "filename": "general/name_reuse.h", "line": 21, "column": 1, - "source_line": "_Bool " + "source_line": "bool convert_to_logical(const char *same_name);" }, "end": null, "declaration_locations": [] @@ -1015,9 +1019,7 @@ "same_name_l": { "name": "same_name_l", "type": { - "model": "CBool", - "qualifiers": [], - "source_text": "extern _Bool same_name_l" + "reference": "bool" }, "storage": [ "extern" @@ -1028,7 +1030,7 @@ "filename": "general/name_reuse.h", "line": 12, "column": 1, - "source_line": "extern " + "source_line": "extern bool same_name_l;" }, "callback_policy": null, "declaration_locations": [] diff --git a/tests/pipeline/preprocessing/test_cli.py b/tests/pipeline/preprocessing/test_cli.py index 575e07f0d..3ba1477cd 100644 --- a/tests/pipeline/preprocessing/test_cli.py +++ b/tests/pipeline/preprocessing/test_cli.py @@ -22,7 +22,7 @@ def test_cli_help_documents_exact_compiler_and_preprocessing_examples(): assert "Compiler used for preprocessing" in res.stdout assert "default: gfortran; cc with --language c" in " ".join(res.stdout.split()) assert "--compile-commands PATH" in res.stdout - assert "-D NAME[=VALUE]" in res.stdout + assert "-D, --define NAME[=VALUE]" in res.stdout def test_cli_c_default_compiler_mode_accepts_include_dirs(tmp_path: Path): diff --git a/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py b/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py index c09e04870..ff95aef3a 100644 --- a/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py +++ b/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py @@ -586,7 +586,8 @@ 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: Value(Int32)" in emitted + assert "value: Int32" in emitted + assert "ref: Addr(Float64)" in emitted assert "@prototype\ndef string_iface(" in emitted assert "read_label: String[8]" in emitted assert native_contract_issues(parse_pyi_text(emitted, module_name=module.name)) == [] diff --git a/tests/semantics/conversion/pyi/test_types_and_values.py b/tests/semantics/conversion/pyi/test_types_and_values.py index 0e76bb2e4..c3b44a9a3 100644 --- a/tests/semantics/conversion/pyi/test_types_and_values.py +++ b/tests/semantics/conversion/pyi/test_types_and_values.py @@ -203,7 +203,7 @@ def set_status( assert parse_pyi_text(emitted, module_name="status_api") == module -def test_convert_pyi_to_ir_uses_reference_default_and_explicit_value_callbacks(): +def test_convert_pyi_to_ir_uses_value_default_and_explicit_reference_callbacks(): module = parse_pyi_text( """ class particle: @@ -211,11 +211,13 @@ class particle: @prototype def callback_shape( - value: Value(Int32), + value: Int32, + scalar_ref: Addr(Float64), values: Float64[:], scalar_storage: Float64[()], - scalar: Float64, - count: Int32, + scalar_value: Float64, + count_ref: Addr(Int32), + derived_value: Value(particle), output: Float64[:], result_storage: Float64[()], ) -> None: ... @@ -235,17 +237,21 @@ def register( False, False, False, + True, False, + True, False, False, ] assert callback_arguments[0].semantic_type.storage is None - assert callback_arguments[1].semantic_type.storage.kind == "array" + assert callback_arguments[1].semantic_type.storage.kind == "reference" assert callback_arguments[2].semantic_type.storage.kind == "array" - assert callback_arguments[3].semantic_type.storage.kind == "reference" - assert callback_arguments[4].semantic_type.storage.mutable is True - assert callback_arguments[5].semantic_type.storage.mutable is True - assert callback_arguments[6].semantic_type.storage.mutable is True + assert callback_arguments[3].semantic_type.storage.kind == "array" + assert callback_arguments[4].semantic_type.storage is None + assert callback_arguments[5].semantic_type.storage.kind == "reference" + assert callback_arguments[6].semantic_type.storage is None + assert callback_arguments[7].semantic_type.storage.mutable is True + assert callback_arguments[8].semantic_type.storage.mutable is True @pytest.mark.parametrize( @@ -460,13 +466,34 @@ def invalid(value: String[:]) -> None: ... @pytest.mark.parametrize( "annotation", [ - "Addr(Float64)", "Addr(Float64[n])", "Addr[2](Float64)", + "Addr(String[8])", + "Addr(particle)", + "Addr(Allocatable[Float64])", + "Addr(Pointer[Float64])", + ], +) +def test_convert_pyi_to_ir_rejects_invalid_prototype_address_wrappers(annotation: str): + with pytest.raises(ValueError, match=r"Addr.*prototype"): + parse_pyi_text( + f"class particle:\n mass: Float64\n\n@prototype\ndef callback(value: {annotation}) -> None: ...", + module_name="callbacks", + ) + + +@pytest.mark.parametrize( + "annotation", + [ + "Value(Float64)", + "Value(Int32)", + "Value(String[8])", + "Value(Allocatable[Float64])", + "Value(Pointer[Float64])", ], ) -def test_convert_pyi_to_ir_rejects_unnecessary_prototype_address_wrappers(annotation: str): - with pytest.raises(ValueError, match=r"Addr\(\.\.\.\) is unnecessary inside prototype declarations"): +def test_convert_pyi_to_ir_rejects_redundant_or_invalid_prototype_value_wrappers(annotation: str): + with pytest.raises(ValueError, match=r"Value.*callback"): parse_pyi_text( f"@prototype\ndef callback(value: {annotation}) -> None: ...", module_name="callbacks", diff --git a/tests/semantics/policy/test_wrapper_policy.py b/tests/semantics/policy/test_wrapper_policy.py index d2898476c..ee555c755 100644 --- a/tests/semantics/policy/test_wrapper_policy.py +++ b/tests/semantics/policy/test_wrapper_policy.py @@ -2,6 +2,7 @@ import pytest +from tests._shared.ownership_policy_support import parse_pyi_text from tests.wrapper.fortran._support import wrapper_source from x2py.parsers.fortran.parser import parse_fortran_project from x2py.pipeline.build import _apply_source_python_exports, _fortran_source_for_pipeline, _merge_wrapper_modules @@ -24,12 +25,14 @@ ObjectKind, OwnershipOwner, PythonBarrierAction, - StorageMode, SetterAction, + StorageMode, TransferMode, ) from x2py.semantics.policy_completion import complete_semantic_policies from x2py.semantics.wrapper_policy import ( + RAW_STRING_ADDRESS_COPY_REASON, + STRING_STORAGE_COPY_REASON, ArgumentHandoffMode, BridgeDataAction, CallbackABIKind, @@ -38,18 +41,15 @@ FunctionWrapperPolicy, ModuleGetterAction, ModuleVariablePolicy, + NativeArrayDescriptorKind, + NativeDescriptorHandoffABI, NativeStatusErrorPolicy, OptionalMode, PythonExceptionKind, - RAW_STRING_ADDRESS_COPY_REASON, - STRING_STORAGE_COPY_REASON, WritebackPhase, completed_function_wrapper_policy, ) -from tests._shared.ownership_policy_support import parse_pyi_text - - FMATH_CONTRACT = Path("tests/wrapper/fortran/scalars/contracts/fmath/__init__.pyi") @@ -218,8 +218,42 @@ def with_scalar(n: Int32) -> tuple[Int32, Int32]: ... hidden = policy.results[1] assert hidden.native_barrier_action is NativeBarrierAction.PASS_CALL_LOCAL_ADDRESS assert policy.native_call_slots[1].owner_path == hidden.owner_path - assert policy.native_call_slots[1].result_position == hidden.result_position - assert policy.native_call_slots[1].native_barrier_action is hidden.native_barrier_action + + +def test_rank_zero_scalar_storage_results_complete_as_numpy_array_policies(): + module = parse_pyi_text( + """ +def direct_storage_result() -> Float64[()]: ... + +@native_call([Return("out", 0)]) +def hidden_storage_result() -> Float64[()]: ... +""", + module_name="rank_zero_storage_results", + ) + complete_semantic_policies(module) + + policies = {function.name: completed_function_wrapper_policy(function) for function in module.functions} + direct_policy = policies["direct_storage_result"] + hidden_policy = policies["hidden_storage_result"] + direct = direct_policy.results[0] + hidden = hidden_policy.results[0] + + assert direct_policy.supported is True + assert hidden_policy.supported is True + assert direct.ownership.kind is ObjectKind.NUMPY_ARRAY + assert direct.array.rank == 0 + assert direct.array.category == "scalar_storage" + assert direct.codegen_action is CodegenAction.COPY_OUT + assert direct.native_barrier_action is NativeBarrierAction.NONE + assert direct.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION + assert hidden.ownership.kind is ObjectKind.NUMPY_ARRAY + assert hidden.array.rank == 0 + assert hidden.array.category == "scalar_storage" + assert hidden.codegen_action is CodegenAction.COPY_OUT + assert hidden.native_barrier_action is NativeBarrierAction.PASS_STORAGE_ADDRESS + assert hidden.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION + assert hidden_policy.native_call_slots[0].result_position == hidden.result_position + assert hidden_policy.native_call_slots[0].native_barrier_action is hidden.native_barrier_action def test_source_hidden_scalar_output_completes_call_local_address_before_planning(): @@ -233,7 +267,7 @@ def test_source_hidden_scalar_output_completes_call_local_address_before_plannin assert policy.native_call_slots[1].native_barrier_action is hidden.native_barrier_action -def test_source_callback_value_override_and_reference_default_are_completed(): +def test_source_callback_value_default_and_explicit_reference_are_completed(): module = _source_semantic_module("fcallback_all_f90.f90", module_name="fcallback_all_f90") function = next(item for item in module.functions if item.name == "apply_value_callback") policy = completed_function_wrapper_policy(function) @@ -251,6 +285,42 @@ def test_source_callback_value_override_and_reference_default_are_completed(): assert extent.adapter_action is CallbackTransferAction.COPY_IN +@pytest.mark.parametrize( + ("prototype", "blocker"), + [ + ( + "def callback_shape(value: Allocatable[Float64]) -> None: ...", + "callback argument 'value' uses unsupported allocatable, pointer, polymorphic, or assumed-type storage", + ), + ( + "def callback_shape(value: Float64 = ...) -> None: ...", + "callback argument 'value' cannot be optional", + ), + ( + "def callback_shape() -> Pointer[Float64]: ...", + "callback result uses unsupported allocatable, pointer, polymorphic, or assumed-type storage", + ), + ], +) +def test_callback_descriptor_and_optional_forms_are_blocked_before_codegen(prototype: str, blocker: str): + module = parse_pyi_text( + f""" +@prototype +{prototype} + +def apply(callback: callback_shape) -> None: ... +""", + module_name="unsupported_callback_shape", + ) + + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + assert isinstance(policy, FunctionWrapperPolicy) + assert policy.supported is False + assert blocker in policy.blockers + + def test_external_declaration_mode_is_completed_from_native_abi_requirements(): module = parse_pyi_text( """ @@ -290,6 +360,48 @@ def create_allocatable() -> Float64 | None: ... assert decision.native_barrier_action is NativeBarrierAction.PASS_VALUE +def test_direct_allocatable_scalar_function_result_is_blocked_before_codegen(): + module = parse_pyi_text( + """ +@native_call([Arg(0)], result=Allocatable(Return(0))) +def maybe_allocatable(flag: Int32) -> Float64 | None: ... +""", + module_name="direct_descriptor_result", + ) + + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + assert isinstance(policy, FunctionWrapperPolicy) + assert policy.supported is False + assert ( + "direct allocatable scalar function results cannot preserve unallocated state; " + "use an allocatable hidden output projection" + ) in policy.blockers + + +def test_direct_high_rank_allocatable_function_result_is_supported_before_codegen(): + module = parse_pyi_text( + """ +@native_call([Addr(Arg(0)), Addr(Arg(1))]) +def make_matrix(n: Int32, m: Int32) -> Allocatable[Float64[:, :]]: ... +""", + module_name="direct_allocatable_matrix_result", + ) + + complete_semantic_policies(module) + + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + assert isinstance(policy, FunctionWrapperPolicy) + assert policy.supported is True + assert policy.blockers == () + result = policy.results[0] + assert result.rank == 2 + assert result.native_array_handle is not None + assert result.native_array_handle.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE + assert result.native_array_handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE + + def test_source_fmath_scalar_policy_accepts_storage_address_native_action(): module = _source_semantic_module("fmath.f", module_name="fmath") function = next(item for item in module.functions if item.name == "ADD_R8") diff --git a/tests/wrapper/fortran/arrays/contracts/farray_results_f90/farray_results_f90.pyi b/tests/wrapper/fortran/arrays/contracts/farray_results_f90/farray_results_f90.pyi index 651440ab0..c377ed726 100644 --- a/tests/wrapper/fortran/arrays/contracts/farray_results_f90/farray_results_f90.pyi +++ b/tests/wrapper/fortran/arrays/contracts/farray_results_f90/farray_results_f90.pyi @@ -58,3 +58,14 @@ def zero_alloc_vector() -> Allocatable[Float64[:]]: ... def maybe_alloc_vector( n: Int32 ) -> Allocatable[Float64[:]]: ... + +@native_call([Addr(Arg(0))]) +def zero_alloc_matrix( + cols: Int32 +) -> Allocatable[Float64[:, :]]: ... + +@native_call([Addr(Arg(0)), Addr(Arg(1))]) +def maybe_alloc_matrix( + rows: Int32, + cols: Int32 +) -> Allocatable[Float64[:, :]]: ... diff --git a/tests/wrapper/fortran/arrays/test_array_results.py b/tests/wrapper/fortran/arrays/test_array_results.py index 764825f72..04822164d 100644 --- a/tests/wrapper/fortran/arrays/test_array_results.py +++ b/tests/wrapper/fortran/arrays/test_array_results.py @@ -1,14 +1,12 @@ """Array-valued function result runtime wrapper tests.""" import gc -from pathlib import Path import shutil +from pathlib import Path import numpy as np import pytest -from x2py import build_pyi_extension -from x2py.runtime.handles import AllocatableArray from tests.wrapper.fortran._support import ( _build_source_or_generated_pyi_and_import, _compile_native_object, @@ -16,6 +14,8 @@ _sole_native_module, wrapper_source, ) +from x2py import build_pyi_extension +from x2py.runtime.handles import AllocatableArray ARRAY_RESULTS_F90_SOURCE = wrapper_source("farray_results_f90.f90") CONTRACT_FIXTURES = Path(__file__).parent / "contracts" @@ -88,6 +88,27 @@ def test_array_results_follow_data_buffer_and_descriptor_handle_contracts( allocated = module.maybe_alloc_vector(np.int32(3)) assert isinstance(allocated, AllocatableArray) np.testing.assert_allclose(allocated.to_numpy(), np.array([5.0, 10.0, 15.0], dtype=np.float64)) + + allocated_matrix = module.maybe_alloc_matrix(np.int32(2), np.int32(3)) + assert isinstance(allocated_matrix, AllocatableArray) + assert allocated_matrix.allocated is True + np.testing.assert_allclose( + allocated_matrix.to_numpy(), + np.array([[110.0, 120.0, 130.0], [210.0, 220.0, 230.0]], dtype=np.float64), + ) + + zero_alloc_matrix = module.zero_alloc_matrix(np.int32(2)) + assert isinstance(zero_alloc_matrix, AllocatableArray) + assert zero_alloc_matrix.allocated is True + assert zero_alloc_matrix.shape == (0, 2) + assert zero_alloc_matrix.to_numpy().shape == (0, 2) + + empty_matrix = module.maybe_alloc_matrix(np.int32(0), np.int32(3)) + assert isinstance(empty_matrix, AllocatableArray) + assert empty_matrix.allocated is False + assert empty_matrix.shape is None + assert empty_matrix.to_numpy() is None + del module gc.collect() np.testing.assert_allclose(matrix, np.array([[12.0, 13.0, 14.0], [22.0, 23.0, 24.0]], dtype=np.float64)) @@ -142,7 +163,7 @@ def test_owned_allocatable_results_preserve_handle_state(tmp_path: Path): contract_package = tmp_path / "allocatable_results" shutil.copytree(CONTRACT_FIXTURES / "farray_results_f90", contract_package) (contract_package / "__init__.pyi").write_text( - "from .farray_results_f90 import maybe_alloc_vector, zero_alloc_vector\n", + "from .farray_results_f90 import maybe_alloc_matrix, maybe_alloc_vector, zero_alloc_matrix, zero_alloc_vector\n", encoding="utf-8", ) result = build_pyi_extension( @@ -165,5 +186,28 @@ def test_owned_allocatable_results_preserve_handle_state(tmp_path: Path): assert zero_sized.shape == (0,) assert zero_sized.to_numpy().shape == (0,) + allocated_matrix = module.maybe_alloc_matrix(np.int32(2), np.int32(3)) + assert isinstance(allocated_matrix, AllocatableArray) + assert allocated_matrix.allocated is True + np.testing.assert_allclose( + allocated_matrix.to_numpy(), + np.array([[110.0, 120.0, 130.0], [210.0, 220.0, 230.0]]), + ) + + zero_sized_matrix = module.zero_alloc_matrix(np.int32(2)) + assert isinstance(zero_sized_matrix, AllocatableArray) + assert zero_sized_matrix.allocated is True + assert zero_sized_matrix.shape == (0, 2) + assert zero_sized_matrix.to_numpy().shape == (0, 2) + + unallocated_matrix = module.maybe_alloc_matrix(np.int32(0), np.int32(3)) + assert isinstance(unallocated_matrix, AllocatableArray) + assert unallocated_matrix.allocated is False + assert unallocated_matrix.shape is None + assert unallocated_matrix.to_numpy() is None + allocated.close() zero_sized.close() + allocated_matrix.close() + zero_sized_matrix.close() + unallocated_matrix.close() diff --git a/tests/wrapper/fortran/build_from_source/test_compiler_verbose.py b/tests/wrapper/fortran/build_from_source/test_compiler_verbose.py index 9b5012d0a..90b4a620b 100644 --- a/tests/wrapper/fortran/build_from_source/test_compiler_verbose.py +++ b/tests/wrapper/fortran/build_from_source/test_compiler_verbose.py @@ -2,6 +2,7 @@ import sys from pathlib import Path +import x2py.compiling.compilers as compiler_module from x2py.compiling.objects import ObjectFile from x2py.compiling.compilers import Compiler from x2py.compiling.compiler_profiles import available_compilers, vendors @@ -89,6 +90,79 @@ def test_python_sysconfig_profile_flags_do_not_override_wrapper_profile(monkeypa assert "-g" not in command +def test_supported_optional_profile_flags_are_used_when_executing(monkeypatch, tmp_path: Path): + compiler = Compiler("GNU") + monkeypatch.setattr(compiler, "_executable", lambda _language, _tools: "gfortran") + monkeypatch.setattr(compiler, "_supports_optional_flag", lambda _executable, flag: flag == "-ftrampoline-impl=heap") + monkeypatch.setattr(Compiler, "run_command", staticmethod(lambda command, _verbose=False: tuple(command))) + object_file = ObjectFile( + source=tmp_path / "bridge.f90", + object_path=tmp_path / "bridge.o", + language="fortran", + ) + + compiler.compile_object(object_file) + + assert "-ftrampoline-impl=heap" in compiler.command_log[0] + + +def test_unsupported_optional_profile_flags_are_omitted(monkeypatch, tmp_path: Path): + compiler = Compiler("GNU") + monkeypatch.setattr(compiler, "_executable", lambda _language, _tools: "gfortran") + monkeypatch.setattr(compiler, "_supports_optional_flag", lambda _executable, _flag: False) + monkeypatch.setattr(Compiler, "run_command", staticmethod(lambda command, _verbose=False: tuple(command))) + object_file = ObjectFile( + source=tmp_path / "bridge.f90", + object_path=tmp_path / "bridge.o", + language="fortran", + ) + + compiler.compile_object(object_file) + + assert "-ftrampoline-impl=heap" not in compiler.command_log[0] + + +def test_optional_profile_flag_probe_reads_the_selected_compiler_help(monkeypatch): + calls = [] + + def completed(command, **kwargs): + calls.append((command, kwargs)) + return type( + "Completed", + (), + { + "returncode": 0, + "stdout": " -ftrampoline-impl= stack\n", + "stderr": "", + }, + )() + + Compiler._supports_optional_flag.cache_clear() + monkeypatch.setattr(compiler_module.subprocess, "run", completed) + + assert Compiler._supports_optional_flag("gfortran-test", "-ftrampoline-impl=heap") is True + assert calls == [ + ( + ("gfortran-test", "-Q", "--help=common"), + { + "capture_output": True, + "text": True, + "check": False, + }, + ) + ] + + +def test_optional_profile_flag_probe_fails_closed_when_the_compiler_cannot_start(monkeypatch): + def unavailable(*_args, **_kwargs): + raise OSError("missing compiler") + + Compiler._supports_optional_flag.cache_clear() + monkeypatch.setattr(compiler_module.subprocess, "run", unavailable) + + assert Compiler._supports_optional_flag("missing-gfortran", "-ftrampoline-impl=heap") is False + + def test_link_keeps_the_declared_object_and_link_argument_order(monkeypatch, tmp_path: Path): compiler = Compiler("GNU", execute_commands=False) monkeypatch.setattr(compiler, "_executable", lambda _language, _tools: "gfortran") diff --git a/tests/wrapper/fortran/callbacks/contracts/fcallback_all_f90/fcallback_all_f90.pyi b/tests/wrapper/fortran/callbacks/contracts/fcallback_all_f90/fcallback_all_f90.pyi index 612630adc..46ba1c6d8 100644 --- a/tests/wrapper/fortran/callbacks/contracts/fcallback_all_f90/fcallback_all_f90.pyi +++ b/tests/wrapper/fortran/callbacks/contracts/fcallback_all_f90/fcallback_all_f90.pyi @@ -1,4 +1,4 @@ -from x2py.contracts import Addr, Arg, Float64, Int32, Return, Returns, String, Value, native_call, prototype +from x2py.contracts import Addr, Arg, Float64, Int32, Return, Returns, String, native_call, prototype class point_t: def __init__( @@ -13,19 +13,19 @@ class point_t: @prototype def value_callback( - value: Value(Int32) + value: Int32 ) -> Int32: ... @prototype def scalar_storage_callback( - value: Float64, - output: Float64, - missing: Float64 + value: Addr(Float64), + output: Addr(Float64), + missing: Addr(Float64) ) -> None: ... @prototype def array_storage_callback( - count: Int32, + count: Addr(Int32), values: Float64[count], output: Float64[count] ) -> None: ... diff --git a/tests/wrapper/fortran/callbacks/contracts/fcallback_array_f90/fcallback_array_f90.pyi b/tests/wrapper/fortran/callbacks/contracts/fcallback_array_f90/fcallback_array_f90.pyi index dad878c74..a334e1218 100644 --- a/tests/wrapper/fortran/callbacks/contracts/fcallback_array_f90/fcallback_array_f90.pyi +++ b/tests/wrapper/fortran/callbacks/contracts/fcallback_array_f90/fcallback_array_f90.pyi @@ -2,13 +2,13 @@ from x2py.contracts import Addr, Arg, Float64, Int32, Returns, native_call, prot @prototype def reduce_callback( - count: Int32, + count: Addr(Int32), values: Float64[count] ) -> Float64: ... @prototype def transform_callback( - count: Int32, + count: Addr(Int32), values: Float64[count] ) -> Float64[count]: ... diff --git a/tests/wrapper/fortran/callbacks/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi b/tests/wrapper/fortran/callbacks/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi index 522d6b7aa..07b89ba71 100644 --- a/tests/wrapper/fortran/callbacks/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi +++ b/tests/wrapper/fortran/callbacks/contracts/fcallback_scalar_f90/fcallback_scalar_f90.pyi @@ -2,17 +2,17 @@ from x2py.contracts import Addr, Arg, Float64, native_call, prototype @prototype def scalar_callback( - value: Float64 + value: Addr(Float64) ) -> Float64: ... @prototype def notify_callback( - value: Float64 + value: Addr(Float64) ) -> None: ... @prototype def callback( - value: Float64 + value: Addr(Float64) ) -> Float64: ... @native_call([Arg(0), Addr(Arg(1))]) diff --git a/tests/wrapper/fortran/module_state/contracts/fallocatable_views_f90/fallocatable_views_f90.pyi b/tests/wrapper/fortran/module_state/contracts/fallocatable_views_f90/fallocatable_views_f90.pyi index bdab7e9b8..cc9de5201 100644 --- a/tests/wrapper/fortran/module_state/contracts/fallocatable_views_f90/fallocatable_views_f90.pyi +++ b/tests/wrapper/fortran/module_state/contracts/fallocatable_views_f90/fallocatable_views_f90.pyi @@ -53,7 +53,7 @@ def make_values( n: Int32 ) -> Allocatable[Float64[:]]: ... -@native_call([Addr(Arg(0)), Addr(Arg(1))]) +@native_call([Addr(Arg(0)), Addr(Arg(1)), Return('values', 0)]) def make_matrix( n: Int32, m: Int32 diff --git a/tests/wrapper/fortran/module_state/test_allocatable_views.py b/tests/wrapper/fortran/module_state/test_allocatable_views.py index bd69a7f77..7426fb407 100644 --- a/tests/wrapper/fortran/module_state/test_allocatable_views.py +++ b/tests/wrapper/fortran/module_state/test_allocatable_views.py @@ -148,15 +148,15 @@ value => target_scale end subroutine create_pointer - function maybe_allocatable(flag) result(value) + subroutine maybe_allocatable(flag, value) integer(4), intent(in) :: flag - real(8), allocatable :: value + real(8), allocatable, intent(out) :: value if (flag /= 0) then allocate(value) value = 3.5_8 end if - end function maybe_allocatable + end subroutine maybe_allocatable function maybe_pointer(flag) result(value) integer(4), intent(in) :: flag diff --git a/tests/wrapper/fortran/scalars/test_scalar_boundary_plan.py b/tests/wrapper/fortran/scalars/test_scalar_boundary_plan.py index a0c663f90..799817413 100644 --- a/tests/wrapper/fortran/scalars/test_scalar_boundary_plan.py +++ b/tests/wrapper/fortran/scalars/test_scalar_boundary_plan.py @@ -94,6 +94,16 @@ def _build_scalar_boundary_modules(tmp_path: Path): value = 43 end subroutine make_raw + function direct_storage_result() result(value) + integer(c_int32_t) :: value + value = 44 + end function direct_storage_result + + subroutine hidden_storage_result(value) + integer(c_int32_t), intent(out) :: value + value = 45 + end subroutine hidden_storage_result + subroutine mapped_status(status, base) integer(c_int32_t), intent(out) :: status integer(c_int32_t), intent(in) :: base @@ -125,6 +135,11 @@ def make_storage(value: Int32[()]) -> None: ... def make_raw(value: Addr(Int32)) -> None: ... +def direct_storage_result() -> Int32[()]: ... + +@native_call([Return("value", 0)]) +def hidden_storage_result() -> Int32[()]: ... + @native_call([Return("status", 0), Addr(Arg(0))]) def mapped_status(base: Int32) -> Int32: ... """, @@ -350,6 +365,18 @@ def test_scalar_value_storage_raw_address_out_and_inout_use_canonical_plan(tmp_p assert module.make_raw(output_raw.ctypes.data) is None assert output_raw[()] == np.int32(43) + direct_storage = module.direct_storage_result() + assert isinstance(direct_storage, np.ndarray) + assert direct_storage.shape == () + assert direct_storage.dtype == np.dtype(np.int32) + assert direct_storage[()] == np.int32(44) + + hidden_storage = module.hidden_storage_result() + assert isinstance(hidden_storage, np.ndarray) + assert hidden_storage.shape == () + assert hidden_storage.dtype == np.dtype(np.int32) + assert hidden_storage[()] == np.int32(45) + assert module.mapped_status(np.int32(4)) == np.int32(15) with pytest.raises(TypeError): diff --git a/tests/wrapper/fortran/strings/contracts/fstrings_f90/fstrings_f90.pyi b/tests/wrapper/fortran/strings/contracts/fstrings_f90/fstrings_f90.pyi index 34b6d0138..f96e7e6dc 100644 --- a/tests/wrapper/fortran/strings/contracts/fstrings_f90/fstrings_f90.pyi +++ b/tests/wrapper/fortran/strings/contracts/fstrings_f90/fstrings_f90.pyi @@ -38,7 +38,7 @@ def string_result_padded() -> String[8]: ... def string_result_c_char() -> String[8]: ... -@native_call([Arg(0)], result=Allocatable(Return(0))) +@native_call([Arg(0), Allocatable(Return('value', 0))]) def string_result_deferred( text: String ) -> String | None: ... diff --git a/tests/wrapper/fortran/strings/test_character_arguments.py b/tests/wrapper/fortran/strings/test_character_arguments.py index d26ef5616..92d2cd8c4 100644 --- a/tests/wrapper/fortran/strings/test_character_arguments.py +++ b/tests/wrapper/fortran/strings/test_character_arguments.py @@ -223,14 +223,14 @@ def test_deferred_character_array_handles_use_canonical_plan(tmp_path: Path): names = [character(kind=c_char, len=4) :: "gold", "blue"] end function make_names_function - function maybe_name(flag) result(name) + subroutine maybe_name(flag, name) integer(kind=4), intent(in) :: flag - character(kind=c_char, len=:), allocatable :: name + character(kind=c_char, len=:), allocatable, intent(out) :: name if (flag /= 0) then allocate(character(kind=c_char, len=4) :: name) name = "blue" end if - end function maybe_name + end subroutine maybe_name subroutine replace_names(names) character(kind=c_char, len=:), allocatable, intent(inout) :: names(:) @@ -257,7 +257,7 @@ def make_names() -> Allocatable[String[:][:]]: ... def make_names_function() -> Allocatable[String[:][:]]: ... -@native_call([Arg(0)], result=Allocatable(Return(0))) +@native_call([Arg(0), Allocatable(Return("name", 0))]) def maybe_name(flag: Int32) -> String | None: ... def replace_names( diff --git a/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py b/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py index dd003508b..74931bcae 100644 --- a/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py +++ b/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py @@ -533,7 +533,7 @@ def update(scale: Float64 | None = ..., target: Float64 | None = ...) -> None: . assert "Default is None." not in c_wrapper -def test_printer_emits_named_prototype_and_reference_with_value_override(): +def test_printer_emits_named_prototype_with_primitive_reference_and_derived_value(): printer = PyiPrinter() missing_reference = SemanticType( "Float64", @@ -553,6 +553,16 @@ def test_printer_emits_named_prototype_and_reference_with_value_override(): "Int32", storage=SemanticStorageContract(kind="reference", read_only=True, pointer_depth=1), ) + descriptor_reference = SemanticType( + "Float64", + metadata={"fortran_allocatable": True}, + storage=SemanticStorageContract(kind="reference", mutable=True, pointer_depth=1), + ) + polymorphic_reference = SemanticType( + "point_t", + metadata={"fortran_polymorphic": True}, + storage=SemanticStorageContract(kind="reference", mutable=True, pointer_depth=1), + ) output_array = SemanticType( "Float64", rank=1, @@ -594,6 +604,16 @@ def test_printer_emits_named_prototype_and_reference_with_value_override(): input_reference, origin=SemanticOrigin(metadata={"value": False}), ), + SemanticArgument( + "descriptor", + descriptor_reference, + origin=SemanticOrigin(metadata={"value": False}), + ), + SemanticArgument( + "polymorphic", + polymorphic_reference, + origin=SemanticOrigin(metadata={"value": False}), + ), SemanticArgument( "write", output_array, @@ -604,6 +624,11 @@ def test_printer_emits_named_prototype_and_reference_with_value_override(): inout_array, origin=SemanticOrigin(metadata={"value": False}), ), + SemanticArgument( + "derived_value", + SemanticType("point_t"), + origin=SemanticOrigin(metadata={"value": True}), + ), ] prototype = SemanticPrototype( name="update_values", @@ -628,8 +653,14 @@ def test_printer_emits_named_prototype_and_reference_with_value_override(): ) assert printer.emit(callback) == "update_values" - assert "@prototype\ndef update_values(" in printer.emit(prototype) - assert "value: Value(Int32)" in printer.emit(prototype) + emitted = printer.emit(prototype) + assert "@prototype\ndef update_values(" in emitted + assert "value: Int32" in emitted + assert "missing: Addr(Float64)" in emitted + assert "read: Addr(Int32)" in emitted + assert "descriptor: Allocatable[Float64]" in emitted + assert "polymorphic: Annotated[point_t, Polymorphic]" in emitted + assert "derived_value: Value(point_t)" in emitted def test_printer_projection_return_helpers_and_keyword_data_members(): diff --git a/tests/wrapper_codegen/test_phase10_callbacks.py b/tests/wrapper_codegen/test_phase10_callbacks.py index 4e02f24c4..eaa31c991 100644 --- a/tests/wrapper_codegen/test_phase10_callbacks.py +++ b/tests/wrapper_codegen/test_phase10_callbacks.py @@ -59,7 +59,7 @@ def _sources(plan): return c_source, bridge -def test_callback_policy_completes_reference_default_and_value_override_before_planning(): +def test_callback_policy_completes_value_default_and_explicit_reference_before_planning(): module = _module() policies = { function.name: function.metadata[models.RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] diff --git a/tests/wrapper_codegen/test_phase2e_scalar_boundaries.py b/tests/wrapper_codegen/test_phase2e_scalar_boundaries.py index 38ea53427..5262736dc 100644 --- a/tests/wrapper_codegen/test_phase2e_scalar_boundaries.py +++ b/tests/wrapper_codegen/test_phase2e_scalar_boundaries.py @@ -5,7 +5,7 @@ import pytest from tests._shared.ownership_policy_support import parse_pyi_text -from x2py.semantics.ownership import NativeBarrierAction, PythonBarrierAction +from x2py.semantics.ownership import CodegenAction, NativeBarrierAction, ObjectKind, PythonBarrierAction from x2py.semantics.policy_completion import complete_semantic_policies from x2py.semantics.wrapper_policy import ArgumentHandoffMode, BridgeDataAction from x2py.wrapper_codegen import WrapperCodeGenerator, WrapperPlanner @@ -16,6 +16,9 @@ def _scalar_boundary_plan(): """ def storage(x: Float64[()]) -> None: ... def raw(x: Addr(Float64)) -> None: ... +def direct_storage_result() -> Float64[()]: ... +@native_call([Return("out", 0)]) +def hidden_storage_result() -> Float64[()]: ... """, module_name="scalar_boundaries", ) @@ -28,10 +31,17 @@ def test_scalar_storage_and_raw_address_plans_keep_explicit_boundary_facts(): functions = {function.binding.python_name: function for function in plan.namespaces[0].functions} storage_function = functions["storage"] raw_function = functions["raw"] + direct_function = functions["direct_storage_result"] + hidden_function = functions["hidden_storage_result"] storage = storage_function.arguments[0] raw = raw_function.arguments[0] + direct_result = direct_function.results[0] + hidden_result = hidden_function.results[0] assert storage.native_call_slot is storage_function.native_call_slots[storage.native_position] + assert storage.object_kind is ObjectKind.NUMPY_ARRAY + assert storage.array.rank == 0 + assert storage.array.category == "scalar_storage" assert storage.binding.python_action is PythonBarrierAction.SCALAR_STORAGE assert storage.binding.writable is True assert storage.bridge.native_action is NativeBarrierAction.PASS_STORAGE_ADDRESS @@ -44,6 +54,18 @@ def test_scalar_storage_and_raw_address_plans_keep_explicit_boundary_facts(): assert raw.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS assert raw.bridge.data_action is BridgeDataAction.ASSOCIATE_VIEW assert raw.bridge.copy_reason is None + assert direct_result.object_kind is ObjectKind.NUMPY_ARRAY + assert direct_result.array.rank == 0 + assert direct_result.array.category == "scalar_storage" + assert direct_result.binding.codegen_action is CodegenAction.COPY_OUT + assert direct_result.bridge.native_action is NativeBarrierAction.NONE + assert direct_result.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION + assert hidden_result.object_kind is ObjectKind.NUMPY_ARRAY + assert hidden_result.array.rank == 0 + assert hidden_result.array.category == "scalar_storage" + assert hidden_result.binding.codegen_action is CodegenAction.COPY_OUT + assert hidden_result.bridge.native_action is NativeBarrierAction.PASS_STORAGE_ADDRESS + assert hidden_result.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION def test_scalar_storage_and_raw_address_lower_to_direct_named_paths(): @@ -63,6 +85,10 @@ def test_scalar_storage_and_raw_address_lower_to_direct_named_paths(): assert "if (!PyLong_Check(bound_x_obj))" in c_source assert "bound_x = PyLong_AsVoidPtr(bound_x_obj);" in c_source assert "bind_c_raw(bound_x);" in c_source + assert "void * bind_c_direct_storage_result(void);" in c_source + assert "void bind_c_hidden_storage_result(void ** out);" in c_source + assert c_source.count("PyArray_New(&PyArray_Type, 0, NULL, NPY_FLOAT64") == 2 + assert "bind_c_hidden_storage_result(&out);" in c_source assert 'subroutine bind_c_storage(bound_x) bind(c, name="bind_c_storage")' in bridge_source assert 'subroutine bind_c_raw(bound_x) bind(c, name="bind_c_raw")' in bridge_source @@ -70,6 +96,20 @@ def test_scalar_storage_and_raw_address_lower_to_direct_named_paths(): assert bridge_source.count("call c_f_pointer(bound_x, x)") == 2 assert "call native_storage(x)" in bridge_source assert "call native_raw(x)" in bridge_source + assert ( + 'function bind_c_direct_storage_result() result(result) bind(c, name="bind_c_direct_storage_result")' + in bridge_source + ) + assert 'subroutine bind_c_hidden_storage_result(out) bind(c, name="bind_c_hidden_storage_result")' in bridge_source + assert "real(c_double) :: result_value" in bridge_source + assert "real(c_double), pointer :: result_copy" in bridge_source + assert "call c_f_pointer(result, result_copy)" in bridge_source + assert "result_copy = result_value" in bridge_source + assert "real(c_double) :: out_value" in bridge_source + assert "real(c_double), pointer :: out_copy" in bridge_source + assert "call c_f_pointer(out, out_copy)" in bridge_source + assert "out_copy = out_value" in bridge_source + assert "dimension()" not in bridge_source def test_scalar_address_handoff_plan_edits_fail_before_lowering(): @@ -77,7 +117,7 @@ def test_scalar_address_handoff_plan_edits_fail_before_lowering(): storage = plan.namespaces[0].functions[0].arguments[0] storage.bridge.handoff_mode = ArgumentHandoffMode.VALUE - with pytest.raises(ValueError, match="invalid-scalar-address-handoff"): + with pytest.raises(ValueError, match="invalid-scalar-storage-handoff-mode"): WrapperCodeGenerator().generate(plan) diff --git a/tests/wrapper_codegen/test_phase7_native_array_handles.py b/tests/wrapper_codegen/test_phase7_native_array_handles.py index 137ee9296..985e7287a 100644 --- a/tests/wrapper_codegen/test_phase7_native_array_handles.py +++ b/tests/wrapper_codegen/test_phase7_native_array_handles.py @@ -39,7 +39,10 @@ def replace( @native_call([Addr(Arg(0))]) def make(n: Int32) -> Allocatable[Float64[:]]: ... -@native_call([Arg(0)], result=Allocatable(Return(0))) +@native_call([Addr(Arg(0)), Addr(Arg(1))]) +def make_matrix(n: Int32, m: Int32) -> Allocatable[Float64[:, :]]: ... + +@native_call([Arg(0), Allocatable(Return("value", 0))]) def deferred(text: String) -> String | None: ... def make_names() -> Allocatable[String[:][:]]: ... @@ -142,6 +145,12 @@ def test_phase7_keeps_datatype_specific_state_under_argument_and_result_plans(): assert owned.native_array_handle.handoff.owner_storage_role is not None assert NativeArrayOperation.DESTROY in owned.native_array_handle.operations + owned_matrix = functions["make_matrix"].results[0] + assert owned_matrix.native_array_handle is not None + assert owned_matrix.native_array_handle.array.rank == 2 + assert owned_matrix.native_array_handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE + assert owned_matrix.native_array_handle.descriptor_ownership is NativeArrayDescriptorOwnership.OWNED + deferred = functions["deferred"].results[0] assert deferred.native_array_handle is None assert deferred.scalar_descriptor is not None @@ -233,8 +242,10 @@ def test_phase7_generated_artifacts_follow_one_typed_action_vocabulary(): assert '"_native_array_descriptor_handoff_for_binding_positional"' in c_source assert '"_native_array_handle_from_generated_ops"' in c_source assert "CFI_CDESC_T(1)" in c_source + assert "CFI_CDESC_T(2)" in c_source assert "real(c_double), allocatable, dimension(:) :: values" in bridge_source assert "real(c_double), pointer, dimension(:) :: values" in bridge_source + assert "real(c_double), allocatable, dimension(:, :) :: result_value" in bridge_source optional_start = bridge_source.index("function bind_c_optional(") optional_end = bridge_source.index("end function bind_c_optional", optional_start) optional_bridge = bridge_source[optional_start:optional_end] @@ -248,11 +259,10 @@ def test_phase7_generated_artifacts_follow_one_typed_action_vocabulary(): assert "bound_values_elem_len = sizeof(double);" in optional_binding assert "bound_values_descriptor_rank = 1;" in optional_binding assert "bound_values = (CFI_cdesc_t *)&bound_values_storage;" in optional_binding - assert "x2py_collect_make_allocatable_array_result" in bridge_source - assert "call x2py_collect_make_allocatable_array_result(native_make(n), result_value)" in bridge_source + assert "result_value = native_make(n)" in bridge_source + assert "result_value = native_make_matrix(n, m)" in bridge_source assert "call move_alloc(result_value, result)" in bridge_source - assert "x2py_collect_deferred_scalar_descriptor_result" in bridge_source - assert "character(kind=c_char, len=:), allocatable :: result_value" in bridge_source + assert "character(kind=c_char, len=:), allocatable :: value_value" in bridge_source 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 @@ -270,15 +280,10 @@ def test_phase7_numeric_owned_result_is_collected_before_persistent_descriptor_m assert "real(c_double), allocatable, dimension(:), intent(out) :: result" in procedure assert "real(c_double), allocatable, dimension(:) :: result_value" in procedure - assert "call x2py_collect_make_allocatable_array_result(native_make(n), result_value)" in procedure + assert "result_value = native_make(n)" in procedure assert "call move_alloc(result_value, result)" in procedure assert "result = result_value" not in procedure - - helper_start = bridge_source.index("subroutine x2py_collect_make_allocatable_array_result(") - helper_end = bridge_source.index("end subroutine", helper_start) - helper = bridge_source[helper_start:helper_end] - assert "if (allocated(value)) then" in helper - assert "target = value" in helper + assert "x2py_collect_make_allocatable_array_result" not in bridge_source @pytest.mark.parametrize( diff --git a/tools/mkdocs_publication.py b/tools/mkdocs_publication.py index 9ecf0ec67..427dc5fb9 100644 --- a/tools/mkdocs_publication.py +++ b/tools/mkdocs_publication.py @@ -110,12 +110,37 @@ def _relative_document_target(source_uri: str, raw_target: str) -> str | None: return posixpath.normpath(posixpath.join(source_parent, unquote(parsed.path))) -def _unlink_unpublished_targets(markdown: str, source_uri: str) -> str: +def _document_route(markdown_uri: str) -> str: + path = PurePosixPath(markdown_uri) + route = path.parent.as_posix() if path.name == "index.md" else path.with_suffix("").as_posix() + return "" if route == "." else route + + +def _unpublished_document_site_target(source_uri: str, raw_target: str, resolved_target: str) -> str: + target_parts = raw_target.strip().split(maxsplit=1) + parsed = urlsplit(target_parts[0]) + source_route = _document_route(source_uri) + target_route = _document_route(resolved_target) + rewritten = posixpath.relpath(target_route or ".", start=source_route or ".") + if rewritten == ".": + rewritten = "" + elif not rewritten.endswith("/"): + rewritten += "/" + if parsed.query: + rewritten += f"?{parsed.query}" + if parsed.fragment: + rewritten += f"#{parsed.fragment}" + if len(target_parts) == 2: + rewritten += f" {target_parts[1]}" + return rewritten + + +def _rewrite_unpublished_document_targets(markdown: str, source_uri: str) -> str: def replace_link(match: re.Match[str]) -> str: label, target = match.groups() resolved = _relative_document_target(source_uri, target) if resolved in _known_document_paths and resolved not in _published_paths: - return label + return f"[{label}]({_unpublished_document_site_target(source_uri, target, resolved)})" return match.group(0) return _MARKDOWN_LINK.sub(replace_link, markdown) @@ -185,7 +210,7 @@ def on_files(files, **_kwargs): def on_page_markdown(markdown: str, page, **_kwargs) -> str: - """Label local drafts and remove production links to unpublished pages.""" + """Label local drafts and preserve production links to unpublished pages.""" source_uri = page.file.src_uri markdown = _rewrite_repository_targets(markdown, source_uri) if _include_drafts: @@ -196,4 +221,4 @@ def on_page_markdown(markdown: str, page, **_kwargs) -> str: ) return warning + markdown return markdown - return _unlink_unpublished_targets(markdown, source_uri) + return _rewrite_unpublished_document_targets(markdown, source_uri) diff --git a/x2py/compiling/compiler_profiles.py b/x2py/compiling/compiler_profiles.py index e191094b9..af3d367f3 100644 --- a/x2py/compiling/compiler_profiles.py +++ b/x2py/compiling/compiler_profiles.py @@ -77,6 +77,7 @@ def _language( debug_flags: tuple[str, ...], release_flags: tuple[str, ...], general_flags: tuple[str, ...], + optional_general_flags: tuple[str, ...] = (), standard_flags: tuple[str, ...], module_output_flag: str | None = None, openmp: dict[str, tuple[str, ...]] | None = None, @@ -89,6 +90,7 @@ def _language( "debug_flags": debug_flags, "release_flags": release_flags, "general_flags": general_flags, + "optional_general_flags": optional_general_flags, "standard_flags": standard_flags, "mpi": {}, "openmp": openmp or {}, @@ -125,6 +127,7 @@ def _language( debug_flags=("-fcheck=bounds", "-g", "-O0"), release_flags=("-O3", "-funroll-loops", "-DNDEBUG"), general_flags=("-fPIC", "-cpp"), + optional_general_flags=("-ftrampoline-impl=heap",), standard_flags=("-std=f2003",), module_output_flag="-J", openmp={"flags": ("-fopenmp",), "libs": ("gomp",)}, diff --git a/x2py/compiling/compilers.py b/x2py/compiling/compilers.py index 0a93354e8..64f45b050 100644 --- a/x2py/compiling/compilers.py +++ b/x2py/compiling/compilers.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Iterable, Mapping +from functools import cache import json import os from pathlib import Path @@ -66,9 +67,15 @@ def compile_object(self, object_file: ObjectFile, *, verbose: bool | int = False object_file.object_path.parent.mkdir(parents=True, exist_ok=True) language = self._language(object_file.language) + executable = self._executable(language, object_file.tools) command = [ - self._executable(language, object_file.tools), - *self._flags(language, object_file.tools, object_file.flags), + executable, + *self._flags( + language, + object_file.tools, + object_file.flags, + executable=executable if self._execute_commands else None, + ), "-c", *self._path_flags("-I", self._include_dirs(language, object_file.tools, object_file.include_dirs)), str(object_file.source), @@ -115,12 +122,18 @@ def link_extension( ) resolved_library_dirs = self._library_dirs(language_info, selected_tools, selected_library_dirs) extension_path = output_path / f"{module_name}{language_info['python']['shared_suffix']}" + executable = self._executable(language_info, selected_tools) if verbose: print(f">> Create shared library: {extension_path}") command = [ - self._executable(language_info, selected_tools), + executable, "-shared", - *self._flags(language_info, selected_tools - {"python"}, flags), + *self._flags( + language_info, + selected_tools - {"python"}, + flags, + executable=executable if self._execute_commands else None, + ), *self._path_flags("-L", resolved_library_dirs), *self._path_flags("-Wl,-rpath", resolved_library_dirs), *(str(path) for path in object_paths), @@ -198,9 +211,12 @@ def _flags( language: Mapping[str, object], tools: Iterable[str], requested: Iterable[str], + *, + executable: str | None = None, ) -> tuple[str, ...]: profile = "debug_flags" if self._debug else "release_flags" values = [*self._strings(language.get(profile, ())), *self._strings(language.get("general_flags", ()))] + values.extend(self._supported_optional_flags(executable, language.get("optional_general_flags", ()))) for tool in sorted(set(tools)): flags = self._tool_mapping(language, tool).get("flags", ()) if tool == "python": @@ -209,6 +225,28 @@ def _flags( values.extend(str(flag) for flag in requested) return tuple(values) + def _supported_optional_flags(self, executable: str | None, flags: object) -> tuple[str, ...]: + """Return profile flags accepted by the selected compiler executable.""" + if executable is None: + return () + return tuple(flag for flag in self._strings(flags) if self._supports_optional_flag(executable, flag)) + + @staticmethod + @cache + def _supports_optional_flag(executable: str, flag: str) -> bool: + """Probe help text instead of making newer profile flags mandatory.""" + help_key = flag.split("=", maxsplit=1)[0] + "=" if "=" in flag else flag + try: + completed = subprocess.run( + (executable, "-Q", "--help=common"), + capture_output=True, + text=True, + check=False, + ) + except OSError: + return False + return completed.returncode == 0 and help_key in f"{completed.stdout}\n{completed.stderr}" + def _include_dirs( self, language: Mapping[str, object], diff --git a/x2py/semantics/ownership.py b/x2py/semantics/ownership.py index 857e71db7..3ca9b2e6e 100644 --- a/x2py/semantics/ownership.py +++ b/x2py/semantics/ownership.py @@ -538,7 +538,6 @@ def decide_semantic_type(self, semantic_type: Any, context: OwnershipContext) -> facts = self._semantic_facts(semantic_type) decision = self._apply_overrides(self._decide(facts, context), facts, context) decision = self._validate_aliased_decision(decision, facts, context) - decision = self._validate_scalar_descriptor_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) @@ -681,6 +680,8 @@ def _decide(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipD return self._handlers[kind](facts, context) def _kind(self, facts: _StorageFacts, context: OwnershipContext) -> ObjectKind: + 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: return ObjectKind.NUMPY_ARRAY if facts.is_string: @@ -926,6 +927,27 @@ def _string_decision(self, facts: _StorageFacts, context: OwnershipContext) -> O descriptor_boundary=True, reason="scalar string descriptor result is copied before native descriptor release", ) + if ( + (facts.allocatable or facts.pointer) + and context.writes_argument + and context.projects_result + and not context.python_visible + ): + storage = StorageMode.HEAP if facts.allocatable else StorageMode.ALIAS + return OwnershipDecision( + ObjectKind.STRING, + OwnershipOwner.PYTHON, + TransferMode.COPY_RETURN, + DestructionPolicy.PYTHON_REFCOUNT, + storage_mode=storage, + boundary_storage_mode=storage, + nullable=True, + descriptor_boundary=True, + mutates_native=True, + projects_result=True, + python_visible=False, + reason="hidden scalar string descriptor output is copied before native descriptor release", + ) if facts.address_role == ADDRESS_ROLE_RAW: return OwnershipDecision( ObjectKind.STRING, @@ -1484,29 +1506,6 @@ def _validate_aliased_decision( """Keep Aliased as addressability metadata, not live-object eligibility.""" return decision - @staticmethod - def _validate_scalar_descriptor_decision( - decision: OwnershipDecision, - facts: _StorageFacts, - context: OwnershipContext, - ) -> OwnershipDecision: - if decision.is_blocked or facts.rank != 0 or not (facts.allocatable or facts.pointer): - return decision - blocker = None - if context.is_argument and context.writes_argument and facts.is_string: - blocker = "scalar descriptor output projection currently supports primitive numeric values only" - if blocker is None: - return decision - return replace( - decision, - owner=OwnershipOwner.UNKNOWN, - transfer=TransferMode.BLOCKED, - destruction=DestructionPolicy.BLOCKED, - borrowed=False, - blocker=blocker, - reason="scalar descriptor construction and readback must have a complete supported policy", - ) - @staticmethod def _validate_pointer_decision( decision: OwnershipDecision, diff --git a/x2py/semantics/pyi2ir.py b/x2py/semantics/pyi2ir.py index 303738a30..dd58d8e32 100644 --- a/x2py/semantics/pyi2ir.py +++ b/x2py/semantics/pyi2ir.py @@ -1929,16 +1929,64 @@ def _prototype_argument_spec(self, node: ast.expr) -> _CallbackArgumentSpec: semantic_type = self.semantic_type(node.args[0]) if semantic_type.rank > 0: raise ValueError("Value(...) callback arguments must be scalar") + if self._is_primitive_scalar_value_type(semantic_type): + raise ValueError( + "Value(...) is unnecessary for primitive callback arguments; " + "bare primitive types are passed by value" + ) + if ( + semantic_type.name == "String" + or semantic_type.storage is not None + 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) if self._is_addr_call(node): - raise ValueError( - "Addr(...) is unnecessary inside prototype declarations; reference passing is the default" - ) + 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) + self._mark_callback_reference_type(semantic_type) + return _CallbackArgumentSpec(semantic_type, False) + + def _prototype_address_argument_spec(self, node: ast.Call) -> _CallbackArgumentSpec: + """Parse the callback-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: + raise ValueError("Addr[...](...) is not supported inside prototype declarations; use Addr(T)") + semantic_type = self.semantic_type(node.args[0]) + if not self._is_primitive_scalar_value_type(semantic_type): + raise ValueError( + "Addr(...) inside prototype declarations is only valid for primitive scalar reference dummies; " + "arrays, strings, and wrapped objects already use reference storage" + ) self._mark_callback_reference_type(semantic_type) return _CallbackArgumentSpec(semantic_type, False) + @staticmethod + def _is_primitive_scalar_value_type(semantic_type: SemanticType) -> bool: + return bool( + semantic_type.rank == 0 + and semantic_type.name not in {"String", "Void"} + and (semantic_type.dtype or semantic_type.name) in SEMANTIC_SCALAR_TYPE_NAMES + and semantic_type.storage is None + and not _PyiAstParser._has_callback_descriptor_metadata(semantic_type) + ) + + @staticmethod + def _has_callback_descriptor_metadata(semantic_type: SemanticType) -> bool: + return any( + semantic_type.metadata.get(name) + for name in ( + "fortran_allocatable", + "fortran_pointer", + "fortran_polymorphic", + "fortran_assumed_type", + ) + ) + @staticmethod def _mark_callback_reference_type(semantic_type: SemanticType) -> None: storage = semantic_type.storage diff --git a/x2py/semantics/wrapper_policy.py b/x2py/semantics/wrapper_policy.py index 8fad3314a..8b9dc77b9 100644 --- a/x2py/semantics/wrapper_policy.py +++ b/x2py/semantics/wrapper_policy.py @@ -13,6 +13,7 @@ ADDRESS_ROLE_METADATA, ADDRESS_ROLE_RAW, BIND_TARGET_METADATA, + SCALAR_STORAGE_CATEGORY, SUPPRESS_DEFAULT_CONSTRUCTOR_METADATA, ) from x2py.semantics.native_array_handles import ( @@ -2165,6 +2166,13 @@ def _callback_transfer_blockers( """Reject callback forms whose typed adapter ABI is incomplete.""" semantic_type = argument.semantic_type blockers = list(_runtime_semantic_validation_blockers(semantic_type, f"callback argument {argument.name!r}")) + if argument.optional: + blockers.append(f"callback argument {argument.name!r} cannot be optional") + if _uses_unsupported_callback_descriptor(semantic_type): + blockers.append( + f"callback argument {argument.name!r} uses unsupported allocatable, pointer, " + "polymorphic, or assumed-type storage" + ) if transfer.passed_by_value and transfer.rank > 0: blockers.append(f"callback argument {argument.name!r} cannot pass an array by value") if semantic_type.name == "String": @@ -2175,13 +2183,7 @@ def _callback_transfer_blockers( blockers.append(f"callback argument {argument.name!r} has incomplete array shape") if semantic_type.name not in _PLAN_PRIMITIVE_SCALAR_TYPES: blockers.append(f"callback argument {argument.name!r} is an unsupported array of derived values") - elif transfer.derived_type_identity is not None: - if any( - semantic_type.metadata.get(name) - for name in ("fortran_allocatable", "fortran_pointer", "fortran_polymorphic") - ): - blockers.append(f"callback argument {argument.name!r} uses unsupported derived descriptor storage") - elif semantic_type.name not in _PLAN_PRIMITIVE_SCALAR_TYPES: + elif transfer.derived_type_identity is None and semantic_type.name not in _PLAN_PRIMITIVE_SCALAR_TYPES: blockers.append(f"callback argument {argument.name!r} has unsupported type {semantic_type.name!r}") return tuple(blockers) @@ -2240,6 +2242,8 @@ def _callback_result_blockers( return ("callback result is missing a completed semantic type",) transfer = result.transfer blockers = list(_runtime_semantic_validation_blockers(return_type, "callback result")) + if _uses_unsupported_callback_descriptor(return_type): + blockers.append("callback result uses unsupported allocatable, pointer, polymorphic, or assumed-type storage") if result.action is CallbackResultAction.REJECT_RESULT: blockers.append(f"callback result type {return_type.name!r} is unsupported") if result.action is CallbackResultAction.RETURN_ARRAY_ADDRESS: @@ -2247,13 +2251,22 @@ def _callback_result_blockers( blockers.append("callback array result requires a complete fixed shape") if return_type.name not in _PLAN_PRIMITIVE_SCALAR_TYPES: blockers.append("callback array result must contain a primitive scalar type") - if result.action is CallbackResultAction.RETURN_DERIVED_ADDRESS and any( - return_type.metadata.get(name) for name in ("fortran_allocatable", "fortran_pointer", "fortran_polymorphic") - ): - blockers.append("callback derived result uses unsupported descriptor storage") return tuple(blockers) +def _uses_unsupported_callback_descriptor(semantic_type: models.SemanticType) -> bool: + """Return whether callback lowering lacks the native descriptor ABI.""" + return any( + semantic_type.metadata.get(name) + for name in ( + "fortran_allocatable", + "fortran_pointer", + "fortran_polymorphic", + "fortran_assumed_type", + ) + ) + + def build_function_wrapper_policy( function: models.SemanticFunction, *, @@ -2751,6 +2764,11 @@ def _result_policies( ) scalar_descriptor = _scalar_descriptor_result_policy(function.return_type, decision) blockers = list(_result_blockers(function.return_type, decision)) + if scalar_descriptor is not None and scalar_descriptor.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE: + blockers.append( + "direct allocatable scalar function results cannot preserve unallocated state; " + "use an allocatable hidden output projection" + ) bridge_data_action, bridge_copy_reason = _result_bridge_data_action(function.return_type) if bridge_data_action is BridgeDataAction.BLOCKED and decision.kind is not ObjectKind.SCALAR: blockers.append("result has no completed bridge data action") @@ -3775,7 +3793,7 @@ def _argument_shape_blockers( """Dispatch one argument to its scalar/string or array policy family.""" if decision.kind is ObjectKind.DERIVED_TYPE: return _derived_argument_shape_blockers(argument, decision, polymorphic) - if int(argument.semantic_type.rank or 0) > 0: + if decision.kind is ObjectKind.NUMPY_ARRAY: return _array_argument_shape_blockers(argument, decision) return _scalar_or_string_argument_shape_blockers(argument, decision) @@ -3970,6 +3988,8 @@ def _array_boundary_blockers( ) -> tuple[str, ...]: """Dispatch one completed array boundary without backend inference.""" action = decision.python_barrier_action + if action is PythonBarrierAction.SCALAR_STORAGE: + return _scalar_storage_array_boundary_blockers(argument, decision) if action is PythonBarrierAction.ARRAY_STORAGE: return _array_storage_boundary_blockers(argument, decision) if action is PythonBarrierAction.RAW_ADDRESS: @@ -3979,6 +3999,51 @@ def _array_boundary_blockers( return (f"argument {argument.name!r} has unsupported array Python action {action.value}",) +def _scalar_storage_array_boundary_blockers( + argument: models.SemanticArgument, + decision: OwnershipDecision, +) -> tuple[str, ...]: + """Require one rank-zero NumPy storage handoff to a scalar native dummy.""" + blockers = [] + if decision.owner is not OwnershipOwner.CALLER: + blockers.append(f"argument {argument.name!r} scalar-storage owner is {decision.owner.value}, not caller") + expected_transfer = TransferMode.IN_PLACE if decision.mutates_native else TransferMode.CALL_LOCAL + if decision.transfer is not expected_transfer: + blockers.append( + f"argument {argument.name!r} scalar-storage transfer is " + f"{decision.transfer.value}, not {expected_transfer.value}" + ) + expected_destruction = DestructionPolicy.CALLER if decision.mutates_native else DestructionPolicy.NONE + if decision.destruction is not expected_destruction: + blockers.append( + f"argument {argument.name!r} scalar-storage destruction is " + f"{decision.destruction.value}, not {expected_destruction.value}" + ) + if decision.storage_mode is not StorageMode.STACK: + blockers.append( + f"argument {argument.name!r} scalar-storage storage is {decision.storage_mode.value}, not stack" + ) + if (decision.boundary_storage_mode or decision.storage_mode) is not StorageMode.STACK: + blockers.append(f"argument {argument.name!r} scalar-storage boundary storage is not stack") + if decision.native_barrier_action is not NativeBarrierAction.PASS_STORAGE_ADDRESS: + blockers.append(f"argument {argument.name!r} scalar storage does not use its storage address") + if decision.codegen_action not in { + CodegenAction.CALL_LOCAL_INPUT, + CodegenAction.IN_PLACE_ARGUMENT, + CodegenAction.IDENTITY_OUTPUT, + }: + blockers.append( + f"argument {argument.name!r} scalar-storage action is " + f"{decision.codegen_action.value}, not a storage-address action" + ) + if decision.descriptor_boundary: + blockers.append(f"argument {argument.name!r} scalar storage must be non-descriptor storage") + array_policy = _array_handoff_policy(argument.semantic_type) + if not _is_scalar_storage_array_policy(array_policy): + blockers.append(f"argument {argument.name!r} is not rank-zero scalar storage") + return tuple(blockers) + + def _array_storage_boundary_blockers( argument: models.SemanticArgument, decision: OwnershipDecision, @@ -4607,8 +4672,13 @@ def _ordinary_array_hidden_result_blockers( label = f"hidden result {argument.name!r}" blockers = list(_ordinary_array_result_blockers(argument.semantic_type, decision, label)) blockers = [item for item in blockers if " native action is " not in item] - if decision.native_barrier_action is not NativeBarrierAction.PASS_ARRAY_BUFFER: - blockers.append(f"{label} native action is {decision.native_barrier_action.value}, not array buffer") + expected_native = ( + NativeBarrierAction.PASS_STORAGE_ADDRESS + if _is_scalar_storage_array_policy(_array_handoff_policy(argument.semantic_type)) + else NativeBarrierAction.PASS_ARRAY_BUFFER + ) + if decision.native_barrier_action is not expected_native: + blockers.append(f"{label} native action is {decision.native_barrier_action.value}, not {expected_native.value}") if decision.python_visible or not decision.projects_result: blockers.append(f"{label} projection visibility is inconsistent") if not isinstance(mapping.native_position, int): @@ -4865,11 +4935,18 @@ def _is_first_lane_scalar_type(semantic_type: models.SemanticType) -> bool: scalar_name = semantic_type.dtype or semantic_type.name return bool( int(semantic_type.rank or 0) == 0 + and not _is_scalar_storage_type(semantic_type) and semantic_type.name != "String" and scalar_name in _PLAN_PRIMITIVE_SCALAR_TYPES ) +def _is_scalar_storage_type(semantic_type: models.SemanticType) -> bool: + 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) + + def _is_plan_string_value_type(semantic_type: models.SemanticType) -> bool: """Return whether one semantic type is a scalar Python string value.""" return bool(int(semantic_type.rank or 0) == 0 and semantic_type.name == "String") @@ -5586,6 +5663,18 @@ def _array_argument_bridge_data_action( optional_mode: OptionalMode, ) -> tuple[BridgeDataAction, str | None]: """Complete one buffer, raw-address, or native-descriptor bridge view.""" + if ( + optional_mode in {OptionalMode.REQUIRED, OptionalMode.NULLABLE_VALUE} + and decision.python_barrier_action is PythonBarrierAction.SCALAR_STORAGE + and decision.native_barrier_action is NativeBarrierAction.PASS_STORAGE_ADDRESS + and decision.codegen_action + in { + CodegenAction.CALL_LOCAL_INPUT, + CodegenAction.IN_PLACE_ARGUMENT, + CodegenAction.IDENTITY_OUTPUT, + } + ): + return BridgeDataAction.ASSOCIATE_VIEW, None if ( optional_mode is OptionalMode.REQUIRED and decision.python_barrier_action is PythonBarrierAction.ARRAY_STORAGE @@ -5773,6 +5862,8 @@ def _argument_handoff_mode(decision: OwnershipDecision) -> ArgumentHandoffMode: return ArgumentHandoffMode.OPAQUE_ADDRESS if decision.python_barrier_action is PythonBarrierAction.RAW_ADDRESS: return ArgumentHandoffMode.OPAQUE_ADDRESS + if decision.python_barrier_action is PythonBarrierAction.SCALAR_STORAGE: + return ArgumentHandoffMode.OPAQUE_ADDRESS if decision.native_barrier_action is NativeBarrierAction.PASS_NATIVE_DESCRIPTOR: return ArgumentHandoffMode.NATIVE_DESCRIPTOR if decision.kind is ObjectKind.NUMPY_ARRAY: @@ -5803,9 +5894,11 @@ def _array_handoff_policy(semantic_type: models.SemanticType) -> ArrayHandoffPol array = storage.array if storage is not None else None if array is None: return None + if semantic_type.name == "String" and array.category == SCALAR_STORAGE_CATEGORY: + return None assumed_rank = array.category == "assumed_rank" rank = _array_handoff_rank(semantic_type, array.rank, assumed_rank) - if rank is not None and rank <= 0: + if rank is not None and rank <= 0 and not (rank == 0 and array.category == SCALAR_STORAGE_CATEGORY): return None shape = tuple(str(item) for item in (array.shape or semantic_type.shape)) axes = tuple(str(item) for item in array.axes) @@ -5815,7 +5908,7 @@ def _array_handoff_policy(semantic_type: models.SemanticType) -> ArrayHandoffPol axes=axes, order=_array_handoff_order(array.order, assumed_rank), native_order=_array_handoff_native_order(array.order, array.copy_order, assumed_rank), - contiguous=_array_handoff_contiguous(array.contiguous, assumed_rank), + contiguous=_array_handoff_contiguous(array.contiguous, assumed_rank, array.category), itemsize=_array_handoff_itemsize(semantic_type), category=array.category, extent_references=tuple(_array_extent_references(item) for item in shape), @@ -5851,8 +5944,10 @@ def _array_handoff_native_order( return copy_order if copy_order is not None else order -def _array_handoff_contiguous(contiguous: bool | None, assumed_rank: bool) -> bool | None: +def _array_handoff_contiguous(contiguous: bool | None, assumed_rank: bool, category: str | None) -> bool | None: """Default assumed-rank handoff to one contiguous native buffer.""" + if category == SCALAR_STORAGE_CATEGORY and contiguous is None: + return True if assumed_rank and contiguous is None: return True return contiguous @@ -5874,13 +5969,15 @@ def _is_phase6_ordinary_array_type(semantic_type: models.SemanticType) -> bool: return False storage = semantic_type.storage array = storage.array if storage is not None else None + scalar_storage = _is_scalar_storage_array_policy(array_policy) + supported_element = semantic_type.name in _PLAN_PRIMITIVE_SCALAR_TYPES or ( + semantic_type.name == "String" and array_policy.itemsize is not None and not scalar_storage + ) + supported_rank = array_policy.rank is None or 1 <= array_policy.rank <= 15 or scalar_storage return bool( array is not None - and ( - semantic_type.name in _PLAN_PRIMITIVE_SCALAR_TYPES - or (semantic_type.name == "String" and array_policy.itemsize is not None) - ) - and (array_policy.rank is None or 1 <= array_policy.rank <= 15) + and supported_element + and supported_rank and (array_policy.rank is None or len(array_policy.shape) == array_policy.rank) and (array_policy.rank is None or len(array_policy.axes) == array_policy.rank) and not array.allocatable @@ -5888,6 +5985,12 @@ def _is_phase6_ordinary_array_type(semantic_type: models.SemanticType) -> bool: ) +def _is_scalar_storage_array_policy(array_policy: ArrayHandoffPolicy | None) -> bool: + return bool( + array_policy is not None and array_policy.rank == 0 and array_policy.category == SCALAR_STORAGE_CATEGORY + ) + + def _is_phase6_raw_array_address_type(semantic_type: models.SemanticType) -> bool: """Return whether one type is a supported concrete raw array pointee.""" if not _is_raw_array_address_type(semantic_type): diff --git a/x2py/wrapper_codegen/c/binding.py b/x2py/wrapper_codegen/c/binding.py index 3c2bf7974..6e7224600 100644 --- a/x2py/wrapper_codegen/c/binding.py +++ b/x2py/wrapper_codegen/c/binding.py @@ -11,6 +11,7 @@ PythonBarrierAction, SetterAction, ) +from x2py.semantics.metadata import SCALAR_STORAGE_CATEGORY from x2py.semantics.wrapper_policy import ( ArgumentHandoffMode, BridgeDataAction, @@ -392,8 +393,26 @@ def _require_array_argument_supported(self, argument: ArgumentTransferPlan) -> N if argument.binding.python_action is PythonBarrierAction.RAW_ADDRESS: self._require_raw_array_argument_supported(argument) return + if argument.binding.python_action is PythonBarrierAction.SCALAR_STORAGE: + self._require_scalar_storage_array_argument_supported(argument) + return self._require_array_buffer_argument_supported(argument) + def _require_scalar_storage_array_argument_supported(self, argument: ArgumentTransferPlan) -> None: + """Require one rank-zero NumPy storage handoff to a scalar native dummy.""" + array = argument.array + if not self._is_scalar_storage_array(array): + raise ValueError(f"Unsupported C scalar-storage array rank for {argument.owner_path!r}") + if argument.bridge.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: + raise ValueError(f"Unsupported C scalar-storage handoff for {argument.owner_path!r}") + if argument.bridge.data_action is not BridgeDataAction.ASSOCIATE_VIEW: + raise ValueError(f"Unsupported C scalar-storage data action for {argument.owner_path!r}") + PrimitiveScalarTypeRegistry.type_for(argument.semantic_type_name) + + @staticmethod + def _is_scalar_storage_array(array) -> bool: + return bool(array is not None and array.rank == 0 and array.category == SCALAR_STORAGE_CATEGORY) + def _require_native_array_handle_argument_supported(self, argument: ArgumentTransferPlan) -> None: """Require one typed standard-descriptor argument handoff.""" handle = argument.native_array_handle @@ -1775,12 +1794,12 @@ def _derived_origin_declarations(self, plan: ModulePlan) -> tuple: CDeclaration( self._derived_origin_active_name(variable), "static atomic_bool", - CodeExpression("ATOMIC_VAR_INIT(false)"), + CodeExpression("false"), ), CDeclaration( self._derived_origin_poisoned_name(variable), "static atomic_bool", - CodeExpression("ATOMIC_VAR_INIT(false)"), + CodeExpression("false"), ), ) ) @@ -7358,6 +7377,8 @@ def _lower_argument_nullable_value( context: _CFunctionContext, ) -> tuple[CDeclaration | CExpressionStatement | CIf, ...]: """Return omitted-or-value conversion nodes for an optional value.""" + if plan.binding.python_action is PythonBarrierAction.SCALAR_STORAGE: + return self._lower_argument_nullable_scalar_storage(plan, context) if plan.object_kind is ObjectKind.NUMPY_ARRAY: return self._lower_argument_nullable_array_storage(plan, context) if plan.object_kind is ObjectKind.STRING: @@ -7368,8 +7389,6 @@ def _lower_argument_nullable_value( raise ValueError( f"Unsupported optional C argument object kind for {plan.owner_path!r}: {plan.object_kind!r}" ) - if plan.binding.python_action is PythonBarrierAction.SCALAR_STORAGE: - return self._lower_argument_nullable_scalar_storage(plan, context) scalar_type = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) names = context.arguments[plan.owner_path] return ( @@ -7758,8 +7777,16 @@ def _lower_result_array_copy( self._array_extent_expression(handoff, axis, expression, context) for axis, expression in enumerate(handoff.shape) ) - dims_name = f"{python_name}_dims" - fortran_order = 0 if handoff.order == "ORDER_C" or handoff.rank == 1 else 1 + dimension_declarations: tuple[CDeclaration, ...] + if handoff.rank == 0: + dims_name = "NULL" + dimension_declarations = () + else: + dims_name = f"{python_name}_dims" + dimension_declarations = ( + CDeclaration(f"{dims_name}[]", "npy_intp", CodeExpression(f"{{{', '.join(dimensions)}}}")), + ) + fortran_order = 0 if handoff.order == "ORDER_C" or handoff.rank <= 1 else 1 base_name = f"{python_name}_base" decrefs = tuple(CExpressionStatement(CodeExpression(f"Py_DECREF({name})")) for name in failure_cleanup) return ( @@ -7775,7 +7802,7 @@ def _lower_result_array_copy( CReturn(CodeExpression("NULL")), ), ), - CDeclaration(f"{dims_name}[]", "npy_intp", CodeExpression(f"{{{', '.join(dimensions)}}}")), + *dimension_declarations, CDeclaration( python_name, "PyObject *", diff --git a/x2py/wrapper_codegen/fortran/bridge.py b/x2py/wrapper_codegen/fortran/bridge.py index fd7116b04..bf5933bbd 100644 --- a/x2py/wrapper_codegen/fortran/bridge.py +++ b/x2py/wrapper_codegen/fortran/bridge.py @@ -12,6 +12,7 @@ PythonBarrierAction, SetterAction, ) +from x2py.semantics.metadata import SCALAR_STORAGE_CATEGORY from x2py.semantics.wrapper_policy import ( ArgumentHandoffMode, BridgeDataAction, @@ -284,8 +285,24 @@ def _require_array_argument_supported(self, argument: ArgumentTransferPlan) -> N if argument.binding.python_action is PythonBarrierAction.RAW_ADDRESS: self._require_raw_array_argument_supported(argument) return + if argument.binding.python_action is PythonBarrierAction.SCALAR_STORAGE: + self._require_scalar_storage_array_argument_supported(argument) + return self._require_array_buffer_argument_supported(argument) + def _require_scalar_storage_array_argument_supported(self, argument: ArgumentTransferPlan) -> None: + """Require one rank-zero NumPy storage handoff to a scalar native dummy.""" + array = argument.array + if not self._is_scalar_storage_array(array): + raise ValueError(f"Unsupported Fortran scalar-storage array rank for {argument.owner_path!r}") + if argument.bridge.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: + raise ValueError(f"Unsupported Fortran scalar-storage handoff for {argument.owner_path!r}") + if argument.bridge.native_action is not NativeBarrierAction.PASS_STORAGE_ADDRESS: + raise ValueError(f"Unsupported Fortran scalar-storage native action for {argument.owner_path!r}") + if argument.bridge.data_action is not BridgeDataAction.ASSOCIATE_VIEW: + raise ValueError(f"Unsupported Fortran scalar-storage data action for {argument.owner_path!r}") + PrimitiveScalarTypeRegistry.type_for(argument.semantic_type_name) + def _require_native_array_handle_argument_supported(self, argument: ArgumentTransferPlan) -> None: """Require one typed standard-descriptor bridge argument.""" handle = argument.native_array_handle @@ -357,7 +374,11 @@ def _require_array_plan_result_supported(self, result: ResultPlan) -> None: def _require_array_plan_result_shape_supported(self, result: ResultPlan) -> None: """Require one fixed-rank non-C-oriented direct result shape.""" array = result.array - if array is None or array.rank is None or not 1 <= array.rank <= 15: + if ( + array is None + or array.rank is None + or (not 1 <= array.rank <= 15 and not self._is_scalar_storage_array(array)) + ): raise ValueError(f"Unsupported Fortran array result rank for {result.owner_path!r}") if array.native_order == "ORDER_C" and array.rank > 1: raise ValueError(f"Unsupported Fortran array result order for {result.owner_path!r}") @@ -404,7 +425,11 @@ def _require_owned_native_array_result_supported(self, result: ResultPlan) -> No def _require_array_result_shape_supported(self, slot: NativeCallSlotPlan) -> None: """Require one fixed-rank non-C-oriented array result shape.""" array = slot.array - if array is None or array.rank is None or not 1 <= array.rank <= 15: + if ( + array is None + or array.rank is None + or (not 1 <= array.rank <= 15 and not self._is_scalar_storage_array(array)) + ): raise ValueError(f"Unsupported Fortran array output rank for {slot.owner_path!r}") if array.native_order == "ORDER_C" and array.rank > 1: raise ValueError(f"Unsupported Fortran array output order for {slot.owner_path!r}") @@ -659,16 +684,7 @@ def _derived_holder_definitions(self, plan: ModulePlan) -> tuple[FortranTypeDefi def _visit_NamespacePlan(self, plan: NamespacePlan) -> tuple[FortranFunction, ...]: """Return bridge procedures directly owned by one Python namespace.""" return ( - *( - procedure - for function in plan.functions - for procedure in ( - self.visit(function), - *self._scalar_descriptor_result_collectors(function), - *self._allocatable_array_result_collectors(function), - *self._allocatable_derived_result_collectors(function), - ) - ), + *(self.visit(function) for function in plan.functions), *(procedure for variable in plan.variables for procedure in self.visit(variable)), ) @@ -2738,10 +2754,28 @@ def _lower_array_argument( raise ValueError(f"Unsupported Fortran array presence mode for {plan.owner_path!r}: {mode!r}") if plan.bridge.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: return self._lower_argument_array_buffer(plan) - if mode is OptionalMode.REQUIRED and plan.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: + if plan.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: + return self._lower_opaque_array_argument(plan, mode) + raise ValueError(f"Unsupported Fortran array handoff for {plan.owner_path!r}: {plan.bridge.handoff_mode!r}") + + def _lower_opaque_array_argument( + self, + plan: ArgumentTransferPlan, + mode: OptionalMode, + ) -> tuple[FortranParameter, ...]: + """Lower raw array addresses and rank-zero scalar-storage arrays.""" + if mode is OptionalMode.REQUIRED and self._is_opaque_array_required_argument(plan): return self._lower_argument_required_opaque_address(plan) + if mode is OptionalMode.NULLABLE_VALUE and self._is_scalar_storage_array(plan.array): + return self._lower_argument_nullable_value(plan) raise ValueError(f"Unsupported Fortran array handoff for {plan.owner_path!r}: {plan.bridge.handoff_mode!r}") + def _is_opaque_array_required_argument(self, plan: ArgumentTransferPlan) -> bool: + """Return whether a required array-shaped argument uses an opaque address.""" + return bool( + plan.binding.python_action is PythonBarrierAction.RAW_ADDRESS or self._is_scalar_storage_array(plan.array) + ) + # Native-array-handle bridge parameters. def _lower_argument_native_array_descriptor( self, @@ -3183,30 +3217,10 @@ def _native_result_expression_invocation( ) -> FortranAssignment | FortranCall | FortranPointerAssignment: """Store one completed native result expression through its handoff leaf.""" direct_result = self._direct_result(plan) - collector = self._native_result_collector_name(plan, direct_result) - if collector is not None: - return FortranCall( - collector, - (CodeExpression(expression), CodeExpression(result_name)), - ) if self._uses_pointer_result_assignment(direct_result): return FortranPointerAssignment(result_name, CodeExpression(expression)) return FortranAssignment(result_name, CodeExpression(expression)) - def _native_result_collector_name( - self, - plan: FunctionPlan, - result: ResultPlan | None, - ) -> str | None: - """Return the preselected nullable-result collector, when required.""" - if self._is_allocatable_scalar_descriptor_result(result): - return self._scalar_descriptor_result_collector_name(plan) - if result is not None and self._is_owned_native_array_result(result): - return self._allocatable_array_result_collector_name(plan) - if self._is_allocatable_derived_holder_result(result): - return self._allocatable_derived_result_collector_name(plan) - return None - def _uses_pointer_result_assignment(self, result: ResultPlan | None) -> bool: """Return whether the completed result keeps native pointer association.""" if self._is_pointer_derived_holder_result(result): @@ -3237,76 +3251,6 @@ def _native_invocation_target( expression = replacements.get(receiver.owner_path, self._native_argument_expression(receiver)) return f"{expression}%{class_call.type_bound_name}", receiver.native_call_slot.native_position - def _allocatable_derived_result_collectors( - self, - plan: FunctionPlan, - ) -> tuple[FortranFunction, ...]: - """Capture an allocatable function result before its temporary expires.""" - result = self._direct_result(plan) - if not self._is_allocatable_derived_holder_result(result): - return () - native_type = f"type({self._derived_native_alias(result.derived.backend_symbol)})" - return ( - FortranFunction( - name=self._allocatable_derived_result_collector_name(plan), - parameters=( - FortranParameter("value", native_type, ("allocatable", "intent(in)")), - FortranParameter("target", native_type, ("allocatable", "intent(out)")), - ), - body=( - FortranIf( - CodeExpression("allocated(value)"), - body=(FortranAssignment("target", CodeExpression("value")),), - ), - ), - is_subroutine=True, - ), - ) - - def _allocatable_array_result_collectors( - self, - plan: FunctionPlan, - ) -> tuple[FortranFunction, ...]: - """Capture an allocatable array result without referencing an absent payload.""" - result = self._direct_result(plan) - if result is None or not self._is_owned_native_array_result(result): - return () - handle = result.native_array_handle - if handle is None or handle.array.rank is None: - raise ValueError(f"Owned result {result.owner_path!r} has no descriptor rank") - attributes = ("allocatable", self._array_dimension_attribute(handle.array.rank)) - element_type = self._array_result_element_type(result) - return ( - FortranFunction( - name=self._allocatable_array_result_collector_name(plan), - parameters=( - FortranParameter("value", element_type, (*attributes, "intent(in)")), - FortranParameter("target", element_type, (*attributes, "intent(out)")), - ), - body=( - FortranIf( - CodeExpression("allocated(value)"), - body=(FortranAssignment("target", CodeExpression("value")),), - ), - ), - is_subroutine=True, - ), - ) - - @staticmethod - def _allocatable_array_result_collector_name(plan: FunctionPlan) -> str: - """Return the stable collector name for one owned array result.""" - return f"x2py_collect_{plan.symbol_name}_allocatable_array_result" - - @staticmethod - def _is_allocatable_derived_holder_result(result: ResultPlan | None) -> bool: - return bool( - result is not None - and result.object_kind is ObjectKind.DERIVED_TYPE - and result.derived is not None - and result.derived.storage is DerivedObjectStorage.ALLOCATABLE_HOLDER - ) - @staticmethod def _is_pointer_derived_holder_result(result: ResultPlan | None) -> bool: return bool( @@ -3316,45 +3260,6 @@ def _is_pointer_derived_holder_result(result: ResultPlan | None) -> bool: and result.derived.storage is DerivedObjectStorage.POINTER_HOLDER ) - @staticmethod - def _allocatable_derived_result_collector_name(plan: FunctionPlan) -> str: - return f"x2py_collect_{plan.symbol_name}_allocatable_derived_result" - - # Nullable rank-zero allocatable result collection. - def _scalar_descriptor_result_collectors( - self, - plan: FunctionPlan, - ) -> tuple[FortranFunction, ...]: - """Preserve an unallocated direct scalar result before copy-out.""" - result = self._direct_result(plan) - if not self._is_allocatable_scalar_descriptor_result(result): - return () - element_type = ( - "character(kind=c_char, len=:)" - if result.object_kind is ObjectKind.STRING - else PrimitiveScalarTypeRegistry.type_for(result.semantic_type_name).fortran_spelling - ) - return ( - FortranFunction( - name=self._scalar_descriptor_result_collector_name(plan), - parameters=( - FortranParameter("value", element_type, ("allocatable", "intent(in)")), - FortranParameter("target", element_type, ("allocatable", "intent(out)")), - ), - body=( - FortranIf( - CodeExpression("allocated(value)"), - body=(FortranAssignment("target", CodeExpression("value")),), - ), - ), - is_subroutine=True, - ), - ) - - def _scalar_descriptor_result_collector_name(self, plan: FunctionPlan) -> str: - """Return the stable helper name for one rank-zero allocatable result.""" - return f"x2py_collect_{plan.symbol_name}_scalar_descriptor_result" - def _native_arguments( self, plan: FunctionPlan, @@ -3652,7 +3557,10 @@ def _opaque_address_declarations(self, plan: FunctionPlan) -> tuple[FortranDecla and argument.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS and argument.bridge.data_action in {BridgeDataAction.ASSOCIATE_VIEW, BridgeDataAction.COPY_REPRESENTATION} - and argument.object_kind in {ObjectKind.SCALAR, ObjectKind.DERIVED_TYPE} + and ( + argument.object_kind in {ObjectKind.SCALAR, ObjectKind.DERIVED_TYPE} + or self._is_scalar_storage_array(argument.array) + ) ) for declaration in ( self._derived_argument_declarations(argument) @@ -3699,7 +3607,10 @@ def _opaque_address_initializers( and argument.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS and argument.bridge.data_action in {BridgeDataAction.ASSOCIATE_VIEW, BridgeDataAction.COPY_REPRESENTATION} - and argument.object_kind in {ObjectKind.SCALAR, ObjectKind.DERIVED_TYPE} + and ( + argument.object_kind in {ObjectKind.SCALAR, ObjectKind.DERIVED_TYPE} + or self._is_scalar_storage_array(argument.array) + ) ) for node in self._opaque_address_initializer_nodes(argument) ) @@ -4416,6 +4327,11 @@ def _direct_array_result_declarations( """Declare typed native and contiguous-copy storage for one array result.""" shape = self._array_result_shape(plan, result) element_type = self._array_result_element_type(result) + if self._is_scalar_storage_array(result.array): + return ( + FortranDeclaration("result_value", element_type), + FortranDeclaration("result_copy", element_type, ("pointer",)), + ) copy_type = "character(kind=c_char)" if result.datatype_family is DatatypeFamily.STRING else element_type return ( FortranDeclaration("result_value", element_type, (f"dimension({', '.join(shape)})",)), @@ -4543,6 +4459,12 @@ def _array_copy_output_declarations( if slot.semantic_type_name is None: raise ValueError(f"Missing array output datatype for {slot.owner_path!r}") element_type = self._array_result_element_type(slot) + if self._is_scalar_storage_array(slot.array): + name = slot.native_name.lower() + return ( + FortranDeclaration(f"{name}_value", element_type), + FortranDeclaration(f"{name}_copy", element_type, ("pointer",)), + ) copy_type = "character(kind=c_char)" if slot.datatype_family is DatatypeFamily.STRING else element_type name = slot.native_name.lower() return ( @@ -4763,8 +4685,15 @@ def _fixed_array_copy_nodes( copy_name: str, ) -> tuple[FortranAssignment | FortranIf, ...]: """Allocate and fill one detached contiguous ordinary-array copy.""" - if rank is None or rank <= 0: + if rank is None or rank < 0: raise ValueError(f"Array copy {value_name!r} requires a fixed positive rank") + if rank == 0: + return self._fixed_scalar_storage_copy_nodes( + itemsize, + target_name=target_name, + value_name=value_name, + copy_name=copy_name, + ) if order == "ORDER_C" and rank > 1: raise ValueError(f"Array copy {value_name!r} requires Fortran element order") if itemsize is not None: @@ -4798,6 +4727,37 @@ def _fixed_array_copy_nodes( ), ) + def _fixed_scalar_storage_copy_nodes( + self, + itemsize: int | None, + *, + target_name: str, + value_name: str, + copy_name: str, + ) -> tuple[FortranAssignment | FortranIf, ...]: + """Allocate and fill one detached copy for a rank-zero NumPy result.""" + if itemsize is not None: + raise ValueError(f"Scalar-storage copy {value_name!r} does not support character itemsize") + return ( + FortranAssignment( + target_name, + CodeExpression(f"c_malloc(max(1_c_size_t, c_sizeof({value_name})))"), + ), + FortranIf( + CodeExpression(f"c_associated({target_name})"), + body=( + FortranCall( + "c_f_pointer", + ( + CodeExpression(target_name), + CodeExpression(copy_name), + ), + ), + FortranAssignment(copy_name, CodeExpression(value_name)), + ), + ), + ) + def _fixed_character_array_copy_nodes( self, itemsize: int, @@ -4955,15 +4915,6 @@ def _owned_direct_result(self, plan: FunctionPlan) -> ResultPlan | None: result = self._direct_result(plan) return result if result is not None and self._is_owned_native_array_result(result) else None - @staticmethod - def _is_allocatable_scalar_descriptor_result(result: ResultPlan | None) -> bool: - """Return whether a direct rank-zero result must preserve unallocated state.""" - return ( - result is not None - and result.scalar_descriptor is not None - and result.scalar_descriptor.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE - ) - @staticmethod def _is_owned_native_array_result(result: ResultPlan) -> bool: """Return whether one result owns persistent standard-descriptor storage.""" @@ -6803,6 +6754,8 @@ def _external_interface_array_result_parameter( """Declare one completed ordinary or descriptor array output.""" if slot.array is None: raise ValueError(f"Array output {slot.owner_path!r} has no shape plan") + if self._is_scalar_storage_array(slot.array): + return FortranParameter(slot.native_name.lower(), self._array_result_element_type(slot)) attributes = [] if slot.native_array_handle is not None: attributes.append( @@ -6855,6 +6808,8 @@ def _native_result_type(self, plan: FunctionPlan, result: ResultPlan | None) -> scalar_type = PrimitiveScalarTypeRegistry.type_for(result.semantic_type_name) return f"{scalar_type.fortran_spelling}, {attribute}" if result.object_kind is ObjectKind.NUMPY_ARRAY: + if self._is_scalar_storage_array(result.array): + return self._array_result_element_type(result) shape = self._array_result_shape(plan, result) return f"{self._array_result_element_type(result)}, dimension({', '.join(shape)})" if result.object_kind is ObjectKind.STRING: @@ -6886,22 +6841,11 @@ def _external_interface_parameter( else () ) if argument.object_kind is ObjectKind.NUMPY_ARRAY: - array = argument.array - if array is None: - raise ValueError(f"Array argument {argument.owner_path!r} has no shape plan") - element_type = self._array_element_fortran_type(argument) - dimension = self._external_array_dimension(plan, argument) - if argument.native_array_handle is not None: - descriptor_attribute = ( - "allocatable" - if argument.native_array_handle.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE - else "pointer" - ) - attributes = (*attributes, descriptor_attribute) - return FortranParameter( + return self._external_interface_array_argument_parameter( + plan, + argument, parameter_name, - element_type, - (*attributes, f"dimension({dimension})"), + attributes, ) if argument.object_kind is ObjectKind.STRING: length = argument.native_call_slot.character_length @@ -6917,6 +6861,34 @@ def _external_interface_parameter( attributes, ) + def _external_interface_array_argument_parameter( + self, + plan: FunctionPlan, + argument: ArgumentTransferPlan, + parameter_name: str, + attributes: tuple[str, ...], + ) -> FortranParameter: + """Declare a native array or scalar-storage dummy from completed array facts.""" + array = argument.array + if array is None: + raise ValueError(f"Array argument {argument.owner_path!r} has no shape plan") + element_type = self._array_element_fortran_type(argument) + if self._is_scalar_storage_array(array): + return FortranParameter(parameter_name, element_type, attributes) + dimension = self._external_array_dimension(plan, argument) + if argument.native_array_handle is not None: + descriptor_attribute = ( + "allocatable" + if argument.native_array_handle.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE + else "pointer" + ) + attributes = (*attributes, descriptor_attribute) + return FortranParameter( + parameter_name, + element_type, + (*attributes, f"dimension({dimension})"), + ) + def _external_array_dimension(self, plan: FunctionPlan, argument: ArgumentTransferPlan) -> str: """Lower the completed native dummy shape without changing its ABI category.""" array = argument.array @@ -6945,6 +6917,10 @@ def _external_assumed_size_dimension(array: ArrayHandoffPlan, shape: list[str]) shape[-1] = "*" return ", ".join(shape) + @staticmethod + def _is_scalar_storage_array(array: ArrayHandoffPlan | None) -> bool: + return bool(array is not None and array.rank == 0 and array.category == SCALAR_STORAGE_CATEGORY) + # Ordinary-array result-shape lowering. def _array_result_shape(self, plan: FunctionPlan, result: ResultPlan) -> tuple[str, ...]: """Lower one result shape through the plan's native scalar roles.""" diff --git a/x2py/wrapper_codegen/generator.py b/x2py/wrapper_codegen/generator.py index 50c55bafb..1d8fe60c9 100644 --- a/x2py/wrapper_codegen/generator.py +++ b/x2py/wrapper_codegen/generator.py @@ -24,6 +24,7 @@ StorageMode, TransferMode, ) +from x2py.semantics.metadata import SCALAR_STORAGE_CATEGORY from x2py.semantics.wrapper_policy import ( ArgumentHandoffMode, BridgeDataAction, @@ -2277,12 +2278,50 @@ def _array_action_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Dispatch completed buffer or raw-address array actions.""" action = plan.binding.python_action + if action is PythonBarrierAction.SCALAR_STORAGE: + return self._scalar_storage_array_action_diagnostics(plan) if action is PythonBarrierAction.ARRAY_STORAGE: return self._array_buffer_action_diagnostics(plan) if action is PythonBarrierAction.RAW_ADDRESS: return self._raw_array_action_diagnostics(plan) return (self._diagnostic(plan.owner_path, "invalid-array-python-action", action.value),) + def _scalar_storage_array_action_diagnostics( + self, + plan: ArgumentTransferPlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate rank-zero NumPy storage passed as a scalar native address.""" + diagnostics = [] + if plan.bridge.native_action is not NativeBarrierAction.PASS_STORAGE_ADDRESS: + diagnostics.append( + self._diagnostic( + plan.owner_path, "invalid-scalar-storage-native-action", plan.bridge.native_action.value + ) + ) + if plan.bridge.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: + diagnostics.append( + self._diagnostic(plan.owner_path, "invalid-scalar-storage-handoff-mode", plan.bridge.handoff_mode.value) + ) + if plan.bridge.data_action is not BridgeDataAction.ASSOCIATE_VIEW: + diagnostics.append( + self._diagnostic(plan.owner_path, "invalid-scalar-storage-data-action", plan.bridge.data_action.value) + ) + if plan.binding.codegen_action not in { + CodegenAction.CALL_LOCAL_INPUT, + CodegenAction.IN_PLACE_ARGUMENT, + CodegenAction.IDENTITY_OUTPUT, + }: + diagnostics.append( + self._diagnostic( + plan.owner_path, + "invalid-scalar-storage-codegen-action", + plan.binding.codegen_action.value, + ) + ) + if not self._is_scalar_storage_array(plan.array): + diagnostics.append(self._diagnostic(plan.owner_path, "invalid-scalar-storage-array", plan.array)) + return tuple(diagnostics) + def _array_buffer_action_diagnostics( self, plan: ArgumentTransferPlan, @@ -2515,7 +2554,7 @@ def _concrete_rank_array_diagnostics( if array is None or array.rank is None: return () diagnostics = [] - if not 1 <= array.rank <= 15: + if not 1 <= array.rank <= 15 and not self._is_scalar_storage_array(array): diagnostics.append(self._diagnostic(plan.owner_path, "invalid-array-rank", array.rank)) if len(array.shape) != array.rank or len(array.axes) != array.rank: diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-array-rank", array.rank)) @@ -3479,7 +3518,9 @@ def _array_result_shape_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlan def _array_result_rank_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagnostic, ...]: """Require a supported concrete ordinary array result rank.""" array = plan.array - if array is not None and (array.rank is None or not 1 <= array.rank <= 15): + if array is not None and ( + array.rank is None or (not 1 <= array.rank <= 15 and not self._is_scalar_storage_array(array)) + ): return (self._diagnostic(plan.owner_path, "invalid-array-result-rank", array.rank),) return () @@ -3528,7 +3569,11 @@ def _array_result_source_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPla expected_native = NativeBarrierAction.NONE else: expected_action = CodegenAction.COPY_OUT - expected_native = NativeBarrierAction.PASS_ARRAY_BUFFER + expected_native = ( + NativeBarrierAction.PASS_STORAGE_ADDRESS + if self._is_scalar_storage_array(plan.array) + else NativeBarrierAction.PASS_ARRAY_BUFFER + ) diagnostics = [] if plan.binding.codegen_action is not expected_action: diagnostics.append( @@ -3540,6 +3585,10 @@ def _array_result_source_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPla ) return tuple(diagnostics) + @staticmethod + def _is_scalar_storage_array(array) -> bool: + return bool(array is not None and array.rank == 0 and array.category == SCALAR_STORAGE_CATEGORY) + def _native_slot_diagnostics(self, plan: NativeCallSlotPlan) -> tuple[WrapperPlanDiagnostic, ...]: """Return hidden literal and hidden result slot diagnostics.""" diagnostics = list( diff --git a/x2py/wrapper_codegen/printers/pyi_printer.py b/x2py/wrapper_codegen/printers/pyi_printer.py index 9f45f5458..342f37781 100644 --- a/x2py/wrapper_codegen/printers/pyi_printer.py +++ b/x2py/wrapper_codegen/printers/pyi_printer.py @@ -598,10 +598,16 @@ def _visible_scalar_descriptor_type(semantic_type: SemanticType) -> SemanticType return visible def _emit_prototype_argument(self, argument: SemanticArgument) -> str: - """Emit one prototype dummy with reference default and one value override.""" + """Emit one prototype dummy using the public callback transport rules.""" + if self._is_prototype_descriptor_type(argument.semantic_type): + return self._prototype_descriptor_type_text(argument.semantic_type) 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): + return inner return f"{self._contract('Value')}({inner})" + if self._is_prototype_primitive_reference(argument.semantic_type): + return f"{self._contract('Addr')}({inner})" return inner def _prototype_argument_inner_type(self, semantic_type: SemanticType) -> str: @@ -618,6 +624,51 @@ def _prototype_argument_inner_type(self, semantic_type: SemanticType) -> str: return self._address_target_type(semantic_type) return self._visit(semantic_type) + def _prototype_descriptor_type_text(self, semantic_type: SemanticType) -> str: + """Render descriptor metadata without a second reference wrapper.""" + if semantic_type.metadata.get("fortran_allocatable") or semantic_type.metadata.get("fortran_pointer"): + return self._visit(semantic_type) + visible = deepcopy(semantic_type) + if visible.storage is not None and visible.storage.kind in {"reference", "address", "pointer"}: + visible.storage = None + return self._visit(visible) + + @staticmethod + def _is_prototype_primitive_value(semantic_type: SemanticType) -> bool: + storage = semantic_type.storage + return bool( + semantic_type.rank == 0 + and semantic_type.name not in {"String", "Void"} + and (semantic_type.dtype or semantic_type.name) in SEMANTIC_SCALAR_TYPE_NAMES + and (storage is None or storage.kind == "value") + and not PyiPrinter._is_prototype_descriptor_type(semantic_type) + ) + + @staticmethod + def _is_prototype_primitive_reference(semantic_type: SemanticType) -> bool: + storage = semantic_type.storage + return bool( + semantic_type.rank == 0 + and semantic_type.name not in {"String", "Void"} + and (semantic_type.dtype or semantic_type.name) in SEMANTIC_SCALAR_TYPE_NAMES + and storage is not None + and storage.kind in {"reference", "address", "pointer"} + and storage.pointer_depth == 1 + and not PyiPrinter._is_prototype_descriptor_type(semantic_type) + ) + + @staticmethod + def _is_prototype_descriptor_type(semantic_type: SemanticType) -> bool: + return any( + semantic_type.metadata.get(name) + for name in ( + "fortran_allocatable", + "fortran_pointer", + "fortran_polymorphic", + "fortran_assumed_type", + ) + ) + def _emit_data_member(self, variable: SemanticVariable) -> str: """Emit a variable in class-field context rather than argument context.""" name = self._data_member_name(variable) From b816cbca0a02c00814bb5e7b05ec39addfb0ec5e Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 00:15:42 +0100 Subject: [PATCH 05/35] improve quality --- x2py/semantics/ownership.py | 173 +++++++++++++++++-------------- x2py/semantics/wrapper_policy.py | 103 +++++++++++------- 2 files changed, 162 insertions(+), 114 deletions(-) diff --git a/x2py/semantics/ownership.py b/x2py/semantics/ownership.py index 3ca9b2e6e..bbf334117 100644 --- a/x2py/semantics/ownership.py +++ b/x2py/semantics/ownership.py @@ -914,8 +914,50 @@ def _function_scalar_descriptor_decision( ) def _string_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: - if context.is_result and (facts.allocatable or facts.pointer): - storage = StorageMode.HEAP if facts.allocatable else StorageMode.ALIAS + descriptor_decision = self._string_descriptor_decision(facts, context) + if descriptor_decision is not None: + return descriptor_decision + if facts.address_role == ADDRESS_ROLE_RAW: + return OwnershipDecision( + ObjectKind.STRING, + OwnershipOwner.CALLER, + TransferMode.IN_PLACE, + DestructionPolicy.CALLER, + mutates_native=True, + reason="raw string address aliases caller-owned fixed-width storage", + ) + if facts.scalar_storage: + return self._scalar_string_storage_decision(context) + if context.is_result: + return OwnershipDecision( + ObjectKind.STRING, + OwnershipOwner.PYTHON, + TransferMode.COPY_RETURN, + DestructionPolicy.PYTHON_REFCOUNT, + reason="string output is copied into a Python string", + ) + if context.writes_argument and not context.reads_argument: + return self._string_output_argument_decision(context) + if context.writes_argument and context.reads_argument: + return self._string_update_argument_decision(context) + return OwnershipDecision( + ObjectKind.STRING, + OwnershipOwner.CALLER, + TransferMode.CALL_LOCAL, + DestructionPolicy.NONE, + reason="string input is converted for the call only", + ) + + @staticmethod + def _string_descriptor_decision( + facts: _StorageFacts, + context: OwnershipContext, + ) -> OwnershipDecision | None: + if not (facts.allocatable or facts.pointer): + return None + + storage = StorageMode.HEAP if facts.allocatable else StorageMode.ALIAS + if context.is_result: return OwnershipDecision( ObjectKind.STRING, OwnershipOwner.PYTHON, @@ -927,13 +969,7 @@ def _string_decision(self, facts: _StorageFacts, context: OwnershipContext) -> O descriptor_boundary=True, reason="scalar string descriptor result is copied before native descriptor release", ) - if ( - (facts.allocatable or facts.pointer) - and context.writes_argument - and context.projects_result - and not context.python_visible - ): - storage = StorageMode.HEAP if facts.allocatable else StorageMode.ALIAS + if context.writes_argument and context.projects_result and not context.python_visible: return OwnershipDecision( ObjectKind.STRING, OwnershipOwner.PYTHON, @@ -948,92 +984,75 @@ def _string_decision(self, facts: _StorageFacts, context: OwnershipContext) -> O python_visible=False, reason="hidden scalar string descriptor output is copied before native descriptor release", ) - if facts.address_role == ADDRESS_ROLE_RAW: + return None + + @staticmethod + def _scalar_string_storage_decision(context: OwnershipContext) -> OwnershipDecision: + if context.is_result: return OwnershipDecision( ObjectKind.STRING, - OwnershipOwner.CALLER, - TransferMode.IN_PLACE, - DestructionPolicy.CALLER, - mutates_native=True, - reason="raw string address aliases caller-owned fixed-width storage", + OwnershipOwner.PYTHON, + TransferMode.COPY_RETURN, + DestructionPolicy.PYTHON_REFCOUNT, + reason="scalar string storage result is copied into a Python string", ) - if facts.scalar_storage: - if context.is_result: - return OwnershipDecision( - ObjectKind.STRING, - OwnershipOwner.PYTHON, - TransferMode.COPY_RETURN, - DestructionPolicy.PYTHON_REFCOUNT, - reason="scalar string storage result is copied into a Python string", - ) - if context.writes_argument: - return OwnershipDecision( - ObjectKind.STRING, - OwnershipOwner.CALLER, - TransferMode.IN_PLACE, - DestructionPolicy.CALLER, - storage_mode=StorageMode.ALIAS, - mutates_native=True, - reason="rank-0 string storage mutates caller-provided NumPy bytes storage", - ) + if context.writes_argument: return OwnershipDecision( ObjectKind.STRING, OwnershipOwner.CALLER, - TransferMode.CALL_LOCAL, - DestructionPolicy.NONE, + TransferMode.IN_PLACE, + DestructionPolicy.CALLER, storage_mode=StorageMode.ALIAS, - reason="rank-0 string storage is borrowed for the duration of the call", - ) - if context.is_result: - return OwnershipDecision( - ObjectKind.STRING, - OwnershipOwner.PYTHON, - TransferMode.COPY_RETURN, - DestructionPolicy.PYTHON_REFCOUNT, - reason="string output is copied into a Python string", + mutates_native=True, + reason="rank-0 string storage mutates caller-provided NumPy bytes storage", ) - if context.writes_argument and not context.reads_argument: - if not context.projects_result: - return OwnershipDecision( - ObjectKind.STRING, - OwnershipOwner.TEMPORARY, - TransferMode.CALL_LOCAL, - DestructionPolicy.CALL_LOCAL, - mutates_native=True, - reason="identity string output uses temporary storage and discards native mutation", - ) + return OwnershipDecision( + ObjectKind.STRING, + OwnershipOwner.CALLER, + TransferMode.CALL_LOCAL, + DestructionPolicy.NONE, + storage_mode=StorageMode.ALIAS, + reason="rank-0 string storage is borrowed for the duration of the call", + ) + + @staticmethod + def _string_output_argument_decision(context: OwnershipContext) -> OwnershipDecision: + if not context.projects_result: return OwnershipDecision( ObjectKind.STRING, - OwnershipOwner.PYTHON, - TransferMode.COPY_RETURN, - DestructionPolicy.PYTHON_REFCOUNT, + OwnershipOwner.TEMPORARY, + TransferMode.CALL_LOCAL, + DestructionPolicy.CALL_LOCAL, mutates_native=True, - reason="string output is copied into a Python string", + reason="identity string output uses temporary storage and discards native mutation", ) - if context.writes_argument and context.reads_argument: - if not context.projects_result: - return OwnershipDecision( - ObjectKind.STRING, - OwnershipOwner.TEMPORARY, - TransferMode.CALL_LOCAL, - DestructionPolicy.CALL_LOCAL, - mutates_native=True, - reason="string update uses a mutable call-local copy and discards native mutation", - ) + return OwnershipDecision( + ObjectKind.STRING, + OwnershipOwner.PYTHON, + TransferMode.COPY_RETURN, + DestructionPolicy.PYTHON_REFCOUNT, + mutates_native=True, + reason="string output is copied into a Python string", + ) + + @staticmethod + def _string_update_argument_decision(context: OwnershipContext) -> OwnershipDecision: + if not context.projects_result: return OwnershipDecision( ObjectKind.STRING, - OwnershipOwner.PYTHON, - TransferMode.COPY_RETURN, - DestructionPolicy.PYTHON_REFCOUNT, + OwnershipOwner.TEMPORARY, + TransferMode.CALL_LOCAL, + DestructionPolicy.CALL_LOCAL, mutates_native=True, - reason="immutable Python strings use copy-in/copy-out replacement for updates", + reason="string update uses a mutable call-local copy and discards native mutation", ) return OwnershipDecision( ObjectKind.STRING, - OwnershipOwner.CALLER, - TransferMode.CALL_LOCAL, - DestructionPolicy.NONE, - reason="string input is converted for the call only", + OwnershipOwner.PYTHON, + TransferMode.COPY_RETURN, + DestructionPolicy.PYTHON_REFCOUNT, + mutates_native=True, + reason="immutable Python strings use copy-in/copy-out replacement for updates", ) def _array_decision(self, facts: _StorageFacts, context: OwnershipContext) -> OwnershipDecision: diff --git a/x2py/semantics/wrapper_policy.py b/x2py/semantics/wrapper_policy.py index 8b9dc77b9..517faf104 100644 --- a/x2py/semantics/wrapper_policy.py +++ b/x2py/semantics/wrapper_policy.py @@ -113,6 +113,18 @@ class BridgeDataAction(str, Enum): BLOCKED = "blocked" +_ARRAY_VALUE_OPTIONAL_MODES = frozenset({OptionalMode.REQUIRED, OptionalMode.NULLABLE_VALUE}) +_ARRAY_DESCRIPTOR_OPTIONAL_MODES = frozenset({OptionalMode.REQUIRED, OptionalMode.DESCRIPTOR}) +_ARRAY_VIEW_CODEGEN_ACTIONS = frozenset( + { + CodegenAction.CALL_LOCAL_INPUT, + CodegenAction.IN_PLACE_ARGUMENT, + CodegenAction.IDENTITY_OUTPUT, + } +) +_RAW_ARRAY_VIEW_CODEGEN_ACTIONS = frozenset({CodegenAction.CALL_LOCAL_INPUT, CodegenAction.IN_PLACE_ARGUMENT}) + + class WritebackPhase(str, Enum): """Ordered phases of one completed replacement writeback.""" @@ -5663,55 +5675,72 @@ def _array_argument_bridge_data_action( optional_mode: OptionalMode, ) -> tuple[BridgeDataAction, str | None]: """Complete one buffer, raw-address, or native-descriptor bridge view.""" - if ( - optional_mode in {OptionalMode.REQUIRED, OptionalMode.NULLABLE_VALUE} + if _scalar_storage_array_bridge_uses_view(decision, optional_mode): + return BridgeDataAction.ASSOCIATE_VIEW, None + if _copy_in_out_array_bridge_uses_view(decision, optional_mode): + return BridgeDataAction.ASSOCIATE_VIEW, None + native_descriptor_action = _native_descriptor_array_bridge_data_action(decision, optional_mode) + if native_descriptor_action is not None: + return native_descriptor_action, None + if _raw_array_address_bridge_uses_view(decision, optional_mode): + return BridgeDataAction.ASSOCIATE_VIEW, None + if _array_storage_bridge_uses_view(decision, optional_mode): + return BridgeDataAction.ASSOCIATE_VIEW, None + return BridgeDataAction.BLOCKED, None + + +def _scalar_storage_array_bridge_uses_view(decision: OwnershipDecision, optional_mode: OptionalMode) -> bool: + return ( + optional_mode in _ARRAY_VALUE_OPTIONAL_MODES and decision.python_barrier_action is PythonBarrierAction.SCALAR_STORAGE and decision.native_barrier_action is NativeBarrierAction.PASS_STORAGE_ADDRESS - and decision.codegen_action - in { - CodegenAction.CALL_LOCAL_INPUT, - CodegenAction.IN_PLACE_ARGUMENT, - CodegenAction.IDENTITY_OUTPUT, - } - ): - return BridgeDataAction.ASSOCIATE_VIEW, None - if ( + and decision.codegen_action in _ARRAY_VIEW_CODEGEN_ACTIONS + ) + + +def _copy_in_out_array_bridge_uses_view(decision: OwnershipDecision, optional_mode: OptionalMode) -> bool: + return ( optional_mode is OptionalMode.REQUIRED and decision.python_barrier_action is PythonBarrierAction.ARRAY_STORAGE and decision.native_barrier_action is NativeBarrierAction.PASS_ARRAY_BUFFER and decision.codegen_action is CodegenAction.COPY_IN_OUT and decision.transfer is TransferMode.COPY_RETURN - ): - return BridgeDataAction.ASSOCIATE_VIEW, None - if ( - optional_mode in {OptionalMode.REQUIRED, OptionalMode.DESCRIPTOR} - and decision.python_barrier_action is PythonBarrierAction.WRAPPER_INSTANCE - and decision.native_barrier_action is NativeBarrierAction.PASS_NATIVE_DESCRIPTOR - ): - if decision.codegen_action is CodegenAction.CALL_LOCAL_INPUT: - return BridgeDataAction.ASSOCIATE_VIEW, None - if decision.codegen_action is CodegenAction.IN_PLACE_ARGUMENT: - return BridgeDataAction.DIRECT_TRANSFER, None - if ( + ) + + +def _native_descriptor_array_bridge_data_action( + decision: OwnershipDecision, + optional_mode: OptionalMode, +) -> BridgeDataAction | None: + if optional_mode not in _ARRAY_DESCRIPTOR_OPTIONAL_MODES: + return None + if decision.python_barrier_action is not PythonBarrierAction.WRAPPER_INSTANCE: + return None + if decision.native_barrier_action is not NativeBarrierAction.PASS_NATIVE_DESCRIPTOR: + return None + if decision.codegen_action is CodegenAction.CALL_LOCAL_INPUT: + return BridgeDataAction.ASSOCIATE_VIEW + if decision.codegen_action is CodegenAction.IN_PLACE_ARGUMENT: + return BridgeDataAction.DIRECT_TRANSFER + return None + + +def _raw_array_address_bridge_uses_view(decision: OwnershipDecision, optional_mode: OptionalMode) -> bool: + return ( optional_mode is OptionalMode.REQUIRED and decision.python_barrier_action is PythonBarrierAction.RAW_ADDRESS and decision.native_barrier_action is NativeBarrierAction.PASS_RAW_ADDRESS - and decision.codegen_action in {CodegenAction.CALL_LOCAL_INPUT, CodegenAction.IN_PLACE_ARGUMENT} - ): - return BridgeDataAction.ASSOCIATE_VIEW, None - if ( - optional_mode in {OptionalMode.REQUIRED, OptionalMode.NULLABLE_VALUE} + and decision.codegen_action in _RAW_ARRAY_VIEW_CODEGEN_ACTIONS + ) + + +def _array_storage_bridge_uses_view(decision: OwnershipDecision, optional_mode: OptionalMode) -> bool: + return ( + optional_mode in _ARRAY_VALUE_OPTIONAL_MODES and decision.python_barrier_action is PythonBarrierAction.ARRAY_STORAGE and decision.native_barrier_action is NativeBarrierAction.PASS_ARRAY_BUFFER - and decision.codegen_action - in { - CodegenAction.CALL_LOCAL_INPUT, - CodegenAction.IN_PLACE_ARGUMENT, - CodegenAction.IDENTITY_OUTPUT, - } - ): - return BridgeDataAction.ASSOCIATE_VIEW, None - return BridgeDataAction.BLOCKED, None + and decision.codegen_action in _ARRAY_VIEW_CODEGEN_ACTIONS + ) # String bridge data policy. From e493b0c0036ca729c61cba5e46405ee4b42ba12e Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 00:39:20 +0100 Subject: [PATCH 06/35] fix errors --- tests/parser/c/generate_c_parser_goldens.py | 44 +++++++++++++++++++++ tests/pipeline/preprocessing/test_cli.py | 3 +- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/tests/parser/c/generate_c_parser_goldens.py b/tests/parser/c/generate_c_parser_goldens.py index 73a4aa105..6bead4ad3 100644 --- a/tests/parser/c/generate_c_parser_goldens.py +++ b/tests/parser/c/generate_c_parser_goldens.py @@ -106,6 +106,25 @@ def _is_project_location(location: object) -> bool: return isinstance(location, dict) and _is_project_filename(location.get("filename")) +def _project_source_line(filename: object, line: object) -> str | None: + if not _is_project_filename(filename) or not isinstance(line, int) or line <= 0: + return None + source = _C_DATA_DIR / filename + if not source.is_file(): + return None + try: + return source.read_text(encoding="utf-8").splitlines()[line - 1] + except IndexError: + return None + + +def _stable_project_location(location: dict) -> dict: + source_line = _project_source_line(location.get("filename"), location.get("line")) + if source_line is None: + return location + return {**location, "source_line": source_line} + + def _has_project_source_location(declaration: object) -> bool: return isinstance(declaration, dict) and _is_project_location(declaration.get("source_location")) @@ -202,8 +221,30 @@ def _system_declaration_reference(declaration: dict) -> str | None: return None +def _stable_bool_payload(qualifiers: object = None) -> dict: + return {"model": "CBool", "qualifiers": list(qualifiers or []), "source_text": "bool"} + + +def _stable_bool_type_payload(declaration: dict) -> dict | None: + if declaration.get("reference") == "bool": + return _stable_bool_payload() + if declaration.get("model") == "CBool": + return _stable_bool_payload(declaration.get("qualifiers")) + if ( + declaration.get("model") == "CTypedef" + and declaration.get("name") == "bool" + and not _is_project_location(declaration.get("source_location")) + ): + return _stable_bool_payload(declaration.get("qualifiers")) + return None + + def _stable_payload_value(value, symbols: dict[str, set[str]]): if isinstance(value, dict): + bool_payload = _stable_bool_type_payload(value) + if bool_payload is not None: + return bool_payload + reference = _system_declaration_reference(value) if reference is not None: return {"reference": _stable_payload_value(reference, symbols)} @@ -250,6 +291,9 @@ def _stable_payload_value(value, symbols: dict[str, set[str]]): "source_line": None, } continue + if _is_project_filename(filename): + stable[key] = _stable_project_location(nested) + continue stable[key] = _stable_payload_value(nested, symbols) return stable if isinstance(value, list): diff --git a/tests/pipeline/preprocessing/test_cli.py b/tests/pipeline/preprocessing/test_cli.py index 3ba1477cd..6f303326e 100644 --- a/tests/pipeline/preprocessing/test_cli.py +++ b/tests/pipeline/preprocessing/test_cli.py @@ -22,7 +22,8 @@ def test_cli_help_documents_exact_compiler_and_preprocessing_examples(): assert "Compiler used for preprocessing" in res.stdout assert "default: gfortran; cc with --language c" in " ".join(res.stdout.split()) assert "--compile-commands PATH" in res.stdout - assert "-D, --define NAME[=VALUE]" in res.stdout + assert "-D" in res.stdout + assert "--define NAME[=VALUE]" in res.stdout def test_cli_c_default_compiler_mode_accepts_include_dirs(tmp_path: Path): From f2e248f500646eebd48e52e6897a8db62b2fed23 Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 00:53:16 +0100 Subject: [PATCH 07/35] fix errors --- .../test_phase7_native_array_handles.py | 4 ++++ x2py/wrapper_codegen/fortran/bridge.py | 17 ++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/wrapper_codegen/test_phase7_native_array_handles.py b/tests/wrapper_codegen/test_phase7_native_array_handles.py index 985e7287a..a5e0a5539 100644 --- a/tests/wrapper_codegen/test_phase7_native_array_handles.py +++ b/tests/wrapper_codegen/test_phase7_native_array_handles.py @@ -261,6 +261,7 @@ def test_phase7_generated_artifacts_follow_one_typed_action_vocabulary(): assert "bound_values = (CFI_cdesc_t *)&bound_values_storage;" in optional_binding assert "result_value = native_make(n)" in bridge_source assert "result_value = native_make_matrix(n, m)" in bridge_source + assert "if (allocated(result_value)) then" in bridge_source assert "call move_alloc(result_value, result)" in bridge_source assert "character(kind=c_char, len=:), allocatable :: value_value" in bridge_source assert "result_itemsize" in c_source @@ -281,7 +282,10 @@ def test_phase7_numeric_owned_result_is_collected_before_persistent_descriptor_m assert "real(c_double), allocatable, dimension(:), intent(out) :: result" in procedure assert "real(c_double), allocatable, dimension(:) :: result_value" in procedure assert "result_value = native_make(n)" in procedure + assert "if (allocated(result_value)) then" in procedure assert "call move_alloc(result_value, result)" in procedure + assert "if (allocated(result)) then" in procedure + assert "deallocate(result)" in procedure assert "result = result_value" not in procedure assert "x2py_collect_make_allocatable_array_result" not in bridge_source diff --git a/x2py/wrapper_codegen/fortran/bridge.py b/x2py/wrapper_codegen/fortran/bridge.py index bf5933bbd..d14df92d5 100644 --- a/x2py/wrapper_codegen/fortran/bridge.py +++ b/x2py/wrapper_codegen/fortran/bridge.py @@ -4352,9 +4352,20 @@ def _direct_result_finalizers( if self._is_owned_deferred_character_result(result): return self._owned_deferred_character_copy_nodes(result, "result", "result_value", "result_copy") return ( - FortranCall( - "move_alloc", - (CodeExpression("result_value"), CodeExpression("result")), + FortranIf( + CodeExpression("allocated(result_value)"), + body=( + FortranCall( + "move_alloc", + (CodeExpression("result_value"), CodeExpression("result")), + ), + ), + else_body=( + FortranIf( + CodeExpression("allocated(result)"), + body=(FortranDeallocate("result"),), + ), + ), ), ) if result.object_kind is ObjectKind.NUMPY_ARRAY: From 388a05da7e22ea5266f6d7ebe0ee3821505039b2 Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 01:23:44 +0100 Subject: [PATCH 08/35] fix errors --- .../test_phase7_native_array_handles.py | 11 ++ x2py/wrapper_codegen/c/binding.py | 153 +++++++++++++++--- x2py/wrapper_codegen/fortran/bridge.py | 124 +++++++++++++- 3 files changed, 269 insertions(+), 19 deletions(-) diff --git a/tests/wrapper_codegen/test_phase7_native_array_handles.py b/tests/wrapper_codegen/test_phase7_native_array_handles.py index a5e0a5539..a615ffc89 100644 --- a/tests/wrapper_codegen/test_phase7_native_array_handles.py +++ b/tests/wrapper_codegen/test_phase7_native_array_handles.py @@ -263,6 +263,11 @@ def test_phase7_generated_artifacts_follow_one_typed_action_vocabulary(): assert "result_value = native_make_matrix(n, m)" in bridge_source assert "if (allocated(result_value)) then" in bridge_source assert "call move_alloc(result_value, result)" in bridge_source + assert "bool bind_c_owned_result_73146804_allocated(CFI_cdesc_t * result);" in c_source + assert "return PyBool_FromLong(bind_c_owned_result_73146804_allocated(owner_descriptor));" in c_source + assert "bind_c_owned_result_73146804_deallocate(owner_descriptor);" in c_source + assert "bind_c_owned_result_73146804_destroy(owner_descriptor);" in c_source + assert "bind_c_owned_result_73146804_shape(owner_descriptor, &extent_0);" in c_source assert "character(kind=c_char, len=:), allocatable :: value_value" in bridge_source assert "result_itemsize" in c_source assert "CFI_type_char" in c_source @@ -288,6 +293,12 @@ def test_phase7_numeric_owned_result_is_collected_before_persistent_descriptor_m assert "deallocate(result)" in procedure assert "result = result_value" not in procedure assert "x2py_collect_make_allocatable_array_result" not in bridge_source + assert "function bind_c_owned_result_73146804_allocated(" in bridge_source + assert "real(c_double), allocatable, dimension(:), intent(in) :: result" in bridge_source + assert "state = allocated(result)" in bridge_source + assert "subroutine bind_c_owned_result_73146804_deallocate(" in bridge_source + assert "real(c_double), allocatable, dimension(:), intent(inout) :: result" in bridge_source + assert "subroutine bind_c_owned_result_73146804_destroy(" in bridge_source @pytest.mark.parametrize( diff --git a/x2py/wrapper_codegen/c/binding.py b/x2py/wrapper_codegen/c/binding.py index 6e7224600..7e4f3fe1f 100644 --- a/x2py/wrapper_codegen/c/binding.py +++ b/x2py/wrapper_codegen/c/binding.py @@ -2135,6 +2135,7 @@ def _module_declarations( ) for derived in self._pointer_holder_types(plan) ), + *self._owned_native_array_bridge_prototypes(plan), *self._derived_field_bridge_prototypes(plan), *self._derived_private_method_prototypes(plan), *self._derived_handle_operation_declarations(plan), @@ -4853,7 +4854,7 @@ def _owned_native_array_operation_body( def _owned_native_array_operation_handler(self, operation: NativeArrayOperation): """Return one directly named operation lowerer.""" handlers = { - NativeArrayOperation.SHAPE: self._owned_native_array_descriptor_record_body, + NativeArrayOperation.SHAPE: self._owned_native_array_shape_body, NativeArrayOperation.TO_NUMPY: self._owned_native_array_descriptor_record_body, NativeArrayOperation.ELEMENT_LENGTH: self._owned_native_array_element_length_body, NativeArrayOperation.ARRAY_ACTUAL: self._owned_native_array_actual_body, @@ -4881,6 +4882,28 @@ def _owned_native_array_descriptor_record_body( raise ValueError(f"Owned result {result.owner_path!r} has no descriptor rank") return self._native_array_descriptor_record_nodes(handle.array.rank, "owner_descriptor") + def _owned_native_array_shape_body( + self, + result: ResultPlan, + ) -> tuple[CDeclaration | CExpressionStatement | CIf | CReturn, ...]: + """Expose extents using the typed compiler descriptor inquiry.""" + if self._is_owned_deferred_character_result(result): + return self._owned_native_array_descriptor_record_body(result) + handle = result.native_array_handle + if handle is None or handle.array.rank is None: + raise ValueError(f"Owned result {result.owner_path!r} has no shape rank") + dimensions = tuple(f"extent_{axis}" for axis in range(handle.array.rank)) + return ( + *(CDeclaration(name, "int64_t", CodeExpression("0")) for name in dimensions), + CExpressionStatement( + CodeExpression( + f"{self._owned_native_array_bridge_operation_name(result, NativeArrayOperation.SHAPE)}" + f"(owner_descriptor, {', '.join(f'&{name}' for name in dimensions)})" + ) + ), + CReturn(CodeExpression(f'Py_BuildValue("({",".join("L" for _ in dimensions)})", {", ".join(dimensions)})')), + ) + def _owned_native_array_actual_body(self, _result: ResultPlan) -> tuple[CReturn, ...]: """Expose the current owned allocation data address.""" return (CReturn(CodeExpression("PyLong_FromVoidPtr(owner_descriptor->base_addr)")),) @@ -4895,7 +4918,16 @@ def _owned_native_array_descriptor_body(self, _result: ResultPlan) -> tuple[CRet def _owned_native_array_allocated_body(self, _result: ResultPlan) -> tuple[CReturn, ...]: """Report the current allocation state.""" - return (CReturn(CodeExpression("PyBool_FromLong(owner_descriptor->base_addr != NULL)")),) + if self._is_owned_deferred_character_result(_result): + return (CReturn(CodeExpression("PyBool_FromLong(owner_descriptor->base_addr != NULL)")),) + return ( + CReturn( + CodeExpression( + f"PyBool_FromLong({self._owned_native_array_bridge_operation_name(_result, NativeArrayOperation.ALLOCATED)}" + "(owner_descriptor))" + ) + ), + ) def _owned_native_array_true_body(self, _result: ResultPlan) -> tuple[CReturn, ...]: """Return one invariant true array capability.""" @@ -4907,17 +4939,17 @@ def _owned_native_array_layout_body(self, _result: ResultPlan) -> tuple[CReturn, def _owned_native_array_deallocate_body( self, - _result: ResultPlan, + result: ResultPlan, ) -> tuple[CDeclaration | CExpressionStatement | CIf | CReturn, ...]: """Deallocate payload while retaining owner storage.""" - return self._owned_native_array_deallocate_nodes(free_owner=False) + return self._owned_native_array_deallocate_nodes(result, NativeArrayOperation.DEALLOCATE, free_owner=False) def _owned_native_array_destroy_body( self, - _result: ResultPlan, + result: ResultPlan, ) -> tuple[CDeclaration | CExpressionStatement | CIf | CReturn, ...]: """Destroy payload and persistent owner storage.""" - return self._owned_native_array_deallocate_nodes(free_owner=True) + return self._owned_native_array_deallocate_nodes(result, NativeArrayOperation.DESTROY, free_owner=True) def _owned_native_array_owner_nodes( self, @@ -5001,10 +5033,24 @@ def _native_array_descriptor_record_nodes( def _owned_native_array_deallocate_nodes( self, + result: ResultPlan, + operation: NativeArrayOperation, *, free_owner: bool, ) -> tuple[CDeclaration | CExpressionStatement | CIf | CReturn, ...]: """Release payload and optionally persistent descriptor storage.""" + if not self._is_owned_deferred_character_result(result): + nodes: list[CExpressionStatement | CReturn] = [ + CExpressionStatement( + CodeExpression( + f"{self._owned_native_array_bridge_operation_name(result, operation)}(owner_descriptor)" + ) + ), + ] + if free_owner: + nodes.append(CExpressionStatement(CodeExpression("free(owner_descriptor)"))) + nodes.append(CExpressionStatement(CodeExpression("Py_RETURN_NONE"))) + return tuple(nodes) nodes: list[CDeclaration | CExpressionStatement | CIf | CReturn] = [ CDeclaration("status", "int", CodeExpression("CFI_SUCCESS")), CIf( @@ -5070,20 +5116,10 @@ def _owned_native_array_resize_body( CExpressionStatement(CodeExpression(f"lower_bounds[{axis}] = 0")), ) ) + release_nodes = self._owned_native_array_resize_release_nodes(result) nodes.extend( ( - CIf( - CodeExpression("owner_descriptor->base_addr != NULL"), - body=( - CExpressionStatement(CodeExpression("status = CFI_deallocate(owner_descriptor)")), - CExpressionStatement( - CodeExpression( - "if (status != CFI_SUCCESS) { PyErr_SetString(PyExc_RuntimeError, " - '"failed to release owned native array before resize"); return NULL; }' - ) - ), - ), - ), + *release_nodes, CExpressionStatement( CodeExpression( "status = CFI_allocate(owner_descriptor, lower_bounds, upper_bounds, " @@ -5101,6 +5137,35 @@ def _owned_native_array_resize_body( ) return tuple(nodes) + def _owned_native_array_resize_release_nodes( + self, + result: ResultPlan, + ) -> tuple[CExpressionStatement | CIf, ...]: + """Release existing owned payload before resize through the selected descriptor path.""" + if not self._is_owned_deferred_character_result(result): + return ( + CExpressionStatement( + CodeExpression( + f"{self._owned_native_array_bridge_operation_name(result, NativeArrayOperation.DEALLOCATE)}" + "(owner_descriptor)" + ) + ), + ) + return ( + CIf( + CodeExpression("owner_descriptor->base_addr != NULL"), + body=( + CExpressionStatement(CodeExpression("status = CFI_deallocate(owner_descriptor)")), + CExpressionStatement( + CodeExpression( + "if (status != CFI_SUCCESS) { PyErr_SetString(PyExc_RuntimeError, " + '"failed to release owned native array before resize"); return NULL; }' + ) + ), + ), + ), + ) + def _owned_native_array_operation_name( self, _function: FunctionPlan | None, @@ -5111,6 +5176,16 @@ def _owned_native_array_operation_name( owner = re.sub(r"\W", "_", result.owner_path).casefold() return f"x2py_owned_{owner}_{operation.value}" + def _owned_native_array_bridge_operation_name( + self, + result: ResultPlan, + operation: NativeArrayOperation, + ) -> str: + """Return the C-visible typed bridge operation symbol.""" + preferred = result.bridge.native_name or "result" + owner = NativeSymbolNames.compact(result.owner_path, preferred, limit=38) + return f"bind_c_owned_{owner}_{operation.value}" + def _owned_native_array_operation_def_name( self, function: FunctionPlan | None, @@ -9539,6 +9614,48 @@ def _bridge_prototype(self, plan: FunctionPlan) -> CFunctionPrototype: (*argument_parameters, *result_parameters, *direct_parameters), ) + def _owned_native_array_bridge_prototypes(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: + """Declare typed Fortran operations over binding-owned result descriptors.""" + return tuple( + prototype + for _function, result in self._owned_native_array_results(plan) + if not self._is_owned_deferred_character_result(result) + for operation in result.native_array_handle.operations + if (prototype := self._owned_native_array_bridge_prototype(result, operation)) is not None + ) + + def _owned_native_array_bridge_prototype( + self, + result: ResultPlan, + operation: NativeArrayOperation, + ) -> CFunctionPrototype | None: + """Return one compiler-backed owned-result operation prototype.""" + if operation is NativeArrayOperation.ALLOCATED: + return CFunctionPrototype( + self._owned_native_array_bridge_operation_name(result, operation), + "bool", + (CParameter("result", "CFI_cdesc_t *"),), + ) + if operation is NativeArrayOperation.SHAPE: + handle = result.native_array_handle + if handle is None or handle.array.rank is None: + raise ValueError(f"Owned result {result.owner_path!r} has no shape rank") + return CFunctionPrototype( + self._owned_native_array_bridge_operation_name(result, operation), + "void", + ( + CParameter("result", "CFI_cdesc_t *"), + *(CParameter(f"extent_{axis}", "int64_t *") for axis in range(handle.array.rank)), + ), + ) + if operation in {NativeArrayOperation.DEALLOCATE, NativeArrayOperation.DESTROY}: + return CFunctionPrototype( + self._owned_native_array_bridge_operation_name(result, operation), + "void", + (CParameter("result", "CFI_cdesc_t *"),), + ) + return None + def _bridge_return_type(self, plan: FunctionPlan) -> str: """Return the direct bridge result type, or void for subroutines.""" result = self._direct_result(plan) diff --git a/x2py/wrapper_codegen/fortran/bridge.py b/x2py/wrapper_codegen/fortran/bridge.py index d14df92d5..37ee58496 100644 --- a/x2py/wrapper_codegen/fortran/bridge.py +++ b/x2py/wrapper_codegen/fortran/bridge.py @@ -684,7 +684,11 @@ def _derived_holder_definitions(self, plan: ModulePlan) -> tuple[FortranTypeDefi def _visit_NamespacePlan(self, plan: NamespacePlan) -> tuple[FortranFunction, ...]: """Return bridge procedures directly owned by one Python namespace.""" return ( - *(self.visit(function) for function in plan.functions), + *( + procedure + for function in plan.functions + for procedure in (self.visit(function), *self._owned_native_array_result_operations(function)) + ), *(procedure for variable in plan.variables for procedure in self.visit(variable)), ) @@ -1830,6 +1834,124 @@ def _scalar_descriptor_direct_result_parameters( parameters.append(FortranParameter("result_length", "integer(c_int64_t)")) return tuple(parameters) + # Owned native-array result operations. + def _owned_native_array_result_operations(self, function: FunctionPlan) -> tuple[FortranFunction, ...]: + """Lower typed operations over binding-owned result descriptors.""" + procedures = [] + for result in function.results: + if not self._supports_owned_native_array_result_operations(result): + continue + handle = result.native_array_handle + if handle is None: + continue + for operation in handle.operations: + procedure = self._owned_native_array_result_operation(result, operation) + if procedure is not None: + procedures.append(procedure) + return tuple(procedures) + + def _supports_owned_native_array_result_operations(self, result: ResultPlan) -> bool: + """Return whether typed helper operations use a Fortran descriptor dummy.""" + return self._is_owned_native_array_result(result) and not self._is_owned_deferred_character_result(result) + + def _owned_native_array_result_operation( + self, + result: ResultPlan, + operation: NativeArrayOperation, + ) -> FortranFunction | None: + """Dispatch one generated operation selected by completed handle policy.""" + if operation is NativeArrayOperation.ALLOCATED: + return self._owned_native_array_result_allocated_operation(result) + if operation is NativeArrayOperation.SHAPE: + return self._owned_native_array_result_shape_operation(result) + if operation in {NativeArrayOperation.DEALLOCATE, NativeArrayOperation.DESTROY}: + return self._owned_native_array_result_deallocate_operation(result, operation) + return None + + def _owned_native_array_result_allocated_operation(self, result: ResultPlan) -> FortranFunction: + """Return allocation state using the compiler's descriptor inquiry.""" + name = self._owned_native_array_result_operation_name(result, NativeArrayOperation.ALLOCATED) + return FortranFunction( + name=name, + parameters=(self._owned_native_array_result_parameter(result, intent="in"),), + result_name="state", + result_type="logical(c_bool)", + bind_name=name, + body=(FortranAssignment("state", CodeExpression("allocated(result)")),), + ) + + def _owned_native_array_result_shape_operation(self, result: ResultPlan) -> FortranFunction: + """Return shape through Fortran when the owned descriptor is allocated.""" + handle = result.native_array_handle + if handle is None or handle.array.rank is None: + raise ValueError(f"Owned result {result.owner_path!r} has no shape rank") + name = self._owned_native_array_result_operation_name(result, NativeArrayOperation.SHAPE) + extents = tuple(FortranParameter(f"extent_{axis}", "integer(c_int64_t)") for axis in range(handle.array.rank)) + present = tuple( + FortranAssignment( + f"extent_{axis}", + CodeExpression(f"size(result, {axis + 1}, kind=c_int64_t)"), + ) + for axis in range(handle.array.rank) + ) + absent = tuple( + FortranAssignment(f"extent_{axis}", CodeExpression("0_c_int64_t")) for axis in range(handle.array.rank) + ) + return FortranFunction( + name=name, + parameters=(self._owned_native_array_result_parameter(result, intent="in"), *extents), + bind_name=name, + body=( + FortranIf( + CodeExpression("allocated(result)"), + body=present, + else_body=absent, + ), + ), + is_subroutine=True, + ) + + def _owned_native_array_result_deallocate_operation( + self, + result: ResultPlan, + operation: NativeArrayOperation, + ) -> FortranFunction: + """Release owned payload through the compiler's descriptor machinery.""" + name = self._owned_native_array_result_operation_name(result, operation) + return FortranFunction( + name=name, + parameters=(self._owned_native_array_result_parameter(result, intent="inout"),), + bind_name=name, + body=( + FortranIf( + CodeExpression("allocated(result)"), + body=(FortranDeallocate("result"),), + ), + ), + is_subroutine=True, + ) + + def _owned_native_array_result_parameter(self, result: ResultPlan, *, intent: str) -> FortranParameter: + """Return the typed descriptor dummy used by owned-result operations.""" + handle = result.native_array_handle + if handle is None or handle.array.rank is None: + raise ValueError(f"Owned result {result.owner_path!r} has no descriptor rank") + return FortranParameter( + "result", + self._array_result_element_type(result), + ("allocatable", self._array_dimension_attribute(handle.array.rank), f"intent({intent})"), + ) + + def _owned_native_array_result_operation_name( + self, + result: ResultPlan, + operation: NativeArrayOperation, + ) -> str: + """Return the C-visible typed operation name for one owned result.""" + preferred = result.bridge.native_name or "result" + owner = NativeSymbolNames.compact(result.owner_path, preferred, limit=38) + return f"bind_c_owned_{owner}_{operation.value}" + def _visit_ModuleVariablePlan(self, plan: ModuleVariablePlan) -> tuple[FortranFunction, ...]: """Lower bridge-owned getter and setter actions into procedures.""" if plan.binding.getter_action is ModuleGetterAction.NATIVE_ARRAY_HANDLE: From be76c4d4d1247c58bb2f5936797b8e9ae8fd02c6 Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 05:07:12 +0100 Subject: [PATCH 09/35] fix errors by adding maybe unallocaed array --- .../wrapper-plan-migration-checklist.md | 3 +- docs/user/guide/allocatables.md | 27 ++- tests/_shared/ownership_policy_support.py | 1 + tests/wrapper/CHECKLIST_COVERAGE.md | 2 +- ...test_scalar_derived_actual_dummy_matrix.py | 7 +- .../module_state/test_allocatable_views.py | 39 +++- .../test_calls_and_policy_metadata.py | 11 ++ .../test_phase7_native_array_handles.py | 70 +++++++- ...ase8_scalar_derived_actual_dummy_matrix.py | 24 +++ x2py/contracts/__init__.py | 2 + x2py/semantics/metadata.py | 1 + x2py/semantics/native_array_handles.py | 3 + x2py/semantics/policy_completion.py | 29 +++ x2py/semantics/pyi2ir.py | 4 + x2py/semantics/wrapper_policy.py | 15 ++ x2py/wrapper_codegen/fortran/bridge.py | 166 ++++++++++++++---- x2py/wrapper_codegen/plan.py | 2 + x2py/wrapper_codegen/planner.py | 1 + x2py/wrapper_codegen/printers/pyi_printer.py | 3 + 19 files changed, 360 insertions(+), 50 deletions(-) diff --git a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md index 70f29ffd0..7179d6923 100644 --- a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md +++ b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md @@ -594,7 +594,7 @@ summary, the exhaustive matrix, and the test tree disagree. | Status | Collected nodes | | --- | ---: | -| `wrapper-plan` | 357 | +| `wrapper-plan` | 358 | | `dual-route` | 0 | | `legacy` | 0 | | `not-applicable` | 76 | @@ -767,6 +767,7 @@ already covered by the new generator. | `tests/wrapper/fortran/module_state/test_allocatable_replacement.py::test_allocatable_replacement_has_no_native_memory_errors[*]` | source/generated-.pyi parity route | module variables/state; native handles/descriptors | `wrapper-plan` | | `tests/wrapper/fortran/module_state/test_allocatable_replacement.py::test_projected_allocatable_descriptor_preserves_same_handle_identity` | canonical reduced owned-result plus projected-descriptor contract | direct persistent descriptor mutation; allocation/reallocation/deallocation; same-handle result identity | `wrapper-plan` | | `tests/wrapper/fortran/module_state/test_allocatable_views.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[*]` | source/generated-.pyi parity with one mixed generation unit | derived class/field handles and parent retention remain Phase 8/9 blockers | `wrapper-plan` | +| `tests/wrapper/fortran/module_state/test_allocatable_views.py::test_maybe_unallocated_direct_allocatable_results_preserve_unallocated_state` | edited semantic `.pyi` contract over the existing allocatable module unit | `MaybeUnallocated` direct allocatable result annotation preserves the unallocated result state without changing default always-allocated result handling | `wrapper-plan` | | `tests/wrapper/fortran/module_state/test_allocatable_views.py::test_scalar_descriptor_module_variables_return_copied_optional_values[*]` | production plan route in source/generated-.pyi parity modes | rank-zero allocatable/pointer arguments, writeback, results, and copied nullable module values | `wrapper-plan` | | `tests/wrapper/fortran/module_state/test_allocatable_views.py::test_plain_allocatable_module_array_exposes_current_live_view[*]` | production plan route after the Phase 7 contract correction | plain and `Aliased` module handles return a current live view or `None`; explicit `.copy()` is independent and a fresh extraction follows current native state | `wrapper-plan` | | `tests/wrapper/fortran/module_state/test_common_blocks.py::*` | source/generated-.pyi parity or parametrized route | scalar calls with internal common-block storage | `wrapper-plan` | diff --git a/docs/user/guide/allocatables.md b/docs/user/guide/allocatables.md index fd85232f4..6c5d0545a 100644 --- a/docs/user/guide/allocatables.md +++ b/docs/user/guide/allocatables.md @@ -63,8 +63,8 @@ contains integer(4), intent(in) :: n integer(4) :: i real(8), allocatable :: arr(:) + allocate(arr(max(n, 0))) if (n > 0) then - allocate(arr(n)) arr = [(real(i, 8)*2, i = 1, n)] end if end function make_values @@ -113,7 +113,28 @@ print(values.to_numpy()) # [10. 20.] - Use normal `T[...]` when you just want to pass array **data**. - Call `.to_numpy()` to get a view; the handle itself is not an array. - Copy data (`view.copy()`) if you need it to survive possible reallocation/deallocation. -- Direct allocatable array function results preserve allocated, zero-sized, and - unallocated handle state. This includes matrices and higher-rank arrays. +- Direct allocatable array function results are treated as always allocated by + default. Empty results should allocate zero extent, such as `allocate(arr(0))`. + The generated bridge can then use the normal fast assignment-plus-`move_alloc` + path. +- Use `Annotated[Allocatable[T[...]], MaybeUnallocated]` only when a direct + allocatable array function result may return unallocated. The Python result is + still an `Allocatable[...]` handle; `handle.allocated` may be `False`, and + `handle.to_numpy()` may return `None`. +- `MaybeUnallocated` uses a GNU-verified helper path that avoids assigning an + unallocated direct function result before `allocated(...)` can be checked. + A native subroutine with an allocatable `intent(out)` output remains the + portable zero-copy spelling for unallocated result state on stricter + compilers. + +```python +from x2py.contracts import Allocatable, Annotated, Float64, Int32, MaybeUnallocated + +def make_values(n: Int32) -> Allocatable[Float64[:]]: ... + +def maybe_values( + n: Int32, +) -> Annotated[Allocatable[Float64[:]], MaybeUnallocated]: ... +``` --- diff --git a/tests/_shared/ownership_policy_support.py b/tests/_shared/ownership_policy_support.py index bdae3f989..672d7de38 100644 --- a/tests/_shared/ownership_policy_support.py +++ b/tests/_shared/ownership_policy_support.py @@ -186,6 +186,7 @@ def _native_array_policy( python_setter="none", native_setter="none", output_projection="none", + result_allocation="not_applicable", release="native_owner", target_lifetime="module", destroy_behavior="none", diff --git a/tests/wrapper/CHECKLIST_COVERAGE.md b/tests/wrapper/CHECKLIST_COVERAGE.md index f497964b8..ab3c9beb2 100644 --- a/tests/wrapper/CHECKLIST_COVERAGE.md +++ b/tests/wrapper/CHECKLIST_COVERAGE.md @@ -90,7 +90,7 @@ recorded progression, not in the live ledger. | Native array handle bridge and binding architecture dispatches from completed descriptor-kind and handle-kind policy pairs | `tests/semantics/policy/test_native_array_ownership.py::test_native_array_handle_dispatcher_routes_completed_policy_to_named_method`, `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_generated_artifacts_follow_one_typed_action_vocabulary`, `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_plan_edits_fail_central_validation` | | Module native-array handles lower from typed borrowed-handle plans to private operation wrappers for state, shape, pointer/descriptor handoff, allocatable `.to_numpy()`/`deallocate()`/`resize(shape)`, pointer `nullify()`, and policy-gated pointer shape-changing operations | `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_module_variables_own_borrowed_handle_plans_and_operation_sets`, `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_generated_artifacts_follow_one_typed_action_vocabulary` | | Generated native array handle construction uses typed operation sets and the runtime factory adapts generated operations to the handle protocol, including shape changes and pointer-address handoff | `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_module_variables_own_borrowed_handle_plans_and_operation_sets`, `tests/runtime/handles/test_factories_and_lifecycle.py::test_generated_handle_factory_adapts_private_operations_to_runtime_protocol`, `tests/runtime/handles/test_factories_and_lifecycle.py::test_generated_handle_factory_splats_shape_operations_to_scalar_extents`, `tests/runtime/handles/test_factories_and_lifecycle.py::test_generated_handle_factory_rejects_invalid_descriptor_kind_and_handoff_result` | -| Owned allocatable results use planned persistent descriptor ownership, collect bridge-local data before transfer, expose owner-addressed operations, and release through the shared handle finalizer path; source and generated-`.pyi` modes retain compiled behavior coverage | `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_numeric_owned_result_is_collected_before_persistent_descriptor_move`, `tests/semantics/policy/test_native_array_ownership.py::test_hidden_allocatable_handle_output_completes_as_owned_result_before_lowering`, `tests/runtime/handles/test_factories_and_lifecycle.py::test_generated_owned_handle_factory_passes_persistent_owner_to_every_operation`, `module_state/test_allocatable_views.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles`, `module_state/test_allocatable_replacement.py::test_allocatable_inout_arrays_mutate_and_return_the_same_handle` | +| Owned allocatable results use planned persistent descriptor ownership, collect bridge-local data before transfer, expose owner-addressed operations, and release through the shared handle finalizer path; source and generated-`.pyi` modes retain compiled behavior coverage | `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_numeric_owned_result_defaults_to_assignment_then_move_alloc`, `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_maybe_unallocated_owned_result_uses_collector_without_local_assignment`, `tests/semantics/policy/test_native_array_ownership.py::test_hidden_allocatable_handle_output_completes_as_owned_result_before_lowering`, `tests/runtime/handles/test_factories_and_lifecycle.py::test_generated_owned_handle_factory_passes_persistent_owner_to_every_operation`, `module_state/test_allocatable_views.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles`, `module_state/test_allocatable_views.py::test_maybe_unallocated_direct_allocatable_results_preserve_unallocated_state`, `module_state/test_allocatable_replacement.py::test_allocatable_inout_arrays_mutate_and_return_the_same_handle` | | Native array descriptor arguments record required and optional-presence roles in the typed plan; direct bridge and binding lowering consume those roles while runtime helpers validate descriptor kind, dtype, rank, and shape metadata | `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_keeps_datatype_specific_state_under_argument_and_result_plans`, `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_generated_artifacts_follow_one_typed_action_vocabulary`, `tests/runtime/handles/test_descriptor_abi.py` | | Concrete-rank numeric array arguments retain the NumPy extraction path and use native handle actuals without converting through `to_numpy()`; optional and assumed-rank behavior remains covered at the public wrapper boundary | `tests/wrapper_codegen/test_phase6a_array_buffers.py::test_required_array_buffer_dispatches_through_named_binding_and_bridge_methods`, `tests/runtime/handles/test_array_actual_abi.py`, `tests/wrapper/fortran/arrays/test_array_contracts.py::test_remaining_array_contracts_are_validated_before_fortran_calls`, `tests/wrapper/fortran/arrays/test_assumed_rank_arrays.py::test_assumed_rank_arguments_dispatch_to_runtime_rank`, `tests/wrapper/fortran/function_calls/test_optional_arguments.py::test_optional_arguments_drive_fortran_present_behavior` | | Runtime normal-array argument packing uses the generated Bind-C array tuple shape for ndarray inputs and native handle array-actual handoff: pointer address, optional runtime rank, optional item size, extents, and optional upper bounds plus unit strides; allocated/associated handles pack without calling `to_numpy()`, and unallocated/unassociated handles block before generated handoff | `tests/runtime/handles/test_array_actual_abi.py::test_array_actual_argument_abi_packer_uses_ndarray_data_pointer_and_shape_fields`, `tests/runtime/handles/test_array_actual_abi.py::test_array_actual_argument_abi_packer_uses_allocatable_native_array_actual_without_numpy_conversion`, `tests/runtime/handles/test_array_actual_abi.py::test_array_actual_argument_abi_packer_uses_pointer_native_array_actual_dtype_metadata`, `tests/runtime/handles/test_array_actual_abi.py::test_array_actual_argument_abi_packer_rejects_absent_handles_before_generated_handoff` | diff --git a/tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py b/tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py index f3f7a0c5c..7e55ee8a3 100644 --- a/tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py +++ b/tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py @@ -82,7 +82,12 @@ def scalar_matrix(tmp_path_factory) -> MatrixBuild: native_fortran_sources=[SOURCE], output_dir=output_dir, ) - package = _import_from_build_dir(result.module_name, result.output_dir) + try: + package = _import_from_build_dir(result.module_name, result.output_dir) + except ImportError as exc: + if "cannot enable executable stack" in str(exc): + pytest.skip("local platform refuses executable-stack wrapper trampolines") + raise return MatrixBuild( package.fscalar_derived_actual_dummy_matrix_f90, package.phase8_left_types, diff --git a/tests/wrapper/fortran/module_state/test_allocatable_views.py b/tests/wrapper/fortran/module_state/test_allocatable_views.py index 7426fb407..c4ab0bb0f 100644 --- a/tests/wrapper/fortran/module_state/test_allocatable_views.py +++ b/tests/wrapper/fortran/module_state/test_allocatable_views.py @@ -268,6 +268,29 @@ def _scalar_descriptor_module(build_mode: str, tmp_path: Path): return _sole_native_module(_import_from_build_dir(result.module_name, result.output_dir)) +def _maybe_unallocated_direct_result_module(tmp_path: Path): + native_object = _compile_native_object(ALLOCATABLE_VIEW_F90_SOURCE, tmp_path / "native") + contract_dir = tmp_path / "contracts" / "fallocatable_views_f90" + contract_dir.mkdir(parents=True) + (contract_dir / "__init__.pyi").write_text("from . import fallocatable_views_f90\n", encoding="utf-8") + contract_text = (CONTRACT_FIXTURES / "fallocatable_views_f90" / "fallocatable_views_f90.pyi").read_text( + encoding="utf-8" + ) + contract_text = contract_text.replace("Int32, Pass", "Int32, MaybeUnallocated, Pass") + contract_text = contract_text.replace( + "def make_values(\n n: Int32\n) -> Allocatable[Float64[:]]: ...", + "def make_values(\n n: Int32\n) -> Annotated[Allocatable[Float64[:]], MaybeUnallocated]: ...", + ) + (contract_dir / "fallocatable_views_f90.pyi").write_text(contract_text, encoding="utf-8") + result = build_pyi_extension( + contract_dir / "__init__.pyi", + native_objects=[native_object], + native_include_dirs=[native_object.parent], + output_dir=tmp_path / "pyi_build", + ) + return _sole_native_module(_import_from_build_dir(result.module_name, result.output_dir)) + + def test_allocatable_module_fields_and_results_expose_lifetime_safe_handles( pyi_parity_build_mode: str, tmp_path: Path, @@ -340,14 +363,12 @@ def test_allocatable_module_fields_and_results_expose_lifetime_safe_handles( made_values = module.make_values(np.int32(3)) np.testing.assert_allclose(made_values.to_numpy(), np.array([3.0, 6.0, 9.0], dtype=np.float64)) - assert module.make_values(np.int32(0)).allocated is False made_matrix = module.make_matrix(np.int32(2), np.int32(2)) np.testing.assert_allclose( made_matrix.to_numpy(), np.array([[111.0, 121.0], [112.0, 122.0]], dtype=np.float64), ) - assert module.make_matrix(np.int32(2), np.int32(0)).allocated is False retained_result_view = made_values.to_numpy() del made_values @@ -385,6 +406,20 @@ def test_allocatable_module_fields_and_results_expose_lifetime_safe_handles( _ = built_values.allocated +def test_maybe_unallocated_direct_allocatable_results_preserve_unallocated_state(tmp_path: Path): + module = _maybe_unallocated_direct_result_module(tmp_path) + + made_values = module.make_values(np.int32(3)) + np.testing.assert_allclose(made_values.to_numpy(), np.array([3.0, 6.0, 9.0], dtype=np.float64)) + assert module.make_values(np.int32(0)).allocated is False + + made_matrix = module.make_matrix(np.int32(2), np.int32(2)) + np.testing.assert_allclose( + made_matrix.to_numpy(), + np.array([[111.0, 121.0], [112.0, 122.0]], dtype=np.float64), + ) + + def test_scalar_descriptor_module_variables_return_copied_optional_values( pyi_parity_build_mode: str, tmp_path: Path, diff --git a/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py b/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py index 74931bcae..d59ffe8d5 100644 --- a/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py +++ b/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py @@ -27,6 +27,7 @@ pytest, rendered_source, ) +from x2py.semantics.metadata import MAYBE_UNALLOCATED_METADATA def test_emit_optional_scalar_output_as_visible_scalar_storage(): @@ -388,6 +389,15 @@ def test_printer_emits_extended_storage_and_callable_forms(): array=SemanticArrayContract(rank=1, shape=["1"], allocatable=True), ), ) + maybe_unallocated_handle = SemanticType( + "Float64", + rank=1, + metadata={MAYBE_UNALLOCATED_METADATA: True}, + storage=SemanticStorageContract( + kind="array", + array=SemanticArrayContract(rank=1, shape=[":"], allocatable=True), + ), + ) pointer_handle = SemanticType( "Float64", rank=1, @@ -448,6 +458,7 @@ def test_printer_emits_extended_storage_and_callable_forms(): assert printer.emit(inferred_array) == "Float64[:, :]" assert printer.emit(allocatable_handle) == "Allocatable[Annotated[Float64[:, :], ORDER_F]]" assert printer.emit(constrained_allocatable_handle) == "Allocatable[Annotated[Bool[1], Finite]]" + assert printer.emit(maybe_unallocated_handle) == "Annotated[Allocatable[Float64[:]], MaybeUnallocated]" assert printer.emit(pointer_handle) == 'Annotated[Pointer[Float64[:]], PointerAssociation("runtime")]' assert printer.emit(string_pointer_handle) == "Pointer[String[8][:]]" assert printer.emit(annotated_array) == "Annotated[Float64[:, :], ORDER_ANY, Finite, Range(1, 3)]" diff --git a/tests/wrapper_codegen/test_phase7_native_array_handles.py b/tests/wrapper_codegen/test_phase7_native_array_handles.py index a615ffc89..43e37dd0e 100644 --- a/tests/wrapper_codegen/test_phase7_native_array_handles.py +++ b/tests/wrapper_codegen/test_phase7_native_array_handles.py @@ -14,6 +14,7 @@ NativeArrayDescriptorOwnership, NativeArrayOperation, NativeArrayOutputProjection, + NativeArrayResultAllocation, NativeArraySourceKind, NativeDescriptorHandoffABI, ) @@ -23,7 +24,20 @@ def _phase7_plan(): module = parse_pyi_text( """ -from x2py.contracts import Addr, Allocatable, Arg, Float64, Int32, Pointer, Return, Returns, String, native_call +from x2py.contracts import ( + Addr, + Allocatable, + Annotated, + Arg, + Float64, + Int32, + MaybeUnallocated, + Pointer, + Return, + Returns, + String, + native_call, +) def normal(values: Float64[:]) -> Float64: ... def alloc(values: Allocatable[Float64[:]]) -> Float64: ... @@ -39,6 +53,9 @@ def replace( @native_call([Addr(Arg(0))]) def make(n: Int32) -> Allocatable[Float64[:]]: ... +@native_call([Addr(Arg(0))]) +def maybe_make(n: Int32) -> Annotated[Allocatable[Float64[:]], MaybeUnallocated]: ... + @native_call([Addr(Arg(0)), Addr(Arg(1))]) def make_matrix(n: Int32, m: Int32) -> Allocatable[Float64[:, :]]: ... @@ -142,9 +159,14 @@ def test_phase7_keeps_datatype_specific_state_under_argument_and_result_plans(): assert owned.native_array_handle is not None assert owned.native_array_handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE assert owned.native_array_handle.descriptor_ownership is NativeArrayDescriptorOwnership.OWNED + assert owned.native_array_handle.result_allocation is NativeArrayResultAllocation.ALWAYS_ALLOCATED assert owned.native_array_handle.handoff.owner_storage_role is not None assert NativeArrayOperation.DESTROY in owned.native_array_handle.operations + maybe_owned = functions["maybe_make"].results[0] + assert maybe_owned.native_array_handle is not None + assert maybe_owned.native_array_handle.result_allocation is NativeArrayResultAllocation.MAYBE_UNALLOCATED + owned_matrix = functions["make_matrix"].results[0] assert owned_matrix.native_array_handle is not None assert owned_matrix.native_array_handle.array.rank == 2 @@ -261,8 +283,9 @@ def test_phase7_generated_artifacts_follow_one_typed_action_vocabulary(): assert "bound_values = (CFI_cdesc_t *)&bound_values_storage;" in optional_binding assert "result_value = native_make(n)" in bridge_source assert "result_value = native_make_matrix(n, m)" in bridge_source - assert "if (allocated(result_value)) then" in bridge_source - assert "call move_alloc(result_value, result)" in bridge_source + assert "call x2py_collect_allocatable_array_result(native_maybe_make(n), result)" in bridge_source + assert "if (allocated(value)) then" in bridge_source + assert "call move_alloc(value, result)" in bridge_source assert "bool bind_c_owned_result_73146804_allocated(CFI_cdesc_t * result);" in c_source assert "return PyBool_FromLong(bind_c_owned_result_73146804_allocated(owner_descriptor));" in c_source assert "bind_c_owned_result_73146804_deallocate(owner_descriptor);" in c_source @@ -274,7 +297,7 @@ def test_phase7_generated_artifacts_follow_one_typed_action_vocabulary(): assert "character(kind=c_char, len=:), allocatable, dimension(:) :: names" in bridge_source -def test_phase7_numeric_owned_result_is_collected_before_persistent_descriptor_move(): +def test_phase7_numeric_owned_result_defaults_to_assignment_then_move_alloc(): bridge_source = next( source.text for source in WrapperCodeGenerator().generate(_phase7_plan()).sources @@ -291,8 +314,8 @@ def test_phase7_numeric_owned_result_is_collected_before_persistent_descriptor_m assert "call move_alloc(result_value, result)" in procedure assert "if (allocated(result)) then" in procedure assert "deallocate(result)" in procedure + assert "call x2py_collect_allocatable_array_result(native_make(n), result)" not in procedure assert "result = result_value" not in procedure - assert "x2py_collect_make_allocatable_array_result" not in bridge_source assert "function bind_c_owned_result_73146804_allocated(" in bridge_source assert "real(c_double), allocatable, dimension(:), intent(in) :: result" in bridge_source assert "state = allocated(result)" in bridge_source @@ -301,6 +324,43 @@ def test_phase7_numeric_owned_result_is_collected_before_persistent_descriptor_m assert "subroutine bind_c_owned_result_73146804_destroy(" in bridge_source +def test_phase7_maybe_unallocated_owned_result_uses_collector_without_local_assignment(): + bridge_source = next( + source.text + for source in WrapperCodeGenerator().generate(_phase7_plan()).sources + if source.path.suffix == ".f90" + ) + start = bridge_source.index("subroutine bind_c_maybe_make(") + end = bridge_source.index("end subroutine", start) + procedure = bridge_source[start:end] + + assert "real(c_double), allocatable, dimension(:), intent(out) :: result" in procedure + assert "call x2py_collect_allocatable_array_result(native_maybe_make(n), result)" in procedure + assert "real(c_double), allocatable, dimension(:) :: value" in procedure + assert "if (allocated(value)) then" in procedure + assert "call move_alloc(value, result)" in procedure + assert "if (allocated(result)) then" in procedure + assert "deallocate(result)" in procedure + assert "result_value = native_make(n)" not in procedure + assert "call move_alloc(result_value, result)" not in procedure + assert "result = result_value" not in procedure + assert "subroutine x2py_collect_allocatable_array_result(" in procedure + + +def test_phase7_maybe_unallocated_is_only_valid_on_direct_allocatable_array_results(): + module = parse_pyi_text( + """ +from x2py.contracts import Allocatable, Annotated, Float64, MaybeUnallocated + +def invalid_argument(values: Annotated[Allocatable[Float64[:]], MaybeUnallocated]) -> Float64: ... +""", + module_name="invalid_maybe_unallocated", + ) + + with pytest.raises(ValueError, match="MaybeUnallocated metadata"): + complete_semantic_policies(module) + + @pytest.mark.parametrize( ("edit", "diagnostic"), [ diff --git a/tests/wrapper_codegen/test_phase8_scalar_derived_actual_dummy_matrix.py b/tests/wrapper_codegen/test_phase8_scalar_derived_actual_dummy_matrix.py index 52855c9bf..be376d595 100644 --- a/tests/wrapper_codegen/test_phase8_scalar_derived_actual_dummy_matrix.py +++ b/tests/wrapper_codegen/test_phase8_scalar_derived_actual_dummy_matrix.py @@ -207,6 +207,29 @@ def test_pointer_result_uses_a_persistent_holder_instead_of_the_removed_blocker( assert result.derived.target_release is DerivedRelease.NATIVE_OWNER +def test_class_only_derived_methods_do_not_emit_unreachable_scoped_trampolines(): + module = parse_pyi_text( + """ +from x2py.contracts import Int32 + +class item: + value: Int32 + + def read(self) -> Int32: ... +""", + module_name="phase8_class_only", + ) + complete_semantic_policies(module) + bridge = next( + source.text + for source in WrapperCodeGenerator().generate(WrapperPlanner().build(module)).sources + if source.path.suffix == ".f90" + ) + + assert "c_funloc(x2py_derived_consumer" not in bridge + assert "bound_self_access == 2_c_int" not in bridge + + def test_validation_rejects_a_pointer_holder_without_completed_target_ownership(): plan = WrapperPlanner().build(_module()) function = next(item for item in plan.namespaces[0].functions if item.symbol_name == "make_pointer") @@ -240,6 +263,7 @@ def test_artifacts_emit_shared_holders_typed_origin_operations_and_one_native_ca assert bridge.count("type :: x2py_item_pointer_holder") == 1 assert "abstract interface" in bridge assert "c_f_procpointer" in bridge + assert "c_funloc(x2py_derived_consumer" in bridge assert "move_alloc" in bridge assert bridge.count("native_object_dummy(") == 1 assert "x2py_derived_origin_ops" in c_source diff --git a/x2py/contracts/__init__.py b/x2py/contracts/__init__.py index c9bb79aef..6d8abae7f 100644 --- a/x2py/contracts/__init__.py +++ b/x2py/contracts/__init__.py @@ -90,6 +90,7 @@ def apply(target): Flat = _ContractExpression() FortranAllocatable = _ContractExpression() Immutable = _ContractExpression() +MaybeUnallocated = _ContractExpression() ORDER_ANY = _ContractExpression() ORDER_C = _ContractExpression() ORDER_F = _ContractExpression() @@ -175,6 +176,7 @@ def apply(target): "IsPresent", "Len", "Matrix", + "MaybeUnallocated", "Opaque", "OpaqueHandle", "ORDER_ANY", diff --git a/x2py/semantics/metadata.py b/x2py/semantics/metadata.py index f71e8519b..ba8f633c0 100644 --- a/x2py/semantics/metadata.py +++ b/x2py/semantics/metadata.py @@ -13,4 +13,5 @@ NATIVE_PROJECTION_METADATA = "native_projection" NATIVE_ARRAY_DESCRIPTOR_METADATA = "native_array_descriptor" NATIVE_ARRAY_HANDLE_POLICY_METADATA = "native_array_handle_policy" +MAYBE_UNALLOCATED_METADATA = "maybe_unallocated" OPTIONAL_ABSENT_HANDLE_METADATA = "optional_absent_handle" diff --git a/x2py/semantics/native_array_handles.py b/x2py/semantics/native_array_handles.py index fa485fea7..4f0d8d212 100644 --- a/x2py/semantics/native_array_handles.py +++ b/x2py/semantics/native_array_handles.py @@ -7,6 +7,7 @@ from x2py.semantics.ownership import OWNERSHIP_POLICY_METADATA, POINTER_POLICY_METADATA from x2py.semantics.metadata import ( + MAYBE_UNALLOCATED_METADATA, NATIVE_ARRAY_DESCRIPTOR_METADATA, NATIVE_ARRAY_HANDLE_POLICY_METADATA, OPTIONAL_ABSENT_HANDLE_METADATA, @@ -29,6 +30,7 @@ _HANDLE_ONLY_METADATA = ( NATIVE_ARRAY_DESCRIPTOR_METADATA, NATIVE_ARRAY_HANDLE_POLICY_METADATA, + MAYBE_UNALLOCATED_METADATA, OPTIONAL_ABSENT_HANDLE_METADATA, OWNERSHIP_POLICY_METADATA, POINTER_POLICY_METADATA, @@ -55,6 +57,7 @@ class NativeArrayHandlePolicy: python_setter: str native_setter: str output_projection: str + result_allocation: str release: str target_lifetime: str destroy_behavior: str diff --git a/x2py/semantics/policy_completion.py b/x2py/semantics/policy_completion.py index ea86ee50a..091dbfc18 100644 --- a/x2py/semantics/policy_completion.py +++ b/x2py/semantics/policy_completion.py @@ -24,6 +24,7 @@ ADDRESS_ROLE_PROJECTION, ADDRESS_ROLE_RAW, BIND_TARGET_METADATA, + MAYBE_UNALLOCATED_METADATA, OPTIONAL_ABSENT_HANDLE_METADATA, PROJECTED_OUTPUT_METADATA, SCALAR_STORAGE_CATEGORY, @@ -859,6 +860,7 @@ def _complete_function( owner_path=f"{owner_path}.{argument.name}", ) if function.return_type is not None: + _validate_maybe_unallocated_return(function, owner_path) decision = default_ownership_policy.decide_semantic_type(function.return_type, OwnershipContext.result()) function.metadata[models.RESOLVED_RETURN_OWNERSHIP_POLICY_METADATA] = decision _complete_native_array_handle_result_policy(function, decision) @@ -1014,6 +1016,15 @@ def _complete_native_array_handle_result_policy( function.metadata[models.RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA] = policy +def _validate_maybe_unallocated_return(function: models.SemanticFunction, owner_path: str) -> None: + """Require MaybeUnallocated only on direct allocatable array function results.""" + return_type = function.return_type + if return_type is None or not return_type.metadata.get(MAYBE_UNALLOCATED_METADATA): + return + if native_array_descriptor_kind(return_type) != "allocatable" or int(return_type.rank or 0) <= 0: + raise ValueError(f"MaybeUnallocated metadata on {owner_path}.return requires an Allocatable[...] array result") + + def _complete_native_array_handle_variable_policy( variable: models.SemanticVariable, context: OwnershipContext, @@ -1060,6 +1071,7 @@ def _native_array_handle_policy( python_setter=_native_array_python_setter(variable), native_setter=_native_array_native_setter(variable), output_projection=_native_array_output_projection(descriptor_kind, handle_kind, context), + result_allocation=_native_array_result_allocation(descriptor_kind, handle_kind, context, semantic_type), release=_native_array_release_responsibility(handle_kind), target_lifetime=_native_array_target_lifetime(descriptor_kind, handle_kind, semantic_type, blocker), destroy_behavior=_native_array_destroy_behavior(handle_kind, blocker), @@ -1180,6 +1192,19 @@ def _native_array_output_projection( return "none" +def _native_array_result_allocation( + descriptor_kind: str, + handle_kind: str, + context: OwnershipContext, + semantic_type: models.SemanticType, +) -> str: + 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" + return "always_allocated" + return "not_applicable" + + def _native_array_release_responsibility(handle_kind: str) -> str: return { "argument_descriptor": "none", @@ -1530,6 +1555,10 @@ def _complete_variable( *, owner_path: str | None = None, ) -> None: + 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" + ) decision = default_ownership_policy.decide_semantic_variable(variable, context) variable.metadata[models.RESOLVED_OWNERSHIP_POLICY_METADATA] = decision _complete_prototype_reference_policy(variable.semantic_type, owner_path=owner_path or variable.name) diff --git a/x2py/semantics/pyi2ir.py b/x2py/semantics/pyi2ir.py index dd58d8e32..84c87b62f 100644 --- a/x2py/semantics/pyi2ir.py +++ b/x2py/semantics/pyi2ir.py @@ -13,6 +13,7 @@ ADDRESS_ROLE_PROJECTION, ADDRESS_ROLE_RAW, BIND_TARGET_METADATA, + MAYBE_UNALLOCATED_METADATA, NATIVE_PROJECTION_METADATA, OPTIONAL_ABSENT_HANDLE_METADATA, PROJECTED_OUTPUT_METADATA, @@ -1740,6 +1741,9 @@ def _apply_metadata_name(self, semantic_type: SemanticType, name: str) -> bool: if name == "Immutable": semantic_type.metadata[PYTHON_VALUE_MUTABILITY_METADATA] = PYTHON_VALUE_IMMUTABLE return True + if name == "MaybeUnallocated": + semantic_type.metadata[MAYBE_UNALLOCATED_METADATA] = True + return True if name == "FortranAllocatable": semantic_type.metadata["fortran_allocatable"] = True return True diff --git a/x2py/semantics/wrapper_policy.py b/x2py/semantics/wrapper_policy.py index 517faf104..7621c84c2 100644 --- a/x2py/semantics/wrapper_policy.py +++ b/x2py/semantics/wrapper_policy.py @@ -709,6 +709,14 @@ class NativeArrayOutputProjection(str, Enum): HANDLE_RESULT = "handle_result" +class NativeArrayResultAllocation(str, Enum): + """Direct native allocatable function result allocation contract.""" + + NOT_APPLICABLE = "not_applicable" + ALWAYS_ALLOCATED = "always_allocated" + MAYBE_UNALLOCATED = "maybe_unallocated" + + class NativeArrayRelease(str, Enum): """Completed release owner for descriptor storage.""" @@ -901,6 +909,7 @@ class NativeArrayHandleWrapperPolicy: setter_action: SetterAction native_assignment: AssignmentMode output_projection: NativeArrayOutputProjection + result_allocation: NativeArrayResultAllocation release: NativeArrayRelease target_lifetime: str destroy_behavior: NativeArrayDestroyBehavior @@ -5114,6 +5123,12 @@ def _native_array_handle_wrapper_policy( setter_action=_native_array_setter_action(completed.python_setter, owner_path), native_assignment=_native_array_assignment(completed.native_setter, owner_path), output_projection=output_projection, + result_allocation=_native_array_enum( + NativeArrayResultAllocation, + completed.result_allocation, + owner_path, + "result allocation", + ), release=_native_array_enum(NativeArrayRelease, completed.release, owner_path, "release"), target_lifetime=completed.target_lifetime, destroy_behavior=_native_array_enum( diff --git a/x2py/wrapper_codegen/fortran/bridge.py b/x2py/wrapper_codegen/fortran/bridge.py index 37ee58496..66b222efb 100644 --- a/x2py/wrapper_codegen/fortran/bridge.py +++ b/x2py/wrapper_codegen/fortran/bridge.py @@ -34,6 +34,7 @@ NativeArrayDescriptorKind, NativeArrayDescriptorInterop, NativeArrayOperation, + NativeArrayResultAllocation, NativeDescriptorHandoffABI, NativeInvocationKind, OptionalMode, @@ -84,6 +85,10 @@ class FortranBridgeGenerator(ClassVisitor): """Recursively lower bridge plan views directly into Fortran nodes.""" + def __init__(self, *, method_prefix: str | None = None): + super().__init__(method_prefix=method_prefix) + self._active_scoped_type_identities: frozenset[tuple[str, str]] = frozenset() + def require_supported(self, plan: ModulePlan) -> None: """Reject unsupported Fortran ABI actions and scalar types.""" for derived in self._derived_types(plan): @@ -605,45 +610,50 @@ def _require_nested_derived_field(field: DerivedFieldPlan) -> None: def _visit_ModulePlan(self, plan: ModulePlan) -> FortranModule: """Return one complete Fortran bridge module.""" - return FortranModule( - name=f"bind_c_{plan.bridge.owner_path}_wrapper", - uses=( - FortranUse("iso_c_binding", self._iso_c_symbols(plan)), - *self._native_module_uses(plan), - ), - type_definitions=self._derived_holder_definitions(plan), - interfaces=( - *self._derived_call_interfaces(plan), - *self._external_interfaces(plan), - *self._module_descriptor_callback_interfaces(plan), - *self._derived_array_callback_interfaces(plan), - *self._allocator_interfaces(plan), - ), - procedures=( - *(procedure for namespace in plan.namespaces for procedure in self.visit(namespace)), - # Typed derived-field access remains separate from class orchestration. - *self._derived_field_procedures(plan), - # Native-aware opaque-owner destruction is Phase 8 substrate, not class orchestration. - *self._class_constructor_procedures(plan), - *(self._derived_destroy_procedure(derived) for derived in self._owned_derived_types(plan)), - *( - self._allocatable_holder_destroy_procedure(derived) - for derived in self._allocatable_holder_types(plan) + previous_scoped = self._active_scoped_type_identities + self._active_scoped_type_identities = self._scoped_origin_type_identities(plan) + try: + return FortranModule( + name=f"bind_c_{plan.bridge.owner_path}_wrapper", + uses=( + FortranUse("iso_c_binding", self._iso_c_symbols(plan)), + *self._native_module_uses(plan), ), - *( - self._allocatable_holder_presence_procedure(derived) - for derived in self._allocatable_holder_types(plan) + type_definitions=self._derived_holder_definitions(plan), + interfaces=( + *self._derived_call_interfaces(plan), + *self._external_interfaces(plan), + *self._module_descriptor_callback_interfaces(plan), + *self._derived_array_callback_interfaces(plan), + *self._allocator_interfaces(plan), ), - *(self._pointer_holder_destroy_procedure(derived) for derived in self._pointer_holder_types(plan)), - *(self._pointer_holder_presence_procedure(derived) for derived in self._pointer_holder_types(plan)), - *( - procedure - for variable in self._derived_origin_variables(plan) - for procedure in self._derived_origin_procedures(variable) + procedures=( + *(procedure for namespace in plan.namespaces for procedure in self.visit(namespace)), + # Typed derived-field access remains separate from class orchestration. + *self._derived_field_procedures(plan), + # Native-aware opaque-owner destruction is Phase 8 substrate, not class orchestration. + *self._class_constructor_procedures(plan), + *(self._derived_destroy_procedure(derived) for derived in self._owned_derived_types(plan)), + *( + self._allocatable_holder_destroy_procedure(derived) + for derived in self._allocatable_holder_types(plan) + ), + *( + self._allocatable_holder_presence_procedure(derived) + for derived in self._allocatable_holder_types(plan) + ), + *(self._pointer_holder_destroy_procedure(derived) for derived in self._pointer_holder_types(plan)), + *(self._pointer_holder_presence_procedure(derived) for derived in self._pointer_holder_types(plan)), + *( + procedure + for variable in self._derived_origin_variables(plan) + for procedure in self._derived_origin_procedures(variable) + ), ), - ), - external_procedures=self._callback_external_adapter_procedures(plan), - ) + external_procedures=self._callback_external_adapter_procedures(plan), + ) + finally: + self._active_scoped_type_identities = previous_scoped def _callback_external_adapter_procedures(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: """Return separately linked callback adapters in stable site order.""" @@ -755,6 +765,7 @@ def _visit_FunctionPlan(self, plan: FunctionPlan) -> FortranFunction: is_subroutine=is_subroutine, internal_procedures=( *optional_procedures, + *self._direct_result_internal_procedures(plan), *internal_procedures, ), ) @@ -1197,6 +1208,7 @@ def _scoped_derived_arguments( argument for argument in arguments if self._derived_argument_uses_access(argument, DerivedActualAccess.SCOPED_ADDRESS) + and self._has_scoped_origin_for_argument(argument) ) @staticmethod @@ -1257,7 +1269,11 @@ def _derived_argument_preparation(self, argument: ArgumentTransferPlan) -> Fortr 5: self._derived_allocatable_transaction_preparation, 6: self._derived_pointer_transaction_preparation, } - cases.extend(FortranCase(code, builders[code](argument)) for code in sorted(compatible) if code in builders) + cases.extend( + FortranCase(code, builders[code](argument)) + for code in sorted(compatible) + if code in builders and (code != 2 or self._has_scoped_origin_for_argument(argument)) + ) cases.append( FortranCase( None, @@ -2033,6 +2049,18 @@ def _lower_module_derived_presence(self, plan: ModuleVariablePlan) -> tuple[Fort def _derived_origin_variables(self, plan: ModulePlan) -> tuple[ModuleVariablePlan, ...]: 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]]: + """Return derived identities with at least one scoped module-origin producer.""" + return frozenset( + variable.derived.handoff.type_identity + for variable in self._derived_origin_variables(plan) + if self._derived_origin_supports(variable, "scoped") + ) + + def _has_scoped_origin_for_argument(self, argument: ArgumentTransferPlan) -> bool: + """Return whether this bridge module can produce a scoped origin for the argument type.""" + return argument.derived is not None and argument.derived.type_identity in self._active_scoped_type_identities + def _derived_origin_procedures(self, variable: ModuleVariablePlan) -> tuple[FortranFunction, ...]: """Emit only the typed leaves supported by one completed module storage.""" builders = { @@ -3339,6 +3367,11 @@ def _native_result_expression_invocation( ) -> FortranAssignment | FortranCall | FortranPointerAssignment: """Store one completed native result expression through its handoff leaf.""" direct_result = self._direct_result(plan) + if self._uses_owned_direct_array_result_collector(plan): + return FortranCall( + self._owned_direct_array_result_collector_name(), + (CodeExpression(expression), CodeExpression("result")), + ) if self._uses_pointer_result_assignment(direct_result): return FortranPointerAssignment(result_name, CodeExpression(expression)) return FortranAssignment(result_name, CodeExpression(expression)) @@ -4274,6 +4307,8 @@ def _direct_result_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclar if result.scalar_descriptor is not None: return self._scalar_descriptor_copy_declarations(result, "result") if self._is_owned_native_array_result(result): + if self._uses_owned_direct_array_result_collector(plan): + return () return self._owned_array_result_declarations(result) if result.object_kind is ObjectKind.NUMPY_ARRAY: return self._direct_array_result_declarations(plan, result) @@ -4471,6 +4506,8 @@ def _direct_result_finalizers( if result.scalar_descriptor is not None: return self._scalar_descriptor_copy_nodes(result, "result") if self._is_owned_native_array_result(result): + if self._uses_owned_direct_array_result_collector(plan): + return () if self._is_owned_deferred_character_result(result): return self._owned_deferred_character_copy_nodes(result, "result", "result_value", "result_copy") return ( @@ -4512,6 +4549,46 @@ def _direct_result_finalizers( copy_name="result_copy", ) + def _direct_result_internal_procedures(self, plan: FunctionPlan) -> tuple[FortranFunction, ...]: + """Return helper procedures needed by direct-result lowering.""" + result = self._direct_result(plan) + if result is None or not self._uses_owned_direct_array_result_collector(plan): + return () + return (self._owned_direct_array_result_collector(result),) + + def _owned_direct_array_result_collector(self, result: ResultPlan) -> FortranFunction: + """Move a GNU allocatable function result without the crashing assignment path.""" + handle = result.native_array_handle + if handle is None or handle.array.rank is None: + raise ValueError(f"Owned result {result.owner_path!r} has no descriptor rank") + element_type = self._array_result_element_type(result) + dimension = self._array_dimension_attribute(handle.array.rank) + return FortranFunction( + name=self._owned_direct_array_result_collector_name(), + parameters=( + FortranParameter("value", element_type, ("allocatable", dimension)), + FortranParameter("result", element_type, ("allocatable", dimension, "intent(out)")), + ), + body=( + FortranIf( + CodeExpression("allocated(value)"), + body=( + FortranCall( + "move_alloc", + (CodeExpression("value"), CodeExpression("result")), + ), + ), + else_body=( + FortranIf( + CodeExpression("allocated(result)"), + body=(FortranDeallocate("result"),), + ), + ), + ), + ), + is_subroutine=True, + ) + @staticmethod def _derived_direct_result_finalizers( result: ResultPlan, @@ -5048,6 +5125,21 @@ def _owned_direct_result(self, plan: FunctionPlan) -> ResultPlan | None: result = self._direct_result(plan) return result if result is not None and self._is_owned_native_array_result(result) else None + def _uses_owned_direct_array_result_collector(self, plan: FunctionPlan) -> bool: + """Return whether a direct function result may be returned unallocated.""" + result = self._direct_result(plan) + return bool( + result is not None + and self._is_owned_native_array_result(result) + and not self._is_owned_deferred_character_result(result) + and result.native_array_handle is not None + and result.native_array_handle.result_allocation is NativeArrayResultAllocation.MAYBE_UNALLOCATED + ) + + @staticmethod + def _owned_direct_array_result_collector_name() -> str: + return "x2py_collect_allocatable_array_result" + @staticmethod def _is_owned_native_array_result(result: ResultPlan) -> bool: """Return whether one result owns persistent standard-descriptor storage.""" diff --git a/x2py/wrapper_codegen/plan.py b/x2py/wrapper_codegen/plan.py index 4e1b517cc..9cd6a1002 100644 --- a/x2py/wrapper_codegen/plan.py +++ b/x2py/wrapper_codegen/plan.py @@ -59,6 +59,7 @@ NativeArrayHandleOrigin, NativeArrayOperation, NativeArrayOutputProjection, + NativeArrayResultAllocation, NativeArrayOwnerRetention, NativeArrayRelease, NativeArraySourceKind, @@ -343,6 +344,7 @@ class NativeArrayHandlePlan(StageRecord): setter_action: SetterAction native_assignment: AssignmentMode output_projection: NativeArrayOutputProjection + result_allocation: NativeArrayResultAllocation release: NativeArrayRelease target_lifetime: str destroy_behavior: NativeArrayDestroyBehavior diff --git a/x2py/wrapper_codegen/planner.py b/x2py/wrapper_codegen/planner.py index 63dd5406a..2ba8ae67b 100644 --- a/x2py/wrapper_codegen/planner.py +++ b/x2py/wrapper_codegen/planner.py @@ -1454,6 +1454,7 @@ def _native_array_handle_plan( setter_action=policy.setter_action, native_assignment=policy.native_assignment, output_projection=policy.output_projection, + result_allocation=policy.result_allocation, release=policy.release, target_lifetime=policy.target_lifetime, destroy_behavior=policy.destroy_behavior, diff --git a/x2py/wrapper_codegen/printers/pyi_printer.py b/x2py/wrapper_codegen/printers/pyi_printer.py index 342f37781..b78a954fd 100644 --- a/x2py/wrapper_codegen/printers/pyi_printer.py +++ b/x2py/wrapper_codegen/printers/pyi_printer.py @@ -16,6 +16,7 @@ ADDRESS_ROLE_PROJECTION, ADDRESS_ROLE_RAW, BIND_TARGET_METADATA, + MAYBE_UNALLOCATED_METADATA, NATIVE_PROJECTION_METADATA, OPTIONAL_ABSENT_HANDLE_METADATA, SCALAR_STORAGE_CATEGORY, @@ -514,6 +515,8 @@ def _semantic_annotation_metadata(self, semantic_type: SemanticType) -> list[str metadata.append(self._contract("Aliased")) if semantic_type.metadata.get(PYTHON_VALUE_MUTABILITY_METADATA) == PYTHON_VALUE_IMMUTABLE: metadata.append(self._contract("Immutable")) + if semantic_type.metadata.get(MAYBE_UNALLOCATED_METADATA): + metadata.append(self._contract("MaybeUnallocated")) pointer_association = semantic_type.metadata.get("fortran_pointer_association") if pointer_association is not None and not self._is_scalar_pointer_descriptor(semantic_type): metadata.append(f"{self._contract('PointerAssociation')}({json.dumps(str(pointer_association))})") From ddbc26bceedc7f522a16a626e230ceb800cf09d8 Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 05:52:34 +0100 Subject: [PATCH 10/35] fix errors by adding maybe unallocaed array --- .../wrapper-plan-migration-checklist.md | 3 +- docs/user/guide/fortran-wrapper.md | 30 +++++---- tests/wrapper/CHECKLIST_COVERAGE.md | 2 +- .../fortran/arrays/test_array_results.py | 61 ++++++++++++++++--- ...test_scalar_derived_actual_dummy_matrix.py | 7 +-- 5 files changed, 74 insertions(+), 29 deletions(-) diff --git a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md index 7179d6923..d54d1dace 100644 --- a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md +++ b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md @@ -594,7 +594,7 @@ summary, the exhaustive matrix, and the test tree disagree. | Status | Collected nodes | | --- | ---: | -| `wrapper-plan` | 358 | +| `wrapper-plan` | 359 | | `dual-route` | 0 | | `legacy` | 0 | | `not-applicable` | 76 | @@ -651,6 +651,7 @@ already covered by the new generator. | `tests/wrapper/fortran/arrays/test_array_contracts.py::*` | source/generated-.pyi parity or parametrized route | ordinary arrays | `wrapper-plan` | | `tests/wrapper/fortran/arrays/test_array_generated_pyi_contracts.py::*` | non-generating: generated semantic .pyi fixture parity | semantic .pyi generation/parsing | `not-applicable` | | `tests/wrapper/fortran/arrays/test_array_results.py::test_array_results_follow_data_buffer_and_descriptor_handle_contracts[*]` | production plan route in source/generated-.pyi parity modes | fixed/runtime-shape ordinary array results; owned allocatable descriptor results; namespace preservation | `wrapper-plan` | +| `tests/wrapper/fortran/arrays/test_array_results.py::test_maybe_unallocated_allocatable_result_preserves_absent_state` | edited semantic `.pyi` contract over the existing array-result native unit | `MaybeUnallocated` direct allocatable vector/matrix result annotations preserve allocated and unallocated result states without changing default always-allocated result handling | `wrapper-plan` | | `tests/wrapper/fortran/arrays/test_array_results.py::test_ordinary_array_results_use_canonical_plan` | canonical production output-only plan route | fixed/runtime-shape ordinary array results; ranks one through fifteen; Fortran order; zero-sized results; allocation/copy/release failure paths | `wrapper-plan` | | `tests/wrapper/fortran/arrays/test_array_results.py::test_owned_allocatable_results_preserve_handle_state` | canonical reduced owned-result contract | allocated and zero-sized wrapper-owned `CFI_CDESC_T` function-result handles; extraction and release | `wrapper-plan` | | `tests/wrapper/fortran/arrays/test_assumed_rank_arrays.py::test_assumed_rank_arguments_dispatch_to_runtime_rank[*]` | source/generated-.pyi parity | ordinary arrays; native-handle actuals deferred to Phase 7 | `wrapper-plan` | diff --git a/docs/user/guide/fortran-wrapper.md b/docs/user/guide/fortran-wrapper.md index ea0356384..933198b6a 100644 --- a/docs/user/guide/fortran-wrapper.md +++ b/docs/user/guide/fortran-wrapper.md @@ -936,6 +936,12 @@ use ordinary return annotations; hidden allocatable array outputs use `Allocatable[T[...]]` handles whose unallocated state remains inside the handle. +Direct allocatable array function results are different: plain +`Allocatable[T[...]]` means the native function must return an allocated +descriptor, using a zero extent for empty data. Use +`Annotated[Allocatable[T[...]], MaybeUnallocated]` only for a direct function +result that may return an unallocated descriptor. + ### Generated Docstrings Generated modules, functions, classes, constructors, methods, overloads, and @@ -1041,18 +1047,19 @@ Allocatable behavior depends on where the allocation lives. Top-level allocatable array function results and non-optional hidden allocatable array outputs return wrapper-owned `AllocatableArray` objects. -Allocated, zero-sized, and unallocated native states all return a present -handle. The handle owns persistent descriptor storage and releases it on -`close()` or finalization. Use a hidden allocatable output dummy when the native -API already expresses the result as an `intent(out)` argument. +The handle owns persistent descriptor storage and releases it on `close()` or +finalization. Plain direct function results must be allocated; use a zero-sized +allocation for empty data. Hidden `intent(out)` allocatable outputs can return +unallocated state portably. A direct function result that may be unallocated +must be annotated as `MaybeUnallocated` in the semantic `.pyi` contract. ```fortran function make_vector(n) result(values) integer, intent(in) :: n real(8), allocatable :: values(:) + allocate(values(max(n, 0))) if (n > 0) then - allocate(values(n)) values = 3.0_8 end if end function make_vector @@ -1064,9 +1071,10 @@ assert values.allocated is True view = values.to_numpy() view[0] = 9.0 -missing = make_vector(0) -assert missing.allocated is False -assert missing.to_numpy() is None +empty = make_vector(0) +assert empty.allocated is True +assert empty.shape == (0,) +assert empty.to_numpy().shape == (0,) ``` ### Allocatable `intent(inout)` Handle Mutation @@ -1219,9 +1227,9 @@ np.testing.assert_array_equal(values, [1.0, 2.0, 3.0, 4.0]) Ordinary returned arrays preserve dtype, rank, required extents, and Fortran ordering for multidimensional results. Numeric results support ranks 1 through 15 and zero-sized dimensions. An allocatable zero-sized -result is a present handle with a zero extent; an unallocated result is a -present handle whose `allocated` property is false and whose `to_numpy()` -result is `None`. +result is a present handle with a zero extent. An unallocated direct +allocatable function result requires `MaybeUnallocated`; then the returned +handle's `allocated` property is false and `to_numpy()` returns `None`. Arrays of derived types are blocked because their element layout, construction, destruction, aliasing, and copy policy are not defined. diff --git a/tests/wrapper/CHECKLIST_COVERAGE.md b/tests/wrapper/CHECKLIST_COVERAGE.md index ab3c9beb2..1906e4475 100644 --- a/tests/wrapper/CHECKLIST_COVERAGE.md +++ b/tests/wrapper/CHECKLIST_COVERAGE.md @@ -90,7 +90,7 @@ recorded progression, not in the live ledger. | Native array handle bridge and binding architecture dispatches from completed descriptor-kind and handle-kind policy pairs | `tests/semantics/policy/test_native_array_ownership.py::test_native_array_handle_dispatcher_routes_completed_policy_to_named_method`, `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_generated_artifacts_follow_one_typed_action_vocabulary`, `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_plan_edits_fail_central_validation` | | Module native-array handles lower from typed borrowed-handle plans to private operation wrappers for state, shape, pointer/descriptor handoff, allocatable `.to_numpy()`/`deallocate()`/`resize(shape)`, pointer `nullify()`, and policy-gated pointer shape-changing operations | `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_module_variables_own_borrowed_handle_plans_and_operation_sets`, `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_generated_artifacts_follow_one_typed_action_vocabulary` | | Generated native array handle construction uses typed operation sets and the runtime factory adapts generated operations to the handle protocol, including shape changes and pointer-address handoff | `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_module_variables_own_borrowed_handle_plans_and_operation_sets`, `tests/runtime/handles/test_factories_and_lifecycle.py::test_generated_handle_factory_adapts_private_operations_to_runtime_protocol`, `tests/runtime/handles/test_factories_and_lifecycle.py::test_generated_handle_factory_splats_shape_operations_to_scalar_extents`, `tests/runtime/handles/test_factories_and_lifecycle.py::test_generated_handle_factory_rejects_invalid_descriptor_kind_and_handoff_result` | -| Owned allocatable results use planned persistent descriptor ownership, collect bridge-local data before transfer, expose owner-addressed operations, and release through the shared handle finalizer path; source and generated-`.pyi` modes retain compiled behavior coverage | `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_numeric_owned_result_defaults_to_assignment_then_move_alloc`, `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_maybe_unallocated_owned_result_uses_collector_without_local_assignment`, `tests/semantics/policy/test_native_array_ownership.py::test_hidden_allocatable_handle_output_completes_as_owned_result_before_lowering`, `tests/runtime/handles/test_factories_and_lifecycle.py::test_generated_owned_handle_factory_passes_persistent_owner_to_every_operation`, `module_state/test_allocatable_views.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles`, `module_state/test_allocatable_views.py::test_maybe_unallocated_direct_allocatable_results_preserve_unallocated_state`, `module_state/test_allocatable_replacement.py::test_allocatable_inout_arrays_mutate_and_return_the_same_handle` | +| Owned allocatable results use planned persistent descriptor ownership, collect bridge-local data before transfer, expose owner-addressed operations, and release through the shared handle finalizer path; source and generated-`.pyi` modes retain compiled behavior coverage | `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_numeric_owned_result_defaults_to_assignment_then_move_alloc`, `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_maybe_unallocated_owned_result_uses_collector_without_local_assignment`, `tests/semantics/policy/test_native_array_ownership.py::test_hidden_allocatable_handle_output_completes_as_owned_result_before_lowering`, `tests/runtime/handles/test_factories_and_lifecycle.py::test_generated_owned_handle_factory_passes_persistent_owner_to_every_operation`, `arrays/test_array_results.py::test_maybe_unallocated_allocatable_result_preserves_absent_state`, `module_state/test_allocatable_views.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles`, `module_state/test_allocatable_views.py::test_maybe_unallocated_direct_allocatable_results_preserve_unallocated_state`, `module_state/test_allocatable_replacement.py::test_allocatable_inout_arrays_mutate_and_return_the_same_handle` | | Native array descriptor arguments record required and optional-presence roles in the typed plan; direct bridge and binding lowering consume those roles while runtime helpers validate descriptor kind, dtype, rank, and shape metadata | `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_keeps_datatype_specific_state_under_argument_and_result_plans`, `tests/wrapper_codegen/test_phase7_native_array_handles.py::test_phase7_generated_artifacts_follow_one_typed_action_vocabulary`, `tests/runtime/handles/test_descriptor_abi.py` | | Concrete-rank numeric array arguments retain the NumPy extraction path and use native handle actuals without converting through `to_numpy()`; optional and assumed-rank behavior remains covered at the public wrapper boundary | `tests/wrapper_codegen/test_phase6a_array_buffers.py::test_required_array_buffer_dispatches_through_named_binding_and_bridge_methods`, `tests/runtime/handles/test_array_actual_abi.py`, `tests/wrapper/fortran/arrays/test_array_contracts.py::test_remaining_array_contracts_are_validated_before_fortran_calls`, `tests/wrapper/fortran/arrays/test_assumed_rank_arrays.py::test_assumed_rank_arguments_dispatch_to_runtime_rank`, `tests/wrapper/fortran/function_calls/test_optional_arguments.py::test_optional_arguments_drive_fortran_present_behavior` | | Runtime normal-array argument packing uses the generated Bind-C array tuple shape for ndarray inputs and native handle array-actual handoff: pointer address, optional runtime rank, optional item size, extents, and optional upper bounds plus unit strides; allocated/associated handles pack without calling `to_numpy()`, and unallocated/unassociated handles block before generated handoff | `tests/runtime/handles/test_array_actual_abi.py::test_array_actual_argument_abi_packer_uses_ndarray_data_pointer_and_shape_fields`, `tests/runtime/handles/test_array_actual_abi.py::test_array_actual_argument_abi_packer_uses_allocatable_native_array_actual_without_numpy_conversion`, `tests/runtime/handles/test_array_actual_abi.py::test_array_actual_argument_abi_packer_uses_pointer_native_array_actual_dtype_metadata`, `tests/runtime/handles/test_array_actual_abi.py::test_array_actual_argument_abi_packer_rejects_absent_handles_before_generated_handoff` | diff --git a/tests/wrapper/fortran/arrays/test_array_results.py b/tests/wrapper/fortran/arrays/test_array_results.py index 04822164d..a93c10df4 100644 --- a/tests/wrapper/fortran/arrays/test_array_results.py +++ b/tests/wrapper/fortran/arrays/test_array_results.py @@ -103,12 +103,6 @@ def test_array_results_follow_data_buffer_and_descriptor_handle_contracts( assert zero_alloc_matrix.shape == (0, 2) assert zero_alloc_matrix.to_numpy().shape == (0, 2) - empty_matrix = module.maybe_alloc_matrix(np.int32(0), np.int32(3)) - assert isinstance(empty_matrix, AllocatableArray) - assert empty_matrix.allocated is False - assert empty_matrix.shape is None - assert empty_matrix.to_numpy() is None - del module gc.collect() np.testing.assert_allclose(matrix, np.array([[12.0, 13.0, 14.0], [22.0, 23.0, 24.0]], dtype=np.float64)) @@ -200,14 +194,61 @@ def test_owned_allocatable_results_preserve_handle_state(tmp_path: Path): assert zero_sized_matrix.shape == (0, 2) assert zero_sized_matrix.to_numpy().shape == (0, 2) + allocated.close() + zero_sized.close() + allocated_matrix.close() + zero_sized_matrix.close() + + +def test_maybe_unallocated_allocatable_result_preserves_absent_state(tmp_path: Path): + """Use an edited contract for direct allocatable results that may be unallocated.""" + native_object = _compile_native_object(ARRAY_RESULTS_F90_SOURCE, tmp_path / "native") + contract_package = tmp_path / "maybe_unallocated_results" + shutil.copytree(CONTRACT_FIXTURES / "farray_results_f90", contract_package) + pyi_path = contract_package / "farray_results_f90.pyi" + contract_text = pyi_path.read_text(encoding="utf-8") + contract_text = contract_text.replace( + "Addr, Allocatable, Arg, Float64, Int32, native_call", + "Addr, Allocatable, Annotated, Arg, Float64, Int32, MaybeUnallocated, native_call", + 1, + ) + contract_text = contract_text.replace( + "def maybe_alloc_vector(\n n: Int32\n) -> Allocatable[Float64[:]]: ...", + "def maybe_alloc_vector(\n n: Int32\n) -> Annotated[Allocatable[Float64[:]], MaybeUnallocated]: ...", + 1, + ) + contract_text = contract_text.replace( + "def maybe_alloc_matrix(\n rows: Int32,\n cols: Int32\n) -> Allocatable[Float64[:, :]]: ...", + "def maybe_alloc_matrix(\n" + " rows: Int32,\n" + " cols: Int32\n" + ") -> Annotated[Allocatable[Float64[:, :]], MaybeUnallocated]: ...", + 1, + ) + pyi_path.write_text(contract_text, encoding="utf-8") + (contract_package / "__init__.pyi").write_text( + "from .farray_results_f90 import maybe_alloc_matrix, maybe_alloc_vector\n", + encoding="utf-8", + ) + result = build_pyi_extension( + contract_package / "__init__.pyi", + native_objects=[native_object], + native_include_dirs=[native_object.parent], + output_dir=tmp_path / "build", + ) + imported = _import_from_build_dir(result.module_name, result.output_dir) + module = imported if hasattr(imported, "maybe_alloc_vector") else _sole_native_module(imported) + + allocated_vector = module.maybe_alloc_vector(np.int32(3)) + assert isinstance(allocated_vector, AllocatableArray) + assert allocated_vector.allocated is True + np.testing.assert_allclose(allocated_vector.to_numpy(), np.array([5.0, 10.0, 15.0])) + unallocated_matrix = module.maybe_alloc_matrix(np.int32(0), np.int32(3)) assert isinstance(unallocated_matrix, AllocatableArray) assert unallocated_matrix.allocated is False assert unallocated_matrix.shape is None assert unallocated_matrix.to_numpy() is None - allocated.close() - zero_sized.close() - allocated_matrix.close() - zero_sized_matrix.close() + allocated_vector.close() unallocated_matrix.close() diff --git a/tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py b/tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py index 7e55ee8a3..f3f7a0c5c 100644 --- a/tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py +++ b/tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py @@ -82,12 +82,7 @@ def scalar_matrix(tmp_path_factory) -> MatrixBuild: native_fortran_sources=[SOURCE], output_dir=output_dir, ) - try: - package = _import_from_build_dir(result.module_name, result.output_dir) - except ImportError as exc: - if "cannot enable executable stack" in str(exc): - pytest.skip("local platform refuses executable-stack wrapper trampolines") - raise + package = _import_from_build_dir(result.module_name, result.output_dir) return MatrixBuild( package.fscalar_derived_actual_dummy_matrix_f90, package.phase8_left_types, From 8e6f6e4684b5d3345db9dedffeeb57c17a9e0031 Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 16:04:48 +0100 Subject: [PATCH 11/35] fix scalar inout hidden variable bug, and namespace flattening when generating from the pyi contract --- docs/user/guide/fortran-wrapper.md | 6 +- docs/user/guide/wrapping-subroutines.md | 16 +++- docs/user/reference/semantic-pyi-format.md | 3 + ...an_conversion_procedures_and_interfaces.py | 34 +++++++ .../test_policy_defaults_and_validation.py | 8 ++ .../test_scalar_writeback_plan.py | 91 ++++++++++++++++++- .../test_calls_and_policy_metadata.py | 19 ++++ x2py/semantics/fortran2ir.py | 25 ++++- x2py/semantics/ownership.py | 9 ++ x2py/wrapper_codegen/printers/pyi_printer.py | 41 +++++++-- 10 files changed, 239 insertions(+), 13 deletions(-) diff --git a/docs/user/guide/fortran-wrapper.md b/docs/user/guide/fortran-wrapper.md index 933198b6a..dfa7973d6 100644 --- a/docs/user/guide/fortran-wrapper.md +++ b/docs/user/guide/fortran-wrapper.md @@ -726,8 +726,10 @@ assert square(3.0) == 9.0 ``` Python immutable scalars cannot expose native in-place mutation. Scalar -`intent(out)` values are hidden and returned as new Python values, while mutable -semantics for strings use replacement projection as described below. +`intent(out)` values are hidden and returned as new Python values. Source-built +primitive scalar `intent(inout)` arguments remain visible inputs and are +returned as replacement values; the original Python scalar object is unchanged. +Mutable semantics for strings use replacement projection as described below. Editable semantic contracts distinguish three numeric scalar boundaries: diff --git a/docs/user/guide/wrapping-subroutines.md b/docs/user/guide/wrapping-subroutines.md index 27097fc4e..57f8f5257 100644 --- a/docs/user/guide/wrapping-subroutines.md +++ b/docs/user/guide/wrapping-subroutines.md @@ -20,7 +20,7 @@ A Fortran `subroutine` has no direct return value. Instead, its `intent(out)` an |-----------------------------|------------------------------|-----------------------------------| | `intent(in)` scalar/array | Visible argument | Not returned | | `intent(out)` scalar | Hidden | Returned as value | -| `intent(inout)` scalar | Hidden (normal case) | Returned as new value | +| `intent(inout)` scalar | Visible argument | Returned as replacement value | | `intent(out)` array | Visible writable NumPy array | Same array, filled and returned | | `intent(inout)` array | Visible writable NumPy array | Mutated in place; normally no extra result | | `intent(out)` allocatable | Hidden (or optional) | `Allocatable[...]` handle | @@ -49,6 +49,12 @@ contains values = factor * values end subroutine scale_in_place + subroutine scale_scalar(value, factor) + real(8), intent(inout) :: value + real(8), intent(in) :: factor + value = factor * value + end subroutine scale_scalar + subroutine fill(values) real(8), intent(out) :: values(:) values = 1.0_8 @@ -81,6 +87,10 @@ data = np.array([4.0, -2.0, 7.0], dtype=np.float64) smallest, largest = api.bounds(data) print(smallest, largest) # -2.0 7.0 +# Scalar inout replacement +updated = api.scale_scalar(np.float64(4.0), np.float64(2.5)) +print(updated) # 10.0 + # In-place mutation arr = np.array([1.0, 2.0, 3.0], dtype=np.float64) api.scale_in_place(arr, np.float64(3.0)) @@ -97,7 +107,9 @@ print(target) # [1. 1. 1. 1.] ## Key Rules -- Scalar `intent(out)` / `intent(inout)` values are **hidden** in the call and returned. +- Scalar `intent(out)` values are hidden in the call and returned. +- Scalar `intent(inout)` values are visible inputs and are also returned as + replacement values; the original Python scalar object is unchanged. - Array `intent(out/inout)` arguments must be pre-allocated by the caller and are mutated in place. - Source-generated `intent(out)` arrays return the same supplied array; diff --git a/docs/user/reference/semantic-pyi-format.md b/docs/user/reference/semantic-pyi-format.md index 06d9a240c..c48ca5fef 100644 --- a/docs/user/reference/semantic-pyi-format.md +++ b/docs/user/reference/semantic-pyi-format.md @@ -414,6 +414,9 @@ For `__init__.pyi`, the package directory name supplies the extension name unless wrapper `--out NAME` is provided. The runtime follows the entry's import policy: `from . import m1` exposes `basic_subroutine.m1`, while `from .m1 import *` explicitly flattens `m1` into the extension root. +Passing a leaf such as `m1.pyi` directly builds a flat extension named `m1` +unless `--out NAME` overrides it, so the same declaration is exposed as +`m1.update(...)` instead of under a package child namespace. A mixed source keeps standalone procedures in the entry contract and marks each one with `@external`: diff --git a/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py b/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py index ff95aef3a..7bc85d91c 100644 --- a/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py +++ b/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py @@ -22,6 +22,7 @@ pytest, semantic_models, ) +from x2py.semantics.metadata import PROJECTED_OUTPUT_METADATA def test_bind_c_name_and_value_calling_convention_reach_semantic_ir(): @@ -380,6 +381,39 @@ def test_scalar_descriptors_record_native_projection_kind(): ] +def test_primitive_scalar_inout_stays_visible_and_projects_replacement_return(): + source = """ +module outputs +contains +subroutine scale_in_place(value, factor) + real(8), intent(inout) :: value + real(8), intent(in) :: factor + value = factor * value +end subroutine scale_in_place +end module outputs +""" + + smod = fortran_module_to_semantic_module(parse_fortran_source(source)) + scale = get_function(smod, "scale_in_place") + + assert scale.arguments[0].metadata[PROJECTED_OUTPUT_METADATA] is True + assert scale.projection == [ + ProjectionMapping( + python_name="value", + native_name="value", + native_position=0, + python_position=0, + result_position=0, + ), + ProjectionMapping( + python_name="factor", + native_name="factor", + native_position=1, + python_position=1, + ), + ] + + def test_function_result(): source = """ module func_mod diff --git a/tests/semantics/policy/test_policy_defaults_and_validation.py b/tests/semantics/policy/test_policy_defaults_and_validation.py index 647175e1f..d9d97d13c 100644 --- a/tests/semantics/policy/test_policy_defaults_and_validation.py +++ b/tests/semantics/policy/test_policy_defaults_and_validation.py @@ -58,6 +58,14 @@ def test_default_policy_decisions_cover_public_object_kinds(): assert scalar.transfer is TransferMode.BY_VALUE assert scalar.codegen_action is CodegenAction.DIRECT_VALUE + scalar_replacement = resolver.decide_semantic_type( + _scalar_type(), + _writable_argument_context(projects_result=True), + ) + assert scalar_replacement.owner is OwnershipOwner.PYTHON + assert scalar_replacement.transfer is TransferMode.COPY_RETURN + assert scalar_replacement.codegen_action is CodegenAction.COPY_IN_OUT + string = resolver.decide_semantic_type(_string_type(), OwnershipContext.result()) assert string.owner is OwnershipOwner.PYTHON assert string.transfer is TransferMode.COPY_RETURN diff --git a/tests/wrapper/fortran/function_calls/test_scalar_writeback_plan.py b/tests/wrapper/fortran/function_calls/test_scalar_writeback_plan.py index b4cf19289..32dd8e5f9 100644 --- a/tests/wrapper/fortran/function_calls/test_scalar_writeback_plan.py +++ b/tests/wrapper/fortran/function_calls/test_scalar_writeback_plan.py @@ -3,6 +3,8 @@ from __future__ import annotations from pathlib import Path +import subprocess +import sys import numpy as np import pytest @@ -12,7 +14,7 @@ _import_from_build_dir, _sole_native_module, ) -from x2py import build_pyi_extension +from x2py import build_fortran_extension, build_pyi_extension def test_scalar_copy_in_out_returns_replacement(tmp_path: Path): @@ -65,3 +67,90 @@ def bump( assert replacement == np.int32(5) with pytest.raises(TypeError): module.bump("bad") + + +def test_source_generated_scalar_inout_contract_returns_replacement_and_keeps_namespace(tmp_path: Path): + source = tmp_path / "outputs.f90" + source.write_text( + """ +module outputs + implicit none +contains + subroutine scale_in_place(value, factor) + real(8), intent(inout) :: value + real(8), intent(in) :: factor + value = factor * value + end subroutine scale_in_place +end module outputs +""", + encoding="utf-8", + ) + + source_result = build_fortran_extension(source, output_dir=tmp_path / "source_build") + source_module = _import_from_build_dir(source_result.module_name, source_result.output_dir) + assert not hasattr(source_module, "scale_in_place") + assert source_module.outputs.scale_in_place(np.float64(4.0), np.float64(2.5)) == np.float64(10.0) + + contract_package = tmp_path / "contracts" / "outputs" + subprocess.run( + [ + sys.executable, + "-m", + "x2py", + "generate", + "--pyi", + str(source), + "--out", + str(contract_package), + ], + capture_output=True, + text=True, + check=True, + ) + entry = contract_package / "__init__.pyi" + leaf = contract_package / "outputs.pyi" + assert entry.read_text(encoding="utf-8") == "from . import outputs\n" + leaf_text = leaf.read_text(encoding="utf-8") + assert ( + 'def scale_in_place(\n value: Float64,\n factor: Float64\n) -> Returns["value", Float64]: ...' + in leaf_text + ) + + native_object = _compile_native_object(source, tmp_path / "native") + package_result = build_pyi_extension( + entry, + native_objects=[native_object], + native_include_dirs=[native_object.parent], + output_dir=tmp_path / "package_build", + ) + sys.modules.pop("outputs.outputs", None) + package_module = _import_from_build_dir(package_result.module_name, package_result.output_dir) + + assert package_result.module_name == "outputs" + assert not hasattr(package_module, "scale_in_place") + assert package_module.outputs.scale_in_place(np.float64(5.0), np.float64(3.0)) == np.float64(15.0) + + leaf_result = build_pyi_extension( + leaf, + native_objects=[native_object], + native_include_dirs=[native_object.parent], + output_name="leaf_outputs", + output_dir=tmp_path / "leaf_build", + ) + leaf_module = _import_from_build_dir(leaf_result.module_name, leaf_result.output_dir) + + assert leaf_module.scale_in_place(np.float64(6.0), np.float64(4.0)) == np.float64(24.0) + assert not hasattr(leaf_module, "outputs") + + entry.write_text("from .outputs import *\n", encoding="utf-8") + flat_result = build_pyi_extension( + entry, + native_objects=[native_object], + native_include_dirs=[native_object.parent], + output_name="flat_outputs", + output_dir=tmp_path / "flat_build", + ) + flat_module = _import_from_build_dir(flat_result.module_name, flat_result.output_dir) + + assert flat_module.scale_in_place(np.float64(7.0), np.float64(5.0)) == np.float64(35.0) + assert not hasattr(flat_module, "outputs") diff --git a/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py b/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py index d59ffe8d5..14f1aef3d 100644 --- a/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py +++ b/tests/wrapper_codegen/printers/test_calls_and_policy_metadata.py @@ -82,6 +82,25 @@ def test_emit_scalar_character_inout_as_replacement_return(): assert f') -> Returns["name", {annotation}]: ...' in code +def test_emit_primitive_scalar_inout_as_visible_replacement_return(): + source = """ +module outputs +contains +subroutine scale_in_place(value, factor) + real(8), intent(inout) :: value + real(8), intent(in) :: factor +end subroutine scale_in_place +end module outputs +""" + + code = generate_pyi(source) + + assert "@native_call([Addr(Arg(0)), Addr(Arg(1))])" in code + assert "value: Float64" in code + assert "factor: Float64" in code + assert ') -> Returns["value", Float64]: ...' in code + + def test_emit_exact_output(): source = """ module simple_mod diff --git a/x2py/semantics/fortran2ir.py b/x2py/semantics/fortran2ir.py index 8a5157037..01c790b9d 100644 --- a/x2py/semantics/fortran2ir.py +++ b/x2py/semantics/fortran2ir.py @@ -24,6 +24,7 @@ ) from x2py.semantics.ownership import set_ownership_metadata from x2py.semantics.metadata import PROJECTED_OUTPUT_METADATA, SCALAR_STORAGE_CATEGORY +from x2py.types.numpy import SEMANTIC_SCALAR_TYPE_NAMES from x2py.utilities.visitor import ClassVisitor from .models import ( @@ -1973,11 +1974,17 @@ def _is_returned_output_argument( *, is_output: bool, semantic_type: SemanticType | None, + is_primitive_scalar_replacement: bool, is_allocatable_replacement: bool, is_character_replacement: bool, is_descriptor_replacement: bool, ) -> bool: - if is_allocatable_replacement or is_character_replacement or is_descriptor_replacement: + if ( + is_primitive_scalar_replacement + or is_allocatable_replacement + or is_character_replacement + or is_descriptor_replacement + ): return True if not is_output or semantic_type is None: return False @@ -2021,9 +2028,15 @@ def _procedure_projection( is_descriptor_replacement = ( reads_argument and writes_argument and FortranToIRConverter._is_scalar_descriptor(arg.semantic_type) ) + is_primitive_scalar_replacement = ( + reads_argument + and writes_argument + and FortranToIRConverter._is_primitive_scalar_replacement(arg.semantic_type) + ) is_returned_output = FortranToIRConverter._is_returned_output_argument( is_output=is_output, semantic_type=arg.semantic_type, + is_primitive_scalar_replacement=is_primitive_scalar_replacement, is_allocatable_replacement=is_allocatable_replacement, is_character_replacement=is_character_replacement, is_descriptor_replacement=is_descriptor_replacement, @@ -2082,6 +2095,16 @@ def _is_scalar_descriptor(semantic_type: SemanticType | None) -> bool: and (semantic_type.metadata.get("fortran_allocatable") or semantic_type.metadata.get("fortran_pointer")) ) + @staticmethod + def _is_primitive_scalar_replacement(semantic_type: SemanticType | None) -> bool: + return bool( + semantic_type is not None + and semantic_type.rank == 0 + and semantic_type.name != "String" + and semantic_type.name in SEMANTIC_SCALAR_TYPE_NAMES + and not FortranToIRConverter._is_scalar_descriptor(semantic_type) + ) + @staticmethod def _scalar_descriptor_kind(semantic_type: SemanticType | None) -> str | None: """Return the ABI-relevant descriptor kind for one rank-zero value.""" diff --git a/x2py/semantics/ownership.py b/x2py/semantics/ownership.py index bbf334117..b2d4f49ad 100644 --- a/x2py/semantics/ownership.py +++ b/x2py/semantics/ownership.py @@ -726,6 +726,15 @@ def _scalar_decision(self, facts: _StorageFacts, context: OwnershipContext) -> O reason="scalar output is returned as a Python value", ) if context.writes_argument and context.reads_argument: + if context.projects_result: + return OwnershipDecision( + ObjectKind.SCALAR, + OwnershipOwner.PYTHON, + TransferMode.COPY_RETURN, + DestructionPolicy.PYTHON_REFCOUNT, + mutates_native=True, + reason="projected scalar update uses call-local native storage and returns a replacement value", + ) return OwnershipDecision( ObjectKind.SCALAR, OwnershipOwner.CALLER, diff --git a/x2py/wrapper_codegen/printers/pyi_printer.py b/x2py/wrapper_codegen/printers/pyi_printer.py index b78a954fd..8c068d9f3 100644 --- a/x2py/wrapper_codegen/printers/pyi_printer.py +++ b/x2py/wrapper_codegen/printers/pyi_printer.py @@ -1445,7 +1445,7 @@ def _projected_return_annotation(self, func: SemanticFunction) -> str: else: parts.append(self._visit(self._visible_wrapped_callable_type(func.return_type))) parts.extend( - self._projected_argument_return(arg, visible=visible) + self._projected_argument_return(func, arg, visible=visible) for _, arg, visible in sorted( self._projected_return_arguments(func), key=lambda item: item[0], @@ -1488,15 +1488,32 @@ def _is_visible_projected_return(func: SemanticFunction, mapping: ProjectionMapp return False return mapping.python_position is not None - def _projected_argument_return(self, arg: SemanticArgument, *, visible: bool) -> str: + def _projected_argument_return( + self, + func_or_arg: SemanticFunction | SemanticArgument, + arg: SemanticArgument | None = None, + *, + visible: bool, + ) -> str: """Handle projected argument return for the current generation context.""" + if isinstance(func_or_arg, SemanticFunction): + if arg is None: + raise TypeError("Function projection return emission requires an argument") + func = func_or_arg + projected_arg = arg + else: + func = None + projected_arg = func_or_arg if visible: - return self._named_return(arg) - return self._plain_projected_return(arg) + return self._named_return(projected_arg, func=func) + return self._plain_projected_return(projected_arg) - def _named_return(self, arg: SemanticArgument) -> str: + def _named_return(self, arg: SemanticArgument, *, func: SemanticFunction | None = None) -> str: """Handle named return for the current generation context.""" - semantic_type = self._visible_projected_type(arg.semantic_type) + semantic_type = self._visible_projected_type( + arg.semantic_type, + unwrap_address_projection=func is not None and self._uses_address_projection(func, arg), + ) descriptor_kind = self._scalar_descriptor_kind(semantic_type) if descriptor_kind is not None: semantic_type = self._visible_scalar_descriptor_type(semantic_type) @@ -1506,13 +1523,23 @@ def _named_return(self, arg: SemanticArgument) -> str: return return_text @staticmethod - def _visible_projected_type(semantic_type: SemanticType) -> SemanticType: + def _visible_projected_type( + semantic_type: SemanticType, + *, + unwrap_address_projection: bool = False, + ) -> SemanticType: """Return the Python-visible type for address-projected scalars.""" wrapped = PyiPrinter._visible_wrapped_callable_type(semantic_type) if wrapped is not semantic_type: return wrapped storage = semantic_type.storage if ( + unwrap_address_projection + and semantic_type.rank == 0 + and storage is not None + and storage.kind in {"address", "reference"} + and storage.pointer_depth == 1 + ) or ( semantic_type.rank == 0 and storage is not None and storage.kind == "address" From a4c14ece0202b493d2f906ba76d4639bb23e26be Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 18:18:36 +0100 Subject: [PATCH 12/35] improve arrays.md --- docs/maintainer/documentation-architecture.md | 3 + .../documentation-content-checklist.md | 9 +- docs/user/guide/arrays.md | 328 +++++++++++++++++- docs/user/guide/data-types.md | 4 +- mkdocs.yml | 4 +- tests/docs/test_structure.py | 31 +- 6 files changed, 350 insertions(+), 29 deletions(-) diff --git a/docs/maintainer/documentation-architecture.md b/docs/maintainer/documentation-architecture.md index 9b6ddfc40..7708134e1 100644 --- a/docs/maintainer/documentation-architecture.md +++ b/docs/maintainer/documentation-architecture.md @@ -72,6 +72,9 @@ sequence. Explicit terminal navigation blocks headed `Next` may link forward because choosing a next destination is their purpose. Outside those blocks, name the later topic in plain text and say that it is covered later instead of asking the reader to leave the current task. +When a section index presents an ordered reading list of pages in that same +section, keep the list and the matching `mkdocs.yml` subsection in the same +order so sidebar next/previous navigation follows the advertised route. `Next` blocks list destinations as bullets, and each bullet includes at least one Markdown link. If an intended destination page does not exist yet, either remove the destination until it is useful or create the draft page with diff --git a/docs/maintainer/roadmap/documentation-content-checklist.md b/docs/maintainer/roadmap/documentation-content-checklist.md index 6db2b5ef4..02b713fd8 100644 --- a/docs/maintainer/roadmap/documentation-content-checklist.md +++ b/docs/maintainer/roadmap/documentation-content-checklist.md @@ -292,24 +292,25 @@ primary placeholder queue. mapping through calls, storage, runtime behavior, and deployment. - [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. - [x] `docs/user/guide/wrapping-functions.md`: maintained scalar, array-result, mixed-output, signature, native-call-limit, and evidence guide. - [x] `docs/user/guide/wrapping-subroutines.md`: maintained input, output, inout, hidden/visible storage, tuple-order, and scalar-replacement guide. - [x] `docs/user/guide/wrapping-modules.md`: maintained module namespace, procedure, constant, variable, saved-state, module-array, and common-block guide. -- [x] `docs/user/guide/arrays.md`: maintained dtype, rank, shape, layout, - stride, lower-bound, assumed-rank, zero-size, result, and validation guide. - [x] `docs/user/guide/optional-arguments.md`: maintained omission, `None`, keyword, input/output, default, limitation, and diagnostic guide. - [x] `docs/user/guide/generic-interfaces.md`: maintained named, type-bound, operator, assignment, exact-dispatch, ambiguity, and overload guide. +- [x] `docs/user/guide/wrapping-derived-types.md`: maintained class, field, + method, constructor, finalizer, nested borrow, layout, and polymorphism guide. - [x] `docs/user/guide/allocatables.md`: maintained scalar projection, copy, replacement, borrowed module/component view, unallocated, lifetime, and limitation guide. - [x] `docs/user/guide/pointers.md`: maintained scalar projection, call-local input, detached result, nullability, target policy, and blocked-reassociation guide. -- [x] `docs/user/guide/wrapping-derived-types.md`: maintained class, field, - method, constructor, finalizer, nested borrow, layout, and polymorphism guide. - [x] `docs/user/guide/memory-management.md`: maintained ownership, transfer, destruction, mutability, release, borrowing, and policy-completion guide. - [x] `docs/user/guide/callbacks.md`: maintained immediate callback contract, diff --git a/docs/user/guide/arrays.md b/docs/user/guide/arrays.md index 5da278209..c148afafb 100644 --- a/docs/user/guide/arrays.md +++ b/docs/user/guide/arrays.md @@ -10,7 +10,15 @@ publication: reviewed # Arrays -x2py passes ordinary numeric Fortran arrays as **NumPy arrays**. Allocatable and pointer arrays use special handle types (`Allocatable[T[...]]` and `Pointer[T[...]]`). The semantic contract clearly specifies element type, rank, shape, layout, strides, and mutability. +x2py passes ordinary Fortran array arguments and array results as **NumPy +arrays**. This page covers the everyday path: create an array with the exact +dtype and layout the generated contract asks for, pass it to the wrapper, and +let x2py validate it before native code runs. + +The contract records the important facts for each array: element dtype, rank, +shape, layout, stride policy, and whether the routine may write to the array. +The sections below start with the normal Fortran-layout path, then show how to +accept common C-order Python arrays and selected strided views intentionally. --- @@ -35,6 +43,36 @@ contains values = values + 1.0_8 end subroutine shift + subroutine sum_columns(size, values, result) + integer(4), intent(in) :: size + real(8), intent(in) :: values(size, size) + real(8), intent(out) :: result(size) + integer(4) :: column + + do column = 1, size + result(column) = sum(values(:, column)) + end do + end subroutine sum_columns + + function sum_flat(count, values) result(total) + integer(4), intent(in) :: count + real(8), intent(in) :: values(*) + real(8) :: total + integer(4) :: index + + total = 0.0_8 + do index = 1, count + total = total + values(index) + end do + end function sum_flat + + subroutine scale_visible_rows(values, out) + real(8), intent(in) :: values(:, :) + real(8), intent(out) :: out(:, :) + + out = 3.0_8 * values + end subroutine scale_visible_rows + function automatic_vector(count) result(values) integer(4), intent(in) :: count real(8) :: values(count) @@ -46,6 +84,11 @@ contains end module array_ops ``` +The first routines cover ordinary Fortran arrays. `sum_columns` makes layout +visible by summing the first native axis of a square matrix, `sum_flat` covers +assumed-size storage, and `scale_visible_rows` accepts an assumed-shape array +that can be strided. + Build it: ```bash @@ -65,47 +108,296 @@ import arrays api = arrays.array_ops -# In-place modification (Fortran order) +# Use Fortran order for a two-dimensional Fortran array. matrix = np.ones((2, 3), dtype=np.float64, order="F") api.scale_matrix(np.int32(2), np.int32(3), matrix) np.testing.assert_array_equal(matrix, np.full((2, 3), 2.0, order="F")) -# Lower-bound aware array +# The Fortran routine uses a lower bound, but Python still sees an ordinary ndarray. shifted = np.zeros(4, dtype=np.float64) api.shift(np.int32(4), shifted) np.testing.assert_array_equal(shifted, np.ones(4, dtype=np.float64)) -# Array return value +# Assumed-size arrays use flat contiguous storage. +flat_values = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64) +assert api.sum_flat(np.int32(flat_values.size), flat_values) == np.float64(10.0) + +# Array function results come back as NumPy arrays. result = api.automatic_vector(np.int32(4)) np.testing.assert_array_equal(result, np.array([2.0, 4.0, 6.0, 8.0], dtype=np.float64)) ``` --- -## Key Concepts +## What x2py Checks - Use **exact NumPy dtypes** (`np.float64`, `np.int32`, etc.). -- For multidimensional arrays intended for Fortran, use `order="F"` or `np.asfortranarray()`. -- The wrapper validates dtype, rank, shape, contiguity, and writeability **before** calling native code. -- No silent casting, copying, or layout conversion happens by default. +- Rank must match the contract: a vector is not a matrix, even when the total + number of elements is the same. +- Shape expressions must match the other arguments, such as `rows` and + `columns` in the example above. +- Arrays written by Fortran must be writable. +- Contiguous contracts reject strided views. Strided contracts accept only the + stride pattern they describe. +- No silent casting, copying, transposing, or layout conversion happens by + default. + +That strictness is intentional. A bad array fails at the Python boundary instead +of producing a confusing native-memory bug. + +Contiguous means the elements are stored without gaps in the layout the +contract names. Fortran-contiguous and C-contiguous arrays can print the same +values but expose a different consecutive memory sequence to native code. +Strided views are useful, but only when the contract and the native routine are +prepared to receive stride metadata. + +--- + +## Layout: Fortran First + +Fortran stores multidimensional arrays in column-major order. For a normal +two-dimensional Fortran array contract, create the NumPy array with +`order="F"` or convert with `np.asfortranarray()` before calling the wrapper: + +```python +values = np.asfortranarray(values, dtype=np.float64) +api.scale_matrix(np.int32(values.shape[0]), np.int32(values.shape[1]), values) +``` + +C-contiguous arrays are useful when a contract explicitly asks for C-order +storage. In semantic `.pyi` contracts, that is normally an explicit layout +annotation such as `ORDER_C`. x2py does not assume that a C-contiguous matrix is +close enough for a Fortran-contiguous contract; it rejects the mismatch so the +native routine sees the layout it was promised. + +--- + +## C-order, Zero Copy + +Many Python users naturally create row-major arrays. Start with the generated +Fortran-oriented contract for `sum_columns`: + +```python +from x2py.contracts import Float64, Int32 + +def sum_columns( + size: Int32, + values: Float64[size, size], + result: Float64[size], +) -> None: ... +``` + +With that contract, pass a Fortran-order matrix. The routine sums columns: + +```python +values = np.array( + [[1.0, 2.0, 3.0], [10.0, 20.0, 30.0], [100.0, 200.0, 300.0]], + dtype=np.float64, + order="F", +) +result = np.empty(values.shape[0], dtype=np.float64) + +api.sum_columns(np.int32(values.shape[0]), values, result) +np.testing.assert_allclose(result, [111.0, 222.0, 333.0]) +``` + +If you intentionally want the same routine to accept a C-contiguous square +matrix without copying, edit the semantic `.pyi` contract to require `ORDER_C`: + +```python +from x2py.contracts import Annotated, Float64, Int32, ORDER_C + +def sum_columns( + size: Int32, + values: Annotated[Float64[size, size], ORDER_C], + result: Float64[size], +) -> None: ... +``` + +The Python call still looks the same, but the array is now C-order. The same +printed matrix produces row sums: + +```python +values = np.array( + [[1.0, 2.0, 3.0], [10.0, 20.0, 30.0], [100.0, 200.0, 300.0]], + dtype=np.float64, + order="C", +) +result = np.empty(values.shape[0], dtype=np.float64) + +api.sum_columns(np.int32(values.shape[0]), values, result) +np.testing.assert_allclose(result, [6.0, 60.0, 600.0]) +``` + +Here `ORDER_C` says the Python-visible array must be C-contiguous. No +transposition happens. For the matrix above, the consecutive C-order storage +sequence is `[1.0, 2.0, 3.0, 10.0, 20.0, 30.0, 100.0, 200.0, 300.0]`, so the +native first-axis groups are Python rows. In Fortran order, the consecutive +sequence is `[1.0, 10.0, 100.0, 2.0, 20.0, 200.0, 3.0, 30.0, 300.0]`, so the +same routine returns column sums instead. x2py treats layout as part of the +contract and rejects the wrong order instead of guessing. + +--- + +## C-order With COPY_F + +Sometimes you want the Python API to accept C-order arrays, but you still want +the native routine to behave exactly like the original Fortran-order call. Add +`COPY_F` beside `ORDER_C`: + +```python +from x2py.contracts import Annotated, COPY_F, Float64, Int32, ORDER_C + +def sum_columns( + size: Int32, + values: Annotated[Float64[size, size], ORDER_C, COPY_F], + result: Float64[size], +) -> None: ... +``` + +Now callers still pass ordinary C-order NumPy arrays, but x2py copies the input +into a Fortran-order temporary before the native call. The result is back to the +original column sums: + +```python +values = np.array( + [[1.0, 2.0, 3.0], [10.0, 20.0, 30.0], [100.0, 200.0, 300.0]], + dtype=np.float64, + order="C", +) +result = np.empty(values.shape[0], dtype=np.float64) + +api.sum_columns(np.int32(values.shape[0]), values, result) +np.testing.assert_allclose(result, [111.0, 222.0, 333.0]) +``` + +`ORDER_C` says what layout Python may pass. `COPY_F` says x2py should create +the Fortran-order representation needed for the native call, preserving the +logical axes. If a visible output array also uses `COPY_F`, x2py copies the +Fortran-order result back into the caller's C-order storage after the call. + +--- + +## Flat Storage + +Fortran assumed-size dummies, such as `values(*)`, do not carry their final +extent in the dummy declaration. The caller supplies a real NumPy array, and a +companion argument such as `count` tells the native routine how much of that +storage to read. + +The generated contract for `sum_flat` uses `Flat`: + +```python +from x2py.contracts import Flat, Float64, Int32 + +def sum_flat( + count: Int32, + values: Float64[Flat], +) -> Float64: ... +``` + +The Python call is just a one-dimensional contiguous array plus the explicit +count: + +```python +values = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64) +total = api.sum_flat(np.int32(values.size), values) +assert total == np.float64(10.0) +``` + +Use `Flat` for native interfaces that consume contiguous storage without a full +shape in the dummy declaration. It is not a shortcut for arbitrary reshaping or +strided slicing. Multidimensional flat-edge forms exist for older storage +interfaces, but the first rule stays the same: the semantic contract describes +the storage the native routine actually consumes. + +--- + +## Strided Views Without Copies + +Some Fortran interfaces accept assumed-shape arrays that do not need contiguous +storage. `scale_visible_rows` is one of those routines. x2py can keep that +useful NumPy behavior visible with a stride-aware contract: + +```python +from x2py.contracts import Float64, Returns + +def scale_visible_rows( + values: Float64[::, ::], + out: Float64[::, ::], +) -> Returns["out", Float64[::, ::]]: ... +``` + +That contract can accept a view where one axis moves through memory with a +stride and the other axis stays dense: + +```python +base = np.asfortranarray( + np.arange(1, 25, dtype=np.float64).reshape((8, 3), order="F") +) + +visible_rows = base[::2, :] # shape (4, 3) +out_storage = np.zeros((8, 3), dtype=np.float64, order="F") +out = out_storage[::2, :] # matching strided output + +api.scale_visible_rows(visible_rows, out) +np.testing.assert_allclose(out, 3.0 * visible_rows) +``` + +The important part is not the slice syntax itself. The important part is that +x2py passes the base address, extents, and positive element strides that Fortran +needs, while still rejecting layouts the contract did not allow. Reversed +slices, broadcasted views, and C-order strided matrices are rejected for this +Fortran-oriented contract. Striding is not an order workaround. + +--- + +## Mutation And Results + +For array arguments, the generated contract tells you who owns the storage and +what changes you should expect: + +| Fortran intent or result | Python behavior | +|--------------------------|-----------------| +| `intent(in)` array | Pass a NumPy array; native code reads it. | +| `intent(inout)` array | Pass a writable NumPy array; native code mutates it in place. | +| `intent(out)` array | Pass preallocated output storage when the contract exposes it. | +| Array function result | Receive a new NumPy array result. | + +Python indexing stays normal NumPy indexing. Fortran lower bounds are part of +the native association rule, not a change to how Python indexes the array. --- ## Common Array Contracts -| Contract | Meaning | -|-----------------------|----------------------------------------------| -| `Float64[:]` | 1D contiguous array | -| `Float64[:, :]` | 2D Fortran-contiguous array | -| `Float64[::]` | 1D strided array | -| `Float64[rows, columns]` | Shape depends on other arguments | -| `Float64[Flat]` | Assumed-size (flat) contiguous storage | -| `Float64[...]` | Assumed-rank (rank 1–15) | +You do not usually write these by hand for source-driven builds, but they are +worth recognizing when you inspect generated contracts: + +| Contract | Meaning | +|----------|---------| +| `Float64[:]` | One-dimensional contiguous array. | +| `Float64[:, :]` | Two-dimensional Fortran-contiguous array. | +| `Annotated[Float64[:, :], ORDER_C]` | Two-dimensional C-contiguous array. | +| `Annotated[Float64[:, :], ORDER_C, COPY_F]` | C-contiguous Python array copied to Fortran order for the native call. | +| `Float64[::]` | One-dimensional strided array. | +| `Float64[::, ::]` | Two-dimensional Fortran-oriented stride-aware array. | +| `Float64[rows, columns]` | Shape depends on other arguments. | +| `Float64[Flat]` | Assumed-size flat storage. | +| `Float64[...]` | Assumed-rank array, currently rank 1 through 15. | + +For most user code, the practical rule is simple: start from the generated +`.pyi`, create NumPy arrays with the exact dtype and layout it names, and let +the wrapper enforce the rest. --- ## Next -- Learn about [Allocatables](allocatables.md) and [Pointers](pointers.md) -- See more examples in [Wrapping Subroutines](wrapping-subroutines.md) +- Continue with [Wrapping Functions](wrapping-functions.md). +- Use [Wrapping Subroutines](wrapping-subroutines.md) when a routine mutates + caller-provided arrays in place. +- Move to [Allocatables](allocatables.md) and [Pointers](pointers.md) later when + the Python API needs native allocation or association state, not just ordinary + NumPy data. - Check the [Language Feature Matrix](../language-support/feature-matrix.md) for supported and unsupported array forms. diff --git a/docs/user/guide/data-types.md b/docs/user/guide/data-types.md index 226044169..29a24d222 100644 --- a/docs/user/guide/data-types.md +++ b/docs/user/guide/data-types.md @@ -114,7 +114,7 @@ assert bool(api.invert(True)) is False A bare primitive type is a Python-visible value: ```python -def scale(value: Float64) -> Float64: ... +def double(value: Float64) -> Float64: ... ``` If the native routine expects that scalar by reference, the generated @@ -122,7 +122,7 @@ If the native routine expects that scalar by reference, the generated ```python @native_call([Addr(Arg(0))]) -def scale(value: Float64) -> Float64: ... +def double(value: Float64) -> Float64: ... ``` Use `T[()]` when the Python boundary is rank-zero NumPy storage. Arguments diff --git a/mkdocs.yml b/mkdocs.yml index a752fa343..3568b2032 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -40,15 +40,15 @@ nav: - User Guide: - Overview: user/guide/index.md - Data Types: user/guide/data-types.md + - Arrays: user/guide/arrays.md - Wrapping Functions: user/guide/wrapping-functions.md - Wrapping Subroutines: user/guide/wrapping-subroutines.md - Wrapping Modules: user/guide/wrapping-modules.md - - Arrays: user/guide/arrays.md - Optional Arguments: user/guide/optional-arguments.md - Generic Interfaces: user/guide/generic-interfaces.md + - Wrapping Derived Types: user/guide/wrapping-derived-types.md - Allocatables: user/guide/allocatables.md - Pointers: user/guide/pointers.md - - Wrapping Derived Types: user/guide/wrapping-derived-types.md - Memory Management: user/guide/memory-management.md - Callbacks: user/guide/callbacks.md - Enumerations: user/guide/enumerations.md diff --git a/tests/docs/test_structure.py b/tests/docs/test_structure.py index 4779eb638..2605ba21b 100644 --- a/tests/docs/test_structure.py +++ b/tests/docs/test_structure.py @@ -44,7 +44,7 @@ "docs/user/reference/cli-commands.md": ("C INCLUDE OPTIONS", "{fortran,c}"), "docs/user/guide/enumerations.md": ("bind(C)",), "docs/user/guide/wrapping-derived-types.md": ("bind(C)",), - "docs/user/guide/arrays.md": ("ORDER_C", "C-contiguous", "C-order", "C-oriented"), + "docs/user/guide/arrays.md": ("ORDER_C", "C-contiguous", "C-order", "C-oriented", 'order="C"'), "docs/user/reference/semantic-pyi-format.md": ( "ORDER_C", "C-contiguous", @@ -135,15 +135,15 @@ REQUIRED_USER_GUIDE_PAGES = [ "user/guide/index.md", "user/guide/data-types.md", + "user/guide/arrays.md", "user/guide/wrapping-functions.md", "user/guide/wrapping-subroutines.md", "user/guide/wrapping-modules.md", - "user/guide/arrays.md", "user/guide/optional-arguments.md", "user/guide/generic-interfaces.md", + "user/guide/wrapping-derived-types.md", "user/guide/allocatables.md", "user/guide/pointers.md", - "user/guide/wrapping-derived-types.md", "user/guide/memory-management.md", "user/guide/callbacks.md", "user/guide/enumerations.md", @@ -551,6 +551,20 @@ def _site_navigation_positions() -> dict[str, int]: return {path: index for index, path in enumerate(paths)} +def _user_guide_index_order() -> list[str]: + _, body = _front_matter(DOCS_ROOT / "user/guide/index.md") + guide_root = (DOCS_ROOT / "user/guide").resolve() + paths: list[str] = [] + for target in MARKDOWN_LINK.findall(body): + resolved = (guide_root / target).resolve() + if resolved.parent != guide_root or resolved.name == "index.md": + continue + relative_path = resolved.relative_to(DOCS_ROOT).as_posix() + if relative_path not in paths: + paths.append(relative_path) + return paths + + @cache def _x2py_cli_help() -> str: commands = [ @@ -868,6 +882,17 @@ def test_site_navigation_includes_all_publishable_lanes_and_excludes_archive() - assert "maintainer/README.md" in positions +def test_user_guide_navigation_follows_index_reading_order() -> None: + positions = _site_navigation_positions() + navigation_order = [ + path + for path, _ in sorted(positions.items(), key=lambda item: item[1]) + if path.startswith("user/guide/") and path != "user/guide/index.md" + ] + + assert navigation_order == _user_guide_index_order() + + @pytest.mark.parametrize("relative_path", REQUIRED_GETTING_STARTED_PAGES) def test_required_getting_started_page_is_maintained_and_navigable(relative_path: str) -> None: path = DOCS_ROOT / relative_path From 38ad7aebd206468d675b3043acb7a97727ff4d33 Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 19:12:40 +0100 Subject: [PATCH 13/35] improve arrays.md and clarify the usage of Flat --- .../recipes/build-multiple-fortran-sources.md | 7 +- .../getting-started/first-wrapped-module.md | 4 +- docs/user/guide/allocatables.md | 8 +- docs/user/guide/arrays.md | 172 +++++++++--------- docs/user/guide/callbacks.md | 6 +- docs/user/guide/data-types.md | 12 +- docs/user/guide/enumerations.md | 14 +- docs/user/guide/fortran-wrapper.md | 13 +- docs/user/guide/generic-interfaces.md | 8 +- docs/user/guide/optional-arguments.md | 12 +- docs/user/guide/pointers.md | 12 +- docs/user/guide/wrapping-derived-types.md | 12 +- docs/user/guide/wrapping-functions.md | 5 +- docs/user/guide/wrapping-modules.md | 4 +- docs/user/guide/wrapping-subroutines.md | 10 +- docs/user/reference/generated-classes.md | 3 +- docs/user/reference/generated-modules.md | 8 +- docs/user/reference/semantic-pyi-format.md | 31 ++-- .../runtime/handles/test_array_actual_abi.py | 45 +++++ tests/semantics/policy/test_wrapper_policy.py | 23 +++ .../fortran/arrays/test_array_contracts.py | 11 ++ .../test_multi_source_builds.py | 28 +++ .../wrapper_codegen/test_phase0d_plan_core.py | 22 +++ .../test_phase6a_array_buffers.py | 5 +- .../test_phase6b_dense_array_shapes.py | 7 + x2py/runtime/handles.py | 17 +- x2py/semantics/wrapper_policy.py | 9 + x2py/wrapper_codegen/c/binding.py | 83 +++++++-- x2py/wrapper_codegen/docstrings.py | 10 +- x2py/wrapper_codegen/generator.py | 7 +- x2py/wrapper_codegen/plan.py | 2 + x2py/wrapper_codegen/planner.py | 2 + 32 files changed, 413 insertions(+), 199 deletions(-) diff --git a/docs/user/examples/recipes/build-multiple-fortran-sources.md b/docs/user/examples/recipes/build-multiple-fortran-sources.md index aff1023ff..1fa710211 100644 --- a/docs/user/examples/recipes/build-multiple-fortran-sources.md +++ b/docs/user/examples/recipes/build-multiple-fortran-sources.md @@ -33,10 +33,11 @@ import sys import numpy as np sys.path.insert(0, "build/multi_api") -import first_api +from first_api.first_api import add_one +from first_api.second_api import double_value -assert first_api.first_api.add_one(np.int32(4)) == np.int32(5) -assert first_api.second_api.double_value(np.int32(4)) == np.int32(10) +assert add_one(np.int32(4)) == np.int32(5) +assert double_value(np.int32(4)) == np.int32(10) ``` ## Ordering Rules diff --git a/docs/user/getting-started/first-wrapped-module.md b/docs/user/getting-started/first-wrapped-module.md index 3812b8580..7aee1f8e9 100644 --- a/docs/user/getting-started/first-wrapped-module.md +++ b/docs/user/getting-started/first-wrapped-module.md @@ -72,9 +72,7 @@ import sys import numpy as np sys.path.insert(0, "build/first-module") -import module_state - -mod = module_state.module_state +import module_state.module_state as mod print(mod.nmax) # 12 print(mod.counter) # 3 diff --git a/docs/user/guide/allocatables.md b/docs/user/guide/allocatables.md index 6c5d0545a..6f3e5eb10 100644 --- a/docs/user/guide/allocatables.md +++ b/docs/user/guide/allocatables.md @@ -93,14 +93,12 @@ import sys import numpy as np sys.path.insert(0, "build/storage") -import storage +from storage.storage import make_values, replace_values -api = storage.storage - -values = api.make_values(np.int32(3)) +values = make_values(np.int32(3)) print(values.to_numpy()) # [2. 4. 6.] -returned = api.replace_values(values) +returned = replace_values(values) assert returned is values # same handle print(values.to_numpy()) # [10. 20.] ``` diff --git a/docs/user/guide/arrays.md b/docs/user/guide/arrays.md index c148afafb..dc97ebfcd 100644 --- a/docs/user/guide/arrays.md +++ b/docs/user/guide/arrays.md @@ -10,15 +10,14 @@ publication: reviewed # Arrays -x2py passes ordinary Fortran array arguments and array results as **NumPy -arrays**. This page covers the everyday path: create an array with the exact -dtype and layout the generated contract asks for, pass it to the wrapper, and -let x2py validate it before native code runs. +x2py passes ordinary Fortran array arguments and results as **NumPy arrays**. +This page shows the everyday path: create arrays with the dtype, shape, layout, +and writeability required by the generated contract, then let x2py validate +those facts before native code runs. -The contract records the important facts for each array: element dtype, rank, -shape, layout, stride policy, and whether the routine may write to the array. -The sections below start with the normal Fortran-layout path, then show how to -accept common C-order Python arrays and selected strided views intentionally. +The examples start with normal Fortran-layout arrays, then show the intentional +cases: accepting C-order arrays, copying C-order arrays into Fortran order, +passing flat assumed-size storage, and using selected strided views. --- @@ -84,12 +83,11 @@ contains end module array_ops ``` -The first routines cover ordinary Fortran arrays. `sum_columns` makes layout -visible by summing the first native axis of a square matrix, `sum_flat` covers -assumed-size storage, and `scale_visible_rows` accepts an assumed-shape array -that can be strided. +`sum_columns` makes layout visible by summing the first native axis of a square +matrix. `sum_flat` covers rank-one assumed-size storage. `scale_visible_rows` +accepts an assumed-shape array that can be strided. -Build it: +Build: ```bash python3 -m x2py arrays.f90 --out-dir build/arrays @@ -104,78 +102,76 @@ import sys import numpy as np sys.path.insert(0, "build/arrays") -import arrays - -api = arrays.array_ops +from arrays.array_ops import ( + automatic_vector, + scale_matrix, + scale_visible_rows, + shift, + sum_columns, + sum_flat, +) -# Use Fortran order for a two-dimensional Fortran array. +# Fortran-order matrix, mutated in place. matrix = np.ones((2, 3), dtype=np.float64, order="F") -api.scale_matrix(np.int32(2), np.int32(3), matrix) -np.testing.assert_array_equal(matrix, np.full((2, 3), 2.0, order="F")) +scale_matrix(np.int32(2), np.int32(3), matrix) +# matrix is now filled with 2.0 -# The Fortran routine uses a lower bound, but Python still sees an ordinary ndarray. +# The Fortran routine uses a lower bound, but Python sees an ordinary ndarray. shifted = np.zeros(4, dtype=np.float64) -api.shift(np.int32(4), shifted) -np.testing.assert_array_equal(shifted, np.ones(4, dtype=np.float64)) +shift(np.int32(4), shifted) +# shifted is now [1.0, 1.0, 1.0, 1.0] -# Assumed-size arrays use flat contiguous storage. -flat_values = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64) -assert api.sum_flat(np.int32(flat_values.size), flat_values) == np.float64(10.0) +# Rank-one assumed-size dummies read the contiguous storage sequence. +flat_matrix = np.asfortranarray( + np.array([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]], dtype=np.float64) +) +total = sum_flat(np.int32(flat_matrix.size), flat_matrix) +# total is np.float64(66.0) # Array function results come back as NumPy arrays. -result = api.automatic_vector(np.int32(4)) -np.testing.assert_array_equal(result, np.array([2.0, 4.0, 6.0, 8.0], dtype=np.float64)) +vec = automatic_vector(np.int32(4)) +# vec is [2.0, 4.0, 6.0, 8.0] ``` --- -## What x2py Checks +## What x2py Validates -- Use **exact NumPy dtypes** (`np.float64`, `np.int32`, etc.). -- Rank must match the contract: a vector is not a matrix, even when the total - number of elements is the same. -- Shape expressions must match the other arguments, such as `rows` and - `columns` in the example above. -- Arrays written by Fortran must be writable. -- Contiguous contracts reject strided views. Strided contracts accept only the - stride pattern they describe. -- No silent casting, copying, transposing, or layout conversion happens by - default. - -That strictness is intentional. A bad array fails at the Python boundary instead -of producing a confusing native-memory bug. +- Exact NumPy dtype, such as `np.float64` or `np.int32`. +- Rank and shape, including shape expressions such as `rows, columns`. +- Required layout and contiguity. +- Writeability for arrays exposed as `intent(out)` or `intent(inout)`. +- The declared stride pattern for stride-aware contracts. +- No silent casting, copying, transposing, or layout conversion by default. Contiguous means the elements are stored without gaps in the layout the contract names. Fortran-contiguous and C-contiguous arrays can print the same values but expose a different consecutive memory sequence to native code. -Strided views are useful, but only when the contract and the native routine are -prepared to receive stride metadata. + +That strictness is intentional. A bad array fails at the Python boundary instead +of producing a confusing native-memory bug. --- ## Layout: Fortran First -Fortran stores multidimensional arrays in column-major order. For a normal -two-dimensional Fortran array contract, create the NumPy array with -`order="F"` or convert with `np.asfortranarray()` before calling the wrapper: +For normal Fortran multidimensional arrays, use Fortran order: ```python -values = np.asfortranarray(values, dtype=np.float64) -api.scale_matrix(np.int32(values.shape[0]), np.int32(values.shape[1]), values) +values = np.asfortranarray(data, dtype=np.float64) +# or +values = np.ones(shape, dtype=np.float64, order="F") ``` -C-contiguous arrays are useful when a contract explicitly asks for C-order -storage. In semantic `.pyi` contracts, that is normally an explicit layout -annotation such as `ORDER_C`. x2py does not assume that a C-contiguous matrix is -close enough for a Fortran-contiguous contract; it rejects the mismatch so the -native routine sees the layout it was promised. +x2py does not assume that a C-contiguous matrix is close enough for a +Fortran-contiguous contract. If the contract asks for Fortran order and you pass +C-order storage, the wrapper rejects the array before native code runs. --- -## C-order, Zero Copy +## Working With C-order Arrays -Many Python users naturally create row-major arrays. Start with the generated -Fortran-oriented contract for `sum_columns`: +Start with the generated Fortran-oriented contract for `sum_columns`: ```python from x2py.contracts import Float64, Int32 @@ -197,10 +193,12 @@ values = np.array( ) result = np.empty(values.shape[0], dtype=np.float64) -api.sum_columns(np.int32(values.shape[0]), values, result) -np.testing.assert_allclose(result, [111.0, 222.0, 333.0]) +sum_columns(np.int32(values.shape[0]), values, result) +# result is [111.0, 222.0, 333.0] ``` +### Option 1: Zero Copy + If you intentionally want the same routine to accept a C-contiguous square matrix without copying, edit the semantic `.pyi` contract to require `ORDER_C`: @@ -225,21 +223,17 @@ values = np.array( ) result = np.empty(values.shape[0], dtype=np.float64) -api.sum_columns(np.int32(values.shape[0]), values, result) -np.testing.assert_allclose(result, [6.0, 60.0, 600.0]) +sum_columns(np.int32(values.shape[0]), values, result) +# result is [6.0, 60.0, 600.0] ``` -Here `ORDER_C` says the Python-visible array must be C-contiguous. No -transposition happens. For the matrix above, the consecutive C-order storage +No transposition happens. For the matrix above, the consecutive C-order storage sequence is `[1.0, 2.0, 3.0, 10.0, 20.0, 30.0, 100.0, 200.0, 300.0]`, so the native first-axis groups are Python rows. In Fortran order, the consecutive sequence is `[1.0, 10.0, 100.0, 2.0, 20.0, 200.0, 3.0, 30.0, 300.0]`, so the -same routine returns column sums instead. x2py treats layout as part of the -contract and rejects the wrong order instead of guessing. +same routine returns column sums instead. ---- - -## C-order With COPY_F +### Option 2: COPY_F Sometimes you want the Python API to accept C-order arrays, but you still want the native routine to behave exactly like the original Fortran-order call. Add @@ -267,8 +261,8 @@ values = np.array( ) result = np.empty(values.shape[0], dtype=np.float64) -api.sum_columns(np.int32(values.shape[0]), values, result) -np.testing.assert_allclose(result, [111.0, 222.0, 333.0]) +sum_columns(np.int32(values.shape[0]), values, result) +# result is [111.0, 222.0, 333.0] ``` `ORDER_C` says what layout Python may pass. `COPY_F` says x2py should create @@ -280,10 +274,9 @@ Fortran-order result back into the caller's C-order storage after the call. ## Flat Storage -Fortran assumed-size dummies, such as `values(*)`, do not carry their final -extent in the dummy declaration. The caller supplies a real NumPy array, and a -companion argument such as `count` tells the native routine how much of that -storage to read. +Fortran assumed-size dummies, such as `values(*)`, do not carry their extent in +the dummy declaration. The caller supplies a real NumPy array, and a companion +argument such as `count` tells the native routine how much storage to read. The generated contract for `sum_flat` uses `Flat`: @@ -296,24 +289,31 @@ def sum_flat( ) -> Float64: ... ``` -The Python call is just a one-dimensional contiguous array plus the explicit -count: +The Python call may pass any contiguous NumPy rank. x2py flattens the storage +sequence to the rank-one native view that `values(*)` expects: ```python -values = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64) -total = api.sum_flat(np.int32(values.size), values) -assert total == np.float64(10.0) +values = np.asfortranarray( + np.array([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]], dtype=np.float64) +) +total = sum_flat(np.int32(values.size), values) +# total is np.float64(66.0) ``` Use `Flat` for native interfaces that consume contiguous storage without a full shape in the dummy declaration. It is not a shortcut for arbitrary reshaping or -strided slicing. Multidimensional flat-edge forms exist for older storage -interfaces, but the first rule stays the same: the semantic contract describes -the storage the native routine actually consumes. +strided slicing. `Float64[Flat]` accepts contiguous arrays of rank 1 through 15, +then passes their element sequence as a rank-one native view. Storage order +still matters: Fortran-contiguous arrays flatten in column-major order, and +C-contiguous arrays flatten in row-major order. + +Multidimensional flat-edge forms are different. `Float64[rows, Flat]` is a +rank-two contract: x2py validates rank two, dtype, Fortran contiguity, and the +`rows` extent, while the final `Flat` axis remains unconstrained. --- -## Strided Views Without Copies +## Strided Views Some Fortran interfaces accept assumed-shape arrays that do not need contiguous storage. `scale_visible_rows` is one of those routines. x2py can keep that @@ -340,11 +340,10 @@ visible_rows = base[::2, :] # shape (4, 3) out_storage = np.zeros((8, 3), dtype=np.float64, order="F") out = out_storage[::2, :] # matching strided output -api.scale_visible_rows(visible_rows, out) -np.testing.assert_allclose(out, 3.0 * visible_rows) +scale_visible_rows(visible_rows, out) +# out now contains 3.0 * visible_rows ``` -The important part is not the slice syntax itself. The important part is that x2py passes the base address, extents, and positive element strides that Fortran needs, while still rejecting layouts the contract did not allow. Reversed slices, broadcasted views, and C-order strided matrices are rejected for this @@ -383,7 +382,8 @@ worth recognizing when you inspect generated contracts: | `Float64[::]` | One-dimensional strided array. | | `Float64[::, ::]` | Two-dimensional Fortran-oriented stride-aware array. | | `Float64[rows, columns]` | Shape depends on other arguments. | -| `Float64[Flat]` | Assumed-size flat storage. | +| `Float64[Flat]` | Assumed-size storage; accepts any contiguous rank and passes a rank-one native view. | +| `Float64[rows, Flat]` | Rank-two Fortran-contiguous storage with a checked first axis and flat final axis. | | `Float64[...]` | Assumed-rank array, currently rank 1 through 15. | For most user code, the practical rule is simple: start from the generated diff --git a/docs/user/guide/callbacks.md b/docs/user/guide/callbacks.md index fd038c069..e59a9fd93 100644 --- a/docs/user/guide/callbacks.md +++ b/docs/user/guide/callbacks.md @@ -118,11 +118,9 @@ import sys import numpy as np sys.path.insert(0, "build/callbacks") -import callbacks +from callbacks.callbacks_api import apply -api = callbacks.callbacks_api - -result = api.apply( +result = apply( lambda value: np.float64(3.0 * value), np.float64(2.5) ) diff --git a/docs/user/guide/data-types.md b/docs/user/guide/data-types.md index 29a24d222..035cfe587 100644 --- a/docs/user/guide/data-types.md +++ b/docs/user/guide/data-types.md @@ -70,14 +70,12 @@ import sys import numpy as np sys.path.insert(0, "build/numeric-types") -import numeric_types +from numeric_types.numeric_types import add_one, conjugate_value, double, invert -api = numeric_types.numeric_types - -assert api.add_one(np.int32(4)) == np.int32(5) -assert api.double(np.float64(1.5)) == np.float64(3.0) -assert api.conjugate_value(np.complex128(1.0 + 2.0j)) == np.complex128(1.0 - 2.0j) -assert bool(api.invert(True)) is False +assert add_one(np.int32(4)) == np.int32(5) +assert double(np.float64(1.5)) == np.float64(3.0) +assert conjugate_value(np.complex128(1.0 + 2.0j)) == np.complex128(1.0 - 2.0j) +assert bool(invert(True)) is False ``` --- diff --git a/docs/user/guide/enumerations.md b/docs/user/guide/enumerations.md index d7158c5d3..1c932e882 100644 --- a/docs/user/guide/enumerations.md +++ b/docs/user/guide/enumerations.md @@ -54,17 +54,15 @@ import sys import numpy as np sys.path.insert(0, "build/colors") -import colors +from colors.colors_api import blue, green, red, round_trip_color, yellow -api = colors.colors_api - -assert api.red == np.int32(-1) -assert api.blue == np.int32(0) -assert api.green == np.int32(10) -assert api.yellow == np.int32(11) +assert red == np.int32(-1) +assert blue == np.int32(0) +assert green == np.int32(10) +assert yellow == np.int32(11) # Pass enumerator values to procedures -result = api.round_trip_color(api.green) +result = round_trip_color(green) assert result == np.int32(10) ``` diff --git a/docs/user/guide/fortran-wrapper.md b/docs/user/guide/fortran-wrapper.md index dfa7973d6..4af8017f7 100644 --- a/docs/user/guide/fortran-wrapper.md +++ b/docs/user/guide/fortran-wrapper.md @@ -1299,12 +1299,13 @@ caller must provide enough storage for the native routine. Generated semantic `.pyi` contracts spell this final assumed-size dimension as `Flat`, for example `Float64[Flat]` for `real(8) :: values(*)`. -`Flat` marks one storage axis rather than forcing rank one. For example, -`Float64[:, Flat]` remains a rank-two Fortran-contiguous Python and bridge -contract. An external interface uses `values(*)` when the preceding extent is -available only from the Python array, because `values(:, *)` is not a legal -Fortran assumed-size declaration; the bridge nevertheless associates the -address with both runtime extents. +`Float64[Flat]` accepts any contiguous NumPy rank from 1 through 15 and passes +the element sequence as a rank-one native view. Multidimensional flat-edge forms +are rank-preserving. `Float64[:, Flat]` remains a rank-two Fortran-contiguous +Python and bridge contract. An external interface uses `values(*)` when the +preceding extent is available only from the Python array because `values(:, *)` +is not a legal Fortran assumed-size declaration; the bridge nevertheless +associates the address with both runtime extents. Non-default lower bounds are preserved when computing shape constraints; they diff --git a/docs/user/reference/semantic-pyi-format.md b/docs/user/reference/semantic-pyi-format.md index e97fcf879..c8d2eea58 100644 --- a/docs/user/reference/semantic-pyi-format.md +++ b/docs/user/reference/semantic-pyi-format.md @@ -496,13 +496,13 @@ the wrapper passes the contiguous element sequence as a rank-one native view. `Float64[3, Flat]` maps to `real :: a(3, *)`, and `Float64[3, 4, Flat]` maps to `real :: a(3, 4, *)`. Those multidimensional -forms are rank-preserving contracts: `Float64[:, Flat]` remains rank two at the -Python and bridge boundary, with only the flat edge unconstrained. Because -`real :: a(:, *)` is not a legal Fortran assumed-size declaration, an external -interface whose prefix extent is known only at runtime uses the -sequence-associated `a(*)` spelling when that procedure requires an explicit -interface; the bridge view still has rank two and receives both runtime -extents. The Python-visible flat dimension remains unconstrained. +forms are flat-edge contracts: the wrapper validates the fixed prefix axes, then +collapses all remaining contiguous Python axes into the final native assumed-size +extent. `Float64[:, Flat]` follows the same rule but reads the prefix extent +from the Python actual. Because `real :: a(:, *)` is not a legal Fortran +assumed-size declaration, source-generated contracts use declared prefix +extents such as `Float64[n, Flat]`; edited contracts may use `:` when the Python +actual should provide that prefix extent. The Python argument may provide more storage than the declared explicit diff --git a/tests/runtime/handles/test_array_actual_abi.py b/tests/runtime/handles/test_array_actual_abi.py index 10fcde815..e36cb7af6 100644 --- a/tests/runtime/handles/test_array_actual_abi.py +++ b/tests/runtime/handles/test_array_actual_abi.py @@ -433,6 +433,40 @@ def test_array_actual_argument_abi_packer_flattens_contiguous_storage_shape(): ) == (values.ctypes.data, values.size) +def test_array_actual_argument_abi_packer_flattens_final_edge_after_checked_prefix(): + values = np.asfortranarray(np.arange(24, dtype=np.float64).reshape((2, 3, 4), order="F")) + + assert _native_array_actual_argument_for_binding_positional( + values, + expected_dtype=np.float64, + expected_rank=2, + expected_shape=(2, None), + expected_layout="F", + require_native_byte_order=True, + require_aligned=True, + require_contiguous=True, + flatten_storage=True, + flat_axis=1, + ) == (values.ctypes.data, 2, 12) + + +def test_array_actual_argument_abi_packer_flattens_leading_edge_before_checked_suffix(): + values = np.arange(24, dtype=np.float64).reshape((2, 3, 4), order="C") + + assert _native_array_actual_argument_for_binding_positional( + values, + expected_dtype=np.float64, + expected_rank=2, + expected_shape=(None, 4), + expected_layout="C", + require_native_byte_order=True, + require_aligned=True, + require_contiguous=True, + flatten_storage=True, + flat_axis=0, + ) == (values.ctypes.data, 6, 4) + + def test_array_actual_argument_abi_packer_flattens_native_handle_shape(): actual = _handoff(252) handle = AllocatableArray( diff --git a/tests/semantics/policy/test_wrapper_policy.py b/tests/semantics/policy/test_wrapper_policy.py index 929102f24..609073c1f 100644 --- a/tests/semantics/policy/test_wrapper_policy.py +++ b/tests/semantics/policy/test_wrapper_policy.py @@ -773,13 +773,59 @@ def sum_flat(n: Int32, values: Float64[Flat]) -> Float64: ... assert argument.array.shape == (":",) assert argument.array.category == "assumed_size" assert argument.array.flatten_python_storage is True + assert argument.array.flat_axis == 0 assert argument.native_array_actual is not None assert argument.native_array_actual.rank == 1 assert argument.native_array_actual.shape == (":",) assert argument.native_array_actual.flatten_storage is True + assert argument.native_array_actual.flat_axis == 0 assert policy.native_call_slots[1].array == argument.array +def test_wrapper_policy_flattens_remaining_axes_for_multidimensional_assumed_size_storage(): + module = parse_pyi_text( + """ +from x2py.contracts import Annotated, Flat, Float64, Int32, ORDER_C + +def sum_fortran(rows: Int32, values: Float64[rows, Flat]) -> Float64: ... +def sum_c(columns: Int32, values: Annotated[Float64[Flat, columns], ORDER_C]) -> Float64: ... +""", + module_name="flat_matrix_argument", + ) + complete_semantic_policies(module) + policies = { + function.name: function.metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] for function in module.functions + } + + fortran_argument = policies["sum_fortran"].arguments[1] + assert fortran_argument.array is not None + assert fortran_argument.array.rank == 2 + assert fortran_argument.array.shape == ("rows", ":") + assert fortran_argument.array.order == "ORDER_F" + assert fortran_argument.array.category == "assumed_size" + assert fortran_argument.array.flatten_python_storage is True + assert fortran_argument.array.flat_axis == 1 + assert fortran_argument.native_array_actual is not None + assert fortran_argument.native_array_actual.rank == 2 + assert fortran_argument.native_array_actual.shape == ("rows", ":") + assert fortran_argument.native_array_actual.flatten_storage is True + assert fortran_argument.native_array_actual.flat_axis == 1 + + c_argument = policies["sum_c"].arguments[1] + assert c_argument.array is not None + assert c_argument.array.rank == 2 + assert c_argument.array.shape == (":", "columns") + assert c_argument.array.order == "ORDER_C" + assert c_argument.array.category == "assumed_size" + assert c_argument.array.flatten_python_storage is True + assert c_argument.array.flat_axis == 0 + assert c_argument.native_array_actual is not None + assert c_argument.native_array_actual.rank == 2 + assert c_argument.native_array_actual.shape == (":", "columns") + assert c_argument.native_array_actual.flatten_storage is True + assert c_argument.native_array_actual.flat_axis == 0 + + def test_wrapper_policy_completes_required_raw_array_address_handoff(): module = parse_pyi_text( """ diff --git a/tests/wrapper_codegen/test_phase6a_array_buffers.py b/tests/wrapper_codegen/test_phase6a_array_buffers.py index e2a1c5798..f993836a7 100644 --- a/tests/wrapper_codegen/test_phase6a_array_buffers.py +++ b/tests/wrapper_codegen/test_phase6a_array_buffers.py @@ -59,6 +59,7 @@ def test_required_array_buffer_has_one_printable_editable_handoff_plan(): assert argument.array.axes == ("dense",) assert argument.array.contiguous is True assert argument.array.flatten_python_storage is False + assert argument.array.flat_axis is None assert argument.array.data_role == argument.binding.handoff_role assert argument.array.extent_roles == (f"{argument.owner_path}:extent:0",) assert argument.array.upper_bound_roles == () @@ -73,8 +74,8 @@ def test_required_array_buffer_dispatches_through_named_binding_and_bridge_metho assert "double bind_c_sum_values(void * values, int64_t values_extent_0);" in c_source assert '"_native_array_actual_argument_for_binding_positional"' in c_source assert ( - 'PyObject_CallFunction(bound_values_helper, "OsiOOiiiiiiii", bound_values_obj, "float64", 1, ' - "bound_values_shape, bound_values_layout, 1, 1, 1, 0, 0, 0, 1, 0)" + 'PyObject_CallFunction(bound_values_helper, "OsiOOiiiiiiiii", bound_values_obj, "float64", 1, ' + "bound_values_shape, bound_values_layout, 1, 1, 1, 0, 0, 0, 1, 0, -1)" ) in c_source assert "bound_values = PyLong_AsVoidPtr(PyTuple_GetItem(bound_values_packed, 0));" in c_source assert "bound_values_extent_0 = (int64_t)PyLong_AsLongLong(PyTuple_GetItem(bound_values_packed, 1));" in c_source diff --git a/tests/wrapper_codegen/test_phase6b_dense_array_shapes.py b/tests/wrapper_codegen/test_phase6b_dense_array_shapes.py index 0110f81d2..32e5b5571 100644 --- a/tests/wrapper_codegen/test_phase6b_dense_array_shapes.py +++ b/tests/wrapper_codegen/test_phase6b_dense_array_shapes.py @@ -117,27 +117,37 @@ def test_dense_array_plan_records_extent_dependencies_flat_storage_and_order(): assert flat.shape == (":",) assert flat.category == "assumed_size" assert flat.flatten_python_storage is True + assert flat.flat_axis == 0 assert flat_rank2_runtime is not None assert flat_rank2_runtime.rank == 2 assert flat_rank2_runtime.shape == (":", ":") assert flat_rank2_runtime.order == "ORDER_F" assert flat_rank2_runtime.category == "assumed_size" - assert flat_rank2_runtime.flatten_python_storage is False + assert flat_rank2_runtime.flatten_python_storage is True + assert flat_rank2_runtime.flat_axis == 1 assert flat_rank2_fixed is not None assert flat_rank2_fixed.rank == 2 assert flat_rank2_fixed.shape == ("3", ":") assert flat_rank2_fixed.order == "ORDER_F" + assert flat_rank2_fixed.flatten_python_storage is True + assert flat_rank2_fixed.flat_axis == 1 assert c_flat_rank2_runtime is not None assert c_flat_rank2_runtime.rank == 2 assert c_flat_rank2_runtime.shape == (":", ":") assert c_flat_rank2_runtime.order == "ORDER_C" assert c_flat_rank2_runtime.category == "assumed_size" + assert c_flat_rank2_runtime.flatten_python_storage is True + assert c_flat_rank2_runtime.flat_axis == 0 assert c_flat_rank2_fixed is not None assert c_flat_rank2_fixed.rank == 2 assert c_flat_rank2_fixed.shape == (":", "3") assert c_flat_rank2_fixed.order == "ORDER_C" + assert c_flat_rank2_fixed.flatten_python_storage is True + assert c_flat_rank2_fixed.flat_axis == 0 assert bounded_flat is not None assert bounded_flat.shape == ("ldb", ":") + assert bounded_flat.flatten_python_storage is True + assert bounded_flat.flat_axis == 1 assert bounded_flat.extent_reference_roles == (("dense_array_shapes.bounded_flat.ldb:value",), ()) @@ -151,10 +161,15 @@ def test_dense_array_lowering_uses_planned_shape_checks_and_bridge_orientation() assert 'bound_values_layout = PyUnicode_FromString("F")' in c_source assert 'bound_values_layout = PyUnicode_FromString("C")' in c_source assert '"_native_array_actual_argument_for_binding_positional"' in c_source - assert "bound_values_shape = Py_None" in c_source + assert "bound_values_shape = PyTuple_New(1)" in c_source + assert "PyTuple_SET_ITEM(bound_values_shape, 0, Py_None)" in c_source assert ( - 'PyObject_CallFunction(bound_values_helper, "OsiOOiiiiiiii", bound_values_obj, "float64", -1, ' - "bound_values_shape, bound_values_layout, 1, 1, 1, 0, 0, 0, 1, 1)" + 'PyObject_CallFunction(bound_values_helper, "OsiOOiiiiiiiii", bound_values_obj, "float64", 1, ' + "bound_values_shape, bound_values_layout, 1, 1, 1, 0, 0, 0, 1, 1, 0)" + ) in c_source + assert ( + 'PyObject_CallFunction(bound_values_helper, "OsiOOiiiiiiiii", bound_values_obj, "float64", 2, ' + "bound_values_shape, bound_values_layout, 1, 1, 1, 0, 0, 0, 1, 1, 1)" ) in c_source assert "call c_f_pointer(bound_values, values, [values_extent_0, values_extent_1])" in bridge_source assert "call c_f_pointer(bound_values, values, [values_extent_1, values_extent_0])" in bridge_source diff --git a/x2py/runtime/handles.py b/x2py/runtime/handles.py index 1bfbb2c97..3c8a0f2fa 100644 --- a/x2py/runtime/handles.py +++ b/x2py/runtime/handles.py @@ -958,6 +958,7 @@ def _native_array_actual_argument_for_binding_positional( include_strides: bool = False, require_contiguous: bool = False, flatten_storage: bool = False, + flat_axis: int | None = None, ) -> tuple[int, ...]: """Pack a normal array actual into generated Bind-C array descriptor fields.""" strided_ndarray = include_strides and isinstance(value, np.ndarray) @@ -978,7 +979,7 @@ def _native_array_actual_argument_for_binding_positional( ) address, shape, itemsize = _normal_array_actual_abi_facts(value, actual, expected_dtype) if flatten_storage: - shape = _flattened_storage_shape(shape) + shape = _flattened_storage_shape(shape, expected_shape, flat_axis) fields = [address] if include_rank: fields.append(len(shape)) @@ -993,14 +994,63 @@ def _native_array_actual_argument_for_binding_positional( return tuple(fields) -def _flattened_storage_shape(shape: tuple[int, ...]) -> tuple[int]: - """Return the one-dimensional element sequence extent for a contiguous actual.""" +def _flattened_storage_shape( + shape: tuple[int, ...], + expected_shape: Sequence[int | None] | int | None, + flat_axis: int | None, +) -> tuple[int, ...]: + """Return native extents for a contiguous actual with one flat edge.""" if not 1 <= len(shape) <= 15: raise TypeError(f"Flat storage expects NumPy array rank 1 through 15; received rank {len(shape)}") + expected = ( + NativeArrayHandleBase._normalize_expected_shape(expected_shape) if expected_shape is not None else (None,) + ) + if len(shape) < len(expected): + raise TypeError(f"Flat storage expects NumPy array rank at least {len(expected)}; received rank {len(shape)}") + axis = 0 if flat_axis is None or int(flat_axis) < 0 else int(flat_axis) + if axis not in {0, len(expected) - 1}: + raise ValueError("Flat storage axis must be the first or final contract dimension") + if axis == 0: + return _leading_flattened_storage_shape(shape, expected) + return _final_flattened_storage_shape(shape, expected) + + +def _final_flattened_storage_shape(shape: tuple[int, ...], expected: tuple[int | None, ...]) -> tuple[int, ...]: + """Keep prefix extents and flatten all remaining axes into the final extent.""" + prefix_count = len(expected) - 1 + _validate_flat_expected_shape(shape[:prefix_count], expected[:prefix_count], offset=0) + return (*shape[:prefix_count], _extent_product(shape[prefix_count:])) + + +def _leading_flattened_storage_shape(shape: tuple[int, ...], expected: tuple[int | None, ...]) -> tuple[int, ...]: + """Flatten leading axes and keep suffix extents at the Python edge.""" + suffix_count = len(expected) - 1 + suffix_shape = shape[len(shape) - suffix_count :] if suffix_count else () + _validate_flat_expected_shape(suffix_shape, expected[1:], offset=len(shape) - suffix_count) + return (_extent_product(shape[: len(shape) - suffix_count]), *suffix_shape) + + +def _extent_product(shape: tuple[int, ...]) -> int: + """Return the element count covered by a flattened extent segment.""" size = 1 for extent in shape: size *= int(extent) - return (size,) + return size + + +def _validate_flat_expected_shape( + actual: tuple[int, ...], + expected: tuple[int | None, ...], + *, + offset: int, +) -> None: + """Validate fixed non-flat dimensions for a flattened storage contract.""" + for axis, (actual_extent, wanted) in enumerate(zip(actual, expected, strict=True)): + if wanted is not None and actual_extent != wanted: + raise TypeError( + f"NumPy array has incompatible shape at axis {offset + axis}: " + f"received {actual!r}, expected {expected!r}" + ) def _normal_array_actual_stride_facts( diff --git a/x2py/semantics/wrapper_policy.py b/x2py/semantics/wrapper_policy.py index ac1db6aee..4d7dd652a 100644 --- a/x2py/semantics/wrapper_policy.py +++ b/x2py/semantics/wrapper_policy.py @@ -854,6 +854,7 @@ class ArrayHandoffPolicy: native_order: str | None contiguous: bool | None flatten_python_storage: bool = False + flat_axis: int | None = None itemsize: int | None = None category: str | None = None extent_references: tuple[tuple[str, ...], ...] = () @@ -885,6 +886,7 @@ class NativeArrayActualPolicy: require_aligned: bool require_contiguous: bool flatten_storage: bool = False + flat_axis: int | None = None @dataclass(frozen=True) @@ -5361,6 +5363,7 @@ def _native_array_actual_policy( require_aligned=True, require_contiguous=array.contiguous is True, flatten_storage=array.flatten_python_storage, + flat_axis=array.flat_axis, ) @@ -5957,6 +5960,7 @@ def _array_handoff_policy(semantic_type: models.SemanticType) -> ArrayHandoffPol native_order=_array_handoff_native_order(array.order, array.copy_order, assumed_rank), contiguous=_array_handoff_contiguous(array.contiguous, assumed_rank, array.category), flatten_python_storage=_array_handoff_flattens_python_storage(array), + 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), @@ -6002,8 +6006,18 @@ def _array_handoff_contiguous(contiguous: bool | None, assumed_rank: bool, categ def _array_handoff_flattens_python_storage(array: models.SemanticArrayContract) -> bool: - """Return whether Python may pass any contiguous rank to a rank-one flat dummy.""" - return bool(array.category == "assumed_size" and array.rank == 1) + """Return whether Python may flatten a contiguous actual through one flat edge.""" + return bool(array.category == "assumed_size" and _array_handoff_flat_axis(array) is not None) + + +def _array_handoff_flat_axis(array: models.SemanticArrayContract) -> int | None: + """Return the concrete flat-edge axis completed by semantic conversion.""" + if array.category != "assumed_size": + return None + for axis, dimension in enumerate(array.source_shape): + if "*" in str(dimension): + return axis + return None def _array_handoff_itemsize(semantic_type: models.SemanticType) -> int | None: diff --git a/x2py/wrapper_codegen/c/binding.py b/x2py/wrapper_codegen/c/binding.py index 7d8a6476f..f6e64114a 100644 --- a/x2py/wrapper_codegen/c/binding.py +++ b/x2py/wrapper_codegen/c/binding.py @@ -6435,13 +6435,14 @@ def _native_array_actual_call_nodes( *self._native_array_actual_layout_nodes(plan, names), CExpressionStatement( CodeExpression( - f'{prefix}_packed = PyObject_CallFunction({prefix}_helper, "OsiOOiiiiiiii", ' + f'{prefix}_packed = PyObject_CallFunction({prefix}_helper, "OsiOOiiiiiiiii", ' f'{names.object_name}, "{actual.dtype}", {self._native_array_actual_expected_rank(actual)}, ' f"{prefix}_shape, {prefix}_layout, " f"{int(actual.writable)}, {int(actual.require_native_byte_order)}, {int(actual.require_aligned)}, " f"{int(plan.array.runtime_rank_role is not None)}, " f"{int(plan.array.itemsize_role is not None)}, {int(bool(plan.array.stride_roles))}, " - f"{int(actual.require_contiguous)}, {int(actual.flatten_storage)})" + f"{int(actual.require_contiguous)}, {int(actual.flatten_storage)}, " + f"{self._native_array_actual_flat_axis(actual)})" ) ), CExpressionStatement(CodeExpression(f"Py_DECREF({prefix}_helper)")), @@ -6461,11 +6462,6 @@ def _native_array_actual_shape_object_nodes( if actual is None: return () prefix = names.value_name - if actual.flatten_storage: - return ( - CExpressionStatement(CodeExpression(f"{prefix}_shape = Py_None")), - CExpressionStatement(CodeExpression("Py_INCREF(Py_None)")), - ) return ( CExpressionStatement(CodeExpression(f"{prefix}_shape = PyTuple_New({actual.rank})")), CExpressionStatement( @@ -6476,7 +6472,12 @@ def _native_array_actual_shape_object_nodes( @staticmethod def _native_array_actual_expected_rank(actual: NativeArrayActualPlan) -> int: """Return the runtime-helper rank selector selected by completed policy.""" - return -1 if actual.flatten_storage else actual.rank + return actual.rank + + @staticmethod + def _native_array_actual_flat_axis(actual: NativeArrayActualPlan) -> int: + """Return the flattened contract axis marker consumed by the runtime helper.""" + return -1 if actual.flat_axis is None else actual.flat_axis def _native_array_actual_shape_nodes( self, @@ -6487,12 +6488,12 @@ def _native_array_actual_shape_nodes( """Build the planned expected-shape tuple for runtime validation.""" actual = plan.native_array_actual array = plan.array - if actual is None or array is None or actual.flatten_storage: + if actual is None or array is None: return () prefix = names.value_name nodes = [] for axis, expression in enumerate(actual.shape): - if expression in {":", "::Strided", "Flat"}: + if expression in {":", "::Strided", "Flat"} or (actual.flatten_storage and axis == actual.flat_axis): nodes.append(CExpressionStatement(CodeExpression("Py_INCREF(Py_None)"))) item = "Py_None" else: @@ -6628,8 +6629,10 @@ def _array_type_and_rank_check( @staticmethod def _array_rank_check_expression(handoff, array: str) -> str: """Render the Python-rank predicate selected by completed array policy.""" - if handoff.rank is None or handoff.flatten_python_storage: + if handoff.rank is None: return f"PyArray_NDIM({array}) < 1 || PyArray_NDIM({array}) > 15" + if handoff.flatten_python_storage: + return f"PyArray_NDIM({array}) < {handoff.rank} || PyArray_NDIM({array}) > 15" return f"PyArray_NDIM({array}) != {handoff.rank}" def _array_access_checks( @@ -6746,10 +6749,11 @@ def _array_shape_checks( if expression in runtime_markers: continue expected = self._array_extent_expression(handoff, axis, expression, context) + actual_axis = self._array_actual_axis_expression(handoff, array, axis) checks.append( CExpressionStatement( CodeExpression( - f"if (PyArray_DIM({array}, {axis}) != (npy_intp)({expected})) {{ " + f"if (PyArray_DIM({array}, {actual_axis}) != (npy_intp)({expected})) {{ " f'PyErr_SetString(PyExc_TypeError, "Argument {plan.binding.python_name} has incompatible ' f'shape at axis {axis}"); return NULL; }}' ) @@ -6757,6 +6761,17 @@ def _array_shape_checks( ) return tuple(checks) + @staticmethod + def _array_actual_axis_expression(handoff, array: str, axis: int) -> str: + """Map one contract axis to the runtime ndarray axis selected by the plan.""" + if not handoff.flatten_python_storage or handoff.flat_axis is None: + return str(axis) + if handoff.flat_axis == 0 and axis > 0: + suffix_count = handoff.rank - 1 + suffix_offset = axis - 1 + return f"(PyArray_NDIM({array}) - {suffix_count} + {suffix_offset})" + return str(axis) + def _array_extent_expression( self, handoff, @@ -6804,9 +6819,7 @@ def _array_extraction_nodes( ) ) if handoff.flatten_python_storage: - nodes.append( - CExpressionStatement(CodeExpression(f"{names.extent_names[0]} = (int64_t)PyArray_SIZE({array})")) - ) + nodes.extend(self._flat_array_extraction_nodes(handoff, names, array)) return tuple(nodes) active_rank = 15 if handoff.rank is None else handoff.rank for axis in range(active_rank): @@ -6820,6 +6833,80 @@ def _array_extraction_nodes( nodes.extend(self._strided_array_extraction_nodes(handoff.rank, names, array)) return tuple(nodes) + def _flat_array_extraction_nodes( + self, + handoff, + names: _CArgumentNames, + array: str, + ) -> tuple[CExpressionStatement | CFor, ...]: + """Compute native extents for a contiguous Python array with one flat edge.""" + if handoff.rank is None or handoff.flat_axis is None: + raise ValueError("Flat array extraction requires a completed concrete flat axis") + if handoff.flat_axis == 0: + return self._leading_flat_array_extraction_nodes(handoff, names, array) + return self._final_flat_array_extraction_nodes(handoff, names, array) + + def _final_flat_array_extraction_nodes( + self, + handoff, + names: _CArgumentNames, + array: str, + ) -> tuple[CExpressionStatement | CFor, ...]: + """Keep prefix extents and flatten all runtime axes at the final flat edge.""" + flat_axis = handoff.flat_axis + nodes: list[CExpressionStatement | CFor] = [ + *( + CExpressionStatement( + CodeExpression(f"{names.extent_names[axis]} = (int64_t)PyArray_DIM({array}, {axis})") + ) + for axis in range(flat_axis) + ), + CExpressionStatement(CodeExpression(f"{names.extent_names[flat_axis]} = 1")), + CFor( + f"int axis = {flat_axis}", + CodeExpression(f"axis < PyArray_NDIM({array})"), + CodeExpression("++axis"), + body=( + CExpressionStatement( + CodeExpression(f"{names.extent_names[flat_axis]} *= (int64_t)PyArray_DIM({array}, axis)") + ), + ), + ), + ] + return tuple(nodes) + + def _leading_flat_array_extraction_nodes( + self, + handoff, + names: _CArgumentNames, + array: str, + ) -> tuple[CExpressionStatement | CFor, ...]: + """Flatten leading runtime axes and keep suffix extents at the Python edge.""" + suffix_count = handoff.rank - 1 + nodes: list[CExpressionStatement | CFor] = [ + CExpressionStatement(CodeExpression(f"{names.extent_names[0]} = 1")), + CFor( + "int axis = 0", + CodeExpression(f"axis < PyArray_NDIM({array}) - {suffix_count}"), + CodeExpression("++axis"), + body=( + CExpressionStatement( + CodeExpression(f"{names.extent_names[0]} *= (int64_t)PyArray_DIM({array}, axis)") + ), + ), + ), + ] + nodes.extend( + CExpressionStatement( + CodeExpression( + f"{names.extent_names[axis]} = (int64_t)PyArray_DIM({array}, " + f"PyArray_NDIM({array}) - {suffix_count} + {axis - 1})" + ) + ) + for axis in range(1, handoff.rank) + ) + return tuple(nodes) + def _strided_array_extraction_nodes( self, rank: int | None, diff --git a/x2py/wrapper_codegen/docstrings.py b/x2py/wrapper_codegen/docstrings.py index b16ff0b7c..c6df7dfe1 100644 --- a/x2py/wrapper_codegen/docstrings.py +++ b/x2py/wrapper_codegen/docstrings.py @@ -583,7 +583,11 @@ def _array_lines(array: ArrayHandoffPlan | None) -> tuple[str, ...]: @staticmethod def _array_rank_line(array: ArrayHandoffPlan) -> str: if array.flatten_python_storage: - return " Rank: 1..15, flattened to native rank 1" + native_rank = 1 if array.rank is None else array.rank + if native_rank == 1: + return " Rank: 1..15, flattened to native rank 1" + edge = "leading" if array.flat_axis == 0 else "final" + return f" Rank: {native_rank}..15, flattened at {edge} Flat axis to native rank {native_rank}" if array.rank is None: return " Rank: 1..15" return f" Rank: {array.rank}" diff --git a/x2py/wrapper_codegen/generator.py b/x2py/wrapper_codegen/generator.py index 316a9774a..ec9c4288d 100644 --- a/x2py/wrapper_codegen/generator.py +++ b/x2py/wrapper_codegen/generator.py @@ -2017,6 +2017,7 @@ def _native_array_actual_shape_diagnostics( and actual.rank == array.rank and actual.shape == array.shape and actual.flatten_storage == array.flatten_python_storage + and actual.flat_axis == array.flat_axis ): return () return (self._diagnostic(plan.owner_path, "inconsistent-array-actual-shape", actual.shape),) diff --git a/x2py/wrapper_codegen/plan.py b/x2py/wrapper_codegen/plan.py index f4b58cf33..33bfa77c9 100644 --- a/x2py/wrapper_codegen/plan.py +++ b/x2py/wrapper_codegen/plan.py @@ -287,6 +287,7 @@ class ArrayHandoffPlan(StageRecord): native_order: str | None contiguous: bool | None flatten_python_storage: bool + flat_axis: int | None itemsize: int | None category: str | None data_role: str @@ -312,6 +313,7 @@ class NativeArrayActualPlan(StageRecord): require_aligned: bool require_contiguous: bool flatten_storage: bool = False + flat_axis: int | None = None @dataclass diff --git a/x2py/wrapper_codegen/planner.py b/x2py/wrapper_codegen/planner.py index ed3ebef3a..c0fb49058 100644 --- a/x2py/wrapper_codegen/planner.py +++ b/x2py/wrapper_codegen/planner.py @@ -1428,6 +1428,7 @@ def _native_array_actual_plan(self, policy: NativeArrayActualPolicy | None) -> N require_aligned=policy.require_aligned, require_contiguous=policy.require_contiguous, flatten_storage=policy.flatten_storage, + flat_axis=policy.flat_axis, ) def _native_array_handle_plan( @@ -1562,6 +1563,7 @@ def _array_plan( native_order=policy.native_order, contiguous=policy.contiguous, flatten_python_storage=policy.flatten_python_storage, + flat_axis=policy.flat_axis, itemsize=policy.itemsize, category=policy.category, data_role=self._value_role(owner_path), From 6b6a87692c1cad274e0ddbd83c90722c5b77e33e Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 20:57:25 +0100 Subject: [PATCH 15/35] improve arrays.md --- docs/user/guide/arrays.md | 266 +++++++++++++++++++------------------- 1 file changed, 134 insertions(+), 132 deletions(-) diff --git a/docs/user/guide/arrays.md b/docs/user/guide/arrays.md index 821a4a1ed..380e9c0e5 100644 --- a/docs/user/guide/arrays.md +++ b/docs/user/guide/arrays.md @@ -1,6 +1,6 @@ --- title: Arrays -description: How to pass NumPy arrays to Fortran routines with x2py — shape, layout, strides, and validation rules +description: NumPy array shape, layout, strides, and validation in x2py audience: users prerequisites: data types, wrapping functions related: allocatables.md, pointers.md, wrapping-subroutines.md @@ -10,19 +10,27 @@ publication: reviewed # Arrays -x2py passes ordinary Fortran array arguments and results as **NumPy arrays**. -This page shows the everyday path: create arrays with the dtype, shape, layout, -and writeability required by the generated contract, then let x2py validate -those facts before native code runs. +x2py exposes Fortran arrays as **NumPy arrays**. +Each generated contract defines the accepted dtype, shape, layout, +writeability, and strides. x2py validates these rules before native code runs. -The examples start with normal Fortran-layout arrays, then show the intentional -cases: accepting C-order arrays, copying C-order arrays into Fortran order, -passing flat assumed-size storage, and using selected strided views. +This page starts with normal Fortran-order arrays. It then covers C-order +arrays, `COPY_F`, `Flat` storage, and strided views. --- ## Complete Example +The page uses one module throughout. Its routines cover: + +- `scale_matrix`: a 2D array mutated in place +- `shift`: lower bounds with normal Python indexing +- `sum_columns`: the effect of storage order +- `sum_flat`: `values(*)` assumed-size storage +- `sum_flat_columns`: a checked prefix and flat final axis +- `scale_visible_rows`: stride-aware assumed-shape arrays +- `automatic_vector`: an array function result + Create `arrays.f90`: ```fortran @@ -97,11 +105,6 @@ contains end module array_ops ``` -`sum_columns` makes layout visible by summing the first native axis of a square -matrix. `sum_flat` covers rank-one assumed-size storage, and -`sum_flat_columns` covers an assumed-size final axis after a checked row count. -`scale_visible_rows` accepts an assumed-shape array that can be strided. - Build: ```bash @@ -129,14 +132,19 @@ shifted = np.zeros(4, dtype=np.float64) shift(np.int32(4), shifted) # shifted is now [1.0, 1.0, 1.0, 1.0] -# Rank-one assumed-size dummies read the contiguous storage sequence. -flat_matrix = np.asfortranarray( - np.array([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]], dtype=np.float64) +# A flat argument can read any contiguous rank. +flat_matrix = np.array( + [ + [1.0, 2.0, 3.0], + [10.0, 20.0, 30.0], + ], + dtype=np.float64, + order="F", ) total = sum_flat(np.int32(flat_matrix.size), flat_matrix) # total is np.float64(66.0) -# Multidimensional assumed-size dummies keep the checked prefix and flatten the rest. +# A flat final axis keeps the prefix and flattens the rest. panels = np.asfortranarray( np.arange(1, 25, dtype=np.float64).reshape((2, 3, 4), order="F") ) @@ -152,25 +160,23 @@ vec = automatic_vector(np.int32(4)) ## What x2py Validates -- Exact NumPy dtype, such as `np.float64` or `np.int32`. -- Rank and shape, including shape expressions such as `rows, columns`. -- Required layout and contiguity. -- Writeability for arrays exposed as `intent(out)` or `intent(inout)`. -- The declared stride pattern for stride-aware contracts. -- No silent casting, copying, transposing, or layout conversion by default. +- Exact NumPy dtype (`np.float64`, `np.int32`, ...) +- Correct rank and shape (including expressions such as `rows, columns`) +- Required layout and contiguity +- Writeability for `intent(out)` or `intent(inout)` arrays +- Declared stride pattern for stride-aware contracts -Contiguous means the elements are stored without gaps in the layout the -contract names. Fortran-contiguous and C-contiguous arrays can print the same -values but expose a different consecutive memory sequence to native code. +**x2py does not silently cast, copy, transpose, or convert layouts.** +A mismatch raises `TypeError` before native code runs. -That strictness is intentional. A bad array fails at the Python boundary instead -of producing a confusing native-memory bug. +Contiguous elements have no gaps between them in the required layout. +Two arrays can print the same values but use different memory orders. --- ## Layout: Fortran First -For normal Fortran multidimensional arrays, use Fortran order: +Use Fortran (column-major) order for normal multidimensional arrays: ```python values = np.asfortranarray(data, dtype=np.float64) @@ -178,13 +184,12 @@ values = np.asfortranarray(data, dtype=np.float64) values = np.ones(shape, dtype=np.float64, order="F") ``` -x2py does not assume that a C-contiguous matrix is close enough for a -Fortran-contiguous contract. If the contract asks for Fortran order and you pass -C-order storage, the wrapper rejects the array before native code runs. +x2py rejects C-contiguous matrices for a Fortran-contiguous contract. +This gives the native routine the layout it expects. --- -## Working With C-order Arrays +## C-order Arrays Start with the generated Fortran-oriented contract for `sum_columns`: @@ -202,7 +207,11 @@ With that contract, pass a Fortran-order matrix. The routine sums columns: ```python values = np.array( - [[1.0, 2.0, 3.0], [10.0, 20.0, 30.0], [100.0, 200.0, 300.0]], + [ + [1.0, 2.0, 3.0], + [10.0, 20.0, 30.0], + [100.0, 200.0, 300.0], + ], dtype=np.float64, order="F", ) @@ -212,10 +221,9 @@ sum_columns(np.int32(values.shape[0]), values, result) # result is [111.0, 222.0, 333.0] ``` -### Option 1: Zero Copy +### Option 1: Accept C-order Without Copy -If you intentionally want the same routine to accept a C-contiguous square -matrix without copying, edit the semantic `.pyi` contract to require `ORDER_C`: +Edit the semantic `.pyi` and add `ORDER_C`: ```python from x2py.contracts import Annotated, Float64, Int32, ORDER_C @@ -227,12 +235,16 @@ def sum_columns( ) -> None: ... ``` -The Python call still looks the same, but the array is now C-order. The same -printed matrix produces row sums: +The call does not change. Pass a C-order array instead. +The same values now produce row sums: ```python values = np.array( - [[1.0, 2.0, 3.0], [10.0, 20.0, 30.0], [100.0, 200.0, 300.0]], + [ + [1.0, 2.0, 3.0], + [10.0, 20.0, 30.0], + [100.0, 200.0, 300.0], + ], dtype=np.float64, order="C", ) @@ -242,17 +254,16 @@ sum_columns(np.int32(values.shape[0]), values, result) # result is [6.0, 60.0, 600.0] ``` -No transposition happens. For the matrix above, the consecutive C-order storage -sequence is `[1.0, 2.0, 3.0, 10.0, 20.0, 30.0, 100.0, 200.0, 300.0]`, so the -native first-axis groups are Python rows. In Fortran order, the consecutive -sequence is `[1.0, 10.0, 100.0, 2.0, 20.0, 200.0, 3.0, 30.0, 300.0]`, so the -same routine returns column sums instead. +No transposition happens. Native code reads the existing storage directly. + +| Python layout | Native grouping | Result | +|---------------|-----------------|--------| +| Fortran order | Python columns | `[111.0, 222.0, 333.0]` | +| C-order | Python rows | `[6.0, 60.0, 600.0]` | -### Option 2: COPY_F +### Option 2: Copy C-order to Fortran Order -Sometimes you want the Python API to accept C-order arrays, but you still want -the native routine to behave exactly like the original Fortran-order call. Add -`COPY_F` beside `ORDER_C`: +Keep `ORDER_C` and add `COPY_F`: ```python from x2py.contracts import Annotated, COPY_F, Float64, Int32, ORDER_C @@ -264,13 +275,16 @@ def sum_columns( ) -> None: ... ``` -Now callers still pass ordinary C-order NumPy arrays, but x2py copies the input -into a Fortran-order temporary before the native call. The result is back to the -original column sums: +x2py copies the input to Fortran order before the native call. +The routine returns the original column sums: ```python values = np.array( - [[1.0, 2.0, 3.0], [10.0, 20.0, 30.0], [100.0, 200.0, 300.0]], + [ + [1.0, 2.0, 3.0], + [10.0, 20.0, 30.0], + [100.0, 200.0, 300.0], + ], dtype=np.float64, order="C", ) @@ -280,18 +294,16 @@ sum_columns(np.int32(values.shape[0]), values, result) # result is [111.0, 222.0, 333.0] ``` -`ORDER_C` says what layout Python may pass. `COPY_F` says x2py should create -the Fortran-order representation needed for the native call, preserving the -logical axes. If a visible output array also uses `COPY_F`, x2py copies the -Fortran-order result back into the caller's C-order storage after the call. +`ORDER_C` validates the caller's layout. +`COPY_F` creates the Fortran-order temporary while preserving logical axes. +For output arrays, x2py copies the result back to the caller's C-order storage. --- ## Flat Storage -Fortran assumed-size dummies, such as `values(*)`, do not carry their extent in -the dummy declaration. The caller supplies a real NumPy array, and a companion -argument such as `count` tells the native routine how much storage to read. +An assumed-size dummy such as `values(*)` does not declare its extent. +A companion argument such as `count` tells the routine how much storage to read. The generated contract for `sum_flat` uses `Flat`: @@ -304,30 +316,33 @@ def sum_flat( ) -> Float64: ... ``` -The Python call may pass any contiguous NumPy rank. x2py flattens the storage -sequence to the rank-one native view that `values(*)` expects: +`Float64[Flat]` accepts a contiguous array with rank 1-15. +Native code sees its storage as rank one: ```python -values = np.asfortranarray( - np.array([[1.0, 2.0, 3.0], [10.0, 20.0, 30.0]], dtype=np.float64) +values = np.array( + [ + [1.0, 2.0, 3.0], + [10.0, 20.0, 30.0], + ], + dtype=np.float64, ) total = sum_flat(np.int32(values.size), values) # total is np.float64(66.0) ``` -Use `Flat` for native interfaces that consume contiguous storage without a full -shape in the dummy declaration. It is not a shortcut for arbitrary reshaping or -strided slicing. +Storage order controls flattening: + +- Fortran-contiguous: column-major order +- C-contiguous: row-major order + +`Flat` rejects strided slices; dtype and contiguity rules still apply. -`Float64[Flat]` accepts any contiguous NumPy array with rank 1 through 15, then -passes its storage sequence as a rank-one native view. Storage order still -matters: Fortran-contiguous arrays flatten in column-major order, and -C-contiguous arrays flatten in row-major order. +`Flat` can appear at one edge of a multidimensional contract. +Other axes remain visible. x2py collapses the remaining Python axes into one +native extent. -Flat can also appear at one edge of a multidimensional contract. The checked -axes stay visible, and x2py collapses all remaining Python axes into the flat -native extent. For a Fortran assumed-size dummy such as -`real(8) :: values(rows, *)`, the generated contract is: +For `real(8) :: values(rows, *)`, the generated contract is: ```python from x2py.contracts import Flat, Float64, Int32 @@ -339,9 +354,8 @@ def sum_flat_columns( ) -> Float64: ... ``` -That contract accepts a Fortran-contiguous actual of rank 2 or higher. A Python -array with shape `(2, 3, 4)` is passed to native code as a rank-two view with -shape `(2, 12)`: +This accepts a Fortran-contiguous array of rank 2 or higher. +Shape `(2, 3, 4)` becomes the native shape `(2, 12)`: ```python panels = np.asfortranarray( @@ -352,19 +366,17 @@ total = sum_flat_columns(np.int32(2), np.int32(12), panels) # total is np.float64(300.0) ``` -The `rows` axis is checked against the first Python axis. The `Flat` axis means -"use the rest of the contiguous storage here"; it does not relax dtype, layout, -or contiguity checks. An edited `Float64[:, Flat]` contract follows the same -rule but reads the first extent from the actual array instead of another -argument. +`Float64[:, Flat]` reads the leading extent from the array itself. + +For C-order buffers, put `Flat` first: +`Annotated[Float64[Flat, columns], ORDER_C]`. +This checks the final Python axis and flattens the leading axes. --- ## Strided Views -Some Fortran interfaces accept assumed-shape arrays that do not need contiguous -storage. `scale_visible_rows` is one of those routines. x2py can keep that -useful NumPy behavior visible with a stride-aware contract: +Use `::` for an assumed-shape axis that supports positive strides: ```python from x2py.contracts import Float64, Returns @@ -375,76 +387,66 @@ def scale_visible_rows( ) -> Returns["out", Float64[::, ::]]: ... ``` -That contract can accept a view where one axis moves through memory with a -stride and the other axis stays dense: +Here, only the first Python axis is sliced: ```python base = np.asfortranarray( np.arange(1, 25, dtype=np.float64).reshape((8, 3), order="F") ) -visible_rows = base[::2, :] # shape (4, 3) +visible_rows = base[::2, :] # shape (4, 3) out_storage = np.zeros((8, 3), dtype=np.float64, order="F") -out = out_storage[::2, :] # matching strided output +out = out_storage[::2, :] # matching strided output scale_visible_rows(visible_rows, out) # out now contains 3.0 * visible_rows ``` -x2py passes the base address, extents, and positive element strides that Fortran -needs, while still rejecting layouts the contract did not allow. Reversed +x2py passes the base address, extents, and positive element strides. Reversed slices, broadcasted views, and C-order strided matrices are rejected for this -Fortran-oriented contract. Striding is not an order workaround. +Fortran-oriented contract. Strides are not an order workaround. --- -## Mutation And Results - -For array arguments, the generated contract tells you who owns the storage and -what changes you should expect: - -| Fortran intent or result | Python behavior | -|--------------------------|-----------------| -| `intent(in)` array | Pass a NumPy array; native code reads it. | -| `intent(inout)` array | Pass a writable NumPy array; native code mutates it in place. | -| `intent(out)` array | Pass preallocated output storage when the contract exposes it. | -| Array function result | Receive a new NumPy array result. | +## Mutation and Results -Python indexing stays normal NumPy indexing. Fortran lower bounds are part of -the native association rule, not a change to how Python indexes the array. +| Fortran intent / result | Python behavior | +|-----------------------------|----------------------------------------------| +| `intent(in)` array | NumPy array read by native code | +| `intent(inout)` array | Writable NumPy array mutated in place | +| `intent(out)` array | Preallocated writable storage | +| Function returning array | New NumPy array result | --- ## Common Array Contracts -You do not usually write these by hand for source-driven builds, but they are -worth recognizing when you inspect generated contracts: - -| Contract | Meaning | -|----------|---------| -| `Float64[:]` | One-dimensional contiguous array. | -| `Float64[:, :]` | Two-dimensional Fortran-contiguous array. | -| `Annotated[Float64[:, :], ORDER_C]` | Two-dimensional C-contiguous array. | -| `Annotated[Float64[:, :], ORDER_C, COPY_F]` | C-contiguous Python array copied to Fortran order for the native call. | -| `Float64[::]` | One-dimensional strided array. | -| `Float64[::, ::]` | Two-dimensional Fortran-oriented stride-aware array. | -| `Float64[rows, columns]` | Shape depends on other arguments. | -| `Float64[Flat]` | Assumed-size storage; accepts any contiguous rank and passes a rank-one native view. | -| `Float64[rows, Flat]` | Fortran-contiguous storage with checked leading axes and remaining axes flattened into the final native extent. | -| `Float64[...]` | Assumed-rank array, currently rank 1 through 15. | - -For most user code, the practical rule is simple: start from the generated -`.pyi`, create NumPy arrays with the exact dtype and layout it names, and let -the wrapper enforce the rest. +`T` means a concrete primitive contract such as `Float64`, `Int32`, or +`Complex128`. The examples above use `Float64` because `arrays.f90` declares +`real(8)`. + +Use this list when reading or editing a generated `.pyi` contract: + +- `T[:]`: 1D contiguous +- `T[:, :]`: 2D Fortran-contiguous +- `Annotated[T[:, :], ORDER_C]`: 2D C-contiguous +- `Annotated[T[:, :], ORDER_C, COPY_F]`: C-order input, Fortran temporary +- `T[::]`: 1D strided +- `T[::, ::]`: 2D stride-aware +- `T[rows, columns]`: shape depends on other arguments +- `T[Flat]`: any contiguous rank, flattened to native rank one +- `T[rows, Flat]`: Fortran-contiguous; checked prefix, remaining axes flattened +- `Annotated[T[Flat, columns], ORDER_C]`: C-contiguous; checked suffix, + leading axes flattened +- `T[...]`: assumed-rank, currently rank 1-15 --- ## Next -- Continue with [Wrapping Functions](wrapping-functions.md). -- Use [Wrapping Subroutines](wrapping-subroutines.md) when a routine mutates - caller-provided arrays in place. -- Move to [Allocatables](allocatables.md) and [Pointers](pointers.md) later when - the Python API needs native allocation or association state, not just ordinary - NumPy data. -- Check the [Language Feature Matrix](../language-support/feature-matrix.md) for supported and unsupported array forms. +- [Wrapping Functions](wrapping-functions.md) and + [Wrapping Subroutines](wrapping-subroutines.md) +- [Allocatables](allocatables.md) and [Pointers](pointers.md) for native + allocation control +- [Language Feature Matrix](../language-support/feature-matrix.md) for support + details From 07bc7d3e02fd5d9918238f3ce4df7869341843af Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 22:35:16 +0100 Subject: [PATCH 16/35] tighten the intent(out/inout) handling for scalars and arrays and dervied types --- .../getting-started/first-wrapped-function.md | 4 +- docs/user/getting-started/index.md | 2 +- docs/user/getting-started/verification.md | 2 +- docs/user/guide/arrays.md | 36 +++--- docs/user/guide/callbacks.md | 2 +- docs/user/guide/data-types.md | 8 +- .../guide/editing-semantic-pyi-contracts.md | 5 +- docs/user/guide/enumerations.md | 8 +- docs/user/guide/fortran-wrapper.md | 109 +++++++++--------- docs/user/guide/generic-interfaces.md | 4 +- docs/user/guide/memory-management.md | 2 +- docs/user/guide/optional-arguments.md | 72 +++++++++--- docs/user/guide/pointers.md | 11 +- docs/user/guide/wrapping-derived-types.md | 11 +- docs/user/guide/wrapping-functions.md | 16 ++- docs/user/guide/wrapping-modules.md | 55 +++++---- docs/user/guide/wrapping-subroutines.md | 16 +-- docs/user/reference/generated-functions.md | 38 +++--- ...an_conversion_procedures_and_interfaces.py | 78 +++++++++++++ .../farray_contracts_f90.pyi | 34 +++--- .../contracts/multid_arrays/multid_arrays.pyi | 16 +-- .../fortran/arrays/test_array_contracts.py | 6 +- .../arrays/test_multidimensional_arrays.py | 8 +- .../fcallback_all_f90/fcallback_all_f90.pyi | 12 +- .../fcallback_array_f90.pyi | 4 +- .../fcallback_derived_f90.pyi | 8 +- .../callbacks/test_all_callback_shapes.py | 14 ++- .../fortran/callbacks/test_array_callbacks.py | 2 +- .../fderived_boundary_f90.pyi | 7 +- .../fderived_boundary_f90.pyi | 7 +- .../test_derived_type_boundaries.py | 4 +- .../derived_types/test_phase8_derived_plan.py | 14 +-- .../fnative_call_examples_f90.pyi | 15 +-- .../contracts/foptional_f90/foptional_f90.pyi | 2 +- .../contracts/foutputs_f90/foutputs_f90.pyi | 17 +-- .../test_native_call_examples.py | 13 +-- .../function_calls/test_optional_arguments.py | 3 +- .../function_calls/test_output_arguments.py | 28 +++-- .../contracts/lapack/__init__.pyi | 44 +++---- .../fscalar_kinds_f90/fscalar_kinds_f90.pyi | 10 +- .../printers/test_types_and_declarations.py | 4 +- x2py/semantics/fortran2ir.py | 25 +++- x2py/wrapper_codegen/docstrings.py | 2 + 43 files changed, 474 insertions(+), 304 deletions(-) diff --git a/docs/user/getting-started/first-wrapped-function.md b/docs/user/getting-started/first-wrapped-function.md index 971aed5a6..a797daa4e 100644 --- a/docs/user/getting-started/first-wrapped-function.md +++ b/docs/user/getting-started/first-wrapped-function.md @@ -50,10 +50,10 @@ sys.path.insert(0, "build/first-function") import scale result = scale.scale(np.float64(3.0), np.float64(2.5)) -print(result) # → 7.5 +print(result) # 7.5 +# The generated Float64 result becomes a Python float. assert isinstance(result, float) -assert result == 7.5 ``` --- diff --git a/docs/user/getting-started/index.md b/docs/user/getting-started/index.md index c345f90e7..8d1139ada 100644 --- a/docs/user/getting-started/index.md +++ b/docs/user/getting-started/index.md @@ -38,7 +38,7 @@ import numpy as np import scale result = scale.scale(np.float64(3.0), np.float64(2.5)) -print(result) # → 7.5 +print(result) # 7.5 ``` The first example exposes a standalone Fortran function directly on the extension. diff --git a/docs/user/getting-started/verification.md b/docs/user/getting-started/verification.md index efcead28e..14d43110e 100644 --- a/docs/user/getting-started/verification.md +++ b/docs/user/getting-started/verification.md @@ -68,7 +68,7 @@ sys.path.insert(0, "build/verify") import scale result = scale.scale(np.float64(3.0), np.float64(2.5)) -print(result) # Should print 7.5 +print(result) # 7.5 ``` --- diff --git a/docs/user/guide/arrays.md b/docs/user/guide/arrays.md index 380e9c0e5..ab887a1e7 100644 --- a/docs/user/guide/arrays.md +++ b/docs/user/guide/arrays.md @@ -17,6 +17,10 @@ writeability, and strides. x2py validates these rules before native code runs. This page starts with normal Fortran-order arrays. It then covers C-order arrays, `COPY_F`, `Flat` storage, and strided views. +Small `intent` note for this page: `intent(in)` reads an array, +`intent(inout)` mutates it, and `intent(out)` fills caller-provided storage. +The subroutines page covers the full return rules. + --- ## Complete Example @@ -125,12 +129,14 @@ from arrays.array_ops import * # Fortran-order matrix, mutated in place. matrix = np.ones((2, 3), dtype=np.float64, order="F") scale_matrix(np.int32(2), np.int32(3), matrix) -# matrix is now filled with 2.0 +print(matrix) +# [[2. 2. 2.] +# [2. 2. 2.]] # The Fortran routine uses a lower bound, but Python sees an ordinary ndarray. shifted = np.zeros(4, dtype=np.float64) shift(np.int32(4), shifted) -# shifted is now [1.0, 1.0, 1.0, 1.0] +print(shifted) # [1. 1. 1. 1.] # A flat argument can read any contiguous rank. flat_matrix = np.array( @@ -142,18 +148,18 @@ flat_matrix = np.array( order="F", ) total = sum_flat(np.int32(flat_matrix.size), flat_matrix) -# total is np.float64(66.0) +print(total) # 66.0 # A flat final axis keeps the prefix and flattens the rest. panels = np.asfortranarray( np.arange(1, 25, dtype=np.float64).reshape((2, 3, 4), order="F") ) panel_total = sum_flat_columns(np.int32(2), np.int32(12), panels) -# panel_total is np.float64(300.0) +print(panel_total) # 300.0 # Array function results come back as NumPy arrays. vec = automatic_vector(np.int32(4)) -# vec is [2.0, 4.0, 6.0, 8.0] +print(vec) # [2. 4. 6. 8.] ``` --- @@ -218,7 +224,7 @@ values = np.array( result = np.empty(values.shape[0], dtype=np.float64) sum_columns(np.int32(values.shape[0]), values, result) -# result is [111.0, 222.0, 333.0] +print(result) # [111. 222. 333.] ``` ### Option 1: Accept C-order Without Copy @@ -251,7 +257,7 @@ values = np.array( result = np.empty(values.shape[0], dtype=np.float64) sum_columns(np.int32(values.shape[0]), values, result) -# result is [6.0, 60.0, 600.0] +print(result) # [ 6. 60. 600.] ``` No transposition happens. Native code reads the existing storage directly. @@ -291,7 +297,7 @@ values = np.array( result = np.empty(values.shape[0], dtype=np.float64) sum_columns(np.int32(values.shape[0]), values, result) -# result is [111.0, 222.0, 333.0] +print(result) # [111. 222. 333.] ``` `ORDER_C` validates the caller's layout. @@ -328,7 +334,7 @@ values = np.array( dtype=np.float64, ) total = sum_flat(np.int32(values.size), values) -# total is np.float64(66.0) +print(total) # 66.0 ``` Storage order controls flattening: @@ -363,7 +369,7 @@ panels = np.asfortranarray( ) total = sum_flat_columns(np.int32(2), np.int32(12), panels) -# total is np.float64(300.0) +print(total) # 300.0 ``` `Float64[:, Flat]` reads the leading extent from the array itself. @@ -399,7 +405,11 @@ out_storage = np.zeros((8, 3), dtype=np.float64, order="F") out = out_storage[::2, :] # matching strided output scale_visible_rows(visible_rows, out) -# out now contains 3.0 * visible_rows +print(out) +# [[ 3. 27. 51.] +# [ 9. 33. 57.] +# [15. 39. 63.] +# [21. 45. 69.]] ``` x2py passes the base address, extents, and positive element strides. Reversed @@ -413,8 +423,8 @@ Fortran-oriented contract. Strides are not an order workaround. | Fortran intent / result | Python behavior | |-----------------------------|----------------------------------------------| | `intent(in)` array | NumPy array read by native code | -| `intent(inout)` array | Writable NumPy array mutated in place | -| `intent(out)` array | Preallocated writable storage | +| `intent(inout)` array | Mutated in place; no Python return | +| `intent(out)` array | Filled in place; no Python return | | Function returning array | New NumPy array result | --- diff --git a/docs/user/guide/callbacks.md b/docs/user/guide/callbacks.md index e59a9fd93..f293df708 100644 --- a/docs/user/guide/callbacks.md +++ b/docs/user/guide/callbacks.md @@ -124,7 +124,7 @@ result = apply( lambda value: np.float64(3.0 * value), np.float64(2.5) ) -assert result == np.float64(7.5) +print(result) # 7.5 ``` --- diff --git a/docs/user/guide/data-types.md b/docs/user/guide/data-types.md index 035cfe587..e2ee15655 100644 --- a/docs/user/guide/data-types.md +++ b/docs/user/guide/data-types.md @@ -72,10 +72,10 @@ import numpy as np sys.path.insert(0, "build/numeric-types") from numeric_types.numeric_types import add_one, conjugate_value, double, invert -assert add_one(np.int32(4)) == np.int32(5) -assert double(np.float64(1.5)) == np.float64(3.0) -assert conjugate_value(np.complex128(1.0 + 2.0j)) == np.complex128(1.0 - 2.0j) -assert bool(invert(True)) is False +print(add_one(np.int32(4))) # 5 +print(double(np.float64(1.5))) # 3.0 +print(conjugate_value(np.complex128(1.0 + 2.0j))) # (1-2j) +print(bool(invert(True))) # False ``` --- diff --git a/docs/user/guide/editing-semantic-pyi-contracts.md b/docs/user/guide/editing-semantic-pyi-contracts.md index 52010ac32..9b81932bc 100644 --- a/docs/user/guide/editing-semantic-pyi-contracts.md +++ b/docs/user/guide/editing-semantic-pyi-contracts.md @@ -334,8 +334,9 @@ The caller supplies scalar storage objects for the visible native scalar slots: ```python base = np.array(4, dtype=np.int32) status = np.empty((), dtype=np.int32) -assert module.scalar_status(base, status) is None -assert status[()] == np.int32(15) +returned = module.scalar_status(base, status) +print(returned) # None +print(status[()]) # 15 ``` This style covers scalar, array, matrix, string, mixed-result, and derived-type diff --git a/docs/user/guide/enumerations.md b/docs/user/guide/enumerations.md index 1c932e882..e99aba3bd 100644 --- a/docs/user/guide/enumerations.md +++ b/docs/user/guide/enumerations.md @@ -51,19 +51,15 @@ python3 -m x2py colors.f90 --out-dir build/colors ```python import sys -import numpy as np sys.path.insert(0, "build/colors") from colors.colors_api import blue, green, red, round_trip_color, yellow -assert red == np.int32(-1) -assert blue == np.int32(0) -assert green == np.int32(10) -assert yellow == np.int32(11) +print(red, blue, green, yellow) # -1 0 10 11 # Pass enumerator values to procedures result = round_trip_color(green) -assert result == np.int32(10) +print(result) # 10 ``` --- diff --git a/docs/user/guide/fortran-wrapper.md b/docs/user/guide/fortran-wrapper.md index 3e3922823..1f983e98e 100644 --- a/docs/user/guide/fortran-wrapper.md +++ b/docs/user/guide/fortran-wrapper.md @@ -173,7 +173,8 @@ import numpy as np sys.path.insert(0, "build/fruntime_abi") import fruntime_abi_f90 -assert fruntime_abi_f90.scale(np.float64(3.0), np.float64(2.5)) == np.float64(7.5) +result = fruntime_abi_f90.scale(np.float64(3.0), np.float64(2.5)) +print(result) # 7.5 ``` Native scalar arguments use their exact NumPy dtype. x2py rejects a Python @@ -482,8 +483,6 @@ Example 2: a `.pyi` build from a native object keeps semantic contracts and native artifacts separate. ```python -from pathlib import Path - from x2py import build_pyi_extension result = build_pyi_extension( @@ -493,8 +492,8 @@ result = build_pyi_extension( output_dir="build/solver", ) -assert result.sources[0] == Path("contracts/solver/__init__.pyi") -assert result.native_build_plan.prebuilt_artifacts[0].kind == "object" +print(result.sources[0]) # contracts/solver/__init__.pyi +print(result.native_build_plan.prebuilt_artifacts[0].kind) # object ``` Example 3: an object followed by a static archive remains ordered in the link @@ -517,8 +516,6 @@ print([item.to_dict() for item in result.native_build_plan.link_items]) Example 4: a direct shared-library path is distinct from a named library. ```python -from pathlib import Path - from x2py import build_pyi_extension result = build_pyi_extension( @@ -529,8 +526,8 @@ result = build_pyi_extension( ) artifact = result.native_build_plan.prebuilt_artifacts[0] -assert artifact.kind == "shared_library" -assert result.native_build_plan.library_dirs == (Path("vendor"),) +print(artifact.kind) # shared_library +print([str(path) for path in result.native_build_plan.library_dirs]) # ['vendor'] ``` Example 5: the ordered representation can express linker control arguments for @@ -672,7 +669,7 @@ view[0] = 9.0 # mutates b%values independent = view.copy() del b # handle and view retain the wrapper owner chain -print(view[0]) +print(view[0]) # 9.0 ``` If a later method reallocates `values`, an older borrowed view is not @@ -722,7 +719,7 @@ end function square ``` ```python -assert square(3.0) == 9.0 +print(square(3.0)) # 9.0 ``` Python immutable scalars cannot expose native in-place mutation. Scalar @@ -852,15 +849,17 @@ end subroutine bounds smallest, largest = bounds(values) ``` -Scalar character and scalar derived-type outputs follow the same hidden-output -shape and return a new `str` or wrapper-owned instance. +Scalar character outputs follow the same hidden-output shape and return a new +`str`. Scalar derived-type objects use caller-provided mutable wrappers, as +described below. ### Caller-Provided Array Outputs A non-allocatable array `intent(out)` remains visible because the caller must provide storage. The wrapper validates dtype, rank, shape, layout, alignment, native byte order, and writeability. Fortran writes into the object and the same -object is returned. +object exposes the result. The wrapper returns `None` unless another output +requires a Python return value. ```fortran subroutine fill(values) @@ -871,15 +870,15 @@ end subroutine fill ```python values = np.empty(4, dtype=np.float64) -returned = fill(values) - -assert returned is values -np.testing.assert_allclose(values, np.ones(4)) +fill(values) +print(values) # [1. 1. 1. 1.] ``` The initial contents of an `intent(out)` array are ignored. An `intent(inout)` -array also remains visible and is mutated in place, but it is not duplicated in -the return value unless other outputs require a tuple. +array also remains visible and is mutated in place. Neither ordinary array is +duplicated in the return value. Array function results and hidden allocatable +outputs still return Python-visible objects because the caller did not provide +their storage. ### Allocatable Outputs @@ -901,12 +900,12 @@ end subroutine build_values ```python values = build_values(3) -assert values.allocated is True -np.testing.assert_allclose(values.to_numpy(), np.full(3, 2.0)) +print(values.allocated) # True +print(values.to_numpy()) # [2. 2. 2.] missing = build_values(0) -assert missing.allocated is False -assert missing.to_numpy() is None +print(missing.allocated) # False +print(missing.to_numpy()) # None ``` Failure to allocate owned descriptor storage after Fortran produced a result @@ -932,9 +931,10 @@ value, status, message = analyze(2.0) ``` Generated `.pyi` signatures and NumPy-style docstrings use the same projection. -`Returns["name", T]` is reserved for a returned value that also remains a -Python-visible argument, such as caller-provided output storage. Hidden outputs -use ordinary return annotations; hidden allocatable array outputs use +`Returns["name", T]` is reserved for an explicit replacement projection that +also remains a Python-visible argument. Generated ordinary writable arrays use +in-place mutation without this projection. Hidden outputs use ordinary return +annotations; hidden allocatable array outputs use `Allocatable[T[...]]` handles whose unallocated state remains inside the handle. @@ -1021,7 +1021,7 @@ X2PY_C_DOCS_END --> @@ -1069,14 +1069,14 @@ end function make_vector ```python values = make_vector(4) -assert values.allocated is True +print(values.allocated) # True view = values.to_numpy() view[0] = 9.0 empty = make_vector(0) -assert empty.allocated is True -assert empty.shape == (0,) -assert empty.to_numpy().shape == (0,) +print(empty.allocated) # True +print(empty.shape) # (0,) +print(empty.to_numpy().shape) # (0,) ``` ### Allocatable `intent(inout)` Handle Mutation @@ -1101,7 +1101,7 @@ values = make_vector(2) returned = replace_values(values) assert returned is values -np.testing.assert_array_equal(values.to_numpy(), [10.0, 20.0]) +print(values.to_numpy()) # [10. 20.] ``` ### Allocatable Fields And Module Arrays @@ -1162,7 +1162,7 @@ end function total ``` ```python -assert total(pointer_handle) == 6.0 +print(total(pointer_handle)) # 6.0 ``` The pointer-descriptor signature requires a `Pointer[T[...]]` handle; a plain @@ -1222,8 +1222,9 @@ end function spectrum ```python values = spectrum(4) +# The array owns its data or retains the Python owner through its base. assert values.flags.owndata or values.base is not None -np.testing.assert_array_equal(values, [1.0, 2.0, 3.0, 4.0]) +print(values) # [1. 2. 3. 4.] ``` Ordinary returned arrays preserve dtype, rank, required extents, and Fortran @@ -1340,7 +1341,7 @@ end subroutine shift ```python values = np.zeros(4, dtype=np.float64) shift(4, values) -np.testing.assert_array_equal(values, np.ones(4)) +print(values) # [1. 1. 1. 1.] ``` ### Assumed Rank @@ -1385,7 +1386,8 @@ X2PY_C_DOCS_END --> - `intent(in)` passes the existing native instance by address without transferring ownership; - `intent(inout)` mutates that existing instance; -- hidden `intent(out)` produces a new wrapper-owned object; and +- `intent(out)` fills a caller-provided instance without returning it again; + and - a function result is copied into a new wrapper-owned native instance before the Fortran temporary expires. @@ -1405,7 +1407,7 @@ end subroutine move_point ```python p = point(x=1.0, y=2.0) move_point(p, 3.0, 4.0) -assert (p.x, p.y) == (4.0, 6.0) +print(p.x, p.y) # 4.0 6.0 ``` ### Nested Components @@ -1460,7 +1462,7 @@ end type circle ```python c = circle(radius=2.0) assert isinstance(c, shape) -assert c.area() == pytest.approx(12.566370614359172) +print(c.area()) # 12.566370614359172 ``` A scalar `class(base), intent(in)` dummy dispatches over the closed set of @@ -1573,12 +1575,12 @@ end module state ``` ```python -assert counter == 0 +print(counter) # 0 counter = np.int32(4) advance() -assert counter == 5 +print(counter) # 5 -assert max_count == 100 +print(max_count) # 100 ``` Parameters become `Final[...]` constants when their value is representable as @@ -1592,7 +1594,7 @@ Allocatable module arrays are attributes returning persistent ```python allocate_values(3) handle = values -assert handle.allocated is True +print(handle.allocated) # True view = handle.to_numpy() view[0] = 5.0 # writes native module storage @@ -1621,7 +1623,7 @@ end subroutine write_shared ```python write_shared(np.int32(17)) -assert read_shared() == 17 +print(read_shared()) # 17 ``` x2py adds no independent lock for module or object state. Concurrency rules are @@ -1689,8 +1691,8 @@ end subroutine edit_name original = "alpha " replacement = edit_name(original) -assert original == "alpha " # Python str is immutable -assert replacement.startswith("X") +print(repr(original)) # 'alpha ' (unchanged) +print(repr(replacement)) # 'Xlpha ' ``` The wrapper copies the input into mutable native storage, calls Fortran, and @@ -1714,7 +1716,7 @@ end function label ``` ```python -assert label() == "ready " +print(repr(label())) # 'ready ' ``` @@ -2066,7 +2068,8 @@ end function apply ``` ```python -assert apply(lambda value: 3.0 * value, np.float64(2.5)) == 7.5 +result = apply(lambda value: 3.0 * value, np.float64(2.5)) +print(result) # 7.5 ``` The generated wrapper keeps a strong reference to the callback only until the @@ -2107,7 +2110,7 @@ def double(array): array *= 2.0 transform(double, values) -np.testing.assert_array_equal(values, [2.0, 2.0, 2.0]) +print(values) # [2. 2. 2.] ``` ### GIL, Threads, And Exceptions @@ -2198,7 +2201,7 @@ make -f build/Makefile.x2py \ ```python values = np.arange(1, 33, dtype=np.float64) -assert parallel_sum(values) == np.sum(values) +print(parallel_sum(values)) # 528.0 ``` x2py does not infer host-memory synchronization. Callers must protect arrays, diff --git a/docs/user/guide/generic-interfaces.md b/docs/user/guide/generic-interfaces.md index 62263d294..e9b01f6ac 100644 --- a/docs/user/guide/generic-interfaces.md +++ b/docs/user/guide/generic-interfaces.md @@ -61,8 +61,8 @@ import numpy as np sys.path.insert(0, "build/generic") from generic.conversions import convert -assert convert(np.int32(4)) == np.int32(14) -assert convert(np.float64(4.0)) == np.float64(4.5) +print(convert(np.int32(4))) # 14 +print(convert(np.float64(4.0))) # 4.5 ``` The correct specific procedure is chosen automatically based on the argument type. diff --git a/docs/user/guide/memory-management.md b/docs/user/guide/memory-management.md index c5c78a171..7bd97198a 100644 --- a/docs/user/guide/memory-management.md +++ b/docs/user/guide/memory-management.md @@ -95,7 +95,7 @@ values = np.ones(4, dtype=np.float64) api.scale(values) # Same array object, possibly mutated in place. -assert values[0] == 2.0 +print(values[0]) # 2.0 ``` The wrapper may mutate through a caller-owned object when the contract allows diff --git a/docs/user/guide/optional-arguments.md b/docs/user/guide/optional-arguments.md index fd4f65113..2da48ccc4 100644 --- a/docs/user/guide/optional-arguments.md +++ b/docs/user/guide/optional-arguments.md @@ -10,7 +10,8 @@ publication: reviewed # Optional Arguments -x2py supports optional scalars, arrays, strings, derived types, and outputs while preserving native `present(...)` semantics. +x2py supports optional scalars, arrays, strings, derived types, and outputs. +It preserves native `present(...)` semantics. --- @@ -31,6 +32,20 @@ contains if (present(offset)) output = output + offset end function adjust + subroutine make_values(size, count, values) + integer(4), intent(in) :: size + integer(4), intent(out) :: count + real(8), intent(out), optional :: values(size) + integer(4) :: index + + count = size + if (present(values)) then + do index = 1, size + values(index) = real(index, 8) + end do + end if + end subroutine make_values + end module adjustments ``` @@ -49,12 +64,12 @@ import sys import numpy as np sys.path.insert(0, "build/optional") -from optional.adjustments import adjust +from optional.adjustments import adjust, make_values -assert adjust(np.int32(5)) == np.int32(5) # omitted -assert adjust(np.int32(5), None) == np.int32(5) # explicit None -assert adjust(np.int32(5), np.int32(3)) == np.int32(8) # provided -assert adjust(np.int32(5), offset=np.int32(10)) == np.int32(15) # keyword +print(adjust(np.int32(5))) # 5 (omitted) +print(adjust(np.int32(5), None)) # 5 (explicit None) +print(adjust(np.int32(5), np.int32(3))) # 8 (provided) +print(adjust(np.int32(5), offset=np.int32(10))) # 15 (keyword) ``` --- @@ -66,7 +81,8 @@ assert adjust(np.int32(5), offset=np.int32(10)) == np.int32(15) # keyword - Providing a concrete value makes the argument **present**. - Use **keyword arguments** when skipping earlier optional parameters. - Optional arrays and derived types also accept `None` to indicate absence. -- Optional `intent(out)` / `intent(inout)` arguments remain visible in Python so you can control `present(...)`. +- Optional `intent(out)` / `intent(inout)` arguments remain visible in Python + so you can control `present(...)`. Scalar allocatable and pointer descriptors are the three-state exception: omission means absent, `None` means present but unallocated or unassociated, @@ -76,21 +92,51 @@ and a concrete value means present storage. ## Optional Outputs -When an output argument is optional: +An optional output remains visible in the Python call. This lets the caller +decide whether the native routine receives it. + +Pass writable storage to make `values` present: + +```python +values = np.empty(3, dtype=np.float64) +count = make_values(np.int32(3), values) + +print(count) # 3 +print(values) # [1. 2. 3.] +``` + +Omit the argument, or pass `None`, to make it absent: + +```python +omitted_count = make_values(np.int32(3)) +none_count = make_values(np.int32(3), None) + +print(omitted_count) # 3 +print(none_count) # 3 +``` + +`count` is a required scalar output, so it is always returned. `values` is +caller-owned mutable storage, so it is never added to the result. + +For optional ordinary array outputs: -- Supplying writable storage uses the normal mutation and return behavior. +- Supplying writable storage mutates that array in place. - Passing `None` or omitting it makes the native dummy absent. -- A result tuple uses `None` at that output position when other results are - still returned; a routine with only that absent output returns `None`. +- Presence does not add an array-or-`None` position to the result. +- A routine with only optional ordinary array outputs returns `None`, whether + those arrays are present or absent. -Always check the generated `.pyi` contract to see the exact return shape when mixing required and optional outputs. +Optional scalar derived-type outputs follow the same in-place rule as arrays. +Primitive scalar outputs and native descriptor outputs have different storage +rules. Check the generated `.pyi` contract when mixing output kinds. --- ## Limitations - Optional procedure pointers and passed procedures are not yet supported. -- x2py does not invent default values — the Fortran procedure is responsible for handling missing arguments. +- x2py does not invent default values. The Fortran procedure handles missing + arguments. --- diff --git a/docs/user/guide/pointers.md b/docs/user/guide/pointers.md index 2948dce21..a223de1fa 100644 --- a/docs/user/guide/pointers.md +++ b/docs/user/guide/pointers.md @@ -78,21 +78,20 @@ Use the generated module: ```python import sys -import numpy as np sys.path.insert(0, "build/pointers") import pointers.pointers_api as pointers_api handle = pointers_api.values -assert handle.associated is False -assert pointers_api.sum_pointer(handle) == np.float64(-1.0) +print(handle.associated) # False +print(pointers_api.sum_pointer(handle)) # -1.0 pointers_api.associate_values() -assert handle.associated is True -assert pointers_api.sum_pointer(handle) == np.float64(6.0) +print(handle.associated) # True +print(pointers_api.sum_pointer(handle)) # 6.0 handle.nullify() -assert handle.associated is False +print(handle.associated) # False ``` --- diff --git a/docs/user/guide/wrapping-derived-types.md b/docs/user/guide/wrapping-derived-types.md index c9bee77a5..59547aaf4 100644 --- a/docs/user/guide/wrapping-derived-types.md +++ b/docs/user/guide/wrapping-derived-types.md @@ -82,17 +82,16 @@ item = points.point(x=np.float64(1.0), y=np.float64(2.0)) # Call method (inout mutation) points.move(item, np.float64(3.0), np.float64(4.0)) -assert item.x == np.float64(4.0) -assert item.y == np.float64(6.0) +print(item.x, item.y) # 4.0 6.0 # Function returning derived type made = points.make_point(np.float64(8.0), np.float64(9.0)) # Nested component container = points.holder() -p.set_origin(container, made) +points.set_origin(container, made) container.origin.x = np.float64(12.0) -assert container.origin.x == np.float64(12.0) +print(container.origin.x) # 12.0 ``` --- @@ -100,9 +99,11 @@ assert container.origin.x == np.float64(12.0) ## Key Concepts - **Ownership**: Wrapper-owned objects are finalized when the Python object is garbage-collected. -- **Mutation**: `intent(inout)` modifies the same native instance. +- **Mutation**: `intent(out)` and `intent(inout)` modify a caller-provided + instance and do not return it again. - **Fields**: Public scalar numeric/logical/complex fields become Python attributes. - **Nested types**: Appear as borrowed child wrappers (they don’t own the memory). +- **Results**: Derived-type function results create new wrapper-owned objects. - **Constructors**: Generated for public scalar fields (keyword-only). --- diff --git a/docs/user/guide/wrapping-functions.md b/docs/user/guide/wrapping-functions.md index 6b80f4d19..194fb45aa 100644 --- a/docs/user/guide/wrapping-functions.md +++ b/docs/user/guide/wrapping-functions.md @@ -10,7 +10,9 @@ publication: reviewed # Wrapping Functions -A Fortran `function` becomes a Python callable. The function’s return value becomes the first item in Python, followed by any `intent(out)` or `intent(inout)` arguments (if present). +A Fortran `function` becomes a Python callable. Its direct result is the first +Python return value. Other outputs follow only when their contract projects +them as results. --- @@ -46,7 +48,7 @@ sys.path.insert(0, "build/scale") import scale result = scale.scale(np.float64(3.0), np.float64(2.5)) -assert result == 7.5 +print(result) # 7.5 ``` --- @@ -88,17 +90,21 @@ sys.path.insert(0, "build/function-results") from function_results.results import squares result = squares(np.int32(4)) -np.testing.assert_array_equal(result, np.array([1.0, 4.0, 9.0, 16.0], dtype=np.float64)) +print(result) # [ 1. 4. 9. 16.] ``` --- ## Functions with Output Arguments -When a function has `intent(out)` or `intent(inout)` arguments, Python returns a **tuple**: +When a function has projected scalar or native-created outputs, Python returns +a **tuple**: > `(function_result, out_arg1, out_arg2, ...)` +Caller-provided ordinary arrays are mutated in place and are not added to this +tuple. + **Example:** ```fortran @@ -114,7 +120,7 @@ end function **Python call:** ```python -total, count = api.sum_with_count(data_array) +total, count = sum_with_count(data_array) ``` --- diff --git a/docs/user/guide/wrapping-modules.md b/docs/user/guide/wrapping-modules.md index a091ec0fd..9958f6a09 100644 --- a/docs/user/guide/wrapping-modules.md +++ b/docs/user/guide/wrapping-modules.md @@ -10,7 +10,7 @@ publication: reviewed # Wrapping Modules -A Fortran `module` becomes a **child Python module** (namespace) inside the generated extension. This preserves the original structure instead of flattening everything to the extension root. +A Fortran `module` becomes a **child Python module** (namespace) inside the generated extension. --- @@ -20,64 +20,73 @@ After building `module_state.f90`: ```python import sys +import numpy as np sys.path.insert(0, "build/first-module") -import module_state.module_state as mod +import module_state + +mod = module_state.module_state # child namespace ``` -See [First Wrapped Module](../getting-started/first-wrapped-module.md) for the full example. +See [First Wrapped Module](../getting-started/first-wrapped-module.md) for the complete source, build command, and usage examples. --- ## Procedures -Module procedures become methods on the child module: +Module functions and subroutines become attributes of the child module: ```python -print(mod.summarize()) # 15 -print(mod.scaled_counter()) # 4.5 +print(mod.summarize()) # 15 +print(mod.scaled_counter()) # 4.5 ``` -Standalone procedures (not inside any module) remain at the extension root. +Standalone procedures (outside any module) remain at the extension root. + +When compiling multiple source files, each Fortran module becomes its own child namespace, while standalone procedures stay on the extension root. The first source file usually determines the extension name (you can override with `--out`). --- -## Public Variables & Constants +## Public Variables and Constants -Supported public scalar variables are exposed as direct attributes: +Supported public scalar variables are exposed as direct Python attributes: ```python mod.counter = np.int32(9) -assert mod.counter == np.int32(9) +print(mod.counter) # 9 +print(mod.summarize()) # 21 -assert mod.nmax == np.int32(12) # parameters are read-only +print(mod.nmax) # 12 (read-only parameter) ``` -- `parameter` declarations become `Final[...]` constants. -- Writing to a constant in Python only shadows the attribute locally — it does **not** change native storage. +- `parameter` declarations become `Final[...]` constants in the generated contract. +- Assigning to a constant in Python only creates a local shadow — it does **not** mutate the native value. --- -## Module Arrays & State +## Module Arrays & Saved State -- Allocatable module arrays appear as `Allocatable[T[...]]` handles. -- You can read the current state with `.to_numpy()` and mutate through the view. -- `save` variables (including procedure-local `save`) persist across calls. +- Allocatable module arrays use the `Allocatable[T[...]]` API. +- Allocation, lifetime, NumPy views, and mutation rules are covered in + the storage and objects section. +- `save` attributes (including procedure-local `save` variables) persist across calls. - Multiple Python imports of the same extension share the same native module state. --- -## Important Notes +## Important Rules -- Private declarations are hidden. -- Common blocks are **not** exposed as Python variables (only through procedures that access them). -- Module state is **shared** — changes are visible across all references to the same extension. +- Private declarations are hidden from the Python API. +- Common blocks are **not** exposed as Python variables (only indirectly through procedures that access them). +- Module state is **shared** native storage — changes made through one reference are visible to all others. +- The extension name is derived from the source filename unless overridden. --- ## Next -- Learn about [Memory Management](memory-management.md) +- Learn about [Memory Management](memory-management.md) — especially important when working with module state - See [Optional Arguments](optional-arguments.md) - Explore [Packaging](packaging.md) for distribution -- For supported module features, check the [Language Feature Matrix](../language-support/feature-matrix.md). +- Check the [Language Feature Matrix](../language-support/feature-matrix.md) + for supported module features and limitations diff --git a/docs/user/guide/wrapping-subroutines.md b/docs/user/guide/wrapping-subroutines.md index 6d7ac060c..742aae1c9 100644 --- a/docs/user/guide/wrapping-subroutines.md +++ b/docs/user/guide/wrapping-subroutines.md @@ -21,8 +21,9 @@ A Fortran `subroutine` has no direct return value. Instead, its `intent(out)` an | `intent(in)` scalar/array | Visible argument | Not returned | | `intent(out)` scalar | Hidden | Returned as value | | `intent(inout)` scalar | Visible argument | Returned as replacement value | -| `intent(out)` array | Visible writable NumPy array | Same array, filled and returned | -| `intent(inout)` array | Visible writable NumPy array | Mutated in place; normally no extra result | +| `intent(out)` array | Visible writable NumPy array | Filled in place; not returned | +| `intent(inout)` array | Visible writable NumPy array | Mutated in place; not returned | +| Derived `intent(out/inout)` | Visible generated object | Mutated in place; not returned | | `intent(out)` allocatable | Hidden (or optional) | `Allocatable[...]` handle | --- @@ -78,7 +79,7 @@ import sys import numpy as np sys.path.insert(0, "build/outputs") -from outputs.outputs import bounds, scale_in_place, scale_scalar +from outputs.outputs import bounds, fill, scale_in_place, scale_scalar # Hidden scalar outputs → returned as tuple data = np.array([4.0, -2.0, 7.0], dtype=np.float64) @@ -96,8 +97,7 @@ print(arr) # [3. 6. 9.] # Caller-provided output array target = np.empty(4, dtype=np.float64) -returned = api.fill(target) -assert returned is target +fill(target) print(target) # [1. 1. 1. 1.] ``` @@ -110,8 +110,10 @@ print(target) # [1. 1. 1. 1.] replacement values; the original Python scalar object is unchanged. - Array `intent(out/inout)` arguments must be pre-allocated by the caller and are mutated in place. -- Source-generated `intent(out)` arrays return the same supplied array; - `intent(inout)` arrays normally communicate through in-place mutation only. +- Ordinary `intent(out/inout)` arrays are not added to the Python result. +- Scalar derived-type objects follow the same in-place rule as arrays. +- Array function results and hidden allocatable outputs still return new + Python-visible objects because the caller did not supply their storage. - The generated `.pyi` contract is the source of truth for what is returned. - For functions with both a return value **and** outputs, the function result comes first in the tuple. diff --git a/docs/user/reference/generated-functions.md b/docs/user/reference/generated-functions.md index 319408076..75d62a8f5 100644 --- a/docs/user/reference/generated-functions.md +++ b/docs/user/reference/generated-functions.md @@ -37,36 +37,34 @@ callbacks keep their explicit semantic annotations. ## Return Projection -A Fortran function's direct result is the first Python return value. Native -output or replacement arguments follow in native argument order. A subroutine -with no visible outputs returns `None`. +A Fortran function's direct result is the first Python return value. Projected +scalar, replacement, or native-created outputs follow in native argument +order. Caller-provided ordinary arrays mutate in place and are not projected by +default. A subroutine with no projected outputs returns `None`. + +Scalar derived-type `intent(out)` and `intent(inout)` arguments follow the same +rule as arrays: the caller supplies a generated mutable object, native code +updates it, and the object is not repeated in the return value. When the Python-visible signature hides or reorders native arguments, the contract uses `@native_call(...)` and `Returns[...]` to preserve the native call shape: ```python -from x2py.contracts import Addr, Arg, Float64, Int32, Returns, native_call +from x2py.contracts import Addr, Arg, Float64, Int32, Return, native_call + +@native_call([Addr(Arg(0)), Return("status", 0)]) +def check_status(n: Int32) -> Int32: ... @native_call([Addr(Arg(0)), Arg(1)]) -def fill_vector( - n: Int32, - values: Float64[n] -) -> Returns["values", Float64[n]]: ... - -@native_call([Addr(Arg(0)), Addr(Arg(1)), Arg(2), Arg(3)]) -def shift_matrix( - n: Int32, - m: Int32, - values: Float64[n, m], - out: Float64[n, m] -) -> Returns["out", Float64[n, m]]: ... +def fill_vector(n: Int32, values: Float64[n]) -> None: ... ``` -`Returns["name", Type]` names a projected Python return. `tuple[...]` is used -when a callable has more than one Python return value. `@native_call` entries -such as `Arg(0)`, `Addr(Arg(0))`, `Return("status", 0)`, `Len(...)`, -`IsPresent(...)`, and `Work(...)` are described in +`Returns["name", Type]` names an explicit replacement return for a value that +also remains visible as an argument. `tuple[...]` is used when a callable has +more than one Python return value. `@native_call` entries such as `Arg(0)`, +`Addr(Arg(0))`, `Return("status", 0)`, `Len(...)`, `IsPresent(...)`, and +`Work(...)` are described in [Semantic `.pyi` Format](semantic-pyi-format.md#misuse-diagnostics-and-risk). Edited native-order contracts may omit `@native_call` only when every native diff --git a/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py b/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py index 7bc85d91c..0eb76841e 100644 --- a/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py +++ b/tests/semantics/conversion/fortran/test_fortran_conversion_procedures_and_interfaces.py @@ -414,6 +414,84 @@ def test_primitive_scalar_inout_stays_visible_and_projects_replacement_return(): ] +def test_ordinary_array_output_stays_visible_without_result_projection(): + source = """ +module outputs +contains +subroutine fill(values) + real(8), intent(out) :: values(:) +end subroutine fill +end module outputs +""" + + smod = fortran_module_to_semantic_module(parse_fortran_source(source)) + fill = get_function(smod, "fill") + + assert PROJECTED_OUTPUT_METADATA not in fill.arguments[0].metadata + assert fill.projection == [ + ProjectionMapping( + python_name="values", + native_name="values", + native_position=0, + python_position=0, + ) + ] + + +def test_scalar_derived_output_stays_visible_without_result_projection(): + source = """ +module outputs +type :: point + real(8) :: x +end type point +contains +subroutine fill(value) + type(point), intent(out) :: value +end subroutine fill +end module outputs +""" + + smod = fortran_module_to_semantic_module(parse_fortran_source(source)) + fill = get_function(smod, "fill") + + assert PROJECTED_OUTPUT_METADATA not in fill.arguments[0].metadata + assert fill.projection == [ + ProjectionMapping( + python_name="value", + native_name="value", + native_position=0, + python_position=0, + ) + ] + + +def test_optional_scalar_derived_output_stays_visible_without_result_projection(): + source = """ +module outputs +type :: point + real(8) :: x +end type point +contains +subroutine fill(value) + type(point), intent(out), optional :: value +end subroutine fill +end module outputs +""" + + smod = fortran_module_to_semantic_module(parse_fortran_source(source)) + fill = get_function(smod, "fill") + + assert PROJECTED_OUTPUT_METADATA not in fill.arguments[0].metadata + assert fill.projection == [ + ProjectionMapping( + python_name="value", + native_name="value", + native_position=0, + python_position=0, + ) + ] + + def test_function_result(): source = """ module func_mod diff --git a/tests/wrapper/fortran/arrays/contracts/farray_contracts_f90/farray_contracts_f90.pyi b/tests/wrapper/fortran/arrays/contracts/farray_contracts_f90/farray_contracts_f90.pyi index 3df3d2218..bd7739ef9 100644 --- a/tests/wrapper/fortran/arrays/contracts/farray_contracts_f90/farray_contracts_f90.pyi +++ b/tests/wrapper/fortran/arrays/contracts/farray_contracts_f90/farray_contracts_f90.pyi @@ -1,4 +1,4 @@ -from x2py.contracts import Addr, Arg, Flat, Float64, Int32, Returns, native_call +from x2py.contracts import Addr, Arg, Flat, Float64, Int32, native_call @native_call([Addr(Arg(0)), Arg(1)]) def sum_assumed_size( @@ -22,79 +22,79 @@ def bump_inout( def fill_out( values: Float64[::] -) -> Returns["values", Float64[::]]: ... +) -> None: ... def shift1( values: Float64[::], out: Float64[::] -) -> Returns["out", Float64[::]]: ... +) -> None: ... def shift2( values: Float64[::, ::], out: Float64[::, ::] -) -> Returns["out", Float64[::, ::]]: ... +) -> None: ... def shift3( values: Float64[::, ::, ::], out: Float64[::, ::, ::] -) -> Returns["out", Float64[::, ::, ::]]: ... +) -> None: ... def shift4( values: Float64[::, ::, ::, ::], out: Float64[::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::]]: ... +) -> None: ... def shift5( values: Float64[::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::, ::]]: ... +) -> None: ... def shift6( values: Float64[::, ::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::, ::, ::]]: ... +) -> None: ... def shift7( values: Float64[::, ::, ::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::, ::, ::, ::]]: ... +) -> None: ... def shift8( values: Float64[::, ::, ::, ::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::, ::, ::, ::, ::]]: ... +) -> None: ... def shift9( values: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::, ::, ::, ::, ::, ::]]: ... +) -> None: ... def shift10( values: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::]]: ... +) -> None: ... def shift11( values: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::]]: ... +) -> None: ... def shift12( values: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::]]: ... +) -> None: ... def shift13( values: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::]]: ... +) -> None: ... def shift14( values: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::]]: ... +) -> None: ... def shift15( values: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::], out: Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::] -) -> Returns["out", Float64[::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::, ::]]: ... +) -> None: ... diff --git a/tests/wrapper/fortran/arrays/contracts/multid_arrays/multid_arrays.pyi b/tests/wrapper/fortran/arrays/contracts/multid_arrays/multid_arrays.pyi index 5f84b5ba7..60ba854e3 100644 --- a/tests/wrapper/fortran/arrays/contracts/multid_arrays/multid_arrays.pyi +++ b/tests/wrapper/fortran/arrays/contracts/multid_arrays/multid_arrays.pyi @@ -1,19 +1,19 @@ -from x2py.contracts import Addr, Arg, Float64, Int32, Returns, native_call +from x2py.contracts import Addr, Arg, Float64, Int32, native_call def scale2_contiguous( a: Float64[:, :], out: Float64[:, :] -) -> Returns["out", Float64[:, :]]: ... +) -> None: ... def scale2_strided( a: Float64[::, ::], out: Float64[::, ::] -) -> Returns["out", Float64[::, ::]]: ... +) -> None: ... def checksum2_strided( a: Float64[::, ::], checksum: Float64[1] -) -> Returns["checksum", Float64[1]]: ... +) -> None: ... @native_call([Addr(Arg(0)), Addr(Arg(1)), Arg(2), Arg(3)]) def scale2_explicit( @@ -21,19 +21,19 @@ def scale2_explicit( cols: Int32, a: Float64[rows, cols], out: Float64[rows, cols] -) -> Returns["out", Float64[rows, cols]]: ... +) -> None: ... def shift3_contiguous( a: Float64[:, :, :], out: Float64[:, :, :] -) -> Returns["out", Float64[:, :, :]]: ... +) -> None: ... def shift3_strided( a: Float64[::, ::, ::], out: Float64[::, ::, ::] -) -> Returns["out", Float64[::, ::, ::]]: ... +) -> None: ... def checksum3_strided( a: Float64[::, ::, ::], checksum: Float64[1] -) -> Returns["checksum", Float64[1]]: ... +) -> None: ... diff --git a/tests/wrapper/fortran/arrays/test_array_contracts.py b/tests/wrapper/fortran/arrays/test_array_contracts.py index 741a71f33..6b2ef983a 100644 --- a/tests/wrapper/fortran/arrays/test_array_contracts.py +++ b/tests/wrapper/fortran/arrays/test_array_contracts.py @@ -191,7 +191,7 @@ def test_remaining_array_contracts_are_validated_before_fortran_calls( empty_rank4 = np.empty((0, 1, 1, 1), dtype=np.float64, order="F") empty_rank4_out = np.empty_like(empty_rank4, order="F") - assert module.shift4(empty_rank4, empty_rank4_out) is empty_rank4_out + assert module.shift4(empty_rank4, empty_rank4_out) is None assert empty_rank4_out.shape == empty_rank4.shape zero_stride_empty = as_strided( @@ -206,12 +206,12 @@ def test_remaining_array_contracts_are_validated_before_fortran_calls( ) assert zero_stride_empty.flags.f_contiguous assert zero_stride_empty_out.flags.f_contiguous - assert module.shift4(zero_stride_empty, zero_stride_empty_out) is zero_stride_empty_out + assert module.shift4(zero_stride_empty, zero_stride_empty_out) is None for rank in range(1, _MAX_WRAPPER_TEST_RANK + 1): shape = (2, *([1] * (rank - 1))) source = np.asfortranarray(np.arange(np.prod(shape), dtype=np.float64).reshape(shape, order="F")) out = np.empty(shape, dtype=np.float64, order="F") - assert getattr(module, f"shift{rank}")(source, out) is out + assert getattr(module, f"shift{rank}")(source, out) is None np.testing.assert_allclose(out, source + rank) diff --git a/tests/wrapper/fortran/arrays/test_multidimensional_arrays.py b/tests/wrapper/fortran/arrays/test_multidimensional_arrays.py index 602177d4a..051ecf48f 100644 --- a/tests/wrapper/fortran/arrays/test_multidimensional_arrays.py +++ b/tests/wrapper/fortran/arrays/test_multidimensional_arrays.py @@ -279,21 +279,21 @@ def test_dense_strided_and_projected_arrays_use_canonical_plan(tmp_path: Path): dense = _matrix() dense_out = np.zeros_like(dense, order="F") - assert module.scale2_contiguous(dense, dense_out) is dense_out + assert module.scale2_contiguous(dense, dense_out) is None np.testing.assert_allclose(dense_out, 2.0 * dense) explicit_out = np.zeros_like(dense, order="F") - assert module.scale2_explicit(np.int32(4), np.int32(3), dense, explicit_out) is explicit_out + assert module.scale2_explicit(np.int32(4), np.int32(3), dense, explicit_out) is None np.testing.assert_allclose(explicit_out, 4.0 * dense) strided = _strided_matrix() strided_out = _strided_matrix_output(strided.shape) - assert module.scale2_strided(strided, strided_out) is strided_out + assert module.scale2_strided(strided, strided_out) is None np.testing.assert_allclose(strided_out, 3.0 * strided) empty = _strided_matrix(0, 3) empty_out = _strided_matrix_output(empty.shape) - assert module.scale2_strided(empty, empty_out) is empty_out + assert module.scale2_strided(empty, empty_out) is None assert empty_out.shape == (0, 3) dense = _matrix() diff --git a/tests/wrapper/fortran/callbacks/contracts/fcallback_all_f90/fcallback_all_f90.pyi b/tests/wrapper/fortran/callbacks/contracts/fcallback_all_f90/fcallback_all_f90.pyi index 46ba1c6d8..ba374014d 100644 --- a/tests/wrapper/fortran/callbacks/contracts/fcallback_all_f90/fcallback_all_f90.pyi +++ b/tests/wrapper/fortran/callbacks/contracts/fcallback_all_f90/fcallback_all_f90.pyi @@ -48,12 +48,12 @@ def apply_value_callback( value: Int32 ) -> Int32: ... -@native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2)), Return('output', 0)]) +@native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2)), Return('output', 2)]) def apply_scalar_storage_callback( callback: scalar_storage_callback, value: Float64, missing: Float64 -) -> Float64: ... +) -> tuple[Returns["value", Float64], Returns["missing", Float64], Float64]: ... @native_call([Arg(0), Addr(Arg(1)), Arg(2), Arg(3)]) def apply_array_storage_callback( @@ -61,7 +61,7 @@ def apply_array_storage_callback( count: Int32, values: Float64[count], output: Float64[count] -) -> Returns["output", Float64[count]]: ... +) -> None: ... @native_call([Arg(0), Arg(1), Return('write_label', 1)]) def apply_string_storage_callback( @@ -69,8 +69,8 @@ def apply_string_storage_callback( update_label: String[8] ) -> tuple[Returns["update_label", String[8]], String[8]]: ... -@native_call([Arg(0), Arg(1), Return('output', 0)]) def apply_point_callback( callback: point_callback, - value: point_t -) -> point_t: ... + value: point_t, + output: point_t +) -> None: ... diff --git a/tests/wrapper/fortran/callbacks/contracts/fcallback_array_f90/fcallback_array_f90.pyi b/tests/wrapper/fortran/callbacks/contracts/fcallback_array_f90/fcallback_array_f90.pyi index a334e1218..e84c5ac80 100644 --- a/tests/wrapper/fortran/callbacks/contracts/fcallback_array_f90/fcallback_array_f90.pyi +++ b/tests/wrapper/fortran/callbacks/contracts/fcallback_array_f90/fcallback_array_f90.pyi @@ -1,4 +1,4 @@ -from x2py.contracts import Addr, Arg, Float64, Int32, Returns, native_call, prototype +from x2py.contracts import Addr, Arg, Float64, Int32, native_call, prototype @prototype def reduce_callback( @@ -25,4 +25,4 @@ def apply_transform( count: Int32, values: Float64[count], output: Float64[count] -) -> Returns["output", Float64[count]]: ... +) -> None: ... diff --git a/tests/wrapper/fortran/callbacks/contracts/fcallback_derived_f90/fcallback_derived_f90.pyi b/tests/wrapper/fortran/callbacks/contracts/fcallback_derived_f90/fcallback_derived_f90.pyi index 86124cf2a..566665cef 100644 --- a/tests/wrapper/fortran/callbacks/contracts/fcallback_derived_f90/fcallback_derived_f90.pyi +++ b/tests/wrapper/fortran/callbacks/contracts/fcallback_derived_f90/fcallback_derived_f90.pyi @@ -1,4 +1,4 @@ -from x2py.contracts import Arg, Float64, Return, native_call, prototype +from x2py.contracts import Float64, prototype class point_t: def __init__( @@ -16,8 +16,8 @@ def point_callback( value: point_t ) -> point_t: ... -@native_call([Arg(0), Arg(1), Return('output', 0)]) def apply_point( callback: point_callback, - value: point_t -) -> point_t: ... + value: point_t, + output: point_t +) -> None: ... diff --git a/tests/wrapper/fortran/callbacks/test_all_callback_shapes.py b/tests/wrapper/fortran/callbacks/test_all_callback_shapes.py index 61acfedf0..649f8931c 100644 --- a/tests/wrapper/fortran/callbacks/test_all_callback_shapes.py +++ b/tests/wrapper/fortran/callbacks/test_all_callback_shapes.py @@ -43,7 +43,7 @@ def array_callback(count, input_values, output_values): output_values[:count] = input_values[:count] + 1.5 result = module.apply_array_storage_callback(array_callback, np.int32(3), values, output) - assert result is output + assert result is None 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): @@ -61,10 +61,14 @@ def string_callback(read_label, write_label, update_label): assert module.apply_string_storage_callback(string_callback, "OLD ") == ("UPDATED!", "WRITTEN!") point = module.point_t(x=np.float64(2.0), y=np.float64(5.0)) - shifted = module.apply_point_callback( - lambda value: module.point_t(x=value.x + 1.0, y=value.y * 2.0), - point, + shifted = module.point_t() + assert ( + module.apply_point_callback( + lambda value: module.point_t(x=value.x + 1.0, y=value.y * 2.0), + point, + shifted, + ) + is None ) - assert isinstance(shifted, module.point_t) assert shifted.x == np.float64(3.0) assert shifted.y == np.float64(10.0) diff --git a/tests/wrapper/fortran/callbacks/test_array_callbacks.py b/tests/wrapper/fortran/callbacks/test_array_callbacks.py index 5b63b27c6..0f5ea2171 100644 --- a/tests/wrapper/fortran/callbacks/test_array_callbacks.py +++ b/tests/wrapper/fortran/callbacks/test_array_callbacks.py @@ -35,5 +35,5 @@ def test_immediate_dummy_procedure_converts_array_arguments_and_results( values, transformed, ) - assert result is transformed + assert result is None np.testing.assert_array_equal(transformed, np.array([2.0, 4.0, 6.0], dtype=np.float64)) diff --git a/tests/wrapper/fortran/derived_types/contracts/fderived_boundary_f90/fderived_boundary_f90.pyi b/tests/wrapper/fortran/derived_types/contracts/fderived_boundary_f90/fderived_boundary_f90.pyi index 55381ca3d..7c3bf696c 100644 --- a/tests/wrapper/fortran/derived_types/contracts/fderived_boundary_f90/fderived_boundary_f90.pyi +++ b/tests/wrapper/fortran/derived_types/contracts/fderived_boundary_f90/fderived_boundary_f90.pyi @@ -1,4 +1,4 @@ -from x2py.contracts import Addr, Arg, Float64, Return, native_call +from x2py.contracts import Addr, Arg, Float64, native_call class point: def __init__( @@ -32,11 +32,12 @@ def move_point( dy: Float64 ) -> None: ... -@native_call([Return('p', 0), Addr(Arg(0)), Addr(Arg(1))]) +@native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))]) def make_point_out( + p: point, x: Float64, y: Float64 -) -> point: ... +) -> None: ... @native_call([Addr(Arg(0)), Addr(Arg(1))]) def make_point( diff --git a/tests/wrapper/fortran/derived_types/contracts/fderived_boundary_phase8_opaque/fderived_boundary_f90.pyi b/tests/wrapper/fortran/derived_types/contracts/fderived_boundary_phase8_opaque/fderived_boundary_f90.pyi index 0307647d2..94b6b20fa 100644 --- a/tests/wrapper/fortran/derived_types/contracts/fderived_boundary_phase8_opaque/fderived_boundary_f90.pyi +++ b/tests/wrapper/fortran/derived_types/contracts/fderived_boundary_phase8_opaque/fderived_boundary_f90.pyi @@ -1,4 +1,4 @@ -from x2py.contracts import Addr, Arg, Float64, Return, native_call +from x2py.contracts import Addr, Arg, Float64, native_call class point: x: Float64 @@ -15,11 +15,12 @@ def move_point( dy: Float64 ) -> None: ... -@native_call([Return('p', 0), Addr(Arg(0)), Addr(Arg(1))]) +@native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))]) def make_point_out( + p: point, x: Float64, y: Float64 -) -> point: ... +) -> None: ... @native_call([Addr(Arg(0)), Addr(Arg(1))]) def make_point( diff --git a/tests/wrapper/fortran/derived_types/test_derived_type_boundaries.py b/tests/wrapper/fortran/derived_types/test_derived_type_boundaries.py index 5c7c08e7d..7e9277db0 100644 --- a/tests/wrapper/fortran/derived_types/test_derived_type_boundaries.py +++ b/tests/wrapper/fortran/derived_types/test_derived_type_boundaries.py @@ -42,8 +42,8 @@ def test_scalar_derived_types_cross_procedure_boundaries( assert point.x == np.float64(5.0) assert point.y == np.float64(7.0) - out_point = module.make_point_out(np.float64(8.0), np.float64(9.0)) - assert isinstance(out_point, module.point) + out_point = module.point() + assert module.make_point_out(out_point, np.float64(8.0), np.float64(9.0)) is None assert out_point.x == np.float64(8.0) assert out_point.y == np.float64(9.0) diff --git a/tests/wrapper/fortran/derived_types/test_phase8_derived_plan.py b/tests/wrapper/fortran/derived_types/test_phase8_derived_plan.py index 918a95946..d8d6fa6c5 100644 --- a/tests/wrapper/fortran/derived_types/test_phase8_derived_plan.py +++ b/tests/wrapper/fortran/derived_types/test_phase8_derived_plan.py @@ -266,10 +266,10 @@ def _exercise_point_boundary(module): assert point.x == np.float64(6.0) assert point.y == np.float64(8.0) - hidden = module.make_point_out(np.float64(10.0), np.float64(11.0)) - assert isinstance(hidden, module.point) - assert hidden.x == np.float64(10.0) - assert hidden.y == np.float64(11.0) + output = module.make_point(np.float64(0.0), np.float64(0.0)) + assert module.make_point_out(output, np.float64(10.0), np.float64(11.0)) is None + assert output.x == np.float64(10.0) + assert output.y == np.float64(11.0) with pytest.raises(TypeError, match="Expected"): point.x = 12.0 @@ -289,9 +289,8 @@ def test_scalar_derived_objects_use_canonical_plan(tmp_path: Path): assert "@x.setter\\n def x(self, value):" in generated_c assert "bind_c_x2py_field_point_x_get" in generated_fortran assert "bind_c_x2py_field_point_x_set" in generated_fortran - assert "p = c_null_ptr" in generated_fortran + assert "call native_make_point_out(p, x, y)" in generated_fortran assert "result = c_null_ptr" in generated_fortran - assert "allocate(p_value, stat=x2py_allocation_status)" in generated_fortran assert "allocate(result_value, stat=x2py_allocation_status)" in generated_fortran @@ -542,7 +541,8 @@ def test_value_copy_and_optional_derived_inputs_match_source_oracle(tmp_path: Pa assert source_module.update_point(source_point) is None assert source_point.x == np.float64(11.0) assert source_point.y == np.float64(22.0) - source_filled = source_module.fill_point() + source_filled = source_module.point() + assert source_module.fill_point(source_filled) is None assert source_filled.x == np.float64(31.0) assert source_filled.y == np.float64(32.0) diff --git a/tests/wrapper/fortran/function_calls/contracts/fnative_call_examples_f90/fnative_call_examples_f90.pyi b/tests/wrapper/fortran/function_calls/contracts/fnative_call_examples_f90/fnative_call_examples_f90.pyi index 43a16203d..1404d104c 100644 --- a/tests/wrapper/fortran/function_calls/contracts/fnative_call_examples_f90/fnative_call_examples_f90.pyi +++ b/tests/wrapper/fortran/function_calls/contracts/fnative_call_examples_f90/fnative_call_examples_f90.pyi @@ -20,7 +20,7 @@ def scalar_status( def fill_vector( n: Int32, values: Float64[n] -) -> Returns["values", Float64[n]]: ... +) -> None: ... @native_call([Addr(Arg(0)), Addr(Arg(1)), Arg(2), Arg(3)]) def shift_matrix( @@ -28,7 +28,7 @@ def shift_matrix( m: Int32, values: Float64[n, m], out: Float64[n, m] -) -> Returns["out", Float64[n, m]]: ... +) -> None: ... @native_call([Arg(0), Return('status', 0)]) def scale_with_status( @@ -42,13 +42,14 @@ def fixed_inout( @native_call([Return('label', 0)]) def make_label() -> String[6]: ... -@native_call([Addr(Arg(0)), Arg(1), Return('status', 2), Return('label', 3)]) +@native_call([Addr(Arg(0)), Arg(1), Return('status', 1), Return('label', 2)]) def summarize_mixed( n: Int32, values: Float64[n] -) -> tuple[Float64, Returns["values", Float64[n]], Int32, String[6]]: ... +) -> tuple[Float64, Int32, String[6]]: ... -@native_call([Addr(Arg(0)), Return('point', 0)]) +@native_call([Addr(Arg(0)), Arg(1)]) def make_point( - scale: Int32 -) -> summary_point: ... + scale: Int32, + point: summary_point +) -> None: ... diff --git a/tests/wrapper/fortran/function_calls/contracts/foptional_f90/foptional_f90.pyi b/tests/wrapper/fortran/function_calls/contracts/foptional_f90/foptional_f90.pyi index 8478f4a22..d4da9f74f 100644 --- a/tests/wrapper/fortran/function_calls/contracts/foptional_f90/foptional_f90.pyi +++ b/tests/wrapper/fortran/function_calls/contracts/foptional_f90/foptional_f90.pyi @@ -28,7 +28,7 @@ def mutate_optional( def fill_optional( n: Int32, values: Float64[::] = ... -) -> Returns["values", Float64[::]] | None: ... +) -> None: ... @native_call([Addr(Arg(0)), Arg(1)]) def optional_status( diff --git a/tests/wrapper/fortran/function_calls/contracts/foutputs_f90/foutputs_f90.pyi b/tests/wrapper/fortran/function_calls/contracts/foutputs_f90/foutputs_f90.pyi index 3422a4b1c..2238b82b3 100644 --- a/tests/wrapper/fortran/function_calls/contracts/foutputs_f90/foutputs_f90.pyi +++ b/tests/wrapper/fortran/function_calls/contracts/foutputs_f90/foutputs_f90.pyi @@ -1,4 +1,4 @@ -from x2py.contracts import Addr, Allocatable, Arg, Float64, Int32, Return, Returns, String, native_call +from x2py.contracts import Addr, Allocatable, Arg, Float64, Int32, Return, String, native_call class output_point: def __init__( @@ -20,14 +20,14 @@ def scalar_status( def fill_vector( n: Int32, values: Float64[n] -) -> Returns["values", Float64[n]]: ... +) -> None: ... @native_call([Addr(Arg(0)), Addr(Arg(1)), Arg(2)]) def fill_matrix( n: Int32, m: Int32, values: Float64[n, m] -) -> Returns["values", Float64[n, m]]: ... +) -> None: ... @native_call([Addr(Arg(0)), Return('values', 0)]) def build_alloc( @@ -39,11 +39,11 @@ def with_scalar( n: Int32 ) -> tuple[Int32, Int32]: ... -@native_call([Addr(Arg(0)), Arg(1), Return('status', 2), Return('built', 3)]) +@native_call([Addr(Arg(0)), Arg(1), Return('status', 1), Return('built', 2)]) def mixed_outputs( n: Int32, values: Float64[n] -) -> tuple[Float64, Returns["values", Float64[n]], Int32, Allocatable[Float64[:]]]: ... +) -> tuple[Float64, Int32, Allocatable[Float64[:]]]: ... def increment( values: Float64[::] @@ -57,7 +57,8 @@ def increment_with_status( @native_call([Return('label', 0)]) def make_label() -> String[8]: ... -@native_call([Addr(Arg(0)), Return('point', 0)]) +@native_call([Addr(Arg(0)), Arg(1)]) def make_point( - scale: Int32 -) -> output_point: ... + scale: Int32, + point: output_point +) -> None: ... diff --git a/tests/wrapper/fortran/function_calls/test_native_call_examples.py b/tests/wrapper/fortran/function_calls/test_native_call_examples.py index c5efaa65e..c203330f8 100644 --- a/tests/wrapper/fortran/function_calls/test_native_call_examples.py +++ b/tests/wrapper/fortran/function_calls/test_native_call_examples.py @@ -29,14 +29,12 @@ def _assert_native_call_examples(module) -> None: assert module.scalar_status(np.int32(4)) == np.int32(15) vector = np.empty(4, dtype=np.float64) - returned_vector = module.fill_vector(np.int32(4), vector) - assert returned_vector is vector + assert module.fill_vector(np.int32(4), vector) is None np.testing.assert_allclose(vector, np.array([1.5, 3.0, 4.5, 6.0], dtype=np.float64)) matrix = np.array([[1.0, 3.0, 5.0], [2.0, 4.0, 6.0]], dtype=np.float64, order="F") shifted = np.empty((2, 3), dtype=np.float64, order="F") - returned_matrix = module.shift_matrix(np.int32(2), np.int32(3), matrix, shifted) - assert returned_matrix is shifted + assert module.shift_matrix(np.int32(2), np.int32(3), matrix, shifted) is None np.testing.assert_allclose(shifted, matrix + 10.0) inout = np.array([2.0, 5.0, 7.0], dtype=np.float64) @@ -49,15 +47,14 @@ def _assert_native_call_examples(module) -> None: assert module.make_label() == "done " mixed_values = np.empty(3, dtype=np.float64) - total, returned_values, status, label = module.summarize_mixed(np.int32(3), mixed_values) + total, status, label = module.summarize_mixed(np.int32(3), mixed_values) assert total == np.float64(3.75) - assert returned_values is mixed_values assert status == np.int32(23) assert label == "mix " np.testing.assert_allclose(mixed_values, np.array([11.0, 12.0, 13.0], dtype=np.float64)) - point = module.make_point(np.int32(7)) - assert isinstance(point, module.summary_point) + point = module.summary_point() + assert module.make_point(np.int32(7), point) is None assert point.total == np.float64(7.5) assert point.code == np.int32(107) diff --git a/tests/wrapper/fortran/function_calls/test_optional_arguments.py b/tests/wrapper/fortran/function_calls/test_optional_arguments.py index 730358c79..e0d076367 100644 --- a/tests/wrapper/fortran/function_calls/test_optional_arguments.py +++ b/tests/wrapper/fortran/function_calls/test_optional_arguments.py @@ -247,8 +247,7 @@ def test_optional_arguments_drive_fortran_present_behavior( np.testing.assert_allclose(mutable, np.array([5.5, 6.5], dtype=np.float64)) output = np.empty(3, dtype=np.float64) - returned_output = module.fill_optional(np.int32(3), output) - assert returned_output is output + assert module.fill_optional(np.int32(3), output) is None np.testing.assert_allclose(output, np.array([11.0, 12.0, 13.0], dtype=np.float64)) assert module.fill_optional(np.int32(3)) is None assert module.fill_optional(np.int32(3), None) is None diff --git a/tests/wrapper/fortran/function_calls/test_output_arguments.py b/tests/wrapper/fortran/function_calls/test_output_arguments.py index f4504222e..eddb57964 100644 --- a/tests/wrapper/fortran/function_calls/test_output_arguments.py +++ b/tests/wrapper/fortran/function_calls/test_output_arguments.py @@ -38,11 +38,11 @@ def test_output_arguments_and_multiple_results_follow_python_projection_rules( assert "scalar_status(n) -> int32" in module.scalar_status.__doc__ assert "status : int32" in module.scalar_status.__doc__ - assert "fill_vector(n, values) -> ndarray[float64]" in module.fill_vector.__doc__ + assert "fill_vector(n, values) -> None" in module.fill_vector.__doc__ assert "Parameters\n----------" in module.fill_vector.__doc__ - assert "Returns\n-------" in module.fill_vector.__doc__ + assert "Returns\n-------\nNone" in module.fill_vector.__doc__ assert "Raises\n------" in module.fill_vector.__doc__ - assert "Native code may update this value; the updated value is returned." in module.fill_vector.__doc__ + assert "Native code may update the supplied storage in place." in module.fill_vector.__doc__ assert "Direction:" not in module.fill_vector.__doc__ assert "Initial contents are ignored." not in module.fill_vector.__doc__ assert "Ownership: Caller-owned" in module.fill_vector.__doc__ @@ -50,18 +50,17 @@ def test_output_arguments_and_multiple_results_follow_python_projection_rules( assert "Descriptor ownership: owned" in module.build_alloc.__doc__ assert "Unallocated state remains inside the returned handle." in module.build_alloc.__doc__ assert "make_label() -> str" in module.make_label.__doc__ - assert "make_point(scale) -> output_point" in module.make_point.__doc__ + assert "make_point(scale, point) -> None" in module.make_point.__doc__ + assert "point : output_point" in module.make_point.__doc__ assert module.scalar_status(np.int32(5)) == np.int32(15) vector = np.empty(4, dtype=np.float64) - returned_vector = module.fill_vector(np.int32(4), vector) - assert returned_vector is vector + assert module.fill_vector(np.int32(4), vector) is None np.testing.assert_allclose(vector, np.array([2.0, 4.0, 6.0, 8.0], dtype=np.float64)) matrix = np.empty((2, 3), dtype=np.float64, order="F") - returned_matrix = module.fill_matrix(np.int32(2), np.int32(3), matrix) - assert returned_matrix is matrix + assert module.fill_matrix(np.int32(2), np.int32(3), matrix) is None np.testing.assert_allclose( matrix, np.array([[11.0, 21.0, 31.0], [12.0, 22.0, 32.0]], dtype=np.float64), @@ -81,12 +80,11 @@ def test_output_arguments_and_multiple_results_follow_python_projection_rules( mixed_vector = np.empty(3, dtype=np.float64) mixed_result = module.mixed_outputs(np.int32(3), mixed_vector) assert mixed_result[0] == np.float64(3.5) - assert mixed_result[1] is mixed_vector - assert mixed_result[2] == np.int32(23) - np.testing.assert_allclose(mixed_result[1], np.array([101.0, 102.0, 103.0], dtype=np.float64)) - assert isinstance(mixed_result[3], AllocatableArray) + assert mixed_result[1] == np.int32(23) + np.testing.assert_allclose(mixed_vector, np.array([101.0, 102.0, 103.0], dtype=np.float64)) + assert isinstance(mixed_result[2], AllocatableArray) np.testing.assert_allclose( - mixed_result[3].to_numpy(), + mixed_result[2].to_numpy(), np.array([201.0, 202.0, 203.0], dtype=np.float64), ) @@ -98,8 +96,8 @@ def test_output_arguments_and_multiple_results_follow_python_projection_rules( assert module.make_label() == "RESULT!!" - point = module.make_point(np.int32(6)) - assert isinstance(point, module.output_point) + point = module.output_point() + assert module.make_point(np.int32(6), point) is None assert point.x == np.float64(6.25) assert point.tag == np.int32(46) diff --git a/tests/wrapper/fortran/real_libraries/contracts/lapack/__init__.pyi b/tests/wrapper/fortran/real_libraries/contracts/lapack/__init__.pyi index 415845176..9d6bbeabc 100644 --- a/tests/wrapper/fortran/real_libraries/contracts/lapack/__init__.pyi +++ b/tests/wrapper/fortran/real_libraries/contracts/lapack/__init__.pyi @@ -401,7 +401,7 @@ def cgecon( @bind("CGEDMD") @external -@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Addr(Arg(4)), Addr(Arg(5)), Addr(Arg(6)), Arg(7), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Addr(Arg(11)), Addr(Arg(12)), Return('K', 0), Arg(13), Arg(14), Addr(Arg(15)), Arg(16), Arg(17), Addr(Arg(18)), Arg(19), Addr(Arg(20)), Arg(21), Addr(Arg(22)), Arg(23), Addr(Arg(24)), Arg(25), Addr(Arg(26)), Arg(27), Addr(Arg(28)), Return('INFO', 10)]) +@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Addr(Arg(4)), Addr(Arg(5)), Addr(Arg(6)), Arg(7), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Addr(Arg(11)), Addr(Arg(12)), Return('K', 0), Arg(13), Arg(14), Addr(Arg(15)), Arg(16), Arg(17), Addr(Arg(18)), Arg(19), Addr(Arg(20)), Arg(21), Addr(Arg(22)), Arg(23), Addr(Arg(24)), Arg(25), Addr(Arg(26)), Arg(27), Addr(Arg(28)), Return('INFO', 1)]) def cgedmd( JOBS: String[1], JOBZ: String[1], @@ -432,11 +432,11 @@ def cgedmd( LRWORK: Int32, IWORK: Int32[Flat], LIWORK: Int32 -) -> tuple[Int32, Returns["EIGS", Complex64[Flat]], Returns["Z", Complex64[LDZ, Flat]], Returns["RES", Float32[Flat]], Returns["B", Complex64[LDB, Flat]], Returns["W", Complex64[LDW, Flat]], Returns["S", Complex64[LDS, Flat]], Returns["ZWORK", Complex64[Flat]], Returns["RWORK", Float32[Flat]], Returns["IWORK", Int32[Flat]], Int32]: ... +) -> tuple[Int32, Int32]: ... @bind("CGEDMDQ") @external -@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Addr(Arg(6)), Addr(Arg(7)), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Arg(11), Addr(Arg(12)), Arg(13), Addr(Arg(14)), Addr(Arg(15)), Addr(Arg(16)), Return('K', 2), Arg(17), Arg(18), Addr(Arg(19)), Arg(20), Arg(21), Addr(Arg(22)), Arg(23), Addr(Arg(24)), Arg(25), Addr(Arg(26)), Arg(27), Addr(Arg(28)), Arg(29), Addr(Arg(30)), Arg(31), Addr(Arg(32)), Return('INFO', 12)]) +@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Addr(Arg(6)), Addr(Arg(7)), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Arg(11), Addr(Arg(12)), Arg(13), Addr(Arg(14)), Addr(Arg(15)), Addr(Arg(16)), Return('K', 0), Arg(17), Arg(18), Addr(Arg(19)), Arg(20), Arg(21), Addr(Arg(22)), Arg(23), Addr(Arg(24)), Arg(25), Addr(Arg(26)), Arg(27), Addr(Arg(28)), Arg(29), Addr(Arg(30)), Arg(31), Addr(Arg(32)), Return('INFO', 1)]) def cgedmdq( JOBS: String[1], JOBZ: String[1], @@ -471,7 +471,7 @@ def cgedmdq( LWORK: Int32, IWORK: Int32[Flat], LIWORK: Int32 -) -> tuple[Returns["X", Complex64[LDX, Flat]], Returns["Y", Complex64[LDY, Flat]], Int32, Returns["EIGS", Complex64[Flat]], Returns["Z", Complex64[LDZ, Flat]], Returns["RES", Float32[Flat]], Returns["B", Complex64[LDB, Flat]], Returns["V", Complex64[LDV, Flat]], Returns["S", Complex64[LDS, Flat]], Returns["ZWORK", Complex64[Flat]], Returns["WORK", Float32[Flat]], Returns["IWORK", Int32[Flat]], Int32]: ... +) -> tuple[Int32, Int32]: ... @bind("CGEEQU") @external @@ -5227,7 +5227,7 @@ def claqsy( @bind("CLAQZ0") @external -@native_call([Arg(0), Arg(1), Arg(2), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5)), Arg(6), Addr(Arg(7)), Arg(8), Addr(Arg(9)), Arg(10), Arg(11), Arg(12), Addr(Arg(13)), Arg(14), Addr(Arg(15)), Arg(16), Addr(Arg(17)), Arg(18), Addr(Arg(19)), Return('INFO', 1)]) +@native_call([Arg(0), Arg(1), Arg(2), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5)), Arg(6), Addr(Arg(7)), Arg(8), Addr(Arg(9)), Arg(10), Arg(11), Arg(12), Addr(Arg(13)), Arg(14), Addr(Arg(15)), Arg(16), Addr(Arg(17)), Arg(18), Addr(Arg(19)), Return('INFO', 0)]) def claqz0( WANTS: String[1], WANTQ: String[1], @@ -5249,7 +5249,7 @@ def claqz0( LWORK: Int32, RWORK: Float32[Flat], REC: Int32 -) -> tuple[Returns["RWORK", Float32[Flat]], Int32]: ... +) -> Int32: ... @bind("CLAQZ1") @external @@ -9584,7 +9584,7 @@ def dgecon( @bind("DGEDMD") @external -@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Addr(Arg(4)), Addr(Arg(5)), Addr(Arg(6)), Arg(7), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Addr(Arg(11)), Addr(Arg(12)), Return('K', 0), Arg(13), Arg(14), Arg(15), Addr(Arg(16)), Arg(17), Arg(18), Addr(Arg(19)), Arg(20), Addr(Arg(21)), Arg(22), Addr(Arg(23)), Arg(24), Addr(Arg(25)), Arg(26), Addr(Arg(27)), Return('INFO', 10)]) +@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Addr(Arg(4)), Addr(Arg(5)), Addr(Arg(6)), Arg(7), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Addr(Arg(11)), Addr(Arg(12)), Return('K', 0), Arg(13), Arg(14), Arg(15), Addr(Arg(16)), Arg(17), Arg(18), Addr(Arg(19)), Arg(20), Addr(Arg(21)), Arg(22), Addr(Arg(23)), Arg(24), Addr(Arg(25)), Arg(26), Addr(Arg(27)), Return('INFO', 1)]) def dgedmd( JOBS: String[1], JOBZ: String[1], @@ -9614,11 +9614,11 @@ def dgedmd( LWORK: Int32, IWORK: Int32[Flat], LIWORK: Int32 -) -> tuple[Int32, Returns["REIG", Float64[Flat]], Returns["IMEIG", Float64[Flat]], Returns["Z", Float64[LDZ, Flat]], Returns["RES", Float64[Flat]], Returns["B", Float64[LDB, Flat]], Returns["W", Float64[LDW, Flat]], Returns["S", Float64[LDS, Flat]], Returns["WORK", Float64[Flat]], Returns["IWORK", Int32[Flat]], Int32]: ... +) -> tuple[Int32, Int32]: ... @bind("DGEDMDQ") @external -@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Addr(Arg(6)), Addr(Arg(7)), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Arg(11), Addr(Arg(12)), Arg(13), Addr(Arg(14)), Addr(Arg(15)), Addr(Arg(16)), Return('K', 2), Arg(17), Arg(18), Arg(19), Addr(Arg(20)), Arg(21), Arg(22), Addr(Arg(23)), Arg(24), Addr(Arg(25)), Arg(26), Addr(Arg(27)), Arg(28), Addr(Arg(29)), Arg(30), Addr(Arg(31)), Return('INFO', 12)]) +@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Addr(Arg(6)), Addr(Arg(7)), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Arg(11), Addr(Arg(12)), Arg(13), Addr(Arg(14)), Addr(Arg(15)), Addr(Arg(16)), Return('K', 0), Arg(17), Arg(18), Arg(19), Addr(Arg(20)), Arg(21), Arg(22), Addr(Arg(23)), Arg(24), Addr(Arg(25)), Arg(26), Addr(Arg(27)), Arg(28), Addr(Arg(29)), Arg(30), Addr(Arg(31)), Return('INFO', 1)]) def dgedmdq( JOBS: String[1], JOBZ: String[1], @@ -9652,7 +9652,7 @@ def dgedmdq( LWORK: Int32, IWORK: Int32[Flat], LIWORK: Int32 -) -> tuple[Returns["X", Float64[LDX, Flat]], Returns["Y", Float64[LDY, Flat]], Int32, Returns["REIG", Float64[Flat]], Returns["IMEIG", Float64[Flat]], Returns["Z", Float64[LDZ, Flat]], Returns["RES", Float64[Flat]], Returns["B", Float64[LDB, Flat]], Returns["V", Float64[LDV, Flat]], Returns["S", Float64[LDS, Flat]], Returns["WORK", Float64[Flat]], Returns["IWORK", Int32[Flat]], Int32]: ... +) -> tuple[Int32, Int32]: ... @bind("DGEEQU") @external @@ -12990,7 +12990,7 @@ def dlaqz1( BETA1: Float64, BETA2: Float64, V: Float64[Flat] -) -> Returns["V", Float64[Flat]]: ... +) -> None: ... @bind("DLAQZ2") @external @@ -19039,7 +19039,7 @@ def sgecon( @bind("SGEDMD") @external -@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Addr(Arg(4)), Addr(Arg(5)), Addr(Arg(6)), Arg(7), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Addr(Arg(11)), Addr(Arg(12)), Return('K', 0), Arg(13), Arg(14), Arg(15), Addr(Arg(16)), Arg(17), Arg(18), Addr(Arg(19)), Arg(20), Addr(Arg(21)), Arg(22), Addr(Arg(23)), Arg(24), Addr(Arg(25)), Arg(26), Addr(Arg(27)), Return('INFO', 10)]) +@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Addr(Arg(4)), Addr(Arg(5)), Addr(Arg(6)), Arg(7), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Addr(Arg(11)), Addr(Arg(12)), Return('K', 0), Arg(13), Arg(14), Arg(15), Addr(Arg(16)), Arg(17), Arg(18), Addr(Arg(19)), Arg(20), Addr(Arg(21)), Arg(22), Addr(Arg(23)), Arg(24), Addr(Arg(25)), Arg(26), Addr(Arg(27)), Return('INFO', 1)]) def sgedmd( JOBS: String[1], JOBZ: String[1], @@ -19069,11 +19069,11 @@ def sgedmd( LWORK: Int32, IWORK: Int32[Flat], LIWORK: Int32 -) -> tuple[Int32, Returns["REIG", Float32[Flat]], Returns["IMEIG", Float32[Flat]], Returns["Z", Float32[LDZ, Flat]], Returns["RES", Float32[Flat]], Returns["B", Float32[LDB, Flat]], Returns["W", Float32[LDW, Flat]], Returns["S", Float32[LDS, Flat]], Returns["WORK", Float32[Flat]], Returns["IWORK", Int32[Flat]], Int32]: ... +) -> tuple[Int32, Int32]: ... @bind("SGEDMDQ") @external -@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Addr(Arg(6)), Addr(Arg(7)), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Arg(11), Addr(Arg(12)), Arg(13), Addr(Arg(14)), Addr(Arg(15)), Addr(Arg(16)), Return('K', 2), Arg(17), Arg(18), Arg(19), Addr(Arg(20)), Arg(21), Arg(22), Addr(Arg(23)), Arg(24), Addr(Arg(25)), Arg(26), Addr(Arg(27)), Arg(28), Addr(Arg(29)), Arg(30), Addr(Arg(31)), Return('INFO', 12)]) +@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Addr(Arg(6)), Addr(Arg(7)), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Arg(11), Addr(Arg(12)), Arg(13), Addr(Arg(14)), Addr(Arg(15)), Addr(Arg(16)), Return('K', 0), Arg(17), Arg(18), Arg(19), Addr(Arg(20)), Arg(21), Arg(22), Addr(Arg(23)), Arg(24), Addr(Arg(25)), Arg(26), Addr(Arg(27)), Arg(28), Addr(Arg(29)), Arg(30), Addr(Arg(31)), Return('INFO', 1)]) def sgedmdq( JOBS: String[1], JOBZ: String[1], @@ -19107,7 +19107,7 @@ def sgedmdq( LWORK: Int32, IWORK: Int32[Flat], LIWORK: Int32 -) -> tuple[Returns["X", Float32[LDX, Flat]], Returns["Y", Float32[LDY, Flat]], Int32, Returns["REIG", Float32[Flat]], Returns["IMEIG", Float32[Flat]], Returns["Z", Float32[LDZ, Flat]], Returns["RES", Float32[Flat]], Returns["B", Float32[LDB, Flat]], Returns["V", Float32[LDV, Flat]], Returns["S", Float32[LDS, Flat]], Returns["WORK", Float32[Flat]], Returns["IWORK", Int32[Flat]], Int32]: ... +) -> tuple[Int32, Int32]: ... @bind("SGEEQU") @external @@ -22445,7 +22445,7 @@ def slaqz1( BETA1: Float32, BETA2: Float32, V: Float32[Flat] -) -> Returns["V", Float32[Flat]]: ... +) -> None: ... @bind("SLAQZ2") @external @@ -28246,7 +28246,7 @@ def zgecon( @bind("ZGEDMD") @external -@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Addr(Arg(4)), Addr(Arg(5)), Addr(Arg(6)), Arg(7), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Addr(Arg(11)), Addr(Arg(12)), Return('K', 0), Arg(13), Arg(14), Addr(Arg(15)), Arg(16), Arg(17), Addr(Arg(18)), Arg(19), Addr(Arg(20)), Arg(21), Addr(Arg(22)), Arg(23), Addr(Arg(24)), Arg(25), Addr(Arg(26)), Arg(27), Addr(Arg(28)), Return('INFO', 10)]) +@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Addr(Arg(4)), Addr(Arg(5)), Addr(Arg(6)), Arg(7), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Addr(Arg(11)), Addr(Arg(12)), Return('K', 0), Arg(13), Arg(14), Addr(Arg(15)), Arg(16), Arg(17), Addr(Arg(18)), Arg(19), Addr(Arg(20)), Arg(21), Addr(Arg(22)), Arg(23), Addr(Arg(24)), Arg(25), Addr(Arg(26)), Arg(27), Addr(Arg(28)), Return('INFO', 1)]) def zgedmd( JOBS: String[1], JOBZ: String[1], @@ -28277,11 +28277,11 @@ def zgedmd( LRWORK: Int32, IWORK: Int32[Flat], LIWORK: Int32 -) -> tuple[Int32, Returns["EIGS", Complex128[Flat]], Returns["Z", Complex128[LDZ, Flat]], Returns["RES", Float64[Flat]], Returns["B", Complex128[LDB, Flat]], Returns["W", Complex128[LDW, Flat]], Returns["S", Complex128[LDS, Flat]], Returns["ZWORK", Complex128[Flat]], Returns["RWORK", Float64[Flat]], Returns["IWORK", Int32[Flat]], Int32]: ... +) -> tuple[Int32, Int32]: ... @bind("ZGEDMDQ") @external -@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Addr(Arg(6)), Addr(Arg(7)), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Arg(11), Addr(Arg(12)), Arg(13), Addr(Arg(14)), Addr(Arg(15)), Addr(Arg(16)), Return('K', 2), Arg(17), Arg(18), Addr(Arg(19)), Arg(20), Arg(21), Addr(Arg(22)), Arg(23), Addr(Arg(24)), Arg(25), Addr(Arg(26)), Arg(27), Addr(Arg(28)), Arg(29), Addr(Arg(30)), Arg(31), Addr(Arg(32)), Return('INFO', 12)]) +@native_call([Arg(0), Arg(1), Arg(2), Arg(3), Arg(4), Arg(5), Addr(Arg(6)), Addr(Arg(7)), Addr(Arg(8)), Arg(9), Addr(Arg(10)), Arg(11), Addr(Arg(12)), Arg(13), Addr(Arg(14)), Addr(Arg(15)), Addr(Arg(16)), Return('K', 0), Arg(17), Arg(18), Addr(Arg(19)), Arg(20), Arg(21), Addr(Arg(22)), Arg(23), Addr(Arg(24)), Arg(25), Addr(Arg(26)), Arg(27), Addr(Arg(28)), Arg(29), Addr(Arg(30)), Arg(31), Addr(Arg(32)), Return('INFO', 1)]) def zgedmdq( JOBS: String[1], JOBZ: String[1], @@ -28316,7 +28316,7 @@ def zgedmdq( LWORK: Int32, IWORK: Int32[Flat], LIWORK: Int32 -) -> tuple[Returns["X", Complex128[LDX, Flat]], Returns["Y", Complex128[LDY, Flat]], Int32, Returns["EIGS", Complex128[Flat]], Returns["Z", Complex128[LDZ, Flat]], Returns["RES", Float64[Flat]], Returns["B", Complex128[LDB, Flat]], Returns["V", Complex128[LDV, Flat]], Returns["S", Complex128[LDS, Flat]], Returns["ZWORK", Complex128[Flat]], Returns["WORK", Float64[Flat]], Returns["IWORK", Int32[Flat]], Int32]: ... +) -> tuple[Int32, Int32]: ... @bind("ZGEEQU") @external @@ -33065,7 +33065,7 @@ def zlaqsy( @bind("ZLAQZ0") @external -@native_call([Arg(0), Arg(1), Arg(2), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5)), Arg(6), Addr(Arg(7)), Arg(8), Addr(Arg(9)), Arg(10), Arg(11), Arg(12), Addr(Arg(13)), Arg(14), Addr(Arg(15)), Arg(16), Addr(Arg(17)), Arg(18), Addr(Arg(19)), Return('INFO', 1)]) +@native_call([Arg(0), Arg(1), Arg(2), Addr(Arg(3)), Addr(Arg(4)), Addr(Arg(5)), Arg(6), Addr(Arg(7)), Arg(8), Addr(Arg(9)), Arg(10), Arg(11), Arg(12), Addr(Arg(13)), Arg(14), Addr(Arg(15)), Arg(16), Addr(Arg(17)), Arg(18), Addr(Arg(19)), Return('INFO', 0)]) def zlaqz0( WANTS: String[1], WANTQ: String[1], @@ -33087,7 +33087,7 @@ def zlaqz0( LWORK: Int32, RWORK: Float64[Flat], REC: Int32 -) -> tuple[Returns["RWORK", Float64[Flat]], Int32]: ... +) -> Int32: ... @bind("ZLAQZ1") @external diff --git a/tests/wrapper/fortran/scalars/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi b/tests/wrapper/fortran/scalars/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi index f545dfbf6..429849c25 100644 --- a/tests/wrapper/fortran/scalars/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi +++ b/tests/wrapper/fortran/scalars/contracts/fscalar_kinds_f90/fscalar_kinds_f90.pyi @@ -1,4 +1,4 @@ -from x2py.contracts import Addr, Arg, Bool, Complex128, Complex64, Float32, Float64, Int16, Int32, Int64, Int8, Returns, native_call +from x2py.contracts import Addr, Arg, Bool, Complex128, Complex64, Float32, Float64, Int16, Int32, Int64, Int8, native_call @native_call([Addr(Arg(0))]) def id_i8( @@ -25,7 +25,7 @@ def copy_i16( n: Int32, values: Int16[n], out: Int16[n] -) -> Returns["out", Int16[n]]: ... +) -> None: ... @native_call([Addr(Arg(0))]) def not_flag( @@ -37,7 +37,7 @@ def invert_flags( n: Int32, values: Bool[n], out: Bool[n] -) -> Returns["out", Bool[n]]: ... +) -> None: ... @native_call([Addr(Arg(0))]) def id_r32( @@ -54,7 +54,7 @@ def copy_r64( n: Int32, values: Float64[n], out: Float64[n] -) -> Returns["out", Float64[n]]: ... +) -> None: ... @native_call([Addr(Arg(0))]) def conj_c64( @@ -71,7 +71,7 @@ def copy_c128( n: Int32, values: Complex128[n], out: Complex128[n] -) -> Returns["out", Complex128[n]]: ... +) -> None: ... @native_call([Addr(Arg(0))]) def id_c_i32( diff --git a/tests/wrapper_codegen/printers/test_types_and_declarations.py b/tests/wrapper_codegen/printers/test_types_and_declarations.py index 4061f26d6..6a7e60610 100644 --- a/tests/wrapper_codegen/printers/test_types_and_declarations.py +++ b/tests/wrapper_codegen/printers/test_types_and_declarations.py @@ -154,7 +154,7 @@ def test_emit_matrix_shapes(): assert "x: Float64[::]" in code assert "y: Float64[::]" in code assert "Annotated[Float64[::]" not in code - assert 'Returns["y", Float64[::]]' in code + assert "y: Float64[::]\n) -> None: ..." in code def test_emit_explicit_bound_ranges_as_extents_without_source_dimension_metadata(): @@ -372,7 +372,7 @@ def test_emit_complex_fem_module(): # -------------------------------------------------------- assert "K: Float64[::, ::]" in code - assert 'Returns["K", Float64[::, ::]]' in code + assert "connectivity: Int32[::, ::]\n) -> None: ..." in code assert "coords: Float64[::, ::]" in code diff --git a/x2py/semantics/fortran2ir.py b/x2py/semantics/fortran2ir.py index 01c790b9d..cdd6707d1 100644 --- a/x2py/semantics/fortran2ir.py +++ b/x2py/semantics/fortran2ir.py @@ -1988,7 +1988,9 @@ def _is_returned_output_argument( return True if not is_output or semantic_type is None: return False - return FortranToIRConverter._is_scalar_copy_return(semantic_type) or semantic_type.rank > 0 + return FortranToIRConverter._is_python_value_scalar_output( + semantic_type + ) or FortranToIRConverter._is_native_descriptor_output(semantic_type) @staticmethod def _is_hidden_output_argument( @@ -2000,7 +2002,7 @@ def _is_hidden_output_argument( if not is_output or getattr(native_arg, "optional", False): return False return ( - FortranToIRConverter._is_scalar_copy_return(semantic_type) + FortranToIRConverter._is_python_value_scalar_output(semantic_type) or FortranToIRConverter._is_allocatable_array(semantic_type) or FortranToIRConverter._is_scalar_descriptor(semantic_type) ) @@ -2087,6 +2089,16 @@ def _is_allocatable_array(semantic_type: SemanticType | None) -> bool: and semantic_type.storage.array.allocatable ) + @staticmethod + def _is_native_descriptor_output(semantic_type: SemanticType | None) -> bool: + if semantic_type is None: + return False + if FortranToIRConverter._is_scalar_descriptor(semantic_type): + return True + storage = semantic_type.storage + array = storage.array if storage is not None else None + return bool(array is not None and (array.allocatable or array.pointer)) + @staticmethod def _is_scalar_descriptor(semantic_type: SemanticType | None) -> bool: return bool( @@ -2138,8 +2150,13 @@ def _scalar_descriptor_projection_value( raise ValueError(f"Scalar descriptor {name!r} has no Python argument or result projection") @staticmethod - def _is_scalar_copy_return(semantic_type: SemanticType | None) -> bool: - return bool(semantic_type is not None and semantic_type.rank == 0) + def _is_python_value_scalar_output(semantic_type: SemanticType | None) -> bool: + return bool( + semantic_type is not None + and semantic_type.rank == 0 + and not FortranToIRConverter._is_scalar_descriptor(semantic_type) + and (semantic_type.name == "String" or semantic_type.name in SEMANTIC_SCALAR_TYPE_NAMES) + ) @staticmethod def _is_scalar_character(semantic_type: SemanticType | None) -> bool: diff --git a/x2py/wrapper_codegen/docstrings.py b/x2py/wrapper_codegen/docstrings.py index c6df7dfe1..68e637eff 100644 --- a/x2py/wrapper_codegen/docstrings.py +++ b/x2py/wrapper_codegen/docstrings.py @@ -435,6 +435,8 @@ def _argument_lines(self, argument: ArgumentTransferPlan) -> tuple[str, ...]: lines.extend(self._array_lines(argument.array)) lines.extend(self._optional_lines(argument)) lines.extend(self._mutation_lines(argument)) + if argument.datatype_family is DatatypeFamily.DERIVED or argument.array is not None: + lines.extend(self._ownership_lines(argument.ownership_owner)) if argument.native_array_handle is not None: lines.append(f" Descriptor ownership: {argument.native_array_handle.descriptor_ownership.value}.") return tuple(lines) From 36160244ba4e4862255f0427c641430e70ce0587 Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Sat, 25 Jul 2026 23:48:53 +0100 Subject: [PATCH 17/35] improve overloading handling --- .../roadmap/semantic-pyi-wrapper-checklist.md | 8 +- .../guide/editing-semantic-pyi-contracts.md | 27 +++- docs/user/guide/error-handling.md | 4 +- docs/user/guide/generic-interfaces.md | 95 ++++++++++-- docs/user/guide/wrapping-derived-types.md | 139 ++++++++++++++++++ docs/user/reference/generated-functions.md | 13 +- docs/user/reference/semantic-pyi-format.md | 39 +++-- mkdocs.yml | 4 +- .../pyi/test_classes_and_overloads.py | 22 ++- tests/semantics/policy/test_wrapper_policy.py | 28 ++++ .../foverloads_f90.pyi | 6 +- .../__init__.pyi | 2 + .../foverloads_f90.pyi | 25 ++++ .../foverloads_f90.pyi | 4 +- .../test_surface_edit_contracts.py | 15 ++ .../foperators_f90/foperators_f90.pyi | 2 + .../foverloads_f90/foverloads_f90.pyi | 7 + .../foverloads_fixed/foverloads_fixed.pyi | 4 +- .../printers/test_classes_and_methods.py | 33 +++++ x2py/semantics/fortran2ir.py | 10 +- x2py/semantics/policy_completion.py | 9 -- x2py/semantics/pyi2ir.py | 10 +- x2py/wrapper_codegen/printers/pyi_printer.py | 10 +- 23 files changed, 445 insertions(+), 71 deletions(-) create mode 100644 tests/wrapper/fortran/edit_pyi_contracts/modified_contracts/foverloads_private_specific_without_bind/__init__.pyi create mode 100644 tests/wrapper/fortran/edit_pyi_contracts/modified_contracts/foverloads_private_specific_without_bind/foverloads_f90.pyi diff --git a/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md b/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md index 123d5b18f..c5e3d5189 100644 --- a/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md +++ b/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md @@ -465,10 +465,10 @@ X2PY_C_DOCS_END --> `tests/wrapper/fortran/edit_pyi_contracts/modified_contracts/foverloads_without_constructor_member/`, and `tests/wrapper/fortran/edit_pyi_contracts/modified_contracts/foverloads_added_bindings/`. -- [x] Module overload groups can be renamed while preserving the native generic - name with `@overload("specific", generic="native_generic")`, and the printer - round-trips that metadata. Evidence: - `tests/semantics/conversion/pyi/test_classes_and_overloads.py::test_convert_pyi_to_ir_renames_module_generic_and_round_trips_native_name` +- [x] Module overload candidates can override the linked specific's native call + with `@bind("native_generic")`, and the printer round-trips that metadata. + Evidence: + `tests/semantics/conversion/pyi/test_classes_and_overloads.py::test_convert_pyi_to_ir_applies_module_overload_bind_and_round_trips_native_name` and `docs/user/reference/semantic-pyi-format.md`. - [x] Explicit owner, transfer, and destruction triples are validated as a complete lifetime policy instead of independent switches. Supported triples diff --git a/docs/user/guide/editing-semantic-pyi-contracts.md b/docs/user/guide/editing-semantic-pyi-contracts.md index 9b81932bc..3bacc8753 100644 --- a/docs/user/guide/editing-semantic-pyi-contracts.md +++ b/docs/user/guide/editing-semantic-pyi-contracts.md @@ -176,11 +176,12 @@ Each candidate is an independent declaration. Removing one candidate narrows runtime dispatch without removing the generic name: ```python -from x2py.contracts import Float64, Int32, overload, private +from x2py.contracts import Float64, Int32, bind, overload, private @private def convert_integer(value: Int32) -> Int32: ... +@bind("convert") @overload("convert_integer") def convert(value: Int32) -> Int32: ... @@ -261,12 +262,10 @@ def norm2(values: Float64[:]) -> Float64: ... Link every Python overload to one concrete native specific: ```python -from x2py.contracts import Float64, Int32, overload, private +from x2py.contracts import Float64, Int32, overload -@private def scale_integer(value: Int32) -> Int32: ... -@private def scale_real(value: Float64) -> Float64: ... @overload("scale_integer") @@ -276,19 +275,31 @@ def scale(value: Int32) -> Int32: ... def scale(value: Float64) -> Float64: ... ``` -To rename the Python overload group while calling an existing native generic, -preserve the native generic explicitly: +Without `@bind`, each candidate calls its linked native specific. The specific +may still use `@private` when it should be hidden from Python. + +Contract `@private` does not prove native visibility. If the linked procedure +is Fortran-private, a direct call fails during the native build. Source-based +generation uses the public generic as the bind target in that case. + +Use `@bind` on an overload declaration when the bridge must call a native +generic instead: ```python -from x2py.contracts import Int32, overload, private +from x2py.contracts import Int32, bind, overload, private @private def convert_integer(value: Int32) -> Int32: ... -@overload("convert_integer", generic="convert") +@bind("convert") +@overload("convert_integer") def convert_number(value: Int32) -> Int32: ... ``` +The overload string still links the concrete contract. `@bind("convert")` +overrides only the native call target. It takes precedence over the linked +procedure's native name. + Candidates must be distinguishable by the implemented runtime dispatcher. Duplicate dtype/rank signatures are rejected because declaration order must not silently choose a native procedure. diff --git a/docs/user/guide/error-handling.md b/docs/user/guide/error-handling.md index 2466f016f..5fd3d88ca 100644 --- a/docs/user/guide/error-handling.md +++ b/docs/user/guide/error-handling.md @@ -1,5 +1,5 @@ --- -title: Error Handling +title: Error Handling & Diagnostics description: How x2py reports errors at different stages and how to diagnose them audience: users, advanced users prerequisites: common beginner workflow, data types @@ -8,7 +8,7 @@ status: maintained publication: reviewed --- -# Error Handling +# Error Handling & Diagnostics x2py reports failures at several distinct stages. Understanding which stage failed helps you know where to look and what to fix. diff --git a/docs/user/guide/generic-interfaces.md b/docs/user/guide/generic-interfaces.md index e9b01f6ac..726d376ce 100644 --- a/docs/user/guide/generic-interfaces.md +++ b/docs/user/guide/generic-interfaces.md @@ -1,6 +1,6 @@ --- -title: Generic Interfaces -description: How x2py supports Fortran named generics, type-bound generics, operators, and defined assignment +title: Generic Interfaces (Overloading) +description: How x2py supports Fortran named generic interfaces and exact overload dispatch audience: users, advanced users prerequisites: wrapping functions, wrapping subroutines, data types related: optional-arguments.md, wrapping-derived-types.md, error-handling.md @@ -8,9 +8,11 @@ status: maintained publication: reviewed --- -# Generic Interfaces +# Generic Interfaces (Overloading) -x2py turns Fortran named generic interfaces (and type-bound generics) into a single Python callable backed by an overload set. Dispatch is based on **exact** dtype, rank, and generated class — no implicit numeric coercion is performed. +x2py turns a Fortran generic interface into one Python callable. The callable +dispatches to a concrete native procedure by exact dtype, rank, and generated +class. It does not apply implicit numeric coercion. --- @@ -47,11 +49,42 @@ end module conversions Build it: ```bash +python3 -m x2py generate --pyi generic.f90 python3 -m x2py generic.f90 --out-dir build/generic ``` --- +## Generated Contract + +The semantic `.pyi` keeps the concrete procedures as private link targets. +Each public declaration adds one candidate to `convert`: + +```python +from x2py.contracts import Float64, Int32, bind, overload, private + +@private +def convert_integer(value: Int32) -> Int32: ... + +@private +def convert_real(value: Float64) -> Float64: ... + +@bind("convert") +@overload("convert_integer") +def convert(value: Int32) -> Int32: ... + +@bind("convert") +@overload("convert_real") +def convert(value: Float64) -> Float64: ... +``` + +The concrete procedures are private because the source exports `convert`, not +`convert_integer` or `convert_real`. Each `@overload` links the candidate to +its concrete contract. `@bind("convert")` tells the bridge to call the public +native generic. + +--- + ## Usage in Python ```python @@ -65,23 +98,56 @@ print(convert(np.int32(4))) # 14 print(convert(np.float64(4.0))) # 4.5 ``` -The correct specific procedure is chosen automatically based on the argument type. +The argument type selects the concrete procedure. `np.int32` calls +`convert_integer`; `np.float64` calls `convert_real`. --- -## Key Rules +## Extending an Overload Set -- Dispatch uses **exact** match on dtype, rank, and generated class. -- If no overload matches, a `TypeError` is raised. -- If two specifics collapse to the same Python signature, wrapper generation fails (ambiguity is rejected). -- Type-bound generics also work and dispatch after accounting for the passed object. +An edited contract can add an existing native procedure to a Python overload +set, even when it was not in the original Fortran interface. + +Suppose the contract already declares `convert_logical`. Add a public overload +declaration that links to it: + +```python +from x2py.contracts import Bool, Int32, overload + +def convert_logical(value: Bool) -> Int32: ... + +@overload("convert_logical") +def convert(value: Bool) -> Int32: ... +``` + +The decorator adds dispatch. It does not create a native implementation. The +target procedure must already exist in the contract and have a compatible call +shape. + +Leave `convert_logical` public to expose both names. Mark it `@private` when it +should only be available through `convert`. This changes Python visibility, +not the native call. The overload still calls the public native specific +directly. + +If that native specific is actually Fortran-private, the bridge cannot call it +directly. Keep `@bind("convert")` on the overload candidate. Source-based +generation adds this bind automatically. --- -## Defined Operators and Assignment +## Key Rules + +- Dispatch uses **exact** match on dtype, rank, and generated class. +- If no overload matches, a `TypeError` is raised. +- If two candidates have the same runtime signature, wrapper generation fails. +- Each `@overload` declaration links to exactly one concrete procedure. +- Without `@bind`, a candidate calls its linked procedure. +- With `@bind`, the candidate calls the named native generic instead. +- `@private` controls Python visibility only. -- Supported operators (`+`, `-`, `*`, `==`, etc.) can be used with normal Python syntax when the native generic defines them. -- Defined assignment (`=`) is exposed as an explicit `.assign(...)` method because Python `=` only rebinds names. +Type-bound generics, defined operators, and defined assignment become methods +on generated derived-type classes. They are introduced after ordinary methods +in Wrapping Derived Types. --- @@ -95,6 +161,7 @@ The correct specific procedure is chosen automatically based on the argument typ ## Next -- Continue with [Wrapping Derived Types](wrapping-derived-types.md) +- Continue with [Wrapping Derived Types](wrapping-derived-types.md) for + type-bound generics and operators - See [Error Handling](error-handling.md) for dispatch errors - For current generic and operator support, refer to the [Language Feature Matrix](../language-support/feature-matrix.md). diff --git a/docs/user/guide/wrapping-derived-types.md b/docs/user/guide/wrapping-derived-types.md index 59547aaf4..7986ca492 100644 --- a/docs/user/guide/wrapping-derived-types.md +++ b/docs/user/guide/wrapping-derived-types.md @@ -108,6 +108,145 @@ print(container.origin.x) # 12.0 --- +## Type-Bound Methods + +A public type-bound procedure becomes a method on the generated class. The +passed object becomes `self` and is not repeated in the Python call. + +```fortran +type :: counter + integer(4) :: value = 0 +contains + procedure :: increment +end type counter + +contains + +subroutine increment(self, amount) + class(counter), intent(inout) :: self + integer(4), intent(in) :: amount + self%value = self%value + amount +end subroutine increment +``` + +```python +item = counters.counter(value=np.int32(4)) +item.increment(np.int32(3)) +print(item.value) # 7 +``` + +The method mutates the existing `counter`; it does not replace the Python +object. + +--- + +## Type-Bound Generics + +A type-bound generic groups several concrete methods under one Python method. +For example, `add` can accept exact integer or real amounts: + +```fortran +type :: counter + integer(4) :: value = 0 +contains + procedure :: add_integer + procedure :: add_real + generic :: add => add_integer, add_real +end type counter +``` + +The generated contract uses the same explicit overload links as a module-level +generic: + +```python +from x2py.contracts import Float64, Int32, overload, private + +class counter: + @private + def add_integer(self, amount: Int32) -> Int32: ... + + @private + def add_real(self, amount: Float64) -> Float64: ... + + @overload("add_integer") + def add(self, amount: Int32) -> Int32: ... + + @overload("add_real") + def add(self, amount: Float64) -> Float64: ... +``` + +```python +print(item.add(np.int32(2))) # exact Int32 candidate +print(item.add(np.float64(0.5))) # exact Float64 candidate +``` + +The passed object participates in native dispatch but is already fixed by the +generated class. The remaining arguments must still match one candidate +exactly. + +--- + +## Defined Operators + +A defined operator with a wrapped derived-type operand becomes a Python magic +method. Its overload candidates are attached to the generated class. + +```fortran +interface operator(+) + module procedure add_points +end interface operator(+) + +contains + +function add_points(left, right) result(output) + type(point), intent(in) :: left, right + type(point) :: output + output%x = left%x + right%x + output%y = left%y + right%y +end function add_points +``` + +The generated contract exposes `operator(+)` as `__add__`: + +```python +from x2py.contracts import overload, private + +class point: + @overload("add_points") + def __add__(self, right: point) -> point: ... + +@private +def add_points(left: point, right: point) -> point: ... +``` + +Python uses the normal operator: + +```python +left = points.point(x=np.float64(1.0), y=np.float64(2.0)) +right = points.point(x=np.float64(3.0), y=np.float64(4.0)) +total = left + right +print(total.x, total.y) # 4.0 6.0 +``` + +| Fortran generic | Python method | Python syntax | +|-----------------|---------------|---------------| +| Binary `+`, `-`, `*`, `/`, `**` | Direct and reflected magic methods | `left + right` | +| Unary `+`, `-` | `__pos__`, `__neg__` | `+value`, `-value` | +| Relational operators | `__eq__`, `__lt__`, and related methods | `left == right` | +| `.and.`, `.or.`, `.not.` | `__and__`, `__or__`, `__invert__` | `left & right`, `~value` | +| Named operator `.name.` | `operator_name` or `r_operator_name` | Explicit method call | +| `assignment(=)` | `assign` | `target.assign(value)` | + +Python `and`, `or`, and `not` cannot be overloaded, so logical operator +generics use `&`, `|`, and `~`. Python assignment only rebinds a name, so +defined assignment uses `.assign(...)`. + +At least one operand must be a wrapped derived type. Other operands can be +supported primitive scalars, arrays, or generated classes. Their dispatch is +exact. + +--- + ## Scalar Actuals And Native Dummies The generated class can represent several native origins. Compatibility depends diff --git a/docs/user/reference/generated-functions.md b/docs/user/reference/generated-functions.md index 75d62a8f5..6e5e44de3 100644 --- a/docs/user/reference/generated-functions.md +++ b/docs/user/reference/generated-functions.md @@ -97,13 +97,21 @@ contract keeps one public name and links each public implementation back to a specific native procedure: ```python -from x2py.contracts import Float64, Int32, overload +from x2py.contracts import Float64, Int32, bind, overload, private +@private +def convert_integer(value: Int32) -> Int32: ... + +@private +def convert_real(value: Float64) -> Float64: ... + +@bind("convert") @overload("convert_integer") def convert( value: Int32 ) -> Int32: ... +@bind("convert") @overload("convert_real") def convert( value: Float64 @@ -113,7 +121,8 @@ def convert( Dispatch is exact. Indistinguishable overloads block generation instead of choosing by declaration order. `@overload(...)` and `@native_call(...)` do not coexist on one declaration; native projection metadata belongs to the linked -specific procedure. +specific procedure. An overload-level `@bind(...)` overrides the native call +target without replacing that linked contract. ## Evidence And Maintenance diff --git a/docs/user/reference/semantic-pyi-format.md b/docs/user/reference/semantic-pyi-format.md index c8d2eea58..b9a72ef58 100644 --- a/docs/user/reference/semantic-pyi-format.md +++ b/docs/user/reference/semantic-pyi-format.md @@ -1737,7 +1737,7 @@ decorator is x2py metadata; it is not `typing.overload` and must not be imported from `typing`. ```python -from x2py.contracts import Addr, Arg, Float64, Int32, Pass, native_call, overload, private +from x2py.contracts import Addr, Arg, Float64, Int32, Pass, bind, native_call, overload, private @private @native_call([Addr(Arg(0))]) @@ -1747,9 +1747,11 @@ def convert_integer(value: Int32) -> Int32: ... @native_call([Addr(Arg(0))]) def convert_real(value: Float64) -> Float64: ... +@bind("convert") @overload("convert_integer") def convert(value: Int32) -> Int32: ... +@bind("convert") @overload("convert_real") def convert(value: Float64) -> Float64: ... @@ -1769,9 +1771,10 @@ when it is needed to resolve a public overload declaration from the standalone that is otherwise part of the wrapper input. `@native_call` is not emitted merely to restate an unchanged native function name. -An overload declaration is only a Python dispatch link. It must not also carry -`@native_call`; the linked concrete procedure owns any native projection, -including argument reordering, `Pass()`, hidden values, and projected returns. +An overload declaration is a Python dispatch link. It must not also carry +`@native_call`; the linked concrete procedure owns argument reordering, +`Pass()`, hidden values, and projected returns. An overload-level `@bind` +changes only the final native call target. The loader resolves only the decorator string. It never guesses a target by signature. The target must exist exactly once, each target may occur only once @@ -1779,16 +1782,26 @@ in one overload set, and the public declaration must agree with the concrete call signature and return type. Missing, duplicate, ambiguous, and incompatible links are deterministic errors. -When a module-level Python overload group is renamed, `generic=` preserves the -native Fortran generic name: +Without overload-level `@bind`, a module candidate calls the linked procedure's +resolved native name. With `@bind`, it calls the named native symbol instead. +This is required when a public generic is the only native entry point for a +private specific: ```python -from x2py.contracts import Int32, overload +from x2py.contracts import Int32, bind, overload, private -@overload("convert_integer", generic="convert") +@private +def convert_integer(value: Int32) -> Int32: ... + +@bind("convert") +@overload("convert_integer") def convert_number(value: Int32) -> Int32: ... ``` +`@private` controls Python visibility only. For edited standalone contracts, +x2py cannot infer whether the linked native procedure is accessible. A direct +call to a Fortran-private specific therefore fails during the native build. + Python method names recover the native generic for ordinary operators. When two distinct Fortran generics share one Python method, the decorator also carries the otherwise unrecoverable operator spelling: @@ -1800,11 +1813,9 @@ from x2py.contracts import Bool, overload def __eq__(self, other: value) -> Bool: ... ``` -For module overloads, the optional `generic=` argument names the native generic -when it differs from the Python overload-set name. For class methods it is -restricted to a compatible operator or assignment generic. It is emitted for -`.eqv.` and `.neqv.`, which would otherwise be indistinguishable from -`operator(==)` and `operator(/=)`. +For class methods, `generic=` is restricted to a compatible operator or +assignment generic. It is emitted for `.eqv.` and `.neqv.`, which would +otherwise be indistinguishable from `operator(==)` and `operator(/=)`. transferring ownership; - `intent(inout)` mutates that existing instance; - `intent(out)` fills a caller-provided instance without returning it again; - and +- a dummy without `intent` follows the conservative `intent(inout)` rule; and - a function result is copied into a new wrapper-owned native instance before the Fortran temporary expires. diff --git a/docs/user/guide/index.md b/docs/user/guide/index.md index 5ee7a9169..152709697 100644 --- a/docs/user/guide/index.md +++ b/docs/user/guide/index.md @@ -20,6 +20,7 @@ behavior, packaging, and distribution. - [Data Types](data-types.md) — Fortran types, semantic `.pyi` names, exact NumPy dtypes, strings, and arrays - [Arrays](arrays.md) — Rank, shape, strides, contiguity, and layout rules +- [Strings](strings.md) — Immutable text, mutable byte storage, and string arrays - [Wrapping Functions](wrapping-functions.md) - [Wrapping Subroutines](wrapping-subroutines.md) - [Wrapping Modules](wrapping-modules.md) @@ -56,6 +57,7 @@ behavior, packaging, and distribution. - **[Fortran Wrapper Guide](fortran-wrapper.md)** — Complete reference for the generated runtime surface - [Editing Semantic `.pyi` Contracts](editing-semantic-pyi-contracts.md) — When and how to customize contracts +- [Raw Addresses](raw-addresses.md) — Advanced primitive, array, and fixed-string address boundaries - [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) — Full annotation reference - [Language Feature Matrix](../language-support/feature-matrix.md) — Current support status (supported / partial / unsupported) diff --git a/docs/user/guide/optional-arguments.md b/docs/user/guide/optional-arguments.md index 2da48ccc4..d863d2289 100644 --- a/docs/user/guide/optional-arguments.md +++ b/docs/user/guide/optional-arguments.md @@ -83,6 +83,8 @@ print(adjust(np.int32(5), offset=np.int32(10))) # 15 (keyword) - Optional arrays and derived types also accept `None` to indicate absence. - Optional `intent(out)` / `intent(inout)` arguments remain visible in Python so you can control `present(...)`. +- An optional argument without `intent` uses the same conservative + `intent(inout)` behavior when present. Scalar allocatable and pointer descriptors are the three-state exception: omission means absent, `None` means present but unallocated or unassociated, @@ -143,4 +145,5 @@ rules. Check the generated `.pyi` contract when mixing output kinds. ## Next - Continue with [Generic Interfaces](generic-interfaces.md). -- For optional outputs and memory, see [Error Handling](error-handling.md) and [Memory Management](memory-management.md). +- For optional outputs and memory, see [Error Handling](error-handling.md) and + [Memory Management](memory-management.md). diff --git a/docs/user/guide/raw-addresses.md b/docs/user/guide/raw-addresses.md new file mode 100644 index 000000000..d394a721f --- /dev/null +++ b/docs/user/guide/raw-addresses.md @@ -0,0 +1,194 @@ +--- +title: Raw Addresses +description: Pass primitive, array, and fixed-string storage addresses through semantic contracts +audience: advanced users +prerequisites: arrays, strings, editing semantic pyi contracts +related: data-types.md, arrays.md, strings.md, editing-semantic-pyi-contracts.md, ../reference/semantic-pyi-format.md +status: maintained +publication: reviewed +--- + +# Raw Addresses + +`Addr(T)` makes an integer address part of the Python API. +x2py casts the address and passes it to native code without owning the memory. + +Use this boundary only when the API must expose an address. Prefer checked +scalar storage, arrays, and strings for normal wrappers. + +## Checked Storage Or Raw Address + +| Contract | Python argument | Validation | +| --- | --- | --- | +| `Int32[()]` | 0-D array with dtype `np.int32` | Dtype, rank, and writeability | +| `Addr(Int32)` | Integer address | Pointer-sized integer only | +| `Float64[rows, columns]` | NumPy array | Dtype, shape, order, and writeability | +| `Addr(Float64[rows, columns])` | Integer address | Extent expressions only | +| `String[8][()]` | 0-D NumPy bytes array with dtype `S8` | Dtype, length, and writeability | +| `Addr(String[8])` | Integer address | Declared fixed length only | + +`T[()]` changes the Python storage representation, not the native primitive +datatype. It is usually the better choice for scalar mutation. + +## Complete Example + +Create `raw_api.f90`: + +```fortran +module raw_api + implicit none +contains + + subroutine increment(value) + integer(4), intent(inout) :: value + value = value + 1 + end subroutine increment + + subroutine scale(rows, columns, values) + integer(4), intent(in) :: rows, columns + real(8), intent(inout) :: values(rows, columns) + values = 2.0_8 * values + end subroutine scale + + subroutine edit_label(label) + character(len=8), intent(inout) :: label + label(1:1) = "X" + end subroutine edit_label + +end module raw_api +``` + +Generate a starter contract: + +```bash +python3 -m x2py generate --pyi raw_api.f90 --out contracts/raw +``` + +Edit `contracts/raw/raw_api.pyi`: + +```python +from x2py.contracts import Addr, Arg, Float64, Int32, String, native_call + +def increment(value: Addr(Int32)) -> None: ... + +@native_call([Addr(Arg(0)), Addr(Arg(1)), Arg(2)]) +def scale( + rows: Int32, + columns: Int32, + values: Addr(Float64[rows, columns]), +) -> None: ... + +def edit_label(label: Addr(String[8])) -> None: ... +``` + +Build from the edited contract and native source: + +```bash +python3 -m x2py contracts/raw/__init__.pyi \ + --native-fortran-sources raw_api.f90 \ + --out-dir build/raw +``` + +## Primitive Address + +Keep the NumPy owner in a variable for the full call: + +```python +import sys + +import numpy as np + +sys.path.insert(0, "build/raw") +from raw.raw_api import increment + +value = np.array(3, dtype=np.int32) +increment(value.ctypes.data) + +print(value[()]) # 4 +``` + +For checked mutation, use `Int32[()]` instead. The call then accepts `value` +directly and validates its storage. + +## Array Address + +`array.ctypes.data` is the address of the first array element. +The owner must contain enough storage for every declared extent. + +```python +import sys + +import numpy as np + +sys.path.insert(0, "build/raw") +from raw.raw_api import scale + +values = np.asfortranarray( + [[1.0, 2.0], [3.0, 4.0]], + dtype=np.float64, +) + +scale(np.int32(2), np.int32(2), values.ctypes.data) +print(values) +# [[2. 4.] +# [6. 8.]] +``` + +The raw address does not carry shape, order, or strides. Passing C-order +storage does not make a Fortran routine use C ordering. + +Every raw array extent must use a literal or a visible scalar argument. +Unresolved forms such as `Addr(Float64[:])` are invalid. + +## Fixed-String Address + +A fixed string address points to exactly the declared number of bytes. +Use a NumPy `S8` owner for `Addr(String[8])`: + +```python +import sys + +import numpy as np + +sys.path.insert(0, "build/raw") +from raw.raw_api import edit_label + +label = np.array("alpha ", dtype="S8") +edit_label(label.ctypes.data) + +print(label[()]) # b'Xlpha ' +``` + +This mutates bytes storage. It does not return a Python `str`. +Use `String[8]` with `Returns[...]` when the result should be immutable text. +Use `String[8][()]` when the wrapper should validate mutable storage. + +## Safety Rules + +- Keep the NumPy or ctypes owner alive until the call returns. +- Do not pass the address of a temporary expression. +- Use the exact native dtype and alignment. +- Supply enough storage for every declared array extent. +- Match the native array ordering and layout. +- Use writable memory when native code may modify it. +- Treat address zero as null only when the native routine allows null. + +x2py cannot validate the pointee's lifetime, dtype, size, shape, order, +alignment, ownership, or writeability. A wrong address can crash the process. + +## `Addr(T)` And `Addr(Arg(...))` + +These spellings describe different boundaries: + +- `Addr(T)` means the Python caller passes an integer address. +- `Addr(Arg(i))` means x2py takes the address of a converted scalar argument. + +Arrays, rank-zero storage, strings, and raw addresses already use storage +addresses. Do not wrap their `Arg(i)` projection in another `Addr(...)`. + +## Next + +- [Strings](strings.md) for immutable and checked mutable character boundaries +- [Editing Semantic `.pyi` Contracts](editing-semantic-pyi-contracts.md) +- [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) for complete + `Addr(...)` validation rules diff --git a/docs/user/guide/strings.md b/docs/user/guide/strings.md new file mode 100644 index 000000000..1ee1667c6 --- /dev/null +++ b/docs/user/guide/strings.md @@ -0,0 +1,186 @@ +--- +title: Strings +description: Immutable strings, mutable character storage, and NumPy byte arrays in x2py +audience: users +prerequisites: data types, arrays +related: data-types.md, arrays.md, raw-addresses.md, ../reference/semantic-pyi-format.md +status: maintained +publication: reviewed +--- + +# Strings + +x2py uses Python `str` for scalar character values. +Mutable character storage uses fixed-width NumPy bytes arrays. + +The contract decides whether native mutation becomes a new `str` or changes +caller-owned storage. + +## Choose A String Boundary + +| Contract | Python value | Native mutation | +| --- | --- | --- | +| `String` | Variable-length `str` | Returned only when projected | +| `String[8]` | `str` encoded as exactly 8 bytes | Returned as a new `str` | +| `String[8][()]` | Rank-zero NumPy array with dtype `S8` | Visible in place | +| `String[8][count]` | NumPy bytes array with dtype `S8` | Visible in place | +| `Addr(String[8])` | Integer address | Visible through caller-owned memory | + +Use normal string and NumPy contracts by default. Raw addresses are an +advanced boundary covered later in the guide. + +## Complete Example + +Create `strings_api.f90`: + +```fortran +module strings_api + implicit none +contains + + subroutine edit_text(text) + character(len=8), intent(inout) :: text + text(1:1) = "X" + end subroutine edit_text + + subroutine edit_buffer(text) + character(len=8), intent(inout) :: text + text(1:1) = "X" + end subroutine edit_buffer + + function make_text() result(text) + character(len=8) :: text + text = "ready" + end function make_text + + subroutine edit_labels(count, labels) + integer(4), intent(in) :: count + character(len=8), intent(inout) :: labels(count) + integer(4) :: index + + do index = 1, count + labels(index)(1:1) = "X" + end do + end subroutine edit_labels + +end module strings_api +``` + +Generate a starter contract: + +```bash +python3 -m x2py generate --pyi strings_api.f90 --out contracts/strings +``` + +Edit `contracts/strings/strings_api.pyi` to use these Python boundaries: + +```python +from x2py.contracts import Addr, Arg, Int32, Returns, String, native_call + +def edit_text(text: String[8]) -> Returns["text", String[8]]: ... + +def edit_buffer(text: String[8][()]) -> None: ... + +def make_text() -> String[8]: ... + +@native_call([Addr(Arg(0)), Arg(1)]) +def edit_labels( + count: Int32, + labels: String[8][count], +) -> None: ... +``` + +Build from the edited contract and native source: + +```bash +python3 -m x2py contracts/strings/__init__.pyi \ + --native-fortran-sources strings_api.f90 \ + --out-dir build/strings +``` + +## Immutable Values + +`String[8]` accepts a Python `str` whose encoded length is exactly eight bytes. +The wrapper copies it into native storage. + +```python +import sys + +sys.path.insert(0, "build/strings") +from strings.strings_api import edit_text, make_text + +original = "alpha " +changed = edit_text(original) + +print(repr(original)) # 'alpha ' +print(repr(changed)) # 'Xlpha ' +print(repr(make_text())) # 'ready ' +``` + +Python strings are immutable. `Returns[...]` copies the changed native buffer +into a new `str`. Without that projection, the mutation is discarded. + +## Mutable Scalar Storage + +`String[8][()]` accepts a rank-zero NumPy bytes array. +Native writes change the same object. + +```python +import sys + +import numpy as np + +sys.path.insert(0, "build/strings") +from strings.strings_api import edit_buffer + +buffer = np.array("alpha ", dtype="S8") +edit_buffer(buffer) + +print(buffer[()]) # b'Xlpha ' +``` + +The public value is bytes storage. Reading `buffer[()]` returns `np.bytes_`, +not `str`. + +## String Arrays + +String arrays use fixed-width NumPy bytes dtypes. The dtype item size is the +Fortran character length. + +```python +import sys + +import numpy as np + +sys.path.insert(0, "build/strings") +from strings.strings_api import edit_labels + +labels = np.array([b"alpha ", b"beta "], dtype="S8") +edit_labels(np.int32(labels.size), labels) + +print(labels) # [b'Xlpha ' b'Xeta '] +``` + +The wrapper checks rank, shape, dtype, and writeability before the call. +Unicode and object arrays are rejected. + +## Length And Encoding + +- `String[8]` requires exactly eight encoded bytes. +- `String` accepts a runtime character length. +- Fixed-width results retain trailing Fortran blanks. +- Embedded NUL bytes are rejected for scalar Python strings. +- `String[8][()]` and `String[8][count]` require dtype `S8`. +- A dummy without `intent` uses the conservative `intent(inout)` behavior. + +Mutable deferred-length scalar storage is not supported. Use a fixed-width +buffer or an immutable replacement result. + +## Next + +- [Wrapping Functions](wrapping-functions.md) +- [Wrapping Subroutines](wrapping-subroutines.md) for complete `intent` and + result-projection rules +- [Raw Addresses](raw-addresses.md) for `Addr(String[n])` +- [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) for the full + character grammar diff --git a/docs/user/guide/wrapping-derived-types.md b/docs/user/guide/wrapping-derived-types.md index 7986ca492..ce648cbdf 100644 --- a/docs/user/guide/wrapping-derived-types.md +++ b/docs/user/guide/wrapping-derived-types.md @@ -101,6 +101,8 @@ print(container.origin.x) # 12.0 - **Ownership**: Wrapper-owned objects are finalized when the Python object is garbage-collected. - **Mutation**: `intent(out)` and `intent(inout)` modify a caller-provided instance and do not return it again. +- **Missing intent**: A dummy without `intent` follows the same conservative + in-place rule as `intent(inout)`. - **Fields**: Public scalar numeric/logical/complex fields become Python attributes. - **Nested types**: Appear as borrowed child wrappers (they don’t own the memory). - **Results**: Derived-type function results create new wrapper-owned objects. diff --git a/docs/user/guide/wrapping-functions.md b/docs/user/guide/wrapping-functions.md index 194fb45aa..66eee8c35 100644 --- a/docs/user/guide/wrapping-functions.md +++ b/docs/user/guide/wrapping-functions.md @@ -129,4 +129,16 @@ total, count = sum_with_count(data_array) - Always pass **exact NumPy dtypes** (`np.float64`, `np.int32`, etc.). - Array results are returned as new NumPy arrays (copies). -- `intent(out)` and `intent(inout)` values are handled by the generated contract. +- Projected scalar outputs follow the direct function result in the return + tuple. +- Caller-provided arrays and derived objects mutate in place. They are not + repeated in the return tuple. +- Without `intent`, an argument uses conservative `intent(inout)` behavior. + Primitive scalar replacements follow the direct function result in the + Python return tuple. + +## Next + +- [Wrapping Subroutines](wrapping-subroutines.md) for the complete argument + projection rules +- [Wrapping Modules](wrapping-modules.md) for native namespaces and state diff --git a/docs/user/guide/wrapping-subroutines.md b/docs/user/guide/wrapping-subroutines.md index ba3230124..2b7b49cde 100644 --- a/docs/user/guide/wrapping-subroutines.md +++ b/docs/user/guide/wrapping-subroutines.md @@ -10,7 +10,9 @@ publication: reviewed # Wrapping Subroutines -A Fortran `subroutine` has no direct return value. Instead, its `intent(out)` and `intent(inout)` arguments are projected into the Python result. +A Fortran `subroutine` has no direct return value. Projected scalar and +native-created outputs form the Python result. Caller-provided mutable objects +change in place. --- @@ -25,11 +27,13 @@ A Fortran `subroutine` has no direct return value. Instead, its `intent(out)` an | `intent(inout)` array | Visible writable NumPy array | Mutated in place; not returned | | Derived `intent(out/inout)` | Visible generated object | Mutated in place; not returned | | `intent(out)` allocatable | Hidden (or optional) | `Allocatable[...]` handle | +| No `intent` | Visible argument | Conservative `intent(inout)` rule | Without `intent`, x2py uses the conservative `intent(inout)` behavior. A primitive scalar stays visible and its replacement value is returned. If the dummy is known to be input-only, remove that projected result from the -generated contract. +generated contract. This is common in legacy sources, but the rule applies to +any dummy declaration without `intent`. --- diff --git a/docs/user/language-support/feature-matrix.md b/docs/user/language-support/feature-matrix.md index a52a24a61..1116a8e3c 100644 --- a/docs/user/language-support/feature-matrix.md +++ b/docs/user/language-support/feature-matrix.md @@ -47,7 +47,7 @@ inspection-only or partial support. | Generic constructor interfaces and overloaded runtime initialization | Supported | [Constructors](../guide/wrapping-derived-types.md#constructors) | [Class policy and lowering](../../developer/source-map.md#common-change-routes) | [Bound constructor tests](../../../tests/wrapper/fortran/derived_types/test_phase9_bound_constructors.py), [overload tests](../../../tests/wrapper/fortran/naming/test_phase9_class_overloads.py) | Candidates require distinguishable completed Python signatures; incomplete or ambiguous sets are blocked before emission. | | Module variables, constants, saved state, and common-block procedure state | Supported | [Wrapping modules](../guide/wrapping-modules.md) | [Module state route](../../developer/feature-to-code-map.md#workflow-feature-pointers) | [Module state tests](../../../tests/wrapper/fortran/module_state/test_module_state.py), [scalar-derived matrix tests](../../../tests/wrapper/fortran/derived_types/test_scalar_derived_actual_dummy_matrix.py), [common-block tests](../../../tests/wrapper/fortran/module_state/test_common_blocks.py) | Common-block storage is not exported as Python variables. Rank-zero derived module objects use direct, scoped, allocation-transaction, or pointer-transaction handoff selected before lowering. | | Fortran enum constants | Supported | [Enumerations](../guide/enumerations.md) | [Semantic constants route](../../developer/source-map.md#common-change-routes) | [Enum tests](../../../tests/wrapper/fortran/scalars/test_fortran_enums.py) | No Python `Enum` or `IntEnum` classes are generated. | -| Scalar character arguments, results, and fields | Supported | [Strings](../guide/data-types.md#strings) | [Character bridge route](../../developer/source-map.md#common-change-routes) | [Character argument tests](../../../tests/wrapper/fortran/strings/test_character_arguments.py), [edge-case tests](../../../tests/wrapper/fortran/strings/test_character_edge_cases.py) | Character arrays use fixed-width NumPy bytes dtype; mutable scalar deferred-length storage is blocked. | +| Scalar character arguments, results, and fields | Supported | [Strings](../guide/strings.md) | [Character bridge route](../../developer/source-map.md#common-change-routes) | [Character argument tests](../../../tests/wrapper/fortran/strings/test_character_arguments.py), [edge-case tests](../../../tests/wrapper/fortran/strings/test_character_edge_cases.py) | Character arrays use fixed-width NumPy bytes dtype; mutable scalar deferred-length storage is blocked. | | Scalar kind coverage | Supported | [Data types](../guide/data-types.md) | [Fortran type probe](../../developer/source-map.md#hotspot-index) | [Scalar kind tests](../../../tests/wrapper/fortran/scalars/test_scalar_kinds.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 | [Packaging](../guide/packaging.md), [multi-source recipe](../examples/recipes/build-multiple-fortran-sources.md) | [Wrapper orchestration](../../developer/source-map.md#common-change-routes) | [Multi-source tests](../../../tests/wrapper/fortran/multiple_files/test_multi_source_builds.py), [compiler verbose tests](../../../tests/wrapper/fortran/build_from_source/test_compiler_verbose.py) | x2py does not discover, reorder, or resolve all external source dependencies. | | Visibility, naming, keyword escaping, and collision policy | Supported | [Visibility and naming](../guide/fortran-wrapper.md#visibility-naming-and-the-python-surface) | [Naming policy](../../developer/source-map.md#hotspot-index) | [Visibility/naming tests](../../../tests/wrapper/fortran/naming/test_visibility_naming.py) | Strict mode rejects names that default mode can normalize. | @@ -86,7 +86,7 @@ X2PY_C_DOCS_END --> | Blocked array forms | Unsupported | [Unsupported array forms](../guide/arrays.md#unsupported-forms) | [Array policy route](../../developer/source-map.md#common-change-routes) | [Array contract tests](../../../tests/wrapper/fortran/arrays/test_array_contracts.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](../guide/wrapping-derived-types.md#inheritance-and-polymorphism) | [Class policy route](../../developer/source-map.md#common-change-routes) | [Inheritance tests](../../../tests/wrapper/fortran/derived_types/test_inheritance.py) | Results, mutable dummies, arrays, polymorphic allocatable/pointer scalars, and `class(*)` are blocked. | | Ambiguous or incomplete constructor overload sets | Unsupported | [Constructor limitations](../guide/wrapping-derived-types.md#constructors) | [Constructor route](../../developer/source-map.md#common-change-routes) | [Constructor overload tests](../../../tests/wrapper/fortran/naming/test_phase9_class_overloads.py), [class-plan validation tests](../../../tests/wrapper_codegen/test_phase9_class_surfaces.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/data-types.md#strings) | [Character bridge route](../../developer/source-map.md#common-change-routes) | [Character edge tests](../../../tests/wrapper/fortran/strings/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. | +| 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/wrapper/fortran/strings/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/wrapper/fortran/scalars/test_scalar_kinds.py) | x2py blocks rather than silently losing precision or Boolean storage semantics. | For diagnostic and inspection commands beyond the main build path, start with `python3 -m x2py --help`, then continue to the -[Fortran wrapper guide](docs/user/guide/fortran-wrapper.md). +[CLI command reference](docs/user/reference/cli-commands.md). diff --git a/docs/developer/fortran-parser-reference.md b/docs/developer/fortran-parser-reference.md index a300dc97f..d2c93a2f7 100644 --- a/docs/developer/fortran-parser-reference.md +++ b/docs/developer/fortran-parser-reference.md @@ -617,8 +617,6 @@ error was raised internally: python -m x2py bad.f90 --debug ``` -`--debug-traceback` remains accepted as a compatibility alias. - The same developer mode can be enabled with the environment variable `FORTRAN_PARSER_DEBUG=1`: @@ -814,8 +812,7 @@ disable ANSI output. On Windows, ANSI console compatibility is enabled through For parser development, `format_diagnostic(debug=True)` appends a note with the internal parser file, line, and function that raised the error. The CLI exposes -this through `--debug`, its compatibility alias `--debug-traceback`, or -`FORTRAN_PARSER_DEBUG=1`; normal CLI parse errors intentionally hide Python +this through `--debug` or `FORTRAN_PARSER_DEBUG=1`; normal CLI parse errors intentionally hide Python tracebacks. The sections below list each error category, the triggering condition, and the diff --git a/docs/developer/source-map.md b/docs/developer/source-map.md index 8910ee005..d693ea0ad 100644 --- a/docs/developer/source-map.md +++ b/docs/developer/source-map.md @@ -40,13 +40,13 @@ change crosses ownership boundaries. | CLI flags, stage selection, output formatting, diagnostics | `x2py/cli.py` | `docs/user/reference/cli-commands.md`, `docs/user/tutorials/basic-wrapper.md`, `docs/user/examples/verified-cookbook.md` | `tests/cli/`, `tests/docs/test_examples.py` | | Compiler preprocessing, include paths, macros, and target flags | `x2py/pipeline/preprocessing.py` | `docs/user/examples/recipes/compiler-preprocessing.md`, `docs/developer/fortran-parser-reference.md` | `tests/pipeline/preprocessing/`, `tests/pipeline/preprocessing/test_parser_boundaries.py` | | Fortran parser facts and diagnostics | `x2py/parsers/fortran/parser.py` | `docs/developer/fortran-parser-reference.md`, `docs/user/examples/recipes/inspect-fortran-api.md` | `tests/parser/`, `tests/parsing/fortran/test_fortran_fixture_suite.py` | -| Semantic `.pyi` parsing, conversion, printing, package generation, and round-trip behavior | `x2py/parsers/pyi/parser.py`, `x2py/pipeline/pyi.py`, `x2py/semantics/pyi2ir.py`, `x2py/wrapper_codegen/printers/pyi_printer.py` | `docs/user/reference/semantic-pyi-format.md`, `docs/user/guide/editing-semantic-pyi-contracts.md`, `docs/user/examples/recipes/semantic-pyi-contracts.md` | `tests/pyi/`, `tests/pipeline/pyi_builds/test_contract_package_generation.py`, `tests/wrapper_codegen/printers/` | +| Semantic `.pyi` parsing, conversion, printing, package generation, and round-trip behavior | `x2py/parsers/pyi/parser.py`, `x2py/pipeline/pyi.py`, `x2py/semantics/pyi2ir.py`, `x2py/wrapper_codegen/printers/pyi_printer.py` | `docs/user/reference/semantic-pyi-format.md`, `docs/user/reference/editing-semantic-pyi-contracts.md`, `docs/user/examples/recipes/semantic-pyi-contracts.md` | `tests/pyi/`, `tests/pipeline/pyi_builds/test_contract_package_generation.py`, `tests/wrapper_codegen/printers/` | | Wrapper-planning errors and support claims | `x2py/semantics/policy_completion.py`, `x2py/wrapper_codegen/planner.py` | `docs/user/reference/diagnostic-codes.md`, `docs/user/language-support/feature-matrix.md` | `tests/semantics/policy/`, `tests/wrapper_codegen/` | -| Source-driven Fortran wrapper orchestration | `x2py/pipeline/build.py` | `docs/user/guide/fortran-wrapper.md`, `docs/user/examples/recipes/build-and-import-cli.md` | `tests/wrapper/fortran/build_from_source/test_build_modes.py`, `tests/wrapper/fortran/multiple_files/test_multi_source_builds.py` | -| Semantic `.pyi` wrapper orchestration from native artifacts | `x2py/pipeline/build.py`, `x2py/pipeline/pyi.py`, `x2py/semantics/pyi2ir.py` | `docs/user/guide/fortran-wrapper.md`, `docs/user/reference/semantic-pyi-format.md` | `tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py`, `tests/wrapper/fortran/build_from_pyi/test_contract_package_runtime.py` | -| Ownership, lifetime, output projection, and unsupported wrapper policy | `x2py/semantics/policy_completion.py`, `x2py/semantics/ownership.py`, `x2py/wrapper_codegen/planner.py` | `docs/user/guide/fortran-wrapper.md`, `docs/user/guide/editing-semantic-pyi-contracts.md` | `tests/semantics/policy/`, `tests/wrapper_codegen/`, `tests/wrapper/fortran/` | +| Source-driven Fortran wrapper orchestration | `x2py/pipeline/build.py` | `docs/user/reference/fortran-wrapper.md`, `docs/user/examples/recipes/build-and-import-cli.md` | `tests/wrapper/fortran/build_from_source/test_build_modes.py`, `tests/wrapper/fortran/multiple_files/test_multi_source_builds.py` | +| Semantic `.pyi` wrapper orchestration from native artifacts | `x2py/pipeline/build.py`, `x2py/pipeline/pyi.py`, `x2py/semantics/pyi2ir.py` | `docs/user/reference/fortran-wrapper.md`, `docs/user/reference/semantic-pyi-format.md` | `tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py`, `tests/wrapper/fortran/build_from_pyi/test_contract_package_runtime.py` | +| Ownership, lifetime, output projection, and unsupported wrapper policy | `x2py/semantics/policy_completion.py`, `x2py/semantics/ownership.py`, `x2py/wrapper_codegen/planner.py` | `docs/user/reference/fortran-wrapper.md`, `docs/user/reference/editing-semantic-pyi-contracts.md` | `tests/semantics/policy/`, `tests/wrapper_codegen/`, `tests/wrapper/fortran/` | | Immediate callback policy, typed adapters, and trampolines | `x2py/semantics/wrapper_policy.py`, `x2py/semantics/policy_completion.py`, `x2py/wrapper_codegen/plan.py`, `x2py/wrapper_codegen/planner.py`, `x2py/wrapper_codegen/c/binding.py`, `x2py/wrapper_codegen/fortran/bridge.py` | `docs/user/guide/callbacks.md`, `docs/user/reference/callbacks.md`, `docs/user/reference/semantic-pyi-format.md` | `tests/wrapper_codegen/test_phase10_callbacks.py`, `tests/wrapper/fortran/callbacks/` | -| Native compilation, binding support, and shared-library linking | `x2py/pipeline/build.py`, `x2py/compiling/compilers.py`, `x2py/compiling/native_support.py` | `docs/user/guide/fortran-wrapper.md`, `docs/developer/build-system.md` | `tests/wrapper/fortran/build_from_source/test_runtime_abi.py`, `tests/wrapper/fortran/build_from_source/test_build_modes.py` | +| Native compilation, binding support, and shared-library linking | `x2py/pipeline/build.py`, `x2py/compiling/compilers.py`, `x2py/compiling/native_support.py` | `docs/user/reference/fortran-wrapper.md`, `docs/developer/build-system.md` | `tests/wrapper/fortran/build_from_source/test_runtime_abi.py`, `tests/wrapper/fortran/build_from_source/test_build_modes.py` | | Public Python exports | `x2py/__init__.py` | `README.md`, `docs/user/reference/python-api.md` | `tests/parsing/fortran/test_public_entrypoints.py` | | Source navigation documentation | `docs/developer/source-map.md`, `docs/developer/feature-to-code-map.md`, package README files | `docs/developer/source-map.md` | `tests/docs/test_structure.py` | @@ -57,8 +57,8 @@ X2PY_C_DOCS_END --> ## Package Map @@ -81,7 +81,7 @@ X2PY_C_DOCS_END --> | `x2py/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/parser/c/`, `docs/developer/c-parser-reference.md` | | `x2py/parsers/pyi/` | Semantic `.pyi` text/file parsing to Python AST. | `parser.py` | `tests/parsing/pyi/`, `docs/user/reference/semantic-pyi-format.md` | | `x2py/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/semantics/`, `tests/pyi/`, `docs/user/reference/semantic-ir.md`, `docs/user/reference/semantic-pyi-format.md` | -| `x2py/wrapper_codegen/` | Canonical wrapper planning, C/Fortran generation, source printing, and semantic `.pyi` printing | `plan.py`, `planner.py`, `generator.py`, `printers/` | `tests/wrapper_codegen/`, `tests/wrapper/`, `docs/user/guide/fortran-wrapper.md` | +| `x2py/wrapper_codegen/` | Canonical wrapper planning, C/Fortran generation, source printing, and semantic `.pyi` printing | `plan.py`, `planner.py`, `generator.py`, `printers/` | `tests/wrapper_codegen/`, `tests/wrapper/`, `docs/user/reference/fortran-wrapper.md` | | `x2py/naming/` | Unified public-name and generated-symbol policy for Python, C, and Fortran targets | `policy.py` | naming, visibility, and wrapper runtime tests | X2PY_C_DOCS_END --> diff --git a/docs/maintainer/design/semantic-multilanguage-wrapper-runtime-architecture.md b/docs/maintainer/design/semantic-multilanguage-wrapper-runtime-architecture.md index dc5e243a7..13254cd62 100644 --- a/docs/maintainer/design/semantic-multilanguage-wrapper-runtime-architecture.md +++ b/docs/maintainer/design/semantic-multilanguage-wrapper-runtime-architecture.md @@ -13,7 +13,7 @@ publication: draft > **Status:** This is a long-term architecture document, not a statement that > every backend below exists. The source-driven Fortran-to-Python wrapper is > implemented and documented in -> [the Fortran wrapper guide](../../user/guide/fortran-wrapper.md). C parsing, semantic IR, +> [the Fortran wrapper reference](../../user/reference/fortran-wrapper.md). C parsing, semantic IR, > `.pyi`, and semantic inspection are implemented, but the runtime backend for > user-supplied C inputs will be added later. Other language backends and the > broader coercion runtime remain design goals. diff --git a/docs/maintainer/design/wrapper-design-notes.md b/docs/maintainer/design/wrapper-design-notes.md index c79dc748c..9d75b2470 100644 --- a/docs/maintainer/design/wrapper-design-notes.md +++ b/docs/maintainer/design/wrapper-design-notes.md @@ -1,7 +1,7 @@ --- title: Wrapper Design Notes audience: maintainers -prerequisites: Fortran wrapper guide, semantic IR reference +prerequisites: Fortran wrapper reference, semantic IR reference related: overall-architecture.md, ../internal-architecture/wrapper-generation-pipeline.md status: design publication: draft @@ -21,7 +21,7 @@ X2PY_C_DOCS_END --> Reference details live in: - `docs/developer/fortran-parser-reference.md` -- `docs/user/guide/fortran-wrapper.md` +- `docs/user/reference/fortran-wrapper.md` - `docs/user/reference/semantic-ir.md` | Source loading and preprocessing | `x2py/pipeline/preprocessing.py` | `docs/developer/source-map.md`, parser references | | Editable semantic contracts | `x2py/parsers/pyi/parser.py`, `x2py/pipeline/pyi.py`, `x2py/semantics/pyi2ir.py`, `x2py/wrapper_codegen/printers/pyi_printer.py` | `docs/user/reference/semantic-pyi-format.md` | | Semantic and wrapper-planning errors | `x2py/semantics/fortran2ir.py`, `x2py/semantics/policy_completion.py`, `x2py/semantics/wrapper_policy.py`, `x2py/wrapper_codegen/planner.py` | `docs/user/guide/error-handling.md` | -| Wrapper policy and lowering | `x2py/semantics/policy_completion.py`, `x2py/semantics/ownership.py`, `x2py/wrapper_codegen/planner.py`, `x2py/wrapper_codegen/generator.py` | `docs/user/guide/fortran-wrapper.md`, ownership docs | +| Wrapper policy and lowering | `x2py/semantics/policy_completion.py`, `x2py/semantics/ownership.py`, `x2py/wrapper_codegen/planner.py`, `x2py/wrapper_codegen/generator.py` | `docs/user/reference/fortran-wrapper.md`, ownership docs | | Native build | `x2py/pipeline/build.py`, `x2py/compiling/compilers.py`, `x2py/compiling/native_support.py` | compiling package README and build-system docs | immutable, and declare as ownership/lifetime policy. It separates editable wrapper policy from native ABI facts and records the failure layers for edited contracts. Evidence: - `docs/user/guide/editing-semantic-pyi-contracts.md`, - `docs/user/guide/fortran-wrapper.md`, and + `docs/user/reference/editing-semantic-pyi-contracts.md`, + `docs/user/reference/fortran-wrapper.md`, and `tests/docs/test_structure.py`. - [x] Edited contracts can remove a class, method, generated constructor, class member, and individual overload candidate from the Python API. They can also @@ -513,7 +513,7 @@ X2PY_C_DOCS_END --> native-order identity calls without `@native_call`, immutable replacement, ownership triples, pointer-policy blockers, runtime constraints, `@raises`, `@hold_gil`, and native-artifact failures. Evidence: - `docs/user/guide/editing-semantic-pyi-contracts.md`, + `docs/user/reference/editing-semantic-pyi-contracts.md`, `tests/wrapper/fortran/edit_pyi_contracts/`, `tests/semantics/policy/`, `tests/wrapper/fortran/runtime_behavior/test_runtime_policy_decorators.py`, diff --git a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md index fe01be9d9..f50ee2518 100644 --- a/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md +++ b/docs/maintainer/roadmap/wrapper-plan-migration-checklist.md @@ -1366,7 +1366,7 @@ validation, allocation, and writeback behavior, while `_build_string_storage_argument()`, `_convert_raw_string_argument()`, and `_convert_string_result()` define the bridge representation. The public contract and observable oracle are -`docs/user/guide/fortran-wrapper.md`, `docs/user/guide/data-types.md`, +`docs/user/reference/fortran-wrapper.md`, `docs/user/guide/data-types.md`, `docs/user/reference/semantic-pyi-format.md`, `tests/wrapper/fortran/strings/test_character_arguments.py`, and `tests/wrapper/fortran/strings/test_character_edge_cases.py`. Direct-plan @@ -1726,7 +1726,7 @@ with the native-handle caller contract recorded as their sole Phase 7 blocker; the direct route is forced only by the internal parity harness. The public behavior is defined by the NumPy array contract in -`docs/user/guide/fortran-wrapper.md`, the array spelling and metadata rules in +`docs/user/reference/fortran-wrapper.md`, the array spelling and metadata rules in `docs/user/reference/semantic-pyi-format.md`, and the existing array wrapper tests. The legacy binding validates exact dtype, rank, every expressible extent, native byte order, alignment, layout/stride requirements, and diff --git a/docs/old_docs/fortran_parser.md b/docs/old_docs/fortran_parser.md index 5bb29ef2b..bd0ca2d00 100644 --- a/docs/old_docs/fortran_parser.md +++ b/docs/old_docs/fortran_parser.md @@ -594,8 +594,6 @@ error was raised internally: python -m x2py bad.f90 --debug ``` -`--debug-traceback` remains accepted as a compatibility alias. - The same developer mode can be enabled with the environment variable `FORTRAN_PARSER_DEBUG=1`: @@ -793,8 +791,7 @@ disable ANSI output. On Windows, ANSI console compatibility is enabled through For parser development, `format_diagnostic(debug=True)` appends a note with the internal parser file, line, and function that raised the error. The CLI exposes -this through `--debug`, its compatibility alias `--debug-traceback`, or -`FORTRAN_PARSER_DEBUG=1`; normal CLI parse errors intentionally hide Python +this through `--debug` or `FORTRAN_PARSER_DEBUG=1`; normal CLI parse errors intentionally hide Python tracebacks. The sections below list each error category, the triggering condition, and the diff --git a/docs/stylesheets/site.css b/docs/stylesheets/site.css index a5d3626ff..e88637b3d 100644 --- a/docs/stylesheets/site.css +++ b/docs/stylesheets/site.css @@ -7,6 +7,31 @@ padding-bottom: 0; } +.wy-side-scroll { + width: 100%; + overflow-y: auto; + scrollbar-color: #9b9b9b #343131; + scrollbar-width: thin; +} + +.wy-side-scroll::-webkit-scrollbar { + width: 0.65rem; +} + +.wy-side-scroll::-webkit-scrollbar-track { + background: #343131; +} + +.wy-side-scroll::-webkit-scrollbar-thumb { + background: #9b9b9b; + border: 2px solid #343131; + border-radius: 0.65rem; +} + +.wy-side-scroll::-webkit-scrollbar-thumb:hover { + background: #c2c2c2; +} + .rst-versions { display: none; } diff --git a/docs/user/changelog/index.md b/docs/user/changelog/index.md index 13370029b..8debcaa4f 100644 --- a/docs/user/changelog/index.md +++ b/docs/user/changelog/index.md @@ -2,7 +2,7 @@ title: Changelog audience: users, developers prerequisites: none -related: ../language-support/index.md, ../guide/distribution.md +related: ../language-support/index.md, ../guide/building-shared-library.md status: planned-documentation publication: draft --- diff --git a/docs/user/examples/recipes/build-and-import-cli.md b/docs/user/examples/recipes/build-and-import-cli.md index 02560230d..6ee4ed8e0 100644 --- a/docs/user/examples/recipes/build-and-import-cli.md +++ b/docs/user/examples/recipes/build-and-import-cli.md @@ -2,7 +2,7 @@ title: Build And Import With The CLI audience: users prerequisites: basic wrapper tutorial, supported compiler toolchain -related: ../verified-cookbook.md, ../../guide/fortran-wrapper.md +related: ../verified-cookbook.md, ../../reference/fortran-wrapper.md status: maintained publication: draft --- diff --git a/docs/user/examples/recipes/build-multiple-fortran-sources.md b/docs/user/examples/recipes/build-multiple-fortran-sources.md index 1fa710211..e36505bc6 100644 --- a/docs/user/examples/recipes/build-multiple-fortran-sources.md +++ b/docs/user/examples/recipes/build-multiple-fortran-sources.md @@ -2,7 +2,7 @@ title: Build Multiple Fortran Sources audience: users, developers prerequisites: basic wrapper tutorial, supported compiler toolchain -related: ../verified-cookbook.md, ../../guide/fortran-wrapper.md +related: ../verified-cookbook.md, ../../reference/fortran-wrapper.md status: maintained publication: draft --- diff --git a/docs/user/examples/recipes/generate-editable-makefile.md b/docs/user/examples/recipes/generate-editable-makefile.md index 1177ea0fc..ee3f5194d 100644 --- a/docs/user/examples/recipes/generate-editable-makefile.md +++ b/docs/user/examples/recipes/generate-editable-makefile.md @@ -2,7 +2,7 @@ title: Generate An Editable Makefile audience: users, developers prerequisites: basic wrapper tutorial, GNU Make, supported compiler toolchain -related: ../verified-cookbook.md, ../../guide/fortran-wrapper.md +related: ../verified-cookbook.md, ../../reference/fortran-wrapper.md status: maintained publication: draft --- diff --git a/docs/user/examples/verified-cookbook.md b/docs/user/examples/verified-cookbook.md index 325543669..fe3ef66a6 100644 --- a/docs/user/examples/verified-cookbook.md +++ b/docs/user/examples/verified-cookbook.md @@ -13,10 +13,9 @@ This cookbook is for lookup. Each recipe answers one practical question and uses checked repository fixtures where the command output is stable. Start with the [basic wrapper tutorial](../tutorials/basic-wrapper.md) if this -is your first x2py workflow. Use the -[Fortran wrapper guide](../guide/fortran-wrapper.md) for the full runtime -contract. The Semantic .pyi Format reference gives the complete editable -wrapper contract syntax. +is your first x2py workflow. The Fortran wrapper reference gives the full +runtime contract. The Semantic .pyi Format reference gives the complete +editable wrapper contract syntax. ## Choose A Recipe @@ -61,10 +60,10 @@ lowered to semantic IR and printed as `.pyi`; runtime wrapping of user-supplied C libraries is not implemented yet. X2PY_C_DOCS_END --> -## Related Documentation +## Next - [Basic wrapper tutorial](../tutorials/basic-wrapper.md) -- [Fortran wrapper guide](../guide/fortran-wrapper.md) +- [Fortran wrapper reference](../reference/fortran-wrapper.md) - Semantic .pyi Format - Semantic IR Reference - Diagnostic Codes diff --git a/docs/user/getting-started/beginner-workflow.md b/docs/user/getting-started/beginner-workflow.md index ac081a31a..92c7c54de 100644 --- a/docs/user/getting-started/beginner-workflow.md +++ b/docs/user/getting-started/beginner-workflow.md @@ -102,7 +102,8 @@ Only do this after you are comfortable with the basic workflow: python3 -m x2py generate --pyi src/scale.f90 --out contracts ``` -Editing contracts is powerful but adds complexity. See **Editing Semantic .pyi Contracts** in the User Guide when you're ready. +Editing contracts is powerful but adds complexity. See +**Editing Semantic .pyi Contracts** in Reference when you're ready. --- diff --git a/docs/user/guide/allocatables.md b/docs/user/guide/allocatables.md index cb6880c9a..4c6ddb130 100644 --- a/docs/user/guide/allocatables.md +++ b/docs/user/guide/allocatables.md @@ -2,7 +2,7 @@ title: Allocatables description: How x2py handles Fortran `allocatable` variables, arrays, and descriptors audience: users, advanced users -prerequisites: arrays +prerequisites: arrays, memory management related: arrays.md, pointers.md, memory-management.md status: maintained publication: reviewed @@ -23,10 +23,10 @@ while the owner of the Python handle depends on where that handle came from. - `allocated` reports whether storage exists; `to_numpy()` returns a borrowed live view of that storage. - Reallocation or deallocation invalidates existing views. -- Module and derived-field handles borrow their native owner. Function results - and caller-created handles own persistent descriptor storage. -- `deallocate()` releases the current allocation but keeps the handle open; - `close()` permanently ends an owned handle. +- Module and derived-field handles borrow their native owner. Returned and + caller-created handles own persistent descriptor storage. +- When available, `deallocate()` releases the current allocation but keeps the + handle open. `close()` permanently ends an owned handle. --- @@ -57,8 +57,8 @@ the current array data held by an allocatable handle. ## Allocatable Array Handle API -`Allocatable[T[...]]` is the semantic contract spelling. Generated Python APIs -return an `AllocatableArray`. You can also create an unallocated handle when a +`Allocatable[T[...]]` is the type annotation. At runtime, generated Python APIs +use an `AllocatableArray`. You can also create an unallocated handle when a routine needs a present descriptor that it will allocate: ```python @@ -71,16 +71,12 @@ api.fill_values(values) assert values.allocated is True ``` -The annotation supplies the element dtype and rank. The handle acquires -compiler-compatible descriptor storage when first passed to a writable -matching wrapper argument. It stays the same Python object after the call. +The annotation supplies the element dtype and rank. The handle creates its +native descriptor storage when first passed to a matching writable argument. +It stays the same Python object after the call. `Allocatable[Float64]()` is not supported because scalar allocatables cross the Python boundary as values rather than array handles. -The same handle may be used by separately built x2py modules when their native -handle and Fortran compiler/runtime ABIs are compatible. See -[Sharing Descriptor Handles Between Modules](data-types.md#sharing-descriptor-handles-between-modules). - A returned or attribute array handle remains present even when its descriptor is unallocated. Reading the Python attribute returns an `Allocatable[T[...]]` handle, not `ndarray | None`. @@ -104,8 +100,8 @@ else: | `dtype` | `numpy.dtype` | Declared array element type. | | `rank` | `int` | Declared number of dimensions. | | `to_numpy()` | `numpy.ndarray \| None` | A live view of current storage, or `None` when unallocated. It never creates an automatic detached snapshot. | -| `deallocate()` | `() -> None` | Deallocates current storage when the generated contract permits this operation. | -| `resize(shape)` | `(int \| Sequence[int]) -> None` | Allocates or resizes storage to `shape` when the generated contract permits this operation. | +| `deallocate()` | `() -> None` | Deallocates current storage when this operation is available for the handle. | +| `resize(shape)` | `(int \| Sequence[int]) -> None` | Allocates or resizes storage to `shape` when this operation is available for the handle. | | `close()` | `() -> None` | Permanently releases an owned descriptor and any remaining allocation. It does nothing on a borrowed handle. | | `closed` | `bool` | Whether an owned handle has been closed. | @@ -126,7 +122,7 @@ when deterministic release matters, such as after using a large allocation. Module and field handles are borrowed. Calling `close()` on one is a no-op: it leaves the handle and its owner's storage unchanged. `deallocate()` changes the -owner's allocation when the generated contract permits it. +owner's allocation when that operation is available. --- @@ -289,7 +285,7 @@ h.resize(8) After `resize()`, `deallocate()`, or a native call that may reallocate the descriptor, discard `view` and call `to_numpy()` again. The independent -Python-owned `saved` copy remains safe. +`saved` copy remains safe. ### Do Not Keep Using A Closed Result @@ -304,17 +300,15 @@ A view normally retains its handle, but an explicit `close()` releases an owned allocatable result immediately. Finish using or copy all views before closing the handle. -### Respect Ownership And Operation Permissions +### Release Only Through The Owner ```python -h.deallocate() # NOT OK when the generated contract does not permit it +h.deallocate() # may be unavailable when h only observes native-owned storage ``` -Allocation, deallocation, and resizing are available only when completed -policy permits them. A module or field handle can borrow storage controlled by -its native owner; the presence of an allocation does not by itself grant -Python permission to release it. Only a permitted descriptor operation or the -native owner's API may deallocate that storage. +Not every borrowed handle lets Python resize or deallocate its owner's storage. +An unavailable operation raises `NotImplementedError`. Use the native owner's +functions to change that storage instead. --- @@ -325,9 +319,14 @@ than `AllocatableArray` handles. An unallocated projected scalar result becomes `None`. Scalar values do not expose persistent allocation state, `to_numpy()`, or descriptor operations. +For an optional scalar allocatable argument, omission makes the argument absent. +Passing `None` makes it present but unallocated, while passing a value makes it +present with that value. See [Optional Arguments](optional-arguments.md). + --- ## Next -- Continue with [Memory Management](memory-management.md) for ownership, - lifetime, and cleanup rules. +- Review [Memory Management](memory-management.md) for the ownership and live + view rules shared by all native storage. +- Continue with [Pointers](pointers.md) for association and target lifetime. diff --git a/docs/user/guide/building-shared-library.md b/docs/user/guide/building-shared-library.md new file mode 100644 index 000000000..2f4cdb90c --- /dev/null +++ b/docs/user/guide/building-shared-library.md @@ -0,0 +1,74 @@ +--- +title: Building the Shared Library +description: How to build and import a Python extension shared library with x2py +audience: users +prerequisites: common beginner workflow +related: ../reference/cli-commands.md, ../examples/recipes/build-and-import-cli.md, ../examples/recipes/build-multiple-fortran-sources.md, ../examples/recipes/generate-editable-makefile.md +status: maintained +publication: reviewed +--- + +# Building the Shared Library + +x2py turns Fortran source into a Python extension module. The final module is a +native shared library that Python imports directly. + +## Build + +Run x2py on the source file and choose a build directory: + +```bash +python3 -m x2py src/scale.f90 --out-dir build/scale +``` + +The shared library and the generated build files are written to +`build/scale`. By default, the module name comes from the source filename. Use +`--out` to choose it explicitly: + +```bash +python3 -m x2py src/scale.f90 --out scale_api --out-dir build/scale_api +``` + +## Import + +Add the build directory to Python's search path, then import the module by its +name: + +```python +import sys + +sys.path.insert(0, "build/scale_api") + +import scale_api +``` + +The shared-library filename includes a platform- and Python-specific suffix, +but the import uses only the module name. + +## Multiple Source Files + +Pass source files in the order required by the compiler. Choosing the module +name explicitly keeps the result clear: + +```bash +python3 -m x2py src/types.f90 src/solver.f90 \ + --out solver \ + --out-dir build/solver +``` + +x2py preserves the given order. It does not discover source dependencies or +external libraries automatically. + +## Compatibility + +The shared library is not universal. It must match the target machine's +operating system and architecture, Python and NumPy, and required compiler +libraries. Rebuilding it on the target machine is the safest choice. + +## Next + +- Follow the complete [build and import recipe](../examples/recipes/build-and-import-cli.md). +- For several files, see the [multiple-source recipe](../examples/recipes/build-multiple-fortran-sources.md). +- To inspect or customize the build, [generate an editable Makefile](../examples/recipes/generate-editable-makefile.md). +- For build or import failures, see [Troubleshooting](../troubleshooting/index.md) + and rerun the build with `--verbose`. diff --git a/docs/user/guide/distribution.md b/docs/user/guide/distribution.md deleted file mode 100644 index a382ff24d..000000000 --- a/docs/user/guide/distribution.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Distribution -description: How to share x2py-based extensions with other users and environments -audience: users, packagers -prerequisites: packaging -related: packaging.md, ../troubleshooting/platform-specific-issues.md, ../getting-started/installation.md -status: maintained -publication: reviewed ---- - -# Distribution - -x2py currently produces **platform-specific native extensions**. There is no stable, universal wheel format yet. The most reliable way to distribute your code is to share the **source + build recipe**. - ---- - -## Recommended Distribution Approach: Source + Build Instructions - -Distribute your project like this: - -``` -my-project/ -├── src/ -│ └── mycode.f90 -├── tests/ -│ └── test_mycode.py -├── BUILDING.md # ← Important! -├── requirements.txt -└── README.md -``` - -**`BUILDING.md`** should clearly document: - -```bash -# Build the extension -python3 -m x2py src/mycode.f90 --out-dir build/mycode - -# Run tests -python3 -m pytest tests/ -``` - -Include: -- Required Python and NumPy versions -- Compiler (e.g. gfortran) and version -- Any extra compiler flags or libraries -- Supported platforms - ---- - -## Sharing Prebuilt Extensions - -You **can** share a built extension, but it is **highly platform-specific**. - -Consumers must match: -- Operating system and architecture -- Python version and implementation -- NumPy ABI -- Compiler ABI and runtime libraries - -Even small differences (e.g. different Python patch version or NumPy build) can cause import or runtime failures. - -**Best practice**: Always include the source and build instructions even if you also provide a prebuilt binary. - ---- - -## Key Limitations - -- No official wheel-building support yet -- No automatic bundling of native dependencies -- No cross-platform guarantees -- Extensions are tied to the exact Python + NumPy + compiler combination used to build them - ---- - -## Release Checklist - -Before releasing: -1. Generate and review the semantic `.pyi` contract -2. Build from a clean directory -3. Run all tests with realistic inputs -4. Document supported platforms and dependencies clearly -5. Test on target environments when possible - ---- - -## Next - -- See [Platform-Specific Issues](../troubleshooting/platform-specific-issues.md) for common distribution problems. diff --git a/docs/user/guide/error-handling.md b/docs/user/guide/error-handling.md index 5fd3d88ca..6a5ac4ff2 100644 --- a/docs/user/guide/error-handling.md +++ b/docs/user/guide/error-handling.md @@ -29,6 +29,21 @@ x2py reports failures at several distinct stages. Understanding which stage fail --- +## Verbose Output And Tracebacks + +Use the two diagnostic flags for different problems: + +| Flag | Use it when | +| --- | --- | +| `--verbose` | A build or link fails and you need the generated files, build steps, timings, or compiler commands. | +| `--debug` | x2py fails unexpectedly and you need the full Python traceback. | + +`--verbose` keeps the normal concise error message. `--debug` exposes x2py's +internal call stack, so it is mainly useful when reporting or investigating an +x2py bug. + +--- + ## Status Projection Example You can turn Fortran status codes into Python exceptions using the `@raises` decorator in an edited contract. @@ -57,7 +72,7 @@ Then: try: api.solve(np.int32(-1)) except RuntimeError as e: - print(e) # "negative input" + print(e) ``` --- @@ -75,6 +90,8 @@ except RuntimeError as e: - Always start with the **full error message** — it usually tells you exactly what went wrong. - Use `--verbose` when investigating build failures. +- Use `--debug` only when an unexpected x2py failure requires a Python + traceback. - For complex contracts, generate the `.pyi` first and inspect it. - Run risky or untrusted callbacks in a subprocess if you need the main process to survive failures. @@ -82,5 +99,5 @@ except RuntimeError as e: ## Next -- [Editing Semantic `.pyi` Contracts](editing-semantic-pyi-contracts.md) +- [Editing Semantic `.pyi` Contracts](../reference/editing-semantic-pyi-contracts.md) - Check the [Diagnostic Codes](../reference/diagnostic-codes.md) reference for detailed error categories diff --git a/docs/user/guide/index.md b/docs/user/guide/index.md index 152709697..d48c095ee 100644 --- a/docs/user/guide/index.md +++ b/docs/user/guide/index.md @@ -3,7 +3,7 @@ title: User Guide description: Detailed guides for wrapping Fortran code with x2py audience: users prerequisites: getting started -related: data-types.md, fortran-wrapper.md, ../language-support/index.md +related: data-types.md, ../reference/fortran-wrapper.md, ../language-support/index.md status: maintained publication: reviewed --- @@ -12,7 +12,7 @@ publication: reviewed This section builds on the [Getting Started](../getting-started/index.md) workflow. It explains data type mapping, supported Fortran constructs, runtime -behavior, packaging, and distribution. +behavior, and how to build the shared library. --- @@ -42,23 +42,20 @@ behavior, packaging, and distribution. - [Callbacks](callbacks.md) - [Enumerations](enumerations.md) +- [Raw Addresses](raw-addresses.md) — Advanced primitive, array, and fixed-string address boundaries - [Error Handling](error-handling.md) --- -## Build and Deployment +## Building -- [Packaging](packaging.md) -- [Distribution](distribution.md) +- [Building the Shared Library](building-shared-library.md) --- -## Contract & Reference +## Reference -- **[Fortran Wrapper Guide](fortran-wrapper.md)** — Complete reference for the generated runtime surface -- [Editing Semantic `.pyi` Contracts](editing-semantic-pyi-contracts.md) — When and how to customize contracts -- [Raw Addresses](raw-addresses.md) — Advanced primitive, array, and fixed-string address boundaries -- [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) — Full annotation reference +- [Reference Overview](../reference/index.md) — CLI, Python API, wrapper behavior, and semantic contracts - [Language Feature Matrix](../language-support/feature-matrix.md) — Current support status (supported / partial / unsupported) --- diff --git a/docs/user/guide/memory-management.md b/docs/user/guide/memory-management.md index fcbc8dfd9..34383e9fa 100644 --- a/docs/user/guide/memory-management.md +++ b/docs/user/guide/memory-management.md @@ -1,311 +1,207 @@ --- title: Memory Management -description: Clear ownership rules, views vs copies, lifetimes, and destruction responsibility in x2py +description: Ownership, live views, copies, and safe cleanup in x2py audience: users, advanced users -prerequisites: arrays, wrapping derived types, allocatables, pointers -related: allocatables.md, pointers.md, editing-semantic-pyi-contracts.md +prerequisites: arrays +related: allocatables.md, pointers.md, wrapping-derived-types.md status: maintained publication: reviewed --- # Memory Management -x2py lets Python work directly with native Fortran storage when that is safe. -That gives you fast wrappers without surprise copies, but it also means -ownership has to be explicit. +x2py can give Python direct access to storage created by Fortran. This avoids +unnecessary copies, but Python must not use that storage after its owner +releases or replaces it. -The guiding question is: +Two questions keep these cases simple: -> Who is allowed to release this storage, and how long is the Python object allowed to use it? +1. Who owns the Python object? +2. Who owns the storage behind it? -x2py answers that question **before** wrapper code is generated. The generated -Python extension then follows the completed policy exactly, so ownership is a -documented contract rather than a runtime guess. +The answers are not always the same. ---- - -## The Short Version - -Most memory-management questions in x2py reduce to three cases: - -| Situation | What to expect | -| --- | --- | -| Python receives a normal value or NumPy array result | Python owns an independent value. | -| Python passes an object into a wrapped call | The same object remains with the caller; native code may mutate through it only when the contract allows it. | -| Python receives a handle, wrapper object, component, or view into native state | The Python object may be live, borrowed, or wrapper-owned; check the ownership kind before keeping views across native calls. | - -!!! tip "A reliable habit" - Before keeping a view or passing an object back into native code, ask two - separate questions: who owns the Python object, and who owns the storage - behind it? - ---- - -## Two Ownership Layers +## Key Concepts -Ownership can describe two different things: - -| Layer | Question | Example | -| --- | --- | --- | -| **Boundary object** | Who supplied and keeps the Python object used in this call? | A NumPy array, handle, or wrapper instance passed as an argument | -| **Target storage** | Who owns the native allocation that object exposes or points at? | Python array data, Fortran module storage, or a wrapper-owned native instance | - -Those layers can have different owners. A caller-owned handle can point at a -native-owned module allocation. A caller-owned child wrapper can borrow storage -owned by its parent wrapper. There is still exactly one owner for each real -allocation. +- Ordinary Python values and independently created NumPy arrays have + Python-managed storage. +- An [allocatable](allocatables.md) or [pointer](pointers.md) handle is a Python + object that describes native array storage. Owning the handle does not always + mean owning the storage it refers to. +- Calling `to_numpy()` on an allocatable or pointer handle returns a live NumPy + view, not a copy. +- A live view can become invalid if native code reallocates, deallocates, or + changes the storage it refers to. +- Passing an object to a wrapped function does not transfer its ownership. +- Release storage only through the object or native API that owns it. --- -## Ownership Terms - -| Term | Plain meaning | Typical example | -| --- | --- | --- | -| **Python-owned** | Python owns an independent value or array. | Function array result, `view.copy()` | -| **Caller-owned** | Your Python code supplied the boundary object and keeps it after the call. | Writable NumPy array, native-backed handle, wrapper instance | -| **Wrapper-owned** | A generated x2py Python object owns native storage. | Derived-type result, owned allocatable result handle | -| **Native-owned** | Fortran module state or another native owner controls release. | Module variable, module allocatable | -| **Borrowed view** | Python sees storage owned by someone else. | `handle.to_numpy()`, nested component | -| **Call-local** | x2py creates temporary storage for one native call only. | Scalar address slot, fixed string buffer | - -### Python-owned +## The Python Object And Its Storage -Python-owned values are ordinary Python or NumPy objects with independent -lifetime. If native storage changes later, a Python-owned copy does not change. +For example, an allocatable or pointer handle can refer to storage owned +somewhere else: ```python +handle = api.values view = handle.to_numpy() -copy = None if view is None else view.copy() ``` -Use this when the data must survive native reallocation, deallocation, or -reassociation. - -### Caller-owned +Python owns the `handle` and `view` objects. The storage visible through +`view`, however, may belong to a Fortran module, a generated result handle, or +another native object. -"Caller" means your Python code: the code that calls the wrapped function. This -label says x2py must preserve the Python object you supplied. It does not always -say who owns every allocation reachable through that object. +Common cases are: -For an ordinary NumPy array, the boundary object and the target storage are both -Python-owned: +| Value seen by Python | Who owns the storage? | +| --- | --- | +| Ordinary Python value or independently created NumPy array | Python | +| `view.copy()` | Python | +| Fortran module variable | The Fortran module | +| Derived-type object constructed or returned by x2py | Its generated Python wrapper | +| Derived-type field that exposes native storage | Usually its parent object | +| Allocatable or pointer handle | Depends on where the handle came from and how its current storage was created | -```python -values = np.ones(4, dtype=np.float64) -api.scale(values) +There must be one clear owner for every allocation. Other objects may view or +refer to that allocation, but they must not release it. -# Same array object, possibly mutated in place. -print(values[0]) # 2.0 -``` +--- -The wrapper may mutate through a caller-owned object when the contract allows -it, but it must not free, reallocate, or secretly replace that object. +## Live Views And Copies -For a native-backed handle or borrowed wrapper, the caller owns the Python -object it passes, but native code or a parent wrapper may still own the target -storage behind it: +Calling `handle.to_numpy()` gives direct access to the handle's current native +storage: ```python -handle = api.values # Python holds the handle object. -view = handle.to_numpy() # Native module storage may be behind the view. -api.update_values(handle) # The call keeps using the same handle object. +view = handle.to_numpy() +if view is not None: + view[0] = 42.0 # changes the native storage ``` -!!! note "Ownership is not transferred" - Passing an object to native code does not transfer the owner of that object - or its target storage. Mutation is allowed only when the completed contract - says that specific object can be written through. +This is fast because no data is copied. It also means that the view is safe +only while the same native storage remains alive. -### Wrapper-owned - -A generated Python class is an extension class produced by x2py and imported -from the generated module. You construct and pass it like a normal Python -object, but internally it controls one native instance. - -For Fortran users, wrapper-owned is mostly visible with supported -`type :: ...` derived types: +Copy the data when it must survive a later native change: ```python -point = geometry.points.point(x=np.float64(1.0), y=np.float64(2.0)) -made = geometry.points.make_point(np.float64(3.0), np.float64(4.0)) -``` - -Here `point` and `made` are Python objects whose generated x2py deallocator is -responsible for finalizing and releasing their native instances exactly once. - -Owned allocatable array results are another wrapper-owned case: their generated -handle owns persistent descriptor storage and releases it on `close()` or -finalization. Module allocatable handles are different because the Fortran -module still owns their target allocation. - -Ordinary NumPy arrays, module variables, and borrowed components are not -wrapper-owned. - -Nested derived-type components are different: `container.origin` may be a -borrowed child wrapper. It gives Python a convenient object for the component, -but the parent object still owns the native storage. - -### Native-owned - -Native-owned storage belongs to Fortran module state or another native owner. -Python can read it, write it through supported setters, or view it through a -handle, but Python is not responsible for releasing it. - -In short, plain and `Aliased` derived module variables remain live native-owned objects. -An `Aliased` module object may use an address-backed borrow; a plain module -object uses generated module-specific access. In both cases, the owning module -state decides when the native storage is valid. - -### Borrowed views - -A borrowed view is a Python object that points at storage owned somewhere else. -The view itself may keep the parent wrapper or module object alive when x2py can -do so, but it cannot stop native code from reallocating or deallocating the -target storage. - -```python -view = api.values.to_numpy() -snapshot = None if view is None else view.copy() +view = handle.to_numpy() +saved = None if view is None else view.copy() -api.resize_values() +api.replace_values() -# view may now be stale; snapshot remains independent Python-owned data. +# Use saved here. Do not keep using view. ``` -### Call-local - -Call-local storage is temporary workspace created only for one wrapped call. -x2py may create a native scalar slot, a fixed-width string buffer, or a -descriptor adapter, pass it to native code, and release it before returning to -Python. - -If the contract says native changes should be returned to Python, policy -completion must also say how that value is projected back. Otherwise, mutation -of call-local storage is intentionally not a persistent Python-visible update. +Reallocation, deallocation, pointer reassociation, resizing, or explicit +cleanup can make an older view invalid. Get a new view after such an operation. --- -## Handles And Target Storage - -`Allocatable[T[...]]` and `Pointer[T[...]]` handles are control objects. They -tell Python whether native storage is allocated or associated, and they provide -`.to_numpy()` when a live view is available. +## Allocatables And Pointers -Owning a handle is not the same thing as owning the target array. A module -allocatable handle can be a stable Python object while the Fortran module still -owns the allocation behind it. A pointer handle can describe an association -without owning the target at all. - -!!! warning "Views are live, not snapshots" - `handle.to_numpy()` returns a view of current native storage. It never - creates an automatic detached snapshot. If native code may reallocate or - deallocate that storage, copy the view first. - ---- +Allocatable and pointer handles both describe native arrays, but they do not +have the same ownership rules: -## Derived Objects And Native Dummies +| Operation | Allocatable handle | Pointer handle | +| --- | --- | --- | +| Check current state | `allocated` | `associated` | +| View current data | `to_numpy()` | `to_numpy()` | +| Remove current storage | `deallocate()` releases the allocation when the operation is available | `deallocate()` releases only a target allocated through this pointer | +| Stop referring to storage without releasing it | Not applicable | `nullify()` | +| End an owned handle | `close()` releases the descriptor and any remaining allocation | `close()` releases the descriptor, not the target | -Derived objects can come from ordinary wrappers, module state, allocatables, -or pointers. Their origin determines which native dummies they can satisfy. +A pointer association does not by itself make the pointer responsible for the +target. If a target was allocated through a pointer, call `deallocate()` before +`nullify()`, reassociating that pointer, or closing it. Otherwise, the target +may be left without an owner. -### Payload Calls +See [Allocatables](allocatables.md) and [Pointers](pointers.md) for their full +APIs and examples. -These dummies receive a derived value, not its allocation or association -descriptor: +--- -| Python object origin | Ordinary or `target` dummy | `value` dummy | -| --- | --- | --- | -| Wrapper-owned ordinary object | Direct object reference | Typed value copy | -| Native module object | Scoped live reference | Scoped typed value | -| Wrapper-owned allocatable holder | Current payload | Payload value copy | -| Module allocatable | Scoped payload | Scoped payload copy | -| Wrapper-owned pointer holder | Current target | Target value copy | -| Module pointer | Module target | Target value copy | +## Owned And Borrowed Handles -An unallocated allocatable or unassociated pointer has no payload. It cannot -satisfy an ordinary, `target`, or `value` dummy. +Allocatable and pointer handles can be owned or borrowed. Caller-created +handles and function-result handles own their descriptor storage. Their +`close()` method permanently ends the handle: -### Descriptor Calls +```python +result.close() +assert result.closed +``` -These dummies can update allocation or association state: +Do not use an owned allocatable or pointer handle after calling `close()` on +it. Owned handles close automatically when Python finalizes them, so call +`close()` yourself only when the resource must be released immediately. -| Python object origin | `allocatable` dummy | `pointer` dummy | -| --- | --- | --- | -| Wrapper-owned ordinary object | Incompatible | Input-only adapter | -| Native module object | Incompatible | Scoped input-only adapter | -| Wrapper-owned allocatable holder | Persistent holder | Payload input-only adapter | -| Module allocatable | Allocation transaction | Scoped payload input-only adapter | -| Wrapper-owned pointer holder | Incompatible | Persistent pointer holder | -| Module pointer | Incompatible | Association transaction | +Module and derived-field handles are borrowed from their native owner. Calling +`close()` on a borrowed handle does nothing: it does not close the handle or +release the owner's storage. -Descriptor dummies accept empty state so native code can establish it. A -nonpointer object can satisfy a pointer dummy only for `intent(in)`. +The resource released by `close()` depends on the handle: -Module allocation and association transactions are restored before the call -returns. Pointer holders own their association variable, not an unknown target. +- Closing an owned allocatable handle releases its descriptor and any + allocation it still contains. +- Closing an owned pointer handle releases only its descriptor. Its target has + a separate lifetime. --- -## Core Rules +## Passing Objects To Functions -1. **Exactly one owner** is responsible for destroying each allocation. -2. Passing an object to native code does not automatically transfer ownership of - the object or its target storage. -3. **Borrowed views** can become stale if the owner reallocates or deallocates. -4. **Copies** are safe but more expensive. -5. Pointers **do not** imply ownership of their target. -6. A view from `.to_numpy()` is live: changes affect the native storage. +Passing an object to a wrapped function does not give the function ownership of +that object. ---- +- A writable NumPy array remains the same Python array, although the function + may change its elements. +- A writable allocatable handle remains the same handle, although the function + may allocate, deallocate, or replace its current storage. +- A writable pointer handle remains the same handle, although the function may + change its association. -## Views vs Copies +If a call may change native storage, finish using or copy any existing views +before the call. Ask the handle for a new view afterward. -```python -view = handle.to_numpy() # borrowed live view -copy = view.copy() # independent Python-owned copy -``` +--- -Use a **view** when performance matters and you know the owner will keep the -storage valid. Use a **copy** when the data must survive later native -reallocation, deallocation, or reassociation. +## Derived Objects And Fields -| Need | Use | -| --- | --- | -| Fast access to current native storage | `view = handle.to_numpy()` | -| Data that survives native changes | `copy = view.copy()` | -| In-place mutation of Python input | Caller-owned boundary object; target owner depends on that object | -| Native object lifetime managed by Python wrapper | Wrapper-owned generated class | +A generated wrapper for a derived-type object can own a native instance. The +wrapper releases that instance automatically when it is finalized. ---- +Both plain and `Aliased` derived module variables remain live objects. The +Fortran module owns their storage, and Python only accesses it. -## Common Situations +A field returned from that object may refer to storage inside its parent. The +generated field object keeps its parent alive, but it cannot stop native code +from replacing or releasing the field's storage. Such a change can invalidate +existing views. -| Situation | Ownership shape | -| --- | --- | -| Ordinary NumPy array passed to a writable argument | Caller-owned boundary object with Python-owned array storage. | -| Native-backed handle passed into a call | Caller-owned boundary object; target storage can remain native-owned. | -| Function returning an ordinary array | Usually a Python-owned NumPy array. | -| Derived-type constructor or derived-type function result | Wrapper-owned generated class instance. | -| Nested derived-type component | Borrowed child wrapper retained through the parent. | -| Module variable | Native-owned state exposed through a getter, setter, handle, or proxy. | -| `Allocatable[T[...]]` handle | Control object for allocation state and live views; target ownership depends on origin. | -| `Pointer[T[...]]` handle | Control object for association state; the pointer does not own the target. | -| Call-local adapter | Internal temporary storage used only during one wrapped call. | +See [Wrapping Derived Types](wrapping-derived-types.md) for construction, +fields, and function arguments. --- -## Practical Guidelines +## Safety Checklist -- Always call `.copy()` on a view before a native operation that might reallocate storage. -- Do not use `del` as a native deallocation API. -- Check `.allocated` / `.associated` before using handles. -- Review the generated `.pyi` contract to understand ownership. -- When a value crosses a boundary, identify both the boundary object owner and - the target storage owner. +- Check `allocated` on an allocatable handle or `associated` on a pointer handle + before reading through it. +- Treat every result of `to_numpy()` as a live view. +- Copy a view before a call that may replace or release its storage. +- Call `deallocate()` on an allocatable handle only when it may release that + allocation. Call it on a pointer handle only for a target allocated through + that pointer. +- Call `nullify()` on a pointer handle to remove its association without + destroying the target. +- Do not use an owned allocatable or pointer handle after `close()`. +- Synchronize access when another thread may change the same native storage. --- ## Next -- Use the [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) for policy metadata details. -- Review [Callbacks](callbacks.md) and [Error Handling](error-handling.md) when ownership affects callable lifetimes or cleanup after failures. +- Read [Allocatables](allocatables.md) for allocation and resizing. +- Read [Pointers](pointers.md) for association and target lifetime. +- Read [Wrapping Derived Types](wrapping-derived-types.md) for object and field + lifetimes. diff --git a/docs/user/guide/optional-arguments.md b/docs/user/guide/optional-arguments.md index d863d2289..281543242 100644 --- a/docs/user/guide/optional-arguments.md +++ b/docs/user/guide/optional-arguments.md @@ -86,16 +86,26 @@ print(adjust(np.int32(5), offset=np.int32(10))) # 15 (keyword) - An optional argument without `intent` uses the same conservative `intent(inout)` behavior when present. -Scalar allocatable and pointer descriptors are the three-state exception: -omission means absent, `None` means present but unallocated or unassociated, -and a concrete value means present storage. +### Scalar Allocatables And Pointers + +For an optional scalar allocatable or pointer, omission and `None` have +different meanings: + +| Python call | What Fortran receives | +| --- | --- | +| `func()` | The argument is absent: `present(value)` is false. | +| `func(None)` | The argument is present but unallocated or unassociated. | +| `func(value)` | The argument is present with `value`. | + +This is the only scalar optional case where explicit `None` does not mean +absence. The scalar crosses the call as a value, not as a persistent handle. --- ## Optional Outputs -An optional output remains visible in the Python call. This lets the caller -decide whether the native routine receives it. +An optional ordinary output remains visible in the Python call. This lets the +caller decide whether the native routine receives it. Pass writable storage to make `values` present: @@ -129,8 +139,10 @@ For optional ordinary array outputs: those arrays are present or absent. Optional scalar derived-type outputs follow the same in-place rule as arrays. -Primitive scalar outputs and native descriptor outputs have different storage -rules. Check the generated `.pyi` contract when mixing output kinds. +For an optional scalar allocatable or pointer output, omit the argument to make +it absent. Pass `None` to make it present without an initial allocation or +association. If its updated value is returned, Python receives a scalar or +`None`, not a handle. --- diff --git a/docs/user/guide/packaging.md b/docs/user/guide/packaging.md deleted file mode 100644 index d952998d3..000000000 --- a/docs/user/guide/packaging.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: Packaging -description: How to package and distribute x2py-based extensions in your projects -audience: users, packagers -prerequisites: common beginner workflow -related: distribution.md, ../reference/cli-commands.md, ../tutorials/packaging.md -status: maintained -publication: reviewed ---- - -# Packaging - -x2py currently focuses on **building importable native extensions**. It does not yet provide a full wheel-building backend or project template. The recommended approach is a clean local project workflow. - ---- - -## Recommended Project Layout - -``` -my-project/ -├── src/ # Fortran source -│ └── scale.f90 -├── build/ # Generated (do not commit) -├── tests/ # Python tests -│ └── test_scale.py -└── pyproject.toml # (optional) -``` - ---- - -## Basic Workflow - -1. **Build the extension** - -```bash -python3 -m x2py src/scale.f90 --out-dir build/scale -``` - -2. **Test it** - -Create `tests/test_scale.py`: - -```python -import sys -import numpy as np - -sys.path.insert(0, "build/scale") -import scale - -def test_scale(): - result = scale.scale(np.float64(3.0), np.float64(2.5)) - assert result == 7.5 - -if __name__ == "__main__": - test_scale() - print("✅ All tests passed") -``` - -Run with: - -```bash -python3 -m pytest tests/ -q -``` - ---- - -## Rebuilding - -When you change source code, compiler flags, or the contract: - -```bash -rm -rf build/scale # Clean previous build -python3 -m x2py src/scale.f90 --out-dir build/scale -``` - ---- - -## Makefile Mode (Advanced) - -For inspectable builds and custom flags: - -```bash -python3 -m x2py generate --makefile src/scale.f90 --out-dir build/scale - -make -f build/scale/Makefile.x2py -``` - ---- - -## Important Notes - -- The extension name is usually taken from the first source file (you can override with `--out`). -- Generated artifacts in `build/` are **not** portable across Python versions, NumPy ABIs, or platforms. -- Keep `src/`, tests, and build commands under version control. -- Do **not** commit the `build/` directory (except in special release processes). - ---- - -## Next - -- See [Distribution](distribution.md) for sharing built extensions -- Check the [Packaging Tutorial](../tutorials/packaging.md) for more advanced setups -- For build or linking problems, see [Troubleshooting](../troubleshooting/index.md) and rerun with `--verbose`. diff --git a/docs/user/guide/pointers.md b/docs/user/guide/pointers.md index aee56c37d..9cb4e4445 100644 --- a/docs/user/guide/pointers.md +++ b/docs/user/guide/pointers.md @@ -58,9 +58,9 @@ not carry a native pointer descriptor. ## Pointer Array Handle API -`Pointer[T[...]]` is the semantic contract spelling. Generated Python APIs -return a `PointerArray`. You can also create an unassociated handle when a -routine needs a present pointer descriptor that it will associate: +`Pointer[T[...]]` is the type annotation. At runtime, generated Python APIs use +a `PointerArray`. You can also create an unassociated handle when a routine +needs a present pointer descriptor that it will associate: ```python import x2py.contracts as xc @@ -72,9 +72,9 @@ api.choose_target(target) assert target.associated is True ``` -The annotation supplies the element dtype and rank. The handle acquires -compiler-compatible descriptor storage when first passed to a writable -matching wrapper argument. It stays the same Python object after the call. +The annotation supplies the element dtype and rank. The handle creates its +native descriptor storage when first passed to a matching writable argument. +It stays the same Python object after the call. `Pointer[Float64]()` is not supported because scalar pointers cross the Python boundary as values rather than array handles. @@ -95,8 +95,7 @@ boundary as values rather than array handles. `associate()` and `nullify()` are available by default. A handle may also support allocation, target deallocation, resizing, and NumPy extraction. -Calling one of these operations when it is unavailable raises -`NotImplementedError`. +An unavailable operation raises `NotImplementedError`. --- @@ -112,8 +111,9 @@ pointers refer to the same target. If `p2` is unassociated, `p1` becomes unassociated. No data is copied. Any previous association of `p1` is removed without destroying its old target. -If `p1` was the only pointer to a target it allocated, call `p1.deallocate()` -before reassociating it to avoid leaking that memory. +If `p1` is responsible for a target created with `p1.allocate()`, call +`p1.deallocate()` before reassociating it. Otherwise, that memory may be left +without a pointer that can release it. --- @@ -187,8 +187,9 @@ api.reassociate_values(p) assert p.associated # the same descriptor was updated in place ``` -For an optional pointer descriptor, omission or `None` makes the native dummy -absent. Passing an unassociated handle makes it present but unassociated. +For an optional pointer descriptor, omission or `None` means the native +argument is absent. Passing an unassociated handle makes the argument present +but unassociated. --- @@ -321,7 +322,7 @@ current = p.to_numpy() Do not use `view` after target deallocation, reassociation, resizing, or reallocation behind the pointer. Extract `current` for the new target. The -independent Python-owned `saved` copy remains safe. +independent `saved` copy remains safe. ### Deallocate Only What This Pointer Allocated @@ -338,9 +339,9 @@ same pointer. If the pointer was only associated with existing storage, use p.nullify() # does not destroy the target ``` -Do not use `nullify()` for a target created with `p.allocate()`. The memory -remains allocated and leaks if no other pointer refers to it. Use -`p.deallocate()` instead. +Do not use `nullify()`, `associate()`, or `close()` while `p` is responsible +for an allocated target. The target remains allocated and may become +unreachable. Use `p.deallocate()` first. Use `resize()` only in the same cases where `deallocate()` is valid. @@ -407,9 +408,15 @@ Scalar pointers appear as `T | None` values at the Python boundary rather than Scalar values do not expose persistent association, `to_numpy()`, or pointer descriptor operations. +For an optional scalar pointer argument, omission makes the argument absent. +Passing `None` makes it present but unassociated, while passing a value makes it +present with that value. See [Optional Arguments](optional-arguments.md). + --- ## Next -- Continue with [Memory Management](memory-management.md) for owner and lifetime - rules shared by arrays, pointers, allocatables, and derived objects. +- Review [Memory Management](memory-management.md) for the ownership and live + view rules shared by all native storage. +- Compare [Allocatables](allocatables.md) when the native object owns an + allocation rather than a pointer association. diff --git a/docs/user/guide/raw-addresses.md b/docs/user/guide/raw-addresses.md index d394a721f..f311e6900 100644 --- a/docs/user/guide/raw-addresses.md +++ b/docs/user/guide/raw-addresses.md @@ -3,7 +3,7 @@ title: Raw Addresses description: Pass primitive, array, and fixed-string storage addresses through semantic contracts audience: advanced users prerequisites: arrays, strings, editing semantic pyi contracts -related: data-types.md, arrays.md, strings.md, editing-semantic-pyi-contracts.md, ../reference/semantic-pyi-format.md +related: data-types.md, arrays.md, strings.md, ../reference/editing-semantic-pyi-contracts.md, ../reference/semantic-pyi-format.md status: maintained publication: reviewed --- @@ -189,6 +189,6 @@ addresses. Do not wrap their `Arg(i)` projection in another `Addr(...)`. ## Next - [Strings](strings.md) for immutable and checked mutable character boundaries -- [Editing Semantic `.pyi` Contracts](editing-semantic-pyi-contracts.md) +- [Editing Semantic `.pyi` Contracts](../reference/editing-semantic-pyi-contracts.md) - [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) for complete `Addr(...)` validation rules diff --git a/docs/user/guide/wrapping-derived-types.md b/docs/user/guide/wrapping-derived-types.md index f7488e3e7..2896e1125 100644 --- a/docs/user/guide/wrapping-derived-types.md +++ b/docs/user/guide/wrapping-derived-types.md @@ -3,7 +3,7 @@ title: Wrapping Derived Types description: How x2py wraps Fortran derived types as Python classes with methods, fields, constructors, and ownership rules audience: users, advanced users prerequisites: wrapping modules, data types -related: memory-management.md, generic-interfaces.md, fortran-wrapper.md +related: memory-management.md, generic-interfaces.md, ../reference/fortran-wrapper.md status: maintained publication: reviewed --- diff --git a/docs/user/guide/wrapping-functions.md b/docs/user/guide/wrapping-functions.md index 8adf957dd..bdb83af31 100644 --- a/docs/user/guide/wrapping-functions.md +++ b/docs/user/guide/wrapping-functions.md @@ -3,7 +3,7 @@ title: Wrapping Functions description: How x2py wraps Fortran `function` procedures — return values, output arguments, arrays, and contracts audience: users prerequisites: data types, first wrapped function -related: wrapping-subroutines.md, arrays.md, fortran-wrapper.md +related: wrapping-subroutines.md, arrays.md, ../reference/fortran-wrapper.md status: maintained publication: reviewed --- diff --git a/docs/user/guide/wrapping-modules.md b/docs/user/guide/wrapping-modules.md index 9958f6a09..8d28bfd0e 100644 --- a/docs/user/guide/wrapping-modules.md +++ b/docs/user/guide/wrapping-modules.md @@ -3,7 +3,7 @@ title: Wrapping Modules description: How x2py exposes Fortran modules as Python namespaces with procedures, variables, and state audience: users prerequisites: data types, first wrapped module -related: wrapping-functions.md, memory-management.md, packaging.md +related: wrapping-functions.md, memory-management.md, building-shared-library.md status: maintained publication: reviewed --- @@ -87,6 +87,6 @@ print(mod.nmax) # 12 (read-only parameter) - Learn about [Memory Management](memory-management.md) — especially important when working with module state - See [Optional Arguments](optional-arguments.md) -- Explore [Packaging](packaging.md) for distribution +- See [Building the Shared Library](building-shared-library.md) - Check the [Language Feature Matrix](../language-support/feature-matrix.md) for supported module features and limitations diff --git a/docs/user/language-support/feature-matrix.md b/docs/user/language-support/feature-matrix.md index 80780532a..ae56a58b3 100644 --- a/docs/user/language-support/feature-matrix.md +++ b/docs/user/language-support/feature-matrix.md @@ -10,7 +10,7 @@ publication: draft # Language Feature Matrix This matrix is the user-facing support index for native-language features. It -does not replace the detailed [Fortran wrapper guide](../guide/fortran-wrapper.md); +does not replace the detailed [Fortran wrapper reference](../reference/fortran-wrapper.md); it points each feature to the owning docs, implementation route, evidence, and limitations. @@ -49,16 +49,16 @@ inspection-only or partial support. | Fortran enum constants | Supported | [Enumerations](../guide/enumerations.md) | [Semantic constants route](../../developer/source-map.md#common-change-routes) | [Enum tests](../../../tests/wrapper/fortran/scalars/test_fortran_enums.py) | No Python `Enum` or `IntEnum` classes are generated. | | Scalar character arguments, results, and fields | Supported | [Strings](../guide/strings.md) | [Character bridge route](../../developer/source-map.md#common-change-routes) | [Character argument tests](../../../tests/wrapper/fortran/strings/test_character_arguments.py), [edge-case tests](../../../tests/wrapper/fortran/strings/test_character_edge_cases.py) | Character arrays use fixed-width NumPy bytes dtype; mutable scalar deferred-length storage is blocked. | | Scalar kind coverage | Supported | [Data types](../guide/data-types.md) | [Fortran type probe](../../developer/source-map.md#hotspot-index) | [Scalar kind tests](../../../tests/wrapper/fortran/scalars/test_scalar_kinds.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 | [Packaging](../guide/packaging.md), [multi-source recipe](../examples/recipes/build-multiple-fortran-sources.md) | [Wrapper orchestration](../../developer/source-map.md#common-change-routes) | [Multi-source tests](../../../tests/wrapper/fortran/multiple_files/test_multi_source_builds.py), [compiler verbose tests](../../../tests/wrapper/fortran/build_from_source/test_compiler_verbose.py) | x2py does not discover, reorder, or resolve all external source dependencies. | -| Visibility, naming, keyword escaping, and collision policy | Supported | [Visibility and naming](../guide/fortran-wrapper.md#visibility-naming-and-the-python-surface) | [Naming policy](../../developer/source-map.md#hotspot-index) | [Visibility/naming tests](../../../tests/wrapper/fortran/naming/test_visibility_naming.py) | Strict mode rejects names that default mode can normalize. | +| Caller-ordered multi-source builds, Makefiles, verbose mode, and output placement | Supported | [Building the shared library](../guide/building-shared-library.md), [multi-source recipe](../examples/recipes/build-multiple-fortran-sources.md) | [Wrapper orchestration](../../developer/source-map.md#common-change-routes) | [Multi-source tests](../../../tests/wrapper/fortran/multiple_files/test_multi_source_builds.py), [compiler verbose tests](../../../tests/wrapper/fortran/build_from_source/test_compiler_verbose.py) | x2py 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/wrapper/fortran/naming/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/wrapper_codegen/test_phase10_callbacks.py), [scalar callback tests](../../../tests/wrapper/fortran/callbacks/test_scalar_callbacks.py), [array callback tests](../../../tests/wrapper/fortran/callbacks/test_array_callbacks.py), [derived callback tests](../../../tests/wrapper/fortran/callbacks/test_derived_callbacks.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) | [Runtime policy tests](../../../tests/wrapper/fortran/runtime_behavior/test_runtime_policies.py), [recursion tests](../../../tests/wrapper/fortran/runtime_behavior/test_runtime_recursion.py), [OpenMP tests](../../../tests/wrapper/fortran/runtime_behavior/test_openmp_runtime.py), [ABI tests](../../../tests/wrapper/fortran/build_from_source/test_runtime_abi.py) | OpenMP and ABI evidence is compiler/platform-specific; callers still own native synchronization. | -| Fortran source wrapper builds | Supported | [Packaging](../guide/packaging.md), [CLI recipe](../examples/recipes/build-and-import-cli.md) | [Wrapper orchestration](../../developer/source-map.md#common-change-routes) | [Build modes](../../../tests/wrapper/fortran/build_from_source/test_build_modes.py), [runtime ABI](../../../tests/wrapper/fortran/build_from_source/test_runtime_abi.py) | Implemented for ordered Fortran source inputs. | +| Fortran source wrapper builds | Supported | [Building the shared library](../guide/building-shared-library.md), [CLI recipe](../examples/recipes/build-and-import-cli.md) | [Wrapper orchestration](../../developer/source-map.md#common-change-routes) | [Build modes](../../../tests/wrapper/fortran/build_from_source/test_build_modes.py), [runtime ABI](../../../tests/wrapper/fortran/build_from_source/test_runtime_abi.py) | Implemented for ordered Fortran source inputs. | ## Supported Inspection Features @@ -73,7 +73,7 @@ X2PY_C_DOCS_END --> ## Unsupported Or Blocked Forms @@ -82,7 +82,7 @@ X2PY_C_DOCS_END --> | --- | --- | --- | --- | --- | --- | | Pointer-array results and unproved reassociation | Unsupported | [Pointer limitations](../guide/pointers.md#unsupported-forms) | [Ownership policy](../../developer/source-map.md#common-change-routes) | [Pointer tests](../../../tests/wrapper/fortran/derived_types/test_pointers.py) | Result handles need stable owner storage and target lifetime; reassociation and ownership-changing operations need explicit completed policy. | | Persistent callbacks and procedure pointers | Unsupported | [Callback limitations](../guide/callbacks.md#unsupported-forms) | [Callback route](../../developer/source-map.md#common-change-routes) | [Callback tests](../../../tests/wrapper/fortran/callbacks/test_scalar_callbacks.py) | Callbacks are valid only during the wrapped call. | -| Advanced multi-source dependency discovery and external-library integration | Unsupported | [Packaging limits](../guide/packaging.md#limitations) | [Build orchestration](../../developer/source-map.md#common-change-routes) | [Multi-source tests](../../../tests/wrapper/fortran/multiple_files/test_multi_source_builds.py) | x2py does not infer dependency graphs, prebuilt module paths, or external library discovery. | +| 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/wrapper/fortran/multiple_files/test_multi_source_builds.py) | x2py 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 contract tests](../../../tests/wrapper/fortran/arrays/test_array_contracts.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](../guide/wrapping-derived-types.md#inheritance-and-polymorphism) | [Class policy route](../../developer/source-map.md#common-change-routes) | [Inheritance tests](../../../tests/wrapper/fortran/derived_types/test_inheritance.py) | Results, mutable dummies, arrays, polymorphic allocatable/pointer scalars, and `class(*)` are blocked. | | Ambiguous or incomplete constructor overload sets | Unsupported | [Constructor limitations](../guide/wrapping-derived-types.md#constructors) | [Constructor route](../../developer/source-map.md#common-change-routes) | [Constructor overload tests](../../../tests/wrapper/fortran/naming/test_phase9_class_overloads.py), [class-plan validation tests](../../../tests/wrapper_codegen/test_phase9_class_surfaces.py) | Candidates must have distinguishable exact runtime signatures and compatible native-owner lifecycles. | @@ -90,8 +90,8 @@ X2PY_C_DOCS_END --> | 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/wrapper/fortran/scalars/test_scalar_kinds.py) | x2py blocks rather than silently losing precision or Boolean storage semantics. | ## Planned Or Reserved Areas diff --git a/docs/user/language-support/index.md b/docs/user/language-support/index.md index 50679ab9a..9acff7f05 100644 --- a/docs/user/language-support/index.md +++ b/docs/user/language-support/index.md @@ -2,7 +2,7 @@ title: Language Support audience: users, developers prerequisites: user guide -related: feature-matrix.md, ../guide/fortran-wrapper.md +related: feature-matrix.md, ../reference/fortran-wrapper.md status: maintained publication: draft --- diff --git a/docs/user/language-support/supported-features.md b/docs/user/language-support/supported-features.md index 19ebc823b..443f8b392 100644 --- a/docs/user/language-support/supported-features.md +++ b/docs/user/language-support/supported-features.md @@ -2,7 +2,7 @@ title: Supported Features audience: users, developers prerequisites: feature matrix -related: feature-matrix.md, ../guide/fortran-wrapper.md +related: feature-matrix.md, ../reference/fortran-wrapper.md status: maintained publication: draft --- diff --git a/docs/user/reference/cli-commands.md b/docs/user/reference/cli-commands.md index 26819e3b6..56179047e 100644 --- a/docs/user/reference/cli-commands.md +++ b/docs/user/reference/cli-commands.md @@ -107,7 +107,7 @@ default output directory shown there is `./__x2py__`. saved build; it does not generate a manifest. Manifest replay accepts only overrides that the replay implementation consumes: `--out`, `--compiler`, `-I`/`--include-dir`, `--json`, `--verbose`, -`--no-color`, and `--debug`/`--debug-traceback`. The manifest owns its output +`--no-color`, and `--debug`. The manifest owns its output directory, input language, preprocessing recipe, wrapper behavior, native inputs, and link plan, so replay rejects flags from those areas instead of silently ignoring them. @@ -377,7 +377,7 @@ Important boundaries: --build-manifest PATH` regenerates `Makefile.x2py` without positional contracts or repeated native flags. Replay may override only `--out`, `--compiler`, `-I`/`--include-dir`, `--json`, `--verbose`, `--no-color`, and - `--debug`/`--debug-traceback`; all other build settings come from the + `--debug`; all other build settings come from the manifest. | `--wrapper-fortran-flags FLAG...` | Appends flags to generated Fortran bridge compilation commands. | | `--wrapper-c-flags FLAG...` | Appends flags to generated binding compilation and extension-link commands. | | `--no-color` | Disables ANSI color in parse diagnostics. | -| `--debug`, `--debug-traceback` | Re-raises parser errors so Python prints a traceback. | +| `--debug` | Re-raises command failures so Python prints a traceback. | When `rich-argparse` is installed, x2py uses its colored help formatter automatically. Install the optional UI dependencies for a published package @@ -439,7 +439,7 @@ X2PY_C_DOCS_END --> ## Related pages - Use [Python API Reference](python-api.md) when calling x2py from Python. -- Use [Fortran Wrapper Guide](../guide/fortran-wrapper.md) for wrapper +- Use [Fortran Wrapper Reference](fortran-wrapper.md) for wrapper build workflows. - Use [Semantic .pyi Format](semantic-pyi-format.md) when editing wrapper contracts. diff --git a/docs/user/reference/configuration-files.md b/docs/user/reference/configuration-files.md index 09f0be25f..bea78a8fb 100644 --- a/docs/user/reference/configuration-files.md +++ b/docs/user/reference/configuration-files.md @@ -2,7 +2,7 @@ title: Configuration Files Reference audience: users, developers prerequisites: packaging, CLI commands -related: cli-commands.md, python-api.md, ../guide/packaging.md, ../../developer/quality-assurance.md +related: cli-commands.md, python-api.md, ../guide/building-shared-library.md, ../../developer/quality-assurance.md status: maintained publication: draft --- @@ -64,7 +64,7 @@ build flags. The preceding `generate --makefile` command is what writes a new Replay accepts only settings that are defined as overrides: `--out`, `--compiler`, `-I`/`--include-dir`, `--json`, `--verbose`, `--no-color`, and -`--debug`/`--debug-traceback`. The manifest remains authoritative for its +`--debug`. The manifest remains authoritative for its output directory, language, preprocessing recipe, wrapper behavior, native inputs, and ordered link plan. Passing one of those saved settings again is an error rather than an ignored command-line value. @@ -119,18 +119,19 @@ Wrapper users select inputs through CLI flags, Python API arguments, semantic selects MkDocs' built-in Read the Docs theme, owns the complete intended navigation tree, and loads the publication hook. The theme configuration keeps the sidebar expanded through four navigation levels. A local stylesheet keeps -the page body adjacent to the sidebar with a `1200px` maximum width, balancing -readable prose with room for code and tables. Code and result blocks use the -available page width up to a consistent `56rem` cap; long lines scroll inside -the block. Local JavaScript and CSS add an accessible copy control to every -rendered code, command-output, and result block, with separate space reserved -beside the text. The production hook includes only pages whose front matter says -`publication: reviewed`. A draft lane index suppresses its complete User, -Developer, or Maintainer lane. Links from documentation pages to existing -source, tests, configuration, and other repository evidence are rendered as -GitHub links because those files are outside the MkDocs source tree. Links -between documentation pages remain site-relative and are never rewritten to -GitHub. +its scrollbar visible and draggable when the navigation is longer than the +screen. The same stylesheet keeps the page body adjacent to the sidebar with a +`1200px` maximum width, balancing readable prose with room for code and tables. +Code and result blocks use the available page width up to a consistent `56rem` +cap; long lines scroll inside the block. Local JavaScript and CSS add an +accessible copy control to every rendered code, command-output, and result +block, with separate space reserved beside the text. The production hook +includes only pages whose front matter says `publication: reviewed`. A draft +lane index suppresses its complete User, Developer, or Maintainer lane. Links +from documentation pages to existing source, tests, configuration, and other +repository evidence are rendered as GitHub links because those files are +outside the MkDocs source tree. Links between documentation pages remain +site-relative and are never rewritten to GitHub. Preview exactly what GitHub Pages will publish with: @@ -173,5 +174,5 @@ Tooling configuration is covered by [`test_check_static_analysis_versions.py`](../../../tests/tools/test_check_static_analysis_versions.py). When a generated file contract changes, update this page with the CLI reference, -Python API reference, packaging guide, and wrapper tests that prove the replay -or Makefile behavior. +Python API reference, shared-library build guide, and wrapper tests that prove +the replay or Makefile behavior. diff --git a/docs/user/guide/editing-semantic-pyi-contracts.md b/docs/user/reference/editing-semantic-pyi-contracts.md similarity index 98% rename from docs/user/guide/editing-semantic-pyi-contracts.md rename to docs/user/reference/editing-semantic-pyi-contracts.md index f8926fb21..0f2b143f9 100644 --- a/docs/user/guide/editing-semantic-pyi-contracts.md +++ b/docs/user/reference/editing-semantic-pyi-contracts.md @@ -1,21 +1,22 @@ --- title: Editing Semantic .pyi Contracts audience: users, advanced users -prerequisites: Fortran wrapper guide, semantic .pyi format -related: fortran-wrapper.md, ../reference/semantic-pyi-format.md +prerequisites: Fortran wrapper reference, semantic .pyi format +related: fortran-wrapper.md, semantic-pyi-format.md status: maintained publication: reviewed --- # Editing Semantic `.pyi` Contracts -This guide is the user-facing contract for changing a generated semantic +This reference explains how to change a generated semantic `.pyi` before building a wrapper. It covers the edits x2py handles, the native facts an edit must preserve, the runtime effect of each supported edit, and the errors raised for unsafe combinations. -The Semantic `.pyi` Format reference later provides the complete grammar. Use -this guide to decide whether a proposed edit is a supported wrapper operation. +The [Semantic `.pyi` Format](semantic-pyi-format.md) provides the complete +grammar. Use this page to decide whether a proposed edit is a supported wrapper +operation. ## The Editing Workflow @@ -232,7 +233,7 @@ def norm2(values: Float64[:]) -> Float64: ... The leaf filename identifies the native module. The declaration name selects the native procedure unless an -[`@bind(...)`](wrapping-functions.md#python-and-native-names) target is present. +[`@bind(...)`](../guide/wrapping-functions.md#python-and-native-names) target is present. ### Add or rename a native target @@ -760,5 +761,5 @@ artifact you are asking x2py to call. ## Next -- [Raw Addresses](raw-addresses.md) for primitive, array, and fixed-string +- [Raw Addresses](../guide/raw-addresses.md) for primitive, array, and fixed-string address boundaries diff --git a/docs/user/guide/fortran-wrapper.md b/docs/user/reference/fortran-wrapper.md similarity index 98% rename from docs/user/guide/fortran-wrapper.md rename to docs/user/reference/fortran-wrapper.md index 4718d04a8..2ca6b7c61 100644 --- a/docs/user/guide/fortran-wrapper.md +++ b/docs/user/reference/fortran-wrapper.md @@ -1,26 +1,27 @@ --- -title: Fortran Wrapper Guide +title: Fortran Wrapper Reference audience: users, advanced users prerequisites: first wrapped module, NumPy basics -related: index.md, editing-semantic-pyi-contracts.md, ../language-support/index.md +related: ../guide/index.md, editing-semantic-pyi-contracts.md, ../language-support/index.md status: maintained publication: reviewed --- -# Fortran Wrapper Guide +# Fortran Wrapper Reference -This guide describes the Python API generated by x2py for Fortran code. It is -both a user reference and the canonical contract for ownership, lifetime, -naming, supported behavior, and current limitations. +This reference describes the Python API generated by x2py for Fortran code. It +is the canonical contract for ownership, lifetime, naming, supported behavior, +and current limitations. -The guide follows the wrapper by subject. Each subject includes a small example -showing the Fortran interface and the corresponding Python use. Examples omit -unrelated module scaffolding when that makes the contract easier to see. +The reference follows the wrapper by subject. Each subject includes a small +example showing the Fortran interface and the corresponding Python use. +Examples omit unrelated module scaffolding when that makes the contract easier +to see. -This guide covers the implemented wrapper for Fortran source inputs. +This reference covers the implemented wrapper for Fortran source inputs. - Foundations: [building a wrapper](#building-and-importing-a-wrapper), [support boundaries](#how-support-claims-are-established), and [ownership and lifetime](#ownership-and-lifetime) -- Arrays and pointers: [allocatables](allocatables.md), - [pointers](pointers.md), [array results](arrays.md), and - [NumPy argument contracts](arrays.md) +- Arrays and pointers: [allocatables](../guide/allocatables.md), + [pointers](../guide/pointers.md), [array results](../guide/arrays.md), and + [NumPy argument contracts](../guide/arrays.md) - Objects and state: [derived types](#derived-types-across-procedure-boundaries), [inheritance](#inheritance-and-polymorphism), [constructors/finalizers](#constructors-initialization-and-finalizers), @@ -1133,7 +1134,7 @@ such a proxy raises `ReferenceError`. The full compatibility matrix includes `TARGET`, `VALUE`, empty state, pointer `INTENT(IN)`, and deliberate incompatibilities. See -[Derived Objects And Native Dummies](memory-management.md#derived-objects-and-native-dummies). +[Derived Objects And Native Dummies](../guide/memory-management.md#derived-objects-and-native-dummies). ## Pointer Arguments, Results, And Association @@ -1171,7 +1172,7 @@ print(total(pointer_handle)) # 6.0 The pointer-descriptor signature requires a `Pointer[T[...]]` handle; a plain NumPy array has no pointer descriptor and is rejected. Scalar pointer inputs remain call-local nullable values and do not expose persistent association to -Python. The complete pointer example earlier in the guide demonstrates a +Python. The complete pointer example earlier in this reference demonstrates a module handle passed to both descriptor and ordinary array parameters. ### Pointer Result Boundary diff --git a/docs/user/reference/index.md b/docs/user/reference/index.md index 6b48625d3..3bcde3874 100644 --- a/docs/user/reference/index.md +++ b/docs/user/reference/index.md @@ -2,23 +2,26 @@ title: Reference audience: users, developers prerequisites: getting started -related: cli-commands.md, python-api.md, semantic-ir.md, semantic-pyi-format.md, callbacks.md +related: cli-commands.md, python-api.md, fortran-wrapper.md, semantic-pyi-format.md, editing-semantic-pyi-contracts.md status: maintained publication: draft --- # Reference -Reference pages describe the command, API, and data contracts that user guides -and developer guides depend on. Workflow guidance belongs in tutorials, examples, -and user guides; this section stays close to the public surfaces. +Reference pages describe the command, API, generated-wrapper, and semantic +contract surfaces that other documentation depends on. They also cover the +advanced contract-editing boundary. Beginner workflows remain in tutorials, +examples, and user guides. ## Pages - [CLI commands](cli-commands.md) - [Python API](python-api.md) +- [Fortran wrapper reference](fortran-wrapper.md) - [Semantic IR](semantic-ir.md) - [Semantic .pyi format](semantic-pyi-format.md) +- [Editing semantic .pyi contracts](editing-semantic-pyi-contracts.md) - [Callbacks](callbacks.md) - [Diagnostic codes](diagnostic-codes.md) - [Generated functions](generated-functions.md) diff --git a/docs/user/reference/semantic-ir.md b/docs/user/reference/semantic-ir.md index a5ceef2c2..03c212358 100644 --- a/docs/user/reference/semantic-ir.md +++ b/docs/user/reference/semantic-ir.md @@ -21,7 +21,7 @@ Sections through [Deferred C Work](#deferred-c-work) describe current semantic behavior. The final self-contained C runtime-contract section is explicitly a design proposal and is not implemented C-input wrapper support. The current Fortran runtime contract is documented separately in -[Fortran wrapper guide](../guide/fortran-wrapper.md). +[Fortran wrapper reference](fortran-wrapper.md). X2PY_C_DOCS_END --> ## Datatype Mapping diff --git a/docs/user/reference/semantic-pyi-format.md b/docs/user/reference/semantic-pyi-format.md index 3acb02b38..ff693f715 100644 --- a/docs/user/reference/semantic-pyi-format.md +++ b/docs/user/reference/semantic-pyi-format.md @@ -11,7 +11,7 @@ publication: draft For the supported edit workflow and runtime consequences of changing a contract, including ownership and destruction examples, see -[Editing semantic `.pyi` contracts](../guide/editing-semantic-pyi-contracts.md). +[Editing semantic `.pyi` contracts](editing-semantic-pyi-contracts.md). For lookup-style commands, use the [verified examples cookbook](../examples/verified-cookbook.md). For the full generated Python contract, use the -[Fortran wrapper guide](../guide/fortran-wrapper.md). +[Fortran wrapper reference](../reference/fortran-wrapper.md). ## Before You Start @@ -251,7 +251,7 @@ Next: - Use the [verified examples cookbook](../examples/verified-cookbook.md) for task-specific recipes. -- Use the [Fortran wrapper guide](../guide/fortran-wrapper.md) for the +- Use the [Fortran wrapper reference](../reference/fortran-wrapper.md) for the complete generated Python behavior. - Use [Semantic .pyi Format](../reference/semantic-pyi-format.md) when editing wrapper contracts. diff --git a/docs/user/tutorials/large-fortran-codebase.md b/docs/user/tutorials/large-fortran-codebase.md index e86054744..141dbd54d 100644 --- a/docs/user/tutorials/large-fortran-codebase.md +++ b/docs/user/tutorials/large-fortran-codebase.md @@ -2,7 +2,7 @@ title: Large Fortran Codebase Tutorial audience: advanced users prerequisites: modern Fortran project tutorial, packaging -related: modern-fortran-project.md, ../guide/packaging.md +related: modern-fortran-project.md, ../guide/building-shared-library.md status: planned-documentation publication: draft --- diff --git a/docs/user/tutorials/packaging.md b/docs/user/tutorials/packaging.md index a339a85dd..fea3f1dd6 100644 --- a/docs/user/tutorials/packaging.md +++ b/docs/user/tutorials/packaging.md @@ -2,7 +2,7 @@ title: Packaging Tutorial audience: users, packagers prerequisites: basic wrapper tutorial -related: ../guide/packaging.md, ../guide/distribution.md +related: ../guide/building-shared-library.md status: planned-documentation publication: draft --- diff --git a/mkdocs.yml b/mkdocs.yml index 00c91ccc1..4b163bbce 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -53,12 +53,9 @@ nav: - Memory Management: user/guide/memory-management.md - Callbacks: user/guide/callbacks.md - Enumerations: user/guide/enumerations.md - - Error Handling & Diagnostics: user/guide/error-handling.md - - Packaging: user/guide/packaging.md - - Distribution: user/guide/distribution.md - - Fortran Wrapper Guide: user/guide/fortran-wrapper.md - - Editing Semantic .pyi Contracts: user/guide/editing-semantic-pyi-contracts.md - Raw Addresses: user/guide/raw-addresses.md + - Error Handling & Diagnostics: user/guide/error-handling.md + - Building the Shared Library: user/guide/building-shared-library.md - Tutorials: - Overview: user/tutorials/index.md - Basic Wrapper Tutorial: user/tutorials/basic-wrapper.md @@ -92,8 +89,10 @@ nav: - Overview: user/reference/index.md - CLI Commands: user/reference/cli-commands.md - Python API: user/reference/python-api.md + - Fortran Wrapper Reference: user/reference/fortran-wrapper.md - Semantic IR: user/reference/semantic-ir.md - Semantic .pyi Format: user/reference/semantic-pyi-format.md + - Editing Semantic .pyi Contracts: user/reference/editing-semantic-pyi-contracts.md - Callbacks: user/reference/callbacks.md - Diagnostic Codes: user/reference/diagnostic-codes.md - Generated Functions: user/reference/generated-functions.md diff --git a/tests/cli/test_argument_contract.py b/tests/cli/test_argument_contract.py index 07b9269c4..5f371a539 100644 --- a/tests/cli/test_argument_contract.py +++ b/tests/cli/test_argument_contract.py @@ -576,8 +576,8 @@ def assert_group_order(help_text, *headings): assert "Name the Python extension and stable NAME.so library" in normalized_build_help assert "Print build paths and metadata as JSON" in normalized_build_help assert 'Native compiler flags (for example, "-O3 -fopenmp")' in normalized_build_help - assert "docs/user/guide/fortran-wrapper.md" in build_help - assert "See docs/user/guide/fortran-wrapper.md for native flags and libraries." in build_help + assert "docs/user/reference/cli-commands.md" in build_help + assert "See docs/user/reference/cli-commands.md for all build options." in build_help assert "Build from a semantic contract:" in build_help assert "Replay a build manifest:" in build_help assert "Manifest overrides: --out, --compiler, -I/--include-dir" in normalized_build_help diff --git a/tests/docs/test_structure.py b/tests/docs/test_structure.py index 7be6de7f2..792c87154 100644 --- a/tests/docs/test_structure.py +++ b/tests/docs/test_structure.py @@ -116,8 +116,10 @@ "user/reference/index.md", "user/reference/cli-commands.md", "user/reference/python-api.md", + "user/reference/fortran-wrapper.md", "user/reference/semantic-ir.md", "user/reference/semantic-pyi-format.md", + "user/reference/editing-semantic-pyi-contracts.md", "user/reference/diagnostic-codes.md", ] REQUIRED_ROADMAP_PAGES = [ @@ -149,12 +151,9 @@ "user/guide/memory-management.md", "user/guide/callbacks.md", "user/guide/enumerations.md", - "user/guide/error-handling.md", - "user/guide/packaging.md", - "user/guide/distribution.md", - "user/guide/fortran-wrapper.md", - "user/guide/editing-semantic-pyi-contracts.md", "user/guide/raw-addresses.md", + "user/guide/error-handling.md", + "user/guide/building-shared-library.md", ] CLI_HELP_GROUP_HEADINGS = [ "commands:", @@ -213,7 +212,6 @@ "--verbose", "--no-color", "--debug", - "--debug-traceback", ] CLI_VISIBLE_HELP_OPTIONS = CLI_REFERENCE_OPTIONS REQUIRED_SOURCE_NAVIGATION_PAGES = [ @@ -277,8 +275,8 @@ "docs/user/examples/recipes/inspect-c-api.md", "docs/user/examples/recipes/inspect-fortran-api.md", "docs/user/examples/recipes/semantic-pyi-contracts.md", - "docs/user/guide/fortran-wrapper.md", - "docs/user/guide/editing-semantic-pyi-contracts.md", + "docs/user/reference/fortran-wrapper.md", + "docs/user/reference/editing-semantic-pyi-contracts.md", "docs/user/reference/cli-commands.md", "docs/user/reference/diagnostic-codes.md", "docs/user/reference/python-api.md", @@ -967,18 +965,7 @@ def test_next_sections_use_linked_bullet_destinations(relative_path: str) -> Non assert MARKDOWN_LINK.search(item), f"{relative_path}:{line_number}: Next item must include a Markdown link" -@pytest.mark.parametrize( - "relative_path", - [ - path - for path in REQUIRED_USER_GUIDE_PAGES - if path - not in { - "user/guide/fortran-wrapper.md", - "user/guide/editing-semantic-pyi-contracts.md", - } - ], -) +@pytest.mark.parametrize("relative_path", REQUIRED_USER_GUIDE_PAGES) def test_user_guide_commands_do_not_expose_fixture_paths(relative_path: str) -> None: page = (DOCS_ROOT / relative_path).read_text(encoding="utf-8") assert "python3 -m x2py tests/" not in page @@ -1139,18 +1126,16 @@ def test_getting_started_pages_keep_advanced_stage_flags_out_of_beginner_path() assert "--json" not in content -def test_user_guide_uses_automatic_wrapper_stage_selection() -> None: +def test_user_guide_shows_direct_shared_library_build() -> None: content = "\n".join( _visible_documentation_source(DOCS_ROOT / relative_path) for relative_path in REQUIRED_USER_GUIDE_PAGES ) - assert "python3 -m x2py generate --makefile src/scale.f90" in content - assert "python3 -m x2py contracts/solver/__init__.pyi \\\n --native-fortran-sources solver.f90" in content - assert "python3 -m x2py generate --makefile mesh.f90 solver.f90 --out-dir build" in content + assert "python3 -m x2py src/scale.f90 --out-dir build/scale" in content -def test_fortran_wrapper_guide_shows_every_common_shared_library_build_input() -> None: - content = _visible_documentation_source(DOCS_ROOT / "user/guide/fortran-wrapper.md") +def test_fortran_wrapper_reference_shows_every_common_shared_library_build_input() -> None: + content = _visible_documentation_source(DOCS_ROOT / "user/reference/fortran-wrapper.md") example = content.split("For example, this command supplies every common build input", maxsplit=1)[1].split( "`--compiler` selects", maxsplit=1 )[0] @@ -1182,7 +1167,8 @@ def test_array_handle_docs_keep_views_copies_and_handles_distinct() -> None: assert "`associate(other)` makes two pointer handles refer to the same target" in pointers assert "If `p2` is unassociated, `p1` becomes" in pointers assert "Do Not Return A Pointer To Expired Local Storage" in pointers - assert "plain and `Aliased` derived module variables remain live native-owned objects" in memory + assert "plain and `Aliased` derived module variables remain live objects" in memory + assert "Fortran module owns their storage" in memory @pytest.mark.parametrize("heading", CLI_HELP_GROUP_HEADINGS) @@ -1354,6 +1340,10 @@ def test_site_theme_keeps_sidebar_open_and_code_blocks_copyable() -> None: assert "margin: 0" in layout_stylesheet assert ".wy-nav-side" in layout_stylesheet assert "padding-bottom: 0" in layout_stylesheet + assert ".wy-side-scroll" in layout_stylesheet + assert "overflow-y: auto" in layout_stylesheet + assert "scrollbar-width: thin" in layout_stylesheet + assert ".wy-side-scroll::-webkit-scrollbar-thumb" in layout_stylesheet assert ".rst-versions" in layout_stylesheet assert "display: none" in layout_stylesheet assert ".rst-content pre" in layout_stylesheet diff --git a/tests/parsing/c/test_c_cli_skeleton.py b/tests/parsing/c/test_c_cli_skeleton.py index f68800675..df5b34866 100644 --- a/tests/parsing/c/test_c_cli_skeleton.py +++ b/tests/parsing/c/test_c_cli_skeleton.py @@ -365,7 +365,7 @@ def test_cli_c_rejects_fortran_only_parse_flags(tmp_path: Path): assert "Fortran-only" in res.stderr -def test_cli_c_no_color_and_debug_traceback_flags_are_accepted(tmp_path: Path): +def test_cli_c_no_color_and_debug_flags_are_accepted(tmp_path: Path): header = tmp_path / "api.h" header.write_text("int run(void);\n", encoding="utf-8") cmd = [ @@ -377,7 +377,7 @@ def test_cli_c_no_color_and_debug_traceback_flags_are_accepted(tmp_path: Path): "--language", "c", "--no-color", - "--debug-traceback", + "--debug", ] res = subprocess.run(cmd, capture_output=True, text=True, check=True) diff --git a/tests/wrapper/fortran/layout_rules/test_wrapper_guide_layout.py b/tests/wrapper/fortran/layout_rules/test_wrapper_guide_layout.py index 1b9c5950d..ca26972e5 100644 --- a/tests/wrapper/fortran/layout_rules/test_wrapper_guide_layout.py +++ b/tests/wrapper/fortran/layout_rules/test_wrapper_guide_layout.py @@ -398,18 +398,18 @@ def test_wrapper_plan_migration_matrix_tracks_collected_wrapper_nodes(): assert _wrapper_plan_migration_summary_counts() == expected_summary -def test_wrapper_language_suite_and_user_guide_link_current_subject_paths(): +def test_wrapper_language_suite_and_reference_link_current_subject_paths(): root_test_modules = sorted(path.name for path in WRAPPER_SUITE_ROOT.glob("test_*.py")) assert root_test_modules == [] assert (WRAPPER_SUITE_ROOT / "README.md").is_file() assert "fortran/README.md" in (WRAPPER_SUITE_ROOT / "README.md").read_text(encoding="utf-8") - guide = (DOCS_ROOT / "user/guide/fortran-wrapper.md").read_text(encoding="utf-8") + reference = (DOCS_ROOT / "user/reference/fortran-wrapper.md").read_text(encoding="utf-8") runtime_paths = [test_path for test_path in SUBJECT_TEST_PATHS if not test_path.startswith("layout_rules/")] - missing = [test_path for test_path in runtime_paths if test_path not in guide] + missing = [test_path for test_path in runtime_paths if test_path not in reference] assert missing == [] - assert "- [x]" not in guide - assert "- [ ]" not in guide + assert "- [x]" not in reference + assert "- [ ]" not in reference def test_stale_wrapper_paths_are_rejected_after_stage_one_moves(): diff --git a/x2py/cli.py b/x2py/cli.py index 1e774de90..e77266235 100644 --- a/x2py/cli.py +++ b/x2py/cli.py @@ -101,9 +101,9 @@ " Replay a build manifest:\n" " python3 -m x2py --build-manifest build/x2py-build.json\n\n" ' See README.md "Quick Start" for the scale.f90 source and expected output.\n' - " See docs/user/guide/fortran-wrapper.md for native flags and libraries.\n\n" + " See docs/user/reference/cli-commands.md for all build options.\n\n" " Manifest overrides: --out, --compiler, -I/--include-dir, --json, --verbose,\n" - " --no-color, and --debug/--debug-traceback." + " --no-color, and --debug." ) _PARSE_HELP_EPILOG = ( f"{_HELP_DIVIDER}\n\n" @@ -1865,8 +1865,6 @@ def _add_diagnostic_controls(group: argparse._ArgumentGroup, *, allow_verbose: b group.add_argument("--no-color", action="store_true", help="Disable ANSI colors in help and diagnostics") group.add_argument( "--debug", - "--debug-traceback", - dest="debug", action="store_true", help="Re-raise command failures and print the full Python traceback", ) diff --git a/x2py/compiling/README.md b/x2py/compiling/README.md index 49f6675a6..09b0d4092 100644 --- a/x2py/compiling/README.md +++ b/x2py/compiling/README.md @@ -51,7 +51,7 @@ policy completion. Those decisions happen before generated sources reach this pa ## Tests And Docs -- Wrapper guide: `docs/user/guide/fortran-wrapper.md` +- Wrapper reference: `docs/user/reference/fortran-wrapper.md` - Build-system docs: `docs/developer/build-system.md` - Quality and static checks: `docs/developer/quality-assurance.md` - Source navigation: `docs/developer/source-map.md`, `docs/developer/feature-to-code-map.md` diff --git a/x2py/parsers/c/cli.py b/x2py/parsers/c/cli.py index b9e7da7e2..6162490ef 100644 --- a/x2py/parsers/c/cli.py +++ b/x2py/parsers/c/cli.py @@ -196,8 +196,6 @@ def main(argv: list[str] | None = None) -> int: parser.add_argument("--no-color", action="store_true", help="Disable ANSI color in parse diagnostics") parser.add_argument( "--debug", - "--debug-traceback", - dest="debug", action="store_true", help="Re-raise parser errors so Python prints a traceback for parser debugging.", ) diff --git a/x2py/parsers/fortran/cli.py b/x2py/parsers/fortran/cli.py index ae77920e8..874b997c0 100644 --- a/x2py/parsers/fortran/cli.py +++ b/x2py/parsers/fortran/cli.py @@ -301,8 +301,6 @@ def main() -> int: ) parser.add_argument( "--debug", - "--debug-traceback", - dest="debug", action="store_true", help="Re-raise parser errors so Python prints a traceback for parser debugging. " "Can also be enabled with FORTRAN_PARSER_DEBUG=1.", From 56eb54022960cb49d9208dc0ed41209c2a5a4f18 Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Tue, 28 Jul 2026 04:54:19 +0100 Subject: [PATCH 30/35] update docs --- .../user/getting-started/beginner-workflow.md | 78 +++++----- .../getting-started/first-wrapped-function.md | 91 ++++++++---- .../getting-started/first-wrapped-module.md | 41 +++++- docs/user/getting-started/index.md | 4 +- docs/user/getting-started/installation.md | 22 +-- docs/user/getting-started/verification.md | 71 ++------- docs/user/guide/allocatables.md | 66 ++++----- docs/user/guide/arrays.md | 11 +- docs/user/guide/callbacks.md | 12 +- docs/user/guide/data-types.md | 54 ++----- docs/user/guide/enumerations.md | 2 +- docs/user/guide/error-handling.md | 16 ++- docs/user/guide/generic-interfaces.md | 72 +++++++--- docs/user/guide/index.md | 14 +- docs/user/guide/memory-management.md | 72 +++++----- docs/user/guide/pointers.md | 35 ++--- docs/user/guide/raw-addresses.md | 33 +++-- docs/user/guide/strings.md | 17 ++- docs/user/guide/wrapping-derived-types.md | 135 ++++++++++++------ docs/user/guide/wrapping-functions.md | 76 +++------- docs/user/guide/wrapping-modules.md | 102 ++++++++++++- docs/user/guide/wrapping-subroutines.md | 11 +- docs/user/reference/fortran-wrapper.md | 2 +- tests/docs/test_structure.py | 87 ++++++++++- .../build_from_pyi/test_pyi_wrapper_builds.py | 8 +- .../fortran/module_state/test_module_state.py | 11 ++ .../fortran/naming/test_defined_operators.py | 4 + x2py/wrapper_codegen/docstrings.py | 12 +- 28 files changed, 690 insertions(+), 469 deletions(-) diff --git a/docs/user/getting-started/beginner-workflow.md b/docs/user/getting-started/beginner-workflow.md index 92c7c54de..e2a98a79e 100644 --- a/docs/user/getting-started/beginner-workflow.md +++ b/docs/user/getting-started/beginner-workflow.md @@ -10,13 +10,15 @@ publication: reviewed # Common Beginner Workflow -Now that you have built individual examples, here is a clean, repeatable workflow you can use for your own projects. -The example project continues to use `scale.f90`. +Now that you have built a function and a module, use this loop for your own +project: edit the source, review its Python interface, build, and test. --- ## Recommended Project Layout +This layout continues with the `scale.f90` example: + ``` my-project/ ├── src/ @@ -24,22 +26,23 @@ my-project/ ├── build/ # ← Generated, do not commit ├── tests/ │ └── test_scale.py -└── contracts/ # Optional: edited semantic contracts +└── contracts/ # Optional edited semantic contracts ``` Keep `src/` and `tests/` under version control. Never commit the `build/` folder. --- -## 1. Review the Contract First +## 1. Edit and Review -Before building, always inspect the generated contract: +Edit the Fortran source, then preview the generated Python interface: ```bash python3 -m x2py generate --pyi src/scale.f90 ``` -This shows you exactly what Python signatures and dtypes x2py expects. +Check the function names, arguments, result types, and required NumPy dtypes. +This review is especially useful after changing a public Fortran declaration. --- @@ -49,16 +52,18 @@ This shows you exactly what Python signatures and dtypes x2py expects. python3 -m x2py src/scale.f90 --out-dir build/scale ``` -Use `--verbose` if you want to see the exact compiler and linker commands. +Rerun the same command after source changes. Add `--verbose` only when you need +the compiler and linker details. --- -## 3. Write a Small Smoke Test +## 3. Write a Small Test Create `tests/test_scale.py`: ```python import sys + import numpy as np sys.path.insert(0, "build/scale") @@ -67,64 +72,55 @@ import scale def test_scale_function(): result = scale.scale(np.float64(3.0), np.float64(2.5)) assert result == 7.5 - -if __name__ == "__main__": - test_scale_function() - print("✅ Test passed") ``` Run it with: ```bash python3 -m pytest tests/test_scale.py -q -# or simply: -python3 tests/test_scale.py ``` --- -## 4. Clean Rebuild When Needed +## 4. Optionally Edit the Contract -When you change the Fortran source or want a completely clean build: +Save a contract package when you want to change the Python interface: ```bash -rm -rf build/scale -python3 -m x2py src/scale.f90 --out-dir build/scale +python3 -m x2py generate --pyi src/scale.f90 --out contracts/scale ``` ---- - -## 5. Advanced: Editing the Semantic Contract (Optional) - -Only do this after you are comfortable with the basic workflow: +Edit `contracts/scale/scale.pyi`, then build through its package entry: ```bash -python3 -m x2py generate --pyi src/scale.f90 --out contracts +python3 -m x2py contracts/scale/__init__.pyi \ + --native-fortran-sources src/scale.f90 \ + --out-dir build/scale-edited ``` -Editing contracts is powerful but adds complexity. See -**Editing Semantic .pyi Contracts** in Reference when you're ready. +Use this form instead of the source build in step 2 when the edited contract +should control the wrapper. The `.pyi` controls the Python surface; the Fortran +source still supplies the native implementation. Keep its native symbol names, +types, rank, and argument order accurate. + +The User Guide introduces small edits next to the feature they affect, such as +renaming a function, changing array layout, adding an overload, or exposing a +module procedure as a method. --- -## Summary of the Workflow +## 5. Diagnose a Failure -1. Edit Fortran source in `src/` -2. Review contract with `generate --pyi` -3. Build with explicit `--out-dir` -4. Test with a Python smoke test -5. Clean rebuild when necessary (`rm -rf build/...`) +If a build fails, rerun it with `--verbose`. If a Python call fails, compare +the arguments with the generated contract. Use a clean output directory only +when you need to rule out stale build files. --- ## Next -- Explore the full [User Guide](../guide/index.md) -- Check the [Language Feature Matrix](../language-support/feature-matrix.md) -- Look at the [Verified Cookbook](../examples/verified-cookbook.md) for more examples - ---- - -**Troubleshooting** -Use `--verbose` on build failures. -Always compare failing calls with the generated contract. +- Continue with the [User Guide](../guide/index.md). +- Check the [Language Feature Matrix](../language-support/feature-matrix.md) for + current support boundaries. +- Use the [Verified Cookbook](../examples/verified-cookbook.md) for more + complete examples. diff --git a/docs/user/getting-started/first-wrapped-function.md b/docs/user/getting-started/first-wrapped-function.md index 5949ef564..0d119b3ab 100644 --- a/docs/user/getting-started/first-wrapped-function.md +++ b/docs/user/getting-started/first-wrapped-function.md @@ -28,6 +28,38 @@ end function scale --- +## Inspect the Generated Contract + +Preview the Python interface before building: + +```bash +python3 -m x2py generate --pyi scale.f90 +``` + +The generated semantic `.pyi` contains: + +```python +from x2py.contracts import Addr, Arg, Float64, external, native_call + +@external +@native_call([Addr(Arg(0)), Addr(Arg(1))]) +def scale( + value: Float64, + factor: Float64 +) -> Float64: ... +``` + +`Float64` means the function requires `numpy.float64` scalar arguments and +returns the same scalar type. `@external` identifies a procedure outside a +Fortran module. `@native_call(...)` maps the two Python arguments to the native +call and passes each scalar by address. + +This file is both the wrapper contract and an editable description of the +Python interface. You can leave it unchanged for this example; later pages +show useful edits in context. + +--- + ## Build the Extension From the directory containing `scale.f90`, run: @@ -40,45 +72,49 @@ This creates an importable `scale` extension module in the `build/first-function --- -## Import and Call +## Inspect the Generated Docstring + +x2py creates NumPy-style docstrings from the same contract. Import the built +extension and inspect the function: ```python import sys -import numpy as np sys.path.insert(0, "build/first-function") import scale -result = scale.scale(np.float64(3.0), np.float64(2.5)) -print(result) # 7.5 -assert result == 7.5 +print(scale.scale.__doc__) ``` ---- +```text +scale(value, factor) -> float64 -## Inspect the Generated Contract +Parameters +---------- +value : float64 +factor : float64 -You can preview the semantic interface without building: - -```bash -python3 -m x2py generate --pyi scale.f90 +Returns +------- +result : float64 ``` -The generated contract has this shape: +`help(scale.scale)` shows the same signature, parameter types, result, and +documented exceptions. Generated modules, classes, methods, and properties +also provide docstrings. + +--- + +## Call the Function ```python -from x2py.contracts import Addr, Arg, Float64, external, native_call +import numpy as np -@external -@native_call([Addr(Arg(0)), Addr(Arg(1))]) -def scale( - value: Float64, - factor: Float64 -) -> Float64: ... +result = scale.scale(np.float64(3.0), np.float64(2.5)) +print(result) # 7.5 +assert result == 7.5 ``` -This contract is the source of truth for the generated wrapper. - --- ## Common Pitfall: Wrong Scalar Type @@ -97,14 +133,11 @@ Always convert at the call site for scalar arguments. --- -## Next - -- Learn how to wrap [Fortran modules](first-wrapped-module.md) -- Read more about [wrapping functions](../guide/wrapping-functions.md) -- Understand the [semantic .pyi format](../reference/semantic-pyi-format.md) +If the build fails, rerun it with `--verbose`. --- -**Troubleshooting** -If the build fails, rerun with `--verbose`. -If the call fails, compare your arguments with the generated contract. +## Next + +- Continue with [Your First Wrapped Module](first-wrapped-module.md). +- For more function behavior, see [Wrapping Functions](../guide/wrapping-functions.md). diff --git a/docs/user/getting-started/first-wrapped-module.md b/docs/user/getting-started/first-wrapped-module.md index 7aee1f8e9..e2e002a7f 100644 --- a/docs/user/getting-started/first-wrapped-module.md +++ b/docs/user/getting-started/first-wrapped-module.md @@ -65,15 +65,47 @@ The extension will be named `module_state`, and the Fortran module will be avail --- -## Usage Example +## Inspect the Generated Docstring + +Import the built module and print its generated docstring: ```python import sys -import numpy as np sys.path.insert(0, "build/first-module") import module_state.module_state as mod +print(mod.__doc__) +``` + +```text +module_state + +Module Attributes +----------------- +nmax : int32 + Read-only constant. +counter : int32 +scale : float64 +saved_counter : int32 + +Functions +--------- +summarize() -> int32 +scaled_counter() -> float64 +next_local() -> int32 +``` + +`help(mod)` shows the same index. Individual functions have their own detailed +docstrings. + +--- + +## Usage Example + +```python +import numpy as np + print(mod.nmax) # 12 print(mod.counter) # 3 print(mod.scale) # 1.5 @@ -119,12 +151,13 @@ python3 -m x2py generate --pyi module_state.f90 - Each Fortran `module` becomes a child Python namespace. - Only **public** entities are exposed. - Private variables (like `hidden_counter`) are hidden. -- Use exact NumPy dtypes when reading/writing variables. +- Assign module variables with the matching NumPy scalar dtype. --- ## Next -- Continue with the [Beginner Workflow](beginner-workflow.md) to learn the full development cycle. +- Continue with the [Beginner Workflow](beginner-workflow.md) to turn these + steps into a repeatable development loop. - For module details, see [Wrapping Modules](../guide/wrapping-modules.md). - For support boundaries, see the [Language Feature Matrix](../language-support/feature-matrix.md). diff --git a/docs/user/getting-started/index.md b/docs/user/getting-started/index.md index 8d1139ada..bf513e6d2 100644 --- a/docs/user/getting-started/index.md +++ b/docs/user/getting-started/index.md @@ -21,10 +21,10 @@ The recommended beginner path uses the **GNU toolchain**, which offers the best Follow these pages in order: 1. **[Installation](installation.md)** — Install x2py and the required native compilers. -2. **[Verification](verification.md)** — Make sure everything works together. +2. **[Verification](verification.md)** — Check the package, headers, and compiler. 3. **[Your First Function](first-wrapped-function.md)** — Wrap a simple scalar Fortran function. 4. **[Your First Module](first-wrapped-module.md)** — Work with Fortran modules and saved state. -5. **[Development Workflow](beginner-workflow.md)** — Learn the edit → build → test loop. +5. **[Development Workflow](beginner-workflow.md)** — Learn the edit → review → build → test loop. --- diff --git a/docs/user/getting-started/installation.md b/docs/user/getting-started/installation.md index aa8a00a78..3d1bdeaf0 100644 --- a/docs/user/getting-started/installation.md +++ b/docs/user/getting-started/installation.md @@ -10,7 +10,8 @@ publication: reviewed # Installation -x2py is currently installed from a local source checkout. Building Python extensions also requires a native GNU toolchain. +x2py is currently installed from a local source checkout. Building Python +extensions also requires GNU Fortran and standard build tools. --- @@ -70,23 +71,6 @@ python3 -m pip install -e ".[qa]" --- -## Header and Compiler Checks - -Run these commands to verify your setup: - -```bash -# Python and NumPy headers -python3 -c "import sysconfig; print(sysconfig.get_path('include'))" -python3 -c "import numpy; print(numpy.get_include())" - -# Compiler -gfortran --version -``` - -Make sure the printed header directories exist. - ---- - ## Platform Support | Platform | Current Status | @@ -100,5 +84,5 @@ Make sure the printed header directories exist. ## Next -- Go to the [Verification](verification.md) page after the checks above pass. +- Go to [Verification](verification.md) to check the installation and compiler. - If setup fails, see [Installation Issues](../troubleshooting/installation-issues.md). diff --git a/docs/user/getting-started/verification.md b/docs/user/getting-started/verification.md index 14d43110e..e73048154 100644 --- a/docs/user/getting-started/verification.md +++ b/docs/user/getting-started/verification.md @@ -10,7 +10,8 @@ publication: reviewed # Verification -After installation, verify the Python package, contract generation, and native build toolchain separately. This makes debugging much easier. +After installation, check the Python package, required headers, and compiler. +The next page uses them together to build a complete extension. --- @@ -22,76 +23,33 @@ Run these commands in your activated virtual environment: # Check x2py and NumPy python3 -c "from importlib.metadata import version; import x2py, numpy; print('x2py:', version('x2py')); print('NumPy:', numpy.__version__)" -# Check CLI entrypoint +# Check the command-line interface python3 -m x2py --help ``` --- -## 2. Verify Contract Generation +## 2. Verify the Required Headers -Use the `scale.f90` file from the homepage example. - -Run this command in the directory containing `scale.f90`: +Print the Python and NumPy header directories: ```bash -python3 -m x2py generate --pyi scale.f90 +python3 -c "import sysconfig; print(sysconfig.get_path('include'))" +python3 -c "import numpy; print(numpy.get_include())" ``` -You should see a clean `.pyi`-style contract. This confirms parsing, semantic analysis, and type probing work correctly. +Both commands should print existing directories. --- -## 3. Verify Native Build Toolchain - -First, check the compiler: +## 3. Verify the Compiler ```bash gfortran --version ``` -Then build the extension: - -```bash -python3 -m x2py scale.f90 --out-dir build/verify -``` - -This should create an importable `scale` module inside `build/verify`. - -Test it: - -```python -import sys -import numpy as np - -sys.path.insert(0, "build/verify") -import scale - -result = scale.scale(np.float64(3.0), np.float64(2.5)) -print(result) # 7.5 -``` - ---- - -## 4. Inspect Generated Files (Optional) - -You can also inspect the build programmatically: - -```python -from x2py import build_fortran_extension - -build = build_fortran_extension("scale.f90", output_dir="build/verify") - -print("Compiled:", build.compiled) -print("Shared library:", build.shared_library) -print("Output directory:", build.output_dir) -``` - -For detailed output when something fails, add `--verbose`: - -```bash -python3 -m x2py scale.f90 --out-dir build/verify --verbose -``` +The output should identify GNU Fortran. If the command is missing, install +`gfortran` or add it to `PATH`. --- @@ -100,14 +58,13 @@ python3 -m x2py scale.f90 --out-dir build/verify --verbose | Failure Type | Recommended Action | |--------------------------------|---------------------------------------------| | Cannot import x2py / NumPy | Check active virtual environment | -| `x2py --help` works but `.pyi` fails | Check diagnostics and reference section | +| A header directory is missing | Reinstall Python development files or NumPy | | Compiler not found | Fix `PATH` or reinstall gfortran | -| Build / linking fails | Run with `--verbose` | -| Builds but import/call fails | Compare against generated contract | --- ## Next -- Proceed to [Your First Wrapped Function](first-wrapped-function.md). +- Build and call [Your First Wrapped Function](first-wrapped-function.md). That + example is the end-to-end verification. - For detailed help, use [Troubleshooting](../troubleshooting/index.md). diff --git a/docs/user/guide/allocatables.md b/docs/user/guide/allocatables.md index 4c6ddb130..836e46d15 100644 --- a/docs/user/guide/allocatables.md +++ b/docs/user/guide/allocatables.md @@ -2,7 +2,7 @@ title: Allocatables description: How x2py handles Fortran `allocatable` variables, arrays, and descriptors audience: users, advanced users -prerequisites: arrays, memory management +prerequisites: arrays related: arrays.md, pointers.md, memory-management.md status: maintained publication: reviewed @@ -12,7 +12,7 @@ publication: reviewed A Fortran allocatable descriptor records whether storage is allocated and, for arrays, its address, shape, and strides. The descriptor controls the allocation, -while the owner of the Python handle depends on where that handle came from. +and an x2py handle gives Python access to that descriptor. ## Key Concepts @@ -20,13 +20,14 @@ while the owner of the Python handle depends on where that handle came from. `Allocatable[T[...]]` handles. - An array handle exposes allocation state and descriptor operations; it is not itself a NumPy array. -- `allocated` reports whether storage exists; `to_numpy()` returns a borrowed - live view of that storage. +- `allocated` reports whether storage exists; `to_numpy()` returns a live view + of that storage. - Reallocation or deallocation invalidates existing views. -- Module and derived-field handles borrow their native owner. Returned and - caller-created handles own persistent descriptor storage. +- Module and derived-field handles expose storage that belongs to their module + or parent object. Returned and caller-created handles have their own + descriptor storage. - When available, `deallocate()` releases the current allocation but keeps the - handle open. `close()` permanently ends an owned handle. + handle open. `close()` permanently ends a returned or caller-created handle. --- @@ -87,7 +88,7 @@ allocation is present: ```python h = api.some_allocatable if h.allocated: - view = h.to_numpy() # borrowed live view + view = h.to_numpy() # live view view[0] = 42.0 else: print("Not allocated") @@ -102,8 +103,8 @@ else: | `to_numpy()` | `numpy.ndarray \| None` | A live view of current storage, or `None` when unallocated. It never creates an automatic detached snapshot. | | `deallocate()` | `() -> None` | Deallocates current storage when this operation is available for the handle. | | `resize(shape)` | `(int \| Sequence[int]) -> None` | Allocates or resizes storage to `shape` when this operation is available for the handle. | -| `close()` | `() -> None` | Permanently releases an owned descriptor and any remaining allocation. It does nothing on a borrowed handle. | -| `closed` | `bool` | Whether an owned handle has been closed. | +| `close()` | `() -> None` | Permanently releases a returned or caller-created descriptor and any remaining allocation. It does nothing on a module or field handle. | +| `closed` | `bool` | Whether a closable handle has been closed. | Calling `deallocate()` or `resize(shape)` when the operation is unavailable raises `NotImplementedError`. @@ -115,14 +116,15 @@ raises `NotImplementedError`. | Operation | What it releases | Handle afterward | | --- | --- | --- | | `deallocate()` | The current array allocation. | Open and usable, with `allocated == False`. | -| `close()` | An owned descriptor and any allocation it still contains. | Permanently closed and unusable. | +| `close()` | This handle's descriptor and any allocation it still contains. | Permanently closed and unusable. | -Finalization closes owned handles automatically. Call `close()` explicitly only -when deterministic release matters, such as after using a large allocation. +Returned and caller-created handles close automatically when Python no longer +uses them. Call `close()` explicitly only when immediate release matters, such +as after using a large allocation. -Module and field handles are borrowed. Calling `close()` on one is a no-op: it -leaves the handle and its owner's storage unchanged. `deallocate()` changes the -owner's allocation when that operation is available. +Calling `close()` on a module or field handle does nothing: it leaves the +handle and the module's or parent object's storage unchanged. `deallocate()` +changes that allocation when the operation is available. --- @@ -149,8 +151,8 @@ assert h.shape == (5,) ### Function Results -An allocatable-array function result becomes an owned `AllocatableArray`. The -handle owns persistent descriptor storage that x2py finalizes automatically: +An allocatable-array function result becomes an `AllocatableArray` with its own +descriptor storage, which x2py releases automatically: ```python values = api.make_values(3) @@ -173,13 +175,13 @@ def maybe_values( The second function still returns a present handle. That handle may have `allocated == False`, in which case `to_numpy()` returns `None`. Returning an -unallocated direct result without `MaybeUnallocated` violates the generated -contract and may crash the wrapper. +unallocated direct result without `MaybeUnallocated` violates the wrapper +contract. ### Output And Inout Arguments A nonoptional allocatable-array `intent(out)` does not consume incoming -allocation state, so it is hidden and returned as a new owned handle. A hidden +allocation state, so it is hidden and returned as a new handle. A hidden output may remain unallocated. An optional `intent(out)` remains visible so omission preserves native @@ -292,23 +294,23 @@ descriptor, discard `view` and call `to_numpy()` again. The independent ```python view = result.to_numpy() result.close() -result.shape # NOT OK: the owned descriptor has been released -view[0] # NOT OK: close() released the owned allocation +result.shape # NOT OK: the descriptor has been released +view[0] # NOT OK: close() released the allocation ``` -A view normally retains its handle, but an explicit `close()` releases an owned -allocatable result immediately. Finish using or copy all views before closing -the handle. +A view normally retains its handle, but an explicit `close()` releases a +returned allocatable result immediately. Finish using or copy all views before +closing the handle. ### Release Only Through The Owner ```python -h.deallocate() # may be unavailable when h only observes native-owned storage +h.deallocate() # may be unavailable for module or field storage ``` -Not every borrowed handle lets Python resize or deallocate its owner's storage. -An unavailable operation raises `NotImplementedError`. Use the native owner's -functions to change that storage instead. +Not every module or field handle lets Python resize or deallocate its storage. +An unavailable operation raises `NotImplementedError`. Use the module's or +parent object's functions to change that storage instead. --- @@ -327,6 +329,6 @@ present with that value. See [Optional Arguments](optional-arguments.md). ## Next -- Review [Memory Management](memory-management.md) for the ownership and live - view rules shared by all native storage. - Continue with [Pointers](pointers.md) for association and target lifetime. +- Then read [Memory Management](memory-management.md) for the rules shared by + both kinds of handle. diff --git a/docs/user/guide/arrays.md b/docs/user/guide/arrays.md index eb35c4971..a08c7465c 100644 --- a/docs/user/guide/arrays.md +++ b/docs/user/guide/arrays.md @@ -456,11 +456,10 @@ Use this list when reading or editing a generated `.pyi` contract: ## Next -- [Wrapping Functions](wrapping-functions.md) and - [Wrapping Subroutines](wrapping-subroutines.md) -- [Strings](strings.md) for fixed-width NumPy byte arrays +- Continue with [Strings](strings.md) for fixed-width NumPy byte arrays. +- Then read [Wrapping Functions](wrapping-functions.md) and + [Wrapping Subroutines](wrapping-subroutines.md). - [Allocatables](allocatables.md) and [Pointers](pointers.md) for native - allocation control -- [Raw Addresses](raw-addresses.md) for unchecked address-based calls + allocation control. - [Language Feature Matrix](../language-support/feature-matrix.md) for support - details + details. diff --git a/docs/user/guide/callbacks.md b/docs/user/guide/callbacks.md index f293df708..15386752d 100644 --- a/docs/user/guide/callbacks.md +++ b/docs/user/guide/callbacks.md @@ -2,7 +2,7 @@ title: Callbacks description: How to pass Python callables to Fortran as callbacks with x2py audience: advanced users -prerequisites: wrapping functions, error handling, data types +prerequisites: wrapping functions, data types related: error-handling.md, memory-management.md, ../reference/semantic-pyi-format.md status: maintained publication: reviewed @@ -23,8 +23,8 @@ 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` | Owned `np.float64` scalar | -| Primitive scalar reference dummy | `value: Addr(Float64)` | Owned `np.float64` scalar | +| 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 | @@ -115,6 +115,7 @@ python3 -m x2py callbacks.f90 --out-dir build/callbacks ```python import sys + import numpy as np sys.path.insert(0, "build/callbacks") @@ -158,8 +159,8 @@ For scalar arguments, choose the spelling from the Fortran callback dummy: | `real(8), intent(in) :: value` | `value: Addr(Float64)` | | `real(8), value :: value` | `value: Float64` | -Both forms call Python with an owned `np.float64` scalar. The difference is the -native calling convention x2py must match. +Both forms call Python with an independent `np.float64` scalar. The difference +is the native calling convention x2py must match. `Value(T)` is only for supported non-primitive scalar value dummies, such as a derived-type callback dummy declared with the Fortran `value` attribute. @@ -212,5 +213,6 @@ boundary: x2py prints the Python traceback and aborts the host process. ## Next +- Continue with [Enumerations](enumerations.md). - Use the [Callbacks Reference](../reference/callbacks.md) for the full prototype contract. - Review [Error Handling](error-handling.md) when callback failure behavior matters. diff --git a/docs/user/guide/data-types.md b/docs/user/guide/data-types.md index 03c7b8a5b..a7c7bf690 100644 --- a/docs/user/guide/data-types.md +++ b/docs/user/guide/data-types.md @@ -67,6 +67,7 @@ python3 -m x2py numeric_types.f90 --out-dir build/numeric-types ```python import sys + import numpy as np sys.path.insert(0, "build/numeric-types") @@ -122,30 +123,6 @@ Array annotations are not array factories: `Float64[:]()` is invalid. Create ordinary arrays with NumPy. Allocatable and pointer descriptor handles have their own default constructors, described in their later user-guide pages. -### Sharing Descriptor Handles Between Modules - -The same allocatable or pointer handle can be passed between separately built -x2py extension modules. The modules do not need to link to each other. Their -matching wrapper arguments must use the same descriptor kind, element type, -and rank. - -For a writable handle, the first wrapper that needs persistent storage attaches -a versioned record defined by x2py's bundled native binding support. Later -wrappers validate that record before using the same descriptor. This is a -zero-copy handoff and therefore requires both extensions to use compatible -versions of: - -- the x2py native-handle ABI; -- the same Fortran compiler descriptor ABI; and -- the same Fortran runtime ABI. - -In practice, build both extensions with the same compiler toolchain and avoid -separately statically linked Fortran runtimes. An incompatible native-handle -record is rejected instead of being interpreted as a descriptor. Pointer -sharing still does not transfer ownership of the pointer target. - ---- - ## Important Rules - Always use **exact NumPy scalar dtypes** (`np.float64`, `np.int32`, etc.). @@ -157,25 +134,18 @@ sharing still does not transfer ownership of the pointer target. ## Values And Native Storage -A bare primitive type is a Python-visible value: +A bare primitive type represents a Python-visible scalar: ```python def double(value: Float64) -> Float64: ... ``` -If the native routine expects that scalar by reference, the generated -`@native_call` records the address handoff while the Python call stays simple: - -```python -@native_call([Addr(Arg(0))]) -def double(value: Float64) -> Float64: ... -``` +The wrapper handles the native call details. Python still passes and receives +`numpy.float64` values. -Use `T[()]` when the Python boundary is rank-zero NumPy storage. Arguments -accept a 0-D NumPy array, and results return a 0-D NumPy array. -Use type-level `Addr(T)` only for an API whose caller supplies a raw integer -address. Prefer normal array and string contracts when the caller can pass the -storage object itself. +`T[()]` represents rank-zero NumPy storage: arguments accept a 0-D NumPy +array, and results return a 0-D NumPy array. Raw integer addresses are an +advanced boundary covered later in the guide. The semantic format can represent wider types such as `Float128` and `Complex256`, but the current Fortran wrapper blocks real storage wider than 64 @@ -185,9 +155,9 @@ bits and complex storage wider than 128 total bits instead of narrowing it. ## Next -- **[Arrays](arrays.md)** - Rank, shape, strides, and contiguity rules -- [Strings](strings.md) - Immutable values and mutable character storage -- [Raw Addresses](raw-addresses.md) - Advanced address-based calls +- Continue with [Arrays](arrays.md) for rank, shape, strides, and contiguity. +- Then read [Strings](strings.md) for immutable values and mutable character + storage. - [Wrapping Derived Types](wrapping-derived-types.md) -- [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) -- Check the [Language Feature Matrix](../language-support/feature-matrix.md) for current type support status. +- Check the [Language Feature Matrix](../language-support/feature-matrix.md) + for current type support. diff --git a/docs/user/guide/enumerations.md b/docs/user/guide/enumerations.md index e99aba3bd..2477a557d 100644 --- a/docs/user/guide/enumerations.md +++ b/docs/user/guide/enumerations.md @@ -83,5 +83,5 @@ print(result) # 10 ## Next -- Continue with [Callbacks](callbacks.md). +- Continue with [Raw Addresses](raw-addresses.md). - Check the [Language Feature Matrix](../language-support/feature-matrix.md) for current enum support. diff --git a/docs/user/guide/error-handling.md b/docs/user/guide/error-handling.md index 6a5ac4ff2..cd26cb1f3 100644 --- a/docs/user/guide/error-handling.md +++ b/docs/user/guide/error-handling.md @@ -18,13 +18,13 @@ x2py reports failures at several distinct stages. Understanding which stage fail | Stage | Typical Cause | What to do | |-----------------------------|----------------------------------------------------|----------| -| Preprocessing / Parsing | Syntax x2py can't model, missing include | Check the diagnostic code and source location | -| Semantic Conversion | Unresolved types, missing contract facts | Fix source or edit the generated `.pyi` | -| Policy Completion & Planning| Unsupported ownership, layout, callback, etc. | Read the full error message — it points to the problematic declaration | +| Parsing | Syntax x2py cannot model, missing include | Check the diagnostic code and source location | +| Interface conversion | Unresolved types or missing interface details | Fix the source or edit the generated `.pyi` | +| Wrapper planning | Unsupported storage, layout, or callback combination | Read the full error message; it points to the declaration | | Compilation / Linking | Compiler issues, missing modules/libraries | Run with `--verbose` to see native commands | -| Import | Missing shared library, ABI mismatch | Check paths and environment | +| Import | Missing library or incompatible build tools | Check paths and environment | | Python Call | Wrong dtype, shape, layout, class, etc. | Match the generated contract | -| Native Execution | Application-level status (e.g. error code) | Use `@raises` projection or handle manually | +| Native Execution | Application-level status, such as an error code | Convert it with `@raises` or handle it manually | | Callback / Fatal | Exception in callback, `stop`, `error stop` | Process usually aborts | --- @@ -69,6 +69,8 @@ def solve(value: Int32) -> None: ... Then: ```python +import numpy as np + try: api.solve(np.int32(-1)) except RuntimeError as e: @@ -99,5 +101,7 @@ except RuntimeError as e: ## Next +- Finish with [Building the Shared Library](building-shared-library.md). - [Editing Semantic `.pyi` Contracts](../reference/editing-semantic-pyi-contracts.md) -- Check the [Diagnostic Codes](../reference/diagnostic-codes.md) reference for detailed error categories +- Check the [Diagnostic Codes](../reference/diagnostic-codes.md) reference for + detailed error categories. diff --git a/docs/user/guide/generic-interfaces.md b/docs/user/guide/generic-interfaces.md index 46067ab85..14e1fbf39 100644 --- a/docs/user/guide/generic-interfaces.md +++ b/docs/user/guide/generic-interfaces.md @@ -60,6 +60,10 @@ python3 -m x2py generic.f90 --out-dir build/generic The semantic `.pyi` keeps the concrete procedures as private link targets. Each public declaration adds one candidate to `convert`: +`@private` hides a concrete procedure from Python. `@overload` links a public +candidate to that procedure, and `@bind` selects the public native generic when +the concrete procedure is private in Fortran. + ```python from x2py.contracts import Float64, Int32, bind, overload, private @@ -78,9 +82,8 @@ def convert(value: Int32) -> Int32: ... def convert(value: Float64) -> Float64: ... ``` -The concrete procedures are private because the source exports `convert`, not -`convert_integer` or `convert_real`. Each `@overload` links the candidate to -its concrete contract. Since those procedures are native-private, +The source exports `convert`, not `convert_integer` or `convert_real`. Since +those concrete procedures are native-private, [`@bind("convert")`](wrapping-functions.md#python-and-native-names) routes both candidates through the public generic. @@ -90,6 +93,7 @@ candidates through the public generic. ```python import sys + import numpy as np sys.path.insert(0, "build/generic") @@ -104,35 +108,61 @@ The argument type selects the concrete procedure. `np.int32` calls --- -## Extending an Overload Set +## Inspect the Overloads + +The module docstring lists one public callable: + +```python +import generic.conversions as conversions + +print(conversions.__doc__) # includes convert(*args, **kwargs) +``` + +The callable docstring lists every accepted signature: + +```python +print(conversions.convert.__doc__) +``` + +The relevant part is: + +```text +convert(*args, **kwargs) + +Supported Signatures +-------------------- +convert(value: int32) -> int32 +convert(value: float64) -> float64 +``` + +Private procedures such as `convert_integer` do not appear. + +--- -An edited contract can add an existing native procedure to a Python overload -set, even when it was not in the original Fortran interface. +## Extend an Overload Set -Suppose the contract already declares `convert_logical`. Add a public overload -declaration that links to it: +An edited contract can add another existing native procedure to the same +Python callable. Suppose the native module and contract also contain a public +`convert_logical`: ```python -from x2py.contracts import Bool, Int32, overload +from x2py.contracts import Bool, Int32, overload, private +@private def convert_logical(value: Bool) -> Int32: ... @overload("convert_logical") def convert(value: Bool) -> Int32: ... ``` -The decorator adds dispatch. It does not create a native implementation. The -target procedure must already exist in the contract and have a compatible call -shape. - -Leave `convert_logical` public to expose both names. Mark it `@private` when it -should only be available through `convert`. This changes Python visibility, -not the native call. The overload still calls the public native specific -directly. +The new declaration makes `convert(np.bool_(...))` select +`convert_logical`. It does not create the native procedure; that procedure +must already exist and match the declaration. `@private` means users reach the +procedure only through `convert`. -If that native specific is actually Fortran-private, the bridge cannot call it -directly. Keep `@bind("convert")` on the overload candidate. Source-based -generation adds this bind automatically. +If the concrete procedure is private in Fortran, keep +`@bind("convert")` on the overload so the native call goes through the public +generic. --- @@ -165,4 +195,6 @@ in Wrapping Derived Types. - Continue with [Wrapping Derived Types](wrapping-derived-types.md) for type-bound generics and operators - See [Error Handling](error-handling.md) for dispatch errors +- See [Editing Semantic `.pyi` Contracts](../reference/editing-semantic-pyi-contracts.md) + for advanced overload changes - For current generic and operator support, refer to the [Language Feature Matrix](../language-support/feature-matrix.md). diff --git a/docs/user/guide/index.md b/docs/user/guide/index.md index d48c095ee..373df1b8c 100644 --- a/docs/user/guide/index.md +++ b/docs/user/guide/index.md @@ -10,9 +10,9 @@ publication: reviewed # User Guide -This section builds on the [Getting Started](../getting-started/index.md) -workflow. It explains data type mapping, supported Fortran constructs, runtime -behavior, and how to build the shared library. +This section continues the [Getting Started](../getting-started/index.md) +workflow. Read it in sidebar order to move from basic values and procedures to +objects, storage, and advanced runtime behavior. --- @@ -62,8 +62,12 @@ behavior, and how to build the shared library. **Important Note** -The recommended workflow is **source-driven** (starting from `.f90` files). -Only move to editing semantic `.pyi` contracts after you understand the default generated behavior and are ready to manage native artifacts manually. +The recommended workflow starts from Fortran source. The generated semantic +`.pyi` file describes the Python interface and native call. Editing that file +lets you customize the wrapper without changing the native implementation. +This guide introduces useful edits on the pages where they matter. The +[editing reference](../reference/editing-semantic-pyi-contracts.md) collects +the complete rules in one place. --- diff --git a/docs/user/guide/memory-management.md b/docs/user/guide/memory-management.md index 34383e9fa..7b9a5a588 100644 --- a/docs/user/guide/memory-management.md +++ b/docs/user/guide/memory-management.md @@ -21,26 +21,11 @@ Two questions keep these cases simple: The answers are not always the same. -## Key Concepts - -- Ordinary Python values and independently created NumPy arrays have - Python-managed storage. -- An [allocatable](allocatables.md) or [pointer](pointers.md) handle is a Python - object that describes native array storage. Owning the handle does not always - mean owning the storage it refers to. -- Calling `to_numpy()` on an allocatable or pointer handle returns a live NumPy - view, not a copy. -- A live view can become invalid if native code reallocates, deallocates, or - changes the storage it refers to. -- Passing an object to a wrapped function does not transfer its ownership. -- Release storage only through the object or native API that owns it. - ---- - ## The Python Object And Its Storage -For example, an allocatable or pointer handle can refer to storage owned -somewhere else: +An [allocatable](allocatables.md) or [pointer](pointers.md) handle is a Python +object that describes native array storage. The storage can belong to the +handle itself, a Fortran module, a parent object, or a separate pointer target: ```python handle = api.values @@ -108,7 +93,7 @@ have the same ownership rules: | View current data | `to_numpy()` | `to_numpy()` | | Remove current storage | `deallocate()` releases the allocation when the operation is available | `deallocate()` releases only a target allocated through this pointer | | Stop referring to storage without releasing it | Not applicable | `nullify()` | -| End an owned handle | `close()` releases the descriptor and any remaining allocation | `close()` releases the descriptor, not the target | +| End a returned or caller-created handle | `close()` releases the descriptor and any remaining allocation | `close()` releases the descriptor, not the target | A pointer association does not by itself make the pointer responsible for the target. If a target was allocated through a pointer, call `deallocate()` before @@ -120,34 +105,46 @@ APIs and examples. --- -## Owned And Borrowed Handles +## Closing Handles -Allocatable and pointer handles can be owned or borrowed. Caller-created -handles and function-result handles own their descriptor storage. Their -`close()` method permanently ends the handle: +Caller-created handles and function-result handles have their own descriptor +storage. Their `close()` method permanently ends the handle: ```python result.close() assert result.closed ``` -Do not use an owned allocatable or pointer handle after calling `close()` on -it. Owned handles close automatically when Python finalizes them, so call -`close()` yourself only when the resource must be released immediately. +Do not use the handle after calling `close()` on it. These handles close +automatically when Python no longer uses them, so call `close()` yourself only +when the resource must be released immediately. -Module and derived-field handles are borrowed from their native owner. Calling -`close()` on a borrowed handle does nothing: it does not close the handle or -release the owner's storage. +Module and derived-field handles expose descriptors belonging to the Fortran +module or parent object. Calling `close()` on one does nothing: it does not +close the handle or release that storage. The resource released by `close()` depends on the handle: -- Closing an owned allocatable handle releases its descriptor and any +- Closing a returned or caller-created allocatable handle releases its descriptor and any allocation it still contains. -- Closing an owned pointer handle releases only its descriptor. Its target has +- Closing a returned or caller-created pointer handle releases only its descriptor. Its target has a separate lifetime. --- +## Sharing Handles Between Extensions + +The same allocatable or pointer handle can be passed between separately built +x2py extensions. Their matching arguments must have the same descriptor kind, +element type, and rank. + +The handoff does not copy array data. Both extensions must use compatible x2py +versions, the same Fortran compiler toolchain, and compatible Fortran +runtimes. An incompatible handle is rejected. Sharing a pointer handle does +not extend the lifetime of its target. + +--- + ## Passing Objects To Functions Passing an object to a wrapped function does not give the function ownership of @@ -170,8 +167,8 @@ before the call. Ask the handle for a new view afterward. A generated wrapper for a derived-type object can own a native instance. The wrapper releases that instance automatically when it is finalized. -Both plain and `Aliased` derived module variables remain live objects. The -Fortran module owns their storage, and Python only accesses it. +Derived module variables remain live objects. +The Fortran module owns their storage. Python only accesses it. A field returned from that object may refer to storage inside its parent. The generated field object keeps its parent alive, but it cannot stop native code @@ -194,14 +191,13 @@ fields, and function arguments. that pointer. - Call `nullify()` on a pointer handle to remove its association without destroying the target. -- Do not use an owned allocatable or pointer handle after `close()`. +- Do not use a returned or caller-created handle after `close()`. - Synchronize access when another thread may change the same native storage. --- ## Next -- Read [Allocatables](allocatables.md) for allocation and resizing. -- Read [Pointers](pointers.md) for association and target lifetime. -- Read [Wrapping Derived Types](wrapping-derived-types.md) for object and field - lifetimes. +- Continue with [Callbacks](callbacks.md). +- Return to [Allocatables](allocatables.md), [Pointers](pointers.md), or + [Wrapping Derived Types](wrapping-derived-types.md) for their full APIs. diff --git a/docs/user/guide/pointers.md b/docs/user/guide/pointers.md index 9cb4e4445..9cc1d02ca 100644 --- a/docs/user/guide/pointers.md +++ b/docs/user/guide/pointers.md @@ -2,7 +2,7 @@ title: Pointers description: How x2py handles Fortran `pointer` variables, results, fields, and descriptors audience: advanced users -prerequisites: arrays, memory management +prerequisites: arrays, allocatables related: allocatables.md, memory-management.md status: maintained publication: reviewed @@ -21,13 +21,13 @@ shape, and strides. It does not by itself say who owns that target. - Scalar pointers appear as `T | None`; array pointers use live `Pointer[T[...]]` handles. - `associated` describes association, not ownership or target lifetime. -- NumPy arrays returned by `to_numpy()` are borrowed live views, not copies. +- NumPy arrays returned by `to_numpy()` are live views, not copies. - Reassociation, resizing, or deallocation can invalidate existing views. - `associate(other)` makes two pointer handles refer to the same target without copying it. - Use `deallocate()` only if this pointer was used to create its current target with `allocate()`. Otherwise, use `nullify()`. -- `close()` releases an owned handle descriptor, not its target. +- `close()` releases a returned or caller-created descriptor, not its target. --- @@ -84,14 +84,14 @@ boundary as values rather than array handles. | `shape` | `tuple[int, ...] \| None` | Current target dimensions, or `None` when unassociated. | | `dtype` | `numpy.dtype` | Declared target element type. | | `rank` | `int` | Declared number of dimensions. | -| `to_numpy()` | `numpy.ndarray \| None` | A borrowed live target view, or `None` when unassociated. | +| `to_numpy()` | `numpy.ndarray \| None` | A live target view, or `None` when unassociated. | | `associate(other)` | `(PointerArray) -> None` | Makes this pointer's association match `other` without copying data. | | `nullify()` | `() -> None` | Removes the association without destroying the target. | | `allocate(shape)` | `(int \| Sequence[int]) -> None` | Creates and associates a target for an unassociated pointer. | | `deallocate()` | `() -> None` | Destroys the current target if this pointer was used to allocate it. | | `resize(shape)` | `(int \| Sequence[int]) -> None` | Replaces the current target when `deallocate()` is valid. | -| `close()` | `() -> None` | Permanently releases owned descriptor storage; it does not deallocate the target. It does nothing on a borrowed handle. | -| `closed` | `bool` | Whether an owned handle has been closed. | +| `close()` | `() -> None` | Permanently releases returned or caller-created descriptor storage; it does not deallocate the target. It does nothing on a module or field handle. | +| `closed` | `bool` | Whether a closable handle has been closed. | `associate()` and `nullify()` are available by default. A handle may also support allocation, target deallocation, resizing, and NumPy extraction. @@ -123,15 +123,15 @@ without a pointer that can release it. | --- | --- | --- | | `nullify()` | This descriptor's association. It does not destroy the target. | Open and usable, with `associated == False`. | | `deallocate()` | A target this pointer was used to allocate. | Open and usable, with `associated == False`. | -| `close()` | Owned descriptor storage. It does not destroy the target. | Permanently closed and unusable. | +| `close()` | This handle's descriptor storage. It does not destroy the target. | Permanently closed and unusable. | -Finalization closes owned descriptors automatically. Call `close()` -explicitly only when deterministic descriptor release matters. It never -destroys the pointer target because descriptor and target ownership are -separate. +Returned and caller-created descriptors close automatically when Python no +longer uses them. Call `close()` explicitly only when immediate descriptor +release matters. It never destroys the pointer target because the descriptor +and target have separate lifetimes. -Module and field pointer handles are borrowed. Calling `close()` on one is a -no-op that leaves the descriptor, target, and handle unchanged. +Calling `close()` on a module or field pointer handle does nothing. It leaves +the descriptor, target, and handle unchanged. --- @@ -157,7 +157,8 @@ print(p.shape) # reflects the new target ### Function Results A pointer-array function result becomes a returned `PointerArray`. The handle -owns persistent descriptor storage, not necessarily the target: +has persistent descriptor storage, but the target can belong to another +object: ```python p = api.selected_values(True) @@ -258,7 +259,7 @@ real(8), pointer :: selected(:) selected => storage(1:6:2) ``` -With descriptor-view extraction enabled, Python preserves that layout: +The NumPy view preserves that layout: ```python view = api.selected.to_numpy() @@ -345,7 +346,7 @@ unreachable. Use `p.deallocate()` first. Use `resize()` only in the same cases where `deallocate()` is valid. -### Nullifying One Pointer Does Not Change Other Aliases +### Nullifying One Pointer Does Not Change Other Pointers ```python first = api.first_pointer @@ -368,7 +369,7 @@ returned_pointer.close() returned_pointer.shape # NOT OK: the descriptor has been released ``` -`close()` releases an owned result descriptor but never deallocates its target. +`close()` releases a returned descriptor but never deallocates its target. An existing NumPy view may still refer to the target, but its safety now depends entirely on that target's separate owner and lifetime. Do not use the closed handle to reason about the view. diff --git a/docs/user/guide/raw-addresses.md b/docs/user/guide/raw-addresses.md index f311e6900..4929b19ec 100644 --- a/docs/user/guide/raw-addresses.md +++ b/docs/user/guide/raw-addresses.md @@ -21,15 +21,28 @@ scalar storage, arrays, and strings for normal wrappers. | Contract | Python argument | Validation | | --- | --- | --- | | `Int32[()]` | 0-D array with dtype `np.int32` | Dtype, rank, and writeability | -| `Addr(Int32)` | Integer address | Pointer-sized integer only | +| `Addr(Int32)` | Integer address | Integer that fits a native address | | `Float64[rows, columns]` | NumPy array | Dtype, shape, order, and writeability | -| `Addr(Float64[rows, columns])` | Integer address | Extent expressions only | +| `Addr(Float64[rows, columns])` | Integer address | Declared array sizes only | | `String[8][()]` | 0-D NumPy bytes array with dtype `S8` | Dtype, length, and writeability | | `Addr(String[8])` | Integer address | Declared fixed length only | `T[()]` changes the Python storage representation, not the native primitive datatype. It is usually the better choice for scalar mutation. +## `Addr(T)` And `Addr(Arg(...))` + +These spellings describe different boundaries: + +- `Addr(T)` means the Python caller passes an integer address. +- Inside `@native_call(...)`, `Arg(i)` selects Python argument `i`, and + `Addr(Arg(i))` tells x2py to pass that converted scalar by address. + +The `@native_call(...)` decorator records how Python arguments are placed in +the native call. Arrays, rank-zero storage, strings, and raw addresses already +use storage addresses, so their `Arg(i)` entry does not need another +`Addr(...)`. + ## Complete Example Create `raw_api.f90`: @@ -173,22 +186,12 @@ Use `String[8][()]` when the wrapper should validate mutable storage. - Use writable memory when native code may modify it. - Treat address zero as null only when the native routine allows null. -x2py cannot validate the pointee's lifetime, dtype, size, shape, order, +x2py cannot validate the addressed memory's lifetime, dtype, size, shape, order, alignment, ownership, or writeability. A wrong address can crash the process. -## `Addr(T)` And `Addr(Arg(...))` - -These spellings describe different boundaries: - -- `Addr(T)` means the Python caller passes an integer address. -- `Addr(Arg(i))` means x2py takes the address of a converted scalar argument. - -Arrays, rank-zero storage, strings, and raw addresses already use storage -addresses. Do not wrap their `Arg(i)` projection in another `Addr(...)`. - ## Next -- [Strings](strings.md) for immutable and checked mutable character boundaries +- Continue with [Error Handling](error-handling.md). - [Editing Semantic `.pyi` Contracts](../reference/editing-semantic-pyi-contracts.md) - [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) for complete - `Addr(...)` validation rules + `Addr(...)` validation rules. diff --git a/docs/user/guide/strings.md b/docs/user/guide/strings.md index 1ee1667c6..7b9c97367 100644 --- a/docs/user/guide/strings.md +++ b/docs/user/guide/strings.md @@ -28,6 +28,7 @@ caller-owned storage. Use normal string and NumPy contracts by default. Raw addresses are an advanced boundary covered later in the guide. +`Returns[...]` tells the wrapper to return the changed value of an argument. ## Complete Example @@ -72,10 +73,12 @@ Generate a starter contract: python3 -m x2py generate --pyi strings_api.f90 --out contracts/strings ``` -Edit `contracts/strings/strings_api.pyi` to use these Python boundaries: +Edit the declarations in `contracts/strings/strings_api.pyi` to use these +Python boundaries. Keep the other generated decorators and native-call +metadata unchanged: ```python -from x2py.contracts import Addr, Arg, Int32, Returns, String, native_call +from x2py.contracts import Int32, Returns, String def edit_text(text: String[8]) -> Returns["text", String[8]]: ... @@ -83,7 +86,6 @@ def edit_buffer(text: String[8][()]) -> None: ... def make_text() -> String[8]: ... -@native_call([Addr(Arg(0)), Arg(1)]) def edit_labels( count: Int32, labels: String[8][count], @@ -178,9 +180,10 @@ buffer or an immutable replacement result. ## Next -- [Wrapping Functions](wrapping-functions.md) +- Continue with [Wrapping Functions](wrapping-functions.md). - [Wrapping Subroutines](wrapping-subroutines.md) for complete `intent` and - result-projection rules -- [Raw Addresses](raw-addresses.md) for `Addr(String[n])` + result-projection rules. +- [Raw Addresses](raw-addresses.md) for the advanced `Addr(String[n])` + boundary. - [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) for the full - character grammar + character grammar. diff --git a/docs/user/guide/wrapping-derived-types.md b/docs/user/guide/wrapping-derived-types.md index 2896e1125..cc908093b 100644 --- a/docs/user/guide/wrapping-derived-types.md +++ b/docs/user/guide/wrapping-derived-types.md @@ -11,12 +11,12 @@ publication: reviewed # Wrapping Derived Types A supported Fortran `type` becomes a **generated Python extension class**. -Constructors and ordinary function results create wrapper-owned Fortran -instances. -Nested components and module-owned objects use borrowed or native-owned -instances of the same generated class. Python accesses fields through generated -getters and setters. Methods call wrapped Fortran procedures. Python never reads -the native memory layout directly. +Constructors and ordinary function results create independent Fortran +instances that are released with their Python objects. A nested component +belongs to its parent, and a module object belongs to the Fortran module. +Python accesses fields through generated getters and setters. Methods call +wrapped Fortran procedures. Python never reads the native memory layout +directly. --- @@ -74,6 +74,7 @@ python3 -m x2py points.f90 --out geometry --out-dir build/geometry ```python import sys + import numpy as np sys.path.insert(0, "build/geometry") @@ -98,29 +99,64 @@ print(container.origin.x) # 12.0 --- +## Inspect the Class + +The class docstring gives a short index: + +```python +print(points.point.__doc__) +``` + +```text +point + +Opaque wrapper for native type point. + +Constructor +----------- +point(*, x=0.0, y=0.0) -> point + +Fields +------ +x : float64 +y : float64 +``` + +The constructor has its own detailed docstring: + +```python +print(points.point.__init__.__doc__) +``` + +--- + ## Key Concepts -- **Ownership**: Wrapper-owned objects are finalized when the Python object is garbage-collected. +- **Lifetime**: A constructed or returned object is released when its Python + object is no longer used. A nested component stays tied to its parent. - **Mutation**: `intent(out)` and `intent(inout)` modify a caller-provided instance and do not return it again. - **Missing intent**: A dummy without `intent` follows the same conservative in-place rule as `intent(inout)`. - **Fields**: Public scalar numeric/logical/complex fields become Python attributes. -- **Nested types**: Appear as borrowed child wrappers (they don’t own the memory). -- **Results**: Derived-type function results create new wrapper-owned objects. +- **Nested types**: Appear as generated objects tied to their parent. +- **Results**: Derived-type function results create new independent objects. - **Default constructor**: Automatically generated from public, writable primitive scalar fields. -- **Default arguments**: Keyword-only (`logical`, `integer`, `real`, and +- **Constructor fields**: Passed by keyword (`logical`, `integer`, `real`, and `complex`). -- **Custom constructor**: Define `__init__` in the edited `.pyi` to call one - native initializer. --- ## Custom Constructor -The default constructor assigns fields directly. An edited `.pyi` can replace -it with a native initializer: +The default constructor assigns public fields directly. If the native module +already provides `initialize_point(item, x, y)`, an edited contract can use it +as the constructor. + +In this mapping, `@bind` selects the native initializer, +`@native_call(...)` gives its argument order, `Pass()` inserts the new +`point`, and `Addr(Arg(i))` passes Python argument `i` by address: ```python from x2py.contracts import Addr, Arg, Float64, Pass, bind, native_call @@ -130,18 +166,16 @@ class point: y: Float64 @bind("initialize_point") - @native_call([Addr(Arg(0)), Pass(), Addr(Arg(1))]) + @native_call([Pass(), Addr(Arg(0)), Addr(Arg(1))]) def __init__(self, x: Float64, y: Float64) -> None: ... ``` -`__init__` and `initialize_point` have different names, so -`@bind("initialize_point")` selects the initializer. `Pass()` marks the new -`point` at zero-based native position 1. That dummy must accept `point`. -Exactly one `Pass()` is required. Other `point` arguments use `Arg(...)` like -ordinary constructor inputs. +Replace the generated field-keyword `__init__` declaration with this one. +The edit changes construction only; it does not create +`initialize_point` in the native module. -The generated module-level function can remain public or be marked `@private`. -The custom declaration replaces only the default field constructor. +After rebuilding, `points.point.__init__.__doc__` starts with +`point(x, y) -> point` and lists both parameters. --- @@ -175,32 +209,38 @@ print(item.value) # 7 The method mutates the existing `counter`; it does not replace the Python object. -### Project A Module Procedure As A Method +### Expose a Module Procedure as a Method + +The `move(item, dx, dy)` procedure from this page's example can remain a +module-level function and also become `point.move(dx, dy)`. -An edited `.pyi` can expose one native procedure in both Python scopes: +`Pass()` supplies `self` to the native call. `Arg(i)` refers to a visible +Python argument. Add the method to the existing `point` class while keeping +the module declaration: ```python from x2py.contracts import Addr, Arg, Float64, Pass, native_call class point: - @native_call([Pass(), Addr(Arg(0))]) - def move_point(self, dx: Float64) -> None: ... + @native_call([Pass(), Addr(Arg(0)), Addr(Arg(1))]) + def move(self, dx: Float64, dy: Float64) -> None: ... -@native_call([Arg(0), Addr(Arg(1))]) -def move_point(item: point, dx: Float64) -> None: ... +@native_call([Arg(0), Addr(Arg(1)), Addr(Arg(2))]) +def move(item: point, dx: Float64, dy: Float64) -> None: ... ``` -Both calls reach native `move_point`: +Both declarations call the existing native `move` procedure: ```python -move_point(item, np.float64(2.0)) -item.move_point(np.float64(2.0)) +points.move(item, np.float64(2.0), np.float64(3.0)) +item.move(np.float64(2.0), np.float64(3.0)) ``` -`Pass()` inserts `item` for the method call. Both declarations already match -native `move_point`, so no `@bind` is needed. +To expose only the method, import `private` and add `@private` to the +module-level declaration. -Mark the module declaration `@private` to expose only the method. +The class docstring now lists `move(dx, dy) -> None` under `Methods`. +`points.point.move.__doc__` contains its complete parameter and return details. --- @@ -296,6 +336,22 @@ total = left + right print(total.x, total.y) # 4.0 6.0 ``` +The magic method docstring shows the accepted operator signatures: + +```python +print(points.point.__add__.__doc__) +``` + +The relevant part is: + +```text +__add__(*args, **kwargs) + +Supported Signatures +-------------------- +__add__(right: point) -> point +``` + | Fortran generic | Python method | Python syntax | |-----------------|---------------|---------------| | Binary `+`, `-`, `*`, `/`, `**` | Direct and reflected magic methods | `left + right` | @@ -317,9 +373,8 @@ exact. ## Next -- Continue with [Allocatables](allocatables.md) and [Pointers](pointers.md) for - advanced storage. -- Review [Memory Management](memory-management.md) before keeping borrowed - objects or views. - ---- +- Continue with [Allocatables](allocatables.md). +- Read [Memory Management](memory-management.md) for the lifetime of native + storage and NumPy views. +- See [Replacing the Generated Constructor](../reference/editing-semantic-pyi-contracts.md#replace-the-generated-constructor) + for the advanced constructor contract. diff --git a/docs/user/guide/wrapping-functions.md b/docs/user/guide/wrapping-functions.md index bdb83af31..ad9224b8f 100644 --- a/docs/user/guide/wrapping-functions.md +++ b/docs/user/guide/wrapping-functions.md @@ -11,40 +11,20 @@ publication: reviewed # Wrapping Functions A Fortran `function` becomes a Python callable. Its direct result is the first -Python return value. Other outputs follow only when their contract projects -them as results. +Python return value. Other outputs follow only when their contract marks them +as Python results. --- ## Basic Scalar Function -Using the `scale.f90` example: - -```bash -python3 -m x2py generate --pyi scale.f90 -python3 -m x2py scale.f90 --out-dir build/scale -``` - -**Generated contract:** - -```python -from x2py.contracts import Addr, Arg, Float64, external, native_call - -@external -@native_call([Addr(Arg(0)), Addr(Arg(1))]) -def scale( - value: Float64, - factor: Float64 -) -> Float64: ... -``` - -**Python call:** +The `scale` function built in +[First Wrapped Function](../getting-started/first-wrapped-function.md) returns +its direct result as one NumPy scalar: ```python -import sys import numpy as np -sys.path.insert(0, "build/scale") import scale result = scale.scale(np.float64(3.0), np.float64(2.5)) @@ -58,7 +38,9 @@ print(result) # 7.5 A contract declaration normally uses one name for both Python and the native procedure. Use `@bind("native_name")` only when those names differ. -This edited contract exposes Python `multiply` while calling native `scale`: +For example, rename the generated declaration to `multiply` and add +`@bind("scale")`. The Python name changes, while the native target remains +`scale`: ```python from x2py.contracts import Addr, Arg, Float64, bind, external, native_call @@ -81,47 +63,24 @@ print(result) # 7.5 contract or adapt an incompatible native interface. Matching names need no `@bind`. +Also update the import in the contract package's `__init__.pyi` when it +re-exports the old Python name. Build the edited package using the +[editable-contract workflow](../getting-started/beginner-workflow.md#4-optionally-edit-the-contract). + The same rule applies to functions, subroutines, and methods. --- ## Array Return Values -Functions can return arrays. These are returned as new NumPy arrays (Fortran-ordered by default). - -**Example** (`function_results.f90`): - -```fortran -module results - implicit none -contains - - function squares(count) result(values) - integer(4), intent(in) :: count - real(8) :: values(count) - integer(4) :: i - - values = [(real(i, 8)**2, i = 1, count)] - end function squares - -end module results -``` - -Build and import this example: - -```bash -python3 -m x2py function_results.f90 --out-dir build/function-results -``` +Functions can return arrays. An ordinary array result becomes a new NumPy +array in Fortran order, as described in +the [`automatic_vector` example](arrays.md#complete-example): ```python -import sys - import numpy as np -sys.path.insert(0, "build/function-results") -from function_results.results import squares - -result = squares(np.int32(4)) +result = automatic_vector(np.int32(4)) print(result) # [ 1. 4. 9. 16.] ``` @@ -172,5 +131,4 @@ total, count = sum_with_count(data_array) ## Next - [Wrapping Subroutines](wrapping-subroutines.md) for the complete argument - projection rules -- [Wrapping Modules](wrapping-modules.md) for native namespaces and state + projection rules. diff --git a/docs/user/guide/wrapping-modules.md b/docs/user/guide/wrapping-modules.md index 8d28bfd0e..a4e9e36d8 100644 --- a/docs/user/guide/wrapping-modules.md +++ b/docs/user/guide/wrapping-modules.md @@ -20,6 +20,7 @@ After building `module_state.f90`: ```python import sys + import numpy as np sys.path.insert(0, "build/first-module") @@ -59,7 +60,8 @@ print(mod.summarize()) # 21 print(mod.nmax) # 12 (read-only parameter) ``` -- `parameter` declarations become `Final[...]` constants in the generated contract. +- `parameter` declarations become read-only constants in the generated + contract. - Assigning to a constant in Python only creates a local shadow — it does **not** mutate the native value. --- @@ -74,6 +76,96 @@ print(mod.nmax) # 12 (read-only parameter) --- +## Shape the Module API With the Contract + +Small contract edits can set initial values or hide names from Python: + +```python +from x2py.contracts import Final, Float64, Int32, private + +nmax: Final[Int32] = 12 +counter: Int32 = 9 +scale: Float64 = 2.0 +saved_counter: private[Int32] + +@private +def scaled_counter() -> Float64: ... +``` + +- `counter` and `scale` are set in the Fortran module when the extension is + imported. They remain writable. +- `private[T]` hides a module variable; `@private` hides a procedure. Both + still exist in Fortran. +- `Final[T] = value` is only for a true constant, such as a Fortran + `parameter`. It does not turn a writable Fortran variable into a read-only + view. + +Deleting a declaration removes that name from the generated Python API. These +edits do not create or rename native variables and procedures; those still +need to exist in the compiled module. + +--- + +## Flatten Module Namespaces + +The package entry `__init__.pyi` controls the Python import layout. Suppose an +extension named `library` contains two Fortran modules. x2py generates: + +```python +# __init__.pyi +from . import module1 +from . import module2 +``` + +The modules remain child namespaces: + +```python +from library.module1 import func1 +from library.module2 import func2 +``` + +To place every public name directly on `library`, replace those imports with +wildcard imports: + +```python +# __init__.pyi +from .module1 import * +from .module2 import * +``` + +Python then uses the flattened API: + +```python +import library + +library.func1() +library.func2() + +# This is also valid: +from library import func1, func2 +``` + +Public functions, variables, constants, and generated classes are exported at +the extension root. If the original module imports were replaced, +`library.module1` and `library.module2` are no longer exported. The native +Fortran modules and their storage do not move; only the Python API changes. + +Wildcard imports never use import order to resolve a collision. If both +modules export the same name, the wrapper build fails and asks for an explicit +choice. Export aliases instead: + +```python +from .module1 import update as update_module1 +from .module2 import update as update_module2 +``` + +This produces `library.update_module1` and `library.update_module2`. You can +also import only selected names instead of flattening every public declaration. +Build the edited entry using the +[editable-contract workflow](../getting-started/beginner-workflow.md#4-optionally-edit-the-contract). + +--- + ## Important Rules - Private declarations are hidden from the Python API. @@ -85,8 +177,8 @@ print(mod.nmax) # 12 (read-only parameter) ## Next -- Learn about [Memory Management](memory-management.md) — especially important when working with module state -- See [Optional Arguments](optional-arguments.md) -- See [Building the Shared Library](building-shared-library.md) +- Continue with [Optional Arguments](optional-arguments.md). +- Read [Memory Management](memory-management.md) before keeping live views of + module storage. - Check the [Language Feature Matrix](../language-support/feature-matrix.md) - for supported module features and limitations + for supported module features and limitations. diff --git a/docs/user/guide/wrapping-subroutines.md b/docs/user/guide/wrapping-subroutines.md index 2b7b49cde..ae1d92883 100644 --- a/docs/user/guide/wrapping-subroutines.md +++ b/docs/user/guide/wrapping-subroutines.md @@ -10,13 +10,13 @@ publication: reviewed # Wrapping Subroutines -A Fortran `subroutine` has no direct return value. Projected scalar and -native-created outputs form the Python result. Caller-provided mutable objects +A Fortran `subroutine` has no direct return value. Scalar outputs and objects +created by Fortran form the Python result. Caller-provided mutable objects change in place. --- -## Argument Projection Rules +## How Arguments Become Python Results | Native Argument | Python Call | Python Result | |-----------------------------|------------------------------|-----------------------------------| @@ -85,6 +85,7 @@ python3 -m x2py outputs.f90 --out-dir build/outputs ```python import sys + import numpy as np sys.path.insert(0, "build/outputs") @@ -130,5 +131,7 @@ print(target) # [1. 1. 1. 1.] ## Next -- Continue with [Optional Arguments](optional-arguments.md) or [Wrapping Modules](wrapping-modules.md). +- Continue with [Wrapping Modules](wrapping-modules.md). +- Then read [Optional Arguments](optional-arguments.md) to control whether a + native argument is present. - For advanced memory management, see [Allocatables](allocatables.md) and [Pointers](pointers.md). diff --git a/docs/user/reference/fortran-wrapper.md b/docs/user/reference/fortran-wrapper.md index 2ca6b7c61..d3a58aacf 100644 --- a/docs/user/reference/fortran-wrapper.md +++ b/docs/user/reference/fortran-wrapper.md @@ -959,7 +959,7 @@ hidden outputs, ordered tuple results, optional omission versus a present `None`, constrained array shape and layout, handle ownership, and native-status exceptions. -Module docstrings index their public functions, module attributes, and classes. +Module docstrings index their public attributes, functions, and classes. Class docstrings index the public constructor, fields, methods, and overloads; the individual constructor, method, overload, and property descriptors also carry focused docstrings. Private wrapper helper names and internal bridge roles diff --git a/tests/docs/test_structure.py b/tests/docs/test_structure.py index 792c87154..f3138c617 100644 --- a/tests/docs/test_structure.py +++ b/tests/docs/test_structure.py @@ -1084,8 +1084,13 @@ def test_first_wrapped_function_shows_contract_and_mentions_later_support_bounda "@external\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") + call_index = page.index("## Call the Function") - assert source_index < build_index < command_index < contract_index + assert source_index < command_index < contract_index < build_index + assert build_index < docstring_index < call_index + assert "editable description of the\nPython interface" in page + assert "scale(value, factor) -> float64" in page assert "assert result == 7.5" in page assert "isinstance(result, float)" not in page assert "## Current Limitations" not in page @@ -1095,10 +1100,22 @@ def test_first_wrapped_module_shows_local_input_and_generated_contract() -> None page = (DOCS_ROOT / "user/getting-started/first-wrapped-module.md").read_text(encoding="utf-8") source_index = page.index("module_state.f90") build_index = page.index("python3 -m x2py module_state.f90") + docstring_index = page.index("## Inspect the Generated Docstring") + usage_index = page.index("## Usage Example") inspect_index = page.index("python3 -m x2py generate --pyi module_state.f90") contract_index = page.index("## Key Rules") - assert source_index < build_index < inspect_index < contract_index + assert source_index < build_index < docstring_index < usage_index < inspect_index < contract_index + assert "print(mod.__doc__)" in page + assert "module_state\n\nModule Attributes" in page + assert "summarize() -> int32" in page + assert "scaled_counter() -> float64" in page + assert "next_local() -> int32" in page + assert "nmax : int32\n Read-only constant." in page + assert "counter : int32" in page + assert "scale : float64" in page + assert "saved_counter : int32" in page + assert "Assignment writes through to native storage." not in page assert "fmodule_vars_f90" not in page assert "## Current Limitations" not in page @@ -1110,12 +1127,70 @@ def test_beginner_workflow_reuses_scale_example_without_renaming_it() -> None: contract_index = page.index("python3 -m x2py generate --pyi src/scale.f90") build_index = page.index("python3 -m x2py src/scale.f90") smoke_index = page.index("result = scale.scale(np.float64(3.0), np.float64(2.5))") - advanced_index = page.index("Editing the Semantic Contract") + editing_index = page.index("## 4. Optionally Edit the Contract") + edited_contract_index = page.index("contracts/scale/__init__.pyi", editing_index) + diagnosis_index = page.index("## 5. Diagnose a Failure") - assert source_reference_index < layout_index < contract_index < build_index < smoke_index < advanced_index + assert source_reference_index < layout_index < contract_index < build_index + assert build_index < smoke_index < editing_index < edited_contract_index < diagnosis_index assert "scale_api" not in page +def test_user_guide_teaches_small_contract_edits_in_context() -> None: + arrays = _visible_documentation_source(DOCS_ROOT / "user/guide/arrays.md") + strings = _visible_documentation_source(DOCS_ROOT / "user/guide/strings.md") + functions = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-functions.md") + modules = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-modules.md") + generics = _visible_documentation_source(DOCS_ROOT / "user/guide/generic-interfaces.md") + derived = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-derived-types.md") + errors = _visible_documentation_source(DOCS_ROOT / "user/guide/error-handling.md") + + assert "Edit the semantic `.pyi` and add `ORDER_C`" in arrays + assert "Edit the declarations in `contracts/strings/strings_api.pyi`" in strings + assert '@bind("scale")' in functions + assert "## Shape the Module API With the Contract" in modules + assert "nmax: Final[Int32] = 12" in modules + assert "counter: Int32 = 9" in modules + assert "scale: Float64 = 2.0" in modules + assert "saved_counter: private[Int32]" in modules + assert "It does not turn a writable Fortran variable into a read-only" in modules + assert "## Flatten Module Namespaces" in modules + assert "from .module1 import *" in modules + assert "from .module2 import *" in modules + assert "library.func1()" in modules + assert "library.module1` and `library.module2` are no longer exported" in modules + assert "the wrapper build fails and asks for an explicit" in modules + assert "## Extend an Overload Set" in generics + assert '@overload("convert_logical")' in generics + assert "## Custom Constructor" in derived + assert '@bind("initialize_point")' in derived + assert "### Expose a Module Procedure as a Method" in derived + assert "def move(self, dx: Float64, dy: Float64)" in derived + assert '@raises(status="status", message="message", success=0)' in errors + + +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") + derived = _visible_documentation_source(DOCS_ROOT / "user/guide/wrapping-derived-types.md") + + assert "## Inspect the Module" not in modules + assert "print(mod.__doc__)" not in modules + + assert "## Inspect the Overloads" in generics + assert "print(conversions.__doc__)" in generics + assert "print(conversions.convert.__doc__)" in generics + assert "convert(value: int32) -> int32" in generics + assert "convert(value: float64) -> float64" in generics + + assert "## Inspect the Class" in derived + assert "print(points.point.__doc__)" in derived + assert "print(points.point.__init__.__doc__)" in derived + assert "`points.point.move.__doc__`" in derived + assert "print(points.point.__add__.__doc__)" in derived + assert "__add__(right: point) -> point" in derived + + def test_getting_started_pages_keep_advanced_stage_flags_out_of_beginner_path() -> None: content = "\n".join( _visible_documentation_source(DOCS_ROOT / relative_path) for relative_path in REQUIRED_GETTING_STARTED_PAGES @@ -1163,11 +1238,11 @@ def test_array_handle_docs_keep_views_copies_and_handles_distinct() -> None: assert "never creates an automatic detached snapshot" in allocatables assert "A NumPy view reflects current native storage." in allocatables assert "A pointer-array function result becomes a returned `PointerArray`." in pointers - assert "owns persistent descriptor storage, not necessarily the target" in pointers + assert "has persistent descriptor storage, but the target can belong to another" in pointers assert "`associate(other)` makes two pointer handles refer to the same target" in pointers assert "If `p2` is unassociated, `p1` becomes" in pointers assert "Do Not Return A Pointer To Expired Local Storage" in pointers - assert "plain and `Aliased` derived module variables remain live objects" in memory + assert "Derived module variables remain live objects" in memory assert "Fortran module owns their storage" in memory diff --git a/tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py b/tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py index 0a84925f7..4d33f9599 100644 --- a/tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py +++ b/tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py @@ -607,13 +607,16 @@ def test_mutable_module_variable_default_initializes_native_storage(tmp_path: Pa root = _generate_pyi(MODULE_VARIABLE_SOURCE, tmp_path / "contracts", MODULE_VARIABLES_GENERATED) leaf = root.parent / "fmodule_vars_f90.pyi" leaf.write_text( - leaf.read_text(encoding="utf-8").replace("counter: Int32", "counter: Int32 = 41"), + leaf.read_text(encoding="utf-8") + .replace("counter: Int32", "counter: Int32 = 41") + .replace("scale: Float64", "scale: Float64 = 2.5"), encoding="utf-8", ) root.write_text( "\n".join( [ "from .fmodule_vars_f90 import counter", + "from .fmodule_vars_f90 import scale", "from .fmodule_vars_f90 import summarize", "", ] @@ -625,9 +628,12 @@ def test_mutable_module_variable_default_initializes_native_storage(tmp_path: Pa module, _payload = _build_pyi_cli(root, native_object, tmp_path / "pyi_build") assert module.counter == np.int32(41) + assert module.scale == np.float64(2.5) assert module.summarize() == np.int32(53) module.counter = np.int32(5) + module.scale = np.float64(1.25) assert module.summarize() == np.int32(17) + assert module.scale == np.float64(1.25) def test_entry_rejects_colliding_wildcard_exports(tmp_path: Path): diff --git a/tests/wrapper/fortran/module_state/test_module_state.py b/tests/wrapper/fortran/module_state/test_module_state.py index 350005493..0e1e67d4f 100644 --- a/tests/wrapper/fortran/module_state/test_module_state.py +++ b/tests/wrapper/fortran/module_state/test_module_state.py @@ -41,6 +41,17 @@ def test_scalar_module_variables_use_attributes_and_parameters_have_no_native_se pyi_parity_build_mode, ) + module_docstring = module.__doc__ + assert module_docstring.startswith("fmodule_vars_f90\n\nModule Attributes") + assert "fmodule_vars_f90.fmodule_vars_f90" not in module_docstring + assert module_docstring.index("Module Attributes") < module_docstring.index("Functions") + assert module_docstring.index("Functions") < module_docstring.index("Classes") + assert "nmax : int32\n Read-only constant." in module_docstring + assert "counter : int32" in module_docstring + assert "scale : float64" in module_docstring + assert "saved_counter : int32" in module_docstring + assert "Assignment writes through to native storage." not in module_docstring + assert module.nmax == np.int32(12) assert isinstance(module.black, module.rgb_color) assert module.black.r == np.int32(0) diff --git a/tests/wrapper/fortran/naming/test_defined_operators.py b/tests/wrapper/fortran/naming/test_defined_operators.py index 027f266b8..1c09a8fb3 100644 --- a/tests/wrapper/fortran/naming/test_defined_operators.py +++ b/tests/wrapper/fortran/naming/test_defined_operators.py @@ -44,6 +44,10 @@ def offset(value): left = vector(5.0) right = vector(2.0) + assert "__add__(*args, **kwargs)" in module.vector.__doc__ + assert "__add__(right: vector) -> vector" in module.vector.__add__.__doc__ + assert "add_vectors" not in module.vector.__add__.__doc__ + assert module.convert(np.int32(2)) == np.int32(12) assert module.convert(np.float64(2.0)) == np.float64(2.5) assert (left + right).value == np.float64(7.0) diff --git a/x2py/wrapper_codegen/docstrings.py b/x2py/wrapper_codegen/docstrings.py index 68e637eff..db4b49340 100644 --- a/x2py/wrapper_codegen/docstrings.py +++ b/x2py/wrapper_codegen/docstrings.py @@ -56,18 +56,18 @@ def namespace( overloads: tuple[OverloadPlan, ...], ) -> str: """Index every public owner in one generated Python namespace.""" - qualified_name = ".".join((module_name, *path)) - lines = [qualified_name, "", f"Generated Python interface for native namespace {qualified_name}."] + display_name = path[-1] if path else module_name + lines = [display_name] callable_lines = ( *(self._first_line(function.binding.docstring) for function in functions if function.binding.public), *(self._first_line(overload.docstring) for overload in overloads), ) - self._append_section(lines, "Functions", callable_lines) self._append_section( lines, "Module Attributes", tuple(line for variable in variables for line in self._module_variable_summary_lines(variable)), ) + self._append_section(lines, "Functions", callable_lines) self._append_section(lines, "Classes", tuple(name for surface in classes for name in surface.python_names)) return "\n".join(lines) @@ -264,16 +264,14 @@ def module_variable(self, variable: ModuleVariablePlan) -> str: lines = [f"{name} : {self._type(variable, nullable=nullable, signature=False)}"] lines.extend(self._array_lines(variable.array)) if variable.binding.getter_action is ModuleGetterAction.CONSTANT_VALUE: - lines.append(" Read-only native constant.") + lines.append(" Read-only constant.") elif variable.binding.getter_action is ModuleGetterAction.BORROWED_ARRAY_VIEW: lines.append(" Native-owned borrowed view; mutations affect module storage.") elif variable.native_array_handle is not None: lines.append(f" Persistent {variable.native_array_handle.descriptor_kind.value} descriptor handle.") elif variable.derived is not None: lines.append(" Live native module object.") - if variable.binding.setter_action is SetterAction.WRITE_THROUGH: - lines.append(" Assignment writes through to native storage.") - elif variable.binding.setter_action is SetterAction.REJECT_REPLACEMENT: + if variable.binding.setter_action is SetterAction.REJECT_REPLACEMENT: lines.append(" Replacement assignment is not supported.") return "\n".join(lines) From d9e33309552c49516d3aa1f0c294c6a55e6ce6cf Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Tue, 28 Jul 2026 18:27:52 +0100 Subject: [PATCH 31/35] update docs --- README.md | 1 - .../contributing/coding-standards.md | 17 -- docs/developer/contributing/index.md | 2 +- docs/developer/development-workflow.md | 60 ++-- docs/developer/feature-to-code-map.md | 6 +- docs/developer/source-map.md | 6 +- .../documentation-content-checklist.md | 9 +- docs/maintainer/roadmap/index.md | 2 +- docs/old_docs/examples.md | 2 +- docs/old_docs/tutorial.md | 2 +- docs/user/examples/index.md | 34 +-- .../examples/recipes/build-and-import-cli.md | 68 ----- .../recipes/build-and-import-python-api.md | 2 +- .../recipes/build-multiple-fortran-sources.md | 69 ----- .../recipes/compiler-preprocessing.md | 2 +- .../examples/recipes/control-cli-output.md | 2 +- .../recipes/generate-editable-makefile.md | 75 ----- docs/user/examples/recipes/inspect-c-api.md | 2 +- .../examples/recipes/inspect-fortran-api.md | 2 +- .../recipes/semantic-pyi-contracts.md | 2 +- .../recipes/use-python-inspection-apis.md | 2 +- docs/user/examples/verified-cookbook.md | 69 ----- .../user/getting-started/beginner-workflow.md | 6 +- .../getting-started/first-wrapped-function.md | 2 +- .../getting-started/first-wrapped-module.md | 3 +- docs/user/getting-started/installation.md | 3 +- docs/user/getting-started/verification.md | 3 +- docs/user/guide/arrays.md | 2 - docs/user/guide/building-shared-library.md | 39 ++- docs/user/guide/callbacks.md | 3 +- docs/user/guide/data-types.md | 4 +- docs/user/guide/enumerations.md | 3 +- docs/user/guide/error-handling.md | 5 +- docs/user/guide/generic-interfaces.md | 3 - docs/user/guide/index.md | 9 +- docs/user/guide/raw-addresses.md | 5 +- docs/user/guide/strings.md | 4 +- docs/user/guide/wrapping-derived-types.md | 4 +- docs/user/guide/wrapping-functions.md | 2 +- docs/user/guide/wrapping-modules.md | 2 - docs/user/language-support/feature-matrix.md | 10 +- docs/user/language-support/index.md | 8 - .../partially-supported-features.md | 20 -- .../user/language-support/planned-features.md | 16 -- .../language-support/supported-features.md | 18 -- .../language-support/unsupported-features.md | 17 -- docs/user/reference/callbacks.md | 119 -------- docs/user/reference/index.md | 1 - docs/user/tutorials/basic-wrapper.md | 257 ------------------ docs/user/tutorials/index.md | 16 +- mkdocs.yml | 11 - tests/docs/test_structure.py | 9 - 52 files changed, 129 insertions(+), 911 deletions(-) delete mode 100644 docs/developer/contributing/coding-standards.md delete mode 100644 docs/user/examples/recipes/build-and-import-cli.md delete mode 100644 docs/user/examples/recipes/build-multiple-fortran-sources.md delete mode 100644 docs/user/examples/recipes/generate-editable-makefile.md delete mode 100644 docs/user/examples/verified-cookbook.md delete mode 100644 docs/user/language-support/partially-supported-features.md delete mode 100644 docs/user/language-support/planned-features.md delete mode 100644 docs/user/language-support/supported-features.md delete mode 100644 docs/user/language-support/unsupported-features.md delete mode 100644 docs/user/reference/callbacks.md delete mode 100644 docs/user/tutorials/basic-wrapper.md diff --git a/README.md b/README.md index f3e822f7a..9d8a9aa0f 100644 --- a/README.md +++ b/README.md @@ -558,7 +558,6 @@ ownership, callback lifetime, ABI shims, or Python-visible projections. - **[User Guide](docs/user/guide/index.md)** — Detailed guidance on data types, functions, subroutines, modules, arrays, derived types, callbacks, ownership, runtime behavior, and packaging @@ -55,7 +56,7 @@ For example, a new CLI stage option normally requires: 1. A focused contract test in `tests/cli/`. 2. Dispatch or output routing in `x2py/cli.py`. 3. Preprocessing tests if the option changes source loading. -4. A copy-paste command in [Verified examples cookbook](../user/examples/verified-cookbook.md). +4. A copy-paste command in the relevant user guide or checked example. 5. A tutorial update only when the main user workflow changes. ## Support Evidence Rule @@ -73,8 +74,8 @@ Use these documentation roles consistently: | Document | Role | | --- | --- | -| [Basic wrapper tutorial](../user/tutorials/basic-wrapper.md) | Main supported user workflow and boundaries | -| [Verified examples cookbook](../user/examples/verified-cookbook.md) | Copy-paste commands and Python API recipes | +| [Getting Started](../user/getting-started/index.md) | Main supported user workflow and boundaries | +| [Examples Gallery](../user/examples/index.md) | Checked commands and Python API recipes | | [Fortran wrapper reference](../user/reference/fortran-wrapper.md) | Implemented Fortran runtime contract, mechanism, ownership, and build modes | | [Fortran parser reference](fortran-parser-reference.md) | Developer inventory for the Fortran frontend | | [Semantic IR reference](../user/reference/semantic-ir.md) | Accepted semantic IR and datatype contract | @@ -173,9 +174,10 @@ PYTHONPATH=. python3 -m pytest -q tests/docs/test_examples.py ## References -- [Tutorial](../user/tutorials/basic-wrapper.md): supported end-to-end user workflow and current - boundaries. -- [Verified examples cookbook](../user/examples/verified-cookbook.md): CLI and Python API recipes. +- [Getting Started](../user/getting-started/index.md): supported end-to-end user + workflow and current boundaries. +- [Examples Gallery](../user/examples/index.md): checked CLI and Python API + recipes. - [Fortran parser reference](fortran-parser-reference.md): Fortran frontend scope, recursive parser organization, API/CLI behavior, diagnostics, fixture workflow, semantic handoff, and tests. @@ -320,8 +322,9 @@ When changing `.pyi` syntax: 1. Add or update parser tests in `tests/parsing/pyi/`. 2. Add or update printer tests in `tests/wrapper_codegen/printers/`. 3. Update fixture tests only if the public generated contract changes. -4. Update [Basic wrapper tutorial](../user/tutorials/basic-wrapper.md) or [Verified examples cookbook](../user/examples/verified-cookbook.md) if users - need to write or read the new syntax. +4. Update the relevant [User Guide](../user/guide/index.md) or checked + [example](../user/examples/index.md) if users need to write or read the new + syntax. 5. Update [Semantic .pyi format](../user/reference/semantic-pyi-format.md) for the full user-facing reference. 6. Update [Semantic IR reference](../user/reference/semantic-ir.md) if the underlying semantic IR contract changes. @@ -348,9 +351,10 @@ When changing datatype mapping: 2. Add `.pyi` printer/loader coverage if the emitted syntax changes. 3. Update semantic fixtures only when serialized semantic IR intentionally changes. -4. Update [Semantic IR reference](../user/reference/semantic-ir.md), plus - [Basic wrapper tutorial](../user/tutorials/basic-wrapper.md) or [Verified examples cookbook](../user/examples/verified-cookbook.md) when the visible - user workflow or examples change. +4. Update [Semantic IR reference](../user/reference/semantic-ir.md), plus the + relevant [User Guide](../user/guide/index.md) or checked + [example](../user/examples/index.md) when the visible user workflow or + examples change. 5. Regenerate and update the exact target mapping snapshots in [Semantic IR reference](../user/reference/semantic-ir.md). The executable documentation test must match the complete output of: @@ -1111,8 +1115,10 @@ X2PY_C_DOCS_END --> `x2py/semantics/c2ir.py` and add coverage in `tests/semantics/conversion/c/`. 7. If the generated `.pyi` changes, update `tests/wrapper_codegen/printers/` or `tests/pipeline/pyi_builds/test_contract_fixtures.py`. -8. Update [C parser reference](c-parser-reference.md), [Basic wrapper tutorial](../user/tutorials/basic-wrapper.md), - [Verified examples cookbook](../user/examples/verified-cookbook.md), or [Semantic IR reference](../user/reference/semantic-ir.md) if users or +8. Update [C parser reference](c-parser-reference.md), the relevant + [User Guide](../user/guide/index.md), checked + [example](../user/examples/index.md), or + [Semantic IR reference](../user/reference/semantic-ir.md) if users or developers need to know the new behavior. X2PY_C_DOCS_END --> @@ -1156,8 +1162,10 @@ metadata item. and `tests/semantics/conversion/fortran/`. 7. If generated `.pyi` changes, update `tests/wrapper_codegen/printers/` and the relevant fixture tests. -8. Update [Fortran parser reference](fortran-parser-reference.md), [Basic wrapper tutorial](../user/tutorials/basic-wrapper.md), - [Verified examples cookbook](../user/examples/verified-cookbook.md), or [Semantic IR reference](../user/reference/semantic-ir.md) as needed. +8. Update [Fortran parser reference](fortran-parser-reference.md), the relevant + [User Guide](../user/guide/index.md), checked + [example](../user/examples/index.md), or + [Semantic IR reference](../user/reference/semantic-ir.md) as needed. Focused verification: @@ -1181,9 +1189,10 @@ X2PY_C_DOCS_END --> there is a deliberate schema decision. 4. If the emitted `.pyi` annotation changes, update `tests/wrapper_codegen/printers/` and `tests/parsing/pyi/`. -5. Update the datatype tables in [Semantic IR reference](../user/reference/semantic-ir.md), and update - [Basic wrapper tutorial](../user/tutorials/basic-wrapper.md) or [Verified examples cookbook](../user/examples/verified-cookbook.md) when a visible - example changes. +5. Update the datatype tables in + [Semantic IR reference](../user/reference/semantic-ir.md), and update the + relevant [User Guide](../user/guide/index.md) or checked + [example](../user/examples/index.md) when a visible example changes. and future packaging boundaries. - [ ] `docs/developer/coding-standards.md`: document Python style, documentation front matter, no-compatibility-layer rule, parser/codegen - organization, and review expectations. + organization, public contributor rules, TODO markers, support-claim + discipline, and review expectations. - [ ] `docs/maintainer/ci-cd.md`: document current GitHub Actions gates, coverage policy, static-analysis policy, docs checks, and local caveats for CI-only environment values. @@ -116,9 +117,6 @@ X2PY_C_DOCS_END --> - [ ] `docs/developer/contributing/review-process.md`: document review focus, support claims, docs completeness, fixture quality, and blocking versus advisory comments. -- [ ] `docs/developer/contributing/coding-standards.md`: document public contributor style - rules, docs metadata, TODO markers, and support-claim discipline. - ### Design And Internal Architecture - [ ] `docs/maintainer/design/overall-architecture.md`: document system components, @@ -329,9 +327,6 @@ primary placeholder queue. multi-source, compatibility, and editable-Makefile guide. - [x] `docs/user/guide/raw-addresses.md`: maintained primitive, array, fixed-string, lifetime, validation, and address-safety guide. -- [x] `docs/user/tutorials/basic-wrapper.md`: maintained basic wrapper workflow. -- [x] `docs/user/examples/verified-cookbook.md`: maintained verified example - cookbook. - [x] `docs/user/examples/recipes/`: maintained recipe lane for checked command and API examples. - [x] `docs/user/language-support/feature-matrix.md`: maintained support matrix. diff --git a/docs/maintainer/roadmap/index.md b/docs/maintainer/roadmap/index.md index 0db6a14be..0062e42a6 100644 --- a/docs/maintainer/roadmap/index.md +++ b/docs/maintainer/roadmap/index.md @@ -2,7 +2,7 @@ title: Roadmap audience: maintainers prerequisites: user language support, developer documentation -related: ../../user/language-support/planned-features.md, wrapper-plan-migration-checklist.md, semantic-pyi-wrapper-checklist.md, native-array-handle-checklist.md, documentation-content-checklist.md +related: ../../user/language-support/feature-matrix.md, wrapper-plan-migration-checklist.md, semantic-pyi-wrapper-checklist.md, native-array-handle-checklist.md, documentation-content-checklist.md status: active-roadmap publication: draft --- diff --git a/docs/old_docs/examples.md b/docs/old_docs/examples.md index e2a02ff1d..4787a77d5 100644 --- a/docs/old_docs/examples.md +++ b/docs/old_docs/examples.md @@ -2,7 +2,7 @@ title: Verified Examples Cookbook audience: users prerequisites: installation, first wrapped function -related: tutorials/basic-wrapper.md, examples-gallery/index.md +related: examples-gallery/index.md status: maintained --- diff --git a/docs/old_docs/tutorial.md b/docs/old_docs/tutorial.md index eb0abb56a..2252f0798 100644 --- a/docs/old_docs/tutorial.md +++ b/docs/old_docs/tutorial.md @@ -2,7 +2,7 @@ title: Tutorial audience: users prerequisites: installation, supported compiler toolchain -related: getting-started/index.md, tutorials/basic-wrapper.md +related: getting-started/index.md status: maintained --- diff --git a/docs/user/examples/index.md b/docs/user/examples/index.md index 2476b076c..d5f3231ee 100644 --- a/docs/user/examples/index.md +++ b/docs/user/examples/index.md @@ -2,37 +2,37 @@ title: Examples Gallery audience: users prerequisites: getting started -related: ../tutorials/index.md, verified-cookbook.md +related: ../tutorials/index.md, ../guide/building-shared-library.md status: planned-documentation publication: draft --- # Examples Gallery -The maintained part of this section is the checked recipe cookbook. Use it when -you need a copy-paste command, a short Python API pattern, or the current -boundary between inspection and runtime wrapper support. +The maintained part of this section is the checked recipes. Use them when you +need a copy-paste command, a short Python API pattern, or the current boundary +between inspection and runtime wrapper support. 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. -## Maintained Recipes - -- [Verified examples cookbook](verified-cookbook.md) -- [Build and import with the CLI](recipes/build-and-import-cli.md) -- [Build and import with the Python API](recipes/build-and-import-python-api.md) -- [Generate an editable Makefile](recipes/generate-editable-makefile.md) -- [Build multiple Fortran sources](recipes/build-multiple-fortran-sources.md) -- [Inspect a Fortran API](recipes/inspect-fortran-api.md) -- [Work with semantic .pyi contracts](recipes/semantic-pyi-contracts.md) -- [Control CLI output](recipes/control-cli-output.md) -- [Use Python inspection APIs](recipes/use-python-inspection-apis.md) -- [Use compiler preprocessing options](recipes/compiler-preprocessing.md) +## Choose A Page +| Goal | Page | +| --- | --- | +| Build and import a first extension | [First Wrapped Function](../getting-started/first-wrapped-function.md) | +| Build from several ordered sources | [Building the Shared Library](../guide/building-shared-library.md#multiple-source-files) | +| Generate and edit `Makefile.x2py` | [Building the Shared Library](../guide/building-shared-library.md#use-a-makefile) | +| Build through Python code | [Build and import with the Python API](recipes/build-and-import-python-api.md) | +| Inspect a Fortran API | [Inspect a Fortran API](recipes/inspect-fortran-api.md) | +| Work with semantic `.pyi` contracts | [Work with semantic `.pyi` contracts](recipes/semantic-pyi-contracts.md) | +| Control command output | [Control CLI output](recipes/control-cli-output.md) | +| 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) | ## Planned Project Examples diff --git a/docs/user/examples/recipes/build-and-import-cli.md b/docs/user/examples/recipes/build-and-import-cli.md deleted file mode 100644 index 6ee4ed8e0..000000000 --- a/docs/user/examples/recipes/build-and-import-cli.md +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: Build And Import With The CLI -audience: users -prerequisites: basic wrapper tutorial, supported compiler toolchain -related: ../verified-cookbook.md, ../../reference/fortran-wrapper.md -status: maintained -publication: draft ---- - -# Build And Import With The CLI - -Use this recipe when you want x2py to compile a Fortran source file into an -importable Python extension from the command line. - -## Input - - -```fortran -module fruntime_abi_f90 -contains - real(8) function scale(value, factor) result(output) - real(8), intent(in) :: value - real(8), intent(in) :: factor - output = value * factor - end function scale -end module fruntime_abi_f90 -``` - -## Build - -```bash -python3 -m x2py tests/data/fortran/wrapper/fruntime_abi_f90.f90 \ - --out-dir build/fruntime_abi \ - --json -``` - -Recognizable Fortran sources select the wrapper build when no inspection stage -is selected, so this is equivalent: - -```bash -python3 -m x2py tests/data/fortran/wrapper/fruntime_abi_f90.f90 \ - --out-dir build/fruntime_abi \ - --json -``` - -## Import - -```python -import sys - -import numpy as np - -sys.path.insert(0, "build/fruntime_abi") -import fruntime_abi_f90 - -result = fruntime_abi_f90.scale(np.float64(3.0), np.float64(2.5)) -print(result) # 7.5 -``` - -## Notes - -- Use `--out-dir` to keep generated sources and build artifacts in one place. -- Use `--verbose` to print compiler and linker commands. -- Exact NumPy scalar dtypes are part of the native ABI contract. - - 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 79edc7a56..dec997116 100644 --- a/docs/user/examples/recipes/build-and-import-python-api.md +++ b/docs/user/examples/recipes/build-and-import-python-api.md @@ -2,7 +2,7 @@ title: Build And Import With The Python API audience: users, developers prerequisites: basic wrapper tutorial, supported compiler toolchain -related: ../verified-cookbook.md, ../../reference/python-api.md +related: ../../reference/python-api.md status: maintained publication: draft --- diff --git a/docs/user/examples/recipes/build-multiple-fortran-sources.md b/docs/user/examples/recipes/build-multiple-fortran-sources.md deleted file mode 100644 index e36505bc6..000000000 --- a/docs/user/examples/recipes/build-multiple-fortran-sources.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Build Multiple Fortran Sources -audience: users, developers -prerequisites: basic wrapper tutorial, supported compiler toolchain -related: ../verified-cookbook.md, ../../reference/fortran-wrapper.md -status: maintained -publication: draft ---- - -# Build Multiple Fortran Sources - -Use this recipe when one Python extension needs declarations or implementations -from more than one Fortran source file. - -## Build - -Pass every source in compiler-valid order. The first semantic module names the -merged extension: - -```bash -python3 -m x2py \ - tests/data/fortran/wrapper/first_api.f90 \ - tests/data/fortran/wrapper/second_api.f90 \ - --out-dir build/multi_api \ - --json -``` - -## Import - -```python -import sys - -import numpy as np - -sys.path.insert(0, "build/multi_api") -from first_api.first_api import add_one -from first_api.second_api import double_value - -assert add_one(np.int32(4)) == np.int32(5) -assert double_value(np.int32(4)) == np.int32(10) -``` - -## Ordering Rules - -x2py does not discover missing sources and does not reorder dependencies. Put -module providers before module consumers, matching the order your compiler -expects for a direct native build. - -## Generate One Contract Package - -The same ordered source list can generate one combined semantic `.pyi` package: - -```bash -python3 -m x2py generate --pyi \ - tests/data/fortran/wrapper/first_api.f90 \ - tests/data/fortran/wrapper/second_api.f90 \ - --out contracts/multi_api -``` - -`contracts/multi_api/__init__.pyi` is the only semantic wrapper input. Native -module leaves are written directly under `contracts/multi_api/`; x2py does not -create per-source subdirectories. - -## Notes - -- The output is one Python extension, not one extension per source file. -- Use `--out-dir` to keep all generated and native artifacts together. -- Use [Generate an editable Makefile](generate-editable-makefile.md) when you - need your build system to run the compile/link step later. diff --git a/docs/user/examples/recipes/compiler-preprocessing.md b/docs/user/examples/recipes/compiler-preprocessing.md index afb69d350..1940cccfd 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: ../verified-cookbook.md, ../../../developer/c-parser-reference.md, ../../../developer/fortran-parser-reference.md +related: ../../../developer/c-parser-reference.md, ../../../developer/fortran-parser-reference.md status: maintained publication: draft --- diff --git a/docs/user/examples/recipes/control-cli-output.md b/docs/user/examples/recipes/control-cli-output.md index 10cd11343..57ae0eb60 100644 --- a/docs/user/examples/recipes/control-cli-output.md +++ b/docs/user/examples/recipes/control-cli-output.md @@ -2,7 +2,7 @@ title: Control CLI Output audience: users, developers prerequisites: installation -related: ../verified-cookbook.md, ../../reference/cli-commands.md +related: ../../reference/cli-commands.md status: maintained publication: draft --- diff --git a/docs/user/examples/recipes/generate-editable-makefile.md b/docs/user/examples/recipes/generate-editable-makefile.md deleted file mode 100644 index ee3f5194d..000000000 --- a/docs/user/examples/recipes/generate-editable-makefile.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: Generate An Editable Makefile -audience: users, developers -prerequisites: basic wrapper tutorial, GNU Make, supported compiler toolchain -related: ../verified-cookbook.md, ../../reference/fortran-wrapper.md -status: maintained -publication: draft ---- - -# Generate An Editable Makefile - -Use this recipe when you want x2py to generate wrapper sources and -`Makefile.x2py`, then let your build environment run the compile and link -steps. For semantic `.pyi` builds, x2py also writes `x2py-build.json`; that -manifest is the source of truth used to generate the Makefile. - -## Generate The Build Files - -```bash -python3 -m x2py generate --makefile tests/data/fortran/wrapper/fruntime_abi_f90.f90 \ - --out-dir build/fruntime_abi \ - --json -``` - -This writes generated wrapper sources, the header-only native binding support, dependency files, and -`build/fruntime_abi/Makefile.x2py`. - -For a semantic `.pyi` contract with native implementation sources, use the same -mode with explicit native inputs: - -```bash -python3 -m x2py generate --makefile contracts/fruntime_abi_f90.pyi \ - --native-fortran-sources native/fruntime_abi_f90.f90 \ - --native-compile-flags="-O3 -fopenmp" \ - --out-dir build/fruntime_abi \ - --json -``` - -This writes `build/fruntime_abi/x2py-build.json` first and then projects -`build/fruntime_abi/Makefile.x2py` from that manifest. - -## Build With GNU Make - -```bash -make -f build/fruntime_abi/Makefile.x2py -j4 \ - X2PY_FFLAGS=-O3 \ - X2PY_CFLAGS=-O3 \ - X2PY_LDFLAGS=-O3 -``` - -The generated Makefile exposes these variables for local override: - -| Variable | Meaning | -| --- | --- | -| `FC` | Fortran compiler | -| `X2PY_LD` | Link command | -| `X2PY_FFLAGS` | Extra Fortran compiler flags | -| `X2PY_LDFLAGS` | Extra linker flags | - - - -## Notes - -- `--makefile` generates the build plan without compiling immediately. -- `--makefile` selects the editable wrapper-build mode directly. -- `--makefile` and `--verbose` are mutually exclusive. -- `.pyi` Makefile generation is replayable through - `python3 -m x2py generate --makefile --build-manifest build/fruntime_abi/x2py-build.json` - or buildable through - `python3 -m x2py --build-manifest build/fruntime_abi/x2py-build.json`. -- User Fortran sources remain in caller-provided order. Generated independent - objects may be built in parallel by Make. diff --git a/docs/user/examples/recipes/inspect-c-api.md b/docs/user/examples/recipes/inspect-c-api.md index 1e59d5b3f..0a0e331aa 100644 --- a/docs/user/examples/recipes/inspect-c-api.md +++ b/docs/user/examples/recipes/inspect-c-api.md @@ -3,7 +3,7 @@ title: Deferred Native API Inspection audience: users, developers prerequisites: installation -related: ../verified-cookbook.md, ../../../developer/c-parser-reference.md +related: ../../../developer/c-parser-reference.md status: maintained publication: draft --- diff --git a/docs/user/examples/recipes/inspect-fortran-api.md b/docs/user/examples/recipes/inspect-fortran-api.md index e034b3b5b..f47dcab9f 100644 --- a/docs/user/examples/recipes/inspect-fortran-api.md +++ b/docs/user/examples/recipes/inspect-fortran-api.md @@ -2,7 +2,7 @@ title: Inspect A Fortran API audience: users, developers prerequisites: basic wrapper tutorial -related: ../verified-cookbook.md, ../../reference/semantic-pyi-format.md +related: ../../reference/semantic-pyi-format.md status: maintained publication: draft --- diff --git a/docs/user/examples/recipes/semantic-pyi-contracts.md b/docs/user/examples/recipes/semantic-pyi-contracts.md index f932e8f60..38bab1c0d 100644 --- a/docs/user/examples/recipes/semantic-pyi-contracts.md +++ b/docs/user/examples/recipes/semantic-pyi-contracts.md @@ -2,7 +2,7 @@ title: Work With Semantic .pyi Contracts audience: users, advanced users prerequisites: semantic .pyi format -related: ../verified-cookbook.md, ../../reference/semantic-pyi-format.md +related: ../../reference/semantic-pyi-format.md status: maintained publication: draft --- diff --git a/docs/user/examples/recipes/use-python-inspection-apis.md b/docs/user/examples/recipes/use-python-inspection-apis.md index 3b9d61588..bb28874fc 100644 --- a/docs/user/examples/recipes/use-python-inspection-apis.md +++ b/docs/user/examples/recipes/use-python-inspection-apis.md @@ -2,7 +2,7 @@ title: Use Python Inspection APIs audience: users, developers prerequisites: installation -related: ../verified-cookbook.md, ../../reference/python-api.md, ../../reference/semantic-ir.md +related: ../../reference/python-api.md, ../../reference/semantic-ir.md status: maintained publication: draft --- diff --git a/docs/user/examples/verified-cookbook.md b/docs/user/examples/verified-cookbook.md deleted file mode 100644 index fe3ef66a6..000000000 --- a/docs/user/examples/verified-cookbook.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Verified Examples Cookbook -audience: users -prerequisites: basic wrapper tutorial -related: ../tutorials/basic-wrapper.md, index.md -status: maintained -publication: draft ---- - -# Verified Examples Cookbook - -This cookbook is for lookup. Each recipe answers one practical question and -uses checked repository fixtures where the command output is stable. - -Start with the [basic wrapper tutorial](../tutorials/basic-wrapper.md) if this -is your first x2py workflow. The Fortran wrapper reference gives the full -runtime contract. The Semantic .pyi Format reference gives the complete -editable wrapper contract syntax. - -## Choose A Recipe - -| Goal | Recipe | -| --- | --- | -| Build a Fortran extension with the CLI and import it | [Build and import with the CLI](recipes/build-and-import-cli.md) | -| Build and import through Python code | [Build and import with the Python API](recipes/build-and-import-python-api.md) | -| Generate wrapper sources and an editable Makefile | [Generate an editable Makefile](recipes/generate-editable-makefile.md) | -| Build one extension from multiple ordered Fortran sources | [Build multiple Fortran sources](recipes/build-multiple-fortran-sources.md) | -| Parse and print `.pyi` | [Inspect a Fortran API](recipes/inspect-fortran-api.md) | -| Work with generated or edited `.pyi` contracts | [Work with semantic .pyi contracts](recipes/semantic-pyi-contracts.md) | -| Combine stages or limit human-readable output | [Control CLI output](recipes/control-cli-output.md) | -| Use parser and semantic APIs from Python code | [Use Python inspection APIs](recipes/use-python-inspection-apis.md) | -| Pass compiler and preprocessing flags | [Use compiler preprocessing options](recipes/compiler-preprocessing.md) | - - - -## Fixture Inputs - -The recipes reuse these checked fixtures: - -| Purpose | Repository fixture | -| --- | --- | -| Compiled Fortran wrapper and scalar call | `tests/data/fortran/wrapper/fruntime_abi_f90.f90` | -| Basic Fortran procedure | `tests/data/fortran/general/basic_subroutine.f90` | -| Rich Fortran module, types, arrays, and visibility | `tests/data/fortran/general/modern_pyi_example.f90` | -| Generated Fortran semantic interface | `tests/pyi/fixtures/general/modern_pyi_example/modern_pyi_example.pyi` | - - - -## Current Boundary - - - -## Next - -- [Basic wrapper tutorial](../tutorials/basic-wrapper.md) -- [Fortran wrapper reference](../reference/fortran-wrapper.md) -- Semantic .pyi Format -- Semantic IR Reference -- Diagnostic Codes diff --git a/docs/user/getting-started/beginner-workflow.md b/docs/user/getting-started/beginner-workflow.md index e2a98a79e..527b49ba4 100644 --- a/docs/user/getting-started/beginner-workflow.md +++ b/docs/user/getting-started/beginner-workflow.md @@ -3,7 +3,7 @@ title: Common Beginner Workflow description: Recommended development loop — edit, review contract, build, test, and rebuild audience: users prerequisites: first wrapped module -related: ../tutorials/basic-wrapper.md, ../examples/verified-cookbook.md, ../reference/cli-commands.md +related: ../guide/index.md status: maintained publication: reviewed --- @@ -120,7 +120,3 @@ when you need to rule out stale build files. ## Next - Continue with the [User Guide](../guide/index.md). -- Check the [Language Feature Matrix](../language-support/feature-matrix.md) for - current support boundaries. -- Use the [Verified Cookbook](../examples/verified-cookbook.md) for more - complete examples. diff --git a/docs/user/getting-started/first-wrapped-function.md b/docs/user/getting-started/first-wrapped-function.md index 0d119b3ab..5de39674b 100644 --- a/docs/user/getting-started/first-wrapped-function.md +++ b/docs/user/getting-started/first-wrapped-function.md @@ -3,7 +3,7 @@ title: First Wrapped Function description: Build and call your first Fortran function as a Python extension audience: users prerequisites: installation, verification -related: first-wrapped-module.md, ../guide/wrapping-functions.md, ../reference/semantic-pyi-format.md +related: first-wrapped-module.md, ../guide/wrapping-functions.md status: maintained publication: reviewed --- diff --git a/docs/user/getting-started/first-wrapped-module.md b/docs/user/getting-started/first-wrapped-module.md index e2e002a7f..e1404ca9a 100644 --- a/docs/user/getting-started/first-wrapped-module.md +++ b/docs/user/getting-started/first-wrapped-module.md @@ -3,7 +3,7 @@ title: First Wrapped Module description: Wrap a Fortran module with public procedures and state variables audience: users prerequisites: first wrapped function -related: beginner-workflow.md, ../guide/wrapping-modules.md, ../language-support/feature-matrix.md +related: beginner-workflow.md, ../guide/wrapping-modules.md status: maintained publication: reviewed --- @@ -160,4 +160,3 @@ python3 -m x2py generate --pyi module_state.f90 - Continue with the [Beginner Workflow](beginner-workflow.md) to turn these steps into a repeatable development loop. - For module details, see [Wrapping Modules](../guide/wrapping-modules.md). -- For support boundaries, see the [Language Feature Matrix](../language-support/feature-matrix.md). diff --git a/docs/user/getting-started/installation.md b/docs/user/getting-started/installation.md index 3d1bdeaf0..a55cea84a 100644 --- a/docs/user/getting-started/installation.md +++ b/docs/user/getting-started/installation.md @@ -3,7 +3,7 @@ title: Installation description: Install x2py from source and set up the native GNU toolchain audience: users, contributors prerequisites: Python 3.10 or newer, repository checkout -related: verification.md, ../troubleshooting/installation-issues.md, ../../developer/quality-assurance.md +related: verification.md status: maintained publication: reviewed --- @@ -85,4 +85,3 @@ python3 -m pip install -e ".[qa]" ## Next - Go to [Verification](verification.md) to check the installation and compiler. -- If setup fails, see [Installation Issues](../troubleshooting/installation-issues.md). diff --git a/docs/user/getting-started/verification.md b/docs/user/getting-started/verification.md index e73048154..9eb9c14d4 100644 --- a/docs/user/getting-started/verification.md +++ b/docs/user/getting-started/verification.md @@ -3,7 +3,7 @@ title: Verification description: Verify that x2py, NumPy, and the native toolchain are working correctly audience: users, contributors prerequisites: installation -related: first-wrapped-function.md, ../troubleshooting/index.md, ../reference/cli-commands.md +related: first-wrapped-function.md status: maintained publication: reviewed --- @@ -67,4 +67,3 @@ The output should identify GNU Fortran. If the command is missing, install - Build and call [Your First Wrapped Function](first-wrapped-function.md). That example is the end-to-end verification. -- For detailed help, use [Troubleshooting](../troubleshooting/index.md). diff --git a/docs/user/guide/arrays.md b/docs/user/guide/arrays.md index a08c7465c..2a260f1c1 100644 --- a/docs/user/guide/arrays.md +++ b/docs/user/guide/arrays.md @@ -461,5 +461,3 @@ Use this list when reading or editing a generated `.pyi` contract: [Wrapping Subroutines](wrapping-subroutines.md). - [Allocatables](allocatables.md) and [Pointers](pointers.md) for native allocation control. -- [Language Feature Matrix](../language-support/feature-matrix.md) for support - details. diff --git a/docs/user/guide/building-shared-library.md b/docs/user/guide/building-shared-library.md index 2f4cdb90c..a7d065646 100644 --- a/docs/user/guide/building-shared-library.md +++ b/docs/user/guide/building-shared-library.md @@ -3,7 +3,7 @@ title: Building the Shared Library description: How to build and import a Python extension shared library with x2py audience: users prerequisites: common beginner workflow -related: ../reference/cli-commands.md, ../examples/recipes/build-and-import-cli.md, ../examples/recipes/build-multiple-fortran-sources.md, ../examples/recipes/generate-editable-makefile.md +related: error-handling.md status: maintained publication: reviewed --- @@ -59,16 +59,37 @@ python3 -m x2py src/types.f90 src/solver.f90 \ x2py preserves the given order. It does not discover source dependencies or external libraries automatically. +## Use a Makefile + +To inspect or customize the build commands, generate a Makefile without +compiling: + +```bash +python3 -m x2py generate --makefile src/scale.f90 --out-dir build/scale +``` + +Edit `Makefile.x2py` before running `make` when customization is needed. Its +most useful settings are near the top: + +| Setting | What it changes | +| --- | --- | +| `FC` | Fortran compiler | +| `X2PY_LD` | Command that creates the shared library | +| `X2PY_FFLAGS` | Extra Fortran compiler flags | +| `X2PY_LDFLAGS` | Extra linker flags | + +The build targets and commands follow these settings and normally do not need +editing. Then build the shared library: + +```bash +make -f build/scale/Makefile.x2py +``` + +You can pass the same ordered list of source files used in the previous +example. This workflow requires GNU Make. + ## Compatibility The shared library is not universal. It must match the target machine's operating system and architecture, Python and NumPy, and required compiler libraries. Rebuilding it on the target machine is the safest choice. - -## Next - -- Follow the complete [build and import recipe](../examples/recipes/build-and-import-cli.md). -- For several files, see the [multiple-source recipe](../examples/recipes/build-multiple-fortran-sources.md). -- To inspect or customize the build, [generate an editable Makefile](../examples/recipes/generate-editable-makefile.md). -- For build or import failures, see [Troubleshooting](../troubleshooting/index.md) - and rerun the build with `--verbose`. diff --git a/docs/user/guide/callbacks.md b/docs/user/guide/callbacks.md index 15386752d..0e262dcca 100644 --- a/docs/user/guide/callbacks.md +++ b/docs/user/guide/callbacks.md @@ -3,7 +3,7 @@ title: Callbacks description: How to pass Python callables to Fortran as callbacks with x2py audience: advanced users prerequisites: wrapping functions, data types -related: error-handling.md, memory-management.md, ../reference/semantic-pyi-format.md +related: error-handling.md, memory-management.md status: maintained publication: reviewed --- @@ -214,5 +214,4 @@ boundary: x2py prints the Python traceback and aborts the host process. ## Next - Continue with [Enumerations](enumerations.md). -- Use the [Callbacks Reference](../reference/callbacks.md) for the full prototype contract. - Review [Error Handling](error-handling.md) when callback failure behavior matters. diff --git a/docs/user/guide/data-types.md b/docs/user/guide/data-types.md index a7c7bf690..61cbe4e5d 100644 --- a/docs/user/guide/data-types.md +++ b/docs/user/guide/data-types.md @@ -3,7 +3,7 @@ title: Data Types description: How x2py maps Fortran types to Python, NumPy dtypes, and semantic contracts audience: users prerequisites: common beginner workflow -related: arrays.md, strings.md, wrapping-derived-types.md, ../reference/semantic-pyi-format.md +related: arrays.md, strings.md, wrapping-derived-types.md status: maintained publication: reviewed --- @@ -159,5 +159,3 @@ bits and complex storage wider than 128 total bits instead of narrowing it. - Then read [Strings](strings.md) for immutable values and mutable character storage. - [Wrapping Derived Types](wrapping-derived-types.md) -- Check the [Language Feature Matrix](../language-support/feature-matrix.md) - for current type support. diff --git a/docs/user/guide/enumerations.md b/docs/user/guide/enumerations.md index 2477a557d..76cc1c381 100644 --- a/docs/user/guide/enumerations.md +++ b/docs/user/guide/enumerations.md @@ -3,7 +3,7 @@ title: Enumerations description: How x2py handles Fortran `enum` and enumerators audience: users prerequisites: wrapping modules, data types -related: wrapping-modules.md, generic-interfaces.md, ../language-support/feature-matrix.md +related: wrapping-modules.md, generic-interfaces.md status: maintained publication: reviewed --- @@ -84,4 +84,3 @@ print(result) # 10 ## Next - Continue with [Raw Addresses](raw-addresses.md). -- Check the [Language Feature Matrix](../language-support/feature-matrix.md) for current enum support. diff --git a/docs/user/guide/error-handling.md b/docs/user/guide/error-handling.md index cd26cb1f3..23e0b6f83 100644 --- a/docs/user/guide/error-handling.md +++ b/docs/user/guide/error-handling.md @@ -3,7 +3,7 @@ title: Error Handling & Diagnostics description: How x2py reports errors at different stages and how to diagnose them audience: users, advanced users prerequisites: common beginner workflow, data types -related: ../reference/diagnostic-codes.md, ../troubleshooting/index.md, callbacks.md +related: callbacks.md status: maintained publication: reviewed --- @@ -102,6 +102,3 @@ except RuntimeError as e: ## Next - Finish with [Building the Shared Library](building-shared-library.md). -- [Editing Semantic `.pyi` Contracts](../reference/editing-semantic-pyi-contracts.md) -- Check the [Diagnostic Codes](../reference/diagnostic-codes.md) reference for - detailed error categories. diff --git a/docs/user/guide/generic-interfaces.md b/docs/user/guide/generic-interfaces.md index 14e1fbf39..2561884ed 100644 --- a/docs/user/guide/generic-interfaces.md +++ b/docs/user/guide/generic-interfaces.md @@ -195,6 +195,3 @@ in Wrapping Derived Types. - Continue with [Wrapping Derived Types](wrapping-derived-types.md) for type-bound generics and operators - See [Error Handling](error-handling.md) for dispatch errors -- See [Editing Semantic `.pyi` Contracts](../reference/editing-semantic-pyi-contracts.md) - for advanced overload changes -- For current generic and operator support, refer to the [Language Feature Matrix](../language-support/feature-matrix.md). diff --git a/docs/user/guide/index.md b/docs/user/guide/index.md index 373df1b8c..26db69f25 100644 --- a/docs/user/guide/index.md +++ b/docs/user/guide/index.md @@ -3,7 +3,7 @@ title: User Guide description: Detailed guides for wrapping Fortran code with x2py audience: users prerequisites: getting started -related: data-types.md, ../reference/fortran-wrapper.md, ../language-support/index.md +related: data-types.md status: maintained publication: reviewed --- @@ -53,13 +53,6 @@ objects, storage, and advanced runtime behavior. --- -## Reference - -- [Reference Overview](../reference/index.md) — CLI, Python API, wrapper behavior, and semantic contracts -- [Language Feature Matrix](../language-support/feature-matrix.md) — Current support status (supported / partial / unsupported) - ---- - **Important Note** The recommended workflow starts from Fortran source. The generated semantic diff --git a/docs/user/guide/raw-addresses.md b/docs/user/guide/raw-addresses.md index 4929b19ec..bf93db460 100644 --- a/docs/user/guide/raw-addresses.md +++ b/docs/user/guide/raw-addresses.md @@ -3,7 +3,7 @@ title: Raw Addresses description: Pass primitive, array, and fixed-string storage addresses through semantic contracts audience: advanced users prerequisites: arrays, strings, editing semantic pyi contracts -related: data-types.md, arrays.md, strings.md, ../reference/editing-semantic-pyi-contracts.md, ../reference/semantic-pyi-format.md +related: data-types.md, arrays.md, strings.md status: maintained publication: reviewed --- @@ -192,6 +192,3 @@ alignment, ownership, or writeability. A wrong address can crash the process. ## Next - Continue with [Error Handling](error-handling.md). -- [Editing Semantic `.pyi` Contracts](../reference/editing-semantic-pyi-contracts.md) -- [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) for complete - `Addr(...)` validation rules. diff --git a/docs/user/guide/strings.md b/docs/user/guide/strings.md index 7b9c97367..3e9b533f5 100644 --- a/docs/user/guide/strings.md +++ b/docs/user/guide/strings.md @@ -3,7 +3,7 @@ title: Strings description: Immutable strings, mutable character storage, and NumPy byte arrays in x2py audience: users prerequisites: data types, arrays -related: data-types.md, arrays.md, raw-addresses.md, ../reference/semantic-pyi-format.md +related: data-types.md, arrays.md, raw-addresses.md status: maintained publication: reviewed --- @@ -185,5 +185,3 @@ buffer or an immutable replacement result. result-projection rules. - [Raw Addresses](raw-addresses.md) for the advanced `Addr(String[n])` boundary. -- [Semantic `.pyi` Format](../reference/semantic-pyi-format.md) for the full - character grammar. diff --git a/docs/user/guide/wrapping-derived-types.md b/docs/user/guide/wrapping-derived-types.md index cc908093b..bd1d1c39f 100644 --- a/docs/user/guide/wrapping-derived-types.md +++ b/docs/user/guide/wrapping-derived-types.md @@ -3,7 +3,7 @@ title: Wrapping Derived Types description: How x2py wraps Fortran derived types as Python classes with methods, fields, constructors, and ownership rules audience: users, advanced users prerequisites: wrapping modules, data types -related: memory-management.md, generic-interfaces.md, ../reference/fortran-wrapper.md +related: memory-management.md, generic-interfaces.md status: maintained publication: reviewed --- @@ -376,5 +376,3 @@ exact. - Continue with [Allocatables](allocatables.md). - Read [Memory Management](memory-management.md) for the lifetime of native storage and NumPy views. -- See [Replacing the Generated Constructor](../reference/editing-semantic-pyi-contracts.md#replace-the-generated-constructor) - for the advanced constructor contract. diff --git a/docs/user/guide/wrapping-functions.md b/docs/user/guide/wrapping-functions.md index ad9224b8f..5280a499c 100644 --- a/docs/user/guide/wrapping-functions.md +++ b/docs/user/guide/wrapping-functions.md @@ -3,7 +3,7 @@ title: Wrapping Functions description: How x2py wraps Fortran `function` procedures — return values, output arguments, arrays, and contracts audience: users prerequisites: data types, first wrapped function -related: wrapping-subroutines.md, arrays.md, ../reference/fortran-wrapper.md +related: wrapping-subroutines.md, arrays.md status: maintained publication: reviewed --- diff --git a/docs/user/guide/wrapping-modules.md b/docs/user/guide/wrapping-modules.md index a4e9e36d8..bc7d1eea8 100644 --- a/docs/user/guide/wrapping-modules.md +++ b/docs/user/guide/wrapping-modules.md @@ -180,5 +180,3 @@ Build the edited entry using the - Continue with [Optional Arguments](optional-arguments.md). - Read [Memory Management](memory-management.md) before keeping live views of module storage. -- Check the [Language Feature Matrix](../language-support/feature-matrix.md) - for supported module features and limitations. diff --git a/docs/user/language-support/feature-matrix.md b/docs/user/language-support/feature-matrix.md index ae56a58b3..025eff9f6 100644 --- a/docs/user/language-support/feature-matrix.md +++ b/docs/user/language-support/feature-matrix.md @@ -1,8 +1,8 @@ --- title: Language Feature Matrix audience: users, developers -prerequisites: user guide, verified examples cookbook -related: supported-features.md, partially-supported-features.md, unsupported-features.md, planned-features.md, ../guide/index.md +prerequisites: user guide +related: ../guide/index.md, ../reference/fortran-wrapper.md status: maintained publication: draft --- @@ -49,14 +49,14 @@ inspection-only or partial support. | Fortran enum constants | Supported | [Enumerations](../guide/enumerations.md) | [Semantic constants route](../../developer/source-map.md#common-change-routes) | [Enum tests](../../../tests/wrapper/fortran/scalars/test_fortran_enums.py) | No Python `Enum` or `IntEnum` classes are generated. | | Scalar character arguments, results, and fields | Supported | [Strings](../guide/strings.md) | [Character bridge route](../../developer/source-map.md#common-change-routes) | [Character argument tests](../../../tests/wrapper/fortran/strings/test_character_arguments.py), [edge-case tests](../../../tests/wrapper/fortran/strings/test_character_edge_cases.py) | Character arrays use fixed-width NumPy bytes dtype; mutable scalar deferred-length storage is blocked. | | Scalar kind coverage | Supported | [Data types](../guide/data-types.md) | [Fortran type probe](../../developer/source-map.md#hotspot-index) | [Scalar kind tests](../../../tests/wrapper/fortran/scalars/test_scalar_kinds.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), [multi-source recipe](../examples/recipes/build-multiple-fortran-sources.md) | [Wrapper orchestration](../../developer/source-map.md#common-change-routes) | [Multi-source tests](../../../tests/wrapper/fortran/multiple_files/test_multi_source_builds.py), [compiler verbose tests](../../../tests/wrapper/fortran/build_from_source/test_compiler_verbose.py) | x2py does not discover, reorder, or resolve all external source dependencies. | +| 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/wrapper/fortran/multiple_files/test_multi_source_builds.py), [compiler verbose tests](../../../tests/wrapper/fortran/build_from_source/test_compiler_verbose.py) | x2py 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/wrapper/fortran/naming/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/wrapper_codegen/test_phase10_callbacks.py), [scalar callback tests](../../../tests/wrapper/fortran/callbacks/test_scalar_callbacks.py), [array callback tests](../../../tests/wrapper/fortran/callbacks/test_array_callbacks.py), [derived callback tests](../../../tests/wrapper/fortran/callbacks/test_derived_callbacks.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) | [Runtime policy tests](../../../tests/wrapper/fortran/runtime_behavior/test_runtime_policies.py), [recursion tests](../../../tests/wrapper/fortran/runtime_behavior/test_runtime_recursion.py), [OpenMP tests](../../../tests/wrapper/fortran/runtime_behavior/test_openmp_runtime.py), [ABI tests](../../../tests/wrapper/fortran/build_from_source/test_runtime_abi.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), [CLI recipe](../examples/recipes/build-and-import-cli.md) | [Wrapper orchestration](../../developer/source-map.md#common-change-routes) | [Build modes](../../../tests/wrapper/fortran/build_from_source/test_build_modes.py), [runtime ABI](../../../tests/wrapper/fortran/build_from_source/test_runtime_abi.py) | Implemented for ordered Fortran source inputs. | +| 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/wrapper/fortran/build_from_source/test_build_modes.py), [runtime ABI](../../../tests/wrapper/fortran/build_from_source/test_runtime_abi.py) | Implemented for ordered Fortran source inputs. | diff --git a/docs/user/language-support/index.md b/docs/user/language-support/index.md index 9acff7f05..651fefa6c 100644 --- a/docs/user/language-support/index.md +++ b/docs/user/language-support/index.md @@ -19,11 +19,3 @@ The matrix links each row to: - the source-navigation route for developers; - runtime, parser, semantic, or documentation evidence; and - the current limitation or blocker. - -## Pages - -- [Feature matrix](feature-matrix.md) -- [Supported features](supported-features.md) -- [Partially supported features](partially-supported-features.md) -- [Unsupported features](unsupported-features.md) -- [Planned features](planned-features.md) diff --git a/docs/user/language-support/partially-supported-features.md b/docs/user/language-support/partially-supported-features.md deleted file mode 100644 index 8062f85d8..000000000 --- a/docs/user/language-support/partially-supported-features.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -title: Partially Supported Features -audience: users, developers -prerequisites: feature matrix -related: feature-matrix.md, unsupported-features.md -status: maintained -publication: draft ---- - -# Partially Supported Features - -Partially supported means a tested subset exists, but related forms are -unsupported during planning, or tracked as future work. - - diff --git a/docs/user/language-support/planned-features.md b/docs/user/language-support/planned-features.md deleted file mode 100644 index 765ec52d6..000000000 --- a/docs/user/language-support/planned-features.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: Planned Features -audience: users, developers -prerequisites: feature matrix -related: unsupported-features.md, feature-matrix.md -status: maintained -publication: draft ---- - -# Planned Features - -Planned means the documentation or roadmap reserves space for a future feature, -but current docs must not present it as supported behavior. - -Use the [Planned Or Reserved Areas](feature-matrix.md#planned-or-reserved-areas) -section of the matrix for the current list. diff --git a/docs/user/language-support/supported-features.md b/docs/user/language-support/supported-features.md deleted file mode 100644 index 443f8b392..000000000 --- a/docs/user/language-support/supported-features.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: Supported Features -audience: users, developers -prerequisites: feature matrix -related: feature-matrix.md, ../reference/fortran-wrapper.md -status: maintained -publication: draft ---- - -# Supported Features - -Supported means the documented subset has current runtime or inspection -evidence. Runtime wrapper rows must link to wrapper tests that compile, import, -call, and check behavior. - -Use the [Supported Runtime Features](feature-matrix.md#supported-runtime-features) -and [Supported Inspection Features](feature-matrix.md#supported-inspection-features) -sections of the matrix for the current list. diff --git a/docs/user/language-support/unsupported-features.md b/docs/user/language-support/unsupported-features.md deleted file mode 100644 index bf0ec0fb1..000000000 --- a/docs/user/language-support/unsupported-features.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Unsupported Features -audience: users, developers -prerequisites: feature matrix -related: partially-supported-features.md, planned-features.md -status: maintained -publication: draft ---- - -# Unsupported Features - -Unsupported means x2py intentionally blocks the form or has no safe wrapper -contract for it yet. Unsupported rows should link to the user-facing limitation -and to wrapper-planning or runtime evidence where possible. - -Use the [Unsupported Or Blocked Forms](feature-matrix.md#unsupported-or-blocked-forms) -section of the matrix for the current list. diff --git a/docs/user/reference/callbacks.md b/docs/user/reference/callbacks.md deleted file mode 100644 index a781c423c..000000000 --- a/docs/user/reference/callbacks.md +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: Callbacks Reference -audience: advanced users, developers -prerequisites: semantic .pyi format, data types -related: semantic-pyi-format.md, ../guide/callbacks.md, generated-functions.md -status: maintained -publication: draft ---- - -# Callbacks Reference - -Callback contracts are native-facing. A normal generated function signature -describes how Python calls the wrapper; a `@prototype` declaration describes -the native callback signature invoked through the generated adapter. - -The wrapper call and the callback call are separate boundaries. `@native_call` -on a generated function controls how Python-visible arguments are lowered into -the native routine. `@prototype` controls how native callback dummy arguments -are received by the adapter and converted before invoking the Python callable. -The callable argument list therefore preserves callback argument order, -value/reference calling, rank, shape, character length, and result shape. It -does not repeat native callback direction. - -## Immediate Callback Scope - -Supported callbacks are call-scoped. The generated wrapper keeps the Python -callable alive only for the active wrapped call, installs a callback context, -passes a generated Fortran adapter to the native routine, and clears the context -when the wrapped call returns. - -Policy completion records the callback ABI, primitive-scalar value projection, -reference writeback for non-scalar storage, context lifecycle, entering-thread -rule, GIL actions, and fatal action before wrapper planning. Direct -wrapper-plan generation then emits one typed Fortran adapter and one native -trampoline per callback site. Neither backend infers callback transport, shape, -or ownership from generated locals. - -Native code must not store the callback or call it later. Stored procedure -pointers, optional dummy procedures, asynchronous callbacks, and cross-thread -callback invocation are unsupported. - -## Prototype Argument Forms - -Declare a named prototype once and use its name as the callback argument type: - -```python -from x2py.contracts import Addr, Float64, Int32, prototype - -@prototype -def transform(count: Addr(Int32), values: Float64[count]) -> Float64[count]: ... - -def apply_transform(callback: transform) -> None: ... -``` - -Prototype arguments use ordinary semantic types. Primitive callback scalars -follow the same default as normal wrapper signatures: a bare primitive is a -native value, and `Addr(T)` marks a primitive reference dummy. Arrays, strings, -and derived-type references carry storage or wrapper objects without `Addr(T)`. - -| Spelling | Fortran callback dummy | Python callback object | -| --- | --- | --- | -| `Int32` | scalar `value` dummy | owned `np.int32` scalar value | -| `Addr(Float64)` | scalar reference dummy | owned `np.float64` scalar value | -| `Float64[n]` | array reference dummy | writable NumPy array view | -| `point_t` | derived reference dummy | generated wrapper object | -| `Value(point_t)` | derived `value` dummy | wrapper over the call-local value copy | - -Do not add `Value(T)` just to make Python receive a primitive scalar: bare -primitive callback arguments already arrive as owned NumPy scalar values. -`Value(T)` is reserved for supported non-primitive scalar value dummies. -`Addr(T)` inside `@prototype` marks a primitive reference dummy; it does not -make the Python callback receive an integer raw address. - -Optional callback dummies and allocatable, pointer, or polymorphic callback -dummies and results are unsupported. Their descriptor and presence semantics -cannot be represented by the current callback transfer plan. - -Prototypes are semantic declarations and never become Python runtime exports. -A prototype defined in another contract module is referenced through a normal -relative semantic import. That import supplies the signature identity and -complete transport contract. Post-IR policy decides whether the backend may use -an implicit external declaration or must import and use the named native -prototype. The explicit path obtains native direction from that real interface; -the semantic prototype does not repeat it. - -## Character Arguments - -Fixed-length character reference dummies use their ordinary type spelling: - -```python -from x2py.contracts import String, prototype - -@prototype -def update_label(label: String[8]) -> None: ... -``` - -The Python callback receives a NumPy scalar bytes array, such as `np.ndarray` -with shape `()` and dtype `S8`, and writes through that storage: - -```python -def update(label): - label[...] = b"done " -``` - -Generated `.pyi` contracts emit `String[n]` regardless of native callback -direction. Callback context makes reference character storage mutable without -adding direction metadata to the annotation. - -## Results And Copy-Back - -Scalar callback results are converted from the Python return value. Primitive -scalar arguments are always owned NumPy scalar values and never scalar-storage -arrays, so scalar reference writeback is unsupported. Array, derived, and -character-storage reference arguments are copied back before the Fortran adapter -returns to native code. - -Callback exceptions, invalid callback return conversion, and unsupported -cross-thread callback execution are fatal at the callback boundary: x2py prints -the Python traceback and aborts the host process. diff --git a/docs/user/reference/index.md b/docs/user/reference/index.md index 3bcde3874..baacf1753 100644 --- a/docs/user/reference/index.md +++ b/docs/user/reference/index.md @@ -22,7 +22,6 @@ examples, and user guides. - [Semantic IR](semantic-ir.md) - [Semantic .pyi format](semantic-pyi-format.md) - [Editing semantic .pyi contracts](editing-semantic-pyi-contracts.md) -- [Callbacks](callbacks.md) - [Diagnostic codes](diagnostic-codes.md) - [Generated functions](generated-functions.md) - [Generated modules](generated-modules.md) diff --git a/docs/user/tutorials/basic-wrapper.md b/docs/user/tutorials/basic-wrapper.md deleted file mode 100644 index 3053251ae..000000000 --- a/docs/user/tutorials/basic-wrapper.md +++ /dev/null @@ -1,257 +0,0 @@ ---- -title: Basic Wrapper Tutorial -audience: users -prerequisites: installation, supported compiler toolchain -related: ../getting-started/index.md, ../examples/verified-cookbook.md -status: maintained -publication: draft ---- - -# Basic Wrapper Tutorial - -This tutorial walks through one beginner path: - -1. inspect a small Fortran source file; -2. generate the semantic contract x2py sees; -3. check whether the contract is ready for wrapping; -4. build a real Python extension; and -5. import the extension and call one function. - -At the end, you should have seen both sides of x2py: - -- the inspection path, which is useful for understanding a native API; and -- the wrapper path, which compiles an importable Python extension from Fortran. - - - -For lookup-style commands, use the -[verified examples cookbook](../examples/verified-cookbook.md). For -the full generated Python contract, use the -[Fortran wrapper reference](../reference/fortran-wrapper.md). - -## Before You Start - -x2py requires Python 3.10 or newer. Wrapper builds also need a working GNU -native toolchain, Python development headers, and NumPy development files. - - - -Install the checkout and inspect the CLI: - -```bash -python3 -m pip install -e . -python3 -m x2py --help -``` - -The examples below use repository fixtures and run from the repository root. -They use `python3`; replace that with your Python 3.10+ executable if needed. - -## What x2py Builds - -The current runtime wrapper backend is implemented for Fortran source inputs. -Given ordered Fortran sources, x2py performs this pipeline: - -```text -Fortran sources - -> compiler preprocessing and target-type probing - -> parser facts - -> semantic IR construction - -> generated native bridge and Python binding - -> compiled Python extension -``` - - - - - -## Step 1: Inspect A Small Fortran Source - -Start with this checked fixture: - - -```fortran -module m1 -contains -subroutine add1(n, x) - integer, intent(in) :: n - real(kind=8), intent(inout), dimension(n) :: x -end subroutine add1 -end module m1 -``` - -Ask x2py for the parser-level source facts: - - -```bash -python3 -m x2py parse tests/data/fortran/general/basic_subroutine.f90 -``` - -Expected output: - - -```text -File: tests/data/fortran/general/basic_subroutine.f90 - Modules: 1 - - module m1 (vars=0, uses=0) - Procedures: 1 - - subroutine add1(n:integer[0], x:real(8)[1]) -``` - -This output is intentionally compact. It says there is one module and one -subroutine, but it does not yet decide the Python wrapper behavior. - -## Step 2: Generate The Editable Contract - -Generate the semantic `.pyi` contract: - - -```bash -python3 -m x2py generate --pyi tests/data/fortran/general/basic_subroutine.f90 -``` - -Expected output: - - -```python -File: tests/data/fortran/general/basic_subroutine.f90 -Root contract: basic_subroutine/basic_subroutine.pyi -from . import m1 - -Module contract: m1.pyi -from x2py.contracts import Addr, Arg, Float64, Int32, native_call - -@native_call([Addr(Arg(0)), Arg(1)]) -def add1( - n: Int32, - x: Float64[n] -) -> None: ... -``` - -Read this as the native boundary x2py must preserve: - -- `n` is a read-only integer value in Python; the native call receives the - address of x2py's converted native slot for that value. -- `x` is a writable rank-one `Float64` array whose size is described by `n`. -- The subroutine returns `None` because it mutates the caller-provided array. - -The full `.pyi` syntax is documented in -[Semantic .pyi Format](../reference/semantic-pyi-format.md). - -## Step 3: Build A Real Extension - -Use a tiny runtime fixture for the first compiled wrapper: - - -```fortran -module fruntime_abi_f90 -contains - real(8) function scale(value, factor) result(output) - real(8), intent(in) :: value - real(8), intent(in) :: factor - output = value * factor - end function scale -end module fruntime_abi_f90 -``` - -From the command line, a build looks like this: - -```bash -python3 -m x2py tests/data/fortran/wrapper/fruntime_abi_f90.f90 \ - --out-dir build/fruntime_abi \ - --json -``` - -The command writes generated bridge, binding, runtime, object, and shared -library artifacts under the output directory. The JSON output reports the -module name and generated files. Recognizable wrapper inputs select the wrapper -build stage automatically when no inspection stage is selected. - -## Step 5: Import And Call The Extension - -This checked Python example builds into a temporary directory, imports the -generated extension from the returned shared-library path, and calls the native -function: - - -```python -from importlib.util import module_from_spec, spec_from_file_location -from pathlib import Path -from tempfile import TemporaryDirectory - -import numpy as np - -from x2py import build_fortran_extension - -source = Path("tests/data/fortran/wrapper/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) - native_module = module.fruntime_abi_f90 - - print(build.module_name) - print(native_module.scale(np.float64(3.0), np.float64(2.5))) -``` - -Expected output: - - -```text -fruntime_abi_f90 -7.5 -``` - -The exact NumPy scalar types are part of the native ABI contract. Passing a -plain Python `float` where the wrapper requires `numpy.float64` raises -`TypeError` instead of silently changing the native conversion. - -## Common Beginner Mistakes - -| Symptom | Check | -| --- | --- | -| Importing the extension fails | Make sure the output directory is on `sys.path`, or load the shared library path returned by the Python API. | -| A Python number is rejected | Pass the exact NumPy scalar dtype required by the native signature. | -| Generated files are hard to inspect | Build with `--out-dir` and optionally `--verbose` to keep and print artifact paths. | - - - -## What You Learned - -You used x2py to: - -- read Fortran source facts; -- inspect the semantic `.pyi` contract; -- build the wrapper plan through the default build path; and -- build, import, and call a generated extension. - -Next: - -- Use the [verified examples cookbook](../examples/verified-cookbook.md) - for task-specific recipes. -- Use the [Fortran wrapper reference](../reference/fortran-wrapper.md) for the - complete generated Python behavior. -- Use [Semantic .pyi Format](../reference/semantic-pyi-format.md) when editing - wrapper contracts. diff --git a/docs/user/tutorials/index.md b/docs/user/tutorials/index.md index c52d461b1..1fa2b3566 100644 --- a/docs/user/tutorials/index.md +++ b/docs/user/tutorials/index.md @@ -9,17 +9,17 @@ publication: draft # Tutorials -Tutorials are ordered from beginner to advanced and should be step-by-step, -runnable, and backed by checked fixtures or tests. +Getting Started covers the first wrapper workflow. These tutorials are for +larger projects and should be step-by-step, runnable, and backed by checked +fixtures or tests. ## Tutorial Order -1. [Basic wrapper tutorial](basic-wrapper.md) -2. [Scientific library tutorial](scientific-library.md) -3. [Numerical solver tutorial](numerical-solver.md) -4. [Modern Fortran project tutorial](modern-fortran-project.md) -5. [Large Fortran codebase tutorial](large-fortran-codebase.md) -6. [Packaging tutorial](packaging.md) +1. [Scientific library tutorial](scientific-library.md) +2. [Numerical solver tutorial](numerical-solver.md) +3. [Modern Fortran project tutorial](modern-fortran-project.md) +4. [Large Fortran codebase tutorial](large-fortran-codebase.md) +5. [Packaging tutorial](packaging.md) ## TODO diff --git a/mkdocs.yml b/mkdocs.yml index 4b163bbce..80a56f6d5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -58,7 +58,6 @@ nav: - Building the Shared Library: user/guide/building-shared-library.md - Tutorials: - Overview: user/tutorials/index.md - - Basic Wrapper Tutorial: user/tutorials/basic-wrapper.md - Large Fortran Codebase: user/tutorials/large-fortran-codebase.md - Modern Fortran Project: user/tutorials/modern-fortran-project.md - Numerical Solver: user/tutorials/numerical-solver.md @@ -74,17 +73,13 @@ nav: - ODE Solver: user/examples/ode-solver.md - OpenMP Example: user/examples/openmp-example.md - Recipes: - - Build and Import With the CLI: user/examples/recipes/build-and-import-cli.md - Build and Import With the Python API: user/examples/recipes/build-and-import-python-api.md - - Generate an Editable Makefile: user/examples/recipes/generate-editable-makefile.md - - Build Multiple Fortran Sources: user/examples/recipes/build-multiple-fortran-sources.md - Inspect a Fortran API: user/examples/recipes/inspect-fortran-api.md # X2PY_C_DOCS: - Inspect a C API: user/examples/recipes/inspect-c-api.md - Work With Semantic .pyi Contracts: user/examples/recipes/semantic-pyi-contracts.md - Control CLI Output: user/examples/recipes/control-cli-output.md - Use Python Inspection APIs: user/examples/recipes/use-python-inspection-apis.md - Use Compiler Preprocessing Options: user/examples/recipes/compiler-preprocessing.md - - Verified Examples Cookbook: user/examples/verified-cookbook.md - Reference: - Overview: user/reference/index.md - CLI Commands: user/reference/cli-commands.md @@ -93,7 +88,6 @@ nav: - Semantic IR: user/reference/semantic-ir.md - Semantic .pyi Format: user/reference/semantic-pyi-format.md - Editing Semantic .pyi Contracts: user/reference/editing-semantic-pyi-contracts.md - - Callbacks: user/reference/callbacks.md - Diagnostic Codes: user/reference/diagnostic-codes.md - Generated Functions: user/reference/generated-functions.md - Generated Modules: user/reference/generated-modules.md @@ -102,10 +96,6 @@ nav: - Language Support: - Overview: user/language-support/index.md - Feature Matrix: user/language-support/feature-matrix.md - - Supported Features: user/language-support/supported-features.md - - Partially Supported Features: user/language-support/partially-supported-features.md - - Unsupported Features: user/language-support/unsupported-features.md - - Planned Features: user/language-support/planned-features.md - FAQ: user/faq/index.md - Troubleshooting: - Overview: user/troubleshooting/index.md @@ -134,7 +124,6 @@ nav: - Overview: developer/contributing/index.md - Contribution Guide: developer/contributing/contribution-guide.md - Pull Request Workflow: developer/contributing/pull-request-workflow.md - - Coding Standards: developer/contributing/coding-standards.md - Review Process: developer/contributing/review-process.md - Maintainer Documentation: - Overview: maintainer/README.md diff --git a/tests/docs/test_structure.py b/tests/docs/test_structure.py index f3138c617..165c966a2 100644 --- a/tests/docs/test_structure.py +++ b/tests/docs/test_structure.py @@ -266,12 +266,7 @@ ] SOURCE_NAVIGATION_PUBLIC_DOCS = [ "README.md", - "docs/user/tutorials/basic-wrapper.md", - "docs/user/examples/verified-cookbook.md", - "docs/user/examples/recipes/build-and-import-cli.md", - "docs/user/examples/recipes/build-multiple-fortran-sources.md", "docs/user/examples/recipes/compiler-preprocessing.md", - "docs/user/examples/recipes/generate-editable-makefile.md", "docs/user/examples/recipes/inspect-c-api.md", "docs/user/examples/recipes/inspect-fortran-api.md", "docs/user/examples/recipes/semantic-pyi-contracts.md", @@ -371,11 +366,7 @@ "Generated reference pages for modules, functions, and classes", ] REQUIRED_EXAMPLE_RECIPE_PAGES = [ - "user/examples/verified-cookbook.md", - "user/examples/recipes/build-and-import-cli.md", "user/examples/recipes/build-and-import-python-api.md", - "user/examples/recipes/generate-editable-makefile.md", - "user/examples/recipes/build-multiple-fortran-sources.md", "user/examples/recipes/inspect-fortran-api.md", "user/examples/recipes/inspect-c-api.md", "user/examples/recipes/semantic-pyi-contracts.md", From c43a8d8db5f330cc7ef95b593d4a91bec87f535e Mon Sep 17 00:00:00 2001 From: Said Hadjout Date: Tue, 28 Jul 2026 19:51:51 +0100 Subject: [PATCH 32/35] update docs --- docs/developer/feature-to-code-map.md | 2 +- docs/developer/source-map.md | 4 +- .../documentation-content-checklist.md | 5 +- .../roadmap/semantic-pyi-wrapper-checklist.md | 4 +- docs/user/guide/index.md | 2 +- .../editing-semantic-pyi-contracts.md | 765 ------------------ docs/user/reference/fortran-wrapper.md | 16 +- docs/user/reference/index.md | 4 +- .../pyi-contracts/calls-and-results.md | 166 ++++ .../pyi-contracts/exports-and-modules.md | 147 ++++ .../pyi-contracts/functions-and-classes.md | 121 +++ docs/user/reference/pyi-contracts/index.md | 133 +++ .../pyi-contracts/ownership-and-lifetimes.md | 121 +++ docs/user/reference/semantic-pyi-format.md | 2 +- mkdocs.yml | 7 +- tests/docs/test_structure.py | 8 +- 16 files changed, 720 insertions(+), 787 deletions(-) delete mode 100644 docs/user/reference/editing-semantic-pyi-contracts.md create mode 100644 docs/user/reference/pyi-contracts/calls-and-results.md create mode 100644 docs/user/reference/pyi-contracts/exports-and-modules.md create mode 100644 docs/user/reference/pyi-contracts/functions-and-classes.md create mode 100644 docs/user/reference/pyi-contracts/index.md create mode 100644 docs/user/reference/pyi-contracts/ownership-and-lifetimes.md diff --git a/docs/developer/feature-to-code-map.md b/docs/developer/feature-to-code-map.md index bef632c5d..bb725bdf3 100644 --- a/docs/developer/feature-to-code-map.md +++ b/docs/developer/feature-to-code-map.md @@ -19,7 +19,7 @@ before documentation may call the behavior supported. | --- | --- | --- | --- | --- | | Fortran parse output | `docs/developer/fortran-parser-reference.md` | `x2py/parsers/fortran/parser.py`, `models.py`, `lexer.py`, `type_resolver.py` | `tests/parsing/fortran/`, `tests/parsing/fortran/test_fortran_fixture_suite.py` | Parser facts and diagnostics match fixtures | | Semantic `.pyi` generation | `docs/user/reference/semantic-pyi-format.md` | `x2py/wrapper_codegen/printers/pyi_printer.py` | `tests/wrapper_codegen/printers/`, `tests/wrapper_codegen/printers/test_modern_example.py` | Printed `.pyi` round-trips or matches fixtures | -| Semantic `.pyi` conversion and editing | `docs/user/reference/editing-semantic-pyi-contracts.md`, `docs/user/reference/semantic-pyi-format.md`, `docs/user/examples/recipes/semantic-pyi-contracts.md` | `x2py/parsers/pyi/parser.py`, `x2py/pipeline/pyi.py`, `x2py/semantics/pyi2ir.py`, `models.py` | `tests/parsing/pyi/`, `tests/pipeline/pyi_builds/test_contract_fixtures.py` | Edited contracts parse to Python AST, then become semantic IR with preserved native facts | +| 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` | `x2py/parsers/pyi/parser.py`, `x2py/pipeline/pyi.py`, `x2py/semantics/pyi2ir.py`, `models.py` | `tests/parsing/pyi/`, `tests/pipeline/pyi_builds/test_contract_fixtures.py` | 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` | `x2py/semantics/fortran2ir.py`, `x2py/semantics/policy_completion.py`, `x2py/wrapper_codegen/planner.py` | `tests/semantics/conversion/`, `tests/semantics/policy/`, `tests/wrapper_codegen/` | Each owning stage rejects unsupported or incomplete contracts | | Fortran wrapper orchestration | `docs/user/reference/fortran-wrapper.md`, `docs/user/guide/building-shared-library.md` | `x2py/pipeline/build.py` | `tests/wrapper/fortran/build_from_source/test_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` | `x2py/semantics/policy_completion.py`, `x2py/wrapper_codegen/plan.py`, `planner.py`, `generator.py` | `tests/semantics/policy/`, `tests/wrapper_codegen/`, `tests/wrapper/fortran/` | Runtime policy is explicit, the typed plan is complete, and generated artifacts compile and run | diff --git a/docs/developer/source-map.md b/docs/developer/source-map.md index de1031916..c01d16944 100644 --- a/docs/developer/source-map.md +++ b/docs/developer/source-map.md @@ -40,11 +40,11 @@ change crosses ownership boundaries. | CLI flags, stage selection, output formatting, diagnostics | `x2py/cli.py` | `docs/user/reference/cli-commands.md`, `docs/user/getting-started/beginner-workflow.md` | `tests/cli/`, `tests/docs/test_examples.py` | | Compiler preprocessing, include paths, macros, and target flags | `x2py/pipeline/preprocessing.py` | `docs/user/examples/recipes/compiler-preprocessing.md`, `docs/developer/fortran-parser-reference.md` | `tests/pipeline/preprocessing/`, `tests/pipeline/preprocessing/test_parser_boundaries.py` | | Fortran parser facts and diagnostics | `x2py/parsers/fortran/parser.py` | `docs/developer/fortran-parser-reference.md`, `docs/user/examples/recipes/inspect-fortran-api.md` | `tests/parser/`, `tests/parsing/fortran/test_fortran_fixture_suite.py` | -| Semantic `.pyi` parsing, conversion, printing, package generation, and round-trip behavior | `x2py/parsers/pyi/parser.py`, `x2py/pipeline/pyi.py`, `x2py/semantics/pyi2ir.py`, `x2py/wrapper_codegen/printers/pyi_printer.py` | `docs/user/reference/semantic-pyi-format.md`, `docs/user/reference/editing-semantic-pyi-contracts.md`, `docs/user/examples/recipes/semantic-pyi-contracts.md` | `tests/pyi/`, `tests/pipeline/pyi_builds/test_contract_package_generation.py`, `tests/wrapper_codegen/printers/` | +| Semantic `.pyi` parsing, conversion, printing, package generation, and round-trip behavior | `x2py/parsers/pyi/parser.py`, `x2py/pipeline/pyi.py`, `x2py/semantics/pyi2ir.py`, `x2py/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/pyi/`, `tests/pipeline/pyi_builds/test_contract_package_generation.py`, `tests/wrapper_codegen/printers/` | | Wrapper-planning errors and support claims | `x2py/semantics/policy_completion.py`, `x2py/wrapper_codegen/planner.py` | `docs/user/reference/diagnostic-codes.md`, `docs/user/language-support/feature-matrix.md` | `tests/semantics/policy/`, `tests/wrapper_codegen/` | | Source-driven Fortran wrapper orchestration | `x2py/pipeline/build.py` | `docs/user/reference/fortran-wrapper.md`, `docs/user/guide/building-shared-library.md` | `tests/wrapper/fortran/build_from_source/test_build_modes.py`, `tests/wrapper/fortran/multiple_files/test_multi_source_builds.py` | | Semantic `.pyi` wrapper orchestration from native artifacts | `x2py/pipeline/build.py`, `x2py/pipeline/pyi.py`, `x2py/semantics/pyi2ir.py` | `docs/user/reference/fortran-wrapper.md`, `docs/user/reference/semantic-pyi-format.md` | `tests/wrapper/fortran/build_from_pyi/test_pyi_wrapper_builds.py`, `tests/wrapper/fortran/build_from_pyi/test_contract_package_runtime.py` | -| Ownership, lifetime, output projection, and unsupported wrapper policy | `x2py/semantics/policy_completion.py`, `x2py/semantics/ownership.py`, `x2py/wrapper_codegen/planner.py` | `docs/user/reference/fortran-wrapper.md`, `docs/user/reference/editing-semantic-pyi-contracts.md` | `tests/semantics/policy/`, `tests/wrapper_codegen/`, `tests/wrapper/fortran/` | +| Ownership, lifetime, output projection, and unsupported wrapper policy | `x2py/semantics/policy_completion.py`, `x2py/semantics/ownership.py`, `x2py/wrapper_codegen/planner.py` | `docs/user/reference/fortran-wrapper.md`, `docs/user/reference/pyi-contracts/ownership-and-lifetimes.md` | `tests/semantics/policy/`, `tests/wrapper_codegen/`, `tests/wrapper/fortran/` | | Immediate callback policy, typed adapters, and trampolines | `x2py/semantics/wrapper_policy.py`, `x2py/semantics/policy_completion.py`, `x2py/wrapper_codegen/plan.py`, `x2py/wrapper_codegen/planner.py`, `x2py/wrapper_codegen/c/binding.py`, `x2py/wrapper_codegen/fortran/bridge.py` | `docs/user/guide/callbacks.md`, `docs/user/reference/semantic-pyi-format.md` | `tests/wrapper_codegen/test_phase10_callbacks.py`, `tests/wrapper/fortran/callbacks/` | | Native compilation, binding support, and shared-library linking | `x2py/pipeline/build.py`, `x2py/compiling/compilers.py`, `x2py/compiling/native_support.py` | `docs/user/reference/fortran-wrapper.md`, `docs/developer/build-system.md` | `tests/wrapper/fortran/build_from_source/test_runtime_abi.py`, `tests/wrapper/fortran/build_from_source/test_build_modes.py` | | Public Python exports | `x2py/__init__.py` | `README.md`, `docs/user/reference/python-api.md` | `tests/parsing/fortran/test_public_entrypoints.py` | diff --git a/docs/maintainer/roadmap/documentation-content-checklist.md b/docs/maintainer/roadmap/documentation-content-checklist.md index 878c20ac5..1fac6e53d 100644 --- a/docs/maintainer/roadmap/documentation-content-checklist.md +++ b/docs/maintainer/roadmap/documentation-content-checklist.md @@ -274,8 +274,9 @@ primary placeholder queue. - [x] `docs/user/reference/semantic-ir.md`: maintained Semantic IR contract. - [x] `docs/user/reference/semantic-pyi-format.md`: maintained semantic `.pyi` contract. -- [x] `docs/user/reference/editing-semantic-pyi-contracts.md`: maintained - editable `.pyi` contract reference. +- [x] `docs/user/reference/pyi-contracts/`: maintained editable `.pyi` + contract reference, organized by exports, callable surfaces, call + projection, and ownership. - [x] `docs/user/reference/fortran-wrapper.md`: maintained Fortran wrapper contract reference. - [x] `docs/user/reference/cli-commands.md`: maintained CLI reference. diff --git a/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md b/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md index 12b7686da..96963e888 100644 --- a/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md +++ b/docs/maintainer/roadmap/semantic-pyi-wrapper-checklist.md @@ -453,7 +453,7 @@ X2PY_C_DOCS_END --> immutable, and declare as ownership/lifetime policy. It separates editable wrapper policy from native ABI facts and records the failure layers for edited contracts. Evidence: - `docs/user/reference/editing-semantic-pyi-contracts.md`, + `docs/user/reference/pyi-contracts/`, `docs/user/reference/fortran-wrapper.md`, and `tests/docs/test_structure.py`. - [x] Edited contracts can remove a class, method, generated constructor, class @@ -513,7 +513,7 @@ X2PY_C_DOCS_END --> native-order identity calls without `@native_call`, immutable replacement, ownership triples, pointer-policy blockers, runtime constraints, `@raises`, `@hold_gil`, and native-artifact failures. Evidence: - `docs/user/reference/editing-semantic-pyi-contracts.md`, + `docs/user/reference/pyi-contracts/`, `tests/wrapper/fortran/edit_pyi_contracts/`, `tests/semantics/policy/`, `tests/wrapper/fortran/runtime_behavior/test_runtime_policy_decorators.py`, diff --git a/docs/user/guide/index.md b/docs/user/guide/index.md index 26db69f25..be5b72c9a 100644 --- a/docs/user/guide/index.md +++ b/docs/user/guide/index.md @@ -59,7 +59,7 @@ The recommended workflow starts from Fortran source. The generated semantic `.pyi` file describes the Python interface and native call. Editing that file lets you customize the wrapper without changing the native implementation. This guide introduces useful edits on the pages where they matter. The -[editing reference](../reference/editing-semantic-pyi-contracts.md) collects +[editing reference](../reference/pyi-contracts/index.md) collects the complete rules in one place. --- diff --git a/docs/user/reference/editing-semantic-pyi-contracts.md b/docs/user/reference/editing-semantic-pyi-contracts.md deleted file mode 100644 index 0f2b143f9..000000000 --- a/docs/user/reference/editing-semantic-pyi-contracts.md +++ /dev/null @@ -1,765 +0,0 @@ ---- -title: Editing Semantic .pyi Contracts -audience: users, advanced users -prerequisites: Fortran wrapper reference, semantic .pyi format -related: fortran-wrapper.md, semantic-pyi-format.md -status: maintained -publication: reviewed ---- - -# Editing Semantic `.pyi` Contracts - -This reference explains how to change a generated semantic -`.pyi` before building a wrapper. It covers the edits x2py handles, the native -facts an edit must preserve, the runtime effect of each supported edit, and the -errors raised for unsafe combinations. - -The [Semantic `.pyi` Format](semantic-pyi-format.md) provides the complete -grammar. Use this page to decide whether a proposed edit is a supported wrapper -operation. - -## The Editing Workflow - -Generate a starter contract package from the native sources: - -```bash -python3 -m x2py generate --pyi native/solver.f90 --out contracts/solver -``` - -Keep the generated package as a baseline, copy it, and edit the copy: - -```text -contracts/ -├── generated_solver/ -│ ├── __init__.pyi -│ └── solver.pyi -└── edited_solver/ - ├── __init__.pyi - └── solver.pyi -``` - -Build the edited entry contract with the same native implementation artifacts: - -```bash -python3 -m x2py contracts/edited_solver/__init__.pyi \ - --native-objects build/solver.o \ - -I build/mod \ - --out-dir build/edited-solver -``` - -The semantic `.pyi` entry contract selects the wrapper build automatically; -the native artifact options provide the implementation to compile or link. -When the implementation should be compiled from source during the edited -contract build, pass the native sources instead: - -```bash -python3 -m x2py contracts/solver/__init__.pyi \ - --native-fortran-sources solver.f90 -``` - -The entry `.pyi` is the sole semantic input to wrapper generation. x2py does -not reparse the native source to restore a removed declaration, projection, or -policy. Objects, archives, shared libraries, module files, and optional native -sources supplied to the build are implementation inputs, not hidden semantic -inputs. - -## What May And May Not Change - -An edited contract contains two kinds of information: - -1. **Native facts** describe the implementation that already exists: native - module and symbol identity, procedure kind, native argument order, ABI type - and kind, rank, storage category, callback signature, and required native - imports. -2. **Wrapper policy** describes the Python surface x2py should generate: - exports, visibility, Python names, overload grouping, result projection, - validation, mutation, ownership, lifetime, destruction, error translation, - and GIL behavior. - -Wrapper policy is editable. Native facts may be rewritten only when the new -facts still describe the supplied native artifacts. x2py validates structural -consistency, but it cannot inspect an arbitrary object or shared library and -prove its ABI. A structurally valid lie about an opaque native binary can still -fail at compile, link, import, or call time. - -The supported edit surface is: - -| Edit | Supported effect | -| --- | --- | -| Remove a declaration or entry import | Remove that function, method, variable, class, constructor, class member, or overload candidate from the Python API. | -| Add `@private` or `private[...]` | Retain a declaration as an internal contract input while hiding it from Python. | -| Add a declaration for an existing native symbol | Wrap that symbol when the declaration supplies all required native facts and the artifact implements them. | -| Change the Python export name or namespace | Edit the entry-package import/export tree; use `@bind(...)` when the Python declaration name differs from its native target. | -| Change overload grouping | Add or remove `@overload("specific")` candidates with distinct supported dtype/rank signatures. | -| Change Python/native argument projection | Add or edit `@native_call(...)` and `Returns[...]`, or remove `@native_call` and expose the complete native argument list in native order. | -| Change visible mutation | Use caller-owned writable storage, or `Immutable` plus an explicit replacement result. | -| Change supported ownership/lifetime policy | Supply a valid `Ownership(...)`, `Transfer(...)`, and `Destruction(...)` triple for the declared storage and context. | -| Translate native status to exceptions | Add `@raises(...)` with valid projected status/message values. | -| Keep the GIL | Add `@hold_gil` for a call that must execute while holding the Python GIL. | - - - -The following are not supported edits: - -- changing ABI dtype, kind, rank, calling convention, native argument order, or - native symbol without supplying a matching implementation; -- declaring that arbitrary native storage is wrapper-owned without a generated - wrapper instance or an implemented native release path; -- requesting a borrowed pointer view without owner retention and stale-view - invalidation; -- using generic `Annotated` helpers such as `Bounded(...)` or `Finite` as if - they already generated runtime checks; they currently round-trip as semantic - constraints only; -- requesting general implicit dtype coercion; wrapper arguments currently use - the exact documented NumPy dtype unless a specific supported path says - otherwise; or -- relying on omitted metadata to select a risky copy, borrow, reassociation, or - destruction policy. - -Unsupported policy is a blocker, not a request for x2py to guess. - -## Removing And Hiding API Members - -### Remove a declaration - -Delete a public declaration from the leaf `.pyi` to remove it from the -generated Python API. For example, deleting `next_local` removes the function -without affecting the remaining module variables and functions: - -```python -from x2py.contracts import Int32 - -counter: Int32 - -def summarize() -> Int32: ... -``` - -This rule applies to top-level functions, module variables, classes, methods, -fields, constructors, and individual overload declarations. x2py does not -recreate a deleted declaration from native source. - -Generated derived types use `__init__(self, *, ...)` when they have eligible -scalar field keywords and `__init__(self)` when they support only native -default construction. Removing either generated declaration suppresses public -construction. Native allocation may still exist internally, but the deleted -public constructor is not regenerated. - -### Hide a declaration but keep it available internally - -Use `@private` for functions, methods, and classes: - -```python -from x2py.contracts import Float64, private - -@private -def scaled_counter() -> Float64: ... -``` - -Use `private[...]` for data declarations or arguments: - -```python -from x2py.contracts import Float64, private - -scale: private[Float64] -``` - -Private declarations can still supply native types, bindings, or helper facts -needed by other public declarations. User-private declarations remain -printable and reloadable so an edited contract round-trip does not expose them. -Ordinary declarations that were private only in the native source remain -omitted from newly generated starter contracts. - -### Remove an overload candidate - -Each candidate is an independent declaration. Removing one candidate narrows -runtime dispatch without removing the generic name: - -```python -from x2py.contracts import Float64, Int32, bind, overload, private - -@private -def convert_integer(value: Int32) -> Int32: ... - -@bind("convert") -@overload("convert_integer") -def convert(value: Int32) -> Int32: ... - -# The generated Float64 candidate was removed intentionally. -``` - -Calls that no longer match a remaining candidate raise `TypeError`. Do not keep -an empty overload declaration as an absence marker; remove it. - -## Editing Module Variable Initializers - -A mutable scalar module variable may include a literal default: - -```python -from x2py.contracts import Int32 - -counter: Int32 = 41 -``` - -For wrapper builds, that value is applied to native module storage during -extension import by calling the generated native setter. The variable remains -writable after import; later reads and writes still use the current native -storage. This form accepts literal values only. Calls, names, and expressions -such as `f(42)`, `x + 1`, or `SOME_NAME` are rejected for mutable module -variables. The declaration must also have a completed write-through native -setter; unsupported module-variable defaults are reported during wrapper planning -instead of being treated as copied Python values. - -Use `Final[...]` for true constants: - -```python -from x2py.contracts import Final, Int32 - -nmax: Final[Int32] = 12 -``` - -## Adding And Renaming Declarations - -### Add a contained procedure already present in a native module - -Add the complete callable declaration to the module leaf: - -```python -from x2py.contracts import Float64 - -def norm2(values: Float64[:]) -> Float64: ... -``` - -The leaf filename identifies the native module. The declaration name selects -the native procedure unless an -[`@bind(...)`](../guide/wrapping-functions.md#python-and-native-names) target is present. - -### Add or rename a native target - -Retain the native procedure name when choosing a different Python name: - -```python -from x2py.contracts import Float64, Int32, bind - -@bind("solver_step") -def step(values: Float64[:]) -> Int32: ... -``` - -For a standalone external symbol, also use `@external`: - -```python -from x2py.contracts import Float64, bind, external - -@external -@bind("vendor_norm2") -def norm2(values: Float64[:]) -> Float64: ... -``` - -### Project a module function as a method - -A class method can call a declared module procedure. Use `Pass()` for the -method receiver: - -```python -from x2py.contracts import Addr, Arg, Float64, Pass, native_call - -class point: - @native_call([Pass(), Addr(Arg(0))]) - def move_point(self, dx: Float64) -> None: ... - -@native_call([Arg(0), Addr(Arg(1))]) -def move_point(item: point, dx: Float64) -> None: ... -``` - -This exposes both `move_point(item, dx)` and `item.move_point(dx)`. The names -occupy different Python scopes. No `@bind` is needed because both declarations -select native `move_point`. - -The two declarations have independent visibility. Add `@private` to the -module declaration when only the method should be public. - -### Add an overload candidate - -Link every Python overload to one concrete native specific: - -```python -from x2py.contracts import Float64, Int32, overload - -def scale_integer(value: Int32) -> Int32: ... - -def scale_real(value: Float64) -> Float64: ... - -@overload("scale_integer") -def scale(value: Int32) -> Int32: ... - -@overload("scale_real") -def scale(value: Float64) -> Float64: ... -``` - -Without `@bind`, each candidate calls its linked native specific. The specific -may still use `@private` when it should be hidden from Python. - -Contract `@private` does not prove native visibility. If the linked procedure -is Fortran-private, a direct call fails during the native build. Source-based -generation uses the public generic as the bind target in that case. - -Use `@bind` on an overload declaration when the bridge must call a native -generic instead: - -```python -from x2py.contracts import Int32, bind, overload, private - -@private -def convert_integer(value: Int32) -> Int32: ... - -@bind("convert") -@overload("convert_integer") -def convert_number(value: Int32) -> Int32: ... -``` - -The overload string still links the concrete contract. `@bind("convert")` -overrides only the native call target. It takes precedence over the linked -procedure's native name. - -Candidates must be distinguishable by the implemented runtime dispatcher. -Duplicate dtype/rank signatures are rejected because declaration order must not -silently choose a native procedure. - -### Replace the generated constructor - -An edited class may bind `__init__` to one concrete native initializer: - -```python -from x2py.contracts import Addr, Arg, Int32, Pass, bind, native_call - -class state: - @bind("init_state") - @native_call([Pass(), Addr(Arg(0))]) - def __init__(self, size: Int32) -> None: ... -``` - -`__init__` and `init_state` have different names, so `@bind("init_state")` -selects the initializer. Exactly one `Pass()` places the newly allocated -`state` in the native argument list. Its position is explicit, so other -`state` arguments are not ambiguous. The selected native dummy must accept -`state`. - -The generated field-keyword constructor and a bound native initializer are -different contracts. Remove the old constructor declaration when replacing it. - -## Editing The Call Shape - -### Remove `@native_call` and expose native order - -When every native dummy remains visible in native order, the edited declaration -does not need `@native_call`: - -```python -from x2py.contracts import Int32 - -def scalar_status( - base: Int32[()], - status: Int32[()], -) -> None: ... -``` - -The caller supplies scalar storage objects for the visible native scalar slots: - -```python -base = np.array(4, dtype=np.int32) -status = np.empty((), dtype=np.int32) -returned = module.scalar_status(base, status) -print(returned) # None -print(status[()]) # 15 -``` - -This style covers scalar, array, matrix, string, mixed-result, and derived-type -native-order calls. An ordinary Python `str` cannot observe mutation of the -temporary native character buffer; use a projected replacement when Python must -see the new string. - -For a native-order scalar character dummy, keep the Python boundary as -`String[n]`: - -```python -from x2py.contracts import String - -def fixed_inout(label: String[8]) -> None: ... -``` - -The caller passes `str`. x2py creates fixed-width character storage, passes its -address to the native call, and discards native mutation because the signature -returns `None`. Add a replacement return when Python should receive the mutated -value: - -```python -from x2py.contracts import Returns, String - -def fixed_inout(label: String[8]) -> Returns["label", String[8]]: ... -``` - -### Project native arguments into Python returns - -Use `Returns[...]` for the Python result contract and `@native_call(...)` when -the native call needs hidden output storage, reordered arguments, constants, -lengths, presence flags, shapes, or work buffers: - -```python -from x2py.contracts import Addr, Arg, Int32, Return, Returns, native_call - -@native_call([Addr(Arg(0)), Return("status", 0)]) -def scalar_status( - base: Int32, -) -> Returns["status", Int32]: ... -``` - -Removing the explicit `status` parameter and adding the result projection are -one edit. A projection must map every required native argument exactly once; -incomplete, duplicate, or out-of-range mappings are contract errors. -The semantic `.pyi` intentionally has no `intent` annotation. For a generated -starter contract, source `intent` only helps select the default visible -arguments and projected results. After loading, the signature, `Returns[...]`, -and exhaustive `@native_call` list are authoritative. An output dummy may -remain caller-supplied storage, while a projected output exists only when the -contract explicitly requests that projection. The bridge may use permissive -writable local storage; the called native procedure retains and enforces its -own direction. - -### Make mutation replacement-only - -`Immutable` says the original Python-visible object must not be mutated. A -writable native argument therefore needs either an explicit replacement result -or an explicit call-local discarded-mutation policy: - -```python -from x2py.contracts import Annotated, Float64, Immutable, Int32, Returns - -def scale_with_status( - values: Annotated[Float64[:], Immutable], - status: Int32[()], -) -> Returns["values", Float64[:]]: ... -``` - -At runtime, x2py copies `values` into mutable native storage, calls native code, -and returns a different NumPy array. The original remains unchanged. - -Mutability is a property of this argument boundary, not of `Float64`, -`String[n]`, or another datatype in isolation. The same datatype may be an -input value, caller-owned writable storage, or a replacement-only value in -different procedures, so datatype spelling cannot replace `Immutable`. - -`Immutable` plus `Transfer("borrowed_view")` on a writable value is -contradictory: one requests replacement-only semantics and the other requests a -writable shared view. The contract fails instead of selecting one silently. - -## Editing Types, Shapes, Layout, And Optionality - -The annotation is runtime policy, not merely an IDE hint. Supported edits can -tighten or broaden validation without changing the native ABI: - -```python -from x2py.contracts import Float64 - -def solve( - matrix: Float64[3, 3], - rhs: Float64[3], -) -> Float64[3]: ... -``` - -Plain multidimensional arrays in a Fortran semantic `.pyi` use `ORDER_F` by -default. Generated contracts omit that default order. Use explicit layout -metadata only to request a non-default Python storage representation. - -The wrapper validates exact dtype, rank, shape, layout, writeability, byte -order, alignment, and zero-sized-array rules required by the selected backend -path. Examples of supported changes include: - -- `Float64[:, :]` to `Float64[3, 3]` to require one shape; -- `ORDER_ANY` when the native path is implemented for either - contiguous orientation; -- `T | None` or a default `= ...` for a genuinely optional native argument; -- `Allocatable`, `Pointer`, `Aliased`, or `PointerPolicy(...)` when those - facts match the native declaration and the selected policy path; and -- `Immutable` for a supported replacement or call-local mutation policy. - -Changing `Float64[:]` to `Int32[:]`, changing rank, or inventing optionality is -not a Python-only conversion. It changes the declared native ABI and is valid -only when the linked implementation has that ABI. - -Generic constraints such as `Bounded(1, 8)` and `Finite` currently survive -parse/print round-trips but do not generate runtime validation. Wrapper -wrapper planning reports `fortran_runtime_constraints_unsupported` instead of building -a wrapper that silently ignores them. General semantic coercions are handled -the same way through `fortran_runtime_coercions_unsupported`. - -## Editing Errors And GIL Behavior - -Use `@raises(...)` to turn a projected native status into a Python exception: - -```python -from x2py.contracts import Float64, Int32, Returns, String, raises - -@raises(status="status", message="message", success=0) -def solve(values: Float64[:]) -> tuple[ - Returns["result", Float64[:]], - Returns["status", Int32], - Returns["message", String], -]: ... -``` - -The named status and optional message must exist in the function's projected -results. Successful calls omit status-only implementation results from the -Python value according to the documented projection. Non-success status raises -the generated Python exception before returning an ordinary result. - -Wrappers release the GIL around ordinary native calls when the call contract -allows it. Add `@hold_gil` when native code must call Python synchronously or -otherwise requires the current Python thread to retain the GIL: - -```python -from x2py.contracts import Float64, hold_gil, prototype - -@prototype -def scalar_callback(value: Float64) -> Float64: ... - -@hold_gil -def invoke_callback(callback: scalar_callback) -> Float64: ... -``` - -These decorators change wrapper runtime policy; they do not change the native -procedure ABI. - -## Ownership, Transfer, And Destruction - -Ownership edits use a complete policy triple: - -```python -from x2py.contracts import Annotated, Destruction, Float64, Ownership, Transfer - -values: Annotated[ - Float64[:], - Ownership("native"), - Transfer("borrowed_view"), - Destruction("native_owner"), -] -``` - -The three values answer different questions: - -- `Ownership(...)`: who owns the authoritative storage? -- `Transfer(...)`: does Python receive a value, temporary, in-place object, - copy, view, or wrapper instance? -- `Destruction(...)`: which runtime releases owned storage, and at what - lifetime boundary? - -They are not three independent switches. x2py validates the combination -against object kind, native storage category, call position, mutability, -nullability, projection, and available release mechanism. An edit can choose -between implemented boundary behaviors; it cannot retroactively change where a -native allocation came from. - -### One `values` example in three descriptor contexts - -The following variants all expose a rank-one `Float64` allocatable handle named -`values`, but their descriptor contexts make ownership and lifetime different. - -#### Fortran-owned module storage - -```python -from x2py.contracts import Aliased, Allocatable, Annotated, Destruction, Float64, Ownership, Transfer - -module_values: Annotated[ - Allocatable[Float64[:]], - Aliased, - Ownership("native"), - Transfer("borrowed_view"), - Destruction("native_owner"), -] -``` - -Python receives a persistent `AllocatableArray` for the module descriptor. -`handle.to_numpy()` exposes a current live view for both plain and `Aliased` -module allocatables. `Aliased` preserves native addressability for other policy; -it is not the extraction switch. NumPy must not free the data. A native -allocate/deallocate routine controls the allocation, and a later native -deallocation or reallocation makes previous views stale. Accessing stale views -is unsupported and may crash. The same handle then reports `allocated is -False`; the module attribute itself does not become `None`. - -Lifecycle: - -```text -Fortran owns descriptor -> handle borrows descriptor -> view borrows allocation - \-> Fortran deallocates authoritative storage -``` - -#### Wrapper-owned component storage - -```python -from x2py.contracts import Allocatable, Annotated, Destruction, Float64, Ownership, Transfer - -class buffer: - values: Annotated[ - Allocatable[Float64[:]], - Ownership("wrapper"), - Transfer("borrowed_view"), - Destruction("wrapper_dealloc"), - ] -``` - -The containing Python extension object owns the native derived-type instance; -the allocatable component belongs to that instance. Access returns an -`AllocatableArray` that retains the wrapper. A NumPy view returned by -`handle.to_numpy()` retains the handle, so the owner chain remains live. The -generated wrapper deallocator finalizes/releases the native instance after the -last owning reference is gone. An explicit native component-deallocation method -may make an existing view stale sooner, so callers must not retain views across -such calls. - -Lifecycle: - -```text -wrapper allocates instance -> field handle retains wrapper -> view retains handle - \-> wrapper finalizes instance -``` - -#### Wrapper-owned result descriptor - -```python -from x2py.contracts import Addr, Allocatable, Annotated, Arg, Destruction, Float64, Int32, Ownership, Return, Transfer, native_call - -@native_call([Addr(Arg(0)), Return("values", 0)]) -def build_values( - n: Int32, -) -> Annotated[ - Allocatable[Float64[:]], - Ownership("wrapper"), - Transfer("wrapper_instance"), - Destruction("wrapper_dealloc"), -]: ... -``` - -Native code produces allocatable output storage. The generated binding -transfers its values into persistent descriptor storage and returns an owned -`AllocatableArray`. The handle remains valid after the native call; -`handle.close()` or finalization releases its allocation. A NumPy view extracted -with `to_numpy()` retains the handle and is not a detached copy. - -Lifecycle: - -```text -Fortran allocates output -> wrapper-owned descriptor receives values - -> result handle releases descriptor on finalization -``` - -These are supported contexts for the same handle concept, not permission to -relabel one allocation arbitrarily. In particular, changing the module-storage -example to `Destruction("wrapper_dealloc")` would be unsafe: the generated -module wrapper has no right to finalize storage owned by Fortran module state. -Call `.copy()` on an extracted view when Python needs an independent NumPy -lifetime. - -### Supported transfer modes - -| Transfer | Supported use | Destruction | -| --- | --- | --- | -| `by_value` | Scalar values returned to Python. | `python_refcount` | -| `call_local` | Converted scalar/string/array inputs, pointer inputs associated only for one call, and explicitly discarded immutable mutation. | `none` or `call_local` | -| `in_place` | Caller-supplied writable scalar storage, NumPy arrays, and existing wrapper instances. | `caller` or the existing wrapper's `wrapper_dealloc` | -| `copy_return` | Strings, ordinary array results, and immutable replacement results copied to Python. | `python_refcount` | -| `snapshot_copy` | Detached copies for explicitly supported projections. Pointer-array handle results instead preserve association in owned descriptor handles; they are not implicit snapshots. | `python_refcount` | -| `borrowed_view` | Target-backed module allocatables and supported fields/components whose owner remains identifiable. | `native_owner` or `wrapper_dealloc` | -| `wrapper_instance` | Derived-type output or owned native descriptor result represented by a Python object controlling persistent wrapper storage. | `wrapper_dealloc` | -| `blocked` | Intentional declaration that no safe implemented transfer exists. | `blocked` | - -### Destruction responsibilities - -| Destruction | Runtime responsibility | -| --- | --- | -| `python_refcount` | Python, NumPy, or a generated base capsule releases Python-owned storage after references are gone. | -| `wrapper_dealloc` | The generated extension object's deallocator finalizes/releases its native instance. | -| `native_owner` | Fortran module state or an external native owner releases storage; Python only borrows. | -| `caller` | The caller retains and releases the object supplied to x2py. | -| `call_local` | Generated bridge cleanup releases the temporary before the wrapper call returns. | -| `none` | x2py created no persistent owned storage for this boundary value. | -| `blocked` | Release responsibility is unknown, contradictory, or not implemented; generation stops. | - -`Ownership("unknown")`, `Transfer("blocked")`, and -`Destruction("blocked")` are useful for making an unresolved contract fail -closed. They are not runtime ownership modes. - -### Ownership combinations that fail - -Examples include: - -- `Immutable` writable storage with `Transfer("borrowed_view")`; -- `Transfer("copy_return")` on an argument with no projected replacement; -- pointer-array results without stable owner storage and target lifetime; -- pointer reassociation or ownership-changing operations without completed - owner, shape, lifetime, and release behavior; -- borrowed pointer views whose policy cannot retain a descriptor owner or - provide descriptor extraction; -- `Ownership("native")` with `Destruction("python_refcount")` for the same - authoritative allocation; and -- `Ownership("python")` with `Destruction("native_owner")`. - -The diagnostic identifies the declaration and rejected policy. x2py does not -silently replace these combinations with its default policy. - -## Package Exports And Namespaces - -The entry `__init__.pyi` controls which leaf modules and declarations enter the -extension's Python export tree. Removing an entry import removes that branch; -adding a relative import adds a contract fragment: - -```python -from . import solver -from .helpers import norm2 -``` - -Leaf files continue to identify native modules. Entry imports compose the -Python package; they do not rename native modules or infer object files. -Supported relative imports include module imports, selective declaration -imports, wildcard flattening, and `as` aliases. Repeating the same export is -idempotent, and exporting both the original name and an alias is allowed when -both exports are explicit. Alias exports share the same native target or -storage, but they do not promise Python object identity: module-variable reads -may return distinct Python objects with the same current value, and function -introspection may show the alias name. Conflicting exports to the same Python -name, missing relative files, and import cycles fail while the contract graph is -loaded. -Only declarations reachable from the entry export policy are emitted as public -Python extension bindings; omitted leaf declarations are not wrapped just -because their leaf file was discovered. - -## Diagnostics For Edited Contracts - -Failures occur at the first layer with enough information: - -1. **Load errors**: invalid Python syntax, unsupported decorators or metadata, - untyped parameters, invalid imports, or import cycles. File-based errors - include the `.pyi` path. -2. **Structural validation errors**: incomplete projections, duplicate native - positions, invalid `@bind`/`@overload` links, conflicting exports, or public - declarations exposing private types. -3. **Policy/planning errors**: incomplete ownership, lifetime, pointer, - coercion, mutation, allocation, or release behavior. -4. **Native build/runtime errors**: the supplied artifact does not implement - the declared symbol or ABI. - -Do not fix a policy blocker by deleting metadata until the wrapper happens to -build. The corrected contract must explicitly describe the intended boundary -behavior and its owner. - -## Troubleshooting - -When an edited contract fails, reduce the edit to one surface change at a time: -visibility, call ordering, ownership, mutability, or type shape. Keep the -generated contract nearby and compare each intentional edit against the native -artifact you are asking x2py to call. - -## Next - -- [Raw Addresses](../guide/raw-addresses.md) for primitive, array, and fixed-string - address boundaries diff --git a/docs/user/reference/fortran-wrapper.md b/docs/user/reference/fortran-wrapper.md index d3a58aacf..5ab1b9ca2 100644 --- a/docs/user/reference/fortran-wrapper.md +++ b/docs/user/reference/fortran-wrapper.md @@ -2,9 +2,9 @@ title: Fortran Wrapper Reference audience: users, advanced users prerequisites: first wrapped module, NumPy basics -related: ../guide/index.md, editing-semantic-pyi-contracts.md, ../language-support/index.md +related: ../guide/index.md, pyi-contracts/index.md, ../language-support/index.md status: maintained -publication: reviewed +publication: draft --- # Fortran Wrapper Reference @@ -271,11 +271,11 @@ the current working directory unless `--out` gives it an explicit path. Generate wrapper sources remain build artifacts; users do not edit them to change the Python API. -The semantic `.pyi` is the editable semantic contract and wrapper-planning surface. +The semantic `.pyi` is the editable contract and wrapper-planning surface. The supported edit workflow, including removal, addition, call projection, -ownership, and destruction, is explained later in Editing Semantic `.pyi` -Contracts. The complete grammar appears later in the Semantic `.pyi` Format -reference. +ownership, and destruction, is explained in +[Editing `.pyi` Contracts](pyi-contracts/index.md). The complete grammar +appears in the Semantic `.pyi` Format reference. The normal CLI build is source-driven: recognizable Fortran sources build wrappers without a stage flag and cannot be combined with `--pyi`. A semantic `.pyi` entry contract also selects the wrapper stage automatically when its @@ -370,8 +370,8 @@ the generated Python API while unaffected public declarations keep their runtime behavior. Misuse handling, diagnostic categories, and risky explicit-contract behavior -are covered later in Editing Semantic `.pyi` Contracts and the Semantic `.pyi` -Format reference. +are covered in [Editing `.pyi` Contracts](pyi-contracts/index.md) and the +Semantic `.pyi` Format reference. The Semantic `.pyi` Wrapper Checklist later records parity completion. diff --git a/docs/user/reference/index.md b/docs/user/reference/index.md index baacf1753..27080d2f5 100644 --- a/docs/user/reference/index.md +++ b/docs/user/reference/index.md @@ -2,7 +2,7 @@ title: Reference audience: users, developers prerequisites: getting started -related: cli-commands.md, python-api.md, fortran-wrapper.md, semantic-pyi-format.md, editing-semantic-pyi-contracts.md +related: cli-commands.md, python-api.md, fortran-wrapper.md, semantic-pyi-format.md, pyi-contracts/index.md status: maintained publication: draft --- @@ -21,7 +21,7 @@ examples, and user guides. - [Fortran wrapper reference](fortran-wrapper.md) - [Semantic IR](semantic-ir.md) - [Semantic .pyi format](semantic-pyi-format.md) -- [Editing semantic .pyi contracts](editing-semantic-pyi-contracts.md) +- [Editing .pyi contracts](pyi-contracts/index.md) - [Diagnostic codes](diagnostic-codes.md) - [Generated functions](generated-functions.md) - [Generated modules](generated-modules.md) diff --git a/docs/user/reference/pyi-contracts/calls-and-results.md b/docs/user/reference/pyi-contracts/calls-and-results.md new file mode 100644 index 000000000..de691dae5 --- /dev/null +++ b/docs/user/reference/pyi-contracts/calls-and-results.md @@ -0,0 +1,166 @@ +--- +title: .pyi Calls and Results +audience: users, advanced users +prerequisites: editing .pyi contracts overview +related: index.md, functions-and-classes.md, ownership-and-lifetimes.md, ../semantic-pyi-format.md, ../../guide/arrays.md, ../../guide/error-handling.md +status: maintained +publication: reviewed +--- + +# Calls and Results + +The function signature describes the Python call. `@native_call(...)` +describes how that call supplies the native arguments. + +## Expose Native Arguments Directly + +When every native argument is visible in native order, `@native_call(...)` is +not needed: + +```python +from x2py.contracts import Int32 + +def scalar_status( + base: Int32[()], + status: Int32[()], +) -> None: ... +``` + +Writable scalar slots use zero-dimensional NumPy arrays: + +```python +import numpy as np + +base = np.array(4, dtype=np.int32) +status = np.empty((), dtype=np.int32) + +module.scalar_status(base, status) +print(status[()]) +``` + +This form also works for arrays and derived objects when their annotations +match the native arguments. For a fixed-width string, a Python `str` can be +passed, but changes made to its temporary native buffer are not visible unless +the contract returns a replacement. + +## Reorder Arguments and Project Outputs + +Use `Returns[...]` for a Python result and `@native_call(...)` when the native +procedure needs hidden output storage, reordered arguments, constants, +lengths, presence flags, shapes, or work buffers: + +```python +from x2py.contracts import Addr, Arg, Int32, Return, Returns, native_call + +@native_call([Addr(Arg(0)), Return("status", 0)]) +def scalar_status(base: Int32) -> Returns["status", Int32]: ... +``` + +Here Python passes one value and receives the native `status` output. Every +required native argument must appear exactly once in the mapping. Missing, +duplicate, and out-of-range positions are errors. + +The mapping may use entries such as `Arg(...)`, `Addr(...)`, `Value(...)`, +`Len(...)`, `IsPresent(...)`, and `Work(...)`. These entries describe the +existing native call; they cannot change what the implementation accepts. See +[Projection Metadata](../semantic-pyi-format.md#projection-metadata) for the +complete syntax. + +There is no `intent` annotation in the `.pyi`. The signature, +`Returns[...]`, and `@native_call(...)` are the complete contract after the +file is loaded. + +## Control Mutation + +`Immutable` means the original Python value must not change. A writable native +argument then needs an explicit replacement result or a supported rule that +discards the temporary mutation: + +```python +from x2py.contracts import Annotated, Float64, Immutable, Returns + +def scale( + values: Annotated[Float64[:], Immutable], +) -> Returns["values", Float64[:]]: ... +``` + +x2py calls the native procedure with separate writable storage and returns the +replacement. The original array remains unchanged. + +Do not combine replacement-only mutation with a writable borrowed view. Those +requests contradict each other and are rejected. + +## Edit Types, Shapes, Layout, and Optionality + +Annotations affect runtime checks; they are not only IDE hints: + +```python +from x2py.contracts import Float64 + +def solve( + matrix: Float64[3, 3], + rhs: Float64[3], +) -> Float64[3]: ... +``` + +Supported edits include: + +- changing an open dimension to a fixed size; +- selecting a supported contiguous layout; +- adding `Immutable` for a supported replacement path; and +- using `T | None` or a default `= ...` for an argument that is genuinely + optional in the native procedure. + +Changing dtype or rank, or inventing optionality, changes the declared native +binary interface. It is valid only when the implementation matches. x2py can +check exact NumPy dtype, rank, shape, layout, writeability, byte order, +alignment, and zero-sized-array rules. Plain multidimensional arrays in a +Fortran contract use Fortran order by default. The +[array guide](../../guide/arrays.md#what-x2py-validates) explains these checks. + +Generic constraints such as `Bounded(1, 8)` and `Finite` can be preserved in a +contract, but they do not yet create runtime validation. Wrapper planning +rejects them instead of silently ignoring them. General implicit dtype +coercion is also unsupported unless a documented path explicitly provides it. + +## Translate Status Results into Exceptions + +Use `@raises(...)` when a projected native status should become a Python +exception: + +```python +from x2py.contracts import Addr, Arg, Int32, Return, String, native_call, raises + +@raises(status="status", message="message", success=0) +@native_call([Addr(Arg(0)), Return("status", 0), Return("message", 1)]) +def solve(value: Int32) -> tuple[Int32, String[32]]: ... +``` + +The named status and optional message must exist in the projected results. A +non-success status raises the generated exception before an ordinary result is +returned. See [Error Handling](../../guide/error-handling.md#status-projection-example) +for the Python behavior. + +## Keep the GIL When Required + +Ordinary native calls release Python's Global Interpreter Lock (GIL) when +their contract allows it. Use `@hold_gil` when the native call must invoke +Python immediately, such as a synchronous callback: + +```python +from x2py.contracts import hold_gil + +@hold_gil +def run_engine() -> None: ... +``` + +Remove `@hold_gil` to return to the normal GIL-releasing behavior when the call +is safe without it. This changes call behavior, not the native procedure +interface. It does not describe a callback signature; callback contracts are +covered in the [Callbacks](../../guide/callbacks.md) guide. + +## Next + +Most users can stop here. Read +[Ownership and Lifetimes](ownership-and-lifetimes.md) only when editing +explicit ownership annotations. diff --git a/docs/user/reference/pyi-contracts/exports-and-modules.md b/docs/user/reference/pyi-contracts/exports-and-modules.md new file mode 100644 index 000000000..65d2777f7 --- /dev/null +++ b/docs/user/reference/pyi-contracts/exports-and-modules.md @@ -0,0 +1,147 @@ +--- +title: .pyi Exports and Modules +audience: users, advanced users +prerequisites: editing .pyi contracts overview +related: index.md, functions-and-classes.md, ../semantic-pyi-format.md, ../../guide/wrapping-modules.md +status: maintained +publication: reviewed +--- + +# Exports and Modules + +The entry `__init__.pyi` controls the extension's Python namespace. Leaf +`.pyi` files describe native modules and their declarations. + +## Choose the Package Shape + +Generated entry contract: + +```python +from . import module1 +from . import module2 +``` + +Python then uses `package.module1` and `package.module2`. To place both +modules' public names directly in `package`, edit the entry contract: + +```python +from .module1 import * +from .module2 import * +``` + +Selective imports and aliases also work: + +```python +from .module1 import solve +from .module2 import reset as clear +``` + +Changing entry imports changes only the Python namespace. It does not rename a +native module or select a native object file. + +Only declarations reachable from `__init__.pyi` are public. Missing files, +import cycles, and two different exports using the same Python name are +errors. Explicit aliases share the same native target, but Python object +identity is not guaranteed for every read. + +## Remove or Hide a Declaration + +Delete a declaration to remove it from the Python API: + +```python +from x2py.contracts import Int32 + +counter: Int32 + +def summarize() -> Int32: ... +``` + +The removed declaration is not regenerated during this build. This works for +functions, variables, classes, methods, fields, constructors, and individual +overload candidates. + +Use `@private` for a function or class that another contract declaration still +needs: + +```python +from x2py.contracts import Float64, private + +@private +def scaled_counter() -> Float64: ... +``` + +Use `private[...]` for a variable or argument: + +```python +from x2py.contracts import Float64, private + +scale: private[Float64] +``` + +Both forms keep the declaration in the contract while hiding it from Python. + +## Add or Rename a Native Procedure + +You may add a declaration when the procedure already exists in the supplied +native implementation: + +```python +from x2py.contracts import Float64 + +def norm2(values: Float64[:]) -> Float64: ... +``` + +In a module leaf, the filename identifies the native module and the function +name selects the native procedure. Use `@bind(...)` when the Python name +differs: + +```python +from x2py.contracts import Float64, Int32, bind + +@bind("solver_step") +def step(values: Float64[:]) -> Int32: ... +``` + +For a standalone external symbol, also use `@external`: + +```python +from x2py.contracts import Float64, bind, external + +@external +@bind("vendor_norm2") +def norm2(values: Float64[:]) -> Float64: ... +``` + +The declaration must include the correct native arguments, types, ranks, and +call shape. Adding Python syntax cannot create a native procedure that is not +present in the linked implementation. + +## Set Module Values at Import + +A writable scalar module variable may have a literal initial value: + +```python +from x2py.contracts import Int32 + +counter: Int32 = 41 +``` + +x2py sets the module variable when the extension is imported. It remains +writable. This works only when x2py can write that native variable, and the +initializer must be a literal rather than a call, name, or expression. + +Use `Final[...]` only for a true read-only constant: + +```python +from x2py.contracts import Final, Int32 + +nmax: Final[Int32] = 12 +``` + +See [Wrapping Modules](../../guide/wrapping-modules.md#shape-the-module-api-with-the-contract) +for the resulting Python usage. + +## Next + +Use [Functions and Classes](functions-and-classes.md) to add methods, +overloads, or a custom constructor. diff --git a/docs/user/reference/pyi-contracts/functions-and-classes.md b/docs/user/reference/pyi-contracts/functions-and-classes.md new file mode 100644 index 000000000..adb88dfb0 --- /dev/null +++ b/docs/user/reference/pyi-contracts/functions-and-classes.md @@ -0,0 +1,121 @@ +--- +title: .pyi Functions and Classes +audience: users, advanced users +prerequisites: editing .pyi contracts overview +related: index.md, exports-and-modules.md, calls-and-results.md, ../semantic-pyi-format.md, ../../guide/wrapping-derived-types.md, ../../guide/generic-interfaces.md +status: maintained +publication: reviewed +--- + +# Functions and Classes + +Declarations may be moved into a more useful Python shape while still calling +the same native procedures. + +## Expose a Module Procedure as a Method + +Keep the module procedure declaration and add a method that calls it. `Pass()` +places `self` in the native argument list: + +```python +from x2py.contracts import Addr, Arg, Float64, Pass, native_call, private + +class point: + @native_call([Pass(), Addr(Arg(0))]) + def move(self, dx: Float64) -> None: ... + +@private +@native_call([Arg(0), Addr(Arg(1))]) +def move(item: point, dx: Float64) -> None: ... +``` + +Python exposes `item.move(dx)`. The private module declaration keeps the native +procedure information but is not callable from Python. Remove `@private` when +both `move(item, dx)` and `item.move(dx)` should be public. + +The method name normally selects the native procedure. Add `@bind("move")` to +the method when its Python name differs from that procedure. + +## Edit an Overload Set + +Each `@overload(...)` declaration is one runtime candidate: + +```python +from x2py.contracts import Addr, Arg, Float64, Int32, bind, native_call, overload, private + +@private +@native_call([Addr(Arg(0))]) +def scale_integer(value: Int32) -> Int32: ... + +@private +@native_call([Addr(Arg(0))]) +def scale_real(value: Float64) -> Float64: ... + +@overload("scale_integer") +def scale(value: Int32) -> Int32: ... + +@overload("scale_real") +def scale(value: Float64) -> Float64: ... +``` + +- Delete one overload declaration to remove only that accepted signature. +- Add a candidate only when its linked concrete procedure exists. +- Keep candidates distinguishable by supported runtime dtype and rank. +- `@private` changes Python visibility; it does not make a native-private + procedure callable. + +Without `@bind`, a candidate calls the concrete procedure named by +`@overload`. When the callable native target must instead be a public generic, +bind it explicitly: + +```python +@bind("convert") +@overload("convert_integer") +def convert_number(value: Int32) -> Int32: ... +``` + +The overload string still links the concrete contract; `@bind("convert")` +selects the native call target. + +## Replace the Constructor + +Generated classes have either a field-keyword constructor or a no-argument +native constructor. Replace it with one concrete native initializer by editing +`__init__`: + +```python +from x2py.contracts import Addr, Arg, Int32, Pass, bind, native_call + +class state: + @bind("init_state") + @native_call([Pass(), Addr(Arg(0))]) + def __init__(self, size: Int32) -> None: ... +``` + +`Pass()` places the newly created `state` object in the native call. Its +position must match the initializer's native argument order, and the selected +native argument must accept `state`. + +Remove the old generated `__init__` when replacing it. Deleting `__init__` +without adding another one makes public construction unavailable. + +## Type-Bound and Magic Methods + +Type-bound and magic methods follow the same rules: + +- keep a concrete native procedure declaration; +- place `self` with `Pass()` when the native call needs it; +- use `@bind(...)` when the Python and native names differ; and +- use `@overload(...)` when one Python method accepts several native + signatures. + +See [Wrapping Derived Types](../../guide/wrapping-derived-types.md#type-bound-methods) +for ordinary methods and +[Defined Operators](../../guide/wrapping-derived-types.md#defined-operators) +for magic methods. Each public declaration must retain a concrete, callable +native target with an exact argument mapping. + +## Next + +Use [Calls and Results](calls-and-results.md) to change how native arguments +appear at the Python boundary. diff --git a/docs/user/reference/pyi-contracts/index.md b/docs/user/reference/pyi-contracts/index.md new file mode 100644 index 000000000..ccfd17e2b --- /dev/null +++ b/docs/user/reference/pyi-contracts/index.md @@ -0,0 +1,133 @@ +--- +title: Editing .pyi Contracts +audience: users, advanced users +prerequisites: generated .pyi contract, wrapper build workflow +related: exports-and-modules.md, functions-and-classes.md, calls-and-results.md, ownership-and-lifetimes.md, ../semantic-pyi-format.md +status: maintained +publication: reviewed +--- + +# Editing `.pyi` Contracts + +x2py's generated `.pyi` files are editable wrapper contracts. They look like +Python stubs, but they also describe native calls, storage, and results. Edit +them to change the Python API without changing the native implementation. + +This section explains supported edits and their effect. For the exact syntax +of every annotation and decorator, use the +[Semantic `.pyi` Format](../semantic-pyi-format.md). + +## Workflow + +Generate a starter contract: + +```bash +python3 -m x2py generate --pyi native/solver.f90 --out contracts/solver +``` + +Edit `contracts/solver/__init__.pyi` and its leaf `.pyi` files, then build from +the entry contract: + +```bash +python3 -m x2py contracts/solver/__init__.pyi \ + --native-fortran-sources native/solver.f90 \ + --out-dir build/solver +``` + +You can provide compiled objects or libraries instead of source. In either +case, the `.pyi` files define the Python API and the native files provide its +implementation. x2py does not reread the native source to restore declarations +you removed from the contract. + +Keep an unchanged generated copy while experimenting. It makes each edit easy +to compare and undo. + +## What Do You Want to Change? + +### Names, Visibility, and Modules + +- [How do I rename or alias a function, variable, or class?](exports-and-modules.md#choose-the-package-shape) +- [How do I reorganize a module's Python namespace?](exports-and-modules.md#choose-the-package-shape) +- [How do I flatten modules or choose what appears at the package root?](exports-and-modules.md#choose-the-package-shape) +- [How do I rename a function without changing its native target?](exports-and-modules.md#add-or-rename-a-native-procedure) +- [How do I hide or remove a function, variable, class, or class member?](exports-and-modules.md#remove-or-hide-a-declaration) +- [How do I add a procedure that already exists in the native implementation?](exports-and-modules.md#add-or-rename-a-native-procedure) +- [How do I set a module variable when the extension is imported?](exports-and-modules.md#set-module-values-at-import) +- [How do I declare a true read-only constant?](exports-and-modules.md#set-module-values-at-import) + +### Functions and Classes + +- [How do I turn a module procedure into a method?](functions-and-classes.md#expose-a-module-procedure-as-a-method) +- [How do I add or remove a function overload?](functions-and-classes.md#edit-an-overload-set) +- [How do I replace or remove a class constructor?](functions-and-classes.md#replace-the-constructor) +- [How do I add or edit a type-bound or magic method?](functions-and-classes.md#type-bound-and-magic-methods) + +### Arguments, Calls, and Results + +- [How do I expose every native argument directly in its native order?](calls-and-results.md#expose-native-arguments-directly) +- [How do I reorder or hide arguments, or turn outputs into Python results?](calls-and-results.md#reorder-arguments-and-project-outputs) +- [How do I pass values, addresses, lengths, presence flags, or temporary work storage?](calls-and-results.md#reorder-arguments-and-project-outputs) +- [How do I change a NumPy dtype, shape, layout, or optional argument?](calls-and-results.md#edit-types-shapes-layout-and-optionality) +- [How do I add a default for a genuinely optional native argument?](calls-and-results.md#edit-types-shapes-layout-and-optionality) +- [How do I return a replacement instead of mutating the original Python value?](calls-and-results.md#control-mutation) +- [How do I pass checked storage or a raw memory address?](../../guide/raw-addresses.md#checked-storage-or-raw-address) +- [How do I turn a native status into a Python exception?](calls-and-results.md#translate-status-results-into-exceptions) +- [How do I keep Python's Global Interpreter Lock (GIL) during a call, or return to the normal releasing behavior?](calls-and-results.md#keep-the-gil-when-required) +- [How do I define or change a callback signature?](../../guide/callbacks.md#choosing-the-prototype-spelling) + +### Storage and Lifetimes + +- [How do I understand or edit a complete ownership rule?](ownership-and-lifetimes.md#the-complete-ownership-rule) +- [How do I choose a value, copy, view, existing object, or new handle?](ownership-and-lifetimes.md#transfer-values) +- [How do I decide who releases persistent storage?](ownership-and-lifetimes.md#destruction-values) +- [How do I understand allocatable or pointer handle ownership?](ownership-and-lifetimes.md#common-handle-cases) +- [Why is an ownership combination rejected?](ownership-and-lifetimes.md#combinations-that-are-rejected) + +Most edits change the Python surface: names, visibility, grouping, or how +native arguments appear as Python parameters and results. + +Some facts must continue to match the supplied implementation: + +- native module and symbol names; +- procedure kind and native argument order; +- datatype, kind, rank, and storage category; +- callback signature; and +- required native imports. + +x2py checks that the contract is internally consistent. It cannot prove that +an arbitrary object or shared library has the binary interface described by +the contract. A contract that gives false native facts may fail while +building, importing, or calling the extension. + +## Safety Checklist + +Before rebuilding: + +- Start from a contract generated for the same native implementation. +- Make one kind of edit at a time. +- Keep native types, ranks, argument order, and symbol names accurate. +- Do not invent optionality, ownership, or a release method. +- Rebuild and call the edited path once before making the next change. + +When x2py rejects an incomplete or unsafe rule, fix the contract instead of +removing metadata until the build happens to pass. + +## Understanding Errors + +- **While loading the `.pyi`:** check Python syntax, imports, decorators, + annotations, and import cycles. +- **While checking the contract:** check duplicate exports, missing links, + invalid projections, and public declarations that expose private types. +- **While planning the wrapper:** check ownership, lifetime, mutation, + allocation, conversion, and release rules. +- **While building or calling:** check that the supplied implementation + matches the declared native symbol and binary interface. + +Errors include the contract path and declaration when that information is +available. Use `--verbose` to see the build commands; use `--debug` when a full +Python traceback is needed. + +## Next + +Start with [Exports and Modules](exports-and-modules.md) for the most common +API edits. diff --git a/docs/user/reference/pyi-contracts/ownership-and-lifetimes.md b/docs/user/reference/pyi-contracts/ownership-and-lifetimes.md new file mode 100644 index 000000000..d8a5dc6f4 --- /dev/null +++ b/docs/user/reference/pyi-contracts/ownership-and-lifetimes.md @@ -0,0 +1,121 @@ +--- +title: .pyi Ownership and Lifetimes +audience: advanced users +prerequisites: editing .pyi contracts overview, allocatable and pointer handles +related: index.md, calls-and-results.md, ../semantic-pyi-format.md, ../../guide/memory-management.md, ../../guide/allocatables.md, ../../guide/pointers.md +status: maintained +publication: reviewed +--- + +# Ownership and Lifetimes + +Generated contracts already contain the ownership rules needed by ordinary +wrappers. Edit these annotations only when deliberately changing how storage +crosses the Python boundary. + +## The Complete Ownership Rule + +An explicit rule has three parts: + +```python +from x2py.contracts import Annotated, Destruction, Float64, Ownership, Transfer + +values: Annotated[ + Float64[:], + Ownership("native"), + Transfer("borrowed_view"), + Destruction("native_owner"), +] +``` + +| Annotation | Question it answers | +| --- | --- | +| `Ownership(...)` | Who owns the original storage? | +| `Transfer(...)` | Does Python receive a value, copy, view, existing object, or new handle? | +| `Destruction(...)` | Who releases persistent storage? | + +The three values form one rule. x2py checks them together with the value's +kind, storage, mutability, nullability, position, and available release +operation. An annotation cannot change where an existing allocation came from. + +Storage markers such as `Allocatable`, `Pointer`, `Aliased`, and +`PointerPolicy(...)` describe the native declaration. Keep them accurate; they +do not by themselves grant permission to copy, release, or reassociate +storage. + +## Common Handle Cases + +An allocatable or pointer array handle stores a native descriptor: the metadata +that records the current storage, shape, and strides. + +The same allocatable-array idea needs different rules in different places: + +| Where the allocatable lives | Ownership | Transfer | Destruction | What Python receives | +| --- | --- | --- | --- | --- | +| Module variable | `native` | `borrowed_view` | `native_owner` | A handle to storage controlled by the module | +| Field of a Python class instance | `wrapper` | `borrowed_view` | `wrapper_dealloc` | A field handle that keeps its class instance alive | +| Function result with persistent descriptor storage | `wrapper` | `wrapper_instance` | `wrapper_dealloc` | An owned result handle that can be closed | + +For a module variable, Python must not claim the right to release module +storage. For a class field, the field handle keeps the containing object alive. +For an owned result, `close()` or finalization releases the handle and its +allocation. + +`handle.to_numpy()` returns a view that keeps the handle alive; it is not a +detached copy. A later native deallocation or reallocation can still make an +older view stale. Use `.copy()` when the NumPy array needs an independent +lifetime. The [Memory Management](../../guide/memory-management.md) guide +explains these handle rules in everyday terms. + +## Transfer Values + +| Transfer | Supported use | +| --- | --- | +| `by_value` | A scalar value returned to Python | +| `call_local` | Temporary converted input, one-call pointer association, or intentionally discarded mutation | +| `in_place` | Caller-supplied writable storage or an existing wrapper object | +| `copy_return` | A string, ordinary array result, or explicit replacement copied to Python | +| `snapshot_copy` | A detached copy for a projection that explicitly supports it | +| `borrowed_view` | A view whose storage owner is known and kept alive | +| `wrapper_instance` | A derived object or descriptor result held by a Python extension object | +| `blocked` | An explicit statement that no safe implemented transfer is known | + +Pointer-array handle results preserve their association in descriptor handles; +they are not implicit snapshots. + +## Destruction Values + +| Destruction | Who releases the storage | +| --- | --- | +| `python_refcount` | Python, NumPy, or a generated base object | +| `wrapper_dealloc` | The generated extension object's deallocator | +| `native_owner` | The module or another native owner | +| `caller` | The caller that supplied the object | +| `call_local` | Temporary cleanup before the wrapper call returns | +| `none` | No persistent storage was created for this boundary value | +| `blocked` | Release responsibility is unknown or unsupported | + +`Ownership("unknown")`, `Transfer("blocked")`, and +`Destruction("blocked")` make an unresolved contract fail safely. They are not +working runtime ownership modes. + +## Combinations That Are Rejected + +x2py rejects rules such as: + +- replacement-only writable storage with `Transfer("borrowed_view")`; +- `Transfer("copy_return")` without a projected replacement; +- a pointer-array result with no stable descriptor owner or target lifetime; +- pointer reassociation without complete owner, shape, lifetime, and release + behavior; +- `Ownership("native")` with `Destruction("python_refcount")` for the same + allocation; and +- `Ownership("python")` with `Destruction("native_owner")`. + +The error identifies the declaration and rejected rule. x2py does not replace +it with a guessed default. + +## Next + +Use the [Semantic `.pyi` Format](../semantic-pyi-format.md#ownership-transfer-and-destruction-policies) +for the complete annotation grammar. diff --git a/docs/user/reference/semantic-pyi-format.md b/docs/user/reference/semantic-pyi-format.md index ff693f715..ee55f3e39 100644 --- a/docs/user/reference/semantic-pyi-format.md +++ b/docs/user/reference/semantic-pyi-format.md @@ -11,7 +11,7 @@ publication: draft For the supported edit workflow and runtime consequences of changing a contract, including ownership and destruction examples, see -[Editing semantic `.pyi` contracts](editing-semantic-pyi-contracts.md). +[Editing `.pyi` contracts](pyi-contracts/index.md). - [x] Generic `Annotated` constraints and semantic coercions are not silently accepted as runtime validation. Fortran wrapper planning reports direct blockers until named validators or conversion actions exist. Evidence: - wrapper-plan tests that prove generic constraints without a runtime validator fail - and `docs/user/reference/semantic-pyi-format.md`. + wrapper-plan tests that prove generic constraints without a runtime validator + fail. +- [ ] Implement runtime validators for `Bounded(...)` and `Finite`, connect + them to completed wrapper policy and generated calls, and document them for + users only after runtime enforcement is tested. +- [ ] Implement explicit runtime dtype-conversion actions before documenting + contract-controlled coercion as a user feature. - [x] The currently documented editable-contract surface has direct modified runtime evidence or focused semantic/planning evidence: removal and hiding, added and renamed bindings, overload pruning and renamed overload groups, native-order identity calls without `@native_call`, immutable replacement, - ownership triples, pointer-policy blockers, runtime constraints, `@raises`, + ownership triples, pointer-policy blockers, `@raises`, `@hold_gil`, and native-artifact failures. Evidence: `docs/user/reference/pyi-contracts/`, `tests/wrapper/fortran/edit_pyi_contracts/`, diff --git a/docs/user/getting-started/beginner-workflow.md b/docs/user/getting-started/beginner-workflow.md index 527b49ba4..6002d167c 100644 --- a/docs/user/getting-started/beginner-workflow.md +++ b/docs/user/getting-started/beginner-workflow.md @@ -107,6 +107,9 @@ The User Guide introduces small edits next to the feature they affect, such as renaming a function, changing array layout, adding an overload, or exposing a module procedure as a method. +Use [Editing `.pyi` Contracts](../reference/pyi-contracts/index.md) to find +every supported edit and its complete rules. + --- ## 5. Diagnose a Failure diff --git a/docs/user/guide/arrays.md b/docs/user/guide/arrays.md index 2a260f1c1..29bab497e 100644 --- a/docs/user/guide/arrays.md +++ b/docs/user/guide/arrays.md @@ -242,6 +242,9 @@ def sum_columns( ) -> None: ... ``` +For the complete dtype, shape, layout, and optionality rules, see +[Edit Types, Shapes, Layout, and Optionality](../reference/pyi-contracts/calls-and-results.md#edit-types-shapes-layout-and-optionality). + The call does not change. Pass a C-order array instead. The same values now produce row sums: diff --git a/docs/user/guide/error-handling.md b/docs/user/guide/error-handling.md index 23e0b6f83..53e971032 100644 --- a/docs/user/guide/error-handling.md +++ b/docs/user/guide/error-handling.md @@ -77,6 +77,9 @@ except RuntimeError as e: print(e) ``` +For the complete status and message rules, see +[Translate Status Results into Exceptions](../reference/pyi-contracts/calls-and-results.md#translate-status-results-into-exceptions). + --- ## Common Python Exceptions diff --git a/docs/user/guide/generic-interfaces.md b/docs/user/guide/generic-interfaces.md index 2561884ed..bc06094a8 100644 --- a/docs/user/guide/generic-interfaces.md +++ b/docs/user/guide/generic-interfaces.md @@ -164,6 +164,9 @@ If the concrete procedure is private in Fortran, keep `@bind("convert")` on the overload so the native call goes through the public generic. +For the complete add, remove, and binding rules, see +[Edit an Overload Set](../reference/pyi-contracts/functions-and-classes.md#edit-an-overload-set). + --- ## Key Rules diff --git a/docs/user/guide/raw-addresses.md b/docs/user/guide/raw-addresses.md index bf93db460..9664265ad 100644 --- a/docs/user/guide/raw-addresses.md +++ b/docs/user/guide/raw-addresses.md @@ -2,7 +2,7 @@ title: Raw Addresses description: Pass primitive, array, and fixed-string storage addresses through semantic contracts audience: advanced users -prerequisites: arrays, strings, editing semantic pyi contracts +prerequisites: arrays, strings, editing .pyi contracts related: data-types.md, arrays.md, strings.md status: maintained publication: reviewed @@ -102,6 +102,9 @@ python3 -m x2py contracts/raw/__init__.pyi \ --out-dir build/raw ``` +For other argument-order and result mappings, see +[Reorder Arguments and Project Outputs](../reference/pyi-contracts/calls-and-results.md#reorder-arguments-and-project-outputs). + ## Primitive Address Keep the NumPy owner in a variable for the full call: diff --git a/docs/user/guide/strings.md b/docs/user/guide/strings.md index 3e9b533f5..433322978 100644 --- a/docs/user/guide/strings.md +++ b/docs/user/guide/strings.md @@ -100,6 +100,9 @@ python3 -m x2py contracts/strings/__init__.pyi \ --out-dir build/strings ``` +For the complete result-mapping rules, see +[Reorder Arguments and Project Outputs](../reference/pyi-contracts/calls-and-results.md#reorder-arguments-and-project-outputs). + ## Immutable Values `String[8]` accepts a Python `str` whose encoded length is exactly eight bytes. diff --git a/docs/user/guide/wrapping-derived-types.md b/docs/user/guide/wrapping-derived-types.md index bd1d1c39f..517d4cb9e 100644 --- a/docs/user/guide/wrapping-derived-types.md +++ b/docs/user/guide/wrapping-derived-types.md @@ -177,6 +177,9 @@ The edit changes construction only; it does not create After rebuilding, `points.point.__init__.__doc__` starts with `point(x, y) -> point` and lists both parameters. +For the complete replacement rules, see +[Replace the Constructor](../reference/pyi-contracts/functions-and-classes.md#replace-the-constructor). + --- ## Type-Bound Methods @@ -242,6 +245,9 @@ module-level declaration. The class docstring now lists `move(dx, dy) -> None` under `Methods`. `points.point.move.__doc__` contains its complete parameter and return details. +For the complete mapping rules, see +[Expose a Module Procedure as a Method](../reference/pyi-contracts/functions-and-classes.md#expose-a-module-procedure-as-a-method). + --- ## Type-Bound Generics @@ -369,6 +375,9 @@ At least one operand must be a wrapped derived type. Other operands can be supported primitive scalars, arrays, or generated classes. Their dispatch is exact. +For the overload rules shared by type-bound generics and operators, see +[Edit an Overload Set](../reference/pyi-contracts/functions-and-classes.md#edit-an-overload-set). + --- ## Next diff --git a/docs/user/guide/wrapping-functions.md b/docs/user/guide/wrapping-functions.md index 5280a499c..d348d35a0 100644 --- a/docs/user/guide/wrapping-functions.md +++ b/docs/user/guide/wrapping-functions.md @@ -69,6 +69,9 @@ re-exports the old Python name. Build the edited package using the The same rule applies to functions, subroutines, and methods. +For the complete naming rules, see +[Add or Rename a Native Procedure](../reference/pyi-contracts/exports-and-modules.md#add-or-rename-a-native-procedure). + --- ## Array Return Values diff --git a/docs/user/guide/wrapping-modules.md b/docs/user/guide/wrapping-modules.md index bc7d1eea8..d981f95f0 100644 --- a/docs/user/guide/wrapping-modules.md +++ b/docs/user/guide/wrapping-modules.md @@ -104,6 +104,11 @@ Deleting a declaration removes that name from the generated Python API. These edits do not create or rename native variables and procedures; those still need to exist in the compiled module. +For the complete rules, see +[Remove or Hide a Declaration](../reference/pyi-contracts/exports-and-modules.md#remove-or-hide-a-declaration) +and +[Set Module Values at Import](../reference/pyi-contracts/exports-and-modules.md#set-module-values-at-import). + --- ## Flatten Module Namespaces @@ -164,6 +169,9 @@ also import only selected names instead of flattening every public declaration. Build the edited entry using the [editable-contract workflow](../getting-started/beginner-workflow.md#4-optionally-edit-the-contract). +For all supported imports, aliases, and namespace layouts, see +[Choose the Package Shape](../reference/pyi-contracts/exports-and-modules.md#choose-the-package-shape). + --- ## Important Rules diff --git a/docs/user/reference/pyi-contracts/calls-and-results.md b/docs/user/reference/pyi-contracts/calls-and-results.md index de691dae5..aa96dd29f 100644 --- a/docs/user/reference/pyi-contracts/calls-and-results.md +++ b/docs/user/reference/pyi-contracts/calls-and-results.md @@ -62,9 +62,9 @@ duplicate, and out-of-range positions are errors. The mapping may use entries such as `Arg(...)`, `Addr(...)`, `Value(...)`, `Len(...)`, `IsPresent(...)`, and `Work(...)`. These entries describe the -existing native call; they cannot change what the implementation accepts. See -[Projection Metadata](../semantic-pyi-format.md#projection-metadata) for the -complete syntax. +existing native call; they cannot change what the implementation accepts. The +complete projection grammar will be covered by the Semantic `.pyi` Format +reference. There is no `intent` annotation in the `.pyi`. The signature, `Returns[...]`, and `@native_call(...)` are the complete contract after the @@ -118,11 +118,6 @@ alignment, and zero-sized-array rules. Plain multidimensional arrays in a Fortran contract use Fortran order by default. The [array guide](../../guide/arrays.md#what-x2py-validates) explains these checks. -Generic constraints such as `Bounded(1, 8)` and `Finite` can be preserved in a -contract, but they do not yet create runtime validation. Wrapper planning -rejects them instead of silently ignoring them. General implicit dtype -coercion is also unsupported unless a documented path explicitly provides it. - ## Translate Status Results into Exceptions Use `@raises(...)` when a projected native status should become a Python diff --git a/docs/user/reference/pyi-contracts/index.md b/docs/user/reference/pyi-contracts/index.md index ccfd17e2b..98fc39474 100644 --- a/docs/user/reference/pyi-contracts/index.md +++ b/docs/user/reference/pyi-contracts/index.md @@ -13,9 +13,8 @@ x2py's generated `.pyi` files are editable wrapper contracts. They look like Python stubs, but they also describe native calls, storage, and results. Edit them to change the Python API without changing the native implementation. -This section explains supported edits and their effect. For the exact syntax -of every annotation and decorator, use the -[Semantic `.pyi` Format](../semantic-pyi-format.md). +This section explains supported edits and their effect. The complete grammar +will be covered by the Semantic `.pyi` Format reference. ## Workflow @@ -73,7 +72,7 @@ to compare and undo. - [How do I pass checked storage or a raw memory address?](../../guide/raw-addresses.md#checked-storage-or-raw-address) - [How do I turn a native status into a Python exception?](calls-and-results.md#translate-status-results-into-exceptions) - [How do I keep Python's Global Interpreter Lock (GIL) during a call, or return to the normal releasing behavior?](calls-and-results.md#keep-the-gil-when-required) -- [How do I define or change a callback signature?](../../guide/callbacks.md#choosing-the-prototype-spelling) +- [How do I describe a callback signature in the contract?](../../guide/callbacks.md#choosing-the-prototype-spelling) ### Storage and Lifetimes diff --git a/docs/user/reference/pyi-contracts/ownership-and-lifetimes.md b/docs/user/reference/pyi-contracts/ownership-and-lifetimes.md index d8a5dc6f4..975324cfc 100644 --- a/docs/user/reference/pyi-contracts/ownership-and-lifetimes.md +++ b/docs/user/reference/pyi-contracts/ownership-and-lifetimes.md @@ -117,5 +117,6 @@ it with a guessed default. ## Next -Use the [Semantic `.pyi` Format](../semantic-pyi-format.md#ownership-transfer-and-destruction-policies) -for the complete annotation grammar. +Return to [Editing `.pyi` Contracts](index.md) to choose another kind of edit. +The complete annotation grammar will be covered by the Semantic `.pyi` Format +reference. diff --git a/docs/user/reference/semantic-pyi-format.md b/docs/user/reference/semantic-pyi-format.md index ee55f3e39..fad42c6e4 100644 --- a/docs/user/reference/semantic-pyi-format.md +++ b/docs/user/reference/semantic-pyi-format.md @@ -1104,13 +1104,12 @@ Use local constants or generated `Final[...]` names for shape symbols. ## Metadata With `Annotated` -`Annotated[...]` carries storage metadata and semantic constraints. It does -not carry source-language argument direction or per-call value/reference -selection. The Fortran semantic pipeline supplies `ORDER_F` as the default -multidimensional layout. Generated contracts omit that default. Write explicit -layout metadata only when the Python-visible storage deliberately differs from -that Fortran representation, such as a row-major input accepted by a Fortran -wrapper. +`Annotated[...]` carries storage and call-boundary metadata. It does not carry +source-language argument direction or per-call value/reference selection. The +Fortran semantic pipeline supplies `ORDER_F` as the default multidimensional +layout. Generated contracts omit that default. Write explicit layout metadata +only when the Python-visible storage deliberately differs from that Fortran +representation, such as a row-major input accepted by a Fortran wrapper. Native call transport belongs to `@native_call`: a wrapped derived object uses its normal reference handoff with `Arg(i)` and exact typed value handoff with `Value(Arg(i))`. The Python API accepts the same opaque wrapper @@ -1310,14 +1309,6 @@ For array descriptors, use `Allocatable[T[...]]` and `Pointer[T[...]]`. active public descriptor spellings. Derived module objects remain live objects; there is no public whole-object snapshot annotation. -Other positional `Annotated` helpers are preserved as semantic constraints: - -```python -from x2py.contracts import Annotated, Bounded, Finite, Int32 - -value: Annotated[Int32, Bounded(1, 8), Finite] -``` - ### Ownership, Transfer, And Destruction Policies Ownership metadata is consumed by the centralized wrapper ownership policy. @@ -2383,15 +2374,9 @@ Loaded but usually not generated from source today: | --- | --- | | `Addr[n](T)` for `n > 1` | direct low-level pointer topology | | `ORDER_ANY` | edited orientation-independent array contract | -| generic `Annotated` constraints | preserved semantic constraints | | additional `@native_call` and `Returns[...]` edits | projection metadata beyond generated output mappings | | source-provenance array helpers | compatibility loading for older or edited stubs | -Generic constraints are not silently treated as runtime checks. Fortran wrapper -wrapper planning reports `fortran_runtime_constraints_unsupported` until a named -constraint has an implemented validator. Semantic coercions similarly report -`fortran_runtime_coercions_unsupported` until a conversion action exists. - ## Rejected Or Not Yet Supported The parser or post-IR policy-completion stage rejects contracts that would be diff --git a/tests/docs/test_structure.py b/tests/docs/test_structure.py index dae19f47d..9a33a76b2 100644 --- a/tests/docs/test_structure.py +++ b/tests/docs/test_structure.py @@ -37,6 +37,8 @@ MARKDOWN_LINK = re.compile(r"\[[^\]]+\]\(([^)#]+)(?:#[^)]+)?\)") NEXT_NAVIGATION = re.compile(r"^\s*(?:#{2,6}\s+Next|\*\*Next\*\*:?)\s*$", re.IGNORECASE) NEXT_SECTION_BOUNDARY = re.compile(r"^\s*(?:#{2,6}\s+|---\s*$|\*\*[^*]+\*\*)") +ALLOWED_CONTEXTUAL_FORWARD_LINK_SOURCE_PREFIXES = ("user/getting-started/", "user/guide/") +ALLOWED_CONTEXTUAL_FORWARD_LINK_PREFIXES = ("user/reference/pyi-contracts/",) C_DOCS_START = "" C_DOCS_DISABLED = "