diff --git a/.githooks/pre-push b/.githooks/pre-push index 6763c652b..3a57292f1 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -12,7 +12,7 @@ DOCUMENTATION_SMOKE_TESTS = ( "tests/docs/test_user_content.py", ) WRAPPER_SMOKE_TEST = ( - "tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::" + "tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::" "test_fortran_wrapper_default_module_name_does_not_collide_with_root_function" ) REQUIRED_TESTS = ("tests/tools", "tests/workflows") diff --git a/.github/workflows/merge-validation.yml b/.github/workflows/merge-validation.yml index 854f1236c..979db129e 100644 --- a/.github/workflows/merge-validation.yml +++ b/.github/workflows/merge-validation.yml @@ -439,113 +439,21 @@ jobs: python tools/print_pytest_failures.py "$report" done - native-libraries: - name: BLAS + LAPACK + FFTPACK + MINPACK · Ubuntu 24.04 · Python 3.12 + real-libraries-portability: + name: Real Libraries Portability needs: [unit-tests, unit-tests-macos] if: >- ${{ !contains(github.event.pull_request.labels.*.name, 'ignore-real-library-wrappers') }} - runs-on: ubuntu-24.04 - timeout-minutes: 120 - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 2 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install test dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e ".[qa]" - python -m pip install \ - "numpy==2.5.1" \ - "meson==1.11.2" \ - "ninja==1.13.0" \ - "scipy==1.18.0" - - name: Install pinned GFortran and LAPACK link dependencies - shell: bash - run: | - packages=(libblas-dev liblapack-dev) - if ! command -v "$PRIK_GFORTRAN_BINARY" >/dev/null 2>&1; then - packages+=("$PRIK_GFORTRAN_PACKAGE") - fi - sudo apt-get update - sudo apt-get install --yes "${packages[@]}" - compiler_dir="$RUNNER_TEMP/prik-gfortran" - mkdir -p "$compiler_dir" - ln -sf "$(command -v "$PRIK_GFORTRAN_BINARY")" "$compiler_dir/gfortran" - echo "$compiler_dir" >> "$GITHUB_PATH" - "$compiler_dir/gfortran" --version - - name: Restore compiled native library cache - uses: actions/cache@v4 - with: - path: ${{ runner.temp }}/prik-real-library-native - key: real-libraries-${{ runner.os }}-gfortran13-${{ hashFiles('examples/blas/native/**', 'examples/lapack/native/**') }} - - name: Run BLAS example and CI full-surface audit - env: - PYTHONPATH: . - HYPOTHESIS_PROFILE: ci - PRIK_REAL_LIBRARY_NATIVE_CACHE_DIR: ${{ runner.temp }}/prik-real-library-native - run: | - source examples/blas/build_all.sh - python -m pytest -q examples/blas/tests examples/blas/ci/full_surface.py - - name: Report reviewed LAPACK inventory - env: - PYTHONPATH: . - run: | - python - <<'PY' - from examples.lapack.routine_inventory import ( - EXPECTED_LAPACK_PROCEDURES, - EXPECTED_LAPACK_SOURCE_FILES, - F2PY_SCALAR_WRITEBACK_ROUTINES, - ROUTINE_GROUPS, - ROUTINES, - SCIPY_VERSION, - ) - - print(f"SciPy version: {SCIPY_VERSION}") - print(f"LAPACK implementation sources: {EXPECTED_LAPACK_SOURCE_FILES}") - print(f"Expected PRIK procedures: {EXPECTED_LAPACK_PROCEDURES}") - print(f"Selected float64 correctness routines: {len(ROUTINES)}") - print(f"f2py scalar writebacks: {len(F2PY_SCALAR_WRITEBACK_ROUTINES)}") - for family, routines in ROUTINE_GROUPS.items(): - print(f" {family}: {len(routines)}") - PY - - name: Run LAPACK example and CI full-surface audit - env: - PYTHONPATH: . - HYPOTHESIS_PROFILE: ci - PRIK_REAL_LIBRARY_NATIVE_CACHE_DIR: ${{ runner.temp }}/prik-real-library-native - run: | - source examples/lapack/build_all.sh - python -m pytest -q examples/lapack/tests examples/lapack/ci/full_surface.py - - name: Run FFTPACK 31-procedure full-surface audit - env: - PYTHONPATH: . - HYPOTHESIS_PROFILE: ci - run: | - source examples/fftpack/build_all.sh - python -m pytest -q examples/fftpack/tests - - name: Run MINPACK 22-procedure and parameter-array full-surface audit - env: - PYTHONPATH: . - HYPOTHESIS_PROFILE: ci - run: | - source examples/minpack/build_all.sh - python -m pytest -q examples/minpack/tests + uses: ./.github/workflows/real-libraries-portability.yml documentation-benchmark: name: Documentation performance benchmark · Ubuntu 24.04 ARM64 · Python 3.12 - needs: native-libraries + needs: real-libraries-portability if: >- ${{ always() && - (needs.native-libraries.result == 'success' || - (needs.native-libraries.result == 'skipped' && + (needs.real-libraries-portability.result == 'success' || + (needs.real-libraries-portability.result == 'skipped' && contains(github.event.pull_request.labels.*.name, 'ignore-real-library-wrappers'))) }} runs-on: ubuntu-24.04-arm @@ -687,7 +595,7 @@ jobs: - compiler-smoke-macos - unit-tests - unit-tests-macos - - native-libraries + - real-libraries-portability - documentation-benchmark - documentation-build runs-on: ubuntu-24.04 @@ -698,10 +606,10 @@ jobs: COMPILER_SMOKE_MACOS_RESULT: ${{ needs.compiler-smoke-macos.result }} UNIT_TESTS_RESULT: ${{ needs.unit-tests.result }} UNIT_TESTS_MACOS_RESULT: ${{ needs.unit-tests-macos.result }} - NATIVE_LIBRARIES_RESULT: ${{ needs.native-libraries.result }} + REAL_LIBRARIES_PORTABILITY_RESULT: ${{ needs.real-libraries-portability.result }} DOCUMENTATION_BENCHMARK_RESULT: ${{ needs.documentation-benchmark.result }} DOCUMENTATION_BUILD_RESULT: ${{ needs.documentation-build.result }} - IGNORE_NATIVE_LIBRARIES: ${{ contains(github.event.pull_request.labels.*.name, 'ignore-real-library-wrappers') }} + IGNORE_REAL_LIBRARIES_PORTABILITY: ${{ contains(github.event.pull_request.labels.*.name, 'ignore-real-library-wrappers') }} steps: - name: Require every staged validation result shell: bash @@ -713,15 +621,15 @@ jobs: "compiler-smoke-macos=$COMPILER_SMOKE_MACOS_RESULT" \ "unit-tests=$UNIT_TESTS_RESULT" \ "unit-tests-macos=$UNIT_TESTS_MACOS_RESULT" \ - "native-libraries=$NATIVE_LIBRARIES_RESULT" \ + "real-libraries-portability=$REAL_LIBRARIES_PORTABILITY_RESULT" \ "documentation-benchmark=$DOCUMENTATION_BENCHMARK_RESULT" \ "documentation-build=$DOCUMENTATION_BUILD_RESULT" do stage=${staged_result%%=*} result=${staged_result#*=} - if [[ "$stage" == "native-libraries" && \ + if [[ "$stage" == "real-libraries-portability" && \ "$result" == "skipped" && \ - "$IGNORE_NATIVE_LIBRARIES" == "true" ]]; then + "$IGNORE_REAL_LIBRARIES_PORTABILITY" == "true" ]]; then continue fi if [[ "$result" != "success" ]]; then diff --git a/.github/workflows/real-libraries-portability.yml b/.github/workflows/real-libraries-portability.yml new file mode 100644 index 000000000..b5fb269c6 --- /dev/null +++ b/.github/workflows/real-libraries-portability.yml @@ -0,0 +1,147 @@ +name: Real Libraries Portability + +on: + workflow_call: + push: + branches: + - main + - release/* + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: real-libraries-portability-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + examples: + name: Real Libraries Portability · ${{ matrix.target }} · Python 3.12 + runs-on: ${{ matrix.runner }} + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + include: + - target: Linux x86-64 + cache_key: linux-x86-64 + runner: ubuntu-24.04 + fortran_compiler: gfortran-13 + fortran_c_compiler: gcc-13 + primary_c_compiler: gcc-13 + secondary_c_compiler: clang-18 + - target: Linux ARM64 + cache_key: linux-arm64 + runner: ubuntu-24.04-arm + fortran_compiler: gfortran-13 + fortran_c_compiler: gcc-13 + primary_c_compiler: gcc-13 + secondary_c_compiler: clang-18 + - target: macOS Intel + cache_key: macos-intel + runner: macos-15-intel + fortran_compiler: gfortran-13 + fortran_c_compiler: gcc-13 + primary_c_compiler: clang + secondary_c_compiler: gcc-13 + - target: macOS ARM64 + cache_key: macos-arm64 + runner: macos-15 + fortran_compiler: gfortran-13 + fortran_c_compiler: gcc-13 + primary_c_compiler: clang + secondary_c_compiler: gcc-13 + env: + PRIK_REAL_LIBRARY_NATIVE_JOBS: "8" + PYTHONPATH: . + HYPOTHESIS_PROFILE: ci + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 2 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install Ubuntu native dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install --yes gfortran-13 libblas-dev liblapack-dev + - name: Ensure macOS GNU compilers are available + if: runner.os == 'macOS' + run: | + if ! command -v "${{ matrix.fortran_compiler }}" >/dev/null 2>&1 || \ + ! command -v "${{ matrix.fortran_c_compiler }}" >/dev/null 2>&1; then + brew install gcc@13 + fi + - name: Configure GNU Fortran and C + shell: bash + run: | + compiler_dir="$RUNNER_TEMP/prik-example-compilers" + mkdir -p "$compiler_dir" + ln -sf "$(command -v "${{ matrix.fortran_compiler }}")" "$compiler_dir/gfortran" + ln -sf "$(command -v "${{ matrix.fortran_c_compiler }}")" "$compiler_dir/gcc" + echo "$compiler_dir" >> "$GITHUB_PATH" + echo "PRIK_REAL_LIBRARY_NATIVE_CACHE_DIR=$RUNNER_TEMP/prik-example-native" >> "$GITHUB_ENV" + - name: Install example dependencies + run: | + python -m pip install --upgrade pip + python -m pip install -e ".[qa]" \ + "numpy==2.5.1" \ + "meson==1.11.2" \ + "ninja==1.13.0" \ + "scipy==1.18.0" + - name: Restore compiled BLAS and LAPACK cache + uses: actions/cache@v4 + with: + path: ${{ runner.temp }}/prik-example-native + key: real-libraries-portability-${{ matrix.cache_key }}-gfortran13-${{ hashFiles('examples/native_library.py', 'examples/blas/native/**', 'examples/lapack/native/**', 'examples/lapack/support/**', 'examples/lapack/xblas_sources.txt') }} + - name: Show target and compilers + run: | + uname -a + python --version + gfortran --version + gcc --version + "${{ matrix.primary_c_compiler }}" --version + "${{ matrix.secondary_c_compiler }}" --version + - name: Run libm with ${{ matrix.primary_c_compiler }} + env: + PRIK_LIBM_CC: ${{ matrix.primary_c_compiler }} + run: | + source examples/libm/build_all.sh + python -m pytest -q examples/libm/tests + - name: Run libm with ${{ matrix.secondary_c_compiler }} + env: + PRIK_LIBM_CC: ${{ matrix.secondary_c_compiler }} + run: | + source examples/libm/build_all.sh + python -m pytest -q examples/libm/tests + - name: Run BLAS example + run: | + source examples/blas/build_all.sh + python -m pytest -q examples/blas/tests + if [[ "${{ matrix.target }}" == "Linux x86-64" ]]; then + python -m pytest -q examples/blas/ci/full_surface.py + fi + - name: Run LAPACK example + run: | + source examples/lapack/build_all.sh + python -m pytest -q examples/lapack/tests + if [[ "${{ matrix.target }}" == "Linux x86-64" ]]; then + python -m pytest -q examples/lapack/ci/full_surface.py + fi + - name: Run FFTPACK example + run: | + source examples/fftpack/build_all.sh + python -m pytest -q examples/fftpack/tests + - name: Run MINPACK example + run: | + source examples/minpack/build_all.sh + python -m pytest -q examples/minpack/tests + - name: Run BSPLINE-FORTRAN example + run: | + source examples/bspline/build_all.sh + python -m pytest -q examples/bspline/tests diff --git a/.github/workflows/real-libraries.yml b/.github/workflows/real-libraries.yml deleted file mode 100644 index e93a51154..000000000 --- a/.github/workflows/real-libraries.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: Real Libraries - -on: - push: - branches: - - main - - release/* - -env: - PRIK_GFORTRAN_BINARY: gfortran-13 - PRIK_GFORTRAN_PACKAGE: gfortran-13 - -jobs: - real-library-wrappers: - name: BLAS + LAPACK + FFTPACK + MINPACK · Ubuntu 24.04 · Python 3.12 - if: >- - ${{ - github.event_name != 'pull_request' || - !contains(github.event.pull_request.labels.*.name, 'ignore-real-library-wrappers') - }} - runs-on: ubuntu-24.04 - timeout-minutes: 120 - permissions: - contents: read - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 2 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - name: Install test dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e ".[qa]" - python -m pip install \ - "numpy==2.5.1" \ - "meson==1.11.2" \ - "ninja==1.13.0" \ - "scipy==1.18.0" - - name: Install pinned GFortran and LAPACK link dependencies - shell: bash - run: | - packages=(libblas-dev liblapack-dev) - if ! command -v "$PRIK_GFORTRAN_BINARY" >/dev/null 2>&1; then - packages+=("$PRIK_GFORTRAN_PACKAGE") - fi - sudo apt-get update - sudo apt-get install --yes "${packages[@]}" - compiler_dir="$RUNNER_TEMP/prik-gfortran" - mkdir -p "$compiler_dir" - ln -sf "$(command -v "$PRIK_GFORTRAN_BINARY")" "$compiler_dir/gfortran" - echo "$compiler_dir" >> "$GITHUB_PATH" - "$compiler_dir/gfortran" --version - - name: Restore compiled native library cache - uses: actions/cache@v4 - with: - path: ${{ runner.temp }}/prik-real-library-native - key: real-libraries-${{ runner.os }}-gfortran13-${{ hashFiles('examples/blas/native/**', 'examples/lapack/native/**') }} - - name: Run BLAS example and CI full-surface audit - env: - PYTHONPATH: . - HYPOTHESIS_PROFILE: ci - PRIK_REAL_LIBRARY_NATIVE_CACHE_DIR: ${{ runner.temp }}/prik-real-library-native - run: | - source examples/blas/build_all.sh - python -m pytest -q examples/blas/tests examples/blas/ci/full_surface.py - - name: Report reviewed LAPACK inventory - env: - PYTHONPATH: . - run: | - python - <<'PY' - from examples.lapack.routine_inventory import ( - EXPECTED_LAPACK_PROCEDURES, - EXPECTED_LAPACK_SOURCE_FILES, - F2PY_SCALAR_WRITEBACK_ROUTINES, - ROUTINE_GROUPS, - ROUTINES, - SCIPY_VERSION, - ) - - print(f"SciPy version: {SCIPY_VERSION}") - print(f"LAPACK implementation sources: {EXPECTED_LAPACK_SOURCE_FILES}") - print(f"Expected PRIK procedures: {EXPECTED_LAPACK_PROCEDURES}") - print(f"Selected float64 correctness routines: {len(ROUTINES)}") - print(f"f2py scalar writebacks: {len(F2PY_SCALAR_WRITEBACK_ROUTINES)}") - for family, routines in ROUTINE_GROUPS.items(): - print(f" {family}: {len(routines)}") - PY - - name: Run LAPACK example and CI full-surface audit - env: - PYTHONPATH: . - HYPOTHESIS_PROFILE: ci - PRIK_REAL_LIBRARY_NATIVE_CACHE_DIR: ${{ runner.temp }}/prik-real-library-native - run: | - source examples/lapack/build_all.sh - python -m pytest -q examples/lapack/tests examples/lapack/ci/full_surface.py - - name: Run FFTPACK 31-procedure full-surface audit - env: - PYTHONPATH: . - HYPOTHESIS_PROFILE: ci - run: | - source examples/fftpack/build_all.sh - python -m pytest -q examples/fftpack/tests - - name: Run MINPACK 22-procedure and parameter-array full-surface audit - env: - PYTHONPATH: . - HYPOTHESIS_PROFILE: ci - run: | - source examples/minpack/build_all.sh - python -m pytest -q examples/minpack/tests diff --git a/AGENTS.md b/AGENTS.md index 18f2596ee..45a6c78b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,14 @@ examples, build or CI workflows, benchmark methodology, or documented limitations. Keep entries concise and outcome-focused; do not add release notes for internal cleanup that has no visible effect. +Treat developer documentation as durable guides, not as per-change +implementation logs. Do not update developer pages merely because code changed, +and do not add incidental low-level details that are unnecessary for following +the documented architecture or maintainer workflow. Update them only when a +documented contract, ownership boundary, workflow, or limitation changes; keep +routine implementation findings in the review summary or a concise CHANGELOG +entry when appropriate. + Ignore: - *.f90 - *.f95 @@ -80,6 +88,24 @@ the selected plan requires a genuinely new emitted-code mechanism; those generators should otherwise keep reusing and dispatching existing planned paths. +To answer an ABI question, or to decide whether something belongs in the +binding or in the Fortran bridge, first ask: **how would this work for a +`bind(C)` procedure, where there is no bridge at all?** A direct entrypoint has +only the binding and the user's C ABI symbol, so whatever the direct route must +do is binding-owned by definition. The bridge then owns exactly the remainder: +the work that makes an ordinary non-`bind(C)` procedure reachable through that +same completed plan. Deriving the boundary this way keeps one shared entrypoint +contract for both routes instead of two parallel designs. + +The question is still decisive when the form cannot be `bind(C)` at all. A +Fortran type that no interoperable interface can declare — a deferred-length +`character(len=:)` dummy, for example, which the standard rejects in a +`bind(C)` interface because character dummies there must have length 1 — proves +that a generated Fortran adapter is mandatory rather than optional, and names +what that adapter has to construct: the non-interoperable local the native +dummy requires. Record that reasoning with the completed policy so the bridge +implements a decided mechanism rather than rediscovering it. + After every implementation task, the final summary must include a breakdown of the stages that actually changed. Relevant stages include parsing, semantic IR construction, post-IR policy completion, wrapper planning/direct lowering, binding @@ -98,7 +124,7 @@ Changes limited to wrapper planning, direct bridge/binding lowering, or native compilation should use the focused owners under `tests/fortran/infrastructure/codegen/`, feature-local `tests/fortran/*/codegen/` directories, and -`tests/fortran/building_shared_library/compiling/` as applicable. Include the +`tests/fortran/infrastructure/building/compiling/` as applicable. Include the relevant end-to-end feature tests whenever a generated or compiled mechanism changes; run a broader suite when behavior spans multiple stages. Do not run LAPACK wrapper tests locally unless the user explicitly asks for them. Local verification may run everything else, including BLAS-only real-library tests; leave LAPACK coverage to GitHub Actions by default. diff --git a/CHANGELOG.md b/CHANGELOG.md index c8a81b5a0..dca3a0ada 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,588 @@ release tags add a leading `v` to the package version. ## Unreleased +- The copied LAPACK example now mirrors Reference LAPACK's default source + selection: XBLAS-only routines are excluded, the two required `INSTALL/` + workspace helpers are bundled, and a failed native build now stops without a + secondary missing-source diagnostic. This makes the maintained 127-routine + example build consistently on Linux and both hosted macOS architectures. + +### Changed + +- Report commands now share one output rule: **`--json` selects the format and + `--out` selects the destination, and neither changes the other.** Without + `--json` every report command prints a human-readable report; with `--json` + it emits the complete record. `--out PATH` writes whichever format was + selected, and bare `--out` writes one file beside each input source, using + `.json` for the record and `.txt` for the report. + + This changes three commands: + + - `parse --out PATH` previously wrote JSON regardless of `--json`; it now + writes the human-readable report unless `--json` is given. Use + `parse --json --out PATH` to keep the previous output. + - `semantics` gains `--json` and `--print-limit`, and now prints a + human-readable summary by default instead of the complete JSON record. Use + `semantics --json` to keep the previous standard-output behavior. The + summary reports each module's functions with their semantic signatures and + every argument's semantic dtype, rank, ownership, and mutability. + - `probe` replaces `--format {json,markdown}` with `--json`. The Markdown + mapping table is now the default standard-output rendering, and the + Markdown output itself is unchanged. + +- `probe` now selects its report from `--expr` rather than from the output + format. Without `--expr` it measures the standard datatype mapping table, so + the bare command reports that table instead of an empty measurement; with + `--expr` it measures the named expressions, which now render in both formats. + The JSON mapping report adds the structured `target_fact` measurement, + `recipe`, and `source_text` alongside the displayed text. The mapping report + now rejects `-I`, `-D`, `-U`, and `--std` instead of accepting options its + fixed inventory cannot use. + +- `prik.pipeline.type_mapping_report` now exposes `c_type_mapping_report()` and + `fortran_type_mapping_report()` returning measured records, plus + `type_mapping_markdown()` and `expression_probe_markdown()` renderers, + replacing `c_type_mapping_markdown()` and `fortran_type_mapping_markdown()`. + +### Fixed + +- Verbose wrapper builds now print each compiler command before it starts, so + failed invocations remain directly replayable. + +- `semantics` and `generate --pyi` now reject non-source inputs instead of + emitting an empty report. + +- Ordinary array arguments now preserve their non-array type check before + accessing NumPy storage. Native-handle-capable branches still avoid repeating + that check after selecting their NumPy fast path. + +- Common scalar and string conversions in C bindings with several Python + outputs now share one linear reference-cleanup path instead of repeating + every earlier `Py_DECREF` at each failure site. Large wrappers retain the + same result ownership and diagnostics while generating smaller C + control-flow graphs. + +- Ordinary NumPy-array arguments no longer repeat `PyArray_Check` inside the + validation helper after the generated fast-path branch has already performed + that check. Dtype, rank, layout, byte-order, alignment, and mutability + validation remain unchanged. + +- Built-in release compiler profiles no longer force loop unrolling in + generated wrappers and native sources. Release builds retain `-O3`, and + callers can still request vendor unrolling flags explicitly; optimized + large-wrapper builds therefore avoid the hidden compilation cost by default. + +- Real Libraries Portability now exposes the matching GNU C driver beside its + selected GNU Fortran driver. Generated C bindings therefore use GCC on + macOS, including its `ISO_Fortran_binding.h` search path, instead of + accidentally resolving Apple's unrelated `gcc`-named Clang driver. + +- The copied BLAS and LAPACK examples now give GNU Fortran a positional archive + input when creating a macOS dynamic library. Apple `ld` still receives the + targeted `-force_load` option, while the compiler driver no longer aborts + with "no input files" on either hosted macOS architecture. + +- The Linux x86-64 BLAS and LAPACK full-surface CI audits now run in the same + shell steps as their example builds, so they reuse the temporary extensions + instead of losing their exported import paths at a GitHub Actions step + boundary. + +- The portable libm tests now read and call `long double` routines through the + public dtype selected by the target-generated contract. Apple ARM64 uses + `numpy.float64`, while targets with wider C `long double` storage use + `numpy.longdouble`. + +- An exact native C scalar passed by address and projected back to Python now + converts its native call-local into the public contract storage type before + constructing the NumPy result. This removes an incompatible-pointer handoff + such as `long long *` to an `int64_t` result helper. + +- Compiler-preprocessed C prototypes with an unnamed builtin parameter, such + as Apple ``'s `long rinttol(double)`, are no longer mistaken for + unsupported K&R definitions. + +- Compiler-preprocessed C headers that provide fallback `_FloatN` typedefs now + parse successfully. This keeps private glibc compatibility declarations from + blocking an allowlisted public API when Clang preprocesses ``. + +- An exact native C type around a NumPy-backed `Arg(...)` now requires its + matching NumPy C storage type. For example, `CLongLong(Arg(0))` accepts + `numpy.longlong` and rejects a distinct `numpy.int64` buffer instead of + passing that buffer to `long long *` through an incompatible pointer. The + rule covers all supported exact C types with matching NumPy storage; scalar + value arguments keep their existing conversion behavior. + +- A C translation unit's module variables, enum or macro constants, and + aggregate type declarations no longer reach wrapper planning. They previously + generated a Fortran adapter module for a C input and failed with a raw + compiler error after writing files; they now fail closed before planning with + `C_DIRECT_NATIVE_GLOBAL_STATE`, `C_DIRECT_ENUM_CONSTANT`, + `C_DIRECT_MACRO_CONSTANT`, or `C_DIRECT_AGGREGATE_TYPE` and leave no output. + +- A C declaration the parser cannot model — for example one carrying an + unsupported calling-convention attribute — is no longer dropped from a + wrapper build's public API. It raises `C_DIRECT_UNMODELED_DECLARATION` + instead of silently publishing a smaller or reinterpreted API. + +- `T[:] | None` and `T[()] | None` in a C semantic contract no longer build a + silently non-nullable wrapper. Nullable C pointers are outside the initial C + lane, so the contract now fails with `C_DIRECT_NULLABLE_POINTER`. + +- A route-neutral `@native_call` reorder in a C contract no longer converts one + argument's Python value using another argument's declared type. + +- An edited array contract can now derive its native extent from the buffer, + as `@native_call([Arg(0).shape[0], Arg(0)])`. A binding-owned extent, length, + or presence producer is no longer mistaken for the argument's own transport + slot, which had also lowered the promoted buffer as a by-value scalar. + +- Fortran `bind(C)` procedures that pass strings, derived objects, or callbacks + through their direct entrypoint build again. The new exact C declaration plan + is now built only for C-source operations. + +- The generated docstring for a call-local scalar address argument no longer + claims that native code updates the supplied storage in place. That update + lands in a call-local copy and is not visible in Python. + +- A `generate --makefile` build invoked with relative paths now produces a + Makefile that runs on a clean tree. The link rule demanded each user object + twice, once under a relative spelling that no rule produced, so `make` stopped + with "No rule to make target". This affected Fortran and C builds alike. + +- C contracts can now pass strings. `String` hands a C `const char *` the + Python object's own NUL-terminated buffer, and `String[...][()]` hands a + `char *` a rank-zero NumPy bytes buffer untouched, so native writes are + visible in place. A declared capacity such as `String[32][()]` additionally + checks the caller's itemsize. PRIK takes no position on whether the callee + reads to a terminator or takes a separate length: the contract states which + form the C code expects. `@raises(message=...)` works too when the projected + message declares a capacity, such as `Returns["message", String[64]]`: the + binding owns that buffer and passes it as `char *`, instead of the bridged + route's owned-allocation `char **`. Arrays of strings, pointer results, and + owned buffers stay fail-closed. + +- `@raises(message=...)` can now name a visible argument instead of a projected + hidden output, in both the C and Fortran lanes. The caller then supplies the + storage the native code writes into — a rank-zero NumPy bytes array for + `String[n][()]`, or a `str` payload for `String` — so no capacity has to be + declared, and the buffer survives the raise for inspection. Only the hidden + form still requires a fixed width, because there the binding allocates the + buffer and the size the native code assumes is not in the signature. + +- `Hidden(name, T)` declares a native output the Python signature never shows. + It is planned, passed, and released exactly like a returned output — the + bridge sees no difference — and only the binding skips building a Python + value from it. This makes `@raises` status and message one instance of a + general mechanism rather than a special case, and it works in both the C and + Fortran lanes. + +- `@raises` targets are now declared with `Hidden(name, T)` inside + `@native_call` instead of occupying a slot in the return annotation. A status + or message is produced by the native call but consumed into the exception, so + listing it as a returned value described a Python signature that never + existed: `-> tuple[Returns["root", Float64], Returns["status", Int32]]` + returned a bare `Float64`. Contracts now say what they mean, and the emitted + `.pyi` normalizes the old spelling to the new one. + +- Rank-zero character storage may now leave its width assumed in the Fortran + lane too: `String[...][()]` takes the width from the caller's NumPy array + instead of the contract, matching what the C lane already accepted. The same + now holds for character arrays, where `String[...][:]` takes every element's + width from the array's itemsize. A declared width still validates the + caller's itemsize; an assumed one accepts whatever the array carries. + +- Rank-zero character storage now always reports its width beside the address, + so a declared and an assumed width generate the same adapter shape instead of + baking a literal into the generated Fortran. A raw string address keeps the + declared width, because a bare caller-supplied address has no Python object + whose size could be measured. + +- A `@raises` message is now read within the capacity its storage declares + instead of by scanning for a terminator. Fortran blank-pads fixed-length + character storage and never writes a NUL, so the previous scan reported the + padding as part of the exception text — a `character(len=64)` message raised + `'negative'` followed by 56 spaces. When the native code did terminate, the + bytes are still taken exactly as written. + +- A `@private` declaration in a C semantic contract is no longer refused as an + unsupported C operation. Being unexported is a shared contract feature, not a + C-lane limit, so the documented `@overload` pattern — private concrete + candidates behind one public Python name — now works for C. + +- A C wrapper build now works when the C compiler is named `cc`. `cc` is the + documented default for the C lane, but its vendor was read from the + executable's filename, which names no vendor. That happened to work where + `cc` links to a vendor-named binary and failed everywhere it does not, such + as macOS. The driver's own `--version` banner now settles the vendor when the + name cannot. + +- The BLAS and LAPACK examples now expect the character selectors that the + conservative `intent(inout)` default returns. Their assertions still encoded + the older `intent(in)` assumption for `character` dummies, so the example + suites failed against the behavior the same release documents. Positional + reads of a returned scalar tuple moved with the selectors; five LAPACK + assertions that had kept passing on a coincidentally equal value now index + the dummy they name. + +- The semantic contract a C build saves beside its extension now describes only + the wrapped file's own API. Preprocessed system-header declarations stay + inspection facts instead of filling the contract with private entries that + the next build could not accept. + +- A C parameter or result written through a typedef now declares the exact + builtin type the compiler probe resolved it to. The generated binding writes + its own prototype, so a typedef name defined only by the wrapped source's + headers previously reached the compiler undeclared and failed the build after + files were written. + ### Added -- Added a native-entrypoint adoption roadmap for selective direct Fortran - `bind(C)` calls and the initial direct-only C wrapper backend, including - conservative starter-contract defaults for ambiguous C pointers. +- Added a portable libm example that regenerates its target-specific semantic + `.pyi` from a reviewed 60-function ISO C99 `math.h` selection before every + build and validates every exported routine with a named numerical test. The + contract records exact native scalar casts without changing its NumPy-facing + signatures, and its dtype assertions follow the active `long` and `long + double` ABIs. A dedicated Real Libraries Portability workflow, reused by the + pull-request gate, runs all maintained examples on Linux x86-64, Linux Arm64, + macOS Intel, and macOS Arm64; libm additionally runs with GCC and Clang on + each platform, and Linux x86-64 retains the deep BLAS and LAPACK audits. + +- `--positional-only` exposes every wrapper whose arguments are all required as + positional-only, renaming them `arg0`..`argN` in the signature, docstring, and + argument diagnostics. A native declaration's parameter names then stay out of + the Python API, which matters for a system header that spells them `__x` or + omits them entirely. A function with an optional argument keeps its keywords, + and a module with overload sets is rejected because overload dispatch selects + a candidate by keyword. + +- `--lto` adds `-flto` to generated and native compilation and to the extension + link. A collision adapter is emitted with hidden visibility, so link-time + optimization can inline the forwarder and drop its definition rather than + exporting it from the extension. + +- Target-specific C contracts now preserve exact scalar call identities with + sparse expressions such as `CLongLong(Arg(0))` and typed native-result + projections. Public annotations remain ordinary NumPy types; policy completes + the conversion before planning and the binding reuses its exact native scalar + storage and direct-result path. Native C scalar names are rejected outside + `@native_call`. + +- `--collision-adapter NAME` and `--collision-adapter-all` now isolate genuine + C identifier collisions only. The separate translation unit includes no + `Python.h`, reconstructs the completed exact native declaration, and emits a + hidden pure forwarder defined once per native symbol even when several Python + callables name it. Only a C-source function is eligible: an explicitly named + symbol that is unknown or ineligible fails before wrapper planning, while + `--collision-adapter-all` passes over Fortran `bind(C)` entrypoints instead + of failing the build. Saved build manifests retain the selected adapter mode + when replayed. + +- Added a published C support guide with executable source and semantic-contract + workflows, CLI and Python build APIs, supported primitive and NumPy-pointer + contracts, compiler preprocessing, and the direct lane's fail-closed limits. + Published the linked language-support, CLI, Python API, diagnostic, reference, + and examples landing pages after aligning their commands and public links with + the implemented surface. + +- Added the initial direct-only C wrapper lane. `build_c_extension`, explicit + `native_c_sources`, and C-native semantic `.pyi` contracts compile, link, + import, and call supported target-probed primitive C symbols directly. The + lane covers arithmetic scalar values, `void`, completed scalar-reference and + primitive NumPy-pointer contracts, and fails closed before planning for + callbacks, aggregates, variadics, pointer results, multi-level or nullable + pointers, ownership-sensitive forms, and other unsupported C ABI features. + C wrapper builds preprocess their sources with the selected C compiler, so an + ordinary `#define`, `#ifdef`, or macro-defined declaration is read the same + way the C inspection routes read it, and only the wrapped translation unit's + own declarations become public API. + +- Every build now writes its semantic `.pyi` contract beside the extension, in a + `contracts/` package inside the build directory (`__prik__/contracts/` by + default). Reshaping the generated Python API no longer needs a separate + `generate --pyi` run: the contract describing the API a build just produced is + always there, and rebuilding from it works directly. It lives in its own + directory so its `__init__.pyi` cannot make the build directory look like a + Python package. + +- Generic constructors declared as `interface ` are now wrapped from + Fortran source. Such an interface is that type's constructor, so its specifics + become the accepted signatures of one overloaded `__init__` rather than a + module-level generic, and a call matching none of them is refused instead of + guessed at. A specific that is `private` in its module is reached through the + public type name, which resolves to the same procedure. Because the interface + supplies every accepted signature, it replaces the keyword-field constructor, + and the generated contract states only the signatures the class accepts. The + three sources of a constructor are now: no user constructor keeps the + keyword-field `__init__`, an `interface ` supplies the overload set, + and an edited `.pyi` declares exactly what it says. A constructor candidate + carries no `@bind`, because the class name already states the generic that + reaches it — the same reason an unrenamed method omits it — and `@private` is + refused on `__init__`, since a constructor is published or absent and the + accessibility of the specific it selects is that procedure's own fact. + +- Added the BSPLINE-FORTRAN example under `examples/bspline`. It wraps the + upstream sources unmodified and validates both public interfaces from Python: + the object-oriented classes over an abstract base with deferred bindings and + generic constructors, and the procedural interpolation routines. Numerical + checks use analytic values and `scipy.interpolate` as independent oracles. It + is the first example project written in modern Fortran rather than FORTRAN 77. + +- BSPLINE-FORTRAN now follows the maintained real-library example workflow: + its checked-in build instructions are verified with the documentation suite, + its full procedural and derived-type surface is exercised in the native + library CI job, and its inventory fails closed if generated exports or named + numerical tests drift. The example now calls all one- through six-dimensional + procedural setup and evaluation routines and constructs every concrete spline + class against an independent affine interpolation result. + +- Abstract Fortran derived types are now wrapped. A `type, abstract ::` + declaration becomes a Python class with no constructor — instantiating it + raises `TypeError` naming the concrete extensions to use instead — while its + extensions remain ordinary Python subclasses that inherit its implemented + bindings. A deferred binding (`procedure(iface), deferred ::`) is declared on + the base and resolved by the object's own type: the generated adapter converts + the address to the caller's concrete type and lets Fortran select the + override, so no Python-side dispatch is involved. An abstract type publishes + no component accessors of its own, because each extension already generates + one for every component it inherits, and it is excluded from the polymorphic + cases a caller can supply, since no object can have it as a dynamic type. In + semantic `.pyi` contracts the class carries `@abstract` and each deferred + binding carries `@abstractmethod`, both re-exported from `prik.contracts`; + a deferred binding never carries `@bind`, because it has no native symbol. + +### Changed + +- Reorganized the C and Fortran test suites around a strict ownership rule: + language features remain under `/`, while shared parsing, + preprocessing, CLI, semantic-representation, contract, build, and policy + evidence live under `infrastructure/`. Focused commands and documentation now + use the corresponding infrastructure owners. + +### Fixed + +- A `bind(C)` character dummy that is a pointer now declares deferred length, + as the Fortran standard requires. GNU Fortran 13 and newer reject the + declared-length spelling earlier releases emitted, so wrapping a + `character(len=N), pointer` module array failed to compile there. Pointer + assignment takes the length from its target, so the associated width is + unchanged. The matching allocatable descriptor consumer travels as an + assumed-length assumed-shape dummy, whose descriptor still carries the + element length. + +- A generic interface whose specifics project an `intent(out)` argument into a + result now reloads from its generated contract. The declaration states the + public signature, so an output the projection turned into a result is not one + of the arguments it accepts; comparing the declaration against the specific's + native argument list rejected every such generic — the common shape in + numerical Fortran — with "Overload declaration 'x' is incompatible with + specific procedure 'y'". The same comparison now drives a type-bound generic's + receiver search. Generated contracts for BSPLINE-FORTRAN's `db1ink`, + `db1val`, and `initialize` load again. + +- A module whose only procedures are `bind(C)` now installs the bundled native + support its derived-type accessors need. Compiled wrapper builds for such a + module previously failed to link with `undefined symbol: + prik_float64_to_numpy`, because native support was requested only for module + variables, for ordinary procedure arguments and results, and for array + components — and a `bind(C)` procedure supplies none of those. Every published + component converts through those helpers, so a type with any component now + requests them. + +- A derived type's `private` and `public` statements are now honored. The + statement before `contains` sets the default accessibility of components and + the statement after it sets the default for type-bound procedures; a + declaration that states its own accessibility still keeps it. The statement + after `contains` previously failed to parse at all, and the one before it + parsed but was discarded — so a type with private components reached the + Fortran compiler as generated accessors that read them, failing with + "Component 'x' is a PRIVATE component of 'y'". Private components and + bindings now simply stay off the generated Python class. Parsed derived types + additionally record `component_visibility` and `binding_visibility`, and each + type-bound binding records the `visibility` it resolves to, so the parser's + serialized form states the accessibility it read. + +- A `type, public ::` declaration is no longer hidden by a module-level + `private` default. The type's own declared accessibility is the most specific + statement about it, so it wins over the module default and over the module's + accessibility lists. Previously such a type — and every one of its methods — + was dropped from the extension silently, with the build still reporting + success. + +- A deferred type-bound binding (`procedure(iface), deferred :: name`) now + parses, so the decision about whether it can be wrapped is reported by policy + as an unsupported derived-type diagnostic naming the binding, rather than by + the parser as a syntax error. Abstract types and deferred bindings remain + unsupported; only the stage that owns the refusal has changed. + +- A named `block` construct (`main: block ... end block main`) is recognized as + the start of a procedure's execution part. A construct name prefix is now + stripped before a statement is classified, so named `do`, `if`, `select`, + `associate`, and `block` constructs are all read as executable rather than as + an unknown declaration. + +- The compiler type probe no longer emits a program it cannot compile. The + probe is a standalone program that cannot `use` a module from the project + being analyzed, because that module has not been compiled yet; an expression + naming a kind parameter declared elsewhere in the project — `storage_size(1_ip, + kind=ip)`, for example — is now left for the requirement report instead of + being compiled into the probe. Previously one such expression failed the whole + probe and with it the entire build. + +### Added + +- Added the C-only `--export-symbols FILE` allowlist for source builds, + `semantics`, and `generate --pyi`, with resolved-name parity through + `build_c_extension(export_symbols=...)`. It promotes exactly the named + reachable functions even from private system headers, excludes every + unlisted declaration, and fails closed for malformed, repeated, missing, + non-function, or ambiguous selections. This lets maintained examples parse + platform headers without publishing their implementation-specific surface. + +- The libm portability audit now reuses the Linux x86-64 and macOS Arm64 CI + jobs and adds one focused Linux Arm64 job. All three regenerate from the + target `math.h` and run the complete 60-function suite without repeating the + heavyweight Fortran real-library matrix. + +- The libm example now stops immediately when contract generation or wrapper + compilation fails, instead of exporting a broken build environment to a + later test command. + +- Added `--assume-intent-in-scalars`, which treats a primitive scalar dummy + that declares no `intent` as `intent(in)` instead of applying the + conservative `intent(inout)` default. Fortran permits an undeclared dummy to + be written, so prik returns its post-call value; for sources that predate the + `intent` attribute this fills the Python return with unmodified controls, and + reference BLAS `ddot` returns `(value, n, incx, incy)` rather than the value + alone. With the option, that call returns `32.0`. The choice is made once in + semantic conversion, where an absent `intent` is interpreted, so the build + and `generate --pyi` describe the same Python surface. It is deliberately + narrow: it covers the primitive and character scalars whose replacement is + otherwise returned, a declared `intent` always wins, and arrays, + derived-type objects, and allocatable or pointer scalars are unaffected. It is an + assertion about the source rather than a fact derived from it — prik does not + inspect the procedure body, so a procedure that does write such a dummy + loses that value, exactly as removing the result from the generated contract + by hand would. The option appears in the first `--help` screen because it + changes the default Python surface, and every command that produces semantic + IR accepts it — the build, `generate --pyi`, and `semantics`. + `--build-manifest` rejects it along with the other saved wrapper settings, + and a `.pyi` wrapper build rejects it because a contract already states its + own results. + +### Changed + +- A scalar `character` dummy that declares no `intent` now uses the same + conservative `intent(inout)` default as every other scalar, so the value the + native procedure left behind is returned. It was silently assumed + `intent(in)`, which meant a procedure that wrote to such a dummy lost that + write with no diagnostic, while an `integer` dummy on the same call had its + write returned. The exception was undocumented and untested; the strings + guide already stated the uniform rule this change makes true. Wrapping + fixed-form sources, where `intent` cannot be declared, therefore returns + `(result, text)` where it previously returned `result` — + `--assume-intent-in-scalars` restores the shorter surface and now covers + character scalars along with primitive ones. An `allocatable` or `pointer` + character scalar with no `intent` likewise now matches its numeric + counterpart and returns a nullable snapshot; the option does not reach either + one, because a snapshot is not a replacement value the caller supplied. + +- Generated wrapper source is now readable. Each generated Fortran adapter and + each CPython binding function carries a short leading comment naming what it + is for — the native procedure an adapter wraps and the C symbol it exports, + the Python callable a binding serves and the entrypoint it calls, and what a + module accessor reads or writes — and an adapter additionally summarizes the + conversions it performs. Generated Fortran modules also separate their + procedures with a blank line instead of running them together. Comment prose + is wrapped well inside the free-form 132-column limit, and each backend + describes only its own plan facet, so the binding never names a Fortran + symbol and the bridge never names a Python one. + +### Fixed + +- Declared-length `character` module arrays (`character(len=4), allocatable :: + arr(:)`, and the `pointer` equivalent) no longer fail in the Fortran + compiler. The generated descriptor-consumer interface and descriptor ABI + parameter both spelled `character(len=:)` regardless of what the array + declared, and an allocatable or pointer dummy accepts a deferred-length + actual only when it declares one itself. Both now spell the declared width. + A deferred-length `character(len=:), allocatable` module *array* still fails + to compile under GNU Fortran 11.4 with an internal compiler error, which is a + compiler defect rather than a wrapper contract. + +### Added + +- Every `character` module-variable form is now wrapped. Declared-length + scalars are readable and writable as ordinary `str` properties, matching how + numeric module variables already behave; a character value has no by-value C + ABI, so the accessors copy through the same fixed-width buffer a character + field already used, and assignment requires exactly the declared byte width + rather than truncating or padding. `allocatable` and `pointer` scalars read + as a detached `str`, or `None` when unallocated or unassociated, through the + same nullable snapshot a descriptor numeric scalar uses, carrying the width + the descriptor holds at the time of the read. `character` `parameter` arrays + are copied once at import into a read-only fixed-width bytes array, the way + numeric parameter arrays already were. Character module arrays report their + own element width through their generated accessor rather than having it + restated by the binding, so an assumed-length (`character(len=*)`) parameter + array works too and takes the dtype width its initializer implied. An + assumed-length scalar still keeps its rejecting setter, having no storage + width to write into. + +- Fixed-shape `character` module arrays with the `target` attribute now expose + the same live fixed-width bytes view numeric module arrays already did, at + any rank. The live-view lane rejected them only because it required a + primitive numeric element type; a character element differs only in carrying + its Fortran element length as the dtype width. Native writes appear in the + view, and Python writes reach the storage Fortran reads. `target` is required + here exactly as it already was for numeric module arrays. + +- Pointer array handles now expose `deallocate()` without a `PointerPolicy` + annotation, matching what allocatable handles already offered. Release stays + manual and caller-driven — prik never frees a native target on its own, on + garbage collection or otherwise — so this is the same responsibility a + Fortran caller takes when writing `deallocate` for the same pointer. + Previously a wrapped procedure that returned freshly allocated pointer + storage leaked with no way to reclaim it from Python. `allocate` and `resize` + still require `PointerPolicy`, because they establish a new target rather + than releasing the one the handle already names. +- Added wrapper support for `allocatable` and `pointer` scalar `character` + values in every direction: `intent(in)`, `intent(out)`, and `intent(inout)` + arguments, and function results, at both deferred (`len=:`) and declared + (`len=n`) length. Policy now completes the adapter-local storage each dummy + needs — its attribute, its length, and who releases it — instead of always + building a plain fixed-length temporary. The C ABI is unchanged: a scalar + character argument still crosses as a byte buffer and a length whatever the + dummy declares. Previously most of these forms either stopped at a policy + diagnostic or reached the Fortran compiler and failed there with + "Actual argument for 'x' must be ALLOCATABLE"; declared-length allocatable + and pointer forms additionally failed plan validation. +- Added a character-length subscription to semantic `.pyi` contracts. The first + subscription after `String` is always the length — `String[...]` assumed, + `String[8]` or `String[n]` explicit, `String[:]` deferred — and an array adds + its shape as a second subscription. Deferred-length scalars therefore have a + contract spelling for the first time, so those procedures rebuild from their + generated contract; the one-subscription array spellings the printer used to + emit (`String[::]`, and `String[n]` for an extent) are replaced by + `String[...][::]` and `String[...][n]`, which the parser had rejected or read + as a scalar length. +- Added wrapper support for mutable scalar character descriptor arguments + (`allocatable` or `pointer`, `intent(inout)`). The dummy stays a `str` + argument and additionally returns the value the native procedure left behind, + or `None` when it leaves the dummy unallocated or unassociated. Policy + completes two decisions for the one dummy — a call-local character-buffer + input and a nullable descriptor result — so the adapter copies back the local + the native procedure may have replaced rather than the caller's buffer. A + pointer dummy additionally records who releases the target the adapter + allocated: the adapter frees it only while the dummy still identifies it, so + storage the native procedure deallocated or replaced is left alone. The dummy + spells as `Allocatable(Arg(i))` or `Pointer(Arg(i))` with `String[:]` or + `String[n]` in a semantic `.pyi` contract, so these procedures also rebuild + from their generated contract. +- Added wrapper support for `allocatable` scalar `character` function results. + The adapter moves the result out through an allocatable dummy rather than + assigning it, which makes allocation a testable fact, so an unallocated + result becomes `None`. Other allocatable scalar function results remain + blocked, because they have no such completed move. - Added `@native_abi("c")` to semantic `.pyi` contracts so Fortran `bind(C)` procedures retain their ABI and optional link label through generated and source-free contract workflows. diff --git a/README.md b/README.md index 5191002e8..84cd6ac5c 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ width="450">

-**PRIK (Python Runtime Interop Kit)** generates native Python bindings from Fortran projects, -producing importable extensions and editable `.pyi` contracts that let you shape Pythonic APIs. +**PRIK (Python Runtime Interop Kit)** generates native Python bindings for +Fortran and C code. [![Tests](https://github.com/PyNumLab/prik/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/PyNumLab/prik/actions/workflows/tests.yml) [![Static Analysis](https://github.com/PyNumLab/prik/actions/workflows/static-analysis.yml/badge.svg?branch=main)](https://github.com/PyNumLab/prik/actions/workflows/static-analysis.yml) @@ -16,20 +16,16 @@ It preserves modules, derived types, arrays, callbacks, and native behavior while letting you reshape the resulting Python API through editable `.pyi` contracts instead of writing low-level binding code. -**Project status: Alpha.** Core Fortran wrapper workflows are -implemented and tested across supported compilers, but public APIs may still -change before `1.0`. +**Project status: Alpha.** Core Fortran workflows and the currently supported +C wrapper features are implemented and tested across supported compilers, but +public APIs may still change before `1.0`. -**PRIK starts with Fortran-to-Python.** Its semantic contract model is designed -to support more native languages over time. - - +PRIK supports both languages. Fortran currently has the broader, more mature +wrapper surface. C provides a focused direct-ABI lane for primitive values, +one-level pointers, NumPy arrays, and strings. In both languages, editable +`.pyi` contracts let you shape the Python API. See [C +Support](https://pynumlab.github.io/prik/user/language-support/c-support/) for +C examples and current limits. [Read the documentation](https://pynumlab.github.io/prik/) for installation, the user guide, examples, and reference material. @@ -40,7 +36,9 @@ the user guide, examples, and reference material. - [Proven on real libraries](#proven-on-real-libraries) - [Key Features](#key-features) - [Performance](#performance) -- [Current limitations](#current-limitations) +- [Current Fortran limitations](#current-fortran-limitations) +- [C support](#c-support) +- [Current C limitations](#current-c-limitations) - [Installation & Quick Start](#installation--quick-start) - [How it works](#how-it-works) - [Python API](#python-api) @@ -59,7 +57,7 @@ python3 -m prik points.f90 --out geometry Create `points.f90`: - + ```fortran module points implicit none @@ -123,15 +121,15 @@ class point: @native_call([Pass(), Addr(Arg(0)), Addr(Arg(1))]) def translate(self, dx: Float64, dy: Float64) -> None: ... - @bind("norm_squared") @native_call([Pass()]) def norm_squared(self) -> Float64: ... ``` -`@bind("move")` keeps the original native target while the declaration's -placement and name define the Python-facing API. `Pass()` supplies the -receiver (`self`) to the native call; `Addr(Arg(...))` passes the remaining -arguments by address as required by the native calling convention. +`@bind("move")` is needed because `translate` has a different Python name. +`norm_squared` needs no `@bind`: matching Python and native names select the +same procedure. `Pass()` supplies the receiver (`self`) to the native call; +`Addr(Arg(...))` passes the remaining arguments by address as required by the +native calling convention. Build from the contract: @@ -159,19 +157,23 @@ without changing the underlying Fortran implementation. ## Proven on real libraries -The maintained projects build real numerical libraries with PRIK and validate -their Python behavior, not just whether the generated wrapper compiles. +PRIK builds and numerically tests six maintained libraries, not just generated +wrappers. -| Project | Validated surface | Capabilities demonstrated | -| --- | --- | --- | -| [BLAS](examples/blas/README.md) | All 155 discovered routines | Scalar, vector, and matrix operations; increments and leading dimensions; in-place updates; independent expectations and f2py comparisons | -| [LAPACK](examples/lapack/README.md) | Complete implementation corpus with 127 reviewed double-precision routines | Linear solves, factorizations, eigenproblems, singular values, work arrays, and large multi-source linking | -| [FFTPACK](examples/fftpack/README.md) | All 31 public procedures | Fourier, cosine, and sine transforms; low-level workspaces; in-place arrays; allocatable results; NumPy and SciPy oracles | -| [MINPACK](examples/minpack/README.md) | All 22 public procedures | Python callbacks; nonlinear and least-squares solvers; Jacobian and workspace writeback; immutable module constants | +| Project | Validated surface | +| --- | --- | +| [BLAS](examples/blas/README.md) | 155 routines: vectors, matrices, in-place updates, and f2py comparisons | +| [LAPACK](examples/lapack/README.md) | 127 float64 routines: solves, factorizations, eigenproblems, and SVD | +| [FFTPACK](examples/fftpack/README.md) | 31 Fourier, cosine, and sine transform procedures | +| [MINPACK](examples/minpack/README.md) | 22 nonlinear and least-squares procedures, including callbacks | +| [BSPLINE-FORTRAN](examples/bspline/README.md) | 15 interpolation routines and modern Fortran classes | +| [libm](examples/libm/README.md) | 60 target-generated ISO C99 math functions | -Together they exercise arrays, callbacks, workspaces, in-place mutation, -allocatable results, module constants, and multi-file linking. The dedicated -Real Libraries CI lane builds and tests all four projects. +The **Real Libraries Portability** workflow runs all six on Linux x86-64, +Linux Arm64, macOS Intel, and macOS Arm64 with Python 3.12. The Fortran +examples use GNU Fortran 13 and GCC 13. libm runs twice on every target: GCC +13 and Clang 18 on Linux; GNU GCC 13 and Apple Clang on macOS. BLAS and LAPACK +also receive their full-surface audits on Linux x86-64. ## Key Features @@ -208,22 +210,131 @@ charts below come from the latest successfully deployed benchmark snapshot. [See the complete results, test environment, and one-command reproduction instructions.](https://pynumlab.github.io/prik/user/performance/) -## Current limitations +## Current Fortran limitations + +PRIK rejects these forms rather than wrapping them unsafely. Most fail before +code generation with a diagnostic naming the boundary and the reason. + +**Types and arrays** + +- arrays of derived types, and assumed-type `type(*)` arrays; +- character arrays that cannot be represented as a fixed-width NumPy bytes + dtype, and `allocatable` and `pointer` character *fields*. +- real and complex storage wider than the target's `long double`. NumPy's + `longdouble` is whatever the target C compiler provides, so `real(10)` and C + `long double` are supported while IEEE quad `real(16)` is refused on a target + whose `long double` is x87 extended precision. The diagnostic names the + measured mantissa width on both sides. + +**Procedures and polymorphism** + +- procedure-pointer module variables, and + callbacks retained after the wrapped call returns; +- polymorphic outputs, mutable polymorphic arguments, polymorphic + `allocatable` and `pointer` scalars, and unlimited polymorphism (`class(*)`). + +The [language feature matrix](https://pynumlab.github.io/prik/user/language-support/feature-matrix/) +records the full support status of every feature with its evidence. The +[C support guide](https://pynumlab.github.io/prik/user/language-support/c-support/) +states the direct C lane's current boundary. + +## C support + +PRIK builds C and Fortran code into importable Python extensions. For C, +generated binding code calls your exported symbol **directly** — no C adapter +and no Fortran bridge in between. + +C has no `intent` and no shape information, so a bare `double *` could be one +value, a mutable output, or an array. PRIK never guesses: it generates a +conservative contract from the source, and you edit it to say what the pointer +actually means. + +Create `stats.c`: + +```c +#include + +double mean(const double *values, size_t count) { + double total = 0.0; + for (size_t i = 0; i < count; ++i) { + total += values[i]; + } + return count == 0 ? 0.0 : total / (double)count; +} + +void extremes(const double *values, size_t count, double *low, double *high) { + *low = values[0]; + *high = values[0]; + for (size_t i = 1; i < count; ++i) { + if (values[i] < *low) { *low = values[i]; } + if (values[i] > *high) { *high = values[i]; } + } +} +``` + +Generate a starter contract: + +```bash +python3 -m prik generate --pyi --language c stats.c --out edited.pyi +``` + +Then edit `edited.pyi` so `values` is an array, `count` is derived from it, +and the two output pointers become Python results: + +```python +from prik.contracts import Arg, Float64, Return, Returns, native_call + +@native_call([Arg(0), Arg(0).shape[0]]) +def mean(values: Float64[:]) -> Float64: ... + +@native_call([Arg(0), Arg(0).shape[0], Return("low", 0), Return("high", 1)]) +def extremes(values: Float64[:]) -> tuple[Returns["low", Float64], Returns["high", Float64]]: ... +``` + +```bash +python3 -m prik --language c edited.pyi --native-c-sources stats.c --out stats +``` -PRIK does not yet support: +```python +import numpy as np +import stats + +values = np.array([3.0, 1.0, 4.0, 1.0, 5.0]) + +print(stats.mean(values)) # 2.8 +print(stats.extremes(values)) # (np.float64(1.0), np.float64(5.0)) +``` -- arrays of derived types; -- procedure pointers, including procedure-pointer module variables and callbacks - retained after the wrapped call; or -- polymorphic outputs, mutable polymorphic arguments, polymorphic arrays, - unlimited polymorphism (`class(*)`), abstract types, and deferred bindings. +`count` never appears in the Python signature — the contract derives it from +the array — and the two output pointers come back as a tuple instead of being +passed in. `mean` and `extremes` need no `@bind` because their Python and C +names match; use `@bind("native_name")` only when they differ. The same rule +applies to Fortran contracts. + +### What C support covers + +The direct C lane supports target-probed arithmetic scalars and `void`, +C-contiguous NumPy arrays of ranks 1–15, and both read-only and writable C +strings. Contracts can also rename or reorder calls, derive lengths and shapes, +return native outputs, overload Python names, and turn status codes into Python +exceptions. + +### Current C limitations + +The direct C lane does not yet cover arrays of strings, multi-level pointers, +structs, unions, function pointers, or callbacks. Unsupported declarations +stop before wrapper generation or compilation; parsing a declaration alone does +not promise that it can be built. + +[Read the C support guide for executable source, `.pyi`, CLI, and Python API +examples.](https://pynumlab.github.io/prik/user/language-support/c-support/) ## Installation & Quick Start PRIK requires **Python 3.10 or newer**, NumPy, Python development headers, -standard build tools, and Fortran and C compilers. GNU Fortran is the default -and is tested on Linux and macOS. LLVM Flang is tested on both platforms; -Intel IFX is tested on Linux. +standard build tools, and a compiler for the code being wrapped. GNU Fortran is +the default Fortran compiler and is tested on Linux and macOS. LLVM Flang is +tested on both platforms; Intel IFX is tested on Linux. Install the published PRIK package in a virtual environment: @@ -319,12 +430,11 @@ The custom wrapper flags appear in the relevant command lines: ## How it works ```text -Fortran sources +Fortran or supported C sources -> compiler preprocessing and target-type probing - -> Fortran parser - -> semantic IR construction - -> post-IR policy completion and ordered wrapper plan - -> direct native-bridge and Python-binding lowering + -> language parser and semantic IR construction + -> completed policy and wrapper plan + -> generated Python binding, with a Fortran bridge where needed -> native compilation and shared-library link -> importable Python extension ``` @@ -334,11 +444,12 @@ For diagnostic and inspection commands beyond the main build path, start with ## Python API -Root entrypoints cover normal Fortran extension builds. Advanced parsing, -semantic conversion, and `.pyi` emission use their owning packages: +Root entrypoints cover Fortran and supported direct C extension builds. +Advanced parsing, semantic conversion, and `.pyi` emission use their owning +packages: ```python -from prik import build_fortran_extension +from prik import build_c_extension, build_fortran_extension result = build_fortran_extension( "points.f90", @@ -349,6 +460,10 @@ print(result.module_name) print(result.shared_library) ``` +Use `build_c_extension("api.c", output_dir="build")` for the source-driven C +lane, or `build_pyi_extension(..., native_language="c", native_c_sources=[...])` +for an authored C contract. The C support guide shows complete examples. + ## Development PRIK is created and maintained by Said Hadjout, with extensive use of @@ -391,11 +506,10 @@ notice when redistributed. - **[Documentation](https://pynumlab.github.io/prik/)** — Learn how to install and use PRIK - **[Getting Started](https://pynumlab.github.io/prik/user/getting-started/)** — Installation, verification, standalone procedures, modules, and rebuild workflow - **[User Guide](https://pynumlab.github.io/prik/user/guide/)** — Data types, functions, modules, arrays, derived types, callbacks, ownership, and runtime behavior +- **[C Support](https://pynumlab.github.io/prik/user/language-support/c-support/)** — Direct C ABI scope, contracts, CLI, Python API, and executable examples - **[Changelog](CHANGELOG.md)** — User-visible changes by release diff --git a/docs/developer/deferred/c-parser.md b/docs/developer/deferred/c-parser.md index 56a7b907d..0e80f05d4 100644 --- a/docs/developer/deferred/c-parser.md +++ b/docs/developer/deferred/c-parser.md @@ -26,8 +26,9 @@ PRIK_C_DOCS_END --> The parser should not assess wrappability. ## CLI Workflow @@ -1054,7 +1057,7 @@ source path or source text -> CParser._assemble_project(...) or parse_c_project(...) -> CProject indexes and cross-file resolution facts -> semantics.c2ir conversion - -> policy completion and `.pyi`; a C-input runtime wrapper backend comes later + -> starter `.pyi` extraction ``` PRIK_C_DOCS_END --> @@ -1113,13 +1116,13 @@ Testing should grow in this order: Executable references: -- Shared CLI behavior: `tests/fortran/command_line_interface/pipeline/` +- Shared CLI behavior: `tests/fortran/infrastructure/cli/pipeline/` Fixture layout should be separate from Fortran: @@ -1159,7 +1162,7 @@ that Linux reference environment. The fixture suite also checks same-stem grouping order and representative raw preprocessing failures. Fatal diagnostic goldens are regenerated with -`C_PARSER_UPDATE_GOLDENS=1 PYTHONPATH=. pytest -q tests/c/parsing/test_c_error_fixture_suite.py`. +`C_PARSER_UPDATE_GOLDENS=1 PYTHONPATH=. pytest -q tests/c/infrastructure/parsing/test_c_error_fixture_suite.py`. The standalone error generator remains available for targeted refreshes. By policy, a paired project records source-to-header include edges but parses each supplied `.c`, `.h`, or `.i` member separately; include traversal is not diff --git a/docs/developer/feature-to-code-map.md b/docs/developer/feature-to-code-map.md index ddbc534b4..acf5a05c0 100644 --- a/docs/developer/feature-to-code-map.md +++ b/docs/developer/feature-to-code-map.md @@ -25,19 +25,19 @@ change crosses a stage boundary. | Capability | Relevant documentation | Change route | Focused evidence | | --- | --- | --- | --- | -| Fortran inspection and semantic IR | [Parsers](packages/parsers.md) | `prik/parsers/fortran/parser.py` → `prik/semantics/fortran2ir.py` → `prik/semantics/models.py` | `tests/fortran/source_parsing/parsing/`, `tests/fortran/semantic_ir/semantics/` | -| CLI commands and reports | [Beginner workflow](../user/getting-started/beginner-workflow.md) | `prik/cli.py` → `prik/parsers/fortran/cli.py` | `tests/fortran/command_line_interface/pipeline/`, `tests/docs/test_examples.py` | -| Source preparation and target types | [Preprocessing](packages/preprocessing.md) | `prik/preprocessing/source.py` → `prik/preprocessing/fortran.py` → `prik/preprocessing/probes/fortran_types.py` → `prik/semantics/scalar_types.py` → `prik/codegen/primitive_scalar_types.py` | `tests/fortran/source_preprocessing/preprocessing/`, `tests/fortran/data_types/` | -| Semantic `.pyi` generation and editing | [.pyi contracts](../user/reference/pyi-contracts/index.md) | `prik/parsers/pyi/parser.py` → `prik/semantics/pyi2ir.py` → `prik/pipeline/pyi.py` → `prik/printers/pyi.py` | `tests/fortran/semantic_pyi_format/parsing/`, `tests/fortran/semantic_pyi_format/semantics/`, `tests/fortran/semantic_pyi_format/pipeline/` | -| Source-first extension builds | [Building the shared library](../user/guide/building-shared-library.md) | `prik/pipeline/build.py` → `prik/pipeline/wrapper.py` → `prik/compiler/compilers.py` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py`, `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py` | -| Contract-first extension builds | [.pyi contracts](../user/reference/pyi-contracts/index.md) | `prik/pipeline/build.py` → `prik/pipeline/pyi.py` → `prik/semantics/pyi2ir.py` | `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py`, `tests/fortran/pyi_contracts/exports_and_modules/` | -| Calls, results, and optional arguments | [Functions](../user/guide/wrapping-functions.md), [subroutines](../user/guide/wrapping-subroutines.md) | `prik/semantics/fortran2ir.py` → `prik/policy/completion.py` → `prik/planning/planner.py` → `prik/codegen/c/binding.py` and `prik/codegen/fortran/bridge.py` | `tests/fortran/functions/`, `tests/fortran/optional_arguments/`, `tests/fortran/pyi_contracts/calls_and_results/` | +| Fortran inspection and semantic IR | [Parsers](packages/parsers.md) | `prik/parsers/fortran/parser.py` → `prik/semantics/fortran2ir.py` → `prik/semantics/models.py` | `tests/fortran/infrastructure/parsing/`, `tests/fortran/infrastructure/semantic_ir/semantics/` | +| CLI commands and reports | [Beginner workflow](../user/getting-started/beginner-workflow.md) | `prik/cli.py` → `prik/parsers/fortran/cli.py` | `tests/fortran/infrastructure/cli/pipeline/`, `tests/docs/test_examples.py` | +| Source preparation and target types | [Preprocessing](packages/preprocessing.md) | `prik/preprocessing/source.py` → `prik/preprocessing/fortran.py` → `prik/preprocessing/probes/fortran_types.py` → `prik/semantics/scalar_types.py` → `prik/codegen/primitive_scalar_types.py` | `tests/fortran/infrastructure/preprocessing/`, `tests/fortran/data_types/` | +| Semantic `.pyi` generation and editing | [.pyi contracts](../user/reference/pyi-contracts/index.md) | `prik/parsers/pyi/parser.py` → `prik/semantics/pyi2ir.py` → `prik/pipeline/pyi.py` → `prik/printers/pyi.py` | `tests/fortran/infrastructure/semantic_pyi/parsing/`, `tests/fortran/infrastructure/semantic_pyi/semantics/`, `tests/fortran/infrastructure/semantic_pyi/pipeline/` | +| Source-first extension builds | [Building the shared library](../user/guide/building-shared-library.md) | `prik/pipeline/build.py` → `prik/pipeline/wrapper.py` → `prik/compiler/compilers.py` | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py`, `tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py` | +| Contract-first extension builds | [.pyi contracts](../user/reference/pyi-contracts/index.md) | `prik/pipeline/build.py` → `prik/pipeline/pyi.py` → `prik/semantics/pyi2ir.py` | `tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py`, `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/` | +| Calls, results, and optional arguments | [Functions](../user/guide/wrapping-functions.md), [subroutines](../user/guide/wrapping-subroutines.md) | `prik/semantics/fortran2ir.py` → `prik/policy/completion.py` → `prik/planning/planner.py` → `prik/codegen/c/binding.py` and `prik/codegen/fortran/bridge.py` | `tests/fortran/functions/`, `tests/fortran/optional_arguments/`, `tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/` | | Arrays | [Arrays](../user/guide/arrays.md) | `prik/semantics/fortran2ir.py` → `prik/policy/completion.py` → `prik/planning/planner.py` → `prik/codegen/c/binding.py` and `prik/codegen/fortran/bridge.py` | `tests/fortran/arrays/` | -| Modules, interfaces, constants, and exported names | [Modules](../user/guide/wrapping-modules.md), [interfaces](../user/guide/generic-interfaces.md), [enumerations](../user/guide/enumerations.md) | `prik/parsers/fortran/parser.py` → `prik/semantics/fortran2ir.py` → `prik/policy/exports.py` → `prik/naming/policy.py` | `tests/fortran/modules/`, `tests/fortran/generic_interfaces/`, `tests/fortran/pyi_contracts/exports_and_modules/` | +| Modules, interfaces, constants, and exported names | [Modules](../user/guide/wrapping-modules.md), [interfaces](../user/guide/generic-interfaces.md), [enumerations](../user/guide/enumerations.md) | `prik/parsers/fortran/parser.py` → `prik/semantics/fortran2ir.py` → `prik/policy/exports.py` → `prik/naming/policy.py` | `tests/fortran/modules/`, `tests/fortran/generic_interfaces/`, `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/` | | Derived objects, allocatables, pointers, and lifetimes | [Derived types](../user/guide/wrapping-derived-types.md), [allocatables](../user/guide/allocatables.md), [pointers](../user/guide/pointers.md), [memory management](../user/guide/memory-management.md) | `prik/policy/ownership.py` → `prik/policy/construction.py` → `prik/policy/native_array_handles.py` → `prik/planning/planner.py` → `prik/runtime/handles.py` | `tests/fortran/derived_types/`, `tests/fortran/allocatables/`, `tests/fortran/pointers/` | | Callbacks | [Callbacks](../user/guide/callbacks.md) | `prik/policy/models.py` → `prik/policy/completion.py` → `prik/planning/planner.py` → `prik/codegen/c/binding.py` and `prik/codegen/fortran/bridge.py` | `tests/fortran/callbacks/` | | Projected errors | [Error handling](../user/guide/error-handling.md) | `prik/policy/models.py` → `prik/policy/completion.py` → `prik/planning/planner.py` → `prik/codegen/c/binding.py` and `prik/codegen/fortran/bridge.py` | `tests/fortran/error_handling/` | -| Native compilation, extension runtime, and public build API | [Compiler](packages/compiler.md), [Quality Assurance](workflows/quality-assurance.md) | `prik/__init__.py` → `prik/pipeline/build.py` → `prik/compiler/objects.py` → `prik/compiler/compilers.py` → `prik/compiler/native_support.py` → `prik/runtime/native_support/` | `tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py`, `tests/fortran/source_parsing/parsing/test_public_entrypoints.py` | +| Native compilation, extension runtime, and public build API | [Compiler](packages/compiler.md), [Quality Assurance](workflows/quality-assurance.md) | `prik/__init__.py` → `prik/pipeline/build.py` → `prik/compiler/objects.py` → `prik/compiler/compilers.py` → `prik/compiler/native_support.py` → `prik/runtime/native_support/` | `tests/fortran/infrastructure/building/end_to_end/test_runtime_compatibility.py`, `tests/fortran/infrastructure/parsing/test_public_entrypoints.py` | Each change route begins with the first owner for a capability; it is not a complete call graph. When a change crosses a representation boundary, the diff --git a/docs/developer/packages/codegen/c-binding.md b/docs/developer/packages/codegen/c-binding.md index 13565938d..c8882281c 100644 --- a/docs/developer/packages/codegen/c-binding.md +++ b/docs/developer/packages/codegen/c-binding.md @@ -23,7 +23,10 @@ extension initialization, and generated Python surfaces. The entrypoint view owns the C ABI prototype and call. The generator may select local names and the necessary C syntax, but never reads adapter-local conversion or original Fortran invocation facts and never chooses ownership, optionality, storage, or -conversion policy. +conversion policy. A completed direct-C entrypoint supplies the native ABI +identity and declaration facts the binding emits before calling the user symbol +directly. Source-free contracts use the completed canonical spelling. The +binding does not infer a nearby C type from a NumPy dtype or emit a C adapter. Ordinary functions use their function-owned entrypoint. Every other externally linked generated call is looked up in the generated support procedure registry. @@ -204,6 +207,8 @@ print(CSourcePrinter().doprint(wrapper)) ``` ```text +/* Python callable 'ping'. */ +/* Calls the native entrypoint 'bind_c_ping'. */ static PyObject * wrap_ping(PyObject * self, PyObject * args, PyObject * kwargs) { static char * kwlist[] = {NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "", kwlist)) return NULL; @@ -241,6 +246,8 @@ static PyObject * wrap_double_value(PyObject * self, PyObject * args, PyObject * #endif /* BINDING_DEMO_WRAPPER_H */ Rendered C binding wrapper: +/* Python callable 'double_value'. */ +/* Calls the native entrypoint 'bind_c_double_value'. */ static PyObject * wrap_double_value(PyObject * self, PyObject * args, PyObject * kwargs) { static char * kwlist[] = {"value", NULL}; PyObject * bound_value_obj; @@ -260,7 +267,8 @@ static PyObject * wrap_double_value(PyObject * self, PyObject * args, PyObject * The header exposes the planned entrypoint prototype. The wrapper's rendered body shows the Python-to-entrypoint call and conversion back to a NumPy scalar result. Policy may route that forward call to an original Fortran `bind(C)` -symbol or a generated Fortran adapter. Binding-owned callback trampolines are +symbol, a generated Fortran adapter, or the completed user C symbol. +Binding-owned callback trampolines are reverse-call entrypoints used by adapter-local callback procedures. ## Change Routes And Evidence diff --git a/docs/developer/packages/codegen/fortran-bridge.md b/docs/developer/packages/codegen/fortran-bridge.md index a131e058b..edf3a0f30 100644 --- a/docs/developer/packages/codegen/fortran-bridge.md +++ b/docs/developer/packages/codegen/fortran-bridge.md @@ -194,6 +194,8 @@ print(FortranSourcePrinter().doprint(bridge_module.procedures[0])) ``` ```text +! Adapter for native procedure 'PING'. +! Exported to the binding as the C symbol 'bind_c_ping'. subroutine bind_c_ping() bind(c, name="bind_c_ping") external :: PING call PING() @@ -245,6 +247,9 @@ module bind_c_bridge_demo_wrapper use bridge_demo, only: native_double_value => DOUBLE_VALUE implicit none contains + + ! Adapter for native procedure 'DOUBLE_VALUE'. + ! Exported to the binding as the C symbol 'bind_c_double_value'. function bind_c_double_value(value) result(result) bind(c, name="bind_c_double_value") real(c_double), value :: value real(c_double) :: result diff --git a/docs/developer/packages/compiler.md b/docs/developer/packages/compiler.md index 015ea8e27..10fdb2d10 100644 --- a/docs/developer/packages/compiler.md +++ b/docs/developer/packages/compiler.md @@ -185,9 +185,9 @@ and conditional support installation. | Evidence | What it establishes | | --- | --- | -| [Compiler profile and command construction](../../../tests/fortran/building_shared_library/compiling/test_compiler_verbose.py) | Coherent C/Fortran driver selection, explicit overrides, profile and user-flag order, optional-flag probing, record-only mode, and preserved link-input order. | -| [Generated-wrapper build handoff](../../../tests/fortran/building_shared_library/pipeline/test_generated_wrapper_build.py) | Generated sources, conditional support installation, explicit C and Fortran object requests, and the final ordered link request passed from the pipeline. | -| [Source build modes](../../../tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py) | The selected source-build mode produces an importable native extension. | +| [Compiler profile and command construction](../../../tests/fortran/infrastructure/building/compiling/test_compiler_verbose.py) | Coherent C/Fortran driver selection, explicit overrides, profile and user-flag order, optional-flag probing, record-only mode, and preserved link-input order. | +| [Generated-wrapper build handoff](../../../tests/fortran/infrastructure/building/pipeline/test_generated_wrapper_build.py) | Generated sources, conditional support installation, explicit C and Fortran object requests, and the final ordered link request passed from the pipeline. | +| [Source build modes](../../../tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py) | The selected source-build mode produces an importable native extension. | | [Native-support surface](../../../tests/fortran/infrastructure/runtime/test_native_support.py) | The bundled payload remains header-only and exposes the small native binding API expected by generated sources. | ## Change Routes diff --git a/docs/developer/packages/contracts.md b/docs/developer/packages/contracts.md index 014138088..d72bdd2b6 100644 --- a/docs/developer/packages/contracts.md +++ b/docs/developer/packages/contracts.md @@ -81,8 +81,8 @@ later stages interpret those facts. | Evidence | What it establishes | | --- | --- | | [Contract runtime tests](../../../tests/fortran/data_types/runtime/) | Concrete scalar constructors and invalid constructor use. | -| [Semantic `.pyi` parser tests](../../../tests/fortran/semantic_pyi_format/parsing/) | Recognition of the public vocabulary and annotation syntax. | -| [Semantic `.pyi` pipeline tests](../../../tests/fortran/semantic_pyi_format/pipeline/) | Contract loading, semantic conversion, and re-emission. | +| [Semantic `.pyi` parser tests](../../../tests/fortran/infrastructure/semantic_pyi/parsing/) | Recognition of the public vocabulary and annotation syntax. | +| [Semantic `.pyi` pipeline tests](../../../tests/fortran/infrastructure/semantic_pyi/pipeline/) | Contract loading, semantic conversion, and re-emission. | The import path and public names are part of the file format. A name being valid Python syntax does not by itself make the corresponding wrapper behavior diff --git a/docs/developer/packages/parsers.md b/docs/developer/packages/parsers.md index bc9924176..3acb723d9 100644 --- a/docs/developer/packages/parsers.md +++ b/docs/developer/packages/parsers.md @@ -17,9 +17,10 @@ semantic-`.pyi` frontend returns a standard Python AST. A parser reports what its input says; it does not assign stable semantic types, choose ownership, decide wrapper support, or emit a Python API. -The `c/` directory is early work for a future C frontend. C support is not yet -complete and is outside the current Fortran-wrapper route, so this guide covers -only the supported Fortran and semantic-`.pyi` parsers. +The `c/` frontend preserves C declarations, types, locations, directives, and +project relationships before semantic conversion. Its detailed parser model is +documented in the [C parser reference](../deferred/c-parser.md); the public +wrapping surface belongs to [C support](../../user/language-support/c-support.md). ## Inputs And Results @@ -60,7 +61,7 @@ prik/parsers/ ├── pyi/ │ ├── __init__.py │ └── parser.py -└── c/ incomplete future C frontend +└── c/ C parser models and project assembly ``` ## Directory Tour @@ -246,11 +247,11 @@ conversion remains the next stage's responsibility. | Evidence | What it establishes | | --- | --- | -| [Fortran parser suite](../../../tests/fortran/source_parsing/parsing/) | Source forms, units, declarations, scopes, diagnostics, project assembly, and parser models. | -| [Public parser entrypoints](../../../tests/fortran/source_parsing/parsing/test_public_entrypoints.py) | File, project, and singular-unit entrypoint contracts. | -| [Source forms and diagnostics](../../../tests/fortran/source_parsing/parsing/test_source_form_and_diagnostics_regressions.py) | Logical source preparation, unit boundaries, and public diagnostic metadata. | -| [Parser CLI](../../../tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py) | Module launcher, report modes, diagnostic presentation, and explicit semantic/`.pyi` inspection modes. | -| [Semantic `.pyi` parsing](../../../tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py) | Raw `ast.Module` results and the AST-to-semantic-conversion handoff. | +| [Fortran parser suite](../../../tests/fortran/infrastructure/parsing/) | Source forms, units, declarations, scopes, diagnostics, project assembly, and parser models. | +| [Public parser entrypoints](../../../tests/fortran/infrastructure/parsing/test_public_entrypoints.py) | File, project, and singular-unit entrypoint contracts. | +| [Source forms and diagnostics](../../../tests/fortran/infrastructure/parsing/test_source_form_and_diagnostics_regressions.py) | Logical source preparation, unit boundaries, and public diagnostic metadata. | +| [Parser CLI](../../../tests/fortran/infrastructure/cli/pipeline/test_stage_dispatch.py) | Module launcher, report modes, diagnostic presentation, and explicit semantic/`.pyi` inspection modes. | +| [Semantic `.pyi` parsing](../../../tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py) | Raw `ast.Module` results and the AST-to-semantic-conversion handoff. | ## Change Routes diff --git a/docs/developer/packages/pipeline.md b/docs/developer/packages/pipeline.md index 39afa79ba..98394a1b4 100644 --- a/docs/developer/packages/pipeline.md +++ b/docs/developer/packages/pipeline.md @@ -19,11 +19,13 @@ commands. ## A Source Build Through This Component -The source-first public entrypoint is `build_fortran_extension`. It delegates -each transformation to its owner, then carries the resulting objects forward: +The source-first public entrypoints are `build_fortran_extension` and +`build_c_extension`. Both delegate each transformation to their owner, then +carry resulting objects forward. The C route consumes a completed direct +entrypoint policy or raises before planning and artifact materialization. ```text -Fortran source +Fortran or C source -> preprocessing, parsing, and semantic conversion -> policy completion -> WrapperPlanner @@ -56,9 +58,9 @@ prik/pipeline/ | Module | Main entrypoints and contents | Change it when | | --- | --- | --- | | [`prik/pipeline/pyi.py`](../../../prik/pipeline/pyi.py) | `pyi_*_to_semantic_module()` loads text, files, or path sets into semantic modules. `emit_module_stubs()` completes copied modules and renders `.pyi` stubs. | Contract loading, external-type reconciliation, per-operation cache behavior, or stub output. | -| [`prik/pipeline/type_mapping_report.py`](../../../prik/pipeline/type_mapping_report.py) | Converts compiler probe facts through semantic conversion and backend dtype projection into a Markdown report. | Datatype-report content or its cross-stage evidence. | +| [`prik/pipeline/type_mapping_report.py`](../../../prik/pipeline/type_mapping_report.py) | Converts compiler probe facts through semantic conversion and backend dtype projection into a measured report record, then renders it as Markdown. | Datatype-report content or its cross-stage evidence. | | [`prik/pipeline/wrapper.py`](../../../prik/pipeline/wrapper.py) | `WrapperGenerator.generate()` freezes and validates a `ModulePlan`, delegates backend generation and printing, and returns an in-memory `GeneratedWrapper`. | Plan-to-rendered-wrapper orchestration. | -| [`prik/pipeline/build.py`](../../../prik/pipeline/build.py) | `build_fortran_extension()`, `build_pyi_extension()`, and `build_pyi_extension_from_manifest()` write artifacts, prepare native inputs, compile/link, and return `WrapperBuildResult`. `NativeBuildPlan` records those native inputs. | Public build behavior, artifact layout, build modes, manifests, scheduling, linking, or extension import. | +| [`prik/pipeline/build.py`](../../../prik/pipeline/build.py) | `build_fortran_extension()`, `build_c_extension()`, `build_pyi_extension()`, and `build_pyi_extension_from_manifest()` write artifacts, prepare native inputs, compile/link, and return `WrapperBuildResult`. `NativeBuildPlan` records those native inputs. | Public build behavior, artifact layout, build modes, manifests, scheduling, linking, or extension import. | ## Module Workflows @@ -76,7 +78,9 @@ prik/pipeline/ when both groups share one physical Fortran payload. - **`type_mapping_report.py` is inspection only.** Its fixed C and Fortran inventories pass through the normal target probes, semantic converters, and - NumPy dtype registry before Markdown rendering. It does not create a wrapper. + NumPy dtype registry into a measured record. `type_mapping_markdown()` is the + only Markdown path for that record, so the table cannot drift from the JSON + form. It does not create a wrapper. ## `build.py` Navigation @@ -106,6 +110,11 @@ combines retained native-language requirements with generated and caller-native object languages, so absence of a generated adapter never implies absence of the Fortran runtime. +Native implementation language is explicit throughout the build and manifest +paths. C and Fortran source collections remain distinct, and a source-free +`.pyi` build selects its native language explicitly rather than deriving it +from a compiler or ABI decorator. + The same rule applies when a source-free direct Fortran contract resolves its symbol from a prebuilt object, static archive, or shared library. Those inputs remain ordered `NativeLinkItem` records; direct routing changes generated @@ -200,10 +209,10 @@ measured fact, semantic identity, and NumPy projection separate. | Evidence | What it establishes | | --- | --- | | [Pipeline infrastructure](../../../tests/fortran/infrastructure/pipeline/) | Plan-to-rendered-wrapper assembly and cross-stage records. | -| [Semantic `.pyi` pipeline](../../../tests/fortran/semantic_pyi_format/pipeline/) | Contract loading, reconciliation, and stub emission. | -| [Build pipeline](../../../tests/fortran/building_shared_library/pipeline/) | Artifact output, manifests, build modes, and build-plan handoffs. | -| [Compilation integration](../../../tests/fortran/building_shared_library/compiling/) | Native command integration. | -| [End-to-end builds](../../../tests/fortran/building_shared_library/end_to_end/) | Build, import, and generated-extension behavior. | +| [Semantic `.pyi` pipeline](../../../tests/fortran/infrastructure/semantic_pyi/pipeline/) | Contract loading, reconciliation, and stub emission. | +| [Build pipeline](../../../tests/fortran/infrastructure/building/pipeline/) | Artifact output, manifests, build modes, and build-plan handoffs. | +| [Compilation integration](../../../tests/fortran/infrastructure/building/compiling/) | Native command integration. | +| [End-to-end builds](../../../tests/fortran/infrastructure/building/end_to_end/) | Build, import, and generated-extension behavior. | ## Change Routes diff --git a/docs/developer/packages/policy.md b/docs/developer/packages/policy.md index 79dd47433..edff1cbec 100644 --- a/docs/developer/packages/policy.md +++ b/docs/developer/packages/policy.md @@ -115,6 +115,13 @@ interoperable; otherwise it keeps the adapter route. Optional non-`VALUE` interoperable dummies use a nullable C pointer, while optional `VALUE` dummies remain adapter-backed. +A C-source or explicitly C-native `.pyi` operation instead selects +`DIRECT_C_ABI` only when completed direct-C policy supports its ABI and +contract. The policy carries the native declaration identity, transport, and +user symbol required downstream. An ineligible C operation raises its stable +diagnostic before `WrapperPlanner` runs and never falls back to +`GENERATED_FORTRAN_ADAPTER`. + An immediate callback is directly interoperable only when both the containing procedure and its named callback prototype retain the Fortran C ABI marker, and every callback argument/result has a supported scalar C value or reference @@ -311,8 +318,8 @@ generate source; that begins only after planning. | Evidence | What it establishes | | --- | --- | -| [Policy completion](../../../tests/fortran/infrastructure/semantics/test_policy_completion.py) | Completion precedes lowering; accessor, projection, and missing-conversion failures remain explicit. | -| [Wrapper policy](../../../tests/fortran/infrastructure/semantics/test_wrapper_policy.py) | Function, result, call-slot, array, export, status, and support policies are complete before planning. | +| [Policy completion](../../../tests/fortran/infrastructure/policy/test_policy_completion.py) | Completion precedes lowering; accessor, projection, and missing-conversion failures remain explicit. | +| [Wrapper policy](../../../tests/fortran/infrastructure/policy/test_wrapper_policy.py) | Function, result, call-slot, array, export, status, and support policies are complete before planning. | | [Ownership policy](../../../tests/fortran/memory_management/policy/test_memory_ownership_policy.py) | Contradictory explicit ownership contracts fail before lowering. | | [Descriptor handle policy](../../../tests/fortran/allocatables/policy/test_allocatable_handle_policy.py) | Allocatable descriptor-handle decisions, ownership, access, and support blockers. | | [Planner boundary](../../../tests/fortran/infrastructure/codegen/test_planner.py) | Planning rejects a missing completed wrapper policy instead of filling it in. | diff --git a/docs/developer/packages/preprocessing.md b/docs/developer/packages/preprocessing.md index 99980cb76..787da2936 100644 --- a/docs/developer/packages/preprocessing.md +++ b/docs/developer/packages/preprocessing.md @@ -17,10 +17,11 @@ compiler invocation, source provenance, native `INCLUDE` expansion, and target probes. It does not parse declarations, construct semantic IR, choose semantic scalar identities, or complete wrapper policy. -The package contains early C-frontend modules: `c.py` collects raw directive -metadata and `probes/c_types.py` measures C ABI facts. C support is not yet -complete; a future C frontend may build on them. They do not participate in -the current Fortran wrapper path. +For C inputs, `c.py` records raw directive metadata and prepares compiler- +preprocessed parser input, while `probes/c_types.py` measures target ABI facts. +The [C parser reference](../deferred/c-parser.md) owns the detailed frontend +workflow and [C support](../../user/language-support/c-support.md) owns the +public wrapping boundary. ## A Fortran Source Through This Stage @@ -189,8 +190,8 @@ compiler, rather than PRIK, supplied the fact. | Evidence | What it establishes | | --- | --- | -| [Fortran preprocessing](../../../tests/fortran/source_preprocessing/preprocessing/) | Adapters, recipes, mappings, native includes, diagnostics, and parser handoffs. | -| [Parser boundaries](../../../tests/fortran/source_preprocessing/preprocessing/test_parser_boundaries.py) | Prepared source reaches parsing with preserved facts and unsupported raw constructs stop at the correct boundary. | +| [Fortran preprocessing](../../../tests/fortran/infrastructure/preprocessing/) | Adapters, recipes, mappings, native includes, diagnostics, and parser handoffs. | +| [Parser boundaries](../../../tests/fortran/infrastructure/preprocessing/test_parser_boundaries.py) | Prepared source reaches parsing with preserved facts and unsupported raw constructs stop at the correct boundary. | | [Fortran type probes](../../../tests/fortran/data_types/probes/test_fortran_type_probes.py) | Compiler facts, requirement evaluation, cache separation, and report validation. | ## Change Routes diff --git a/docs/developer/packages/printers.md b/docs/developer/packages/printers.md index a5b71e9ae..f6f7c850d 100644 --- a/docs/developer/packages/printers.md +++ b/docs/developer/packages/printers.md @@ -125,6 +125,7 @@ module bind_c_printer_demo_wrapper use printer_demo, only: native_double_value => DOUBLE_VALUE implicit none contains + function bind_c_double_value(value) result(result) bind(c, name="DOUBLE_VALUE") real(c_double), value :: value real(c_double) :: result @@ -163,8 +164,8 @@ wrapper policy. | Evidence | What it establishes | | --- | --- | | [Native source printers](../../../tests/fortran/infrastructure/printers/test_source_printers.py) | C and Fortran serialization, rejection of wrapper plans, line wrapping, literal preservation, and unsplittable-line diagnostics. | -| [Semantic `.pyi` conversion smoke](../../../tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py) | Emitted contract fixtures can be parsed and converted through the normal semantic-`.pyi` route. | -| [`.pyi` imports and packages](../../../tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_imports_and_packages.py) | Isolated emission state, imports, aliases, packages, name collisions, and opaque dependencies. | +| [Semantic `.pyi` conversion smoke](../../../tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_conversion_smoke.py) | Emitted contract fixtures can be parsed and converted through the normal semantic-`.pyi` route. | +| [`.pyi` imports and packages](../../../tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_imports_and_packages.py) | Isolated emission state, imports, aliases, packages, name collisions, and opaque dependencies. | ## Change Routes diff --git a/docs/developer/packages/runtime.md b/docs/developer/packages/runtime.md index b4cbc6242..bb6269727 100644 --- a/docs/developer/packages/runtime.md +++ b/docs/developer/packages/runtime.md @@ -95,7 +95,7 @@ the compiler installs it into a generated `binding_support/` directory. | [Pointer runtime tests](../../../tests/fortran/pointers/runtime/) | Association, nullification, pointer descriptors, and views. | | [Memory-management runtime tests](../../../tests/fortran/memory_management/runtime/) | Owner retention, release, and array handoffs. | | [Native-support tests](../../../tests/fortran/infrastructure/runtime/) | Bundled payload discovery and installation inputs. | -| [Compiled runtime compatibility](../../../tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py) | The payload and Python runtime working through a real extension. | +| [Compiled runtime compatibility](../../../tests/fortran/infrastructure/building/end_to_end/test_runtime_compatibility.py) | The payload and Python runtime working through a real extension. | An outstanding zero-copy NumPy view cannot be revoked after native reallocation, deallocation, or pointer reassociation. Users must discard or diff --git a/docs/developer/packages/semantics.md b/docs/developer/packages/semantics.md index e4b13667a..8c89f4688 100644 --- a/docs/developer/packages/semantics.md +++ b/docs/developer/packages/semantics.md @@ -17,9 +17,10 @@ and public identities, shapes, storage contracts, projections, provenance, and raw contract metadata. It does not complete ownership, choose lowering actions, plan wrappers, or emit source. -`c2ir.py` is preparatory work for a future C frontend. C support is not yet -complete and is outside the current Fortran-wrapper route, so this guide covers -the supported Fortran and semantic-`.pyi` paths. +`c2ir.py` converts modeled C declarations into the same semantic graph. The +[C parser reference](../deferred/c-parser.md) owns that frontend handoff and +[C support](../../user/language-support/c-support.md) owns the supported public +surface. ## Inputs And Shared Representation @@ -67,14 +68,14 @@ prik/semantics/ ├── ownership_metadata.py ├── native_array_handles.py ├── native_contract.py -└── c2ir.py incomplete future C frontend +└── c2ir.py C parser-model conversion ``` ## Directory Tour | Module | Public boundary and result | Change it when | | --- | --- | --- | -| [`prik/semantics/__init__.py`](../../../prik/semantics/__init__.py) | Re-exports frontend-conversion helpers. Its C exports are preparatory, not a supported C wrapper route. | The semantic-conversion import surface changes. | +| [`prik/semantics/__init__.py`](../../../prik/semantics/__init__.py) | Re-exports frontend-conversion helpers for Fortran, C, and semantic `.pyi` inputs. | The semantic-conversion import surface changes. | | [`prik/semantics/models.py`](../../../prik/semantics/models.py) | Defines the shared `SemanticModule` graph, its declarations, types, contracts, projections, origins, and equality rules. | A later stage needs a new language-neutral fact. | | [`prik/semantics/scalar_types.py`](../../../prik/semantics/scalar_types.py) | `SemanticScalarSpec` and the scalar catalogue define stable scalar identities, families, and intrinsic storage widths without backend spellings. | Stable scalar vocabulary or intrinsic scalar facts change. | | [`prik/semantics/fortran2ir.py`](../../../prik/semantics/fortran2ir.py) | `FortranToIRConverter` and file/module/project helpers convert parser models with optional compiler facts into semantic modules. | A Fortran source fact needs different semantic meaning. | @@ -283,11 +284,11 @@ before policy completion or any backend lowering begins. | Evidence | What it establishes | | --- | --- | -| [Semantic IR conversion](../../../tests/fortran/semantic_ir/semantics/) | Fortran-model conversion, compile-time requirements, specialization, and semantic graph properties. | +| [Semantic IR conversion](../../../tests/fortran/infrastructure/semantic_ir/semantics/) | Fortran-model conversion, compile-time requirements, specialization, and semantic graph properties. | | [Fortran datatype semantics](../../../tests/fortran/data_types/semantics/) | Stable scalar identities, storage facts, and compiler-measurement handoffs. | -| [Semantic `.pyi` conversion](../../../tests/fortran/semantic_pyi_format/semantics/) | Contract constructs, imports, external references, projections, classes, overloads, and round trips. | -| [Native array handles](../../../tests/fortran/infrastructure/semantics/test_native_array_handles.py) | Descriptor marking and separation of handle, data, and element facts. | -| [Native contract validation](../../../tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py) | Native-contract preparation, validation, and diagnostic ownership. | +| [Semantic `.pyi` conversion](../../../tests/fortran/infrastructure/semantic_pyi/semantics/) | Contract constructs, imports, external references, projections, classes, overloads, and round trips. | +| [Native array handles](../../../tests/fortran/infrastructure/policy/test_native_array_handles.py) | Descriptor marking and separation of handle, data, and element facts. | +| [Native contract validation](../../../tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py) | Native-contract preparation, validation, and diagnostic ownership. | ## Change Routes diff --git a/docs/developer/roadmap/documentation-content-checklist.md b/docs/developer/roadmap/documentation-content-checklist.md index ca9954333..fe478e34f 100644 --- a/docs/developer/roadmap/documentation-content-checklist.md +++ b/docs/developer/roadmap/documentation-content-checklist.md @@ -57,22 +57,9 @@ more specialized pages. ### Troubleshooting, FAQ, And Releases -- [ ] `docs/user/troubleshooting/index.md`: route users by symptom: install, build, - compiler, runtime, platform, wrapper contract, and generated artifact issues. -- [ ] `docs/user/troubleshooting/installation-issues.md`: document missing Python - headers, NumPy, compiler packages, virtual environments, and platform package - names. -- [ ] `docs/user/troubleshooting/build-issues.md`: document compile/link failures, - missing native libraries, Makefile regeneration, output directories, and - verbose logs. - [ ] `docs/user/troubleshooting/compiler-issues.md`: document compiler detection, Fortran flags, preprocessing, ABI probes, GNU ABI assumptions, and kind support failures. -- [ ] `docs/user/troubleshooting/runtime-issues.md`: document import failures, - symbol lookup errors, dtype or shape errors, callback exceptions, finalization, - and cleanup symptoms. -- [ ] `docs/user/troubleshooting/platform-specific-issues.md`: document Linux, - macOS, Windows, compiler, linker, and shared-library path caveats. - [x] `CHANGELOG.md`: defines the changelog policy and release-note shape at the repository root, where package users and GitHub visitors can find it. @@ -106,46 +93,20 @@ The old TODO-only contributor pages, duplicate pipeline/codebase maps, completed wrapper-plan and native-array migration ledgers, and separate internal indexes were removed after their stable facts moved to these owners. -### Tutorials And Examples +### Examples + +Only runnable pages belong in this queue. Add a tutorial, troubleshooting page, +or project example when its checked content is ready. -- [ ] `docs/user/tutorials/numerical-solver.md`: add a fast checked solver fixture, - build command, Python call, expected numeric output, and validation notes. -- [ ] `docs/user/tutorials/scientific-library.md`: document a small multi-routine - library workflow, package shape, generated `.pyi` review, and regression - checks. -- [ ] `docs/user/tutorials/modern-fortran-project.md`: document modules, derived - types, arrays, constructors, and limitations using checked modern Fortran - examples. -- [ ] `docs/user/tutorials/large-fortran-codebase.md`: document source ordering, - dependency strategy, generated contract review, staged verification, and - current limits for automatic dependency discovery. -- [ ] `docs/user/tutorials/packaging.md`: document packaging a generated extension, - native artifacts, wheel limitations, and reproducible build notes. - [ ] `docs/user/examples/blas-wrapper.md`: add the minimal BLAS-style runtime example or document the external dependency, with build, import, and numerical assertions. - [ ] `docs/user/examples/lapack-wrapper.md`: document the LAPACK example as CI-owned by default, including why local runs are optional and what evidence CI supplies. -- [ ] `docs/user/examples/openmp-example.md`: document supported OpenMP path, - required compiler flags, runtime environment variables, and fallback behavior. -- [ ] `docs/user/examples/object-oriented-fortran.md`: document classes, - type-bound procedures, construction, finalization, and unsupported object - model features with checked output. -- [ ] `docs/user/examples/ode-solver.md`: add a compact checked ODE fixture, - expected result tolerance, and failure troubleshooting. -- [ ] `docs/user/examples/cfd-mini-example.md`: define a small enough fixture, - supported array contracts, build command, and runtime validation. -- [ ] `docs/user/examples/mpi-example.md`: keep this page explicitly - not-yet-implemented until MPI build, runtime, and distribution constraints have - real evidence. ### Project Entry And Site Shell -- [ ] `docs/user/tutorials/index.md`: explain which tutorials are maintained and which - are planned, with expected prerequisites and runtime cost. -- [ ] `docs/user/examples/index.md`: split verified cookbook recipes from - planned larger examples and state the evidence required for each example. - [x] `docs/developer/packages/index.md`: route contributors from each production package to its canonical guide. - [x] `docs/developer/index.md`: distinguish implemented package references, diff --git a/docs/developer/roadmap/fortran-test-suite-cleanup-checklist.md b/docs/developer/roadmap/fortran-test-suite-cleanup-checklist.md index 5a3c1afc1..afaa90c7b 100644 --- a/docs/developer/roadmap/fortran-test-suite-cleanup-checklist.md +++ b/docs/developer/roadmap/fortran-test-suite-cleanup-checklist.md @@ -235,7 +235,7 @@ Rules: - [x] Public argument parsing and output formatting belong in the owning input language's command-line feature. - [x] Cross-feature Fortran command contracts belong in - `tests/fortran/command_line_interface/pipeline/`. + `tests/fortran/infrastructure/cli/pipeline/`. - [x] A CLI test that builds, imports, calls, and verifies a Fortran extension belongs in the owning feature's `end_to_end/` directory, normally `building_shared_library/end_to_end/`. @@ -387,12 +387,12 @@ directory. Audit and place every artifact beside its final behavioral owner. | `tests/data/fortran/general/` | Owning feature/stage; feature-neutral setup is minimized beside its final public-capability owner | | `tests/data/fortran/errors/` | Fixture directory of the first rejecting stage | | `tests/data/fortran/blas/` and `lapack/` | `examples/blas/native/` and `examples/lapack/native/` | -| Parser regressions extracted from SciFortran | `tests/fortran/source_parsing/parsing/test_real_world_interaction_regressions.py` | +| Parser regressions extracted from SciFortran | `tests/fortran/infrastructure/parsing/test_real_world_interaction_regressions.py` | | Parser source/JSON pairs | Beside their parser owner | -| Language-neutral `.pyi` syntax | `tests/fortran/semantic_pyi_format/` | -| Fortran `.pyi` build fixtures | `tests/fortran/semantic_pyi_format/{pipeline,end_to_end}/fixtures/` | +| Language-neutral `.pyi` syntax | `tests/fortran/infrastructure/semantic_pyi/` | +| Fortran `.pyi` build fixtures | `tests/fortran/infrastructure/semantic_pyi/{pipeline,end_to_end}/fixtures/` | | Generated contract goldens | Beside their generation/package-shape owner | -| Edited contracts | `tests/fortran/pyi_contracts//end_to_end/fixtures/` | +| Edited contracts | `tests/fortran/infrastructure/semantic_pyi/contracts//end_to_end/fixtures/` | | Invalid `.pyi` contracts | Fixture directory of the first rejecting stage | ### Native sources @@ -466,9 +466,9 @@ An edited contract is authoritative input, not expected generated output. | Owner | What it proves | | --- | --- | -| `tests/fortran/semantic_pyi_format/pipeline/` | Loading, import graph, package assembly, build plan, and diagnostics | -| `tests/fortran/semantic_pyi_format/end_to_end/` | An ordinary contract is authoritative input and produces a working extension | -| `tests/fortran/pyi_contracts//end_to_end/` | A documented edit changes the built API or runtime behavior | +| `tests/fortran/infrastructure/semantic_pyi/pipeline/` | Loading, import graph, package assembly, build plan, and diagnostics | +| `tests/fortran/infrastructure/semantic_pyi/end_to_end/` | An ordinary contract is authoritative input and produces a working extension | +| `tests/fortran/infrastructure/semantic_pyi/contracts//end_to_end/` | A documented edit changes the built API or runtime behavior | The end-to-end baseline contains: @@ -1089,7 +1089,7 @@ attributed all 303 SciFortran sources to upstream revision measured 37 lines plus 27 branches that the focused parser suite had not reached. A follow-up contextual-coverage audit traced all 64 items to 12 source units and reduced them to five named inline tests in -`tests/fortran/source_parsing/parsing/test_real_world_interaction_regressions.py`. +`tests/fortran/infrastructure/parsing/test_real_world_interaction_regressions.py`. The focused parser suite now executes all 64 formerly unique items without the third-party project. Existing focused tests retain the historical `CLASS(...)`, CPP, scope, `EXTERNAL`, `SAVE`/local-type, `USE`-rename, and @@ -1413,8 +1413,8 @@ compilation, linking, loading, and the same runtime smoke all succeed. - [ ] Implement GNU, Intel ifx, LLVM Flang, and NVIDIA nvfortran one profile at a time. - [x] Add focused command/capability tests under - `tests/fortran/building_shared_library/compiling/` and - `tests/fortran/source_preprocessing/preprocessing/`. + `tests/fortran/infrastructure/building/compiling/` and + `tests/fortran/infrastructure/preprocessing/`. - [ ] Carry compiler-derived target facts through semantics and the shared plan; bridge/binding generators do not infer semantic policy from compiler family. - [x] Give unknown and unsupported compilers explicit diagnostics. diff --git a/docs/developer/roadmap/index.md b/docs/developer/roadmap/index.md index c37e6808a..7bd6590ed 100644 --- a/docs/developer/roadmap/index.md +++ b/docs/developer/roadmap/index.md @@ -2,7 +2,7 @@ title: Active Roadmaps audience: developers, maintainers, contributors prerequisites: contributor architecture guide, current support matrix -related: ../../user/language-support/feature-matrix.md, native-entrypoint-adoption-checklist.md, semantic-pyi-wrapper-checklist.md, fortran-test-suite-cleanup-checklist.md, documentation-content-checklist.md +related: ../../user/language-support/feature-matrix.md, semantic-pyi-wrapper-checklist.md, fortran-test-suite-cleanup-checklist.md, documentation-content-checklist.md status: active-roadmap publication: draft --- @@ -16,7 +16,6 @@ decisions and evidence routes have moved to canonical documentation. ## Active Work - [Semantic `.pyi` wrapper completion](semantic-pyi-wrapper-checklist.md) -- [Native entrypoint and adapter adoption](native-entrypoint-adoption-checklist.md) - [Language-first test suite and remaining compiler/CI work](fortran-test-suite-cleanup-checklist.md) - [Remaining documentation content](documentation-content-checklist.md) diff --git a/docs/developer/roadmap/native-entrypoint-adoption-checklist.md b/docs/developer/roadmap/native-entrypoint-adoption-checklist.md deleted file mode 100644 index d5f2dbd70..000000000 --- a/docs/developer/roadmap/native-entrypoint-adoption-checklist.md +++ /dev/null @@ -1,959 +0,0 @@ ---- -title: Native Entrypoint and Adapter Adoption Checklist -audience: maintainers -prerequisites: contributor architecture guide, policy stage, planning stage, pipeline component, testing strategy -related: ../architecture.md, ../packages/policy.md, ../packages/planning.md, ../packages/pipeline.md, ../testing-strategy.md, ../../user/reference/semantic-pyi-format.md, ../../user/language-support/feature-matrix.md, index.md -status: active-roadmap -publication: draft ---- - -# Native Entrypoint and Adapter Adoption Checklist - -This checklist tracks two related changes: - -1. existing Fortran `bind(C)` operations can bypass the generated Fortran - adapter when their completed ABI contract is directly callable; and -2. the first C wrapper backend accepts only operations that the generated C - binding can call directly, without a generated native C adapter. - -This is an implementation roadmap, not a current support claim. The -[language feature matrix](../../user/language-support/feature-matrix.md) -remains authoritative until compiled and imported runtime evidence exists. - -## Terminology And Fixed Decisions - -- The **binding** is the generated CPython C extension. Every wrapped module - still has a binding even when it has no native adapter. -- A native **adapter** is optional generated Fortran or C code between that - binding and the user's native operation. The existing generated Fortran - `bind(C)` bridge is the Fortran adapter. -- A **direct C ABI entrypoint** means that the binding calls the user's - linkable C ABI symbol. Binding-local conversion, validation, temporary - storage, writeback, and Python result construction are still allowed and do - not by themselves require an adapter. -- Every callable native operation owns one completed entrypoint decision. - Functions, subroutines, overload candidates, methods, constructors, - destructors, and callable getter, setter, or lifecycle operations are - decided individually. A class, overload set, or module does not impose one - route on all of its operations. -- Fortran source records `bind(C)` and its optional native label as ABI facts. - A source-free Fortran semantic `.pyi` contract uses `@native_abi("c")` to - record the same fact; `@bind("symbol")` continues to mean symbol naming - only. -- A C source or C semantic-contract build is C ABI by language identity. It - does not use an opposite or redundant per-function ABI decorator. -- `bind(C)` is necessary but not sufficient for a direct Fortran route. Policy - considers the whole operation: linkability, calling convention, argument - projection, representation, ownership, lifetime, nullability, mutation, - writeback, callbacks, result projection, and lifecycle behavior. Planning, - binding generation, and adapter generation never infer the route from a - datatype or source spelling. -- Initial C wrapper support has no generated native C-adapter fallback. An - operation is either completed as a direct C ABI entrypoint or rejected by a - policy diagnostic before planning. -- Generated support procedures are not adapters for a user procedure. Derived - field accessors, module-variable accessors, constructors, destructors, holder - lifecycle operations, descriptor operations, and callback trampolines keep - their own implementation owner. A module whose user procedures are all - direct may therefore still require generated Fortran support source; that - source must not contain adapter wrappers for those direct procedures. -- Traditional compiler-specific Fortran external ABIs, including ordinary - BLAS/LAPACK-style procedures without `bind(C)`, continue through a Fortran - adapter. Direct calls to unstandardized compiler symbols are outside this - roadmap. - -## Required Plan And Artifact Shapes - -| Native module shape | Required generated artifacts | -| --- | --- | -| Ordinary Fortran procedures only | C binding plus a Fortran adapter containing every wrapped operation. | -| Mixed ordinary and directly callable `bind(C)` Fortran procedures | C binding plus generated Fortran source containing only operations selected for adaptation and independently required support procedures. Direct user operations are absent from the adapter membership. | -| Directly callable `bind(C)` Fortran procedures only, with no Fortran-owned support operations | C binding and header; no generated Fortran source or object. | -| Directly callable `bind(C)` Fortran procedures plus Fortran-owned support operations | C binding and header plus support-only Fortran source/object. No direct user procedure receives an adapter wrapper. | -| Supported initial C module | C binding and header; no native C adapter source or adapter object. | -| C operation that would require a native adapter | Policy diagnostic before planning or source generation. No partial wrapper artifacts. | - -Adapter membership and generated-support membership are derived independently -from completed per-operation decisions. Neither is a module-level semantic -switch. A generated Fortran file may initially contain both groups, but an -artifact assertion must still distinguish adapted user operations from -generated support procedures. The file and object are absent only when both -groups are empty. - -## Goal 1 — Behavior-Preserving Entrypoint Separation - -This is the first implementation goal. It creates the architectural boundary -needed by direct routing without enabling direct routing, making the adapter -optional, adding C runtime wrapping, or changing any generated source. - -During this goal every currently supported Fortran operation remains backed by -the generated Fortran adapter. Only the shared wrapper-plan representation and -the plan facets consumed by the two generators change: - -```text -FunctionPlan -├── binding -│ └── Python extraction, validation, local storage, and result construction -├── entrypoint -│ └── C ABI symbol, prototype, ordered parameters, actual projection, and result transport -└── bridge - └── adapter-local conversion and invocation of the original Fortran procedure -``` - -Argument, result, native-call, and callable-operation plans follow the same -ownership split. The entrypoint is the shared C ABI handshake. The binding -uses it to declare and call the generated adapter; the Fortran bridge uses it -to declare the matching `bind(C)` procedure. Only the bridge plan describes -what happens after entry into that procedure. - -Goal 1 applies to every externally linked generated callable, not only ordinary -wrapped functions. The module entrypoint registry therefore also owns class -allocation, derived destruction and holder lifecycle helpers, derived-field -and module-member accessors, derived-origin transactions, native-array -descriptor and lifecycle operations, and callback trampolines. Binding-local -static Python helpers and bridge-internal procedures are not entrypoints. - -The entrypoint contract is bidirectional. It owns both arguments sent from the -binding and results returned through a C function return, output parameters, -presence flags, runtime lengths, or descriptor pointers. Binding plans own -conversion of that completed C storage into Python objects; bridge plans own -conversion of original Fortran results into the matching C ABI transport. - -### Canonical Developer Documentation - -Update the maintained developer documentation as part of Goal 1, before the -corresponding Python implementation. These pages describe implemented state, -so do not mark the separation complete until code and evidence match them. - -- [x] Update `docs/developer/packages/planning.md` with the - binding/entrypoint/bridge plan tree, bidirectional argument and result - transport, field ownership, validation boundary, and generator consumers. -- [x] Update `docs/developer/packages/codegen/c-binding.md` so the documented - input is `binding + entrypoint`, including binding-local input extraction, - entrypoint invocation, returned/output C storage, and Python result - construction. Its runnable plan example must use the new records while - preserving the rendered C output. -- [x] Update `docs/developer/packages/codegen/fortran-bridge.md` so the - documented input is `entrypoint + bridge`: entrypoint records define the - public `bind(C)` argument/result boundary, while bridge records define - adapter-local conversion and the original Fortran call. Its runnable plan - example must preserve the rendered Fortran output. -- [x] Update `docs/developer/packages/codegen.md` and the concise plan/codegen - wording in `docs/developer/architecture.md` so their stage diagrams and - boundaries include the shared entrypoint facet without claiming direct-call - support. -- [x] Update `CHANGELOG.md` under Unreleased for the maintainer-visible wrapper - plan representation. Do not change user guides or the language feature - matrix because Goal 1 adds no user-visible wrapper support. -- [x] Run `tests/docs` after the executable documentation examples and links - have been updated. - -### Plan Separation - -- [x] Add always-present native-entrypoint function, argument, result, and - ordered-parameter records to the shared wrapper plan. -- [x] Keep `WrapperPlanner` as the single projection stage and make it - construct binding, entrypoint, and bridge facets directly from completed - upstream facts. All three facets must be complete before - `WrapperGenerator` freezes the plan; neither generator may derive an - entrypoint from a bridge record or perform a post-planning split. -- [x] Move the C-visible adapter symbol, prototype, parameter order and types, - value/address projection, hidden-output transport, and direct-return ABI out - of bridge-only records and into the entrypoint records. -- [x] Keep original Fortran invocation, native barrier actions, adapter-local - representation conversion, copy reasons, declaration/import behavior, and - original native-call ordering in bridge records. -- [x] Keep the bridge facet mandatory for every current operation during this - goal. Do not add a direct action, optional bridge module, C wrapper route, or - zero-adapter artifact behavior yet. -- [x] Validate that entrypoint roles are produced by binding-local storage and - consumed by the matching bridge declaration, while bridge-only roles are not - exposed as binding inputs. -- [x] Remove the old conflated fields rather than retaining aliases or - compatibility properties. - -### Generator Consumption Boundaries - -- [x] Make C binding generation consume only binding and entrypoint facets for - prototypes, argument extraction, call setup, the native call, writeback, and - Python result construction. It must not read bridge-native actions, - adapter-local copies, or original Fortran invocation facts. -- [x] Replace generic binding names such as `_bridge_call` only where they now - represent the shared entrypoint call. Feature-specific helpers that still - select a real bridge operation may retain bridge terminology. -- [x] Make Fortran bridge generation consume the entrypoint facet for its - public `bind(C)` declaration and the bridge facet for adapter-local - conversion and the original Fortran call. -- [x] Keep wrapper orchestration and generated artifact assembly unchanged: - every current wrapper still contains its existing Fortran bridge, C binding, - and header. - -### Auxiliary Callable Coverage - -These items reopen Goal 1 after the ordinary-function separation exposed -remaining implicit ABI agreements. A helper is not separated merely because -the C generator avoids a `.bridge` attribute: its symbol, existence, ordered -parameters, and result transport must be recorded once by planning. - -- [x] Add planner-owned auxiliary entrypoint operation and signature records to - the module entrypoint facet. Each record must identify its owning operation, - exported symbol, ordered parameters, result ABI, and any rank, descriptor, - callback, or scalar-type facts needed by both lowerers. -- [x] Plan class allocation, derived destruction, allocatable/pointer holder - presence and destruction, direct/holder derived-field accessors, and - module-derived member accessors as individual entrypoint operations. -- [x] Plan derived-origin `present`, `address`, `scoped`, `checkout`, and - `restore` operations individually. Operation availability must be fixed by - planning instead of reconstructed from storage kind in either generator. -- [x] Plan native-array auxiliary operations for function results, default - arguments, module variables, derived fields, and module-derived members, - including descriptor callbacks and rank-dependent extent parameters. -- [x] Split callback handoff facts so the binding-local context/trampoline - implementation, shared trampoline entrypoint signature, and bridge-local - adapter/original callback ABI are explicit. Static abort helpers remain - binding-local. -- [x] Make the C binding obtain every externally linked auxiliary symbol and C - prototype from the planned operation registry. It may still construct - binding-local static helper names and temporaries. -- [x] Make the Fortran generator obtain every auxiliary `bind(C)` symbol and - public parameter/result contract from the same planned operation registry. - It may still create adapter-local declarations, conversions, and internal - procedures after the entrypoint boundary. -- [x] Validate one-to-one coverage: no duplicate operation keys or symbols, no - missing operation required by a binding/bridge plan, no unconsumed auxiliary - entrypoint, and no generator-local fallback that reconstructs a symbol or - ABI when its plan record is absent. -- [x] Add focused tests covering scalar/string/array/derived accessors, origin - transactions, lifecycle helpers, native-array operations, constructors, and - callbacks. Editing an auxiliary entrypoint must affect both boundary - lowerings, while editing bridge-local implementation facts must not affect - the C declaration or call. - -### Behavior-Preservation Evidence - -- [x] Add focused planner and generator tests proving that changing a - bridge-only native-invocation fact cannot change the C binding, while an - entrypoint change is visible to both sides of the shared C ABI boundary. -- [x] Preserve the existing rendered C binding, Fortran bridge, header, - generated semantic contracts, compiler inputs, and imported runtime - behavior. Existing generated fixtures must not be refreshed to accept - differences from this refactor. -- [x] Run the affected infrastructure, codegen, compilation, and end-to-end - feature tests across the current Fortran surface. Leave LAPACK runtime - coverage to GitHub Actions unless it is explicitly requested. -- [x] Run the required static-analysis suite because Python planning and - generator code changes in this goal. - -Goal 1 established the target in which the binding reads -`binding + entrypoint`, the Fortran generator reads `entrypoint + bridge`, and -every C-visible operation has one planner-owned entrypoint contract. A -follow-up consumer audit found remaining cross-facet reads and backend-specific -auxiliary signature fields. Goal 2 Stage 0 owns that closure before direct -routing begins; existing generated artifacts and runtime behavior remain the -baseline. - -## Goal 2 — Selective Direct Fortran Routing - -Start this goal by closing the remaining Goal 1 consumer-boundary leaks in -Stage 0. Do not enable selective direct routing until that stage is complete. -Complete the stages in order. Each stage must expose a completed record to the -next stage; a later stage must not rediscover the decision. - -Goal 2 accepts only Fortran native inputs. It may change the generated C -binding because that binding must call direct Fortran `bind(C)` entrypoints, -but it does not add C source parsing, C semantic-contract input, or native C -wrapping. Goal 3 owns those capabilities. - -### Current Goal 2 Status (2026-08-15) - -Goal 2 is **complete by checklist items**: **96 of 96 items are complete**. -Stages 0–8, all fourteen feature rows, source/generated/source-free contract -parity, zero-adapter and mixed builds, broad verification, and the maintained -direct-entrypoint benchmark evidence are complete. - -The maintained ARM64 runner used Python 3.12 and NumPy/f2py 2.5.1. Its pinned -preflight found no generated Fortran procedure wrapper in either direct route. -The f2py C/API object referred to all three user labels and its native object -defined them despite Meson's `.c.o` and `.f90.o` filenames; the linked extension -also defined all three. The corresponding PRIK binding object, native object, -and linked extension proved the same relationships. The paired runtime, -adapter-control, and clean-build results are published as separate generated -sections of the Performance page without changing the normal-interface -geometric-mean population. - -### Goal 2 Testing Layers - -Keep architectural ownership evidence separate from feature behavior: - -- **Infrastructure tests** may construct, freeze, or deliberately edit - completed semantic policies and wrapper-plan facets. They prove stage - handoffs, facet ownership, cross-facet isolation, validation, selected - symbols and signatures, passing conventions, adapter membership, and - generated-artifact assembly. Place them with the focused owner under - `tests/fortran/infrastructure/`, primarily its `semantics/`, `codegen/`, and - `pipeline/` directories. They must not stand in for a user-input or compiled - feature test. -- **Feature tests** must start from a real Fortran source fixture or an - authoritative semantic `.pyi` fixture and pass through the canonical - parsing/contract, semantic, policy, planning, generation, compilation, and - import routes applicable to the assertion. Policy and codegen tests may stop - at their owning stage, while end-to-end tests compile, import, call the - Python API, and inspect only the relevant generated-artifact membership or - ABI invariant. Direct and mixed source fixtures, plus generated and edited - `.pyi` replay where supported, provide the adoption evidence. -- **Exact-output regression evidence** belongs centrally in infrastructure, - not as a snapshot duplicated by every feature. Before Stage 0 changes code, - record one representative ordinary non-`bind(C)` source/semantic-contract - baseline and protect the exact generated C binding, C header, and Fortran - adapter bytes. Stage 0 must not refresh that baseline. Keep it passing in - later stages for ordinary operations whose completed projection and passing - plan did not change. -- Do not require old generated bytes for a non-`bind(C)` operation whose - route-neutral `@native_call` materialization intentionally moves from the - Fortran adapter to the C binding in Stages 2-4. For that case, focused - infrastructure assertions must prove the new owner and generated ABI - structure, while source/`.pyi` feature tests preserve compiled Python - behavior. Any golden update must identify this planned mechanism change; it - cannot be used to conceal unrelated formatting or output churn. - -### Stage 0 — Strict Consumer Boundaries And Entrypoint Vocabulary - -- [x] Audit every C-binding read and make C lowering consume only binding plus - native-entrypoint facets. Audit every Fortran-adapter read and make Fortran - lowering consume only native-entrypoint plus bridge facets. Neutral parent - records may retain owner/type identity needed to locate those facets, but - must not carry backend behavioral choices that let one lowerer bypass the - boundary. -- [x] Remove every current Fortran-lowering dependency on binding facts. In - particular, replace the module-getter, raw-address selection, raw-array call - selection, and argument-role reads of `.binding` with the corresponding - completed bridge or entrypoint facts. Remove `PythonBarrierAction` from the - Fortran generator once no adapter mechanism consumes Python-boundary policy. -- [x] Confirm that C lowering contains no bridge-facet read. Names of real - adapter symbols may still use adapter/bridge terminology, but the C generator - must obtain their existence, symbol, signature, and call transport from the - shared entrypoint plan rather than a bridge record. -- [x] Audit every ordinary and generated-support entrypoint field. An - entrypoint record may contain only the symbol, ordered C ABI, parameter and - result roles, and matching C/Fortran declaration facts that describe the - same shared boundary, plus the single implementation-owner flag needed to - decide which side defines the operation. Move binding-only extraction, - temporaries, Python actions, and local C expressions into binding plans; move - adapter-body locals, conversion, and original invocation into bridge plans. -- [x] Keep `c_name` and `fortran_name` together in the shared entrypoint when - they name the corresponding formal parameter in the C declaration and - Fortran `bind(C)` declaration of that same operation. They need not be - textually equal. Likewise, `const`, `intent`, or a neutral direction may stay - in the entrypoint when they describe the matching declarations of that - boundary. Move a name or attribute out only when it instead describes a - binding local, an adapter-body local, or the original Fortran procedure after - the entrypoint boundary. -- [x] Validate that paired C and Fortran entrypoint spellings describe one - interoperable parameter/result contract. Do not require neutral vocabulary - merely to avoid language-specific names, and do not use a paired spelling as - a container for unrelated backend behavior. -- [x] Audit facts duplicated between binding and entrypoint or between bridge - and entrypoint, including handoff and length roles. Store a true C ABI fact - once in the entrypoint. Keep two records only when they describe genuinely - different boundaries, and name the distinction explicitly rather than - validating accidental equality. -- [x] Project backend-local derived capsule and holder inventories explicitly - alongside the generated support procedure registry. Make C lowering consume - binding inventories for static CPython helper membership, make Fortran - lowering consume bridge inventories for typed-holder definitions and field - bodies, and make both consume only registry records for external procedure - existence, symbols, and ABIs. Remove result, argument, module-variable, - constructor, release, storage, and call-case walks that rediscover module - inventories in either lowerer, including namespace-level holder-method - copies. -- [x] Rename `NativeEntrypointOperationPlan` to - `GeneratedSupportProcedureEntrypointPlan` before adding routing actions, and - use **generated support procedure entrypoint** instead of **auxiliary - operation** in the maintained planning and code-generation documentation. - “Procedure” covers Fortran functions, Fortran subroutines, and C functions, - including C functions returning `void`. This record represents a - wrapper-internal procedure that is nevertheless an externally linked C ABI - symbol; do not call it `InternalFunctionPlan`, which could incorrectly imply - a non-linkable helper or a Fortran internal procedure. -- [x] Update planner construction, model exports, validation, and both lowerers - atomically without a compatibility alias. Preserve the single shared ABI - contract. Retain exactly one clearly named implementation-owner field whose - only job is to select which generated side defines the support procedure and - which side declares or calls it. Make no generated-source or runtime change - as part of Stage 0. -- [x] Keep `WrapperGenerator` free to validate relationships across the frozen - complete plan before lowering, but do not let that orchestration validation - become a fallback that copies or repairs missing backend/entrypoint facts. - Backend generators themselves must respect the strict facet boundary. -- [x] Add or update only focused infrastructure tests for Stage 0. Prove that a - binding-only edit cannot change Fortran output, a bridge-only edit cannot - change C output, and a shared entrypoint edit changes both sides of the same - ABI. Cover ordinary functions and generated support procedures, including - both implementation owners, and add a focused guard against future direct - cross-facet reads. -- [x] Capture the canonical ordinary non-`bind(C)` exact-output baseline before - implementation and prove that Stage 0 preserves every byte of its generated - C binding, C header, and Fortran adapter. Do not regenerate the expected - files to accept a Stage 0 difference. -- [x] Preserve all rendered C, Fortran, header, build, and runtime behavior in - Stage 0. Existing feature-local behavioral, ABI, compilation, and end-to-end - invariants must pass normally. Feature tests may change only to remove - obsolete assertions about duplicated internal plan fields; infrastructure - tests own the new architectural boundary assertions. - -### Stage 1 — Semantic Contract And Source Facts - -- [x] Add and document `@native_abi("c")` for Fortran semantic `.pyi` - procedures, including composition with `@bind("symbol")`, `@standalone`, - methods, overload candidates, and callable prototypes where applicable. -- [x] Preserve the ABI marker and renamed native label through Fortran source - conversion, `.pyi` parsing, generated-stub printing, and source-free `.pyi` - loading. -- [x] Preserve Fortran language and source-origin facts so - `@native_abi("c")` is interpreted as the ABI of a Fortran procedure rather - than as evidence of a C native input. -- [x] Keep `@native_call(...)` as a language- and route-neutral semantic - mapping from the Python-visible signature to the original native procedure - signature. Preserve its ordered arguments, hidden results, typed literals, - `Addr`/`Value` projections, lengths, presence values, shapes, strides, and - work values without assuming that a Fortran adapter will execute them. -- [x] Reject contradictory or misplaced ABI annotations with a semantic - diagnostic instead of ignoring them. - -### Stage 2 — Completed Entrypoint Policy - -- [x] Add an explicit per-operation `NativeEntrypointAction` with direct C ABI - and generated Fortran-adapter actions. Do not add a generated C-adapter - action until that emitted mechanism is implemented. -- [x] Complete the entrypoint action before `WrapperPlanner` starts. A missing, - blocked, or internally inconsistent action must stop at the policy boundary. -- [x] Define one central eligibility policy that considers all ABI, transfer, - ownership, result, and lifecycle facts. Do not duplicate eligibility tests in - the planner or either generator. -- [x] Complete one entrypoint passing convention for every parameter and result - transport before planning: C value, pointer/reference, nullable pointer, - C descriptor pointer, runtime handle, C function return, or output storage. - Policy owns this decision; neither lowerer may infer it from Fortran `VALUE`, - datatype, `intent`, pointer syntax, descriptor shape, or the selected route. -- [x] Separate route-neutral `@native_call` projection facts from - adapter-specific data actions. Complete one binding-owned projection action - for every mapping item—including argument selection, ordering, address/value - choice, hidden output storage, typed literals, computed scalar facts, and - supported work storage—before selecting a route. The binding action produces - a C-side entrypoint actual for both direct and adapted operations. -- [x] Restrict adapter-specific actions to representation or invocation work - that cannot be performed at the shared C boundary, such as reconstructing - Fortran character or array views, converting ordinary logical storage, - handling allocatable/pointer semantics, omitting absent optional dummies on - noninteroperable original calls, or invoking module, type-bound, generic, or - defined operations. Select the Fortran adapter when such work is required. -- [x] Complete an explicit entrypoint optionality action independently of the - Python default/nullable surface. At minimum distinguish required values, - absence represented by a null ordinary pointer, absence represented by a - null C descriptor pointer, an explicit native presence value already present - in the declared C signature, adapter-side Fortran omission, and blocked. -- [x] Direct-route a standard-interoperable non-`VALUE` optional `bind(C)` dummy - by making the binding pass a non-null pointer when present and `NULL` when - absent; the original Fortran procedure then observes `present(dummy)` - directly, without an adapter branch. Do not infer native optionality merely - because a C parameter is a nullable pointer. -- [x] Preserve descriptor optionality as three distinct states when that - feature is adopted: a null descriptor pointer means the optional dummy is - absent, a non-null descriptor with no allocation/association means the dummy - is present with empty descriptor state, and a non-null populated descriptor - means present with a value. -- [x] Do not direct-route an optional Fortran `VALUE` dummy through a - compiler-specific hidden presence argument. Keep it adapter-backed, or block - it when no adapter is available, unless a later standard and compiler-probed - portable C ABI mechanism is explicitly adopted. -- [x] Treat a Fortran procedure without the C ABI fact as adapter-backed even - when its scalar signature resembles C. -- [x] Keep scalar Boolean policy explicit: directly routed Fortran - `logical(c_bool)` uses the `Bool` contract, accepts Python `bool` and - `numpy.bool_`, and returns Python `bool`. Measured ordinary Fortran logical - storage continues through its existing adapter conversion. - -### Stage 3 — Shared Wrapper Planning - -- [x] Make the bridge facet separated in Goal 1 optional while keeping the - native-entrypoint plan always present. Completed policy alone decides whether - that optional facet exists. -- [x] Replace the mandatory module bridge plan with zero or more native - generated-code groups. Keep adapted user-operation membership distinct from - generated-support-procedure membership even if the initial implementation - emits both groups in one Fortran source. Goal 2 creates only - Fortran-generated groups; Goal 3 owns native C grouping. -- [x] Give the binding one planned call symbol and ABI signature regardless of - whether that symbol belongs to the user library or a generated adapter. -- [x] Plan one authoritative ordered call-projection sequence from - `@native_call` for every route. Each slot must own its binding-side source and - materialization action, its completed value/reference/descriptor/handle - passing convention, and its entrypoint ABI actual; an adapted slot may - additionally own a bridge facet describing only the Fortran-local conversion - and original-call expression. -- [x] Derive entrypoint parameter order and actual projection directly from - that shared sequence, never from `BridgeCallSlotPlan`. Remove the current - assumption that entrypoint groups can be ordered from original-Fortran bridge - slots, because a direct operation has no bridge slot. -- [x] For both direct and adapted actions, make the binding realize reordered - arguments, typed literals, address/value projection, hidden outputs, lengths, - presence values, shapes, strides, and supported work storage. An adapted - entrypoint receives those completed C-side actuals instead of recreating - their `@native_call` sources inside the Fortran bridge. -- [x] Store the completed optionality action and its exact pointer, descriptor, - or declared presence actual in the entrypoint slot. A direct plan must not - retain a bridge optional-dispatch requirement; an adapted plan may attach an - omission branch only when the original Fortran invocation requires it. -- [x] Retain `BridgeCallSlotPlan` only as an optional adapter facet attached to - a shared projected slot, or replace it with an equivalently narrow adapter - record. It may select a converted Fortran expression or optional invocation - branch, but it must not own a second ordering, source mapping, hidden literal, - or hidden-storage decision. Direct operations have no such facet. -- [x] Store the original Fortran invocation kind only in the optional adapter - facet: subroutine `call` or function-result assignment, including the planned - assignment target. Do not infer it from the C entrypoint return transport; a - Fortran function may use a `void` C entrypoint with output storage, and a C - return may instead carry status. The binding does not consume this fact, and - a direct operation has no original-call facet. -- [x] Validate that direct operations have no adapter plan, adapted operations - have exactly one matching adapter plan, and every binding call target is - linkable through the extension build plan. -- [x] Derive module build requirements from both independent sets: - `any(operation requires adapter)` and - `any(support entrypoint has a Fortran implementation owner)`. Never store a - second module-wide policy choice or treat a generated support procedure as an - adapter for a direct user operation. - -### Stage 4 — Binding And Adapter Lowering - -- [x] Reuse the separated Goal 1 binding/entrypoint boundary, but extend its - planned actual kinds and mechanical lowering for the route-neutral - `@native_call` projections that are currently realized only after entering - the Fortran adapter. Do not create separate direct and adapted binding - pipelines; both consume only binding and entrypoint facets without - re-evaluating the mapping or signature. -- [x] Make binding lowering execute the planned entrypoint actual sequence for - both direct and adapted operations, without parsing semantic decorators or - consulting bridge slots. The binding may materialize only the local C - temporaries selected by completed policy. -- [x] Make the binding lowerer the sole owner that realizes each planned C - passing convention at the call site: emit a value expression, address, - nullable pointer, descriptor pointer, handle, function-return assignment, or - output-storage address exactly as recorded by the entrypoint plan. This rule - applies equally when the target symbol is a generated Fortran adapter or the - user's direct C ABI symbol. -- [x] Make binding lowering realize direct optional absence mechanically as the - planned `NULL`, descriptor pointer, or declared presence actual. It must not - generate a Fortran-style omission decision or treat every nullable C pointer - as a native optional argument. -- [x] Generate a Fortran adapter procedure only for operations whose completed - action selected it. -- [x] Make Fortran lowering consume only the optional adapter facet of each - shared projected slot. It may convert an already supplied C-side actual and - form the original Fortran invocation, but it must not reimplement - `@native_call` ordering, source selection, literals, or hidden-storage - materialization, or choose whether the binding-to-entrypoint call passes a - value or reference. The Fortran compiler still applies the original dummy's - calling convention when the adapter invokes the original procedure, but the - adapter only follows its completed conversion and invocation facet. Direct - Fortran entrypoints have no adapter facets. -- [x] Emit no generated Fortran source when both the selected adapter-operation - set and the Fortran-owned support-procedure set are empty. When only the - support set is nonempty, emit support-only source and no wrapper for a direct - user operation. -- [x] Reuse existing binding-local extraction, conversion, validation, - temporary-storage, writeback, cleanup, and Python-result paths for direct - calls whenever their completed plans are identical. -- [x] Keep generic reusable CPython/NumPy conversion helpers in native support; - keep operation-specific direct-call glue in the generated binding. Do not add - a C adapter generator or native C input lowering in Goal 2. - -#### Stages 2-4 Architectural Acceptance - -- [x] Complete the `@native_call` and value/reference ownership relocation - before enabling selective direct routing. Treat this relocation as an - architectural change with unchanged feature behavior: retain and pass every - feature-local policy, ABI, compilation, and end-to-end invariant, while - removing only obsolete assertions about the former implementation owner. -- [x] Add or update only focused infrastructure tests for this ownership - relocation, primarily under `tests/fortran/infrastructure/codegen/`. Prove - that the shared entrypoint plan owns the ordered projections and completed - passing conventions, that the C binding realizes their call-site actuals for - an adapted target, and that Fortran lowering consumes only the remaining - conversion/invocation facets. -- [x] Do not rewrite a feature behavior or ABI expectation merely to - accommodate the relocation. If an existing feature test fails, identify and - preserve the maintained invariant that it protects; remove or replace only - an obsolete implementation-shape assertion. Later direct-route stages add - their own feature evidence because they add observable support and artifact - shapes. Goal 3 separately owns C adoption evidence. - -### Stage 5 — Pipeline, Compilation, And Linking - -- [x] Allow `GeneratedWrapper` to contain zero generated native sources while - retaining one or more C binding sources and the generated header. Represent - adapter and generated-support membership separately even if they share a - physical Fortran source initially. -- [x] Materialize and compile only the native generated-code groups present in - the result. Progress output, generated-file records, Makefiles, and saved - build manifests must represent zero-generated-source, selective-adapter, and - support-only builds factually. -- [x] Select the final link driver from all native and generated object - languages and their runtime requirements, not from the presence of a - Fortran adapter. An all-direct Fortran module can still require the Fortran - linker and runtime. -- [x] Preserve native object and library ordering for source-driven and - semantic-`.pyi` builds in all-direct and mixed routes. - -### Stage 6 — Fortran Scalar Adoption Baseline - -- [x] Add a Fortran all-direct fixture containing safely interoperable - `bind(C)` scalar functions and subroutines, including a renamed native label. - Its end-to-end build must emit, compile, import, and call successfully with - no generated Fortran adapter source or object. -- [x] Add a mixed Fortran fixture containing direct `bind(C)` and ordinary - procedures. Its end-to-end build must prove equivalent Python behavior and - that the generated adapter contains only the ordinary procedures. -- [x] Add source, generated-`.pyi`, and source-free edited-`.pyi` parity for - the ABI marker, renamed symbol, selected entrypoint, public NumPy scalar - results, and Boolean exception. -- [x] Add direct and adapted Fortran projection fixtures covering reordered - scalar arguments, `Addr` and `Value`, a hidden scalar result, and a typed - hidden literal. Prove from generated artifacts and compiled runtime behavior - that the binding executes the planned `@native_call` sequence without a - generated adapter for the direct case, and passes the same binding-owned - sequence through the adapter without reconstructing it for the adapted case. -- [x] Add a direct `bind(C)` non-`VALUE` optional scalar fixture proving omitted, - explicit `None`, and present values produce the expected `present(...)` - states with no adapter. Distinguish a nullable pointer in the direct C ABI - signature from Fortran optionality, and prove that an optional Fortran - `VALUE` dummy selects an adapter or a pre-generation blocker rather than a - compiler-specific direct ABI. - -### Stage 7 — Feature-Local Direct And Mixed Adoption - -Adopt direct routing one feature at a time after the scalar baseline. Every -callable feature row that is claimed as direct must own both fixture shapes -below under its existing `tests/fortran//end_to_end/fixtures/` -directory. Parser, semantic-IR, CLI, and infrastructure directories do not need -native fixtures merely because they exist under `tests/fortran/`. - -- [x] Add `_direct_bind_c_f90.f90`, containing only user procedures - whose completed contracts select direct C ABI entrypoints. Cover both a - function and subroutine when the feature supports both. Prove that no direct - user procedure appears in adapter membership. When the fixture has no - Fortran-owned support procedures, prove that no generated Fortran source or - object exists. -- [x] Add `_mixed_bind_c_f90.f90`, containing at least one directly - callable `bind(C)` procedure and at least one ordinary or otherwise - adapter-required procedure. Prove per-operation selection, equivalent Python - behavior, and that generated adapter membership contains only the latter. -- [x] For features such as derived types, module state, ownership handles, and - callbacks, allow the direct fixture to generate the accessors, lifecycle - helpers, descriptor operations, or trampolines selected independently by - their support-entrypoint plans. Prove that a resulting Fortran artifact is - support-only with respect to direct user procedures; do not call the entire - module adapter-backed merely because support code exists. -- [x] Reuse the owning feature's existing behavioral assertions and semantic - `.pyi` replay route. Add the direct and mixed cases without replacing or - weakening ordinary-procedure coverage, and keep source, generated-`.pyi`, and - source-free edited-`.pyi` decisions equivalent where that feature supports - those inputs. -- [x] Add the fixture pair only when completed policy supports the feature's - direct ABI mechanism. Until then, keep the feature-matrix cell unchecked and - retain a focused blocker test instead of adding a nominal `bind(C)` fixture - that still relies on an unacknowledged adapter. - -### Stage 8 — Direct-Entrypoint Performance Evidence - -Add performance cases only after their correctness, route selection, generated -artifacts, and compiled runtime behavior pass outside the timer. - -- [x] Add same-source `bind(C)` no-op, scalar-function, and scalar-subroutine - workloads that isolate binding-to-native call overhead. The PRIK build must - prove that none of those user procedures has a generated adapter wrapper. -- [x] Measure the equivalent ordinary-Fortran PRIK operations separately so the - cost difference between PRIK's adapted and direct routes is visible without - attributing native-kernel work to either route. -- [x] Build the f2py direct-call comparison with its documented - [`--no-wrap-functions`](https://numpy.org/doc/stable/f2py/usage.html) mode for - Fortran functions and - `--skip-empty-wrappers` where applicable. Keep f2py's Python C/API binding; - these flags concern generated Fortran wrapper procedures/files rather than - removal of the Python binding. -- [x] Inspect the generated binding object, native object, linked extension, - and generated-source membership with the pinned NumPy version before - describing the maintained result. Prove that the binding refers directly to - the three user labels and that both the native object and linked extension - define them. -- [x] Keep the benchmark procedures' Fortran names and `bind(C)` labels equal so - both tools consume the same source without a benchmark-only symbol rewrite. - Test renamed native labels separately in the correctness suite, and use a - standalone or module source shape only after artifact inspection proves the - intended f2py native-call path. -- [x] Keep the existing default-interface PRIK/f2py results intact. Publish the - direct-entrypoint cohort separately unless the benchmark methodology, - paired-suite validation, labels, and geometric-mean population are - deliberately revised and documented. -- [x] Use identical native operations, Python-visible inputs, numerical result - values, optimization flags, GIL policy, process-order balancing, CPU - affinity, and correctness checks for each cross-tool pair. Preserve and - record each tool's natural result class instead of hiding PRIK's exact NumPy - scalar and f2py's built-in scalar behind a normalization shim. Record route - and wrapper-mode metadata so default, adapted, and direct results cannot be - merged silently. -- [x] Add both runtime-call and clean small-build cases. The build case must - report generated/compiled source membership so a missing PRIK adapter or an - empty f2py wrapper file is an evidenced artifact fact, not an inference from - elapsed time. -- [x] Update `benchmarks/README.md`, benchmark workflows, and tooling tests under - `tests/tools/` for the separate direct-entrypoint cohort without changing the - generated Performance page or its published snapshot. -- [x] After a complete paired run on the maintained benchmark runner, update - the generated Performance-page methodology and published snapshot with the - direct-entrypoint cohort. - -## Goal 2 Fortran Feature Adoption Matrix - -After the scalar baseline, adopt features by native ABI mechanism rather than -by copying the entire existing Fortran suite. A feature row is complete only -when it has policy, plan/lowering, generated-artifact, compiled runtime, and -semantic-`.pyi` parity evidence through the Stage 7 direct and mixed fixture -pair. Use the central scalar fixtures for cross-feature module and pipeline -invariants rather than duplicating those assertions in every feature. - -| Feature boundary | Fortran direct and mixed evidence | Special acceptance concerns | -| --- | --- | --- | -| Numeric and Boolean scalars | [x] | Exact NumPy numeric results; Python Boolean results; `logical(c_bool)` direct storage versus ordinary Fortran logical adapter conversion. | -| Reference, input/output, and projected results | [x] | Address projection, mutation, writeback ordering, tuple results, and direct function returns. | -| Numeric and Boolean arrays | [x] | Dtype, rank, shape, order, alignment, mutability, copy/writeback, zero extents, and explicit Boolean-storage compatibility. | -| Strings and character buffers | [x] | Length source, terminators, encoding, embedded NUL, mutation, ownership, and returned-buffer lifetime. | -| Enumerations and constants | [x] | Underlying integer ABI, exported constants, and no invented Python enum layout. | -| Optional and nullable values | [x] | Fortran presence representation, null pointers, omitted Python arguments, and output projection. | -| Raw addresses and native pointers | [x] | Pointee type, nullability, ownership, target lifetime, and reassociation or writeback. | -| Structs, derived types, fields, and methods | [x] | By-value versus pointer ABI, opaque/accessor routes, construction, destruction, borrowing, and layout proof. `bind(C)` alone never authorizes direct aggregate layout. | -| Module variables and native global state | [x] | Direct exported storage versus generated accessor operations, mutability, saved state, and ownership. | -| Generics, overloads, and defined operations | [x] | Each candidate owns its entrypoint action; dispatch owns no shared adapter route. | -| Immediate callbacks | [x] | Function-pointer ABI, callback argument/result conversion, GIL entry, exception handling, and call-scoped lifetime. | -| Allocatable, pointer, and descriptor-backed storage | [x] | Descriptor ABI, allocation ownership, release responsibility, optional presence, nullable state, and runtime/compiler dependencies. | -| Error/status projection and GIL release | [x] | Call target remains independent of status checking, cleanup order, and GIL policy. | -| Standalone, multi-source, and external-library builds | [x] | Native symbol scope, object/library order, module dependencies, and final link-driver selection. | - -## Goal 2 Required Evidence Owners - -- Entrypoint completion and blockers: `tests/fortran//policy/`. -- Canonical byte-for-byte ordinary non-`bind(C)` generated-output regression: - one focused owner under `tests/fortran/infrastructure/codegen/`, covering the - generated C binding, C header, and Fortran adapter without duplicating the - snapshot across feature directories. -- Stages 2-4 projection-ownership relocation: focused - `tests/fortran/infrastructure/codegen/` tests. Existing feature-local tests - remain unchanged regression evidence and must pass. -- Selective adapter membership, direct binding call targets, and generated - artifact sets introduced by later adoption stages: the owning - `tests/fortran//codegen/` and infrastructure owners for - cross-feature artifact invariants. -- Direct and mixed compiled behavior for each adopted feature: its Stage 7 - fixtures and owning `tests/fortran//end_to_end/` tests. A direct - fixture with generated support operations proves support-only membership, - while a fixture with neither adapters nor support proves complete generated - Fortran source/object absence. -- Zero-adapter materialization, compile scheduling, link-driver selection, - Makefiles, manifests, and progress records: - `tests/fortran/building_shared_library/pipeline/` and - `tests/fortran/building_shared_library/compiling/`. -- Compiled Fortran feature behavior: the owning - `tests/fortran//end_to_end/` directory. The scalar adoption starts by - replacing the current assumption that every procedure in - `tests/fortran/data_types/end_to_end/test_value_and_bind_c.py` appears in the - generated adapter. -- Direct-entrypoint runtime and clean-build performance: benchmark correctness - and artifact preflight outside timing, paired `pyperf` results, and benchmark - tooling tests under `tests/tools/`. These supplement rather than replace - feature-local correctness evidence. -- Generated and edited semantic-contract parity: - `tests/fortran/semantic_pyi_format/` plus feature-local end-to-end fixtures. - -Artifact assertions protect observable generated and build behavior: whether -an adapter source/object exists, which native operations it exports, which -symbol the binding calls, and which link driver is selected. Tests should not -freeze private class names, complete plan field inventories, or incidental -source formatting. - -## Definition Of Goal 2 Fortran Readiness - -Selective direct Fortran routing is ready to claim only when: - -- [x] Stage 0 proves that binding lowering cannot read bridge facets and - Fortran lowering cannot read binding facets for ordinary or generated - support procedures; -- [x] Fortran source, generated `.pyi`, and source-free `.pyi` inputs preserve - the `bind(C)` ABI fact, native symbol, and selected per-operation route; -- [x] all-direct and mixed Fortran routes pass through the shared plan and - pipeline changes without changing ordinary-procedure behavior; -- [x] zero-adapter generated artifacts, compilation, linking, manifests, - Makefiles, verbose output, and imports have focused evidence; and -- [x] each checked Goal 2 feature row has policy, codegen, artifact, - compilation, runtime, and semantic-contract parity evidence. - -Goal 2 completion does not claim that PRIK accepts native C inputs. - -## Goal 3 — Initial Direct-Only C Adoption - -Start Goal 3 only after Goal 2 is complete. Goal 3 adds C as a native input -language by reusing the completed binding-to-entrypoint path. It does not add a -generated native C adapter: an operation is either directly supported or -blocked by completed policy before planning and source generation. - -### Stage 0 — C Language And Contract Inputs - -- [ ] Add C source conversion and authoritative source-free C semantic - contracts while preserving `source_language = "c"` on semantic modules, - native inputs, and build records. -- [ ] Treat a C procedure as C ABI by language identity. Do not require or - synthesize `@native_abi("c")`; that decorator remains the source-free - Fortran spelling for an original `bind(C)` procedure. -- [ ] Preserve C symbols, `void` versus value returns, typedef-resolved scalar - types, pointer depth, qualifiers, structs, and function-pointer facts needed - by completed policy. Do not infer ownership, nullability, or aggregate layout - merely from pointer or typedef syntax. -- [ ] Add language-owned parsing, semantic-contract, and diagnostic tests - under `tests/c/` without importing Fortran-specific fixture helpers. - -#### Conservative C Starter-Contract Defaults - -C source conversion must preserve only what the declaration proves. The -generated starter contract is deliberately low-level; it must not guess -whether a pointer denotes one scalar, an array, an output, owned storage, or a -retained address. - -| C declaration | Default generated semantic `.pyi` | Preserved meaning | -| --- | --- | --- | -| `T value` | `value: T` | Primitive scalar passed by value. | -| `T *value` | `value: Addr(T)` | Unrefined mutable one-level pointer with no invented rank or shape. | -| `const T *value` | `value: Addr(T)`, with `const` retained in origin and policy facts | Unrefined read-only one-level pointer; `const` does not make it a scalar or array. | -| `T **value` | `value: Addr[2](T)` | Two native pointer levels; support may remain policy-blocked after serialization. | -| return `T` | `-> T` | Direct primitive scalar result. | -| return `T *` | `-> Addr(T)` | Raw pointer result with no invented ownership, lifetime, NumPy storage, or destruction policy. | - -An authoritative semantic `.pyi` supplies the missing API meaning. It may -refine `Addr(T)` to `T[()]` for caller-provided rank-zero scalar storage, -`T[n]` or `T[:]` for proved array storage, or retain `Addr(T)` intentionally -as a raw address. `Addr(Arg(i))` requests the address of call-local scalar -storage, while a matching `Returns["name", T]` requests mutation readback. -Direction uses the explicit `In`, `Out`, or `InOut` contract, and nullability -uses an explicit `| None`; neither is inferred from pointer syntax. - -The source default must not infer an array from an adjacent extent parameter, -infer output behavior from a parameter name, interpret non-`const` as -input/output, or interpret `char *` as a string. C parameter array syntax still -decays to a pointer at the ABI; retain its dimensions as source provenance and -emit a shaped public contract only when they establish a real validation -constraint. Raw pointer contracts do not imply ownership transfer, native -retention safety, or automatic cleanup. Serialization alone does not make an -operation eligible: completed policy must block any pointer contract whose -ownership, lifetime, nullability, transfer, or result behavior remains unsafe -or unsupported. - -### Stage 1 — Direct-Only C Policy - -- [ ] Reuse `NativeEntrypointAction.DIRECT_C_ABI` for supported C operations - and complete eligibility before `WrapperPlanner` starts. Do not introduce a - C-adapter action or fallback. -- [ ] Reuse the entrypoint passing conventions and route-neutral - `@native_call` projections completed in Goal 2. A C operation that needs an - unsupported conversion, ownership, lifetime, callback, aggregate, or result - mechanism must fail with a documented policy diagnostic. -- [ ] Keep C pointer nullability distinct from Fortran optional presence. A - nullable C pointer may receive `NULL`, but it does not imply a hidden - presence convention or omitted native argument. -- [ ] Define C `_Bool` through the same public `Bool` contract: accept Python - `bool` and `numpy.bool_`, return Python `bool`, and require an explicit safe - mechanism before treating NumPy Boolean array storage as C `_Bool` array - storage. -- [ ] Complete all transfer, ownership, destruction, mutation, writeback, - nullability, result projection, and release facts before planning, following - the same policy boundary as Fortran. - -### Stage 2 — Planning, Lowering, And Pipeline Reuse - -- [ ] Make supported C operations produce the same always-present entrypoint - facet and no bridge facet. The C binding consumes only binding plus - entrypoint and calls the user C symbol directly. -- [ ] Reuse Goal 2 binding-local extraction, validation, temporary storage, - passing-convention lowering, writeback, cleanup, and Python-result paths - whenever the completed plans are identical. Add a new lowering mechanism - only when a genuinely new planned C ABI action requires it. -- [ ] Generate no native C adapter source or object. Verify that an - adapter-required C operation fails before files are written or compiler - commands run. -- [ ] Compile and link C inputs through language-aware native build records. - Select the final link driver and runtime dependencies from all input and - generated object languages rather than from adapter presence. -- [ ] Cover source-driven and source-free semantic-contract builds, saved - generated artifacts, Makefiles, manifests, verbose output, and imports. - -### Stage 3 — C Scalar Baseline - -- [ ] Add C scalar fixtures and compiled end-to-end tests for every initially - supported integer, real, complex, and Boolean contract, including functions - returning values and functions returning `void` with input/output pointers. -- [ ] Cover renamed symbols and route-neutral projections, including reordered - arguments, `Addr`, `Value`, hidden result storage, and typed literals where - the C contract supports them. -- [ ] Prove from generated artifacts and build records that the binding calls - the user symbol and no native C adapter source or object exists. -- [ ] Add at least one parseable C operation whose unsupported ABI or transfer - mechanism produces the documented pre-planning diagnostic. - -### Stage 4 — C Feature-Local Adoption - -Adopt one C feature row at a time. A row remains unchecked when any required -operation needs an unavailable adapter mechanism; do not weaken the feature -contract or silently generate a fallback merely to mark it complete. - -| Feature boundary | Initial C direct-only evidence | Special acceptance concerns | -| --- | --- | --- | -| Numeric and Boolean scalars | [ ] | Exact NumPy numeric results; Python Boolean results; scalar C `_Bool` conversion. | -| Reference, input/output, and projected results | [ ] | Pointer direction, mutation, writeback ordering, tuple results, and direct function returns. | -| Numeric and Boolean arrays | [ ] | Dtype, rank, shape, order, alignment, mutability, copy/writeback, zero extents, and explicit C `_Bool` storage handling. | -| Strings and character buffers | [ ] | Length source, terminators, encoding, embedded NUL, mutation, ownership, and returned-buffer lifetime. | -| Enumerations and constants | [ ] | Underlying integer ABI, exported constants, and no invented Python enum layout. | -| Nullable values | [ ] | Null-pointer policy, omitted Python arguments, and output projection without invented native optionality. | -| Raw addresses and native pointers | [ ] | Pointee type, pointer depth, qualifiers, nullability, ownership, target lifetime, and reassociation or writeback. | -| Structs, fields, and methods | [ ] | By-value versus pointer ABI, opaque/accessor routes, construction, destruction, borrowing, and proven layout. | -| Native global state | [ ] | Direct exported storage versus generated accessors, mutability, lifetime, and ownership. | -| Overloads and generated dispatch | [ ] | Each selected C symbol owns an entrypoint action; dispatch owns no shared adapter route. | -| Immediate callbacks | [ ] | Function-pointer ABI, callback argument/result conversion, GIL entry, exception handling, and call-scoped lifetime. | -| Error/status projection and GIL release | [ ] | Call target remains independent of status checking, cleanup order, and GIL policy. | -| Multi-source and external-library builds | [ ] | Native symbol scope, object/library order, dependencies, runtime requirements, and final link-driver selection. | - -### Goal 3 Required Evidence Owners - -- Completed C policy and blockers: `tests/c//policy/`. -- Direct call targets, signatures, and generated artifact sets: - `tests/c//codegen/` plus focused cross-language infrastructure - owners where the pipeline invariant spans languages. -- Compiled behavior: `tests/c//end_to_end/`, using C-owned fixtures - and the same named public invariants as the corresponding Fortran feature. -- C parsing and semantic-contract parity: the language-owned parser and - semantic-format tests under `tests/c/`. -- Zero-adapter materialization, compilation, linker selection, Makefiles, - manifests, progress output, and imports: the relevant pipeline and compiling - owners extended with C-native inputs. - -## Definition Of Initial C Readiness - -Initial direct-only C wrapper support is ready to claim only when: - -- [ ] the scalar baseline passes through C source and authoritative source-free - C semantic contracts; -- [ ] supported C operations call their user symbols without a native adapter; -- [ ] unsupported adapter-required operations fail at completed policy with a - documented diagnostic and no partial generated artifacts; -- [ ] zero-adapter compilation, linking, manifests, Makefiles, verbose output, - and imports have focused evidence; -- [ ] Goal 2 Fortran direct and adapted routes remain green after shared-path - reuse; and -- [ ] the user-facing language feature matrix lists only C rows proved by - compiled runtime tests. diff --git a/docs/developer/roadmap/semantic-pyi-wrapper-checklist.md b/docs/developer/roadmap/semantic-pyi-wrapper-checklist.md index 9d3b99810..7dc1c66d1 100644 --- a/docs/developer/roadmap/semantic-pyi-wrapper-checklist.md +++ b/docs/developer/roadmap/semantic-pyi-wrapper-checklist.md @@ -132,12 +132,12 @@ Runtime wrapper tests are organized by stable subjects under `build_from_pyi/modified_contracts/basic_subroutine/flatten_m1.pyi`, `build_from_pyi/modified_contracts/basic_subroutine/alias_increment.pyi`, and - `tests/fortran/semantic_pyi_format/pipeline/fixtures/invalid/projection_metadata/incomplete_native_call.pyi`. + `tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/invalid/projection_metadata/incomplete_native_call.pyi`. - [x] Generated `.pyi` packages are checked fixtures. Runtime wrapper contract packages live under `tests/wrapper/fortran//contracts//`; explicit `--pyi --out` package-shape fixtures that do not compile wrappers live under - `tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/`. + `tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/`. Refresh is explicit through `WRAPPER_UPDATE_PYI_FIXTURES=1`. - [x] Modified runtime fixtures use `.pyi`, record their intentional difference @@ -146,7 +146,7 @@ Runtime wrapper tests are organized by stable subjects under - [x] `.py` files are rejected as semantic `.pyi` contract inputs by the Python API. - [x] The reviewed packages under - `tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/` + `tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/` are the canonical exact `.pyi` generation-regression corpus and are not used as edited runtime contracts. - [x] Explicit Fortran `--pyi --out` package-shape fixtures that do not compile @@ -311,8 +311,8 @@ PRIK_C_DOCS_END --> ### Stage 6 — Replayable JSON, Native Compilation, And Makefiles Runtime evidence lives in -`tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py`, -`tests/fortran/semantic_pyi_format/end_to_end/`, and CLI surface +`tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py`, +`tests/fortran/infrastructure/semantic_pyi/end_to_end/`, and CLI surface evidence lives in `tests/cli/`. - [x] Python API `.pyi` builds accept output directory, extension naming, @@ -353,7 +353,7 @@ evidence lives in `tests/cli/`. Real BLAS/LAPACK artifact-shape evidence lives in `examples/blas/` and `examples/lapack/`. Native bundle, order, transitive-library, and failure-path evidence lives in -`tests/fortran/building_shared_library/end_to_end/test_native_bundles.py`. +`tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py`. - [x] Full real BLAS and LAPACK source corpora under `examples/blas/native/` and `examples/lapack/native/` @@ -418,8 +418,8 @@ PRIK_C_DOCS_END --> `prik/policy/completion.py`; direct ownership subpasses stay behind that entrypoint. Planning and lowering consume completed policy metadata instead of recomputing policy from raw datatypes. Evidence: - `tests/fortran/infrastructure/semantics/test_policy_completion.py`, - `tests/fortran/infrastructure/semantics/test_ownership.py`, + `tests/fortran/infrastructure/policy/test_policy_completion.py`, + `tests/fortran/infrastructure/policy/test_ownership.py`, feature-local `tests/fortran/*/policy/`, `tests/fortran/infrastructure/codegen/`, and `prik/semantics/README.md`. @@ -427,7 +427,7 @@ PRIK_C_DOCS_END --> `prik/parsers/pyi/parser.py` parses text/files to Python AST, and `prik/semantics/pyi2ir.py` converts that AST into `SemanticModule` objects before semantic policy completion runs. Evidence: - `tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py::test_pyi_parser_returns_python_ast_only`, + `tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py::test_pyi_parser_returns_python_ast_only`, `prik/semantics/README.md`, and `docs/developer/architecture.md` and the detailed architecture component guides. @@ -451,13 +451,13 @@ PRIK_C_DOCS_END --> loader semantic errors prefix messages with the `.pyi` contract path while syntax errors keep Python's filename field. Evidence: `docs/user/reference/semantic-pyi-format.md` and - `tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_pyi_file_to_semantic_module_and_modules_forward_module_name_encoding_and_filename`. + `tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py::test_pyi_file_to_semantic_module_and_modules_forward_module_name_encoding_and_filename`. - [x] A modified module `.pyi` can remove a public function and hide public declarations with `@private` or `private[...]` while preserving unaffected runtime behavior. Evidence: - `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py` + `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py` and - `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_variables_visibility/`. + `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_variables_visibility/`. - [x] A dedicated user guide documents the supported editable contract surface, including what users may remove, hide, add, rename, project, validate, make immutable, and declare as ownership/lifetime policy. It separates editable @@ -470,13 +470,13 @@ PRIK_C_DOCS_END --> member, and individual overload candidate from the Python API. They can also add renamed `@bind(...)` declarations and a renamed module overload group without reparsing native source. Evidence: - `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py` + `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py` and - `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/`. + `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/`. - [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/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets` + `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets` 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 @@ -531,8 +531,8 @@ PRIK_C_DOCS_END --> `tests/fortran/error_handling/semantics/test_status_contract_semantics.py`, `tests/fortran/error_handling/codegen/test_status_error_lowering.py`, `tests/fortran/error_handling/end_to_end/test_status_projection.py`, - `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py`, - `tests/fortran/pyi_contracts/exports_and_modules/`, and + `tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py`, + `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/`, and `tests/wrapper/CHECKLIST_COVERAGE.md`. + ```fortran real(8) function scale(value, factor) result(output) real(8), intent(in) :: value @@ -68,6 +70,41 @@ print(result) # 7.5 No manual binding code is required. PRIK derives the native wrapper and a readable Python signature from the Fortran source. +## From C to Python in one command + +Create `native_math.c`: + +```c +double add(double left, double right) { + return left + right; +} +``` + +Build an importable extension: + +```bash +python3 -m prik --language c native_math.c \ + --compiler cc \ + --out native_math \ + --out-dir build +``` + +Call the generated Python API: + +```python +import sys + +import numpy as np + +sys.path.insert(0, "build") +import native_math + +print(native_math.add(np.float64(3.0), np.float64(2.5))) # 5.5 +``` + +This source build also writes an editable contract. For C pointers, arrays, +and authored contracts, see [C Support](user/language-support/c-support.md). + ## Shape the Python API For a richer API, PRIK lets you reshape the generated Python surface without @@ -188,15 +225,15 @@ class point: @native_call([Pass(), Addr(Arg(0)), Addr(Arg(1))]) def translate(self, dx: Float64, dy: Float64) -> None: ... - @bind("norm_squared") @native_call([Pass()]) def norm_squared(self) -> Float64: ... ``` -`@bind("move")` keeps the original native target while the declaration's -placement and name define the Python-facing API. `Pass()` supplies the -receiver (`self`) to the native call; `Addr(Arg(...))` passes the remaining -arguments by address as required by the native calling convention. +`@bind("move")` maps the Python-facing `translate` method to the native +`move` procedure. `norm_squared` needs no `@bind` because its Python and +native names already match. `Pass()` supplies the receiver (`self`) to the +native call; `Addr(Arg(...))` passes the remaining arguments by address as +required by the native calling convention. Build from the contract: @@ -277,6 +314,10 @@ values below `1.0×` favor f2py. [Install PRIK →](user/getting-started/installation.md){ .prik-primary-cta } [Read Getting Started →](user/getting-started/index.md){ .prik-primary-cta } +**Wrapping a supported C API?** + +[Read C Support →](user/language-support/c-support.md){ .prik-primary-cta } + **Working on PRIK itself?** [Read Developer Documentation →](developer/index.md){ .prik-primary-cta } diff --git a/docs/user/about.md b/docs/user/about.md index 4ef7cc2d1..e1246410c 100644 --- a/docs/user/about.md +++ b/docs/user/about.md @@ -13,11 +13,13 @@ publication: reviewed PRIK — **Python Runtime Interop Kit** — is an open-source project for exposing native libraries through natural Python APIs. -Its first implemented target is **Fortran-to-Python interoperability**, with a -focus on modern Fortran features and native behavior that traditional wrapper -generators often cannot represent reliably. PRIK is for Python users who need -native numerical, scientific, or systems code without having to design and -maintain the entire language boundary themselves. +PRIK supports **Fortran-to-Python and C-to-Python interoperability**, with a +focus on native behavior that traditional wrapper generators often cannot +represent reliably. In both languages, editable `.pyi` contracts let you shape +the Python API. The [C support guide](language-support/c-support.md) describes +its current coverage. PRIK is for Python users who need native numerical, +scientific, or systems code without having to design and maintain the entire +language boundary themselves. The broader goal is to offer the same clear workflow for more native languages without hiding the ownership, memory, and calling rules needed to keep that @@ -64,13 +66,10 @@ retaining the information required to call the native code correctly. ## Current scope and direction -PRIK is currently an **alpha project** focused on Fortran-to-Python -interoperability. Current work is expanding modern Fortran support and -strengthening compatibility across compilers, platforms, and architectures. - -The next major phase will focus on **C-to-Python interoperability**, extending -PRIK's semantic model and wrapper-generation pipeline beyond Fortran while -preserving the same high-level, user-friendly workflow. +PRIK is currently an **alpha project** for Fortran-to-Python and +C-to-Python interoperability. Current work is expanding modern Fortran +support, broadening C beyond its documented ABI subset, and strengthening +compatibility across compilers, platforms, and architectures. Longer term, PRIK is intended to support additional native languages and execution environments, including C++, CUDA, and other backends. These are diff --git a/docs/user/examples/bspline-wrapper.md b/docs/user/examples/bspline-wrapper.md new file mode 100644 index 000000000..ae4d69934 --- /dev/null +++ b/docs/user/examples/bspline-wrapper.md @@ -0,0 +1,245 @@ +--- +title: Build and Validate BSPLINE-FORTRAN with PRIK +audience: users, advanced users +prerequisites: derived types, arrays, packaging +related: fftpack-wrapper.md, ../guide/wrapping-derived-types.md +status: maintained +publication: reviewed +--- + +# Build and Validate BSPLINE-FORTRAN with PRIK + +This example takes the checked-in +[BSPLINE-FORTRAN](https://github.com/jacobwilliams/bspline-fortran) source and +builds an importable Python extension with the complete interpolation surface: +15 public procedural routines, eight order constants, and seven public classes. + +It evaluates B-splines from one to six dimensions. The tests compare results +with analytic functions and SciPy rather than treating the wrapper as its own +reference. + +### What this example shows + +- Wrap a modern multi-file Fortran library as one Python extension. +- Construct and call derived types over an abstract Fortran base. +- Check procedural and object-oriented interpolation with NumPy arrays. + +You should already be comfortable with NumPy arrays, Python classes, and +building a local Fortran extension. + +--- + +## Versions used + +| Component | Version / source | +| --- | --- | +| PRIK | current repository checkout | +| BSPLINE-FORTRAN | [version 7.4.0, commit `047c7244`](https://github.com/jacobwilliams/bspline-fortran/tree/047c7244) | +| Python | 3.12 in the dedicated CI job | +| NumPy | 2.5.1 | +| SciPy | 1.18.0 | +| Fortran compiler | GNU Fortran 13 in CI; a compatible `gfortran` works locally | + +The repository owns the checked-in source snapshot under +`examples/bspline/native/`, so the example does not download code during its +build. + +--- + +## 1. Prepare the repository and toolchain + +Clone PRIK, create a virtual environment, and install the Python tools used by +the dedicated CI job: + +```bash +git clone https://github.com/PyNumLab/prik.git +cd prik +python3 -m venv .venv +. .venv/bin/activate +python3 -m pip install --upgrade pip +python3 -m pip install -e ".[qa]" "numpy==2.5.1" "scipy==1.18.0" +``` + +Install GNU Fortran separately. On Ubuntu: + +```bash +sudo apt-get update +sudo apt-get install --yes gfortran +gfortran --version +``` + +All remaining commands run from the repository root with the virtual +environment active. The complete runnable project lives under +[`examples/bspline/`](../../../examples/bspline/). + +--- + +## 2. Build the PRIK wrapper + +BSPLINE-FORTRAN separates its kind definitions, procedural routines, and +object-oriented types into ordered source files. The build command passes those +three public sources in dependency order: + + +```bash +export EXAMPLE_WORKSPACE="$PWD" +export BSPLINE_BUILD_ROOT="$(mktemp -d)" + +mkdir -p "$BSPLINE_BUILD_ROOT/prik/generated" +cd "$BSPLINE_BUILD_ROOT/prik" + +python3 -m prik \ + "$EXAMPLE_WORKSPACE/examples/bspline/native/bspline_kinds_module.F90" \ + "$EXAMPLE_WORKSPACE/examples/bspline/native/bspline_sub_module.f90" \ + "$EXAMPLE_WORKSPACE/examples/bspline/native/bspline_oo_module.f90" \ + --out prik_bspline \ + --out-dir "$BSPLINE_BUILD_ROOT/prik/generated" \ + --compiler "$(command -v gfortran)" \ + --jobs 8 \ + --wrapper-fortran-flags="-O0 -g0" \ + --wrapper-c-flags="-O0 -g0" +``` + +The example uses `-O0` so the tests focus on correct results. PRIK compiles the +native source and generated bridge into one extension. + +For normal use, source the convenience entrypoint: + +```bash +source examples/bspline/build_all.sh +``` + +It builds the extension and exports its directory on `PYTHONPATH` for the +current shell. + +--- + +## 3. Use the generated Python API + +The object-oriented module exposes an abstract `bspline_class` and six concrete +dimension-specific subclasses. The `bspline_1d` generic constructor accepts an +empty form and a data-driven form: + +```python +import numpy as np +import prik_bspline.bspline_oo_module as bspline + +x = np.linspace(0.0, 2.0 * np.pi, 25) +spline = bspline.bspline_1d(x, np.sin(x), np.int32(4)) + +value, iflag = spline.evaluate(np.float64(1.234), np.int32(0)) +area, iflag = spline.integral(np.float64(0.0), np.float64(np.pi)) +``` + +The abstract base is exported but cannot be constructed. Its concrete +extensions inherit the base bindings and answer its deferred operations: + +```python +bspline.bspline_class() +# TypeError: bspline_class is an abstract native type and cannot be +# instantiated; create one of its concrete extensions instead + +issubclass(bspline.bspline_1d, bspline.bspline_class) # True +``` + +The procedural module exposes the matching `db1ink` through `db6ink` setup +routines and `db1val` through `db6val` evaluators. Pass ordinary NumPy arrays; +PRIK performs the ABI conversion inside the generated wrapper. + +--- + +## 4. Run the complete test suite + +After the build finishes, run: + +```bash +python3 -m pytest -q examples/bspline/tests +``` + +The tests cover every exported routine and class: + +| Family | Public surface | +| --- | ---: | +| Interpolation setup | 6 routines | +| Evaluation | 6 routines | +| Definite integrals | 2 routines | +| Status reporting | 1 routine | +| Order constants | 8 constants | +| Derived types | 1 abstract base + 6 concrete classes | + +The inventory test fails if an expected export disappears, an extra public +export appears, or a procedural routine has no named numerical test. + +--- + +## 5. See how results are validated + +The suite checks interpolation against analytic values and SciPy, along with +constructor behavior, inheritance, abstract-base dispatch, generated status, +and Fortran-order array handling. This test comes directly from the runnable +suite and shows the procedural one-dimensional definite integral: + + +```python +def test_db1sqad(bspline_sub): + x = np.linspace(0.0, np.pi, 60) + knots, bcoef, nx = _interpolant(bspline_sub, x, np.sin(x)) + work = np.zeros(3 * int(CUBIC), dtype=np.float64) + + value, iflag = bspline_sub.db1sqad(knots, bcoef, nx, CUBIC, np.float64(0.0), np.float64(np.pi), work) + assert iflag == np.int32(0) + assert value == pytest.approx(2.0, abs=1.0e-6) +``` + +It builds a cubic spline for `sin(x)`, integrates it from zero to π, and checks +the known value of two. + +--- + +## 6. Run focused examples + +After building the extension, run a family or one routine: + +```bash +python3 -m pytest -q examples/bspline/tests/test_object_oriented_api.py +python3 -m pytest -q \ + examples/bspline/tests/test_procedural_api.py::test_db1ink +python3 -m pytest -q examples/bspline/tests -k db6 +``` + +- Derived-type examples → + [`test_object_oriented_api.py`](../../../examples/bspline/tests/test_object_oriented_api.py) +- Procedural numerical examples → + [`test_procedural_api.py`](../../../examples/bspline/tests/test_procedural_api.py) +- Public surface and coverage check → + [`test_routine_coverage.py`](../../../examples/bspline/tests/test_routine_coverage.py) +- Reviewed inventory → + [`routine_inventory.py`](../../../examples/bspline/routine_inventory.py) +- Copyable project instructions → + [`examples/bspline/README.md`](../../../examples/bspline/README.md) + +--- + +## Troubleshooting + +- Confirm that `gfortran` is available on `PATH`. +- Use `source examples/bspline/build_all.sh`; executing it in a child shell does + not preserve the exported `PYTHONPATH`. +- Run one failing procedure with `-vv -s` to retain its compiler and wrapper + diagnostics. + +--- + +## Source provenance + +The native files under +[`examples/bspline/native/`](../../../examples/bspline/native/) are the +BSPLINE-FORTRAN 7.4.0 snapshot at +[commit `047c7244`](https://github.com/jacobwilliams/bspline-fortran/tree/047c7244). +The upstream `bspline_defc_module` least-squares fitter and its +`bspline_blas_module` bridge are intentionally outside this interpolation +example. + +See the [upstream repository](https://github.com/jacobwilliams/bspline-fortran) +and its bundled BSD-3-Clause license before redistributing the vendored native +source. diff --git a/docs/user/examples/cfd-mini-example.md b/docs/user/examples/cfd-mini-example.md deleted file mode 100644 index b8d6bf871..000000000 --- a/docs/user/examples/cfd-mini-example.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -title: CFD Mini-Example -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 - -Reserved runnable example for a small CFD-oriented native project. - -## TODO - -- TODO: Define a compact fixture that is fast enough for documentation - verification. -- TODO: Document memory layout and performance limitations. diff --git a/docs/user/examples/index.md b/docs/user/examples/index.md index e01ec6400..920516cc5 100644 --- a/docs/user/examples/index.md +++ b/docs/user/examples/index.md @@ -2,52 +2,44 @@ title: Examples Gallery audience: users prerequisites: getting started -related: ../tutorials/index.md, ../guide/building-shared-library.md +related: ../guide/building-shared-library.md, ../language-support/c-support.md, ../reference/cli-commands.md, ../reference/python-api.md status: maintained -publication: draft +publication: reviewed --- # Examples Gallery -This section includes checked recipes and four complete real-library examples: -BLAS, LAPACK, FFTPACK, and MINPACK. Each one provides build commands, Python -usage, and numerical checks for its public routines. +This section includes six complete real-library examples: BLAS, LAPACK, +FFTPACK, MINPACK, BSPLINE-FORTRAN, and libm. Each one provides build commands, +Python usage, and numerical checks for its public routines. -The larger project examples below are placeholders for future complete runnable -projects. Each one must include source, build command, import command, runtime -check, limitations, and test evidence before it is marked maintained. +## CI portability + +The **Real Libraries Portability** workflow runs every example on Linux +x86-64, Linux Arm64, macOS Intel, and macOS Arm64 with Python 3.12. GNU +Fortran 13 and GCC 13 build the Fortran examples. libm is tested with GCC 13 +and Clang 18 on Linux, and GNU GCC 13 and Apple Clang on macOS. BLAS and LAPACK +add full-surface audits on Linux x86-64. + +For a smaller first workflow, start with one of the checked guides below. Each +links to a complete source, build, import, or result path, rather than a +draft-only recipe. ## Choose a page | Goal | Page | | --- | --- | | Build and import a first extension | [First Wrapped Function](../getting-started/first-wrapped-function.md) | +| Build a first Fortran module | [First Wrapped Module](../getting-started/first-wrapped-module.md) | | Build from several ordered sources | [Building the Shared Library](../guide/building-shared-library.md#multiple-source-files) | | Generate and edit `Makefile.prik` | [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) | +| Build through Python code | [Python API](../reference/python-api.md#building-an-extension) | +| Inspect source or control command output | [CLI Commands](../reference/cli-commands.md#parse-and-semantics) | +| Work with semantic `.pyi` contracts | [Editing `.pyi` Contracts](../reference/pyi-contracts/index.md) | +| Build a supported C API | [C Support](../language-support/c-support.md) | | Build and validate the complete Reference BLAS | [BLAS wrapper](blas-wrapper.md) | | Build complete Reference LAPACK and validate 127 float64 routines | [LAPACK wrapper](lapack-wrapper.md) | | Wrap and validate all 31 FFTPACK procedures with NumPy and SciPy | [FFTPACK wrapper](fftpack-wrapper.md) | | Wrap all 22 MINPACK procedures and use Python callbacks | [MINPACK wrapper](minpack-wrapper.md) | - -## Planned Project Examples - -- [ODE solver](ode-solver.md) -- [CFD mini-example](cfd-mini-example.md) -- [Object-oriented Fortran example](object-oriented-fortran.md) -- [MPI example](mpi-example.md) -- [OpenMP example](openmp-example.md) - -## TODO - -- TODO: Add further runnable checked examples one at a time. -- TODO: Keep examples with unavailable runtime support marked not yet - implemented. +| Build and validate modern Fortran classes and 15 interpolation routines | [BSPLINE-FORTRAN wrapper](bspline-wrapper.md) | +| Wrap 60 target-generated ISO C99 math routines from a system library | [libm wrapper](libm-wrapper.md) | diff --git a/docs/user/examples/lapack-wrapper.md b/docs/user/examples/lapack-wrapper.md index f6348362e..8879f3405 100644 --- a/docs/user/examples/lapack-wrapper.md +++ b/docs/user/examples/lapack-wrapper.md @@ -84,16 +84,18 @@ libraries for companion support symbols: ```bash export EXAMPLE_WORKSPACE="$PWD" export LAPACK_BUILD_ROOT="$(mktemp -d)" -export LAPACK_SHARED_LIBRARY="$( +LAPACK_SHARED_LIBRARY="$( python -m examples.native_library lapack \ --compiler "$(command -v gfortran)" \ --jobs 8 )" +export LAPACK_SHARED_LIBRARY export LAPACK_MODULE_DIR="$(dirname "$LAPACK_SHARED_LIBRARY")/modules" +export LAPACK_SOURCE_ROOT="$(dirname "$LAPACK_SHARED_LIBRARY")/wrapper_sources" mkdir -p "$LAPACK_BUILD_ROOT/prik/generated" cd "$LAPACK_BUILD_ROOT/prik" -python -m prik "$EXAMPLE_WORKSPACE/examples/lapack/native" \ +python -m prik "$LAPACK_SOURCE_ROOT" \ --out prik_reference_lapack_example \ --out-dir "$LAPACK_BUILD_ROOT/prik/generated" \ --compiler "$(command -v gfortran)" \ @@ -270,7 +272,9 @@ def test_dpotrf_reconstructs_spd_matrix(prik_lapack, scipy_lapack, f2py_lapack): f2py_result = f2py_lapack.dpotrf(b"L", 2, f2py_a, 0) scipy_factor, scipy_info = scipy_lapack.dpotrf(stored.copy(order="F"), lower=1, clean=0) - assert prik_scalars == (2, 2, 0) + # LAPACK declares no intent on its dummies, so the conservative + # intent(inout) default returns every scalar, character selectors included. + assert prik_scalars == ("L", 2, 2, 0) assert f2py_result is None assert scipy_info == 0 prik_lower = np.tril(prik_a) @@ -338,11 +342,15 @@ The official versioned archive is The repository boundary is precise: -- [`examples/lapack/native/`](../../../examples/lapack/native/) owns 2,062 implementation sources. +- [`examples/lapack/native/`](../../../examples/lapack/native/) owns the complete 2,062-file source snapshot. Of those, 2,061 are byte-for-byte the upstream `SRC/` directory; the repository adds its project-local `dlamch.f` machine-parameter implementation. +- The official default build excludes the 130 sources in [`examples/lapack/xblas_sources.txt`](../../../examples/lapack/xblas_sources.txt), which require the separately distributed XBLAS library. + PRIK and the reusable native library use the remaining 1,932 sources and expose 1,936 procedures. +- [`examples/lapack/support/`](../../../examples/lapack/support/) owns the two `INSTALL/` workspace-rounding helpers required by that default source set. - Upstream test programs, timing programs, examples and matrix generators are **not** part of the library source set. - [`examples/blas/native/`](../../../examples/blas/native/) separately owns the 155 Reference BLAS sources. They are consumed as dependencies and are not copied into the LAPACK directory. +- Installed LAPACK and BLAS libraries provide support routines outside the copied default source set. To independently audit the official archive: diff --git a/docs/user/examples/libm-wrapper.md b/docs/user/examples/libm-wrapper.md new file mode 100644 index 000000000..b901a3cb7 --- /dev/null +++ b/docs/user/examples/libm-wrapper.md @@ -0,0 +1,325 @@ +--- +title: Build and Validate libm with PRIK +audience: users, advanced users +prerequisites: C support, semantic .pyi contracts +related: ../language-support/c-support.md, ../reference/cli-commands.md +status: maintained +publication: reviewed +--- + +# Build and Validate libm with PRIK + +This example wraps 60 reviewed ISO C99 routines from the platform's standard +math library and validates every one with a named numerical test. The build +regenerates the semantic `.pyi` for the active C compiler and target. + +It follows the maintained real-library example structure: a reviewed native +surface, copyable build scripts, a grouped routine inventory, fail-closed +coverage audits, numerical tests, documentation, and CI execution. + +### What this example shows + +- Generate a target-specific contract from the platform's own `` and a + reviewed function allowlist. +- Link an existing system library without vendoring or compiling its sources. +- Preserve exact native `long`, `long long`, and `int` identities while keeping + ordinary NumPy types in the public Python signature. +- Test every exported function and audit the inventory against the built module. + +Read [C support](../language-support/c-support.md) and the +[CLI reference](../reference/cli-commands.md) first if the direct C workflow is +new to you. + +--- + +## Versions used + +| Component | Version / source | +| --- | --- | +| PRIK | current repository checkout | +| libm | the target's C standard math library | +| Python | 3.12 in the dedicated CI job | +| NumPy | 2.5.1 in CI | +| C compiler | Linux GCC and Apple Clang in CI | + +The declarations selected by the example are ISO C99. The generated contract, +NumPy dtypes, compiler, and library link remain target-specific. + +--- + +## 1. Prepare the repository and toolchain + +```bash +git clone https://github.com/PyNumLab/prik.git +cd prik +python3 -m venv .venv +. .venv/bin/activate +python3 -m pip install --upgrade pip +python3 -m pip install -e ".[qa]" "numpy==2.5.1" +``` + +Install a C compiler and the Python development headers. On Ubuntu: + +```bash +sudo apt-get update +sudo apt-get install --yes build-essential python3-dev +``` + +All remaining commands run from the repository root. The runnable project is +under [`examples/libm/`](../../../examples/libm/). + +--- + +## 2. Review the selected API + +[`libm_probe.h`](../../../examples/libm/libm_probe.h) contains only +`#include `, so the active toolchain supplies every declaration. +[`iso_c99_routines.txt`](../../../examples/libm/iso_c99_routines.txt) is the +reviewed 60-function public surface. The export allowlist excludes the rest of +the platform header and fails if a requested ISO C99 function is missing. + +Generate the contract for the active target with: + +```bash +mkdir -p build +python3 -m prik generate --pyi --language c examples/libm/libm_probe.h \ + --compiler "$(command -v cc)" \ + --std c99 \ + --include-exposure roots-only \ + --export-symbols examples/libm/iso_c99_routines.txt \ + --out build/libm_api.pyi +``` + +The compiler probe maps the C types to target-sized public contract dtypes. The +generated `@native_call` expressions retain an exact C scalar type wherever +normalization would otherwise erase a distinction needed by the declaration. + +Macros are not part of this surface. If an API must expose a macro, provide an +ordinary native function that evaluates it and wrap that function. + +`frexp`, `modf`, and `remquo` are excluded because their output pointers need +an authored direction/projection contract. `nan` needs authored string +semantics, and non-ISO Bessel extensions are outside the reviewed ISO C99 +selection. + +--- + +## 3. Build the wrapper + +The maintained script generates the target contract, compiles the binding, and +links libm: + + +```bash +export EXAMPLE_WORKSPACE="$PWD" +export LIBM_BUILD_ROOT="$(mktemp -d)" + +LIBM_COMPILER="${PRIK_LIBM_CC:-cc}" +if ! LIBM_COMPILER_PATH="$(command -v "$LIBM_COMPILER")"; then + echo "libm example: C compiler not found: $LIBM_COMPILER" >&2 + return 1 2>/dev/null || exit 1 +fi +export LIBM_COMPILER_PATH + +mkdir -p "$LIBM_BUILD_ROOT/prik/contract" "$LIBM_BUILD_ROOT/prik/generated" +cd "$LIBM_BUILD_ROOT/prik" + +if ! python3 -m prik generate --pyi --language c \ + "$EXAMPLE_WORKSPACE/examples/libm/libm_probe.h" \ + --compiler "$LIBM_COMPILER_PATH" \ + --std c99 \ + --include-exposure roots-only \ + --export-symbols "$EXAMPLE_WORKSPACE/examples/libm/iso_c99_routines.txt" \ + --out "$LIBM_BUILD_ROOT/prik/contract/libm_api.pyi"; then + return 1 2>/dev/null || exit 1 +fi + +if ! python3 -m prik --language c "$LIBM_BUILD_ROOT/prik/contract/libm_api.pyi" \ + --out prik_reference_libm \ + --out-dir "$LIBM_BUILD_ROOT/prik/generated" \ + --compiler "$LIBM_COMPILER_PATH" \ + --native-library m \ + --positional-only \ + --collision-adapter-all; then + return 1 2>/dev/null || exit 1 +fi +``` + +For normal use, source the convenience entrypoint: + +```bash +source examples/libm/build_all.sh +``` + +It also exports the built extension directory on `PYTHONPATH` for the current +shell. + +--- + +## 4. Understand exact native scalar types + +On an LP64 target, C `long` and `long long` may both map to public `Int64`, but +they remain distinct C types. A target-generated contract keeps the native +result declaration explicitly when needed: + +```python +@native_call([Arg(0)], result=CLongLong(Return(0))) +def llrint(x: Float64) -> Int64: ... +``` + +The expression's position determines its direction. Inside the native argument +list, a cast describes a native parameter. In `result=...`, it declares the +native function result, which the binding converts into Python result slot 0. + +The binding therefore declares `llrint` as returning `long long`, receives that +value, and converts it to the public `Int64` storage. `lrint` similarly retains +C `long`, whose public result may be `Int32` or `Int64` on different targets. +When the target's canonical fixed-width typedef is already a typedef of +`long`, no `CLong` expression is needed; otherwise generation emits one even +when the two C types have the same width. These sparse casts preserve ABI type +identity. The separate `--collision-adapter-all` mechanism prevents selected +`math.h` declarations from colliding with identifiers in Python's headers. +LTO is optional and is deliberately not required by this example. + +--- + +## 5. Run the complete test suite + +```bash +python3 -m pytest -q examples/libm/tests +``` + +The inventory contains exactly 60 routines: + +| Family | Routines | +| --- | ---: | +| Trigonometric | 7 | +| Hyperbolic | 6 | +| Exponential and logarithmic | 7 | +| Power and roots | 4 | +| Rounding, truncation, and remainder | 12 | +| Floating-point manipulation | 13 | +| Error and gamma functions | 4 | +| Single and extended precision | 7 | +| **Total** | **60** | + +--- + +## 6. See how results are validated + +Tests compare Python's `math` module where it has the same operation and use +independent identities elsewhere. The complete elementary group demonstrates +the NumPy scalar boundary, tolerance-based transcendental comparisons, exact +results where the operation permits them, and the precision benefit of +specialized operations such as `expm1`: + + +```python +def test_elementary(libm): + assert np.isclose(libm.sin(np.float64(1.0)), math.sin(1.0), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.cos(np.float64(1.0)), math.cos(1.0), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.tan(np.float64(0.5)), math.tan(0.5), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.asin(np.float64(0.5)), math.asin(0.5), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.acos(np.float64(0.5)), math.acos(0.5), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.atan(np.float64(0.5)), math.atan(0.5), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose( + libm.atan2(np.float64(1.0), np.float64(2.0)), + math.atan2(1.0, 2.0), + rtol=DOUBLE_TOLERANCE, + atol=DOUBLE_TOLERANCE, + ) + assert np.isclose(libm.sinh(np.float64(0.75)), math.sinh(0.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.cosh(np.float64(0.75)), math.cosh(0.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.tanh(np.float64(0.75)), math.tanh(0.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.asinh(np.float64(0.75)), math.asinh(0.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.acosh(np.float64(1.75)), math.acosh(1.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.atanh(np.float64(0.75)), math.atanh(0.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.exp(np.float64(1.0)), math.e, rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + + # exp2 is exact on a whole exponent, so no tolerance is needed. + assert libm.exp2(np.float64(10.0)) == 1024.0 + + # expm1 keeps the precision that exp(x) - 1 loses for small x. + assert np.isclose( + libm.expm1(np.float64(1e-9)), + math.expm1(1e-9), + rtol=DOUBLE_TOLERANCE, + atol=DOUBLE_TOLERANCE, + ) + assert libm.expm1(np.float64(1e-9)) != math.exp(1e-9) - 1.0 + + assert np.isclose(libm.log(np.float64(math.e)), 1.0, rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert libm.log2(np.float64(1024.0)) == 10.0 + assert np.isclose(libm.log10(np.float64(1000.0)), 3.0, rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.log1p(np.float64(1e-9)), math.log1p(1e-9), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert libm.pow(np.float64(2.0), np.float64(10.0)) == 1024.0 + assert libm.sqrt(np.float64(144.0)) == 12.0 + assert np.isclose(libm.cbrt(np.float64(27.0)), 3.0, rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert libm.hypot(np.float64(3.0), np.float64(4.0)) == 5.0 +``` + +Precision is asserted rather than assumed. The suite checks `float` results as +`float32`, follows the target representation for `long double`, derives C +`int` and C `long` NumPy dtypes from the running target, and checks supported +`long long` results. Rounding-sensitive functions are compared under the +active floating-point mode, transcendental results use tolerances, and `fma` +is checked for one fused rounding. + +On Apple ARM64, C `long double` has the same 64-bit storage width as `double`, +so the generated public contract uses `Float64` and the example passes +`numpy.float64`. A target with wider `long double` storage instead uses +`Float128` and `numpy.longdouble`; the native declaration remains `long double` +in either case. The generated `.pyi` is the authority for that public dtype, +while `CLongDouble` in `@native_call` directs the private scalar conversion and +does not add a second accepted Python dtype. The numerical tests use the dtype +named by the generated `sinl` annotation. + +--- + +## 7. Run focused examples + +```bash +python3 -m pytest -q examples/libm/tests/test_numerical.py::test_special +python3 -m pytest -q examples/libm/tests/test_numerical.py::test_rounding +python3 -m pytest -q examples/libm/tests/test_numerical.py::test_precision +``` + +- Platform declaration probe → + [`libm_probe.h`](../../../examples/libm/libm_probe.h) +- Reviewed function selection → + [`iso_c99_routines.txt`](../../../examples/libm/iso_c99_routines.txt) +- Public routine list → + [`routine_inventory.py`](../../../examples/libm/routine_inventory.py) +- Routine coverage checks → + [`test_routine_coverage.py`](../../../examples/libm/tests/test_routine_coverage.py) +- Copyable project instructions → + [`examples/libm/README.md`](../../../examples/libm/README.md) + +--- + +## Troubleshooting + +- Confirm that `cc` is on `PATH` and Python development headers are installed. +- Set `PRIK_LIBM_CC` to use a compiler other than `cc`. +- Use `source examples/libm/build_all.sh`; a child shell cannot preserve its + exported `PYTHONPATH`. +- The `--native-library m` spelling is platform build configuration. If the + target exposes its C math symbols without a separate libm, adjust that link + item for the target. +- Keep `--collision-adapter-all` when regenerating this wrapper; it isolates + any selected `math.h` identifier already declared by a binding header. + +## CI portability coverage + +The shared [Real Libraries Portability coverage](index.md#ci-portability) runs +every maintained example on four hosted targets. libm runs twice per target: +GCC 13 and Clang 18 on Linux, GNU GCC 13 and Apple Clang on macOS. These lanes +exercise the target's own `math.h`, libm, scalar probe, generated contract, and +collision adapter. Native Windows/MSVC remains outside PRIK's current POSIX C +build lane. + +## Source provenance + +There are no vendored implementation sources or copied prototypes. The example +parses the target's `math.h` and links its math library through the reviewed +ISO C99 name selection. diff --git a/docs/user/examples/mpi-example.md b/docs/user/examples/mpi-example.md deleted file mode 100644 index b4fc6eb5e..000000000 --- a/docs/user/examples/mpi-example.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: MPI Example -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 - -Not yet implemented. This page reserves documentation for future MPI-related -wrapper examples and distribution constraints. - -## TODO - -- TODO: Define the supported MPI contract before adding examples. -- TODO: Add runnable CI or manual-verification evidence before changing this - status. diff --git a/docs/user/examples/object-oriented-fortran.md b/docs/user/examples/object-oriented-fortran.md deleted file mode 100644 index 01cace948..000000000 --- a/docs/user/examples/object-oriented-fortran.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -title: Object-Oriented Fortran Example -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 - -Reserved runnable example for derived types, type-bound procedures, inheritance, -constructors, and finalizers. - -## TODO - -- TODO: Add runtime-backed examples for supported object-oriented features. -- TODO: Mark unsupported inheritance or polymorphic cases through language - support links. diff --git a/docs/user/examples/ode-solver.md b/docs/user/examples/ode-solver.md deleted file mode 100644 index 1947e2470..000000000 --- a/docs/user/examples/ode-solver.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: ODE Solver Example -audience: users, advanced users -prerequisites: callbacks, arrays -related: ../tutorials/numerical-solver.md, ../guide/callbacks.md -status: planned-documentation -publication: draft ---- - -# ODE Solver Example - -Reserved runnable example for an ODE solver workflow. - -## TODO - -- TODO: Add a solver example with runtime assertions. -- TODO: Document callback lifetime and error propagation if callbacks are used. diff --git a/docs/user/examples/openmp-example.md b/docs/user/examples/openmp-example.md deleted file mode 100644 index 75084bb49..000000000 --- a/docs/user/examples/openmp-example.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: OpenMP Example -audience: advanced users -prerequisites: runtime troubleshooting, platform-specific troubleshooting -related: mpi-example.md, ../guide/error-handling.md -status: planned-documentation -publication: draft ---- - -# OpenMP Example - -Reserved runnable example for OpenMP-enabled native code and runtime behavior. - -## TODO - -- TODO: Document current OpenMP runtime support with checked tests. -- TODO: Add compiler flag, runtime library, and concurrency limitations. 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 986b80934..1d512dc4d 100644 --- a/docs/user/examples/recipes/build-and-import-python-api.md +++ b/docs/user/examples/recipes/build-and-import-python-api.md @@ -25,7 +25,7 @@ import numpy as np from prik import build_fortran_extension -source = Path("tests/fortran/building_shared_library/end_to_end/fixtures/native/fruntime_abi_f90.f90") +source = Path("tests/fortran/infrastructure/building/end_to_end/fixtures/native/fruntime_abi_f90.f90") with TemporaryDirectory() as output_dir: build = build_fortran_extension(source, output_dir=output_dir) module = build.import_module() diff --git a/docs/user/examples/recipes/control-cli-output.md b/docs/user/examples/recipes/control-cli-output.md index 7c332791c..fdfbd2244 100644 --- a/docs/user/examples/recipes/control-cli-output.md +++ b/docs/user/examples/recipes/control-cli-output.md @@ -19,7 +19,7 @@ need to inspect module variables and derived-type fields: ```bash -python3 -m prik parse tests/fortran/source_parsing/parsing/fixtures/general/modern_pyi_example.f90 \ +python3 -m prik parse tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.f90 \ --show-vars ``` @@ -29,7 +29,7 @@ Use `--print-limit` to keep long reports readable while preserving totals: ```bash -python3 -m prik parse tests/fortran/source_parsing/parsing/fixtures/general/modern_pyi_example.f90 \ +python3 -m prik parse tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.f90 \ --show-vars --print-limit 1 ``` @@ -37,7 +37,7 @@ Expected output: ```text -File: tests/fortran/source_parsing/parsing/fixtures/general/modern_pyi_example.f90 +File: tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.f90 Modules: 1 - module modern_math_physics (vars=2, uses=0) Variables: 2 @@ -60,7 +60,7 @@ Choose one inspection stage per command. For parser details, run: ```bash -python3 -m prik parse tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 +python3 -m prik parse tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.f90 ``` ## Notes diff --git a/docs/user/examples/recipes/inspect-c-api.md b/docs/user/examples/recipes/inspect-c-api.md index a4681db0b..d7eb49652 100644 --- a/docs/user/examples/recipes/inspect-c-api.md +++ b/docs/user/examples/recipes/inspect-c-api.md @@ -13,8 +13,10 @@ publication: draft PRIK_C_DOCS_END --> + ```fortran module m1 contains @@ -29,14 +29,14 @@ end module m1 ```bash -python3 -m prik parse tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 +python3 -m prik parse tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.f90 ``` Expected output: ```text -File: tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 +File: tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.f90 Modules: 1 - module m1 (vars=0, uses=0) Procedures: 1 @@ -47,14 +47,14 @@ File: tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 ```bash -python3 -m prik generate --pyi tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 +python3 -m prik generate --pyi tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.f90 ``` Expected output: ```python -File: tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 +File: tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.f90 Root contract: basic_subroutine/basic_subroutine.pyi from . import m1 diff --git a/docs/user/examples/recipes/semantic-pyi-contracts.md b/docs/user/examples/recipes/semantic-pyi-contracts.md index 40e15fbc3..9d8927b3a 100644 --- a/docs/user/examples/recipes/semantic-pyi-contracts.md +++ b/docs/user/examples/recipes/semantic-pyi-contracts.md @@ -15,7 +15,7 @@ semantic contract. ## Generate A Starter Contract ```bash -python3 -m prik generate --pyi tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 \ +python3 -m prik generate --pyi tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.f90 \ --out contracts/basic_subroutine ``` diff --git a/docs/user/faq/index.md b/docs/user/faq/index.md index ca4b05ccb..3727b2d03 100644 --- a/docs/user/faq/index.md +++ b/docs/user/faq/index.md @@ -88,7 +88,9 @@ PRIK also covers important Fortran features: supported [pointer forms](../guide/pointers.md), native errors as [Python exceptions](../guide/error-handling.md), and [overloaded procedures](../guide/generic-interfaces.md). PRIK is currently -alpha, so check the linked guides for exact limitations. The +alpha, so check the linked guides for exact limitations, or the +[language feature matrix](../language-support/feature-matrix.md) for every +supported and blocked form in one table. The [performance results](../performance.md) cover only their measured runtime and clean-build workloads. diff --git a/docs/user/guide/building-shared-library.md b/docs/user/guide/building-shared-library.md index bc846ed29..dc290298c 100644 --- a/docs/user/guide/building-shared-library.md +++ b/docs/user/guide/building-shared-library.md @@ -10,8 +10,9 @@ publication: reviewed # Building the Shared Library -prik turns Fortran source into a Python extension module. The final module is a -native shared library that Python imports directly. +prik turns Fortran and C source into Python extension modules. The final module +is a native shared library that Python imports directly. The C workflow has its +own documented support boundary. This page continues with `scale.f90` from the [Common Beginner Workflow](../getting-started/beginner-workflow.md). @@ -58,6 +59,14 @@ GNU, IFX, and Flang are tested on Linux. See [Compiler Toolchains](../getting-started/installation.md#compiler-toolchains) for versions and other recognized options. +## Build a primitive C API directly + +PRIK supports C source as well. Start with [C +Support](../language-support/c-support.md) for complete source and +semantic-contract examples, Python API, supported C and NumPy types, pointer +contracts, preprocessing, generated Makefiles, and current limits. C input +always requires `--language c`. + ## Import Add the build directory to Python's search path, then import the module by its @@ -122,6 +131,7 @@ most useful settings are near the top: | Setting | What it changes | | --- | --- | | `FC` | Fortran compiler | +| `CC` | C compiler, including explicit C implementation sources | | `PRIK_LD` | Command that creates the shared library | | `PRIK_FFLAGS` | Extra Fortran compiler flags | | `PRIK_CFLAGS` | Extra C binding compiler flags | @@ -142,3 +152,12 @@ example. This workflow requires GNU Make. 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. + +## Every build option + +This page covers the common build paths. For the complete option surface — +native sources, objects, libraries, ordered link items, wrapper compiler flags, +and manifest replay — see the +[CLI commands reference](../reference/cli-commands.md), or run +`python3 -m prik --help-build`. To drive the same builds from Python instead of +a shell, see the [Python API reference](../reference/python-api.md). diff --git a/docs/user/guide/data-types.md b/docs/user/guide/data-types.md index 8aea24663..11c710470 100644 --- a/docs/user/guide/data-types.md +++ b/docs/user/guide/data-types.md @@ -36,6 +36,7 @@ Create `numeric_types.f90`: ```fortran module numeric_types + use iso_c_binding, only: c_long_double, c_long_double_complex use iso_fortran_env, only: int32, real64 implicit none contains @@ -50,11 +51,21 @@ contains output = 2.0_real64 * value end function double + real(c_long_double) function double_extended(value) result(output) + real(c_long_double), intent(in) :: value + output = 2.0_c_long_double * value + end function double_extended + complex(real64) function conjugate_value(value) result(output) complex(real64), intent(in) :: value output = conjg(value) end function conjugate_value + complex(c_long_double_complex) function conjugate_extended(value) result(output) + complex(c_long_double_complex), intent(in) :: value + output = conjg(value) + end function conjugate_extended + logical(kind=1) function invert(flag) result(output) logical(kind=1), intent(in) :: flag output = .not. flag @@ -78,7 +89,7 @@ python3 -m prik numeric_types.f90 --out-dir build/numeric-types The generated `numeric_types.pyi` is: ```python -from prik.contracts import Addr, Arg, Bool8, Complex128, Float64, Int32, native_call +from prik.contracts import Addr, Arg, Bool8, Complex128, Complex256, Float128, Float64, Int32, native_call @native_call([Addr(Arg(0))]) def add_one( @@ -90,11 +101,21 @@ def double( value: Float64 ) -> Float64: ... +@native_call([Addr(Arg(0))]) +def double_extended( + value: Float128 +) -> Float128: ... + @native_call([Addr(Arg(0))]) def conjugate_value( value: Complex128 ) -> Complex128: ... +@native_call([Addr(Arg(0))]) +def conjugate_extended( + value: Complex256 +) -> Complex256: ... + @native_call([Addr(Arg(0))]) def invert( flag: Bool8 @@ -121,12 +142,22 @@ import sys import numpy as np sys.path.insert(0, "build/numeric-types") -from numeric_types.numeric_types import add_one, conjugate_value, double, invert - -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(invert(True)) # False +from numeric_types.numeric_types import ( + add_one, + conjugate_extended, + conjugate_value, + double, + double_extended, + invert, +) + +print(add_one(np.int32(4))) # 5 +print(double(np.float64(1.5))) # 3.0 +# np.float64 cannot hold this value; np.longdouble keeps it. +print(double_extended(np.longdouble("1.0000000000000000001"))) +print(conjugate_value(np.complex128(1.0 + 2.0j))) # (1-2j) +print(conjugate_extended(np.clongdouble(1.0 + 2.0j))) # (1-2j) +print(invert(True)) # False ``` @@ -137,6 +168,8 @@ Result: ```text 5 3.0 +2.0000000000000000002 +(1-2j) (1-2j) False ``` @@ -151,12 +184,21 @@ False | `integer(8)` / `int64` | `Int64` | `np.int64` | `np.int64` | | `real(4)` | `Float32` | `np.float32` | `np.float32` | | `real(8)` / `real64` | `Float64` | `np.float64` | `np.float64` | +| `real(c_long_double)` — `real(10)` on x86-64 | `Float128` | `np.longdouble` | `np.longdouble` | | `complex(4)` | `Complex64` | `np.complex64` | `np.complex64` | | `complex(8)` | `Complex128` | `np.complex128` | `np.complex128` | +| `complex(c_long_double_complex)` — `complex(10)` on x86-64 | `Complex256` | `np.clongdouble` | `np.clongdouble` | | `logical` | `Bool8`-`Bool64` | `bool` or `np.bool_` | `bool` | | `character` | `String` / `String[n]` | Depends on the string boundary | Depends on the string boundary | | Derived Type | Generated Class | Instance of that class | Instance of that class | +`Float128` and `Complex256` mean the target's `long double`, not a fixed +128-bit format. On x86-64 that is x87 extended precision, so `real(10)` and +`complex(10)` map to it and `real(16)` does not; on a target whose `long +double` is IEEE quad, `real(16)` maps to it instead. prik decides from the +mantissa width the compiler reports, never from storage size — see +[Unsupported Widths And Forms](#unsupported-widths-and-forms). + Boolean contract names describe native storage, not different Python dtypes: | Semantic Contract | Native Logical Storage Represented | Scalar Input | Direct Result | Array Storage | @@ -229,9 +271,17 @@ NumPy scalar listed in the mapping table; Boolean scalar results are Python ## Unsupported Widths And Forms -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. +`Float128` and `Complex256` name the target's `long double`, which NumPy +exposes as `longdouble` and `clongdouble`. Storage size alone cannot identify +that format: on x86-64 both x87 extended precision and IEEE binary128 occupy +128 bits and differ only in mantissa width. + +prik therefore compares the compiler-measured mantissa against the target's +`long double` rather than trusting the declaration. On a target whose `long +double` is x87 extended precision this accepts C `long double` and Fortran +`real(10)`, and refuses `real(16)` with a diagnostic naming both widths -- +rather than narrowing it silently. On a target whose `long double` is IEEE +quad, the same rule accepts `real(16)`. --- diff --git a/docs/user/guide/error-handling.md b/docs/user/guide/error-handling.md index 08f7a6a35..81ff4ce47 100644 --- a/docs/user/guide/error-handling.md +++ b/docs/user/guide/error-handling.md @@ -113,14 +113,14 @@ python3 -m prik generate --pyi status_api.f90 --out contracts/status Add `@raises` to project the hidden native outputs into an exception: ```python -from prik.contracts import Addr, Arg, Int32, Return, String, native_call, raises, standalone +from prik.contracts import Addr, Arg, Hidden, Int32, String, native_call, raises, standalone @standalone @raises(status="status", message="message", success=0) -@native_call([Addr(Arg(0)), Return("status", 0), Return("message", 1)]) +@native_call([Addr(Arg(0)), Hidden("status", Int32), Hidden("message", String[32])]) def solve( value: Int32 -) -> tuple[Int32, String[32]]: ... +) -> None: ... ``` Build from the edited contract and native source: diff --git a/docs/user/guide/generic-interfaces.md b/docs/user/guide/generic-interfaces.md index 07b6b7efb..9095d698a 100644 --- a/docs/user/guide/generic-interfaces.md +++ b/docs/user/guide/generic-interfaces.md @@ -226,8 +226,8 @@ in Wrapping Derived Types. ## Limitations -- Source generic interfaces are not inferred as constructors automatically. - Edited exact constructor overload sets are supported. +- Only an interface named for a derived type becomes that type's constructor. + Any other generic interface stays an overloaded module function. - Polymorphic (`class(*)`) arguments and results are blocked. - Arrays of derived types and complex polymorphic cases are not supported yet. diff --git a/docs/user/guide/index.md b/docs/user/guide/index.md index d7d0f855b..bc7bcb1a0 100644 --- a/docs/user/guide/index.md +++ b/docs/user/guide/index.md @@ -64,4 +64,12 @@ the complete rules in one place. --- +**Checking whether a feature is supported** + +Each page below documents its own limitations. For the complete picture in one +table — including unsupported and partially supported forms — see the +[language feature matrix](../language-support/feature-matrix.md). + +--- + Start with **[Data Types](data-types.md)**. diff --git a/docs/user/guide/strings.md b/docs/user/guide/strings.md index bb115818a..a52b29d66 100644 --- a/docs/user/guide/strings.md +++ b/docs/user/guide/strings.md @@ -248,8 +248,106 @@ b'Xlpha ' - `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. +## Allocatable And Pointer Scalar Strings + +A scalar `character` dummy may carry the `allocatable` or `pointer` attribute, +at a deferred length (`character(len=:)`) or a declared one +(`character(len=8)`). Every combination is supported, in every direction: + +| Fortran dummy | Python surface | +| --- | --- | +| `intent(in)` | A `str` argument. | +| `intent(out)` | A returned `str`, or `None` when the procedure leaves it unallocated or unassociated. | +| `intent(inout)` | A `str` argument that also returns the value the procedure left behind, or `None`. | +| function result | A returned `str`, or `None`. | + +The attribute never changes the Python surface, and it never changes how the +value crosses into native code — a scalar string is always a byte buffer and a +length. It changes only the storage PRIK builds inside the generated adapter, +because an `allocatable` or `pointer` dummy will not accept a plain temporary as +its actual argument. + +An update keeps its `str` argument and adds a return value, because the native +procedure chooses the new value during the call and the caller's string cannot +hold it: + +```fortran +subroutine grow(value) + character(len=:), allocatable, intent(inout) :: value + if (allocated(value)) value = value // '!!!' +end subroutine grow +``` + +```python +print(grow("ab")) # ab!!! +``` + +The Python string you pass is never modified; the reallocated value comes back +as the result. A procedure that deallocates the dummy returns `None`, which is +how you tell an unallocated result from an empty string: + +```python +print(drop("abc")) # None +print(repr(empty_out("abc"))) # '' +``` + +### Pointer Dummies And Native Storage + +A `pointer` dummy needs an associated actual argument, so PRIK allocates a +target for the call. What happens to that target afterwards is the native +procedure's decision, and PRIK follows it: + +| The native procedure… | Python receives | PRIK's target | +| --- | --- | --- | +| writes through the pointer | the edited value | freed after the call | +| leaves it alone | the value passed in | freed after the call | +| deallocates it | `None` | already freed; not freed again | +| nullifies it | `None` | orphaned by the procedure | +| reassociates it elsewhere | the new target's value | orphaned by the procedure | + +PRIK copies the value out of whatever the dummy ends up holding and never frees +native storage, because it cannot know whether that storage is a static target, +a fresh allocation, or something the library still owns. Two consequences are +worth planning for: a procedure that reassociates or nullifies the dummy +orphans the target PRIK allocated for that call, and a procedure that returns a +freshly allocated pointer each call leaks unless it also frees it. Prefer an +`allocatable` dummy, whose release is unambiguous, when you control the Fortran +side. + +### Spelling Them In A Contract + +In a semantic `.pyi` contract, the attribute is a `native_call` projection and +the length is the first subscription after `String`: + +| Contract | Fortran | +| --- | --- | +| `String` | `character(len=*)` — the caller fixes the length | +| `String[8]` | `character(len=8)` — exactly eight encoded bytes | +| `String[:]` | `character(len=:)` — the length comes from allocation | + +So the procedure above generates: + +```python +@native_call([Allocatable(Arg(0))]) +def grow(value: String[:] | None) -> Returns["value", String[:]] | None: ... +``` + +`Allocatable(...)` and `Pointer(...)` carry the attribute, and they wrap the +argument, the projected output, or the result: + +```python +@native_call([Pointer(Arg(0))]) +def edit(value: String[4] | None) -> Returns["value", String[4]] | None: ... + +@native_call([], result=Allocatable(Return(0))) +def build() -> String[:] | None: ... +``` + +Arrays keep the length in that same first slot and add their shape second, as in +`String[8][:]` or `Allocatable[String[:][:]]`. Keep the native call and +storage declarations accurate when editing; [Calls and +Results](../reference/pyi-contracts/calls-and-results.md) explains the shared +argument and result rules. ## Next diff --git a/docs/user/guide/wrapping-derived-types.md b/docs/user/guide/wrapping-derived-types.md index eb512884f..a96cb16bb 100644 --- a/docs/user/guide/wrapping-derived-types.md +++ b/docs/user/guide/wrapping-derived-types.md @@ -224,6 +224,45 @@ print(points.point.__init__.__doc__) --- +## Which Constructor You Get + +The Fortran source decides which constructor the generated class publishes: + +| Source | Generated Python constructor | +| --- | --- | +| No user constructor | Keyword-field `__init__` over the public components | +| `interface ` present | Overloaded `__init__` from its specific functions | +| Edited `.pyi` | Exactly what the contract declares | + +An interface named for a derived type is that type's constructor, so its +specifics become the accepted signatures: + +```fortran +type, public :: box + integer(4) :: count = 0 + real(8) :: value = 0.0d0 +end type box + +interface box + module procedure box_empty, box_from_count, box_from_value +end interface box +``` + +```python +box() # box_empty +box(np.int32(7)) # box_from_count +box(np.float64(2.5)) # box_from_value +box("unsupported") # TypeError: no matching overload for __init__ +``` + +Each specific may be `private` in its module — the type name is public and +resolves to the same procedure, so the generated wrapper calls through it. + +When a constructor interface exists it replaces the keyword-field form, and the +generated contract states only the signatures the class actually accepts. + +--- + ## Custom Constructor The default constructor assigns public fields directly. If the native module @@ -318,6 +357,40 @@ item.move(np.float64(2.0), np.float64(3.0)) To expose only the method, import `private` and add `@private` to the module-level declaration. +## What The Source Already Hides + +prik reads the accessibility a type declares and does not publish what the type +keeps to itself, so a contract is not needed to hide internals: + +```fortran +module solver + implicit none + private ! module default + + type,public :: state ! exported despite the module default + private ! components default to private + real(8) :: work(8) = 0.0d0 ! internal, not a Python attribute + integer(4),public :: steps = 0 + contains + private ! bindings default to private + procedure :: advance_once ! internal, not a Python method + procedure,public :: run => advance_once + end type state +end module solver +``` + +The generated `state` class exposes `steps` and `run` only. Each rule is the +Fortran one: + +| Declaration | Effect on the Python class | +| --- | --- | +| `type, public ::` | Exported, even when the module defaults to `private` | +| `type, private ::` | Not exported, even when the module defaults to `public` | +| `private` before `contains` | Components default to hidden | +| `private` after `contains` | Type-bound procedures default to hidden | +| `integer, public ::` on a component | Published regardless of the type default | +| `procedure, public ::` on a binding | Published regardless of the type default | + The class docstring now lists `move(dx, dy) -> None` under `Methods`. `points.point.move.__doc__` contains its complete parameter and return details. @@ -398,6 +471,60 @@ and unlimited polymorphism (`class(*)`) are not supported. --- +## Abstract Types And Deferred Bindings + +A `type, abstract ::` declaration has no instances, so its Python class has no +constructor. Its extensions are ordinary Python subclasses, and a deferred +binding resolves through the object you actually hold. + +```fortran +type, public, abstract :: shape_base + private + integer(4) :: sides = 0 +contains + private + procedure(area_interface), deferred, public :: area + procedure, public, non_overridable :: side_count => shape_side_count +end type shape_base + +type, extends(shape_base), public :: circle + real(8) :: radius = 1.0d0 +contains + procedure, public :: area => circle_area +end type circle +``` + +```python +import numpy as np +import shapes.abstract_hierarchy as shapes + +shapes.shape_base() +# TypeError: shape_base is an abstract native type and cannot be instantiated; +# create one of its concrete extensions instead + +circle = shapes.circle(radius=np.float64(2.0)) +print(circle.area()) # 12.566370614 +print(circle.side_count()) # 0, from the abstract base +print(isinstance(circle, shapes.shape_base)) # True +``` + +The rules follow the Fortran declaration: + +| Fortran | Python | +| --- | --- | +| `type, abstract ::` | Class with no constructor; instantiating it raises `TypeError` | +| `type, extends(base) ::` | Subclass of the base's generated class | +| `procedure(iface), deferred ::` | Declared on the base, resolved by the object's own type | +| `procedure, non_overridable ::` | Ordinary inherited method | +| Component of an abstract type | Reached through the extension that inherits it | + +A deferred binding needs no Python-side dispatch: the generated adapter converts +the object's address to its own concrete type and lets Fortran resolve the +override. The same applies when a procedure takes `class(base)` — the boundary +is still limited to required scalar inputs, as above. + +--- + ## Type-Bound Generics A type-bound generic groups several concrete methods under one Python method. diff --git a/docs/user/guide/wrapping-subroutines.md b/docs/user/guide/wrapping-subroutines.md index e9a67368a..c6b630569 100644 --- a/docs/user/guide/wrapping-subroutines.md +++ b/docs/user/guide/wrapping-subroutines.md @@ -28,12 +28,58 @@ change in place. | 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 | +| No `intent`, assumed input | Visible argument | Not returned (opt-in, see below) | Without `intent`, prik 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. This is common in legacy sources, but the rule applies to -any dummy declaration without `intent`. +scalar stays visible and its replacement value is returned — `character` +scalars included, on the same terms as numeric ones. This is common in legacy +sources, but the rule applies to any dummy declaration without `intent`. + +Two ways to drop a result you know the native procedure never writes: + +- remove that projected result from the generated contract, one dummy at a + time; or +- pass `--assume-intent-in-scalars`, which applies the same choice to every + scalar in the build that declares no `intent`. + +### `--assume-intent-in-scalars` + +`intent` did not exist before Fortran 90, so a fixed-form source cannot declare +it and its absence carries no information about the procedure. This option lets +you say so: + +```bash +python3 -m prik ddot.f --out blas --assume-intent-in-scalars +``` + +```python +# default ddot(...) -> tuple[float64, int32, int32, int32] +# --assume-intent-in-scalars ddot(...) -> float64 +``` + +The option is an assertion you make about the source, not a fact prik derives +from it. prik does not inspect the procedure body, so a procedure that *does* +write such a dummy silently loses that value, exactly as it would if you +removed the result from the contract by hand. Use it on sources whose scalar +arguments are known controls; leave it off when you are not sure. + +It is deliberately narrow: + +| Declaration | Effect | +| --- | --- | +| Primitive scalar with no `intent` | Treated as `intent(in)`; not returned | +| `character` scalar with no `intent` | Treated as `intent(in)`; not returned | +| Any declared `intent` | Unchanged — a declared `intent` always wins | +| Array with no `intent` | Unchanged — still mutated in place, never returned | +| Derived-type object with no `intent` | Unchanged — still mutated in place | +| Allocatable or pointer scalar with no `intent` | Unchanged — its result is a nullable snapshot, not a replacement | + +Every command that produces semantic IR accepts the option — the build, +`generate --pyi`, and `semantics` — because it changes how a missing `intent` +is read rather than how the wrapper is emitted. A contract generated with the +option and a direct build with the option therefore describe the same Python +surface. A `.pyi` wrapper build rejects it: a contract already states its own +results, so edit the contract there instead. --- diff --git a/docs/user/index.md b/docs/user/index.md index 12413020e..2abe8fcc7 100644 --- a/docs/user/index.md +++ b/docs/user/index.md @@ -9,9 +9,9 @@ publication: reviewed # User Documentation -PRIK is the Python Runtime Interop Kit. Use these pages to install PRIK, verify your environment, build your first -Fortran wrappers, and understand the supported behavior of generated Python -extensions. +PRIK is the Python Runtime Interop Kit. Use these pages to install PRIK, verify +your environment, build Fortran and C wrappers, and understand the behavior of +generated Python extensions. Current C coverage is documented in C Support. ## Start Here @@ -24,3 +24,15 @@ standalone wrapper, the first module wrapper, and the beginner edit-build-test loop. The User Guide covers supported Fortran wrapper features, runtime behavior, and extension builds. Performance presents the reproducible PRIK and f2py comparison. + +## Then + +- [Language Support](language-support/index.md) — C and Fortran feature + coverage, including the evidence behind each claim. +- [Reference](reference/index.md) — the exact CLI, Python API, generated-wrapper, + and `.pyi` contract surfaces. +- [Examples](examples/index.md) — complete wrappers for BLAS, LAPACK, FFTPACK, + and MINPACK. +- [Troubleshooting](troubleshooting/compiler-issues.md) — compiler detection, + selection, and toolchain problems. +- [FAQ](faq/index.md) — short answers to common questions. diff --git a/docs/user/language-support/c-support.md b/docs/user/language-support/c-support.md new file mode 100644 index 000000000..f63e616a0 --- /dev/null +++ b/docs/user/language-support/c-support.md @@ -0,0 +1,866 @@ +--- +title: C Support +description: Build supported C APIs as NumPy-aware Python extensions. +audience: users +prerequisites: installation, basic Python and NumPy +related: index.md, feature-matrix.md, ../reference/cli-commands.md, ../reference/python-api.md, ../reference/pyi-contracts/calls-and-results.md +status: maintained +publication: reviewed +--- + +# C Support + +PRIK builds a supported subset of C APIs as importable Python extensions. The +generated binding calls your exported C symbol directly; there is no generated +C or Fortran adapter in between. + +The C lane is best for standalone numerical functions with primitive values, +NumPy buffers, and explicit output storage. It is deliberately fail-closed: +parsing a declaration does not promise that it can be wrapped, and an unsupported +form stops the build before native compilation. + +## Requirements + +Install PRIK and NumPy, then make sure a C compiler and the development headers +for the Python that will import the extension are available. `cc` is the default +compiler; use `--compiler` when the native project requires another one. + +To see the C types and NumPy dtypes selected for a particular compiler target, +run: + +```bash +python3 -m prik probe --language c --compiler cc +``` + +## Build a scalar C function + +This first example is source-driven: PRIK reads the C declaration, builds the +extension, and writes an editable contract alongside it. + +
+
+ + + +
+ +
+ +Create `native_math.c`: + +```c +double add(double left, double right) { + return left + right; +} +``` + +Build it with an explicit language selection: + +```bash +python3 -m prik --language c native_math.c \ + --compiler cc \ + --out native_math \ + --out-dir build +``` + +
+ +
+ +PRIK writes `build/contracts/native_math.pyi`: + +```python +from prik.contracts import Float64 + +def add(left: Float64, right: Float64) -> Float64: ... +``` + +To inspect the contract without compiling, run: + +```bash +python3 -m prik generate --pyi --language c native_math.c --out native_math.pyi +``` + +
+ +
+ +Then import and call the extension: + +```python +import sys + +import numpy as np + +sys.path.insert(0, "build") +import native_math + +print(native_math.add(np.float64(3.0), np.float64(2.5))) +``` + +```text +5.5 +``` + +
+
+ +PRIK validates arithmetic arguments at the native boundary. Pass the matching +NumPy scalar—for example, `np.float64` for a C `double`. + +The source build writes an editable semantic `.pyi` contract beside the +extension. Use that contract when a pointer needs a more precise Python meaning +than the C declaration can express. + +## Author a contract for pointers and arrays + +C syntax cannot tell whether `double *` means one scalar or the first element +of an array. A source-generated contract therefore starts conservatively. When +the parameter is a NumPy buffer, state the shape and native call order in an +authored `.pyi` contract. + +
+
+ + + +
+ +
+ +Create `scale.c`: + +```c +#include + +void scale(size_t count, double *values) { + for (size_t index = 0; index < count; ++index) { + values[index] *= 2.0; + } +} +``` + +
+ +
+ +Create `scale.pyi`: + +```python +from prik.contracts import Arg, Float64, native_call + +@native_call([Arg(0).shape[0], Arg(0)]) +def scale(values: Float64[:]) -> None: ... +``` + +`Arg(0).shape[0]` provides `count`; `Arg(0)` passes the NumPy buffer to +`double *values`. + +```bash +python3 -m prik --language c scale.pyi \ + --native-c-sources scale.c \ + --compiler cc \ + --out scale \ + --out-dir build +``` + +
+ +
+ +```python +import sys + +import numpy as np + +sys.path.insert(0, "build") +import scale + +values = np.array([1.0, 2.0, 3.0], dtype=np.float64) +scale.scale(values) +print(values) +``` + +```text +[2. 4. 6.] +``` + +
+
+ +Supported arrays have ranks 1 through 15, primitive non-Boolean elements, and +C-contiguous NumPy storage. PRIK validates dtype, rank, shape, layout, and +writeability before calling C. + +Use `Float64[()]` when the caller should provide one writable scalar slot. + +### Choose the pointer contract + +`Arg(i)` uses the annotation's normal C representation: a bare numeric scalar +crosses by value, while rank-zero and array storage cross by address. Use +`Addr(Arg(i))` only when a bare scalar must become a C pointer. + +| C parameter | Python contract | `@native_call` entry | Native effect | +| --- | --- | --- | --- | +| `double value` | `value: Float64` | `Arg(0)` (or omit `@native_call`) | Passes `double` by value. | +| `double *value` | `value: Float64` | `Addr(Arg(0))` | Passes the address of call-local scalar storage; mutation is discarded unless returned. | +| `double *value` | `value: Float64[()]` | `Arg(0)` (or omit `@native_call`) | Passes the caller's zero-dimensional NumPy storage address; mutation is visible in place. | +| `double *values` | `values: Float64[:]`, `Float64[4]`, or `Float64[n]` | `Arg(0)` | Passes the validated C-contiguous NumPy data address. | + +For an authored scalar read-back, write the address projection and return the +call-local value explicitly: + +```python +from prik.contracts import Addr, Arg, Float64, Returns, native_call + +@native_call([Addr(Arg(0))]) +def scale_scalar(value: Float64) -> Returns["value", Float64]: ... +``` + +A source-generated contract for `double *value` already contains this +`Addr(Arg(0))` projection. Do not wrap `Float64[()]` or an array in `Addr(...)`: +their normal native representation is already an address. + +Do not leave a pointer as a scalar when C indexes it as an array. A generated +source contract is conservative; promote the parameter to a shaped NumPy array +before calling a buffer API. + +An authored contract is authoritative. If the source C declaration is +`const T *`, do not author writable storage or write-back through it: writing +through a const-qualified C pointer is undefined behavior. + +## Rename, reorder, and address arguments + +An authored contract can present an existing C ABI under a better Python name +and argument order. It names the real C symbol, then states each native +argument explicitly. + +When the Python declaration and C symbol have the same name, omit `@bind`: +that name is the default native target. Use `@bind("native_name")` only for a +different C symbol. The same default applies to Fortran semantic contracts. + +
+
+ + + +
+ +
+ +Create `projected.c`: + +```c +int combine_native(int right, int *left, int bias) { + return 100 * right + 10 * *left + bias; +} + +void read_status(int value, int *output) { + *output = value + 1; +} +``` + +
+ +
+ +Create `projected.pyi`: + +```python +from prik.contracts import Addr, Arg, Int32, Return, bind, native_call + +@bind("combine_native") +@native_call([Arg(1), Addr(Arg(0)), Int32(5)]) +def combine(left: Int32, right: Int32) -> Int32: ... + +@bind("read_status") +@native_call([Arg(0), Return("output", 0)]) +def status(value: Int32) -> Int32: ... +``` + +`combine` is the Python name, `combine_native` is the linked C symbol, +`Addr(Arg(0))` passes the address of `left`, and `Int32(5)` supplies the literal +third native argument. `Return(...)` turns the output pointer into the Python +result. + +```bash +python3 -m prik --language c projected.pyi \ + --native-c-sources projected.c \ + --compiler cc \ + --out projected \ + --out-dir build +``` + +
+ +
+ +```python +import sys + +import numpy as np + +sys.path.insert(0, "build") +import projected + +print(projected.combine(np.int32(2), np.int32(3))) +print(projected.status(np.int32(7))) +``` + +```text +325 +8 +``` + +
+
+ +## Return several C outputs + +Use a named `Return(...)` slot for every native output pointer that should +become part of the Python return value. + +
+
+ + + +
+ +
+ +Create `stats.c`: + +```c +#include + +void stats_compute(size_t count, const double *values, double *mean, double *total) { + double sum = 0.0; + for (size_t index = 0; index < count; ++index) { + sum += values[index]; + } + *total = sum; + *mean = count ? sum / (double)count : 0.0; +} +``` + +
+ +
+ +Create `stats.pyi`: + +```python +from prik.contracts import Arg, Float64, Return, Returns, bind, native_call + +@bind("stats_compute") +@native_call([Arg(0).shape[0], Arg(0), Return("mean", 0), Return("total", 1)]) +def summarize(values: Float64[:]) -> tuple[Returns["mean", Float64], Returns["total", Float64]]: ... +``` + +```bash +python3 -m prik --language c stats.pyi \ + --native-c-sources stats.c \ + --compiler cc \ + --out stats \ + --out-dir build +``` + +
+ +
+ +```python +import sys + +import numpy as np + +sys.path.insert(0, "build") +import stats + +mean, total = stats.summarize(np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64)) +print(mean, total) +``` + +```text +2.5 10.0 +``` + +
+
+ +See [Calls and Results](../reference/pyi-contracts/calls-and-results.md) for +the full shared contract vocabulary. + +## Pass C strings + +Choose the string contract from what the C function does with the pointer: + +| C parameter | Contract | Python value | +| --- | --- | --- | +| Read-only `const char *` | `String` | Python `str` | +| Writable `char *` | `String[n][()]` or `String[...][()]` | Rank-zero NumPy `S` array | + +`String` borrows the UTF-8 buffer of the Python `str`, which CPython +NUL-terminates. The native function must not write through it. For writable +storage, use a caller-owned NumPy bytes array. A stated capacity such as +`String[32][()]` also checks the array itemsize; `String[...][()]` accepts the +itemsize the caller supplies. + +
+
+ + + +
+ +
+ +Create `text.c`: + +```c +#include +#include + +int name_length(const char *text) { + return (int)strlen(text); +} + +void shout(const char *text, char *out) { + size_t index = 0; + for (; text[index]; ++index) { + char value = text[index]; + out[index] = (value >= 'a' && value <= 'z') ? (char)(value - 32) : value; + } + out[index] = '\0'; +} +``` + +
+ +
+ +Create `text.pyi`: + +```python +from prik.contracts import Int32, String + +def name_length(text: String) -> Int32: ... + +def shout(text: String, out: String[32][()]) -> None: ... +``` + +```bash +python3 -m prik --language c text.pyi \ + --native-c-sources text.c \ + --compiler cc \ + --out text \ + --out-dir build +``` + +
+ +
+ +```python +import sys + +import numpy as np + +sys.path.insert(0, "build") +import text + +print(text.name_length("hello")) +buffer = np.array(b"", dtype="S32") +text.shout("hello", buffer) +print(buffer[()]) +``` + +```text +5 +b'HELLO' +``` + +
+
+ +When C uses an explicit byte length, pass it with `Len(Arg(i))` in +`@native_call(...)`. The contract does not impose a terminator convention of +its own. + +## Hide native outputs and raise Python exceptions + +Use `Hidden(name, T)` for C output storage that Python never returns. This is +particularly useful for status values and diagnostic messages consumed by +`@raises`. + +
+
+ + + +
+ +
+ +Create `checked.c`: + +```c +#include + +void checked_sqrt(double value, double *root, int *status, char *message) { + if (value < 0.0) { + *status = -1; + *root = 0.0; + strcpy(message, "value must not be negative"); + return; + } + *status = 0; + message[0] = '\0'; + *root = value == 4.0 ? 2.0 : value; +} +``` + +
+ +
+ +Create `checked.pyi`: + +```python +from prik.contracts import Arg, Float64, Hidden, Int32, Return, Returns, String, native_call, raises + +@raises(status="status", message="message", success=0) +@native_call([Arg(0), Return("root", 0), Hidden("status", Int32), Hidden("message", String[64])]) +def checked_sqrt(value: Float64) -> Returns["root", Float64]: ... +``` + +```bash +python3 -m prik --language c checked.pyi \ + --native-c-sources checked.c \ + --compiler cc \ + --out checked \ + --out-dir build +``` + +
+ +
+ +```python +import sys + +import numpy as np + +sys.path.insert(0, "build") +import checked + +print(checked.checked_sqrt(np.float64(4.0))) +try: + checked.checked_sqrt(np.float64(-1.0)) +except RuntimeError as error: + print(error) +``` + +```text +2.0 +value must not be negative +``` + +
+
+ +The function returns only `root`; `status` and `message` become a +`RuntimeError` on failure. A hidden message needs a fixed capacity because PRIK +allocates the native buffer. + +A visible message buffer is also valid when the caller owns it: + +```python +@raises(status="status", message="message", success=0) +@native_call([Arg(0), Arg(1), Hidden("status", Int32)]) +def checked(value: Float64, message: String[64][()]) -> None: ... +``` + +Here `message` is a rank-zero `np.ndarray` with dtype `S64`; the caller can +inspect it after the exception. `String` can also name a visible message when +the C API declares `const char *`; that borrows a Python `str`. If that native +code writes through the borrowed pointer, handling that unsafe contract is the +C API author's responsibility. Prefer NumPy storage for a writable message. + +## Present several C symbols as one Python name + +An authored contract can dispatch supported dtype/rank variants behind one +Python name. Mark the concrete candidates `@private`, then name them with +`@overload(...)`. + +
+
+ + + +
+ +
+ +Create `overloads.c`: + +```c +int scale_integer(int value) { return value * 2; } + +double scale_real(double value) { return value * 2.0; } +``` + +
+ +
+ +Create `overloads.pyi`: + +```python +from prik.contracts import Float64, Int32, overload, private + +@private +def scale_integer(value: Int32) -> Int32: ... + +@private +def scale_real(value: Float64) -> Float64: ... + +@overload("scale_integer") +def scale(value: Int32) -> Int32: ... + +@overload("scale_real") +def scale(value: Float64) -> Float64: ... +``` + +```bash +python3 -m prik --language c overloads.pyi \ + --native-c-sources overloads.c \ + --compiler cc \ + --out overloads \ + --out-dir build +``` + +
+ +
+ +```python +import sys + +import numpy as np + +sys.path.insert(0, "build") +import overloads + +print(overloads.scale(np.int32(21))) +print(overloads.scale(np.float64(1.5))) +print([name for name in dir(overloads) if not name.startswith("_")]) +``` + +```text +42 +3.0 +['scale'] +``` + +
+
+ +Candidates must remain distinguishable by their supported dtype and rank. + +## What is supported + +- Externally linked functions with `void`, arithmetic scalars, and C99 complex + values whose ABI the selected compiler can probe. +- One-level primitive pointer parameters, expressed as a scalar address, + rank-zero NumPy storage, a projected result, or a C-contiguous NumPy array. +- Rank-zero C string inputs and storage, hidden outputs, status projection, + symbol renaming, reordered arguments, typed literals, and derived lengths or + shapes. +- Overload sets whose candidates are distinguishable by supported dtype and + rank. +- `@nogil` calls that do not access Python state. +- Ordinary compiler preprocessing, including standard includes and macros. + +## Qualifiers and compiler attributes + +Use C qualifiers as constraints when authoring a contract: `const T *` must not +be presented as writable NumPy storage. `const` and `restrict` themselves do +not add a separate Python type or calling convention. + +Common non-ABI attributes, such as `deprecated` and `warn_unused_result`, do +not change a wrapper. An attribute that may change the ABI, symbol identity, or +layout—such as a calling convention or alignment attribute—stops the build +instead of being ignored. + +Compiler-preprocessed system headers may define an unavailable extended +floating spelling, such as `_Float32`, through a compatibility `typedef`. +PRIK accepts those declarations as parsing context so that an unrelated private +header declaration does not block a reviewed public surface. This tolerance +does not add direct-wrapper support for the extended floating type itself. +Prototype parameters may also omit their names: a declaration such as +`long rinttol(double)` remains a modern prototype and is not treated as a K&R +definition. Actual K&R definitions remain unsupported. + +## Exact native scalar identities + +Generated C contracts are target-specific and representation-based. Distinct C +types such as `long` and `long long` may therefore use the same public NumPy +contract type. When their exact identity matters to the call, generation keeps +it as a sparse operator inside `@native_call(...)`: + +```python +from prik.contracts import Arg, CLongLong, Float64, Int64, Return, native_call + +@native_call([Arg(0)], result=CLongLong(Return(0))) +def llround(value: Float64) -> Int64: ... +``` + +The public signature continues to use ordinary NumPy contract types. Scalars +and scalar addresses accept exactly that public dtype and are converted +directionally; the native C spelling does not add a second accepted Python +scalar type. Ranked arguments instead require the corresponding exact NumPy +element storage so the pointer path remains zero-copy. See +[Calls and Results: Preserve an Exact C Scalar at the Native +Call](../reference/pyi-contracts/calls-and-results.md#preserve-an-exact-c-scalar-at-the-native-call) +for arguments, addresses, results, arrays, and the supported exact-storage +rules. + +## Symbols your binding's own headers declare + +Exact native scalar casts make compatible duplicate declarations harmless, but +they cannot resolve a genuine identifier collision: a header included by the +binding may already declare the same name for a different API. Name that symbol +to isolate it from `Python.h`: + +```bash +python3 -m prik --language c vendor.pyi \ + --native-library vendor \ + --collision-adapter evaluate \ + --out vendor_api --out-dir build +``` + +The build writes a separate adapter translation unit that includes no Python +header. Its signature is reconstructed from the completed exact native C types +and it only forwards to the original symbol: + +```c +long long evaluate(double x); + +long long prik_collision_adapter_evaluate(double x) { + return (evaluate)(x); +} +``` + +The adapter targets a real function symbol; PRIK does not expose macros. The +forwarder has hidden visibility, so it is not part of the extension's exported +ABI. It is correct with or without the shared `--lto` build optimization. Use +`--collision-adapter-all` to adapt every eligible function instead of naming +each one. Only C-source functions are eligible; generated Fortran bridge +symbols and Fortran `bind(C)` procedures are not. + +This isolates a declaration collision inside the binding translation unit. It +does not choose between two different linked libraries that both export the +same external symbol; normal target linker and loader resolution must already +select the intended implementation. + +A source-free `.pyi` must preserve every exact native scalar identity needed by +the declaration. A target-generated contract does this automatically; an +edited contract uses the same `@native_call` operators explicitly. See [CLI +Commands](../reference/cli-commands.md#wrapper-builds) for complete selection, +validation, and LTO behavior. + +## Current limits + +PRIK rejects these forms rather than guessing their ABI or memory contract: + +- callbacks and function pointers; `struct`, `union`, and C global-state + wrappers; and enum constants; +- variadic functions, `static` symbols, unsupported calling conventions, + `volatile`, and `_Atomic` values; +- pointer results, multi-level pointers, raw or nullable pointers, and APIs + with retained or ownership-sensitive pointers; +- arrays of strings, Boolean arrays, native C array declarators, arrays outside + ranks 1–15, and Fortran-ordered C arrays. + +For a feature-by-feature view, see the [language support +matrix](feature-matrix.md). The C parser can inspect a broader set of +declarations than this runtime lane; use its output to understand source, not +as a build promise. + +## Build and inspect APIs + +The examples above use the CLI. For application and test code, use +`build_c_extension()` for source builds or `build_pyi_extension()` for authored +contracts, then import the returned `WrapperBuildResult`. See the +[Python API](../reference/python-api.md) for those calls and [CLI +Commands](../reference/cli-commands.md) for build, generation, Makefile, and +inspection options. + +### Native dependencies + +Pass public C source files as positional inputs. Add implementation-only C +files with `--native-c-sources`, compiler flags with +`--native-c-compile-flags`, existing objects with `--native-objects`, and +libraries with `--native-library` and `--native-library-dir`. These complete +the native link without becoming Python API declarations. + +For headers and conditional source, pass the same preprocessing information as +the native project: `-I`, `-D`, `--std`, and, when available, +`--compile-commands build/compile_commands.json`. + +To wrap a reviewed subset of a broad or system header, keep included files +private and select the exact reachable functions from a file: + +```bash +python3 -m prik generate --pyi --language c api_probe.h \ + --include-exposure roots-only \ + --export-symbols reviewed_functions.txt \ + --out contracts/api.pyi +``` + +The export file names the reviewed functions that become public, including +functions declared by an otherwise-private system header. Every unlisted +declaration is excluded. This selects the semantic API rather than linker +exports: selected functions still need native link inputs and a signature the +direct C lane supports. See [CLI Commands: C include +exposure](../reference/cli-commands.md#c-include-exposure) for the file format +and fail-closed validation rules. + +The Python build API accepts the already-resolved names instead of a CLI text +file: + +```python +build = build_c_extension( + "api_probe.c", + export_symbols=("evaluate", "normalize"), + native_libraries=("vendor",), +) +``` + +### Inspect a broader C API + +The C parser and contract generator accept more syntax than the direct wrapper +lane. Use them to examine declarations, not as a promise that each declaration +can be built: + +```bash +python3 -m prik parse --language c include/library.h --json +python3 -m prik semantics --language c include/library.h +python3 -m prik generate --pyi --language c include/library.h --out contracts/library.pyi +``` + +For a project header that needs its normal preprocessing configuration: + +```bash +python3 -m prik parse --language c include/library.h \ + -I include \ + -D LIBRARY_ENABLE_FAST=1 \ + --std c11 \ + --compile-commands build/compile_commands.json +``` + +Only declarations in the wrapped translation unit become a source build's +public API; headers supply declarations and preprocessing context. For the +broader Fortran wrapper surface, start with the [User Guide](../guide/index.md). diff --git a/docs/user/language-support/feature-matrix.md b/docs/user/language-support/feature-matrix.md index d09eb6bdf..e01daf634 100644 --- a/docs/user/language-support/feature-matrix.md +++ b/docs/user/language-support/feature-matrix.md @@ -1,24 +1,49 @@ --- title: Language Feature Matrix -audience: users, developers -prerequisites: user guide -related: ../guide/index.md, ../reference/fortran-wrapper.md +audience: users +prerequisites: getting started +related: index.md, c-support.md, ../reference/cli-commands.md, ../reference/diagnostic-codes.md status: maintained -publication: draft +publication: reviewed --- # Language Feature Matrix This matrix is the user-facing support index for native-language features. It -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. +points each feature to its user guide, implementation route, evidence, and +limitations. Start with [C Support](c-support.md) for the complete direct-C +workflow, or the [User Guide](../guide/index.md) for the broader Fortran +workflow. A row may claim support only when the linked evidence proves that behavior in the current repository. Runtime wrapper support requires compiled, imported, and called wrapper tests. Parser or semantic support alone is listed as inspection-only or partial support. +## At A Glance + +**Fortran wrapping works end to end** for scalars, arrays, strings, functions, +subroutines, modules, derived types, and module state. Build from source with +one command, or edit the generated `.pyi` contract to reshape the Python API +without changing the native code. **C wrapping is supported too**; its current +direct-ABI coverage is documented in [C Support](c-support.md). + +| You want to wrap | Status | +| --- | --- | +| Scalar arguments and results, all documented kinds | Supported | +| NumPy arrays — rank, shape, layout, strides, in-place mutation | Supported | +| Functions, subroutines, modules, module variables and constants | Supported | +| Derived types with fields, methods, constructors, finalizers | Supported | +| Optional arguments, generic interfaces, defined operators | Supported | +| Fixed-width character strings | Supported | +| Python callbacks passed into Fortran | Supported, call-scoped only | +| Allocatable arrays and pointer arrays | Supported / partially supported | +| Arrays of derived types, procedure pointers, `class(*)` | Unsupported | +| Target-probed primitive C APIs | Supported, direct-only lane | + +The detailed rows below add the owning docs, source route, evidence, and exact +limitation for each feature. + ## Status Meanings | Status | Meaning | @@ -35,68 +60,63 @@ inspection-only or partial support. | --- | --- | --- | --- | --- | --- | | Scalar functions, subroutines, and baseline arrays | Supported | [Functions](../guide/wrapping-functions.md), [subroutines](../guide/wrapping-subroutines.md) | [Wrapper pipeline](../../developer/architecture.md#build-architecture) | [Verified baseline tests](../../../tests/fortran/data_types/end_to_end/test_verified_baseline.py) | Native scalar arguments require exact NumPy dtypes where documented. | | Generic procedure interfaces | Supported | [Generic interfaces](../guide/generic-interfaces.md) | [Feature route](../../developer/feature-to-code-map.md#feature-routes) | [Generic interface tests](../../../tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py) | Defined operators and assignment are tracked separately. | -| Defined operators and assignment overloads | Supported | [Defined operators](../guide/generic-interfaces.md#defined-operators) | [Bridge and binding generation](../../developer/codebase-map.md#cross-stage-hotspots) | [Defined operator tests](../../../tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py) | Supported operators are those covered by the wrapper guide and runtime tests. | -| Output arguments and multiple results | Supported | [Subroutine projection](../guide/wrapping-subroutines.md) | [Ownership and lowering](../../developer/codebase-map.md#cross-stage-hotspots) | [Calls and results tests](../../../tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py), [function result tests](../../../tests/fortran/functions/end_to_end/test_documented_function_journeys.py) | Tuple ordering and caller-provided array behavior follow the wrapper guide. | +| Defined operators and assignment overloads | Supported | [Defined operators](../guide/generic-interfaces.md) | [Bridge and binding generation](../../developer/codebase-map.md#cross-stage-hotspots) | [Defined operator tests](../../../tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py) | Supported operators are those covered by the wrapper guide and runtime tests. | +| Output arguments and multiple results | Supported | [Subroutine projection](../guide/wrapping-subroutines.md) | [Ownership and lowering](../../developer/codebase-map.md#cross-stage-hotspots) | [Calls and results tests](../../../tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py), [function result tests](../../../tests/fortran/functions/end_to_end/test_documented_function_journeys.py) | Tuple ordering and caller-provided array behavior follow the wrapper guide. | | Optional arguments | Supported | [Optional arguments](../guide/optional-arguments.md) | [Binding generation](../../developer/codebase-map.md#cross-stage-hotspots) | [Optional argument tests](../../../tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py) | Unsupported optional combinations fail during wrapper planning. | | Allocatable array handles, descriptor arguments, and owned results | Supported | [Allocatables](../guide/allocatables.md) | [Ownership policy](../../developer/codebase-map.md#cross-stage-hotspots) | [Allocatable runtime tests](../../../tests/fortran/allocatables/end_to_end/test_allocatable_handles.py), [scalar-derived matrix tests](../../../tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py) | Array module/field handles borrow their owner; result handles own persistent descriptor storage. Wrapper-owned scalar-derived allocatables use typed holders; module scalar allocatables use reversible `move_alloc` transactions for compatible dummies. | | Pointer scalar projections and array handles | Partially supported | [Pointers](../guide/pointers.md) | [Ownership policy](../../developer/codebase-map.md#cross-stage-hotspots) | [Pointer handle tests](../../../tests/fortran/pointers/end_to_end/test_pointer_handles.py), [pointer policy tests](../../../tests/fortran/pointers/policy/test_pointer_ownership_policy.py), [scalar-derived matrix tests](../../../tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py) | Descriptor arguments, module/field handles, strided views, wrapper-owned pointer-array results and outputs, scalar-derived pointer holders, and module pointer reassociation transactions are supported. Target deallocation and writable reassociation remain policy-gated. | -| Array-valued function results | Supported | [Array results](../guide/arrays.md#array-results) | [Array lowering](../../developer/codebase-map.md#cross-stage-hotspots) | [Array result tests](../../../tests/fortran/arrays/end_to_end/test_array_results.py) | Ownership and dtype/shape behavior are limited to documented array result forms. | +| Array-valued function results | Supported | [Array results](../guide/arrays.md#mutation-and-results) | [Array lowering](../../developer/codebase-map.md#cross-stage-hotspots) | [Array result tests](../../../tests/fortran/arrays/end_to_end/test_array_results.py) | Ownership and dtype/shape behavior are limited to documented array result forms. | | NumPy array argument contracts | Supported | [Arrays](../guide/arrays.md) | [Bridge and binding generation](../../developer/codebase-map.md#cross-stage-hotspots) | [Array contract tests](../../../tests/fortran/arrays/end_to_end/test_array_contract_validation.py), [multidimensional tests](../../../tests/fortran/arrays/end_to_end/test_layout_and_strided_arrays.py) | Wrong dtype, rank, shape, contiguity, alignment, or mutability is rejected. | | Derived-type scalar boundaries and methods | Supported | [Derived types](../guide/wrapping-derived-types.md) | [Class lowering](../../developer/codebase-map.md#cross-stage-hotspots) | [Derived boundary tests](../../../tests/fortran/derived_types/end_to_end/test_derived_boundaries.py), [method tests](../../../tests/fortran/derived_types/end_to_end/test_type_bound_methods.py) | Derived-type arrays and some polymorphic forms are not included. | | Default and keyword constructors with finalizers | Supported | [Constructors and finalizers](../guide/wrapping-derived-types.md#key-concepts) | [Ownership policy](../../developer/codebase-map.md#cross-stage-hotspots) | [Constructor/finalizer tests](../../../tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py), [borrowed finalizer tests](../../../tests/fortran/derived_types/end_to_end/test_borrowed_components.py) | Construction commits ownership only after initialization; borrowed wrappers never run an owning finalizer. | -| Generic constructor interfaces and overloaded runtime initialization | Supported | [Constructors](../guide/wrapping-derived-types.md#custom-constructor) | [Class policy and lowering](../../developer/codebase-map.md#cross-stage-hotspots) | [Edited class surface tests](../../../tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py), [class policy tests](../../../tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.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#feature-routes) | [Module state tests](../../../tests/fortran/modules/end_to_end/test_module_variables_and_state.py), [scalar-derived matrix tests](../../../tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py), [common-block tests](../../../tests/fortran/modules/end_to_end/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. | +| Generic constructor interfaces and overloaded runtime initialization | Supported | [Constructors](../guide/wrapping-derived-types.md#custom-constructor) | [Class policy and lowering](../../developer/codebase-map.md#cross-stage-hotspots) | [Edited class surface tests](../../../tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py), [class policy tests](../../../tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.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#feature-routes) | [Module state tests](../../../tests/fortran/modules/end_to_end/test_module_variables_and_state.py), [scalar-derived matrix tests](../../../tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py), [common-block tests](../../../tests/fortran/modules/end_to_end/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. `character` module state is supported in every form: a declared-length scalar reads and writes as `str` at exactly its declared byte width, an `allocatable` or `pointer` scalar reads as a detached `str` or `None`, and arrays reach Python as fixed-width bytes. Only declared-length non-descriptor scalars are writable by assignment; descriptor scalars are read-only snapshots for numeric and `character` state alike, and arrays are mutated in place through their view or handle rather than rebound. | | Fortran enum constants | Supported | [Enumerations](../guide/enumerations.md) | [Semantic constants route](../../developer/codebase-map.md#cross-stage-hotspots) | [Enum runtime tests](../../../tests/fortran/enumerations/end_to_end/test_enum_runtime.py), [enum semantic tests](../../../tests/fortran/enumerations/semantics/test_enum_semantics.py), [enum diagnostics](../../../tests/fortran/enumerations/parsing/test_enum_diagnostics.py) | No Python `Enum` or `IntEnum` classes are generated. | -| Scalar character arguments, results, and fields | Supported | [Strings](../guide/strings.md) | [Character bridge route](../../developer/codebase-map.md#cross-stage-hotspots) | [Character argument tests](../../../tests/fortran/strings/end_to_end/test_character_boundaries.py), [edge-case tests](../../../tests/fortran/strings/end_to_end/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/codebase-map.md#cross-stage-hotspots) | [Scalar kind tests](../../../tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py) | Wider real, complex, and explicit logical storage is blocked without portable NumPy mapping. | -| Caller-ordered multi-source builds, Makefiles, verbose mode, and output placement | Supported | [Building the shared library](../guide/building-shared-library.md) | [Wrapper orchestration](../../developer/codebase-map.md#cross-stage-hotspots) | [Multi-source tests](../../../tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py), [compiler verbose tests](../../../tests/fortran/building_shared_library/compiling/test_compiler_verbose.py) | prik does not discover, reorder, or resolve all external source dependencies. | -| Visibility, naming, keyword escaping, and collision policy | Supported | [Visibility and naming](../reference/fortran-wrapper.md#visibility-naming-and-the-python-surface) | [Naming policy](../../developer/codebase-map.md#cross-stage-hotspots) | [Visibility/naming tests](../../../tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_naming.py) | Strict mode rejects names that default mode can normalize. | +| Scalar character arguments, results, and fields | Supported | [Strings](../guide/strings.md) | [Character bridge route](../../developer/codebase-map.md#cross-stage-hotspots) | [Character argument tests](../../../tests/fortran/strings/end_to_end/test_character_boundaries.py), [edge-case tests](../../../tests/fortran/strings/end_to_end/test_character_edge_cases.py) | Character arrays use fixed-width NumPy bytes dtype. Scalar `character` `allocatable` and `pointer` values are supported for `intent(in)`, `intent(out)`, `intent(inout)`, and function results, at deferred (`len=:`) and declared (`len=n`) length; a mutable dummy returns the value the procedure left behind, or `None`. prik copies out of native pointer storage and never frees it, so a procedure that allocates a fresh target per call leaks unless it frees its own. | +| Character arrays and caller-supplied deferred-length character storage | Supported | [Strings](../guide/strings.md) | [Character bridge route](../../developer/codebase-map.md#cross-stage-hotspots) | [Character edge tests](../../../tests/fortran/strings/end_to_end/test_character_edge_cases.py) | Character arrays use fixed-width NumPy bytes dtype, whose width each accessor reports from the Fortran declaration; Unicode/object arrays are unsupported. Scalar `character` `allocatable` and `pointer` values work for every intent and as function results. A mutable `pointer` dummy that the native procedure reassociates without deallocating orphans the target the adapter allocated for that call. A deferred-length `character(len=:), allocatable` module array does not build under GNU Fortran 11.4, which raises an internal compiler error on that declaration. | +| Scalar kind coverage | Supported | [Data types](../guide/data-types.md) | [Fortran type probe](../../developer/codebase-map.md#cross-stage-hotspots) | [Scalar kind tests](../../../tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py) | Real and complex storage wider than the target's `long double` is blocked; `real(10)` and C `long double` map to NumPy `longdouble`. All `logical` kinds are supported and adapt to one-byte NumPy Booleans at the boundary. | +| Caller-ordered multi-source builds, Makefiles, verbose mode, and output placement | Supported | [Building the shared library](../guide/building-shared-library.md) | [Wrapper orchestration](../../developer/codebase-map.md#cross-stage-hotspots) | [Multi-source tests](../../../tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py), [compiler verbose tests](../../../tests/fortran/infrastructure/building/compiling/test_compiler_verbose.py) | prik does not discover, reorder, or resolve all external source dependencies. | +| Visibility, naming, keyword escaping, and collision policy | Supported | [Generic interfaces](../guide/generic-interfaces.md#key-rules) | [Naming policy](../../developer/codebase-map.md#cross-stage-hotspots) | [Visibility/naming tests](../../../tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_naming.py) | Strict mode rejects names that default mode can normalize. | | Immediate call-scoped Python callbacks | Supported | [Callbacks](../guide/callbacks.md) | [Callback bridge route](../../developer/codebase-map.md#cross-stage-hotspots) | [Callback plan tests](../../../tests/fortran/callbacks/codegen/test_callback_planning.py), [scalar callback tests](../../../tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py), [array callback tests](../../../tests/fortran/callbacks/end_to_end/test_array_callbacks.py), [combined shape tests](../../../tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py) | Direct wrapper-plan generation supports entering-thread callbacks only. Stored, optional, asynchronous, or cross-thread callbacks are unsupported. | -| Runtime error projection, GIL policy, recursion, OpenMP path, and GNU ABI checks | Supported | [Error handling](../guide/error-handling.md) | [Runtime route](../../developer/codebase-map.md#cross-stage-hotspots) | [Status projection runtime](../../../tests/fortran/error_handling/end_to_end/test_status_projection.py), [status and GIL lowering](../../../tests/fortran/error_handling/codegen/test_status_error_lowering.py), [recursion tests](../../../tests/fortran/error_handling/end_to_end/test_runtime_recursion.py), [OpenMP tests](../../../tests/fortran/error_handling/end_to_end/test_openmp_runtime.py), [ABI tests](../../../tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py) | OpenMP and ABI evidence is compiler/platform-specific; callers still own native synchronization. | -| Fortran source wrapper builds | Supported | [Building the shared library](../guide/building-shared-library.md) | [Wrapper orchestration](../../developer/codebase-map.md#cross-stage-hotspots) | [Build modes](../../../tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py), [runtime ABI](../../../tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py) | Implemented for ordered Fortran source inputs. | +| Runtime error projection, GIL policy, recursion, OpenMP path, and GNU ABI checks | Supported | [Error handling](../guide/error-handling.md) | [Runtime route](../../developer/codebase-map.md#cross-stage-hotspots) | [Status projection runtime](../../../tests/fortran/error_handling/end_to_end/test_status_projection.py), [status and GIL lowering](../../../tests/fortran/error_handling/codegen/test_status_error_lowering.py), [recursion tests](../../../tests/fortran/error_handling/end_to_end/test_runtime_recursion.py), [OpenMP tests](../../../tests/fortran/error_handling/end_to_end/test_openmp_runtime.py), [ABI tests](../../../tests/fortran/infrastructure/building/end_to_end/test_runtime_compatibility.py) | OpenMP and ABI evidence is compiler/platform-specific; callers still own native synchronization. | +| Fortran source wrapper builds | Supported | [Building the shared library](../guide/building-shared-library.md) | [Wrapper orchestration](../../developer/codebase-map.md#cross-stage-hotspots) | [Build modes](../../../tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py), [runtime ABI](../../../tests/fortran/infrastructure/building/end_to_end/test_runtime_compatibility.py) | Implemented for ordered Fortran source inputs. | +| C source and C-native semantic-contract builds | Supported | [C Support](c-support.md) | [Direct C route](../../developer/packages/pipeline.md) | [C scalar runtime](../../../tests/c/primitive_scalars/end_to_end/test_direct_c_scalar_matrix.py), [pointer contracts](../../../tests/c/primitive_pointers/end_to_end/test_direct_c_pointer_contracts.py), [C build pipeline](../../../tests/c/infrastructure/building/pipeline/test_c_build_cli.py) | Current C coverage is arithmetic values, `void`, renamed symbols, route-neutral scalar projections, and completed one-level numeric pointers. The binding calls the user C symbol; no C adapter is generated. | - +| `value` arguments and existing `bind(C)` procedures | Supported | [Data types](../guide/data-types.md) | [ABI route](../../developer/codebase-map.md#cross-stage-hotspots) | [`value` and `bind(C)` tests](../../../tests/fortran/data_types/end_to_end/test_value_and_bind_c.py) | Existing `bind(C)` support is deliberately ABI-guarded. | +| Opaque `bind(C)` and `sequence` derived-type layout through accessors | Supported | [Derived types](../guide/wrapping-derived-types.md) | [Bridge generation](../../developer/codebase-map.md#cross-stage-hotspots) | [Derived layout tests](../../../tests/fortran/derived_types/end_to_end/test_opaque_layout.py) | Direct C struct layout access is not enabled. | ## Supported Inspection Features | Feature | Status | User docs | Source owner | Evidence | Limitations | | --- | --- | --- | --- | --- | --- | -| Fortran parse, semantic IR, and `.pyi` inspection | Supported | [Fortran inspection recipe](../examples/recipes/inspect-fortran-api.md), [semantic IR](../reference/semantic-ir.md) | [Fortran parser route](../../developer/codebase-map.md#cross-stage-hotspots) | [Fortran parser fixtures](../../../tests/fortran/source_parsing/parsing/test_fortran_fixture_suite.py), [Fortran semantic tests](../../../tests/fortran/semantic_ir/semantics/) | Inspection support does not by itself prove runtime wrapper support. | -| Semantic `.pyi` wrapper builds from explicit native artifacts | Partially supported | [Semantic `.pyi` contracts](../examples/recipes/semantic-pyi-contracts.md), [`.pyi` format](../reference/semantic-pyi-format.md) | [`.pyi` build route](../../developer/architecture.md#build-architecture) | [format and authoritative-input tests](../../../tests/fortran/semantic_pyi_format/), [multi-source contract tests](../../../tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py), [native build plan tests](../../../tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py) | Current runtime parity is limited; source/generated/modified multi-source package parity is covered, and broader parity remains tracked in the checklist. | -| Scalar inheritance and polymorphic dispatch | Partially supported | [Inheritance and polymorphism](../reference/fortran-wrapper.md#inheritance-and-polymorphism) | [Class lowering route](../../developer/codebase-map.md#cross-stage-hotspots) | [Inheritance tests](../../../tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py) | Polymorphic results, mutable dummies, arrays, allocatable/pointer scalars, and `class(*)` are blocked. | +| Fortran parse, semantic IR, and `.pyi` inspection | Supported | [CLI commands](../reference/cli-commands.md#parse-and-semantics) | [Fortran parser route](../../developer/codebase-map.md#cross-stage-hotspots) | [Fortran parser fixtures](../../../tests/fortran/infrastructure/parsing/test_fortran_fixture_suite.py), [Fortran semantic tests](../../../tests/fortran/infrastructure/semantic_ir/semantics/) | Inspection support does not by itself prove runtime wrapper support. | +| Semantic `.pyi` wrapper builds from explicit native artifacts | Partially supported | [Editing `.pyi` contracts](../reference/pyi-contracts/index.md) | [`.pyi` build route](../../developer/architecture.md#build-architecture) | [format and authoritative-input tests](../../../tests/fortran/infrastructure/semantic_pyi/), [multi-source contract tests](../../../tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py), [native build plan tests](../../../tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py) | Current runtime parity is limited; source/generated/modified multi-source package parity is covered, and broader parity remains tracked in the checklist. | +| Scalar inheritance and polymorphic dispatch | Partially supported | [Inheritance and polymorphic input](../guide/wrapping-derived-types.md#inheritance-and-polymorphic-input-dispatch) | [Class lowering route](../../developer/codebase-map.md#cross-stage-hotspots) | [Inheritance tests](../../../tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py) | Abstract types wrap as non-instantiable Python base classes and deferred bindings resolve through the caller's concrete type. Polymorphic results, mutable dummies, arrays, allocatable/pointer scalars, and `class(*)` are blocked. | | Assumed-size, assumed-rank, and lower-bound array contracts | Partially supported | [Arrays](../guide/arrays.md) | [Array bridge route](../../developer/codebase-map.md#cross-stage-hotspots) | [Assumed-rank tests](../../../tests/fortran/arrays/end_to_end/test_assumed_rank_arrays.py) | Assumed type and derived-type arrays remain blocked. Character arrays require fixed-width NumPy bytes dtype. | -| Generated reference pages for modules, functions, and classes | Partially supported | [Reference index](../reference/index.md) | [Codebase map](../../developer/codebase-map.md) | [Documentation reference checks](../../../tests/docs/test_reference_and_codebase_map.py), [semantic contract tests](../../../tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py) | Maintained manual references exist for generated functions, modules, classes, and generated file contracts; automated reference inventory generation has not been selected. | +| Generated wrapper API documentation | Partially supported | [Editing `.pyi` contracts](../reference/pyi-contracts/index.md) | [Codebase map](../../developer/codebase-map.md) | [Documentation reference checks](../../../tests/docs/test_reference_and_codebase_map.py), [semantic contract tests](../../../tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py) | Published guides cover the shared generated surface; automatic per-symbol reference generation has not been selected. | - +| C parse, semantic IR, and `.pyi` inspection | Partially supported | [C Support](c-support.md#build-and-inspect-apis) | [C parser route](../../developer/codebase-map.md#cross-stage-hotspots) | [C parser fixtures](../../../tests/c/infrastructure/parsing/test_c_fixture_suite.py), [C semantic tests](../../../tests/c/infrastructure/semantic_ir/semantics/) | Parser coverage is broader than the direct-only runtime lane; parser acceptance is not a runtime-support claim. | ## Unsupported Or Blocked Forms +prik blocks these before code generation and reports the boundary and the +reason, rather than emitting a wrapper that could lose precision, corrupt +memory, or outlive its native storage. + | Feature | Status | User docs | Source owner | Evidence | Limitations | | --- | --- | --- | --- | --- | --- | | Unproved pointer lifetime and ownership-changing operations | Unsupported | [Pointer safety](../guide/pointers.md#safety-checklist) | [Ownership policy](../../developer/codebase-map.md#cross-stage-hotspots) | [Pointer policy tests](../../../tests/fortran/pointers/policy/test_pointer_ownership_policy.py), [pointer runtime tests](../../../tests/fortran/pointers/runtime/test_pointer_handle_protocol.py) | Native targets must outlive every handle use; allocation, target deallocation, resize, and writable reassociation require explicit completed policy. | | Persistent callbacks and procedure pointers | Unsupported | [Callback limitations](../guide/callbacks.md#important-limitations) | [Callback route](../../developer/codebase-map.md#cross-stage-hotspots) | [Callback policy tests](../../../tests/fortran/callbacks/policy/test_callback_policy.py), [scalar callback tests](../../../tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py) | Callbacks are valid only during the wrapped call. | -| Advanced multi-source dependency discovery and external-library integration | Unsupported | [Multiple source files](../guide/building-shared-library.md#multiple-source-files) | [Build orchestration](../../developer/codebase-map.md#cross-stage-hotspots) | [Multi-source tests](../../../tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py) | prik does not infer dependency graphs, prebuilt module paths, or external library discovery. | -| Blocked array forms | Unsupported | [Unsupported array forms](../guide/arrays.md#unsupported-forms) | [Array policy route](../../developer/codebase-map.md#cross-stage-hotspots) | [Array semantic tests](../../../tests/fortran/arrays/semantics/test_array_semantics.py), [diagnostics](../reference/diagnostic-codes.md) | Assumed type `type(*)`, arrays of derived types, and character arrays not representable as fixed-width bytes need missing runtime contracts. | -| Unsupported polymorphic forms | Unsupported | [Inheritance limits](../reference/fortran-wrapper.md#inheritance-and-polymorphism) | [Class policy route](../../developer/codebase-map.md#cross-stage-hotspots) | [Inheritance tests](../../../tests/fortran/derived_types/codegen/test_class_surfaces.py) | Results, mutable dummies, arrays, polymorphic allocatable/pointer scalars, and `class(*)` are blocked. | -| Ambiguous or incomplete constructor overload sets | Unsupported | [Constructor limitations](../reference/fortran-wrapper.md#constructors-initialization-and-finalizers) | [Constructor route](../../developer/codebase-map.md#cross-stage-hotspots) | [Constructor semantic tests](../../../tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py), [class-plan validation tests](../../../tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py) | Candidates must have distinguishable exact runtime signatures and compatible native-owner lifecycles. | -| Character arrays and mutable deferred-length character storage | Partially supported | [Strings](../guide/strings.md) | [Character bridge route](../../developer/codebase-map.md#cross-stage-hotspots) | [Character edge tests](../../../tests/fortran/strings/end_to_end/test_character_edge_cases.py) | Character arrays use fixed-width NumPy bytes dtype. Fixed and allocatable deferred element length maps to dtype itemsize; Unicode/object arrays and mutable scalar deferred-length storage are unsupported. | -| Wider-than-supported real, complex, and logical storage | Unsupported | [Datatype limits](../guide/data-types.md#unsupported-widths-and-forms) | [Type probing](../../developer/codebase-map.md#cross-stage-hotspots) | [Scalar kind tests](../../../tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py) | prik blocks rather than silently losing precision or Boolean storage semantics. | - - +| Advanced multi-source dependency discovery and external-library integration | Unsupported | [Multiple source files](../guide/building-shared-library.md#multiple-source-files) | [Build orchestration](../../developer/codebase-map.md#cross-stage-hotspots) | [Multi-source tests](../../../tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py) | prik does not infer dependency graphs, prebuilt module paths, or external library discovery. | +| Blocked array forms | Unsupported | [Arrays](../guide/arrays.md) | [Array policy route](../../developer/codebase-map.md#cross-stage-hotspots) | [Array semantic tests](../../../tests/fortran/arrays/semantics/test_array_semantics.py), [diagnostics](../reference/diagnostic-codes.md) | Assumed type `type(*)`, arrays of derived types, and character arrays not representable as fixed-width bytes need missing runtime contracts. | +| Unsupported polymorphic forms | Unsupported | [Inheritance limits](../guide/wrapping-derived-types.md#inheritance-and-polymorphic-input-dispatch) | [Class policy route](../../developer/codebase-map.md#cross-stage-hotspots) | [Inheritance tests](../../../tests/fortran/derived_types/codegen/test_class_surfaces.py) | Results, mutable dummies, arrays, polymorphic allocatable/pointer scalars, and `class(*)` are blocked. Abstract types and deferred bindings are supported. | +| Ambiguous or incomplete constructor overload sets | Unsupported | [Constructor limitations](../guide/wrapping-derived-types.md#custom-constructor) | [Constructor route](../../developer/codebase-map.md#cross-stage-hotspots) | [Constructor semantic tests](../../../tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py), [class-plan validation tests](../../../tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.py) | Candidates must have distinguishable exact runtime signatures and compatible native-owner lifecycles. A Fortran `interface ` is wrapped as the type's overloaded constructor. | +| Real and complex storage wider than the target `long double` | Unsupported | [Datatype limits](../guide/data-types.md#unsupported-widths-and-forms) | [Type probing](../../developer/codebase-map.md#cross-stage-hotspots) | [Scalar kind tests](../../../tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py) | prik compares the compiler-measured mantissa against the target's `long double` instead of trusting storage size, which alone cannot separate x87 extended precision from IEEE binary128. `real(16)` is blocked on an x87 target; `real(10)` and C `long double` are supported. | + +| C direct-lane exclusions | Unsupported | [C Support](c-support.md#current-limits) | [Direct C policy](../../developer/packages/policy.md) | [C direct-policy blockers](../../../tests/c/primitive_scalars/policy/test_direct_c_policy.py), [no-artifact rejection](../../../tests/c/infrastructure/building/pipeline/test_c_direct_rejections.py) | Callbacks, aggregates, variadics, unsupported calling conventions, nullable or retained pointers, pointer results, and multi-level pointers fail before wrapper planning. PRIK does not use a C or Fortran adapter as a fallback. | ## Planned Or Reserved Areas | Feature | Status | User docs | Source owner | Evidence | Limitations | | --- | --- | --- | --- | --- | --- | -| Full semantic `.pyi` parity across all wrapper scenarios | Planned | [Semantic `.pyi` format](../reference/semantic-pyi-format.md) | [`.pyi` route](../../developer/architecture.md#build-architecture) | [semantic `.pyi` feature tests](../../../tests/fortran/semantic_pyi_format/) | Only the documented implemented subset is supported. | -| MPI examples and distribution constraints | Not implemented | [MPI example](../examples/mpi-example.md) | [Planned examples](../examples/index.md) | [Documentation navigation checks](../../../tests/docs/test_navigation.py) | No support contract or runnable evidence exists yet. | +| Full semantic `.pyi` parity across all wrapper scenarios | Planned | [Editing `.pyi` contracts](../reference/pyi-contracts/index.md) | [`.pyi` route](../../developer/architecture.md#build-architecture) | [semantic `.pyi` feature tests](../../../tests/fortran/infrastructure/semantic_pyi/) | Only the documented implemented subset is supported. | diff --git a/docs/user/language-support/index.md b/docs/user/language-support/index.md index 651fefa6c..538c0392d 100644 --- a/docs/user/language-support/index.md +++ b/docs/user/language-support/index.md @@ -1,21 +1,39 @@ --- title: Language Support -audience: users, developers -prerequisites: user guide -related: feature-matrix.md, ../reference/fortran-wrapper.md +audience: users +prerequisites: getting started +related: c-support.md, feature-matrix.md, ../reference/diagnostic-codes.md status: maintained -publication: draft +publication: reviewed --- # Language Support -Start with the [language feature matrix](feature-matrix.md). It is the -authoritative support index for implemented, partially implemented, -unsupported, and planned language features. +**Will PRIK wrap my code?** Choose the path that matches your source: -The matrix links each row to: +- [C Support](c-support.md) is the complete workflow for C projects. Current + C wrapper coverage is the direct ABI subset documented on that page. +- The [language feature matrix](feature-matrix.md) is the authoritative + Fortran-and-C index for implemented, partial, unsupported, and planned + features. + +Start with its **At A Glance** table for a fast yes or no, then read the +detailed row for the feature you care about. + +Every matrix row gives you: - the user-facing docs for the behavior; -- the source-navigation route for developers; +- the implementation route for contributors; - runtime, parser, semantic, or documentation evidence; and - the current limitation or blocker. + +A feature is listed as supported only when that linked evidence proves the +behavior in the current repository. Runtime wrapper support requires compiled, +imported, and called tests — not merely a parser that accepts the syntax. In +particular, C parsing accepts a wider set of source facts than the current +direct C wrapper lane; use the C guide's limits before treating a parsed C +declaration as buildable. + +If a feature is unsupported, PRIK blocks it before code generation and reports +the boundary and the reason. See [diagnostic codes](../reference/diagnostic-codes.md) +for what a specific rejection means. diff --git a/docs/user/reference/cli-commands.md b/docs/user/reference/cli-commands.md index 484637327..57be53fe3 100644 --- a/docs/user/reference/cli-commands.md +++ b/docs/user/reference/cli-commands.md @@ -1,188 +1,204 @@ --- title: CLI Commands Reference -audience: users, developers +audience: users prerequisites: installation -related: python-api.md, configuration-files.md +related: python-api.md, ../language-support/c-support.md, ../guide/building-shared-library.md status: maintained -publication: draft +publication: reviewed --- # CLI Commands Reference -This page documents the checked command surface exposed by: - -```bash -prik --version -python3 -m prik --help -python3 -m prik --help-build -``` - -`prik --version` and `python3 -m prik --version` print the installed -distribution version and exit successfully. The value comes from the same -package metadata as `prik.__version__`. - -With no subcommand, prik builds a wrapper from Fortran source or a semantic -`.pyi` contract. Four focused subcommands expose parsing, semantic inspection, -artifact generation, and target probing. The default `--help` output is a -concise overview of common inputs, build controls, and commands. Use -`--help-build` for every default-build option. - -Every help page places a clear one-line purpose directly below its usage. Its -examples are grouped by task rather than presented as an unlabeled command -list, so users can scan directly to a basic invocation, a different frontend -or output form, or a cross-target workflow. The examples remain illustrative; -the option groups above them are the exhaustive command contract. - - - -## Command shapes +With no subcommand, prik builds a wrapper. Four subcommands expose the earlier +stages without building one. ```bash python3 -m prik INPUT [INPUT ...] [BUILD OPTIONS] python3 -m prik {parse,semantics,generate,probe} [OPTIONS] ... ``` -The default compiled build accepts one or more Fortran source `INPUT` values, -or exactly one semantic `.pyi` entry contract. Do not mix those two input -forms. When `--build-manifest PATH` is supplied, omit positional input -entirely. In the second form, select one of the four command names shown in -braces; `COMMAND` is not a literal command or input. Inspection and -contract-generation commands advertise their own supported frontend languages -in their focused help; compiled wrapper generation is currently Fortran-only. -The concise top-level help lists `INPUT` under `positional arguments:` and the -common flags under `build options:`. All help section headings use lowercase -for the same presentation in plain and colored output. Full build help and -every source-taking subcommand use the same concise section style. Positional -`INPUT` values appear under `positional arguments:`. Full build help puts -`--language` and manifest selection under `input selection:`, while -source-taking subcommands use `input options:` for their corresponding -controls. Output and diagnostic controls always have separate groups. Each -subcommand describes shared compiler and include flags in terms of that -subcommand's actual stage rather than copying the default-build wording. -Accordingly, full default-build help advertises `--language {fortran}` only; -`parse`, `semantics`, `generate --pyi`, and `probe` advertise -`--language {fortran,c}` because those paths currently support both frontends. - - - -The top-level help intentionally lists only common build options. Run -`python3 -m prik --help-build` for the complete build surface. Each subcommand -has its own options; use `parse --help`, `semantics --help`, `generate --help`, -or `probe --help` after `python3 -m prik` to see only the options relevant to -that command. The concise build list covers output naming and location, build -compiler and include-directory selection, native compile flags such as `-O3`, -native libraries, compiler job limits, and verbose build output. -Command-specific help describes the stage-specific role of shared flags; for -example, `parse --help` explains -that `--compiler` and `-I` configure preprocessing. The concise build help does -not mislabel them as preprocessing-only options. It also keeps short examples -for a basic source build, an explicitly named extension, and semantic contract -generation; `--help-build` labels its basic build, semantic-contract build, -and manifest-replay examples separately. Both help levels reuse the canonical -`points.f90` and `geometry` naming from the -[derived-type guide](../guide/wrapping-derived-types.md#complete-example), -which contains a complete source, build, import flow, and expected result. +| Command | Purpose | +| --- | --- | +| no subcommand | Builds one importable extension from Fortran source, a supported direct C source, or a semantic `.pyi` contract. | +| `parse` | Prints parser facts and diagnostics. | +| `semantics` | Prints language-neutral semantic IR as JSON. | +| `generate` | Writes `.pyi` contracts, wrapper sources, or a Makefile without compiling. | +| `probe` | Prints compiler-target datatype and ABI facts. | -The full build help uses the following two forms: +## Getting help -```text -usage: python3 -m prik INPUT [INPUT ...] - [OUTPUT OPTIONS] [COMPILER OPTIONS] [WRAPPER OPTIONS] - [NATIVE OPTIONS] [DIAGNOSTIC OPTIONS] - python3 -m prik --build-manifest PATH [MANIFEST OVERRIDES] +```bash +prik --version # installed distribution version +python3 -m prik --help # common inputs, build controls, and commands +python3 -m prik --help-build # every default-build option ``` -Its groups are exhaustive rather than curated: `input selection` contains the -frontend and manifest selectors; `output options` contains the module name, -build directory, and structured-result selection; `compiler options` contains -every compiler and preprocessing control; `wrapper options` contains generated -wrapper naming and compiler behavior; `native options` contains native sources, -flags, objects, libraries, directories, and ordered link items; and -`diagnostic options` contains verbose, color, and traceback controls. The -default output directory shown there is `./__prik__`. - -`--build-manifest PATH` reads an existing `prik-build.json` and replays the -saved build; it does not generate a manifest. Manifest replay accepts only -overrides that the replay implementation consumes: -`--out`, `--compiler`, `-I`/`--include-dir`, `--jobs`, `--json`, `--verbose`, -`--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. +`--help` is a curated overview; `--help-build` is the exhaustive build surface. +Each subcommand has its own help — `parse --help`, `semantics --help`, +`generate --help`, `probe --help` — describing that stage's role for shared +flags such as `--compiler` and `-I`. -| Command | Purpose | -| --- | --- | -| no subcommand | Builds and imports one extension path from Fortran source or a semantic `.pyi` contract. | -| `parse` | Prints parser facts and diagnostics. | -| `semantics` | Prints language-neutral semantic IR. | -| `generate` | Generates `.pyi` contracts, wrapper sources, or a Makefile build without compiling an extension. | -| `probe` | Probes compiler-target datatype facts as JSON or a Markdown mapping table. | +`prik --version` and `python3 -m prik --version` print the same value as +`prik.__version__`. + +When `rich-argparse` is installed, prik uses its colored help formatter +automatically. Install it with `python3 -m pip install 'prik[pretty]'`, or from +an editable checkout with `python3 -m pip install -e '.[pretty]'`. Plain +`argparse` help is the deterministic fallback; `--no-color` or `NO_COLOR` +selects it explicitly. ## Input selection +The default build accepts either one or more Fortran or supported C source +`INPUT` values, or exactly one semantic `.pyi` entry contract — never both. With +`--build-manifest PATH`, omit positional input entirely. + | Option | Purpose | | --- | --- | -| `paths` | Source files, `.pyi` files, or directories. Omit only when using `--build-manifest`. | +| `paths` | Source files, `.pyi` files, or directories. Omit only with `--build-manifest`. | | `--version` | Prints the installed PRIK version and exits. | -| `--language fortran` | Selects the Fortran frontend explicitly when suffix inference is unavailable. | -| `--jobs N` | Limits concurrent compiler processes to `N`; the default uses the CPUs available to prik. | +| `--language {fortran,c}` | Selects the source or source-free contract language explicitly. C source and C-native contracts require `c`. | +| `--build-manifest PATH` | Replays a saved `prik-build.json`. It does not generate one. | +| `--jobs N` | Limits concurrent compiler processes. The default uses available CPUs. | - +Compiled wrapper builds support Fortran and the documented direct-only C +primitive lane. C paths require `--language c`; the parser also accepts more C +forms than that runtime lane, which fail before wrapper planning. -## Parse and semantics +Directories are expanded recursively in deterministic path order. Fortran +source files can usually be inferred from their suffix. C files, directories, +and unknown suffixes require `--language c`. + +## Wrapper builds -Inspection is selected by a subcommand rather than a stage flag. Compact usage -lines leave the complete command-specific option inventory to the groups below -them: +A positional Fortran or C source is both a semantic input and a native +implementation source. A `.pyi` is only the semantic contract, so it needs at +least one explicit native input: `--native-fortran-sources`, `--native-c-sources`, `--native-objects`, +`--native-library`, or `--native-link-item`. + +| Option | Purpose | +| --- | --- | +| `--out NAME` | Python module name, `PyInit_` symbol, and stable `NAME.so` alias. Accepts `NAME` or `NAME.so`, and requires a value. | +| `--out-dir DIR` | Where generated artifacts and the ABI-suffixed extension are built. Default `./__prik__`. | +| `--compiler COMPILER` | The input-language compiler used for preprocessing, datatype measurement, native compilation, and linking. Defaults to `gfortran` for Fortran and `cc` for C. | +| `-I DIR`, `--include-dir DIR` | Build-wide include directory. Repeat to preserve search order. | +| `--strict-wrapper-names` | Rejects Python names that would need escaping or a collision suffix. | +| `--assume-intent-in-scalars` | Treats a primitive scalar dummy that declares no `intent` as `intent(in)`, so its value is not returned. A declared `intent` always wins; arrays, derived-type objects, and `character` values are unaffected. Also accepted by `generate --pyi`, where it removes the same results from the generated contract, and by `semantics`. | +| `--no-compile-input-sources` | Treats positional sources as semantic inputs only. Requires an explicit native input. | +| `--native-fortran-sources PATH ...` | Compiles extra native sources without exposing them as public API. | +| `--native-c-sources PATH ...` | Compiles extra C sources without exposing them as public API. | +| `--native-compile-flags FLAG ...` | Flags for native implementation compilation. | +| `--native-c-compile-flags FLAG ...` | C implementation compiler flags. | +| `--native-objects PATH ...` | Links object files, static archives, or shared libraries. | +| `--native-library NAME ...` | Links system libraries by name — `--native-library openblas` passes `-lopenblas`. | +| `--native-link-item KIND:VALUE ...` | Ordered link items. `KIND` is `object`, `archive`, `shared-library`, `library`, or `arg`. | +| `--native-library-dir DIR ...` | Library search directories and runtime paths. | +| `--lto` | Enables link-time optimization for Fortran and C builds by adding `-flto` to generated and native compilation and to the extension link. | +| `--collision-adapter NAME ...` | Calls native symbol `NAME` through a forwarder defined in a separate translation unit, so the binding never declares an identifier its own headers already declare. | +| `--collision-adapter-all` | Applies `--collision-adapter` to every direct C symbol in the build. | +| `--positional-only` | For Fortran and C, exposes every wrapper whose arguments are all required as positional-only, renaming them `arg0`..`argN`. | +| `--wrapper-compiler-debug` | Uses the compiler debug profile instead of release. | +| `--wrapper-fortran-flags FLAG ...` | Flags for generated Fortran bridge compilation. | +| `--wrapper-c-flags FLAG ...` | Flags for generated binding compilation and extension linking. | + +Build rules worth knowing: + +- prik selects the generated binding compiler from its own profile; + `--compiler` controls the input-language side. +- `--native-compile-flags` also applies to internal datatype measurement for + source builds, so target-changing flags such as `-fdefault-integer-8` affect + both native compilation and the semantic wrapper types. +- Native input options accept multiple values and may be repeated; supplied + source, artifact, and link-item order is preserved. For values starting with + `-`, use the equals form: `--native-compile-flags="-O3 -fopenmp"`. +- Source-driven builds may add native sources, objects, and libraries to + complete the link. These augment the positional sources without becoming + semantic inputs. +- Manifest replay accepts only `--out`, `--compiler`, `-I`/`--include-dir`, + `--jobs`, `--json`, `--verbose`, `--no-color`, and `--debug`. The manifest + owns output directory, input language, preprocessing recipe, wrapper + behavior, native inputs, and link plan, so other flags are rejected rather + than silently ignored. + +- A source-free C `.pyi` contract is C-native only when `--language c` is + supplied. PRIK does not infer that identity from the contract filename, + compiler, native source list, or `@native_abi("c")`. + +- `--lto` is an optional build optimization for both Fortran and C. It applies + to native sources, generated bridge and binding compilation, and the final + extension link. Collision adapters remain correct without it. + +- `--positional-only` applies equally to Fortran and C. It removes argument + names from the Python API of any function whose arguments are all required, + so a native declaration's parameter names stop being part of the contract. + Use it when source parameter names should not become public keywords; a + system header may spell them `__x`, or omit them entirely. A function with an + optional argument keeps its keywords because skipping one still requires + naming the rest, and a module containing overload sets is rejected because + overload dispatch selects a candidate by keyword. + +- `--collision-adapter` is for a genuine identifier collision with a header + included by the generated binding. The adapter unit includes no Python + header and reconstructs the exact native declaration from completed + `@native_call` types. Width-normalized `long` and `long long` distinctions do + not by themselves require an adapter. Only a C-source function is eligible; + a Fortran `bind(C)` procedure and a generated bridge symbol are not. + The adapter isolates the binding's declaration; it does not disambiguate two + linked libraries that export the same symbol. + +## Parse and semantics ```bash python3 -m prik parse INPUT [INPUT ...] [OPTIONS] python3 -m prik semantics INPUT [INPUT ...] [OPTIONS] - -python3 -m prik parse points.f90 -python3 -m prik semantics points.f90 ``` -Parse-report controls such as `--show-vars` and `--print-limit` appear only in -`prik parse --help`. Target datatype measurement is internal to semantic -conversion and wrapping. Use the separate `prik probe` command only when you -want to inspect or save the measured target facts yourself. +| Option | Purpose | +| --- | --- | +| `--show-vars` | Includes module, submodule, program, and block-data variables in human-readable parse reports. | +| `--print-limit N` | Shows at most `N` items per repeated section in human-readable reports. | +| `--json` | Emits the complete JSON record instead of the human-readable report. | -The parse examples distinguish basic inspection, a detailed report, and an -alternate frontend. The semantics examples distinguish basic conversion, an -alternate frontend, and writing the combined semantic IR to a named JSON file. +Both commands follow the same rule: **`--json` selects the format and `--out` +selects the destination, and neither changes the other.** With no `--json` the +command prints a human-readable report; with `--json` it prints the complete +record. With no `--out` that goes to standard output; `--out PATH` writes it to +`PATH`, and bare `--out` writes one file beside each input source, using +`.json` for the record and `.txt` for the report. -`semantics` always writes its language-neutral report as JSON. With no `--out`, -it prints the combined report to standard output. `--out PATH` writes that -combined report to `PATH`; `--out` without a path writes one `.json` file beside -each input source. +`semantics` reports each module's functions with their semantic signatures, and +every argument's semantic dtype, rank, ownership, and mutability — the policy +decisions a parse report cannot show. It accepts source inputs only; use a +source file rather than a generated `.pyi` contract. + +Target datatype measurement happens automatically inside semantic conversion. +Use `probe` only when you want to inspect those facts yourself. + +For C input, select the language on each command: + +```bash +python3 -m prik parse path/to/api.h --language c --json +python3 -m prik semantics path/to/api.c --language c +``` + +Parsing reports source declarations and diagnostics; it does not promise that +the declaration fits the direct C wrapper contract. Read [C +Support](../language-support/c-support.md) before building a C API. ## Generate `generate` requires exactly one output mode: ```bash -python3 -m prik generate (--pyi | --sources | --makefile) - INPUT [INPUT ...] [OPTIONS] -python3 -m prik generate (--sources | --makefile) - --build-manifest PATH [OVERRIDES] +python3 -m prik generate (--pyi | --sources | --makefile) INPUT [INPUT ...] [OPTIONS] +python3 -m prik generate (--sources | --makefile) --build-manifest PATH [OVERRIDES] ``` | Mode | Purpose | | --- | --- | | `--pyi` | Writes the editable semantic `.pyi` contract. | -| `--sources` | Writes wrapper source files without compiling native objects or an extension. | -| `--makefile` | Writes wrapper sources, the replay manifest when applicable, and `Makefile.prik` without compiling. | +| `--sources` | Writes wrapper sources without compiling. | +| `--makefile` | Writes wrapper sources, the replay manifest when applicable, and `Makefile.prik`. | ```bash python3 -m prik generate --pyi points.f90 --out contracts @@ -190,243 +206,114 @@ python3 -m prik generate --sources points.f90 --out-dir build python3 -m prik generate --makefile points.f90 --out-dir build ``` -These examples reuse `points.f90` from the -[derived-type guide](../guide/wrapping-derived-types.md#complete-example). - -These modes are mutually exclusive. Source and Makefile generation still run -the preprocessing and semantic-policy stages needed to produce a valid wrapper -plan; they skip native object compilation and extension linking. Their -generated commands use the build-wide `--compiler` and `-I` contract. In -`--pyi` mode those same options apply only to source preprocessing and datatype -measurement because no native build is generated. - -The help page presents `generation modes` immediately after the standard -`options` group, then `positional arguments`, `input options`, compiler and -frontend-specific include controls, wrapper and native controls, output, -diagnostics, and examples. `native options` keeps native sources, compiler -flags, objects, libraries, library directories, and ordered link items -together, matching `--help-build`. `--build-manifest` reads an existing -manifest and regenerates wrapper artifacts; it is not a contract-generation -input. +For a C source contract, `--language c` is valid with `--pyi`: + +```bash +python3 -m prik generate --pyi --language c path/to/api.c --out contracts +``` + +`--sources` and `--makefile` still run preprocessing and semantic policy to +produce a valid wrapper plan; they skip object compilation and linking, and +use `--out-dir`. With no `--out`, `generate --pyi` prints every generated +contract. `--pyi` uses `--out` to write its contract package, and there +`--compiler` and `-I` affect only preprocessing and datatype measurement. + +In `.pyi` Makefile mode, prik writes `/prik-build.json` first, then +generates `/Makefile.prik` from that manifest. ## Probe -`probe` uses `--language fortran` and compiler-oriented flags instead of nested -language commands. JSON is the default; `--format markdown` prints the target -datatype mapping table. Its help examples distinguish basic native probes, a -human-readable mapping table, ABI-affecting compiler flags that change default -kinds, and a cross-target probe run through a target runner. Pass each raw -compiler flag separately, for example -`--compiler-arg=-fdefault-real-8 --compiler-arg=-fdefault-integer-8`. +`probe` measures one of two reports. Without `--expr` it measures the standard +datatype mapping table; with `--expr` it measures exactly the Fortran integer +expressions you name. `--json` then selects how that measurement is +rendered: the JSON record is complete and the default Markdown table is +converted from it, so both formats always describe the same measurement. ```bash python3 -m prik probe --language {fortran,c} --compiler COMPILER [OPTIONS] -``` - - -```bash python3 -m prik probe --language fortran --compiler gfortran-13 +python3 -m prik probe --language c --compiler cc --json +python3 -m prik probe --language fortran --compiler gfortran-13 \ + --expr "selected_real_kind(15,307)" ``` - - | Option | Purpose | | --- | --- | -| `--language fortran` | Selects the Fortran target probe. | - -| `--compiler COMPILER` | Selects the exact native or cross compiler. | -| `--format {json,markdown}` | Chooses the machine-readable report or mapping table. | -| `--expr EXPR` | Adds a Fortran integer expression to the JSON probe; repeat for more expressions. | -| `--runner ARG` | Adds one cross-target runner command item; repeat for multiple arguments. | -| `--cache-dir PATH` | Selects reusable probe storage. | -| `--refresh` | Ignores reusable results and probes the target again. | -| `--out PATH` | Writes the probe report instead of printing it. | - -Compiler preprocessing flags are accepted for JSON probes. Markdown mappings -accept compiler arguments, runner, cache, and refresh options because they -measure the standard mapping table rather than an individual preprocessed -source expression. +| `--language {fortran,c}` | Selects the target probe. | +| `--compiler COMPILER` | The exact native or cross compiler. | +| `--json` | Emits the complete JSON record instead of the Markdown table. | +| `--expr EXPR` | Measures one Fortran integer expression instead of the mapping table. Repeat for more. | +| `--runner ARG` | Adds one cross-target runner command item. Repeat for more. | +| `--cache-dir PATH` | Reusable probe storage. | +| `--refresh` | Ignores reusable results and probes again. | +| `--out PATH` | Writes the selected format instead of printing it. | + +Pass each raw compiler flag separately, for example +`--compiler-arg=-fdefault-real-8 --compiler-arg=-fdefault-integer-8`. The +mapping report accepts compiler, compiler arguments, runner, cache, and refresh +options only, because its inventory is fixed and preprocessing cannot change +it; `-I`, `-D`, `-U`, and `--std` apply to `--expr` measurements, which are +compiled from generated source. ## Compiler preprocessing -These options control compiler preprocessing before Fortran parsing. - - +These options control preprocessing before parsing. | Option | Purpose | | --- | --- | -| `--preprocessor-adapter {auto,gnu-fortran,command-template}` | Selects the Fortran compiler adapter or a custom command template. | -| `--compiler COMPILER` | Uses an exact compiler or preprocessor executable. Defaults to `gfortran` for Fortran. | +| `--preprocessor-adapter {auto,gcc-compatible-c,gnu-fortran,command-template}` | Selects the compiler adapter or a custom command template. | +| `--compiler COMPILER` | An exact compiler or preprocessor executable. Defaults to `gfortran` for Fortran and `cc` for C. | | `--preprocess-template TEMPLATE` | Runs a custom command-template preprocessor. | -| `-I DIR`, `--include-dir DIR` | Adds an include directory during compiler preprocessing. | +| `-I DIR`, `--include-dir DIR` | Adds an include directory. | | `-D NAME[=VALUE]`, `--define NAME[=VALUE]` | Defines a preprocessing macro. | | `-U NAME`, `--undef NAME` | Undefines a preprocessing macro. | -| `--std STANDARD` | Passes a Fortran language standard such as `f2008` or `f2018`. | -| `--compiler-arg ARG` | Passes one raw compiler preprocessing argument. Repeat for multiple arguments. | - - - - +| `--std STANDARD` | Passes a language standard such as `c11`, `c23`, `f2008`, or `f2018`. | +| `--compiler-arg ARG` | Passes one raw compiler argument. Repeat for more. | - +Use the equals form when a value starts with `-`, for example +`--compiler-arg=-target`. -Use `--compiler-arg=-target` style spelling when the value itself starts with -`-`. +`--compile-commands PATH` reads per-file C preprocessing commands from a +`compile_commands.json` database. It is available only for C input. - - - - - -## Parse report controls +These C-only options decide which reachable project headers become public +wrapper declarations. They affect parsing, semantic inspection, and generated +C contracts—not whether the native compiler can find an include file. | Option | Purpose | | --- | --- | -| `--show-vars` | Includes module, submodule, program, and block-data variables in human-readable Fortran parse reports. | -| `--print-limit N` | Shows at most `N` items per repeated section in human-readable parse reports. | - -## Wrapper builds - -With no subcommand, recognizable Fortran source, semantic `.pyi` input, or a -saved manifest builds a wrapper. A positional Fortran source is both a semantic -input and a native implementation source. A `.pyi` is only the semantic -contract, so it requires at least one explicit native implementation input. -Generation without compilation belongs to the `generate` subcommand. - -| Option | Purpose | -| --- | --- | -| `--compiler COMPILER` | Selects the input-language compiler used throughout a wrapper build: preprocessing, datatype measurement, native and generated-bridge compilation, and extension linking. The default is `gfortran`; the generated binding continues to use prik's binding-compiler profile. | -| `-I DIR`, `--include-dir DIR` | Adds a build-wide compiler include directory. Source builds use it during preprocessing; source and `.pyi` builds use it for native and generated wrapper compilation. Repeat to preserve search order. | -| `--strict-wrapper-names` | Rejects Python wrapper names that require escaping or collision suffixes. | -| `--build-manifest PATH` | Reads an existing semantic `.pyi` wrapper build manifest and replays its saved build. It does not generate the manifest. | -| `--no-compile-input-sources` | Treats positional Fortran sources as semantic inputs only. Requires an explicit native input; `--native-fortran-sources` remain compiled hidden implementation sources. | -| `--native-fortran-sources PATH [PATH ...]` | Compiles additional native Fortran implementation sources without using them as semantic inputs. | -| `--native-compile-flags FLAG [FLAG ...]` | Adds compiler flags to native implementation source compilation. Native source compilation is currently Fortran-only. | -| `--native-objects PATH [PATH ...]` | Links one or more native object, static archive, or shared library paths into the extension. | -| `--native-library NAME [NAME ...]` | Links system libraries by name. For example, `--native-library openblas` passes `-lopenblas` to the linker. | -| `--native-link-item KIND:VALUE [KIND:VALUE ...]` | Adds ordered extension link items. `KIND` is `object`, `archive`, `shared-library`, `library`, or `arg`. | -| `--native-library-dir DIR [DIR ...]` | Adds native library search directories and runtime paths for extension linking. | - -Important boundaries: - -- `parse`, `semantics`, `generate`, and `probe` are the only subcommands. -- For compiled wrapper builds, `--out NAME` selects the Python module name, - `PyInit_` symbol, JSON `module_name`, and stable `NAME.so` alias in the - current directory. Use `--out-dir DIR` to choose where generated artifacts - and the ABI-suffixed extension are built. Give `--out` an explicit path to - place the stable alias elsewhere. -- Wrapper `--out` requires a value and accepts `NAME` or `NAME.so`. -- `generate --sources` and `generate --makefile` use `--out-dir`; `generate - --pyi` uses `--out` for its contract package. -- `.pyi` wrapper builds require at least one native implementation input such - as `--native-fortran-sources`, `--native-objects`, `--native-library`, or - `--native-link-item`. -- Source-driven builds accept individual Fortran files or directories. - Directories are expanded recursively in deterministic path order. -- `--no-compile-input-sources` keeps positional Fortran sources as semantic inputs - but removes them from native compilation. It requires an explicit native - implementation through `--native-fortran-sources`, `--native-objects`, - `--native-library`, or `--native-link-item`. Sources passed through - `--native-fortran-sources` are still compiled without becoming public API. -- Source-driven builds may use the same native source, object, library, - include-directory, library-directory, and ordered-link options to complete - the extension build. These inputs augment the positional implementation - sources; they do not become semantic wrapper inputs. -- In a wrapper build, `--compiler` is a build input rather than a - preprocessing-only setting. It selects the input-language compiler command - used for preprocessing and datatype measurement, then for native source and - generated bridge compilation, and finally for extension linking. prik still - selects the generated binding compiler from its compiler profile. -- `-I DIR` is build-wide: prik preserves the supplied order in preprocessing - and in native, bridge, and binding compilation. Use it for source includes, - compiler-produced module files, and native interface directories. -- `--native-compile-flags` compiles the native implementation. The public name - identifies the native compilation phase rather than the current source - language; native source compilation is currently Fortran-only. - `--wrapper-fortran-flags` compiles the generated Fortran bridge, and - `--wrapper-c-flags` compiles the generated binding and supplies additional - extension-link flags. -- For source-driven builds, prik also applies `--native-compile-flags` to its - internal datatype measurement. Target-changing flags such as - `-fdefault-integer-8` or `-fdefault-real-8` therefore affect both native - compilation and the semantic wrapper types without separate probe options. -- Native input options accept one or more values per occurrence and may also be - repeated. prik preserves the supplied source, artifact, and link-item order. - For compiler flags or prefixed library names that start with `-`, group them - with the equals form, for example `--native-compile-flags="-O3 -fopenmp"` or - `--native-library="-lblas -llapack"`. -- In `.pyi` Makefile mode, prik writes `/prik-build.json` first and - generates `/Makefile.prik` from that manifest. -- `--build-manifest PATH` reads a saved manifest and rebuilds from it; it does - not generate the manifest. `generate --makefile - --build-manifest PATH` regenerates `Makefile.prik` without positional - contracts or repeated native flags. Replay may override only `--out`, - `--compiler`, `-I`/`--include-dir`, `--json`, `--verbose`, `--no-color`, and - `--debug`; all other build settings come from the - manifest. - - +| `--include-exposure {reachable-project,roots-only}` | Exposes reachable project headers by default, or only the root inputs. | +| `--public-include PATH_OR_PATTERN` | Exposes declarations from matching included files. Repeat as needed. | +| `--private-include PATH_OR_PATTERN` | Hides declarations from matching included files. Repeat as needed. | +| `--export-symbols FILE` | Selects the exact reachable C functions named by FILE and makes those declarations public, including declarations from otherwise-private system headers. | + +`--export-symbols` is a function-only allowlist for commands that produce +semantic IR: source builds, `semantics`, and `generate --pyi`. The UTF-8 file +contains one ASCII C identifier per line; blank lines and text after `#` are ignored. +Every listed name must resolve to exactly one reachable function. Empty files, +invalid or repeated names, unknown names, names of non-function declarations, +and ambiguous declarations fail the command. All declarations not selected by +the file are removed from that semantic surface. This makes the allowlist the +explicit exception to `roots-only`, system-header privacy, and matching +`--private-include` rules; it does not change native linking or make an +unsupported selected signature buildable. ## Output and diagnostics | Option | Purpose | | --- | --- | -| `--json` | Selects JSON instead of the default human-readable output for commands that support both formats. Semantic reports are always JSON and therefore do not expose this flag. | -| `--out [PATH]` | Writes command output, selects a generated `.pyi` package directory, or names the wrapper Python module and final `.so`. | -| `--out-dir DIR` | Selects the wrapper build output directory. The default is `./__prik__`. | -| `--verbose` | Announces and completes binding, bridge, and header source-text generation in order, then each written artifact, source/object compilation pair, and final extension path before printing the exact compiler or linker command; it times each non-writing operation and reports total build time last. | -| `--wrapper-compiler-debug` | Uses the compiler debug profile for direct wrapper builds instead of the default release profile. | -| `--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. | +| `--json` | Selects the complete JSON record instead of the human-readable report. Available on `parse`, `semantics`, `probe`, and wrapper builds. | +| `--out [PATH]` | Destination for the selected format, generated `.pyi` package directory, or the wrapper module and final `.so`. It never changes which format is produced. | +| `--out-dir DIR` | Wrapper build output directory. Default `./__prik__`. | +| `--verbose` | Announces each generation, artifact, and compile step. It prints every compiler or linker command before starting it, times each operation, and reports total build time last. | | `--no-color` | Disables ANSI color in parse diagnostics. | -| `--debug` | Re-raises command failures so Python prints a traceback. | +| `--debug` | Re-raises failures so Python prints a traceback. | -When `rich-argparse` is installed, prik uses its colored help formatter -automatically. Install the optional UI dependencies for a published package -with `python3 -m pip install 'prik[pretty]'`, or from an editable source -checkout with `python3 -m pip install -e '.[pretty]'`. Plain `argparse` help -remains the deterministic fallback, and `--no-color` or `NO_COLOR` selects it -explicitly. - -Use `--out` for command output, generated `.pyi` contract packages, or -the wrapper Python module and final `.so`. Use `--out-dir` for wrapper build artifacts. -Wrapper build JSON includes generated artifact paths, -`native_build_plan`, the structured native compile/link plan for the extension, -and for semantic `.pyi` builds the normalized replay `manifest`. +Wrapper build JSON includes generated artifact paths, `native_build_plan`, and +for semantic `.pyi` builds the normalized replay `manifest`. ## Checked workflows @@ -439,23 +326,25 @@ and for semantic `.pyi` builds the normalized replay `manifest`. | Print semantic IR | `python3 -m prik semantics path/to/file.f90` | | Emit a semantic `.pyi` contract directory | `python3 -m prik generate --pyi path/to/file.f90 --out contracts` | | Build a Fortran wrapper | `python3 -m prik path/to/file.f` | -| Build a Fortran wrapper with native compiler and link flags | `python3 -m prik path/to/file.f90 --native-compile-flags="-O3 -fopenmp" --wrapper-c-flags=-fopenmp` | +| Build a supported C wrapper | `python3 -m prik --language c path/to/file.c --compiler cc` | +| Parse a C header as JSON | `python3 -m prik parse path/to/api.h --language c --json` | +| Parse C with the native project's preprocessing flags | `python3 -m prik parse path/to/api.h --language c --compiler clang -I include -D API_EXPORT= --std c11` | +| Build with native compiler and link flags | `python3 -m prik path/to/file.f90 --native-compile-flags="-O3 -fopenmp" --wrapper-c-flags=-fopenmp` | | Build from a semantic contract and native object | `python3 -m prik contracts/module.pyi --native-objects build/module.o -I build` | -| Build a Fortran wrapper with an explicit module and `.so` name | `python3 -m prik path/to/file.f90 --out my_extension` | +| Build a C-native semantic contract | `python3 -m prik --language c contracts/module.pyi --native-c-sources native/module.c --compiler cc` | +| Build with an explicit module and `.so` name | `python3 -m prik path/to/file.f90 --out my_extension` | | Generate wrapper sources only | `python3 -m prik generate --sources dependency.f90 api.f90 --out-dir build` | | Generate an editable Makefile | `python3 -m prik generate --makefile dependency.f90 api.f90 --out-dir build` | | Generate a `.pyi` replay manifest and Makefile | `python3 -m prik generate --makefile contracts/module.pyi --native-fortran-sources native/module.f90 --out-dir build --json` | | Replay a `.pyi` manifest | `python3 -m prik --build-manifest build/prik-build.json` | - +The `points.f90` examples reuse the source from the +[derived-type guide](../guide/wrapping-derived-types.md#complete-example), +which has a complete source, build, import, and result flow. ## Related pages -- Use [Python API Reference](python-api.md) when calling prik from Python. -- Use [Fortran Wrapper Reference](fortran-wrapper.md) for wrapper - build workflows. -- Use [Semantic .pyi Format](semantic-pyi-format.md) when editing wrapper - contracts. +- [Python API Reference](python-api.md) — the same workflows from Python. +- [C Support](../language-support/c-support.md) — the direct C lane's complete + source, contract, build, and Python workflows. +- [Editing `.pyi` Contracts](pyi-contracts/index.md) — supported contract edits. diff --git a/docs/user/reference/configuration-files.md b/docs/user/reference/configuration-files.md index 3327a939a..6db3040a2 100644 --- a/docs/user/reference/configuration-files.md +++ b/docs/user/reference/configuration-files.md @@ -195,9 +195,9 @@ boundaries, reference links, and documentation checklist synchronization. ## Evidence And Maintenance Manifest and Makefile replay behavior is covered by -[`test_pyi_build_modes.py`](../../../tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py) and +[`test_pyi_build_modes.py`](../../../tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py) and source-build Makefile behavior by -[`test_build_modes.py`](../../../tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py). +[`test_build_modes.py`](../../../tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py). Tooling configuration is covered by [`test_reference_and_codebase_map.py`](../../../tests/docs/test_reference_and_codebase_map.py), diff --git a/docs/user/reference/diagnostic-codes.md b/docs/user/reference/diagnostic-codes.md index b5e61f7d9..7accc6047 100644 --- a/docs/user/reference/diagnostic-codes.md +++ b/docs/user/reference/diagnostic-codes.md @@ -1,122 +1,203 @@ --- title: Diagnostic Codes -audience: users, developers +audience: users prerequisites: error handling -related: index.md, ../troubleshooting/index.md +related: index.md, ../language-support/feature-matrix.md, ../language-support/c-support.md, ../troubleshooting/compiler-issues.md status: maintained -publication: draft +publication: reviewed --- # Diagnostic Codes -Diagnostic codes are stable category identifiers for users, tests, and tooling. -They are not source line numbers, occurrence counters, or process exit statuses. - -Categories use explicit symbolic names such as `PARSE_INVALID_SYNTAX` and -`C_UNRESOLVED_INCLUDE`. The name describes the failure class directly. - -## Fatal Parser Errors - -Fatal parser errors stop parsing and are rendered by the CLI without a Python -traceback unless `--debug` is used. - -| Code | Frontend | Meaning | -| --- | --- | --- | -| `PARSE_ERROR` | Fortran | Fallback for a manually constructed or defensive Fortran parse error without a narrower category. | -| `PARSE_INVALID_SYNTAX` | Fortran | Syntax cannot be consumed in a modeled Fortran grammar region. | -| `PARSE_WRONG_ENTRYPOINT` | Fortran | A singular public parser API was called for a different source-unit kind. | -| `PARSE_AMBIGUOUS_ENTRYPOINT` | Fortran | A singular public parser API matched more than one source unit. | -| `PARSE_EXPECTED_UNIT` | Fortran | An internal unit visitor received the wrong source-unit kind. | -| `PARSE_MISSING_UNIT_END` | Fortran | A source unit has no closing statement. | -| `PARSE_MISMATCHED_UNIT_END` | Fortran | A named source-unit closing statement does not match its opener. | -| `PARSE_UNEXPECTED_UNIT_END` | Fortran | A closing statement appears while another nested unit is active. | -| `PARSE_DUPLICATE_UNIT` | Fortran | A scope contains duplicate named source units of the same kind. | -| `PARSE_DUPLICATE_PROCEDURE` | Fortran | A scope contains duplicate procedure names. | -| `PARSE_MALFORMED_HEADER` | Fortran | A module or procedure header is unsupported or malformed. | -| `PARSE_UNSUPPORTED_RESULT_TYPE` | Fortran | A function header contains an unsupported result-type prefix. | -| `PARSE_DUPLICATE_DECLARATION` | Fortran | A procedure symbol is declared more than once. | -| `PARSE_UNKNOWN_PARAMETER_TYPE` | Fortran | A `PARAMETER` symbol has no declared type where one is required. | -| `PARSE_DUPLICATE_PARAMETER` | Fortran | A procedure contains duplicate `PARAMETER` declarations. | -| `PARSE_DUPLICATE_SYMBOL` | Fortran | A file or project scope contains a duplicate symbol. | -| `PARSE_UNSUPPORTED_OPENMP_DIRECTIVE` | Fortran | A modeled specification region contains an unsupported OpenMP directive. | -| `PARSE_MISSING_DERIVED_TYPE_END` | Fortran | A derived-type declaration has no matching closing statement. | -| `PARSE_EXECUTABLE_IN_SPECIFICATION` | Fortran | An executable statement appears in a non-executable specification region. | -| `PARSE_UNSUPPORTED_DECLARATION` | Fortran | A declaration-shaped line uses an unsupported datatype form. | -| `PARSE_UNSUPPORTED_TYPE_BOUND_DECLARATION` | Fortran | A derived-type `contains` region has an unsupported binding declaration. | -| `PARSE_UNRESOLVED_ARGUMENT_TYPE` | Fortran | A defensive invariant could not apply a declared argument type. | -| `PARSE_UNKNOWN_FUNCTION_RESULT_TYPE` | Fortran | A function result has no resolvable datatype. | -| `PARSE_IMPLICIT_NONE_UNDECLARED_SYMBOL` | Fortran | `implicit none` requires a missing argument or result declaration. | -| `PARSE_MISSING_FUNCTION_RESULT` | Fortran | A defensive invariant found a function without a result variable. | -| `PARSE_RESULT_SHADOWS_ARGUMENT` | Fortran | A function result name shadows an argument. | -| `PARSE_DUPLICATE_VARIABLE` | Fortran | A module-like scope contains conflicting duplicate variable declarations. | -| `PARSE_UNKNOWN_VARIABLE_TYPE` | Fortran | A module variable still has an unknown datatype after parsing. | -| `PARSE_DUPLICATE_FIELD` | Fortran | A derived type contains duplicate fields. | -| `PARSE_UNKNOWN_FIELD_TYPE` | Fortran | A derived-type field still has an unknown datatype after parsing. | -| `PARSE_DUPLICATE_ARGUMENT` | Fortran | A procedure argument list repeats a name. | -| `PARSE_PREPROCESSING_REQUIRED` | Fortran | Raw CPP directives require compiler preprocessing before parser entry. | -| `PARSE_INTERNAL_STATE` | Fortran | A defensive internal parser invariant was violated. | - - - -## Preprocessing Diagnostics - -Compiler-backed preprocessing failures are rendered by the CLI without a -Python traceback unless `--debug` is used. They occur before the parser consumes -the expanded source. +When prik rejects your source, it prints a stable code in brackets. Look that +code up here to find out what class of problem it is. + +```text +points.f90:5:1: error[PARSE_MISSING_UNIT_END]: Missing end module for module 'points'. + | +5 | module points + | ^ +``` + +The code is a category identifier — not a line number, a counter, or an exit +status. Codes are stable across releases, so you can match on them in scripts +and tests. + +Add `--debug` to any command to re-raise the failure with a Python traceback. +Add `--no-color` if the highlighting is hard to read. + +## Parser errors + +These stop parsing. The first tables cover the Fortran frontend; the C parser +codes follow them. + +### Unit and block structure + +A source unit or block is not closed correctly, or contains something that +cannot appear where it does. | Code | Meaning | | --- | --- | -| `PREPROCESSOR_NOT_FOUND` | The configured compiler/preprocessor executable could not be started. | -| `PREPROCESSOR_FAILED` | The compiler/preprocessor returned a non-zero status, timed out, or could not be executed. Compiler stderr is preserved. | -| `INVALID_COMPILER_ARGUMENTS` | The preprocessing configuration is invalid, such as a malformed macro name or unusable compile database entry. | +| `PARSE_INVALID_SYNTAX` | Syntax cannot be consumed in a modeled grammar region. | +| `PARSE_MISSING_UNIT_END` | A source unit has no closing statement. | +| `PARSE_MISMATCHED_UNIT_END` | A named closing statement does not match its opener. | +| `PARSE_UNEXPECTED_UNIT_END` | A closing statement appears while another nested unit is active. | +| `PARSE_MISSING_DERIVED_TYPE_END` | A derived-type declaration has no matching closing statement. | +| `PARSE_EXECUTABLE_IN_SPECIFICATION` | An executable statement appears in a specification region. | + +### Duplicate names + +The same name is declared twice where prik needs one definition. + +| Code | Meaning | +| --- | --- | +| `PARSE_DUPLICATE_UNIT` | A scope contains duplicate named source units of the same kind. | +| `PARSE_DUPLICATE_PROCEDURE` | A scope contains duplicate procedure names. | +| `PARSE_DUPLICATE_DECLARATION` | A procedure symbol is declared more than once. | +| `PARSE_DUPLICATE_SYMBOL` | A file or project scope contains a duplicate symbol. | +| `PARSE_DUPLICATE_PARAMETER` | A procedure contains duplicate `PARAMETER` declarations. | +| `PARSE_DUPLICATE_VARIABLE` | A module-like scope contains conflicting duplicate variable declarations. | +| `PARSE_DUPLICATE_FIELD` | A derived type contains duplicate fields. | +| `PARSE_DUPLICATE_ARGUMENT` | A procedure argument list repeats a name. | + +### Unresolved types + +prik could not determine a datatype it needs. Adding an explicit declaration +usually fixes these. + +| Code | Meaning | +| --- | --- | +| `PARSE_IMPLICIT_NONE_UNDECLARED_SYMBOL` | `implicit none` requires a missing argument or result declaration. | +| `PARSE_UNKNOWN_PARAMETER_TYPE` | A `PARAMETER` symbol has no declared type where one is required. | +| `PARSE_UNKNOWN_VARIABLE_TYPE` | A module variable still has an unknown datatype after parsing. | +| `PARSE_UNKNOWN_FIELD_TYPE` | A derived-type field still has an unknown datatype after parsing. | +| `PARSE_UNKNOWN_FUNCTION_RESULT_TYPE` | A function result has no resolvable datatype. | +| `PARSE_UNRESOLVED_ARGUMENT_TYPE` | A declared argument type could not be applied. | + +### Unsupported forms + +The syntax is valid Fortran, but outside the modeled subset. Check the +[language feature matrix](../language-support/feature-matrix.md). + +| Code | Meaning | +| --- | --- | +| `PARSE_MALFORMED_HEADER` | A module or procedure header is unsupported or malformed. | +| `PARSE_UNSUPPORTED_DECLARATION` | A declaration-shaped line uses an unsupported datatype form. | +| `PARSE_UNSUPPORTED_RESULT_TYPE` | A function header contains an unsupported result-type prefix. | +| `PARSE_UNSUPPORTED_TYPE_BOUND_DECLARATION` | A derived-type `contains` region has an unsupported binding declaration. | +| `PARSE_UNSUPPORTED_OPENMP_DIRECTIVE` | A modeled specification region contains an unsupported OpenMP directive. | +| `PARSE_MISSING_FUNCTION_RESULT` | A function has no result variable. | +| `PARSE_RESULT_SHADOWS_ARGUMENT` | A function result name shadows an argument. | + +### Preprocessing required + +| Code | Meaning | +| --- | --- | +| `PARSE_PREPROCESSING_REQUIRED` | Raw CPP directives need compiler preprocessing before the parser runs. | + +### API misuse and internal invariants + +You will normally see these only when calling the parser API directly. + +| Code | Meaning | +| --- | --- | +| `PARSE_WRONG_ENTRYPOINT` | A singular parser API was called for a different source-unit kind. | +| `PARSE_AMBIGUOUS_ENTRYPOINT` | A singular parser API matched more than one source unit. | +| `PARSE_EXPECTED_UNIT` | An internal unit visitor received the wrong source-unit kind. | +| `PARSE_INTERNAL_STATE` | A defensive internal parser invariant was violated. | +| `PARSE_ERROR` | Fallback for a parse error with no narrower category. | + +### C parser errors + +| Code | Meaning | +| --- | --- | +| `CPARSE_INVALID_SYNTAX` | Syntax cannot be consumed in a modeled C grammar region. | +| `CPARSE_PREPROCESSING_REQUIRED` | Raw preprocessing directives require compiler preprocessing before parser entry. | +| `CPARSE_UNSUPPORTED_KNR_DEFINITION` | A K&R-style function definition is unsupported. | +| `CPARSE_INVALID_SPECIFIER_SEQUENCE` | A primitive type-specifier sequence is invalid. | +| `CPARSE_ERROR` | Fallback for a C parse error with no narrower category. | + +## Preprocessing errors + +These happen before the parser sees the source, while running the compiler as a +preprocessor. Compiler stderr is preserved in the message. + +```text +: error[PREPROCESSOR_NOT_FOUND]: preprocessor not found: nosuchcompiler +``` + +| Code | Meaning | +| --- | --- | +| `PREPROCESSOR_NOT_FOUND` | The configured compiler or preprocessor could not be started. | +| `PREPROCESSOR_FAILED` | The preprocessor returned a non-zero status, timed out, or could not run. | +| `INVALID_COMPILER_ARGUMENTS` | The preprocessing configuration is invalid, such as a malformed macro name. | | `UNSUPPORTED_COMPILER_CAPABILITY` | The selected adapter was asked for metadata it cannot provide. | -| `PROVENANCE_UNAVAILABLE` | Expanded source was produced, but the adapter cannot provide accurate source mappings. | -| `INCLUDE_NOT_FOUND` | A native Fortran `include "..."` target could not be resolved or read. | -| `INCLUDE_CYCLE` | Recursive native Fortran INCLUDE expansion found a cycle. | +| `PROVENANCE_UNAVAILABLE` | Source expanded, but the adapter cannot provide accurate source mappings. | +| `INCLUDE_NOT_FOUND` | A Fortran `include "..."` target could not be resolved or read. | +| `INCLUDE_CYCLE` | Recursive Fortran `INCLUDE` expansion found a cycle. | + +## Wrapper planning errors + +These come from the wrapper build, after the source parsed and its semantic +policy completed. **They do not carry a bracketed code.** Instead they name the +declaration and the specific policy that has no supported lowering: -## Wrapper Planning Errors +```text +prik: error: Semantic function 'm3.make' has unsupported wrapper policy: +result is an unsupported array of derived values; result has no completed +bridge data action +``` -Wrapper planning errors are emitted by the default wrapper build after semantic -policy completion. The owner path identifies the declaration whose completed -policy has no supported lowering. +The quoted owner path locates the declaration. The reasons after the colon +identify a missing completed policy or an unsupported combination of completed +policies. Either reshape the native declaration, or check whether the form is +supported at all in the +[language feature matrix](../language-support/feature-matrix.md). -Reasons identify a missing completed policy or an unsupported -completed-policy combination. These are build-stage diagnostics rather than a -separate inspection report; see -[Error Handling](../guide/error-handling.md#wrapper-planning-errors) for the -repair workflow. +See [Error Handling](../guide/error-handling.md) for the repair workflow and +how these map to Python exceptions at runtime. - +## C report diagnostics - +They do not necessarily stop inspection, but a C wrapper build refuses to +silently drop a top-level declaration with an unmodeled declaration, +declarator, or compiler-extension diagnostic. - +| `C_DUPLICATE_TAG_DEFINITION` | A struct, union, or enum tag has more than one definition. | + +## Direct C wrapper diagnostics + +These identifiers name a C declaration or authored contract outside the +direct-only lane. They are policy diagnostics rather than bracketed parser +codes. Each may end in `:name` to identify the affected return, argument, or +declaration. + +| Code | Meaning | +| --- | --- | +| `C_DIRECT_CALLBACK`, `C_DIRECT_VARIADIC_FUNCTION` | A callback or variadic function needs an adapter ABI that the direct lane does not create. | +| `C_DIRECT_AGGREGATE_TYPE`, `C_DIRECT_UNRESOLVED_PRIMITIVE_ABI`, `C_DIRECT_UNPROBED_PRIMITIVE_ABI` | An aggregate or a primitive with no measured target ABI cannot cross the direct boundary. | +| `C_DIRECT_ARRAY_DECLARATOR`, `C_DIRECT_ARRAY_RANK`, `C_DIRECT_ARRAY_CONTRACT`, `C_DIRECT_ARRAY_PASSING`, `C_DIRECT_ARRAY_TRANSFORMATION`, `C_DIRECT_ARRAY_ORDER` | An array declaration or authored NumPy contract is outside the supported rank, passing, shape, transformation, or C-order rules. | +| `C_DIRECT_POINTER_DEPTH`, `C_DIRECT_POINTER_RESULT`, `C_DIRECT_NULLABLE_POINTER`, `C_DIRECT_RAW_ADDRESS`, `C_DIRECT_CONST_POINTER_OUTPUT` | A pointer has unsupported depth, result, nullability, raw-address, or const-output semantics. | +| `C_DIRECT_BOOL_ARRAY` | Boolean arrays do not have a supported direct C array contract. | +| `C_DIRECT_TRANSLATION_UNIT_LOCAL_SYMBOL`, `C_DIRECT_UNSUPPORTED_CALLING_CONVENTION`, `C_DIRECT_UNSUPPORTED_QUALIFIER` | The symbol is not externally callable through the documented direct ABI. | +| `C_DIRECT_NATIVE_GLOBAL_STATE`, `C_DIRECT_ENUM_CONSTANT`, `C_DIRECT_MACRO_CONSTANT` | Native global state and constants are not exposed by the direct C wrapper lane. | +| `C_DIRECT_UNMODELED_DECLARATION` | A declaration would otherwise be omitted from a C wrapper build. | + +See [C Support](../language-support/c-support.md#current-limits) for the +supported boundary and the repair choices. diff --git a/docs/user/reference/fortran-wrapper.md b/docs/user/reference/fortran-wrapper.md index 38465cb79..5e02f7572 100644 --- a/docs/user/reference/fortran-wrapper.md +++ b/docs/user/reference/fortran-wrapper.md @@ -34,12 +34,12 @@ is validated. PRIK_C_DOCS_END --> ## Contents @@ -94,7 +94,7 @@ PRIK_C_DOCS_END --> Build the checked scalar example: ```bash -python3 -m prik tests/fortran/building_shared_library/end_to_end/fixtures/native/fruntime_abi_f90.f90 \ +python3 -m prik tests/fortran/infrastructure/building/end_to_end/fixtures/native/fruntime_abi_f90.f90 \ --out-dir build/fruntime_abi ``` @@ -389,7 +389,7 @@ The equivalent Python entrypoint returns structured artifact paths: from prik import build_fortran_extension result = build_fortran_extension( - "tests/fortran/building_shared_library/end_to_end/fixtures/native/fruntime_abi_f90.f90", + "tests/fortran/infrastructure/building/end_to_end/fixtures/native/fruntime_abi_f90.f90", output_dir="build/fruntime_abi", ) print(result.module_name) @@ -1750,9 +1750,35 @@ PRIK_C_DOCS_END --> Character arrays use fixed-width NumPy bytes dtypes such as `S5`; the dtype itemsize is the Fortran element length. Deferred-length allocatable character arrays carry that length at runtime and return a fresh fixed-width bytes array. -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. +Python Unicode arrays, object arrays, `allocatable` and `pointer` character +fields at any length, and mutable character-buffer fields remain blocked until +an explicit field and encoding policy exists. Plain fixed-length character +fields are supported. + +Character module variables are supported in every form. A declared-length +scalar is readable and writable as `str` at exactly the declared byte width; an +`allocatable` or `pointer` scalar reads as a detached `str`, or `None` when +unallocated or unassociated. Character module arrays reach Python as +fixed-width bytes arrays — `allocatable` and `pointer` ones through a handle, +fixed-shape `target` ones as a live view, and `parameter` ones as a read-only +snapshot copied at import. Each array accessor reports its own element width, +so an assumed-length (`character(len=*)`) `parameter` array takes the dtype +width its initializer implied. + +Only a declared-length non-descriptor scalar is writable by assignment +(`module.label = "PYTHON!!"`, at exactly the declared byte width). An +`allocatable` or `pointer` scalar reads as a detached snapshot and cannot be +replaced, the same way a numeric descriptor module scalar cannot; a `parameter` +is a constant, so assigning to it rebinds the Python name without reaching +Fortran. Module arrays are mutated in place through their view or handle rather +than rebound. + +A deferred-length `character(len=:), allocatable` module *array* does not build +under GNU Fortran 11.4, which raises an internal compiler error on that +declaration. Scalar +`allocatable` and `pointer` character dummies and results are supported in +every direction; see +[Strings](../guide/strings.md#allocatable-and-pointer-scalar-strings). ## Scalar Types And Kind Coverage @@ -2208,12 +2234,13 @@ outputs. Native `stop` or `error stop` can terminate the Python process. An edited semantic `.pyi` can opt into status projection: ```python -from prik.contracts import Float64, Int32, Returns, String, raises +from prik.contracts import Arg, Float64, Hidden, Int32, String, native_call, raises @raises(status="status", message="message", success=0) +@native_call([Arg(0), Hidden("status", Int32), Hidden("message", String[64])]) def solve( x: Float64[:], -) -> tuple[Returns["status", Int32], Returns["message", String]]: ... +) -> None: ... ``` ```python @@ -2222,9 +2249,10 @@ solve(bad_values) # raises RuntimeError(message) otherwise ``` The status target must be a hidden scalar integer output. The optional message -target must be a hidden string output. Annotated status and message values are -consumed rather than returned. prik cannot recover from native termination, -process abort, or a callback failure crossing a native callback boundary. +may be a hidden string output or a visible rank-zero NumPy bytes buffer that the +caller supplies. Hidden status and message values are consumed rather than +returned. prik cannot recover from native termination, process abort, or a +callback failure crossing a native callback boundary. ### GIL Policy @@ -2348,7 +2376,7 @@ wrappers: | Pointers | Scalar-derived pointer results without stable typed holder storage, expired-target results, and unproved reassociation or ownership-changing operations | Stable target lifetime, descriptor identity, typed holder storage, or explicit operation policy. | | Polymorphism | Results, mutable dummies, arrays, allocatable/pointer scalars, `class(*)` | Dynamic type, allocation, replacement, and ownership. | | Constructors | Incomplete or indistinguishable constructor overload sets | Every candidate needs a complete exact runtime signature and compatible owner lifecycle. | -| Characters | Mutable scalar allocatable character dummies and deferred-length mutable fields | Allocation, encoding, replacement, and destruction. | +| Characters | Deferred-length mutable character fields | Allocation, encoding, replacement, and destruction. | | Kinds | Real wider than 64 bits, complex wider than 128 bits, wider explicit logical storage | Portable NumPy round-trip without silent precision loss. | | Callbacks | Stored, optional, cross-thread, or procedure-pointer callbacks | Persistent ownership, thread, exception, nullability, and teardown. | diff --git a/docs/user/reference/generated-classes.md b/docs/user/reference/generated-classes.md index d958eeff4..16c9908ac 100644 --- a/docs/user/reference/generated-classes.md +++ b/docs/user/reference/generated-classes.md @@ -157,7 +157,7 @@ Generated class behavior is covered by [`test_inheritance_and_polymorphism.py`](../../../tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py). Exact class-method and constructor overloads, including explicit bound construction, are covered by -[`test_edited_class_surfaces.py`](../../../tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py). +[`test_edited_class_surfaces.py`](../../../tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py). When class behavior changes, update this page with the derived-type user guide, semantic `.pyi` reference, generated contract fixtures, and ownership evidence. diff --git a/docs/user/reference/generated-functions.md b/docs/user/reference/generated-functions.md index 9b822e6ee..6c4906741 100644 --- a/docs/user/reference/generated-functions.md +++ b/docs/user/reference/generated-functions.md @@ -138,7 +138,7 @@ target without replacing that linked contract. ## Evidence And Maintenance Function and subroutine call surfaces are covered by -[`test_edited_call_surfaces.py`](../../../tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py), +[`test_edited_call_surfaces.py`](../../../tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py), [`test_documented_function_journeys.py`](../../../tests/fortran/functions/end_to_end/test_documented_function_journeys.py), [`test_optional_runtime.py`](../../../tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py), and [`test_generic_interfaces.py`](../../../tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py). diff --git a/docs/user/reference/generated-modules.md b/docs/user/reference/generated-modules.md index 03750eecd..c1d78c7f9 100644 --- a/docs/user/reference/generated-modules.md +++ b/docs/user/reference/generated-modules.md @@ -131,9 +131,9 @@ requests; colliding names fail. Module package shape, child namespaces, variable access, and import policy are covered by [`test_module_variables_and_state.py`](../../../tests/fortran/modules/end_to_end/test_module_variables_and_state.py), -[`test_contract_package_runtime.py`](../../../tests/fortran/semantic_pyi_format/end_to_end/test_contract_package_runtime.py), -[`test_multi_source_builds.py`](../../../tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py), and -[`test_source_generated_pyi_contracts.py`](../../../tests/fortran/building_shared_library/pipeline/test_source_generated_contracts.py). +[`test_contract_package_runtime.py`](../../../tests/fortran/infrastructure/semantic_pyi/end_to_end/test_contract_package_runtime.py), +[`test_multi_source_builds.py`](../../../tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py), and +[`test_source_generated_pyi_contracts.py`](../../../tests/fortran/infrastructure/building/pipeline/test_source_generated_contracts.py). When module namespace behavior changes, update this page, generated package fixtures, [Semantic `.pyi` Format](semantic-pyi-format.md), and the module diff --git a/docs/user/reference/index.md b/docs/user/reference/index.md index 27080d2f5..1e0bc8798 100644 --- a/docs/user/reference/index.md +++ b/docs/user/reference/index.md @@ -1,37 +1,43 @@ --- title: Reference -audience: users, developers +audience: users prerequisites: getting started -related: cli-commands.md, python-api.md, fortran-wrapper.md, semantic-pyi-format.md, pyi-contracts/index.md +related: cli-commands.md, python-api.md, pyi-contracts/index.md, diagnostic-codes.md, ../language-support/index.md status: maintained -publication: draft +publication: reviewed --- # Reference -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 .pyi contracts](pyi-contracts/index.md) -- [Diagnostic codes](diagnostic-codes.md) -- [Generated functions](generated-functions.md) -- [Generated modules](generated-modules.md) -- [Generated classes](generated-classes.md) -- [Configuration files](configuration-files.md) - -## Generated Wrapper Surface - -The generated function, module, and class pages document the maintained Python -surface produced by wrapper builds. They are manually maintained references -backed by checked contracts and runtime tests. A generated-reference toolchain -can replace the inventory details later, but it must preserve the same public -rules. +Reference pages describe the exact command, API, and editable-contract +surfaces. They assume you have already built a wrapper — start with [Getting +Started](../getting-started/index.md) and the [User Guide](../guide/index.md) +if you have not. + +## Drive PRIK + +- [CLI commands](cli-commands.md) — every command, option, and checked workflow. +- [Python API](python-api.md) — the build entrypoints and advanced package imports. +- [C Support](../language-support/c-support.md) — the direct C lane's source, + contract, and build workflows. + +## Shape the API with contracts + +- [Editing `.pyi` contracts](pyi-contracts/index.md) — the complete supported + editing workflow. +- [Exports and modules](pyi-contracts/exports-and-modules.md) — names, + visibility, and package shape. +- [Functions and classes](pyi-contracts/functions-and-classes.md) — methods, + overloads, and constructors. +- [Calls and results](pyi-contracts/calls-and-results.md) — native call order, + arguments, mutation, and results. + +The contract pages describe the shared generated Python surface. Start from a +contract generated for the same native implementation, then rebuild and call +the changed path once. + +## Diagnose and check support + +- [Diagnostic codes](diagnostic-codes.md) — what a rejected wrapper is telling you. +- [Language feature matrix](../language-support/feature-matrix.md) — whether a + feature is supported at all, with its evidence. diff --git a/docs/user/reference/pyi-contracts/calls-and-results.md b/docs/user/reference/pyi-contracts/calls-and-results.md index 68e25d066..3a7e2c63c 100644 --- a/docs/user/reference/pyi-contracts/calls-and-results.md +++ b/docs/user/reference/pyi-contracts/calls-and-results.md @@ -66,6 +66,84 @@ existing native call; they cannot change what the implementation accepts. The complete projection grammar will be covered by the Semantic `.pyi` Format reference. +## Preserve an Exact C Scalar at the Native Call + +A target-specific C contract may intentionally expose two distinct C types as +the same NumPy dtype. For example, both `long` and `long long` may use signed +64-bit values, so both public signatures use `Int64`. C still treats the two +native types as distinct. + +Use a C scalar cast only around the affected native-call expression: + +```python +from prik.contracts import Arg, CLongLong, Float64, Int64, native_call + +@native_call([CLongLong(Arg(0)), Arg(1)]) +def accumulate(count: Int64, scale: Float64) -> None: ... +``` + +The public annotation is authoritative: the user passes a NumPy `int64`, not a +`numpy.longlong` merely because `CLongLong` appears in `@native_call`. The +binding extracts the public value into `int64_t`, then emits the native call as: + +```c +accumulate((long long)contract_count, contract_scale); +``` + +The same sparse form records a native function result whose C identity was +lost by width-based normalization: + +```python +from prik.contracts import Arg, CLongLong, Float64, Int64, Return, native_call + +@native_call([Arg(0)], result=CLongLong(Return(0))) +def llround(value: Float64) -> Int64: ... +``` + +The decorator position determines the direction. A native scalar wrapper in +the ordered list describes a native parameter; it may wrap `Arg(i)` or an +output-parameter `Return(i)`. In `result=...`, it declares the native function +result. Here the binding declares a `long long` result, receives it, and +converts it into the public `Int64` result slot selected by `Return(0)`. + +Unchanged arguments and results retain their ordinary lowering. Native C scalar +names are call-expression operators: using `CLongLong` or `CLong` as a +function annotation, field type, or return annotation is an error. Generated C +contracts add these operators only when the active target's canonical contract +storage is not C-compatible with the source declaration. + +For a scalar address, conversion happens before taking the address: + +```python +@native_call([Addr(CLongLong(Arg(0)))]) +def update(value: Int64) -> Int64: ... +``` + +This converts the extracted `int64_t` into a `long long` call-local and passes +that local's address, so the callee receives a genuine `long long *`. It never +casts `int64_t *` to an incompatible pointer type. If the updated scalar is a +Python result, the binding converts that call-local back into the public +`Int64` dtype; Python scalar inputs themselves are immutable. + +For a ranked argument, the same operator selects the exact NumPy storage that +can cross the pointer boundary without a cast: + +```python +@native_call([CLongLong(Arg(0))]) +def update_many(values: Int64[:]) -> None: ... +``` + +The public value type remains signed 64-bit integer, but the caller must supply +an array created with `dtype=numpy.longlong` when `long long` is distinct from +the target's canonical `int64_t`. An ordinary `numpy.int64` array is rejected +on that target even when it has the same width and representation. The binding +passes the accepted `numpy.longlong` storage directly as `long long *`; it does +not reinterpret an incompatible pointer or allocate a conversion copy. +This exact-storage rule applies to every supported signed, unsigned, real, and +complex C scalar type with corresponding NumPy storage, including `CLong`, +`CUnsignedLongLong`, and `CLongDoubleComplex`. C `_Bool` arrays remain +unsupported because NumPy Boolean array storage is not C `_Bool` storage. + There is no `intent` annotation in the `.pyi`. The signature, `Returns[...]`, and `@native_call(...)` are the complete contract after the file is loaded. @@ -146,17 +224,20 @@ Use `@raises(...)` when a projected native status should become a Python exception: ```python -from prik.contracts import Addr, Arg, Int32, Return, String, native_call, raises +from prik.contracts import Addr, Arg, Hidden, Int32, 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]]: ... +@native_call([Addr(Arg(0)), Hidden("status", Int32), Hidden("message", String[32])]) +def solve(value: Int32) -> None: ... ``` -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. +Declare the status and any native-only message with `Hidden(name, T)`: it is +produced by the native call but never reaches Python, so it does not appear in +the return annotation. A message may instead name a visible rank-zero NumPy +bytes buffer that the caller supplies. 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. ## Release the GIL for a Native Call diff --git a/docs/user/reference/pyi-contracts/index.md b/docs/user/reference/pyi-contracts/index.md index 5eea2f836..170a5505b 100644 --- a/docs/user/reference/pyi-contracts/index.md +++ b/docs/user/reference/pyi-contracts/index.md @@ -2,7 +2,7 @@ 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, ../semantic-pyi-format.md +related: exports-and-modules.md, functions-and-classes.md, calls-and-results.md status: maintained publication: reviewed --- @@ -13,8 +13,8 @@ prik'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. The complete grammar -will be covered by the Semantic `.pyi` Format reference. +This section explains the supported editing subset and its effect. Start from +the generated contract and make only the documented edits below. ## Workflow diff --git a/docs/user/reference/python-api.md b/docs/user/reference/python-api.md index 983de8a38..ad2b0b812 100644 --- a/docs/user/reference/python-api.md +++ b/docs/user/reference/python-api.md @@ -1,70 +1,126 @@ --- title: Python API Reference -audience: users, developers +audience: users prerequisites: installation -related: cli-commands.md, fortran-wrapper.md, ../../developer/packages/index.md +related: cli-commands.md, ../language-support/c-support.md, ../../developer/packages/index.md status: maintained -publication: draft +publication: reviewed --- # Python API Reference -`prik` is a small normal-user facade. It exposes the installed version and the -three ways to build a wrapper. It does not re-export parser models, semantic -conversion, compiler probes, runtime handles, plans, or CLI implementation. -Import those advanced tools from the package that owns them. +`prik` is a small facade. The root package exposes the installed version and +the four ways to build a wrapper — nothing else. Parser models, semantic +conversion, compiler probes, runtime handles, and plans are imported from the +package that owns them. + ```python import prik -sorted(prik.__all__) +print(sorted(prik.__all__)) +``` + + +```text +['__version__', 'build_c_extension', 'build_fortran_extension', 'build_pyi_extension', 'build_pyi_extension_from_manifest'] ``` ## Root API | Symbol | Use it for | | --- | --- | -| `__version__` | Read the installed PRIK distribution version. | -| `build_fortran_extension` | Build an extension from Fortran source plus optional native-only inputs. | -| `build_pyi_extension` | Build an extension from semantic `.pyi` contracts plus explicit native implementation inputs. | -| `build_pyi_extension_from_manifest` | Replay a saved semantic-`.pyi` build manifest or generate its Makefile. | +| `__version__` | The installed PRIK distribution version. | +| `build_c_extension` | Build C extensions from source within the documented support boundary. | +| `build_fortran_extension` | Build from Fortran source, plus optional native-only inputs. | +| `build_pyi_extension` | Build from semantic `.pyi` contracts, plus explicit native implementation inputs. | +| `build_pyi_extension_from_manifest` | Replay a saved `.pyi` build manifest, or generate its Makefile. | + +## Building an extension -For normal builds, import directly from the root: +Every build entrypoint returns a `WrapperBuildResult`. Call `import_module()` +on it to load the extension without editing `sys.path`: + ```python +from pathlib import Path +from tempfile import TemporaryDirectory + from prik import build_fortran_extension -result = build_fortran_extension("solver.f90", output_dir="build/solver") -module = result.import_module() +source = Path("tests/fortran/infrastructure/building/end_to_end/fixtures/native/fruntime_abi_f90.f90") +with TemporaryDirectory() as output_dir: + build = build_fortran_extension(source, output_dir=output_dir) + print(build.module_name) + print(type(build).__module__ + "." + type(build).__name__) +``` + + +```text +fruntime_abi_f90 +prik.pipeline.build.WrapperBuildResult +``` + +Import `WrapperBuildResult` and the native-build plan records from +`prik.pipeline.build` only when you need to inspect or construct them. + +### Build a supported C source + +Use `build_c_extension` for C source builds. The source must fit the current +[C Support](../language-support/c-support.md) contract; broader C declarations +are not adapted automatically. + +```python +import numpy as np + +from prik import build_c_extension + +build = build_c_extension("native_math.c", output_dir="build") +native_math = build.import_module() +print(native_math.add(np.float64(3.0), np.float64(2.5))) ``` -The functions return `prik.pipeline.build.WrapperBuildResult`. Import result -models and native-build plan records from `prik.pipeline.build` only when you -need to inspect or construct those advanced values. +For an authored C semantic contract, use `build_pyi_extension` with +`native_language="c"` and `native_c_sources=[...]`. The [C Support +guide](../language-support/c-support.md#author-a-contract-for-pointers-and-arrays) +shows the complete contract and build. -## Advanced Package Imports +## Advanced package imports + +Reach past the root facade when you need a single stage rather than a build. | Need | Import from | Main entrypoints | | --- | --- | --- | | Fortran source facts and diagnostics | `prik.parsers.fortran` | `parse_fortran_file`, `parse_fortran_project`, `FortranParser`, parser models, `FortranParseError` | +| C source facts and diagnostics | `prik.parsers.c` | `parse_c_file`, `parse_c_project`, `CParser`, parser models, `CParseError` | | Raw semantic `.pyi` syntax | `prik.parsers.pyi` | `parse_pyi_text`, `parse_pyi_file` | -| Semantic conversion | `prik.semantics.fortran2ir` or `prik.semantics.pyi2ir` | Fortran conversion helpers or `convert_pyi_to_ir` | +| Semantic conversion | `prik.semantics.fortran2ir`, `prik.semantics.pyi2ir` | Fortran conversion helpers, `convert_pyi_to_ir` | +| C semantic conversion | `prik.semantics.c2ir` | `CToIRConverter`, `c_file_to_semantic_module`, `c_file_to_semantic_modules` | | `.pyi` loading and stub emission | `prik.pipeline.pyi` | `pyi_*_to_semantic_module`, `emit_module_stubs` | | Build records and results | `prik.pipeline.build` | `WrapperBuildResult`, `NativeBuildPlan`, `NativeCompilationUnit`, `NativePrebuiltArtifact`, `NativeLinkItem` | -| Target type probing | `prik.preprocessing.probes.fortran_types` | probe source, requirements, expressions, and report/error types | +| Target type probing | `prik.preprocessing.probes.fortran_types` | probe source, requirements, expressions, report and error types | +| C target type probing | `prik.preprocessing.probes.c_types` | `probe_c_standard_types`, `probe_c_standard_types_cached`, and C probe records/error type | | Runtime descriptor handles | `prik.runtime.handles` | `NativeArrayHandleBase`, `AllocatableArray`, `PointerArray` | | Semantic `.pyi` vocabulary | `prik.contracts` | scalar, array, ownership, and native-call contract markers | -| CLI implementation | `prik.cli` | `main()`; shell users should run `python3 -m prik` instead | - -The [Fortran wrapper reference](fortran-wrapper.md) documents the normal build -functions. The [package guides](../../developer/packages/index.md) explain -advanced module responsibilities and their focused tests. +| CLI implementation | `prik.cli` | `main()` — shell users should run `python3 -m prik` instead | -## Current Boundaries +## Boundaries -- Root imports are intentionally small and do not load parser or semantic - implementation modules. +- Root imports stay small and do not load parser or semantic implementation + modules. - A parser success is only a source fact. Semantic conversion, policy - completion, planning, and generation are separate stages. -- The C-input frontend is deferred from the published workflow. Its internal - parser package is not a root API. + completion, planning, and generation are separate stages that can each + reject input the parser accepted. +- C source builds are limited to the documented direct-only primitive lane. + Other parser-accepted C forms fail before wrapper planning rather than using + a generated adapter. + +## Related pages + +- [CLI Commands](cli-commands.md) — the same workflows from a shell. +- [C Support](../language-support/c-support.md) — C source, contract, CLI, and + Python workflows. +- [Editing `.pyi` Contracts](pyi-contracts/index.md) — supported API-shaping + edits. +- [Package guides](../../developer/packages/index.md) — module responsibilities + and their focused tests. diff --git a/docs/user/reference/semantic-ir.md b/docs/user/reference/semantic-ir.md index 878841c4f..bebbcce48 100644 --- a/docs/user/reference/semantic-ir.md +++ b/docs/user/reference/semantic-ir.md @@ -18,9 +18,10 @@ PRIK_C_DOCS_END --> @@ -30,7 +31,7 @@ PRIK_C_DOCS_END --> This document records the shared scalar datatype policy used when C and Fortran parser facts are converted to semantic IR. The semantic names are the stable bridge between parser-native type spellings, `.pyi` output, policy completion, -the implemented Fortran wrapper, and a future C-input wrapper backend. +the implemented Fortran wrapper, and the direct-only primitive C backend. PRIK_C_DOCS_END --> ### Semantic Names @@ -502,9 +503,10 @@ work includes: PRIK_C_DOCS_END --> PRIK_C_DOCS_END --> +## Character Length And Shape + +A `String` annotation carries two independent facts. The first subscription is +the character length; the second, when present, is the scalar-storage or array +shape. + +| Contract | Character length | Python/storage shape | +| --- | --- | --- | +| `String` | assumed | scalar | +| `String[...]` | assumed | scalar | +| `String[8]` | explicit `8` | scalar | +| `String[n]` | explicit `n` | scalar | +| `String[:]` | deferred | scalar | +| `String[8][()]` | explicit `8` | rank-0 storage | +| `String[8][:]` | explicit `8` | contiguous rank-1 | +| `String[8][::]` | explicit `8` | stride-aware rank-1 | +| `String[8][n]` | explicit `8` | extent `n` | +| `String[...][:]` | assumed | contiguous rank-1 | +| `String[...][::]` | assumed | stride-aware rank-1 | +| `String[...][n]` | assumed | extent `n` | +| `String[:][:]` | deferred | contiguous rank-1 | +| `String[:][::]` | deferred | stride-aware rank-1 | + +Bare `String` is the scalar shorthand for `String[...]`. Because an array always +spells its length first, a single subscription is never a shape: `String[::]` is +rejected with a diagnostic naming the second-subscription form. + +The three lengths mean different things at the native boundary: + +- `String[...]` is `character(len=*)`: the actual argument fixes the length for + the call, and native code cannot change it. +- `String[8]` is `character(len=8)`: the length is part of the contract, and the + wrapper requires exactly that many encoded bytes. +- `String[:]` is `character(len=:)`: the length is established by allocation and + may change during the call, so the dummy also needs `allocatable` or + `pointer` storage. A `String[:]` output is `None` when it is unallocated. + +The length is independent of the descriptor attribute. `Allocatable(Arg(i))` +and `Pointer(Arg(i))` name the attribute of the native dummy, and either one +combines with `String[n]` or `String[:]`: + +```python +@native_call([Allocatable(Arg(0))]) +def grow(value: String[:] | None) -> Returns["value", String[:]] | None: ... + +@native_call([Pointer(Arg(0))]) +def relabel(value: String[4] | None) -> Returns["value", String[4]] | None: ... + +@native_call([], result=Allocatable(Return(0))) +def build() -> String[:] | None: ... +``` + +A scalar character dummy with either attribute is a `str` argument that also +projects a result, because the native procedure may replace the storage rather +than write through it. The projected result is `None` when the procedure leaves +the dummy unallocated or unassociated. + ## Python And Native Boundaries Semantic `.pyi` annotations describe two related but separate boundaries: @@ -978,6 +1036,7 @@ arguments, or scalar by-address projection differs from the default lowering. | `Float64[()]` | rank-zero NumPy array with dtype `np.float64` | storage address | | `Float64[n]`, `Float64[:]`, `Float64[:, :]` | NumPy array storage | data address | | `String[n]` | Python `str` whose encoded length is exactly `n` | address of prik's call-local fixed-width character storage | +| `String[:]` | Python `str`; `None` when an output is unallocated | deferred-length character local built by the generated adapter, carrying the attribute `Allocatable(...)` or `Pointer(...)` names | | `String[n][:]`, `String[:][:]` | NumPy bytes array storage | character array descriptor/data contract | | `String[n][()]` | rank-zero NumPy bytes array with dtype `S` | fixed-width character storage copied back into the NumPy array when native code mutates it | | `Addr(Float64)`, `Addr(Float64[n])`, `Addr(String[n])` | integer raw address such as `array.ctypes.data` or a `ctypes` buffer address | that raw address | @@ -1293,7 +1352,7 @@ Loaded compatibility metadata: | --- | --- | | `Contiguous` | source provenance says the array is contiguous | | `ArrayCategory("...")` | source array category provenance | -| `FortranAllocatable` | older scalar character allocatable metadata; generated contracts use `Allocatable[String]` | +| `FortranAllocatable` | older scalar character allocatable metadata; generated contracts use `Allocatable[String[:]]` | +```bash +export EXAMPLE_WORKSPACE="$PWD" +export BSPLINE_BUILD_ROOT="$(mktemp -d)" + +mkdir -p "$BSPLINE_BUILD_ROOT/prik/generated" +cd "$BSPLINE_BUILD_ROOT/prik" + +python3 -m prik \ + "$EXAMPLE_WORKSPACE/examples/bspline/native/bspline_kinds_module.F90" \ + "$EXAMPLE_WORKSPACE/examples/bspline/native/bspline_sub_module.f90" \ + "$EXAMPLE_WORKSPACE/examples/bspline/native/bspline_oo_module.f90" \ + --out prik_bspline \ + --out-dir "$BSPLINE_BUILD_ROOT/prik/generated" \ + --compiler "$(command -v gfortran)" \ + --jobs 8 \ + --wrapper-fortran-flags="-O0 -g0" \ + --wrapper-c-flags="-O0 -g0" +``` + +`-O0` keeps the example focused on correctness. The build writes its generated +contract beside the extension; it does not edit the upstream source. + +## The Python API + +```python +import numpy as np +import prik_bspline.bspline_oo_module as bspline + +x = np.linspace(0.0, 2.0 * np.pi, 25) +spline = bspline.bspline_1d(x, np.sin(x), np.int32(4)) # generic constructor + +value, iflag = spline.evaluate(np.float64(1.234), np.int32(0)) +print(value) # about 0.943811 + +area, iflag = spline.integral(np.float64(0.0), np.float64(np.pi)) +print(area) # about 2.0 +``` + +The abstract base is present but cannot be constructed: + +```python +bspline.bspline_class() +# TypeError: bspline_class is an abstract native type and cannot be +# instantiated; create one of its concrete extensions instead + +issubclass(bspline.bspline_1d, bspline.bspline_class) # True +``` + +## Run focused tests + +After the quick-start build, run one interface family or routine: + +```bash +python3 -m pytest -q examples/bspline/tests/test_object_oriented_api.py +python3 -m pytest -q examples/bspline/tests/test_procedural_api.py::test_db1ink +python3 -m pytest -q examples/bspline/tests -k db6 +``` + +## What is validated + +The suite builds every public procedural family from one to six dimensions, +then evaluates an affine function through every generated evaluator. It also +checks one-dimensional analytic values, derivatives, definite integrals, and +callback-driven integration, plus a SciPy interpolation comparison. The +object-oriented tests construct and evaluate every concrete spline class, and +check the abstract-base, inheritance, deferred-binding, and generic-constructor +contracts. + +The routine-coverage test compares the reviewed inventory with the generated +exports and requires one named numerical test for every procedural routine. + +## Scope + +The upstream `bspline_defc_module` (least-squares fitting) and its +`bspline_blas_module` bridge are intentionally outside this interpolation +example. [`routine_inventory.py`](routine_inventory.py) records that boundary. + +## Upstream + +BSPLINE-FORTRAN is by Jacob Williams and is distributed under a BSD-3-Clause +licence, included at [`native/LICENSE`](native/LICENSE). The vendored sources +are version 7.4.0 (commit `047c7244`). diff --git a/examples/bspline/__init__.py b/examples/bspline/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/bspline/build_all.sh b/examples/bspline/build_all.sh new file mode 100644 index 000000000..59e783a5d --- /dev/null +++ b/examples/bspline/build_all.sh @@ -0,0 +1,3 @@ +source examples/bspline/build_prik.sh +cd "$EXAMPLE_WORKSPACE" +export PYTHONPATH="$BSPLINE_BUILD_ROOT/prik${PYTHONPATH:+:$PYTHONPATH}" diff --git a/examples/bspline/build_prik.sh b/examples/bspline/build_prik.sh new file mode 100644 index 000000000..47d75fb48 --- /dev/null +++ b/examples/bspline/build_prik.sh @@ -0,0 +1,16 @@ +export EXAMPLE_WORKSPACE="$PWD" +export BSPLINE_BUILD_ROOT="$(mktemp -d)" + +mkdir -p "$BSPLINE_BUILD_ROOT/prik/generated" +cd "$BSPLINE_BUILD_ROOT/prik" + +python3 -m prik \ + "$EXAMPLE_WORKSPACE/examples/bspline/native/bspline_kinds_module.F90" \ + "$EXAMPLE_WORKSPACE/examples/bspline/native/bspline_sub_module.f90" \ + "$EXAMPLE_WORKSPACE/examples/bspline/native/bspline_oo_module.f90" \ + --out prik_bspline \ + --out-dir "$BSPLINE_BUILD_ROOT/prik/generated" \ + --compiler "$(command -v gfortran)" \ + --jobs 8 \ + --wrapper-fortran-flags="-O0 -g0" \ + --wrapper-c-flags="-O0 -g0" diff --git a/examples/bspline/conftest.py b/examples/bspline/conftest.py new file mode 100644 index 000000000..bf5c16cec --- /dev/null +++ b/examples/bspline/conftest.py @@ -0,0 +1,17 @@ +"""Import the BSPLINE-FORTRAN extension built by ``build_all.sh``.""" + +import importlib + +import pytest + + +@pytest.fixture(scope="session") +def bspline_oo(): + """Return the object-oriented B-spline namespace.""" + return importlib.import_module("prik_bspline").bspline_oo_module + + +@pytest.fixture(scope="session") +def bspline_sub(): + """Return the procedural B-spline namespace.""" + return importlib.import_module("prik_bspline").bspline_sub_module diff --git a/examples/bspline/native/LICENSE b/examples/bspline/native/LICENSE new file mode 100644 index 000000000..dc5bb75cd --- /dev/null +++ b/examples/bspline/native/LICENSE @@ -0,0 +1,125 @@ +BSPLINE-FORTRAN: Multidimensional B-Spline Interpolation of Data on a Regular Grid + +Copyright (c) 2015-2023, Jacob Williams +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +* The names of its contributors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +!----------------------------------------------------------------------------------------- +! +! BSPLINE-FORTRAN includes code from CMLIB, a public domain library +! from the National Institute of Standards and Technology (NIST) +! +! The CMLIB license is given below: +! +!----------------------------------------------------------------------------------------- + +The research software provided on this web site ("software") is provided by NIST as a +public service. You may use, copy and distribute copies of the software in any medium, +provided that you keep intact this entire notice. You may improve, modify and create +derivative works of the software or any portion of the software, and you may copy and +distribute such modifications or works. Modified works should carry a notice stating that +you changed the software and should note the date and nature of any such change. Please +explicitly acknowledge the National Institute of Standards and Technology as the source +of the software. + +The software is expressly provided "AS IS." NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, +IMPLIED, IN FACT OR ARISING BY OPERATION OF LAW, INCLUDING, WITHOUT LIMITATION, THE +IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT +AND DATA ACCURACY. NIST NEITHER REPRESENTS NOR WARRANTS THAT THE OPERATION OF THE SOFTWARE +WILL BE UNINTERRUPTED OR ERROR-FREE, OR THAT ANY DEFECTS WILL BE CORRECTED. NIST DOES NOT +WARRANT OR MAKE ANY REPRESENTATIONS REGARDING THE USE OF THE SOFTWARE OR THE RESULTS +THEREOF, INCLUDING BUT NOT LIMITED TO THE CORRECTNESS, ACCURACY, RELIABILITY, OR +USEFULNESS OF THE SOFTWARE. + +You are solely responsible for determining the appropriateness of using and distributing +the software and you assume all risks associated with its use, including but not limited +to the risks and costs of program errors, compliance with applicable laws, damage to or +loss of data, programs or equipment, and the unavailability or interruption of operation. +This software is not intended to be used in any situation where a failure could cause risk +of injury or damage to property. The software was developed by NIST employees. NIST +employee contributions are not subject to copyright protection within the United States. + +!----------------------------------------------------------------------------------------- +! LAPACK License +!----------------------------------------------------------------------------------------- + +Copyright (c) 1992-2022 The University of Tennessee and The University + of Tennessee Research Foundation. All rights + reserved. +Copyright (c) 2000-2022 The University of California Berkeley. All + rights reserved. +Copyright (c) 2006-2022 The University of Colorado Denver. All rights + reserved. + +$COPYRIGHT$ + +Additional copyrights may follow + +$HEADER$ + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer listed + in this license in the documentation and/or other materials + provided with the distribution. + +- Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +The copyright holders provide no reassurances that the source code +provided does not infringe any patent, copyright, or any other +intellectual property rights of third parties. The copyright holders +disclaim any liability to any recipient for claims brought against +recipient by any third party for infringement of that parties +intellectual property rights. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +!----------------------------------------------------------------------------------------- +! +! BSPLINE-FORTRAN includes code from the SLATEC Common Mathematical Library, +! A public domain work of the U.S. government. +! +! https://netlib.org/slatec/ +! +!----------------------------------------------------------------------------------------- diff --git a/examples/bspline/native/bspline_kinds_module.F90 b/examples/bspline/native/bspline_kinds_module.F90 new file mode 100644 index 000000000..9330acd19 --- /dev/null +++ b/examples/bspline/native/bspline_kinds_module.F90 @@ -0,0 +1,40 @@ +!***************************************************************************************** +!> author: Jacob Williams +! license: BSD +! +!### Description +! Numeric kind definitions for BSpline-Fortran. + + module bspline_kinds_module + + use,intrinsic :: iso_fortran_env + + implicit none + + private + +#ifdef REAL32 + integer,parameter,public :: wp = real32 !! Real working precision [4 bytes] +#elif REAL64 + integer,parameter,public :: wp = real64 !! Real working precision [8 bytes] +#elif REAL128 + integer,parameter,public :: wp = real128 !! Real working precision [16 bytes] +#else + integer,parameter,public :: wp = real64 !! Real working precision if not specified [8 bytes] +#endif + +#ifdef INT8 + integer,parameter,public :: ip = int8 !! Integer working precision [1 byte] +#elif INT16 + integer,parameter,public :: ip = int16 !! Integer working precision [2 bytes] +#elif INT32 + integer,parameter,public :: ip = int32 !! Integer working precision [4 bytes] +#elif INT64 + integer,parameter,public :: ip = int64 !! Integer working precision [8 bytes] +#else + integer,parameter,public :: ip = int32 !! Integer working precision if not specified [4 bytes] +#endif + +!***************************************************************************************** + end module bspline_kinds_module +!***************************************************************************************** diff --git a/examples/bspline/native/bspline_oo_module.f90 b/examples/bspline/native/bspline_oo_module.f90 new file mode 100644 index 000000000..0a7c57495 --- /dev/null +++ b/examples/bspline/native/bspline_oo_module.f90 @@ -0,0 +1,2823 @@ +!***************************************************************************************** +!> author: Jacob Williams +! license: BSD +! date: 12/6/2015 +! +! Object-oriented style wrappers to [[bspline_sub_module]]. +! This module provides classes ([[bspline_1d(type)]], [[bspline_2d(type)]], +! [[bspline_3d(type)]], [[bspline_4d(type)]], [[bspline_5d(type)]], and [[bspline_6d(type)]]) +! which can be used instead of the main subroutine interface. + + module bspline_oo_module + + use bspline_kinds_module, only: wp, ip + use,intrinsic :: iso_fortran_env, only: error_unit + use bspline_sub_module + + implicit none + + private + + integer(ip),parameter :: int_size = storage_size(1_ip,kind=ip) !! size of a default integer [bits] + integer(ip),parameter :: logical_size = storage_size(.true.,kind=ip) !! size of a default logical [bits] + integer(ip),parameter :: real_size = storage_size(1.0_wp,kind=ip) !! size of a `real(wp)` [bits] + + type,public,abstract :: bspline_class + !! Base class for the b-spline types + private + integer(ip) :: inbvx = 1_ip !! internal variable used by [[dbvalu]] for efficient processing + integer(ip) :: iflag = 1_ip !! saved `iflag` from the list routine call. + logical :: initialized = .false. !! true if the class is initialized and ready to use + logical :: extrap = .false. !! if true, then extrapolation is allowed during evaluation + contains + private + procedure,non_overridable :: destroy_base !! destructor for the abstract type + procedure,non_overridable :: set_extrap_flag !! internal routine to set the `extrap` flag + procedure(destroy_func),deferred,public :: destroy !! destructor + procedure(size_func),deferred,public :: size_of !! size of the structure in bits + procedure,public,non_overridable :: status_ok !! returns true if the last `iflag` status code was `=0`. + procedure,public,non_overridable :: status_message => get_bspline_status_message !! retrieve the last + !! status message + procedure,public,non_overridable :: clear_flag => clear_bspline_flag !! to reset the `iflag` saved in the class. + end type bspline_class + + abstract interface + + pure subroutine destroy_func(me) + !! interface for bspline destructor routines + import :: bspline_class + implicit none + class(bspline_class),intent(inout) :: me + end subroutine destroy_func + + pure function size_func(me) result(s) + !! interface for size routines + import :: bspline_class,ip + implicit none + class(bspline_class),intent(in) :: me + integer(ip) :: s !! size of the structure in bits + end function size_func + + end interface + + type,extends(bspline_class),public :: bspline_1d + !! Class for 1d b-spline interpolation. + !! + !!@note The 1D class also contains two methods + !! for computing definite integrals. + private + integer(ip) :: nx = 0_ip !! Number of \(x\) abcissae + integer(ip) :: kx = 0_ip !! The order of spline pieces in \(x\) + real(wp),dimension(:),allocatable :: bcoef !! array of coefficients of the b-spline interpolant + real(wp),dimension(:),allocatable :: tx !! The knots in the \(x\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: work_val_1 !! [[db1val] work array of dimension `3*kx` + contains + private + generic,public :: initialize => initialize_1d_auto_knots,initialize_1d_specify_knots + procedure :: initialize_1d_auto_knots + procedure :: initialize_1d_specify_knots + procedure,public :: evaluate => evaluate_1d + procedure,public :: destroy => destroy_1d + procedure,public :: size_of => size_1d + procedure,public :: integral => integral_1d + procedure,public :: fintegral => fintegral_1d + final :: finalize_1d + end type bspline_1d + + type,extends(bspline_class),public :: bspline_2d + !! Class for 2d b-spline interpolation. + private + integer(ip) :: nx = 0_ip !! Number of \(x\) abcissae + integer(ip) :: ny = 0_ip !! Number of \(y\) abcissae + integer(ip) :: kx = 0_ip !! The order of spline pieces in \(x\) + integer(ip) :: ky = 0_ip !! The order of spline pieces in \(y\) + real(wp),dimension(:,:),allocatable :: bcoef !! array of coefficients of the b-spline interpolant + real(wp),dimension(:),allocatable :: tx !! The knots in the \(x\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: ty !! The knots in the \(y\) direction for the spline interpolant + integer(ip) :: inbvy = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloy = 1_ip !! internal variable used for efficient processing + real(wp),dimension(:),allocatable :: work_val_1 !! [[db2val] work array of dimension `ky` + real(wp),dimension(:),allocatable :: work_val_2 !! [[db2val] work array of dimension `3_ip*max(kx,ky)` + contains + private + generic,public :: initialize => initialize_2d_auto_knots,initialize_2d_specify_knots + procedure :: initialize_2d_auto_knots + procedure :: initialize_2d_specify_knots + procedure,public :: evaluate => evaluate_2d + procedure,public :: destroy => destroy_2d + procedure,public :: size_of => size_2d + final :: finalize_2d + end type bspline_2d + + type,extends(bspline_class),public :: bspline_3d + !! Class for 3d b-spline interpolation. + private + integer(ip) :: nx = 0_ip !! Number of \(x\) abcissae + integer(ip) :: ny = 0_ip !! Number of \(y\) abcissae + integer(ip) :: nz = 0_ip !! Number of \(z\) abcissae + integer(ip) :: kx = 0_ip !! The order of spline pieces in \(x\) + integer(ip) :: ky = 0_ip !! The order of spline pieces in \(y\) + integer(ip) :: kz = 0_ip !! The order of spline pieces in \(z\) + real(wp),dimension(:,:,:),allocatable :: bcoef !! array of coefficients of the b-spline interpolant + real(wp),dimension(:),allocatable :: tx !! The knots in the \(x\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: ty !! The knots in the \(y\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: tz !! The knots in the \(z\) direction for the spline interpolant + integer(ip) :: inbvy = 1_ip !! internal variable used for efficient processing + integer(ip) :: inbvz = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloy = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloz = 1_ip !! internal variable used for efficient processing + real(wp),dimension(:,:),allocatable :: work_val_1 !! [[db3val] work array of dimension `ky,kz` + real(wp),dimension(:),allocatable :: work_val_2 !! [[db3val] work array of dimension `kz` + real(wp),dimension(:),allocatable :: work_val_3 !! [[db3val] work array of dimension `3_ip*max(kx,ky,kz)` + contains + private + generic,public :: initialize => initialize_3d_auto_knots,initialize_3d_specify_knots + procedure :: initialize_3d_auto_knots + procedure :: initialize_3d_specify_knots + procedure,public :: evaluate => evaluate_3d + procedure,public :: destroy => destroy_3d + procedure,public :: size_of => size_3d + final :: finalize_3d + end type bspline_3d + + type,extends(bspline_class),public :: bspline_4d + !! Class for 4d b-spline interpolation. + private + integer(ip) :: nx = 0_ip !! Number of \(x\) abcissae + integer(ip) :: ny = 0_ip !! Number of \(y\) abcissae + integer(ip) :: nz = 0_ip !! Number of \(z\) abcissae + integer(ip) :: nq = 0_ip !! Number of \(q\) abcissae + integer(ip) :: kx = 0_ip !! The order of spline pieces in \(x\) + integer(ip) :: ky = 0_ip !! The order of spline pieces in \(y\) + integer(ip) :: kz = 0_ip !! The order of spline pieces in \(z\) + integer(ip) :: kq = 0_ip !! The order of spline pieces in \(q\) + real(wp),dimension(:,:,:,:),allocatable :: bcoef !! array of coefficients of the b-spline interpolant + real(wp),dimension(:),allocatable :: tx !! The knots in the \(x\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: ty !! The knots in the \(y\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: tz !! The knots in the \(z\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: tq !! The knots in the \(q\) direction for the spline interpolant + integer(ip) :: inbvy = 1_ip !! internal variable used for efficient processing + integer(ip) :: inbvz = 1_ip !! internal variable used for efficient processing + integer(ip) :: inbvq = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloy = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloz = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloq = 1_ip !! internal variable used for efficient processing + real(wp),dimension(:,:,:),allocatable :: work_val_1 !! [[db4val]] work array of dimension `ky,kz,kq` + real(wp),dimension(:,:),allocatable :: work_val_2 !! [[db4val]] work array of dimension `kz,kq` + real(wp),dimension(:),allocatable :: work_val_3 !! [[db4val]] work array of dimension `kq` + real(wp),dimension(:),allocatable :: work_val_4 !! [[db4val]] work array of dimension `3_ip*max(kx,ky,kz,kq)` + contains + private + generic,public :: initialize => initialize_4d_auto_knots,initialize_4d_specify_knots + procedure :: initialize_4d_auto_knots + procedure :: initialize_4d_specify_knots + procedure,public :: evaluate => evaluate_4d + procedure,public :: destroy => destroy_4d + procedure,public :: size_of => size_4d + final :: finalize_4d + end type bspline_4d + + type,extends(bspline_class),public :: bspline_5d + !! Class for 5d b-spline interpolation. + private + integer(ip) :: nx = 0_ip !! Number of \(x\) abcissae + integer(ip) :: ny = 0_ip !! Number of \(y\) abcissae + integer(ip) :: nz = 0_ip !! Number of \(z\) abcissae + integer(ip) :: nq = 0_ip !! Number of \(q\) abcissae + integer(ip) :: nr = 0_ip !! Number of \(r\) abcissae + integer(ip) :: kx = 0_ip !! The order of spline pieces in \(x\) + integer(ip) :: ky = 0_ip !! The order of spline pieces in \(y\) + integer(ip) :: kz = 0_ip !! The order of spline pieces in \(z\) + integer(ip) :: kq = 0_ip !! The order of spline pieces in \(q\) + integer(ip) :: kr = 0_ip !! The order of spline pieces in \(r\) + real(wp),dimension(:,:,:,:,:),allocatable :: bcoef !! array of coefficients of the b-spline interpolant + real(wp),dimension(:),allocatable :: tx !! The knots in the \(x\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: ty !! The knots in the \(y\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: tz !! The knots in the \(z\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: tq !! The knots in the \(q\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: tr !! The knots in the \(r\) direction for the spline interpolant + integer(ip) :: inbvy = 1_ip !! internal variable used for efficient processing + integer(ip) :: inbvz = 1_ip !! internal variable used for efficient processing + integer(ip) :: inbvq = 1_ip !! internal variable used for efficient processing + integer(ip) :: inbvr = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloy = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloz = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloq = 1_ip !! internal variable used for efficient processing + integer(ip) :: ilor = 1_ip !! internal variable used for efficient processing + real(wp),dimension(:,:,:,:),allocatable :: work_val_1 !! [[db5val]] work array of dimension `ky,kz,kq,kr` + real(wp),dimension(:,:,:),allocatable :: work_val_2 !! [[db5val]] work array of dimension `kz,kq,kr` + real(wp),dimension(:,:),allocatable :: work_val_3 !! [[db5val]] work array of dimension `kq,kr` + real(wp),dimension(:),allocatable :: work_val_4 !! [[db5val]] work array of dimension `kr` + real(wp),dimension(:),allocatable :: work_val_5 !! [[db5val]] work array of dimension `3_ip*max(kx,ky,kz,kq,kr)` + contains + private + generic,public :: initialize => initialize_5d_auto_knots,initialize_5d_specify_knots + procedure :: initialize_5d_auto_knots + procedure :: initialize_5d_specify_knots + procedure,public :: evaluate => evaluate_5d + procedure,public :: destroy => destroy_5d + procedure,public :: size_of => size_5d + final :: finalize_5d + end type bspline_5d + + type,extends(bspline_class),public :: bspline_6d + !! Class for 6d b-spline interpolation. + private + integer(ip) :: nx = 0_ip !! Number of \(x\) abcissae + integer(ip) :: ny = 0_ip !! Number of \(y\) abcissae + integer(ip) :: nz = 0_ip !! Number of \(z\) abcissae + integer(ip) :: nq = 0_ip !! Number of \(q\) abcissae + integer(ip) :: nr = 0_ip !! Number of \(r\) abcissae + integer(ip) :: ns = 0_ip !! Number of \(s\) abcissae + integer(ip) :: kx = 0_ip !! The order of spline pieces in \(x\) + integer(ip) :: ky = 0_ip !! The order of spline pieces in \(y\) + integer(ip) :: kz = 0_ip !! The order of spline pieces in \(z\) + integer(ip) :: kq = 0_ip !! The order of spline pieces in \(q\) + integer(ip) :: kr = 0_ip !! The order of spline pieces in \(r\) + integer(ip) :: ks = 0_ip !! The order of spline pieces in \(s\) + real(wp),dimension(:,:,:,:,:,:),allocatable :: bcoef !! array of coefficients of the b-spline interpolant + real(wp),dimension(:),allocatable :: tx !! The knots in the \(x\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: ty !! The knots in the \(y\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: tz !! The knots in the \(z\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: tq !! The knots in the \(q\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: tr !! The knots in the \(r\) direction for the spline interpolant + real(wp),dimension(:),allocatable :: ts !! The knots in the \(s\) direction for the spline interpolant + integer(ip) :: inbvy = 1_ip !! internal variable used for efficient processing + integer(ip) :: inbvz = 1_ip !! internal variable used for efficient processing + integer(ip) :: inbvq = 1_ip !! internal variable used for efficient processing + integer(ip) :: inbvr = 1_ip !! internal variable used for efficient processing + integer(ip) :: inbvs = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloy = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloz = 1_ip !! internal variable used for efficient processing + integer(ip) :: iloq = 1_ip !! internal variable used for efficient processing + integer(ip) :: ilor = 1_ip !! internal variable used for efficient processing + integer(ip) :: ilos = 1_ip !! internal variable used for efficient processing + real(wp),dimension(:,:,:,:,:),allocatable :: work_val_1 !! [[db6val]] work array of dimension `ky,kz,kq,kr,ks` + real(wp),dimension(:,:,:,:),allocatable :: work_val_2 !! [[db6val]] work array of dimension `kz,kq,kr,ks` + real(wp),dimension(:,:,:),allocatable :: work_val_3 !! [[db6val]] work array of dimension `kq,kr,ks` + real(wp),dimension(:,:),allocatable :: work_val_4 !! [[db6val]] work array of dimension `kr,ks` + real(wp),dimension(:),allocatable :: work_val_5 !! [[db6val]] work array of dimension `ks` + real(wp),dimension(:),allocatable :: work_val_6 !! [[db6val]] work array of dimension `3_ip*max(kx,ky,kz,kq,kr,ks)` + contains + private + generic,public :: initialize => initialize_6d_auto_knots,initialize_6d_specify_knots + procedure :: initialize_6d_auto_knots + procedure :: initialize_6d_specify_knots + procedure,public :: evaluate => evaluate_6d + procedure,public :: destroy => destroy_6d + procedure,public :: size_of => size_6d + final :: finalize_6d + end type bspline_6d + + interface bspline_1d + !! Constructor for [[bspline_1d(type)]] + procedure :: bspline_1d_constructor_empty,& + bspline_1d_constructor_auto_knots,& + bspline_1d_constructor_specify_knots + end interface + interface bspline_2d + !! Constructor for [[bspline_2d(type)]] + procedure :: bspline_2d_constructor_empty,& + bspline_2d_constructor_auto_knots,& + bspline_2d_constructor_specify_knots + end interface + interface bspline_3d + !! Constructor for [[bspline_3d(type)]] + procedure :: bspline_3d_constructor_empty,& + bspline_3d_constructor_auto_knots,& + bspline_3d_constructor_specify_knots + end interface + interface bspline_4d + !! Constructor for [[bspline_4d(type)]] + procedure :: bspline_4d_constructor_empty,& + bspline_4d_constructor_auto_knots,& + bspline_4d_constructor_specify_knots + end interface + interface bspline_5d + !! Constructor for [[bspline_5d(type)]] + procedure :: bspline_5d_constructor_empty,& + bspline_5d_constructor_auto_knots,& + bspline_5d_constructor_specify_knots + end interface + interface bspline_6d + !! Constructor for [[bspline_6d(type)]] + procedure :: bspline_6d_constructor_empty,& + bspline_6d_constructor_auto_knots,& + bspline_6d_constructor_specify_knots + end interface + + contains +!***************************************************************************************** + +!***************************************************************************************** +!> +! This routines returns true if the `iflag` code from the last +! routine called was `=0`. Maybe of the routines have output `iflag` +! variables, so they can be checked explicitly, or this routine +! can be used. +! +! If the class is initialized using a function constructor, then +! this is the only way to know if it was properly initialized, +! since those are pure functions with not output `iflag` arguments. +! +! If `status_ok=.false.`, then the error message can be +! obtained from the [[get_bspline_status_message]] routine. +! +! Note: after an error condition, the [[clear_bspline_flag]] routine +! can be called to reset the `iflag` to 0. + + elemental function status_ok(me) result(ok) + + implicit none + + class(bspline_class),intent(in) :: me + logical :: ok + + ok = ( me%iflag == 0_ip ) + + end function status_ok +!***************************************************************************************** + +!***************************************************************************************** +!> +! This sets the `iflag` variable in the class to `0` +! (which indicates that everything is OK). It can be used +! after an error is encountered. + + elemental subroutine clear_bspline_flag(me) + + implicit none + + class(bspline_class),intent(inout) :: me + + me%iflag = 0_ip + + end subroutine clear_bspline_flag +!***************************************************************************************** + +!***************************************************************************************** +!> +! Get the status message from a [[bspline_class]] routine call. +! +! If `iflag` is not included, then the one in the class is used (which +! corresponds to the last routine called.) +! Otherwise, it will convert the +! input `iflag` argument into the appropriate message. +! +! This is a wrapper for [[get_status_message]]. + + pure function get_bspline_status_message(me,iflag) result(msg) + + implicit none + + class(bspline_class),intent(in) :: me + character(len=:),allocatable :: msg !! status message associated with the flag + integer(ip),intent(in),optional :: iflag !! the corresponding status code + + if (present(iflag)) then + msg = get_status_message(iflag) + else + msg = get_status_message(me%iflag) + end if + + end function get_bspline_status_message +!***************************************************************************************** + +!***************************************************************************************** +!> +! Actual size of a [[bspline_1d]] structure in bits. + + pure function size_1d(me) result(s) + + implicit none + + class(bspline_1d),intent(in) :: me + integer(ip) :: s !! size of the structure in bits + + s = 2_ip*int_size + logical_size + 2_ip*int_size + + if (allocated(me%bcoef)) s = s + real_size*size(me%bcoef,kind=ip) + if (allocated(me%tx)) s = s + real_size*size(me%tx,kind=ip) + if (allocated(me%work_val_1)) s = s + real_size*size(me%work_val_1,kind=ip) + + end function size_1d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Actual size of a [[bspline_2d]] structure in bits. + + pure function size_2d(me) result(s) + + implicit none + + class(bspline_2d),intent(in) :: me + integer(ip) :: s !! size of the structure in bits + + s = 2_ip*int_size + logical_size + 6_ip*int_size + + if (allocated(me%bcoef)) s = s + real_size*size(me%bcoef,1_ip,kind=ip)*& + size(me%bcoef,2_ip,kind=ip) + if (allocated(me%tx)) s = s + real_size*size(me%tx,kind=ip) + if (allocated(me%ty)) s = s + real_size*size(me%ty,kind=ip) + if (allocated(me%work_val_1)) s = s + real_size*size(me%work_val_1,kind=ip) + if (allocated(me%work_val_2)) s = s + real_size*size(me%work_val_2,kind=ip) + + end function size_2d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Actual size of a [[bspline_3d]] structure in bits. + + pure function size_3d(me) result(s) + + implicit none + + class(bspline_3d),intent(in) :: me + integer(ip) :: s !! size of the structure in bits + + s = 2_ip*int_size + logical_size + 10_ip*int_size + + if (allocated(me%bcoef)) s = s + real_size*size(me%bcoef,1_ip,kind=ip)*& + size(me%bcoef,2_ip,kind=ip)*& + size(me%bcoef,3_ip,kind=ip) + if (allocated(me%tx)) s = s + real_size*size(me%tx,kind=ip) + if (allocated(me%ty)) s = s + real_size*size(me%ty,kind=ip) + if (allocated(me%tz)) s = s + real_size*size(me%tz,kind=ip) + if (allocated(me%work_val_1)) s = s + real_size*size(me%work_val_1,1_ip,kind=ip)*& + size(me%work_val_1,2_ip,kind=ip) + if (allocated(me%work_val_2)) s = s + real_size*size(me%work_val_2,kind=ip) + if (allocated(me%work_val_3)) s = s + real_size*size(me%work_val_3,kind=ip) + + end function size_3d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Actual size of a [[bspline_4d]] structure in bits. + + pure function size_4d(me) result(s) + + implicit none + + class(bspline_4d),intent(in) :: me + integer(ip) :: s !! size of the structure in bits + + s = 2_ip*int_size + logical_size + 14_ip*int_size + + if (allocated(me%bcoef)) s = s + real_size*size(me%bcoef,1_ip,kind=ip)*& + size(me%bcoef,2_ip,kind=ip)*& + size(me%bcoef,3_ip,kind=ip)*& + size(me%bcoef,4_ip,kind=ip) + if (allocated(me%tx)) s = s + real_size*size(me%tx,kind=ip) + if (allocated(me%ty)) s = s + real_size*size(me%ty,kind=ip) + if (allocated(me%tz)) s = s + real_size*size(me%tz,kind=ip) + if (allocated(me%tq)) s = s + real_size*size(me%tq,kind=ip) + if (allocated(me%work_val_1)) s = s + real_size*size(me%work_val_1,1_ip,kind=ip)*& + size(me%work_val_1,2_ip,kind=ip)*& + size(me%work_val_1,3_ip,kind=ip) + if (allocated(me%work_val_2)) s = s + real_size*size(me%work_val_2,1_ip,kind=ip)*& + size(me%work_val_2,2_ip,kind=ip) + if (allocated(me%work_val_3)) s = s + real_size*size(me%work_val_3,kind=ip) + if (allocated(me%work_val_4)) s = s + real_size*size(me%work_val_4,kind=ip) + + end function size_4d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Actual size of a [[bspline_5d]] structure in bits. + + pure function size_5d(me) result(s) + + implicit none + + class(bspline_5d),intent(in) :: me + integer(ip) :: s !! size of the structure in bits + + s = 2_ip*int_size + logical_size + 18_ip*int_size + + if (allocated(me%bcoef)) s = s + real_size*size(me%bcoef,1_ip,kind=ip)*& + size(me%bcoef,2_ip,kind=ip)*& + size(me%bcoef,3_ip,kind=ip)*& + size(me%bcoef,4_ip,kind=ip)*& + size(me%bcoef,5_ip,kind=ip) + if (allocated(me%tx)) s = s + real_size*size(me%tx,kind=ip) + if (allocated(me%ty)) s = s + real_size*size(me%ty,kind=ip) + if (allocated(me%tz)) s = s + real_size*size(me%tz,kind=ip) + if (allocated(me%tq)) s = s + real_size*size(me%tq,kind=ip) + if (allocated(me%tr)) s = s + real_size*size(me%tr,kind=ip) + if (allocated(me%work_val_1)) s = s + real_size*size(me%work_val_1,1_ip,kind=ip)*& + size(me%work_val_1,2_ip,kind=ip)*& + size(me%work_val_1,3_ip,kind=ip)*& + size(me%work_val_1,4_ip,kind=ip) + if (allocated(me%work_val_2)) s = s + real_size*size(me%work_val_2,1_ip,kind=ip)*& + size(me%work_val_2,2_ip,kind=ip)*& + size(me%work_val_2,3_ip,kind=ip) + if (allocated(me%work_val_3)) s = s + real_size*size(me%work_val_3,1_ip,kind=ip)*& + size(me%work_val_3,2_ip,kind=ip) + if (allocated(me%work_val_4)) s = s + real_size*size(me%work_val_4,kind=ip) + if (allocated(me%work_val_5)) s = s + real_size*size(me%work_val_5,kind=ip) + + end function size_5d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Actual size of a [[bspline_6d]] structure in bits. + + pure function size_6d(me) result(s) + + implicit none + + class(bspline_6d),intent(in) :: me + integer(ip) :: s !! size of the structure in bits + + s = 2_ip*int_size + logical_size + 22_ip*int_size + + if (allocated(me%bcoef)) s = s + real_size*size(me%bcoef,1_ip,kind=ip)*& + size(me%bcoef,2_ip,kind=ip)*& + size(me%bcoef,3_ip,kind=ip)*& + size(me%bcoef,4_ip,kind=ip)*& + size(me%bcoef,5_ip,kind=ip)*& + size(me%bcoef,6,kind=ip) + if (allocated(me%tx)) s = s + real_size*size(me%tx,kind=ip) + if (allocated(me%ty)) s = s + real_size*size(me%ty,kind=ip) + if (allocated(me%tz)) s = s + real_size*size(me%tz,kind=ip) + if (allocated(me%tq)) s = s + real_size*size(me%tq,kind=ip) + if (allocated(me%tr)) s = s + real_size*size(me%tr,kind=ip) + if (allocated(me%ts)) s = s + real_size*size(me%ts,kind=ip) + if (allocated(me%work_val_1)) s = s + real_size*size(me%work_val_1,1_ip,kind=ip)*& + size(me%work_val_1,2_ip,kind=ip)*& + size(me%work_val_1,3_ip,kind=ip)*& + size(me%work_val_1,4_ip,kind=ip)*& + size(me%work_val_1,5_ip,kind=ip) + if (allocated(me%work_val_2)) s = s + real_size*size(me%work_val_2,1_ip,kind=ip)*& + size(me%work_val_2,2_ip,kind=ip)*& + size(me%work_val_2,3_ip,kind=ip)*& + size(me%work_val_2,4_ip,kind=ip) + if (allocated(me%work_val_3)) s = s + real_size*size(me%work_val_3,1_ip,kind=ip)*& + size(me%work_val_3,2_ip,kind=ip)*& + size(me%work_val_3,3_ip,kind=ip) + if (allocated(me%work_val_4)) s = s + real_size*size(me%work_val_4,1_ip,kind=ip)*& + size(me%work_val_4,2_ip,kind=ip) + if (allocated(me%work_val_5)) s = s + real_size*size(me%work_val_5,kind=ip) + if (allocated(me%work_val_6)) s = s + real_size*size(me%work_val_6,kind=ip) + + end function size_6d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Destructor for contents of the base [[bspline_class]] class. +! (this routine is called by the extended classes). + + pure subroutine destroy_base(me) + + implicit none + + class(bspline_class),intent(inout) :: me + + me%inbvx = 1_ip + me%iflag = 1_ip + me%initialized = .false. + me%extrap = .false. + + end subroutine destroy_base +!***************************************************************************************** + +!***************************************************************************************** +!> +! Destructor for [[bspline_1d]] class. + + pure subroutine destroy_1d(me) + + implicit none + + class(bspline_1d),intent(inout) :: me + + call me%destroy_base() + + me%nx = 0_ip + me%kx = 0_ip + if (allocated(me%bcoef)) deallocate(me%bcoef) + if (allocated(me%tx)) deallocate(me%tx) + if (allocated(me%work_val_1)) deallocate(me%work_val_1) + + end subroutine destroy_1d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Destructor for [[bspline_2d]] class. + + pure subroutine destroy_2d(me) + + implicit none + + class(bspline_2d),intent(inout) :: me + + call me%destroy_base() + + me%nx = 0_ip + me%ny = 0_ip + me%kx = 0_ip + me%ky = 0_ip + me%inbvy = 1_ip + me%iloy = 1_ip + if (allocated(me%bcoef)) deallocate(me%bcoef) + if (allocated(me%tx)) deallocate(me%tx) + if (allocated(me%ty)) deallocate(me%ty) + if (allocated(me%work_val_1)) deallocate(me%work_val_1) + if (allocated(me%work_val_2)) deallocate(me%work_val_2) + + end subroutine destroy_2d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Destructor for [[bspline_3d]] class. + + pure subroutine destroy_3d(me) + + implicit none + + class(bspline_3d),intent(inout) :: me + + call me%destroy_base() + + me%nx = 0_ip + me%ny = 0_ip + me%nz = 0_ip + me%kx = 0_ip + me%ky = 0_ip + me%kz = 0_ip + me%inbvy = 1_ip + me%inbvz = 1_ip + me%iloy = 1_ip + me%iloz = 1_ip + if (allocated(me%bcoef)) deallocate(me%bcoef) + if (allocated(me%tx)) deallocate(me%tx) + if (allocated(me%ty)) deallocate(me%ty) + if (allocated(me%tz)) deallocate(me%tz) + if (allocated(me%work_val_1)) deallocate(me%work_val_1) + if (allocated(me%work_val_2)) deallocate(me%work_val_2) + if (allocated(me%work_val_3)) deallocate(me%work_val_3) + + end subroutine destroy_3d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Destructor for [[bspline_4d]] class. + + pure subroutine destroy_4d(me) + + implicit none + + class(bspline_4d),intent(inout) :: me + + me%nx = 0_ip + me%ny = 0_ip + me%nz = 0_ip + me%nq = 0_ip + me%kx = 0_ip + me%ky = 0_ip + me%kz = 0_ip + me%kq = 0_ip + me%inbvy = 1_ip + me%inbvz = 1_ip + me%inbvq = 1_ip + me%iloy = 1_ip + me%iloz = 1_ip + me%iloq = 1_ip + if (allocated(me%bcoef)) deallocate(me%bcoef) + if (allocated(me%tx)) deallocate(me%tx) + if (allocated(me%ty)) deallocate(me%ty) + if (allocated(me%tz)) deallocate(me%tz) + if (allocated(me%tq)) deallocate(me%tq) + if (allocated(me%work_val_1)) deallocate(me%work_val_1) + if (allocated(me%work_val_2)) deallocate(me%work_val_2) + if (allocated(me%work_val_3)) deallocate(me%work_val_3) + if (allocated(me%work_val_4)) deallocate(me%work_val_4) + + end subroutine destroy_4d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Destructor for [[bspline_5d]] class. + + pure subroutine destroy_5d(me) + + implicit none + + class(bspline_5d),intent(inout) :: me + + me%nx = 0_ip + me%ny = 0_ip + me%nz = 0_ip + me%nq = 0_ip + me%nr = 0_ip + me%kx = 0_ip + me%ky = 0_ip + me%kz = 0_ip + me%kq = 0_ip + me%kr = 0_ip + me%inbvy = 1_ip + me%inbvz = 1_ip + me%inbvq = 1_ip + me%inbvr = 1_ip + me%iloy = 1_ip + me%iloz = 1_ip + me%iloq = 1_ip + me%ilor = 1_ip + if (allocated(me%bcoef)) deallocate(me%bcoef) + if (allocated(me%tx)) deallocate(me%tx) + if (allocated(me%ty)) deallocate(me%ty) + if (allocated(me%tz)) deallocate(me%tz) + if (allocated(me%tq)) deallocate(me%tq) + if (allocated(me%tr)) deallocate(me%tr) + if (allocated(me%work_val_1)) deallocate(me%work_val_1) + if (allocated(me%work_val_2)) deallocate(me%work_val_2) + if (allocated(me%work_val_3)) deallocate(me%work_val_3) + if (allocated(me%work_val_4)) deallocate(me%work_val_4) + if (allocated(me%work_val_5)) deallocate(me%work_val_5) + + end subroutine destroy_5d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Destructor for [[bspline_6d]] class. + + pure subroutine destroy_6d(me) + + implicit none + + class(bspline_6d),intent(inout) :: me + + me%nx = 0_ip + me%ny = 0_ip + me%nz = 0_ip + me%nq = 0_ip + me%nr = 0_ip + me%ns = 0_ip + me%kx = 0_ip + me%ky = 0_ip + me%kz = 0_ip + me%kq = 0_ip + me%kr = 0_ip + me%ks = 0_ip + me%inbvy = 1_ip + me%inbvz = 1_ip + me%inbvq = 1_ip + me%inbvr = 1_ip + me%inbvs = 1_ip + me%iloy = 1_ip + me%iloz = 1_ip + me%iloq = 1_ip + me%ilor = 1_ip + me%ilos = 1_ip + if (allocated(me%bcoef)) deallocate(me%bcoef) + if (allocated(me%tx)) deallocate(me%tx) + if (allocated(me%ty)) deallocate(me%ty) + if (allocated(me%tz)) deallocate(me%tz) + if (allocated(me%tq)) deallocate(me%tq) + if (allocated(me%tr)) deallocate(me%tr) + if (allocated(me%ts)) deallocate(me%ts) + if (allocated(me%work_val_1)) deallocate(me%work_val_1) + if (allocated(me%work_val_2)) deallocate(me%work_val_2) + if (allocated(me%work_val_3)) deallocate(me%work_val_3) + if (allocated(me%work_val_4)) deallocate(me%work_val_4) + if (allocated(me%work_val_5)) deallocate(me%work_val_5) + if (allocated(me%work_val_6)) deallocate(me%work_val_6) + + end subroutine destroy_6d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Finalizer for [[bspline_1d]] class. Just a wrapper for [[destroy_1d]]. + pure elemental subroutine finalize_1d(me) + type(bspline_1d),intent(inout) :: me; call me%destroy() + end subroutine finalize_1d +!***************************************************************************************** +!***************************************************************************************** +!> +! Finalizer for [[bspline_2d]] class. Just a wrapper for [[destroy_2d]]. + pure elemental subroutine finalize_2d(me) + type(bspline_2d),intent(inout) :: me; call me%destroy() + end subroutine finalize_2d +!***************************************************************************************** +!***************************************************************************************** +!> +! Finalizer for [[bspline_3d]] class. Just a wrapper for [[destroy_3d]]. + pure elemental subroutine finalize_3d(me) + type(bspline_3d),intent(inout) :: me; call me%destroy() + end subroutine finalize_3d +!***************************************************************************************** +!***************************************************************************************** +!> +! Finalizer for [[bspline_4d]] class. Just a wrapper for [[destroy_4d]]. + pure elemental subroutine finalize_4d(me) + type(bspline_4d),intent(inout) :: me; call me%destroy() + end subroutine finalize_4d +!***************************************************************************************** +!***************************************************************************************** +!> +! Finalizer for [[bspline_5d]] class. Just a wrapper for [[destroy_5d]]. + pure elemental subroutine finalize_5d(me) + type(bspline_5d),intent(inout) :: me; call me%destroy() + end subroutine finalize_5d +!***************************************************************************************** +!***************************************************************************************** +!> +! Finalizer for [[bspline_6d]] class. Just a wrapper for [[destroy_6d]]. + pure elemental subroutine finalize_6d(me) + type(bspline_6d),intent(inout) :: me; call me%destroy() + end subroutine finalize_6d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Sets the `extrap` flag in the class. + + pure subroutine set_extrap_flag(me,extrap) + + implicit none + + class(bspline_class),intent(inout) :: me + logical,intent(in),optional :: extrap !! if not present, then False is used + + if (present(extrap)) then + me%extrap = extrap + else + me%extrap = .false. + end if + + end subroutine set_extrap_flag +!***************************************************************************************** + +!***************************************************************************************** +!> +! It returns an empty [[bspline_1d]] type. Note that INITIALIZE still +! needs to be called before it can be used. +! Not really that useful except perhaps in some OpenMP applications. + + pure elemental function bspline_1d_constructor_empty() result(me) + + implicit none + + type(bspline_1d) :: me + + end function bspline_1d_constructor_empty +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_1d]] type (auto knots). +! This is a wrapper for [[initialize_1d_auto_knots]]. + + pure function bspline_1d_constructor_auto_knots(x,fcn,kx,extrap) result(me) + + implicit none + + type(bspline_1d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: fcn !! `(nx)` array of function values to interpolate. `fcn(i)` should + !! contain the function value at the point `x(i)` + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_1d_auto_knots(me,x,fcn,kx,me%iflag,extrap) + + end function bspline_1d_constructor_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_1d]] type (user-specified knots). +! This is a wrapper for [[initialize_1d_specify_knots]]. + + pure function bspline_1d_constructor_specify_knots(x,fcn,kx,tx,extrap) result(me) + + implicit none + + type(bspline_1d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: fcn !! `(nx)` array of function values to interpolate. `fcn(i)` should + !! contain the function value at the point `x(i)` + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_1d_specify_knots(me,x,fcn,kx,tx,me%iflag,extrap) + + end function bspline_1d_constructor_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_1d]] type (with automatically-computed knots). +! This is a wrapper for [[db1ink]]. + + pure subroutine initialize_1d_auto_knots(me,x,fcn,kx,iflag,extrap) + + implicit none + + class(bspline_1d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: fcn !! `(nx)` array of function values to interpolate. `fcn(i)` should + !! contain the function value at the point `x(i)` + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(out) :: iflag !! status flag (see [[db1ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: iknot + integer(ip) :: nx + + call me%destroy() + + nx = size(x,kind=ip) + + me%nx = nx + me%kx = kx + + allocate(me%tx(nx+kx)) + allocate(me%bcoef(nx)) + allocate(me%work_val_1(3_ip*kx)) + + iknot = 0_ip !knot sequence chosen by db1ink + + call db1ink(x,nx,fcn,kx,iknot,me%tx,me%bcoef,iflag) + + if (iflag==0_ip) then + call me%set_extrap_flag(extrap) + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_1d_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_1d]] type (with user-specified knots). +! This is a wrapper for [[db1ink]]. + + pure subroutine initialize_1d_specify_knots(me,x,fcn,kx,tx,iflag,extrap) + + implicit none + + class(bspline_1d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: fcn !! `(nx)` array of function values to interpolate. `fcn(i)` should + !! contain the function value at the point `x(i)` + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + integer(ip),intent(out) :: iflag !! status flag (see [[db1ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: nx + + call me%destroy() + + nx = size(x,kind=ip) + + call check_knot_vectors_sizes(nx=nx,kx=kx,tx=tx,iflag=iflag) + + if (iflag == 0_ip) then + + me%nx = nx + me%kx = kx + + allocate(me%tx(nx+kx)) + allocate(me%bcoef(nx)) + allocate(me%work_val_1(3_ip*kx)) + + me%tx = tx + + call db1ink(x,nx,fcn,kx,1_ip,me%tx,me%bcoef,iflag) + + call me%set_extrap_flag(extrap) + + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_1d_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluate a [[bspline_1d]] interpolate. This is a wrapper for [[db1val]]. + + pure subroutine evaluate_1d(me,xval,idx,f,iflag) + + implicit none + + class(bspline_1d),intent(inout) :: me + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag (see [[db1val]]) + + if (me%initialized) then + call db1val(xval,idx,me%tx,me%nx,me%kx,me%bcoef,f,iflag,& + me%inbvx,me%work_val_1,extrap=me%extrap) + else + iflag = 1_ip + end if + me%iflag = iflag + + end subroutine evaluate_1d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluate a [[bspline_1d]] definite integral. This is a wrapper for [[db1sqad]]. + + pure subroutine integral_1d(me,x1,x2,f,iflag) + + implicit none + + class(bspline_1d),intent(inout) :: me + real(wp),intent(in) :: x1 !! left point of interval + real(wp),intent(in) :: x2 !! right point of interval + real(wp),intent(out) :: f !! integral of the b-spline over \( [x_1, x_2] \) + integer(ip),intent(out) :: iflag !! status flag (see [[db1sqad]]) + + if (me%initialized) then + call db1sqad(me%tx,me%bcoef,me%nx,me%kx,x1,x2,f,iflag,me%work_val_1) + else + iflag = 1_ip + end if + me%iflag = iflag + + end subroutine integral_1d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluate a [[bspline_1d]] definite integral. This is a wrapper for [[db1fqad]]. + + subroutine fintegral_1d(me,fun,idx,x1,x2,tol,f,iflag) + + implicit none + + class(bspline_1d),intent(inout) :: me + procedure(b1fqad_func) :: fun !! external function of one argument for the + !! integrand `bf(x)=fun(x)*dbvalu(tx,bcoef,nx,kx,idx,x,inbv)` + integer(ip),intent(in) :: idx !! order of the spline derivative, `0 <= idx <= k-1` + !! `idx=0` gives the spline function + real(wp),intent(in) :: x1 !! left point of interval + real(wp),intent(in) :: x2 !! right point of interval + real(wp),intent(in) :: tol !! desired accuracy for the quadrature + real(wp),intent(out) :: f !! integral of `bf(x)` over \( [x_1, x_2] \) + integer(ip),intent(out) :: iflag !! status flag (see [[db1sqad]]) + + if (me%initialized) then + call db1fqad(fun,me%tx,me%bcoef,me%nx,me%kx,idx,x1,x2,tol,f,iflag,me%work_val_1) + else + iflag = 1_ip + end if + me%iflag = iflag + + end subroutine fintegral_1d +!***************************************************************************************** + +!***************************************************************************************** +!> +! It returns an empty [[bspline_2d]] type. Note that INITIALIZE still +! needs to be called before it can be used. +! Not really that useful except perhaps in some OpenMP applications. + + elemental function bspline_2d_constructor_empty() result(me) + + implicit none + + type(bspline_2d) :: me + + end function bspline_2d_constructor_empty +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_2d]] type (auto knots). +! This is a wrapper for [[initialize_2d_auto_knots]]. + + pure function bspline_2d_constructor_auto_knots(x,y,fcn,kx,ky,extrap) result(me) + + implicit none + + type(bspline_2d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:),intent(in) :: fcn !! `(nx,ny)` matrix of function values to interpolate. + !! `fcn(i,j)` should contain the function value at the + !! point (`x(i)`,`y(j)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_2d_auto_knots(me,x,y,fcn,kx,ky,me%iflag,extrap) + + end function bspline_2d_constructor_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_2d]] type (user-specified knots). +! This is a wrapper for [[initialize_2d_specify_knots]]. + + pure function bspline_2d_constructor_specify_knots(x,y,fcn,kx,ky,tx,ty,extrap) result(me) + + implicit none + + type(bspline_2d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:),intent(in) :: fcn !! `(nx,ny)` matrix of function values to interpolate. + !! `fcn(i,j)` should contain the function value at the + !! point (`x(i)`,`y(j)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ty !! The `(ny+ky)` knots in the \(y\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_2d_specify_knots(me,x,y,fcn,kx,ky,tx,ty,me%iflag,extrap) + + end function bspline_2d_constructor_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_2d]] type (with automatically-computed knots). +! This is a wrapper for [[db2ink]]. + + pure subroutine initialize_2d_auto_knots(me,x,y,fcn,kx,ky,iflag,extrap) + + implicit none + + class(bspline_2d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:),intent(in) :: fcn !! `(nx,ny)` matrix of function values to interpolate. + !! `fcn(i,j)` should contain the function value at the + !! point (`x(i)`,`y(j)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(out) :: iflag !! status flag (see [[db2ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: iknot + integer(ip) :: nx,ny + + call me%destroy() + + nx = size(x,kind=ip) + ny = size(y,kind=ip) + + me%nx = nx + me%ny = ny + + me%kx = kx + me%ky = ky + + allocate(me%tx(nx+kx)) + allocate(me%ty(ny+ky)) + allocate(me%bcoef(nx,ny)) + allocate(me%work_val_1(ky)) + allocate(me%work_val_2(3_ip*max(kx,ky))) + + iknot = 0_ip !knot sequence chosen by db2ink + + call db2ink(x,nx,y,ny,fcn,kx,ky,iknot,me%tx,me%ty,me%bcoef,iflag) + + if (iflag==0_ip) then + call me%set_extrap_flag(extrap) + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_2d_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_2d]] type (with user-specified knots). +! This is a wrapper for [[db2ink]]. + + pure subroutine initialize_2d_specify_knots(me,x,y,fcn,kx,ky,tx,ty,iflag,extrap) + + implicit none + + class(bspline_2d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:),intent(in) :: fcn !! `(nx,ny)` matrix of function values to interpolate. + !! `fcn(i,j)` should contain the function value at the + !! point (`x(i)`,`y(j)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ty !! The `(ny+ky)` knots in the \(y\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + integer(ip),intent(out) :: iflag !! status flag (see [[db2ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: nx,ny + + call me%destroy() + + nx = size(x,kind=ip) + ny = size(y,kind=ip) + + call check_knot_vectors_sizes(nx=nx,kx=kx,tx=tx,& + ny=ny,ky=ky,ty=ty,& + iflag=iflag) + + if (iflag == 0_ip) then + + me%nx = nx + me%ny = ny + + me%kx = kx + me%ky = ky + + allocate(me%tx(nx+kx)) + allocate(me%ty(ny+ky)) + allocate(me%bcoef(nx,ny)) + allocate(me%work_val_1(ky)) + allocate(me%work_val_2(3_ip*max(kx,ky))) + + me%tx = tx + me%ty = ty + + call db2ink(x,nx,y,ny,fcn,kx,ky,1_ip,me%tx,me%ty,me%bcoef,iflag) + + call me%set_extrap_flag(extrap) + + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_2d_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluate a [[bspline_2d]] interpolate. This is a wrapper for [[db2val]]. + + pure subroutine evaluate_2d(me,xval,yval,idx,idy,f,iflag) + + implicit none + + class(bspline_2d),intent(inout) :: me + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + real(wp),intent(in) :: yval !! \(y\) coordinate of evaluation point. + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idy !! \(y\) derivative of piecewise polynomial to evaluate. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag (see [[db2val]]) + + if (me%initialized) then + call db2val(xval,yval,& + idx,idy,& + me%tx,me%ty,& + me%nx,me%ny,& + me%kx,me%ky,& + me%bcoef,f,iflag,& + me%inbvx,me%inbvy,me%iloy,& + me%work_val_1,me%work_val_2,& + extrap=me%extrap) + else + iflag = 1_ip + end if + + me%iflag = iflag + + end subroutine evaluate_2d +!***************************************************************************************** + +!***************************************************************************************** +!> +! It returns an empty [[bspline_3d]] type. Note that INITIALIZE still +! needs to be called before it can be used. +! Not really that useful except perhaps in some OpenMP applications. + + elemental function bspline_3d_constructor_empty() result(me) + + implicit none + + type(bspline_3d) :: me + + end function bspline_3d_constructor_empty +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_3d]] type (auto knots). +! This is a wrapper for [[initialize_3d_auto_knots]]. + + pure function bspline_3d_constructor_auto_knots(x,y,z,fcn,kx,ky,kz,extrap) result(me) + + implicit none + + type(bspline_3d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:),intent(in) :: fcn !! `(nx,ny,nz)` matrix of function values to interpolate. + !! `fcn(i,j,k)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_3d_auto_knots(me,x,y,z,fcn,kx,ky,kz,me%iflag,extrap) + + end function bspline_3d_constructor_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_3d]] type (user-specified knots). +! This is a wrapper for [[initialize_3d_specify_knots]]. + + pure function bspline_3d_constructor_specify_knots(x,y,z,fcn,kx,ky,kz,tx,ty,tz,extrap) result(me) + + implicit none + + type(bspline_3d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:),intent(in) :: fcn !! `(nx,ny,nz)` matrix of function values to interpolate. + !! `fcn(i,j,k)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ty !! The `(ny+ky)` knots in the \(y\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tz !! The `(nz+kz)` knots in the \(z\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_3d_specify_knots(me,x,y,z,fcn,kx,ky,kz,tx,ty,tz,me%iflag,extrap) + + end function bspline_3d_constructor_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_3d]] type (with automatically-computed knots). +! This is a wrapper for [[db3ink]]. + + pure subroutine initialize_3d_auto_knots(me,x,y,z,fcn,kx,ky,kz,iflag,extrap) + + implicit none + + class(bspline_3d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:),intent(in) :: fcn !! `(nx,ny,nz)` matrix of function values to interpolate. + !! `fcn(i,j,k)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(out) :: iflag !! status flag (see [[db3ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: iknot + integer(ip) :: nx,ny,nz + + call me%destroy() + + nx = size(x,kind=ip) + ny = size(y,kind=ip) + nz = size(z,kind=ip) + + me%nx = nx + me%ny = ny + me%nz = nz + + me%kx = kx + me%ky = ky + me%kz = kz + + allocate(me%tx(nx+kx)) + allocate(me%ty(ny+ky)) + allocate(me%tz(nz+kz)) + allocate(me%bcoef(nx,ny,nz)) + allocate(me%work_val_1(ky,kz)) + allocate(me%work_val_2(kz)) + allocate(me%work_val_3(3_ip*max(kx,ky,kz))) + + iknot = 0_ip !knot sequence chosen by db3ink + + call db3ink(x,nx,y,ny,z,nz,& + fcn,& + kx,ky,kz,& + iknot,& + me%tx,me%ty,me%tz,& + me%bcoef,iflag) + + if (iflag==0_ip) then + call me%set_extrap_flag(extrap) + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_3d_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_3d]] type (with user-specified knots). +! This is a wrapper for [[db3ink]]. + + pure subroutine initialize_3d_specify_knots(me,x,y,z,fcn,kx,ky,kz,tx,ty,tz,iflag,extrap) + + implicit none + + class(bspline_3d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:),intent(in) :: fcn !! `(nx,ny,nz)` matrix of function values to interpolate. + !! `fcn(i,j,k)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ty !! The `(ny+ky)` knots in the \(y\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tz !! The `(nz+kz)` knots in the \(z\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + integer(ip),intent(out) :: iflag !! status flag (see [[db3ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: nx,ny,nz + + call me%destroy() + + nx = size(x,kind=ip) + ny = size(y,kind=ip) + nz = size(z,kind=ip) + + call check_knot_vectors_sizes(nx=nx,kx=kx,tx=tx,& + ny=ny,ky=ky,ty=ty,& + nz=nz,kz=kz,tz=tz,& + iflag=iflag) + + if (iflag == 0_ip) then + + me%nx = nx + me%ny = ny + me%nz = nz + + me%kx = kx + me%ky = ky + me%kz = kz + + allocate(me%tx(nx+kx)) + allocate(me%ty(ny+ky)) + allocate(me%tz(nz+kz)) + allocate(me%bcoef(nx,ny,nz)) + allocate(me%work_val_1(ky,kz)) + allocate(me%work_val_2(kz)) + allocate(me%work_val_3(3_ip*max(kx,ky,kz))) + + me%tx = tx + me%ty = ty + me%tz = tz + + call db3ink(x,nx,y,ny,z,nz,& + fcn,& + kx,ky,kz,& + 1_ip,& + me%tx,me%ty,me%tz,& + me%bcoef,iflag) + + call me%set_extrap_flag(extrap) + + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_3d_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluate a [[bspline_3d]] interpolate. This is a wrapper for [[db3val]]. + + pure subroutine evaluate_3d(me,xval,yval,zval,idx,idy,idz,f,iflag) + + implicit none + + class(bspline_3d),intent(inout) :: me + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + real(wp),intent(in) :: yval !! \(y\) coordinate of evaluation point. + real(wp),intent(in) :: zval !! \(z\) coordinate of evaluation point. + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idy !! \(y\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idz !! \(z\) derivative of piecewise polynomial to evaluate. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag (see [[db3val]]) + + if (me%initialized) then + call db3val(xval,yval,zval,& + idx,idy,idz,& + me%tx,me%ty,me%tz,& + me%nx,me%ny,me%nz,& + me%kx,me%ky,me%kz,& + me%bcoef,f,iflag,& + me%inbvx,me%inbvy,me%inbvz,& + me%iloy,me%iloz,& + me%work_val_1,me%work_val_2,me%work_val_3,& + extrap=me%extrap) + else + iflag = 1_ip + end if + + me%iflag = iflag + + end subroutine evaluate_3d +!***************************************************************************************** + +!***************************************************************************************** +!> +! It returns an empty [[bspline_4d]] type. Note that INITIALIZE still +! needs to be called before it can be used. +! Not really that useful except perhaps in some OpenMP applications. + + elemental function bspline_4d_constructor_empty() result(me) + + implicit none + + type(bspline_4d) :: me + + end function bspline_4d_constructor_empty +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_4d]] type (auto knots). +! This is a wrapper for [[initialize_4d_auto_knots]]. + + pure function bspline_4d_constructor_auto_knots(x,y,z,q,fcn,kx,ky,kz,kq,extrap) result(me) + + implicit none + + type(bspline_4d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq)` matrix of function values to interpolate. + !! `fcn(i,j,k,l)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_4d_auto_knots(me,x,y,z,q,fcn,kx,ky,kz,kq,me%iflag,extrap) + + end function bspline_4d_constructor_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_4d]] type (user-specified knots). +! This is a wrapper for [[initialize_4d_specify_knots]]. + + pure function bspline_4d_constructor_specify_knots(x,y,z,q,fcn,kx,ky,kz,kq,& + tx,ty,tz,tq,extrap) result(me) + + implicit none + + type(bspline_4d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq)` matrix of function values to interpolate. + !! `fcn(i,j,k,l)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ty !! The `(ny+ky)` knots in the \(y\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tz !! The `(nz+kz)` knots in the \(z\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tq !! The `(nq+kq)` knots in the \(q\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_4d_specify_knots(me,x,y,z,q,fcn,kx,ky,kz,kq,tx,ty,tz,tq,me%iflag,extrap) + + end function bspline_4d_constructor_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_4d]] type (with automatically-computed knots). +! This is a wrapper for [[db4ink]]. + + pure subroutine initialize_4d_auto_knots(me,x,y,z,q,fcn,kx,ky,kz,kq,iflag,extrap) + + implicit none + + class(bspline_4d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq)` matrix of function values to interpolate. + !! `fcn(i,j,k,l)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(out) :: iflag !! status flag (see [[db4ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: iknot + integer(ip) :: nx,ny,nz,nq + + call me%destroy() + + nx = size(x,kind=ip) + ny = size(y,kind=ip) + nz = size(z,kind=ip) + nq = size(q,kind=ip) + + me%nx = nx + me%ny = ny + me%nz = nz + me%nq = nq + + me%kx = kx + me%ky = ky + me%kz = kz + me%kq = kq + + allocate(me%tx(nx+kx)) + allocate(me%ty(ny+ky)) + allocate(me%tz(nz+kz)) + allocate(me%tq(nq+kq)) + allocate(me%bcoef(nx,ny,nz,nq)) + allocate(me%work_val_1(ky,kz,kq)) + allocate(me%work_val_2(kz,kq)) + allocate(me%work_val_3(kq)) + allocate(me%work_val_4(3_ip*max(kx,ky,kz,kq))) + + iknot = 0_ip !knot sequence chosen by db4ink + + call db4ink(x,nx,y,ny,z,nz,q,nq,& + fcn,& + kx,ky,kz,kq,& + iknot,& + me%tx,me%ty,me%tz,me%tq,& + me%bcoef,iflag) + + if (iflag==0_ip) then + call me%set_extrap_flag(extrap) + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_4d_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_4d]] type (with user-specified knots). +! This is a wrapper for [[db4ink]]. + + pure subroutine initialize_4d_specify_knots(me,x,y,z,q,fcn,& + kx,ky,kz,kq,tx,ty,tz,tq,iflag,extrap) + + implicit none + + class(bspline_4d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq)` matrix of function values to interpolate. + !! `fcn(i,j,k,l)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ty !! The `(ny+ky)` knots in the \(y\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tz !! The `(nz+kz)` knots in the \(z\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tq !! The `(nq+kq)` knots in the \(q\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + integer(ip),intent(out) :: iflag !! status flag (see [[db4ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: nx,ny,nz,nq + + call me%destroy() + + nx = size(x,kind=ip) + ny = size(y,kind=ip) + nz = size(z,kind=ip) + nq = size(q,kind=ip) + + call check_knot_vectors_sizes(nx=nx,kx=kx,tx=tx,& + ny=ny,ky=ky,ty=ty,& + nz=nz,kz=kz,tz=tz,& + nq=nq,kq=kq,tq=tq,& + iflag=iflag) + + if (iflag == 0_ip) then + + me%nx = nx + me%ny = ny + me%nz = nz + me%nq = nq + + me%kx = kx + me%ky = ky + me%kz = kz + me%kq = kq + + allocate(me%tx(nx+kx)) + allocate(me%ty(ny+ky)) + allocate(me%tz(nz+kz)) + allocate(me%tq(nq+kq)) + allocate(me%bcoef(nx,ny,nz,nq)) + allocate(me%work_val_1(ky,kz,kq)) + allocate(me%work_val_2(kz,kq)) + allocate(me%work_val_3(kq)) + allocate(me%work_val_4(3_ip*max(kx,ky,kz,kq))) + + me%tx = tx + me%ty = ty + me%tz = tz + me%tq = tq + + call db4ink(x,nx,y,ny,z,nz,q,nq,& + fcn,& + kx,ky,kz,kq,& + 1_ip,& + me%tx,me%ty,me%tz,me%tq,& + me%bcoef,iflag) + + call me%set_extrap_flag(extrap) + + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_4d_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluate a [[bspline_4d]] interpolate. This is a wrapper for [[db4val]]. + + pure subroutine evaluate_4d(me,xval,yval,zval,qval,idx,idy,idz,idq,f,iflag) + + implicit none + + class(bspline_4d),intent(inout) :: me + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + real(wp),intent(in) :: yval !! \(y\) coordinate of evaluation point. + real(wp),intent(in) :: zval !! \(z\) coordinate of evaluation point. + real(wp),intent(in) :: qval !! \(q\) coordinate of evaluation point. + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idy !! \(y\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idz !! \(z\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idq !! \(q\) derivative of piecewise polynomial to evaluate. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag (see [[db4val]]) + + if (me%initialized) then + call db4val(xval,yval,zval,qval,& + idx,idy,idz,idq,& + me%tx,me%ty,me%tz,me%tq,& + me%nx,me%ny,me%nz,me%nq,& + me%kx,me%ky,me%kz,me%kq,& + me%bcoef,f,iflag,& + me%inbvx,me%inbvy,me%inbvz,me%inbvq,& + me%iloy,me%iloz,me%iloq,& + me%work_val_1,me%work_val_2,me%work_val_3,me%work_val_4,& + extrap=me%extrap) + else + iflag = 1_ip + end if + + me%iflag = iflag + + end subroutine evaluate_4d +!***************************************************************************************** + +!***************************************************************************************** +!> +! It returns an empty [[bspline_5d]] type. Note that INITIALIZE still +! needs to be called before it can be used. +! Not really that useful except perhaps in some OpenMP applications. + + elemental function bspline_5d_constructor_empty() result(me) + + implicit none + + type(bspline_5d) :: me + + end function bspline_5d_constructor_empty +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_5d]] type (auto knots). +! This is a wrapper for [[initialize_5d_auto_knots]]. + + pure function bspline_5d_constructor_auto_knots(x,y,z,q,r,fcn,kx,ky,kz,kq,kr,extrap) result(me) + + implicit none + + type(bspline_5d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: r !! `(nr)` array of \(r\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq,nr)` matrix of function values to interpolate. + !! `fcn(i,j,k,l,m)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`,`r(m)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kr !! The order of spline pieces in \(r\) + !! ( \( 2 \le k_r < n_r \) ) + !! (order = polynomial degree + 1) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_5d_auto_knots(me,x,y,z,q,r,fcn,kx,ky,kz,kq,kr,me%iflag,extrap) + + end function bspline_5d_constructor_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_5d]] type (user-specified knots). +! This is a wrapper for [[initialize_5d_specify_knots]]. + + pure function bspline_5d_constructor_specify_knots(x,y,z,q,r,fcn,& + kx,ky,kz,kq,kr,& + tx,ty,tz,tq,tr,extrap) result(me) + + implicit none + + type(bspline_5d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: r !! `(nr)` array of \(r\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq,nr)` matrix of function values to interpolate. + !! `fcn(i,j,k,l,m)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`,`r(m)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kr !! The order of spline pieces in \(r\) + !! ( \( 2 \le k_r < n_r \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ty !! The `(ny+ky)` knots in the \(y\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tz !! The `(nz+kz)` knots in the \(z\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tq !! The `(nq+kq)` knots in the \(q\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tr !! The `(nr+kr)` knots in the \(r\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_5d_specify_knots(me,x,y,z,q,r,fcn,kx,ky,kz,kq,kr,tx,ty,tz,tq,tr,me%iflag,extrap) + + end function bspline_5d_constructor_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_5d]] type (with automatically-computed knots). +! This is a wrapper for [[db5ink]]. + + pure subroutine initialize_5d_auto_knots(me,x,y,z,q,r,fcn,kx,ky,kz,kq,kr,iflag,extrap) + + implicit none + + class(bspline_5d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: r !! `(nr)` array of \(r\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq,nr)` matrix of function values to interpolate. + !! `fcn(i,j,k,l,m)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`,`r(m)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kr !! The order of spline pieces in \(r\) + !! ( \( 2 \le k_r < n_r \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(out) :: iflag !! status flag (see [[db5ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: iknot + integer(ip) :: nx,ny,nz,nq,nr + + call me%destroy() + + nx = size(x,kind=ip) + ny = size(y,kind=ip) + nz = size(z,kind=ip) + nq = size(q,kind=ip) + nr = size(r,kind=ip) + + me%nx = nx + me%ny = ny + me%nz = nz + me%nq = nq + me%nr = nr + + me%kx = kx + me%ky = ky + me%kz = kz + me%kq = kq + me%kr = kr + + allocate(me%tx(nx+kx)) + allocate(me%ty(ny+ky)) + allocate(me%tz(nz+kz)) + allocate(me%tq(nq+kq)) + allocate(me%tr(nr+kr)) + allocate(me%bcoef(nx,ny,nz,nq,nr)) + allocate(me%work_val_1(ky,kz,kq,kr)) + allocate(me%work_val_2(kz,kq,kr)) + allocate(me%work_val_3(kq,kr)) + allocate(me%work_val_4(kr)) + allocate(me%work_val_5(3_ip*max(kx,ky,kz,kq,kr))) + + iknot = 0_ip !knot sequence chosen by db5ink + + call db5ink(x,nx,y,ny,z,nz,q,nq,r,nr,& + fcn,& + kx,ky,kz,kq,kr,& + iknot,& + me%tx,me%ty,me%tz,me%tq,me%tr,& + me%bcoef,iflag) + + if (iflag==0_ip) then + call me%set_extrap_flag(extrap) + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_5d_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_5d]] type (with user-specified knots). +! This is a wrapper for [[db5ink]]. + + pure subroutine initialize_5d_specify_knots(me,x,y,z,q,r,fcn,& + kx,ky,kz,kq,kr,& + tx,ty,tz,tq,tr,iflag,extrap) + + implicit none + + class(bspline_5d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: r !! `(nr)` array of \(r\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq,nr)` matrix of function values to interpolate. + !! `fcn(i,j,k,l,m)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`,`r(m)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kr !! The order of spline pieces in \(r\) + !! ( \( 2 \le k_r < n_r \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ty !! The `(ny+ky)` knots in the \(y\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tz !! The `(nz+kz)` knots in the \(z\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tq !! The `(nq+kq)` knots in the \(q\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tr !! The `(nr+kr)` knots in the \(r\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + integer(ip),intent(out) :: iflag !! status flag (see [[db5ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: nx,ny,nz,nq,nr + + call me%destroy() + + nx = size(x,kind=ip) + ny = size(y,kind=ip) + nz = size(z,kind=ip) + nq = size(q,kind=ip) + nr = size(r,kind=ip) + + call check_knot_vectors_sizes(nx=nx,kx=kx,tx=tx,& + ny=ny,ky=ky,ty=ty,& + nz=nz,kz=kz,tz=tz,& + nq=nq,kq=kq,tq=tq,& + nr=nr,kr=kr,tr=tr,& + iflag=iflag) + + if (iflag == 0_ip) then + + me%nx = nx + me%ny = ny + me%nz = nz + me%nq = nq + me%nr = nr + + me%kx = kx + me%ky = ky + me%kz = kz + me%kq = kq + me%kr = kr + + allocate(me%tx(nx+kx)) + allocate(me%ty(ny+ky)) + allocate(me%tz(nz+kz)) + allocate(me%tq(nq+kq)) + allocate(me%tr(nr+kr)) + allocate(me%bcoef(nx,ny,nz,nq,nr)) + allocate(me%work_val_1(ky,kz,kq,kr)) + allocate(me%work_val_2(kz,kq,kr)) + allocate(me%work_val_3(kq,kr)) + allocate(me%work_val_4(kr)) + allocate(me%work_val_5(3_ip*max(kx,ky,kz,kq,kr))) + + me%tx = tx + me%ty = ty + me%tz = tz + me%tq = tq + me%tr = tr + + call db5ink(x,nx,y,ny,z,nz,q,nq,r,nr,& + fcn,& + kx,ky,kz,kq,kr,& + 1_ip,& + me%tx,me%ty,me%tz,me%tq,me%tr,& + me%bcoef,iflag) + + call me%set_extrap_flag(extrap) + + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_5d_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluate a [[bspline_5d]] interpolate. This is a wrapper for [[db5val]]. + + pure subroutine evaluate_5d(me,xval,yval,zval,qval,rval,idx,idy,idz,idq,idr,f,iflag) + + implicit none + + class(bspline_5d),intent(inout) :: me + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + real(wp),intent(in) :: yval !! \(y\) coordinate of evaluation point. + real(wp),intent(in) :: zval !! \(z\) coordinate of evaluation point. + real(wp),intent(in) :: qval !! \(q\) coordinate of evaluation point. + real(wp),intent(in) :: rval !! \(r\) coordinate of evaluation point. + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idy !! \(y\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idz !! \(z\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idq !! \(q\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idr !! \(r\) derivative of piecewise polynomial to evaluate. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag (see [[db5val]]) + + if (me%initialized) then + call db5val(xval,yval,zval,qval,rval,& + idx,idy,idz,idq,idr,& + me%tx,me%ty,me%tz,me%tq,me%tr,& + me%nx,me%ny,me%nz,me%nq,me%nr,& + me%kx,me%ky,me%kz,me%kq,me%kr,& + me%bcoef,f,iflag,& + me%inbvx,me%inbvy,me%inbvz,me%inbvq,me%inbvr,& + me%iloy,me%iloz,me%iloq,me%ilor,& + me%work_val_1,me%work_val_2,me%work_val_3,me%work_val_4,me%work_val_5,& + extrap=me%extrap) + else + iflag = 1_ip + end if + + me%iflag = iflag + + end subroutine evaluate_5d +!***************************************************************************************** + +!***************************************************************************************** +!> +! It returns an empty [[bspline_6d]] type. Note that INITIALIZE still +! needs to be called before it can be used. +! Not really that useful except perhaps in some OpenMP applications. + + elemental function bspline_6d_constructor_empty() result(me) + + implicit none + + type(bspline_6d) :: me + + end function bspline_6d_constructor_empty +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_6d]] type (auto knots). +! This is a wrapper for [[initialize_6d_auto_knots]]. + + pure function bspline_6d_constructor_auto_knots(x,y,z,q,r,s,fcn,& + kx,ky,kz,kq,kr,ks,extrap) result(me) + + implicit none + + type(bspline_6d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: r !! `(nr)` array of \(r\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: s !! `(ns)` array of \(s\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq,nr,ns)` matrix of function values to interpolate. + !! `fcn(i,j,k,l,m,n)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`,`r(m)`,`s(n)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kr !! The order of spline pieces in \(r\) + !! ( \( 2 \le k_r < n_r \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ks !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_6d_auto_knots(me,x,y,z,q,r,s,fcn,kx,ky,kz,kq,kr,ks,me%iflag,extrap) + + end function bspline_6d_constructor_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Constructor for a [[bspline_6d]] type (user-specified knots). +! This is a wrapper for [[initialize_6d_specify_knots]]. + + pure function bspline_6d_constructor_specify_knots(x,y,z,q,r,s,fcn,& + kx,ky,kz,kq,kr,ks,& + tx,ty,tz,tq,tr,ts,extrap) result(me) + + implicit none + + type(bspline_6d) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: r !! `(nr)` array of \(r\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: s !! `(ns)` array of \(s\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq,nr,ns)` matrix of function values to interpolate. + !! `fcn(i,j,k,l,m,n)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`,`r(m)`,`s(n)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kr !! The order of spline pieces in \(r\) + !! ( \( 2 \le k_r < n_r \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ks !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ty !! The `(ny+ky)` knots in the \(y\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tz !! The `(nz+kz)` knots in the \(z\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tq !! The `(nq+kq)` knots in the \(q\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tr !! The `(nr+kr)` knots in the \(r\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ts !! The `(ns+ks)` knots in the \(s\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + call initialize_6d_specify_knots(me,x,y,z,q,r,s,fcn,& + kx,ky,kz,kq,kr,ks,& + tx,ty,tz,tq,tr,ts,me%iflag,extrap) + + end function bspline_6d_constructor_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_6d]] type (with automatically-computed knots). +! This is a wrapper for [[db6ink]]. + + pure subroutine initialize_6d_auto_knots(me,x,y,z,q,r,s,fcn,& + kx,ky,kz,kq,kr,ks,iflag,extrap) + + implicit none + + class(bspline_6d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: r !! `(nr)` array of \(r\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: s !! `(ns)` array of \(s\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq,nr,ns)` matrix of function values to interpolate. + !! `fcn(i,j,k,l,m,n)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`,`r(m)`,`s(n)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kr !! The order of spline pieces in \(r\) + !! ( \( 2 \le k_r < n_r \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ks !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(out) :: iflag !! status flag (see [[db6ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: iknot + integer(ip) :: nx,ny,nz,nq,nr,ns + + call me%destroy() + + nx = size(x,kind=ip) + ny = size(y,kind=ip) + nz = size(z,kind=ip) + nq = size(q,kind=ip) + nr = size(r,kind=ip) + ns = size(s,kind=ip) + + me%nx = nx + me%ny = ny + me%nz = nz + me%nq = nq + me%nr = nr + me%ns = ns + + me%kx = kx + me%ky = ky + me%kz = kz + me%kq = kq + me%kr = kr + me%ks = ks + + allocate(me%tx(nx+kx)) + allocate(me%ty(ny+ky)) + allocate(me%tz(nz+kz)) + allocate(me%tq(nq+kq)) + allocate(me%tr(nr+kr)) + allocate(me%ts(ns+ks)) + allocate(me%bcoef(nx,ny,nz,nq,nr,ns)) + allocate(me%work_val_1(ky,kz,kq,kr,ks)) + allocate(me%work_val_2(kz,kq,kr,ks)) + allocate(me%work_val_3(kq,kr,ks)) + allocate(me%work_val_4(kr,ks)) + allocate(me%work_val_5(ks)) + allocate(me%work_val_6(3_ip*max(kx,ky,kz,kq,kr,ks))) + + iknot = 0_ip !knot sequence chosen by db6ink + + call db6ink(x,nx,y,ny,z,nz,q,nq,r,nr,s,ns,& + fcn,& + kx,ky,kz,kq,kr,ks,& + iknot,& + me%tx,me%ty,me%tz,me%tq,me%tr,me%ts,& + me%bcoef,iflag) + + if (iflag==0_ip) then + call me%set_extrap_flag(extrap) + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_6d_auto_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Initialize a [[bspline_6d]] type (with user-specified knots). +! This is a wrapper for [[db6ink]]. + + pure subroutine initialize_6d_specify_knots(me,x,y,z,q,r,s,fcn,& + kx,ky,kz,kq,kr,ks,& + tx,ty,tz,tq,tr,ts,iflag,extrap) + + implicit none + + class(bspline_6d),intent(inout) :: me + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: r !! `(nr)` array of \(r\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: s !! `(ns)` array of \(s\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:,:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq,nr,ns)` matrix of function values to interpolate. + !! `fcn(i,j,k,l,m,n)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`,`r(m)`,`s(n)`) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! The order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kr !! The order of spline pieces in \(r\) + !! ( \( 2 \le k_r < n_r \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ks !! The order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ty !! The `(ny+ky)` knots in the \(y\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tz !! The `(nz+kz)` knots in the \(z\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tq !! The `(nq+kq)` knots in the \(q\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: tr !! The `(nr+kr)` knots in the \(r\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + real(wp),dimension(:),intent(in) :: ts !! The `(ns+ks)` knots in the \(s\) direction + !! for the spline interpolant. + !! Must be non-decreasing. + integer(ip),intent(out) :: iflag !! status flag (see [[db6ink]]) + logical,intent(in),optional :: extrap !! if true, then extrapolation is allowed + !! (default is false) + + integer(ip) :: nx,ny,nz,nq,nr,ns + + call me%destroy() + + nx = size(x,kind=ip) + ny = size(y,kind=ip) + nz = size(z,kind=ip) + nq = size(q,kind=ip) + nr = size(r,kind=ip) + ns = size(s,kind=ip) + + call check_knot_vectors_sizes(nx=nx,kx=kx,tx=tx,& + ny=ny,ky=ky,ty=ty,& + nz=nz,kz=kz,tz=tz,& + nq=nq,kq=kq,tq=tq,& + nr=nr,kr=kr,tr=tr,& + ns=ns,ks=ks,ts=ts,& + iflag=iflag) + + if (iflag == 0_ip) then + + me%nx = nx + me%ny = ny + me%nz = nz + me%nq = nq + me%nr = nr + me%ns = ns + + me%kx = kx + me%ky = ky + me%kz = kz + me%kq = kq + me%kr = kr + me%ks = ks + + allocate(me%tx(nx+kx)) + allocate(me%ty(ny+ky)) + allocate(me%tz(nz+kz)) + allocate(me%tq(nq+kq)) + allocate(me%tr(nr+kr)) + allocate(me%ts(ns+ks)) + allocate(me%bcoef(nx,ny,nz,nq,nr,ns)) + allocate(me%work_val_1(ky,kz,kq,kr,ks)) + allocate(me%work_val_2(kz,kq,kr,ks)) + allocate(me%work_val_3(kq,kr,ks)) + allocate(me%work_val_4(kr,ks)) + allocate(me%work_val_5(ks)) + allocate(me%work_val_6(3_ip*max(kx,ky,kz,kq,kr,ks))) + + me%tx = tx + me%ty = ty + me%tz = tz + me%tq = tq + me%tr = tr + me%ts = ts + + call db6ink(x,nx,y,ny,z,nz,q,nq,r,nr,s,ns,& + fcn,& + kx,ky,kz,kq,kr,ks,& + 1_ip,& + me%tx,me%ty,me%tz,me%tq,me%tr,me%ts,& + me%bcoef,iflag) + + call me%set_extrap_flag(extrap) + + end if + + me%initialized = iflag==0_ip + me%iflag = iflag + + end subroutine initialize_6d_specify_knots +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluate a [[bspline_6d]] interpolate. This is a wrapper for [[db6val]]. + + pure subroutine evaluate_6d(me,xval,yval,zval,qval,rval,sval,idx,idy,idz,idq,idr,ids,f,iflag) + + implicit none + + class(bspline_6d),intent(inout) :: me + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + real(wp),intent(in) :: yval !! \(y\) coordinate of evaluation point. + real(wp),intent(in) :: zval !! \(z\) coordinate of evaluation point. + real(wp),intent(in) :: qval !! \(q\) coordinate of evaluation point. + real(wp),intent(in) :: rval !! \(r\) coordinate of evaluation point. + real(wp),intent(in) :: sval !! \(s\) coordinate of evaluation point. + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idy !! \(y\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idz !! \(z\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idq !! \(q\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idr !! \(r\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: ids !! \(s\) derivative of piecewise polynomial to evaluate. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag (see [[db6val]]) + + if (me%initialized) then + call db6val(xval,yval,zval,qval,rval,sval,& + idx,idy,idz,idq,idr,ids,& + me%tx,me%ty,me%tz,me%tq,me%tr,me%ts,& + me%nx,me%ny,me%nz,me%nq,me%nr,me%ns,& + me%kx,me%ky,me%kz,me%kq,me%kr,me%ks,& + me%bcoef,f,iflag,& + me%inbvx,me%inbvy,me%inbvz,me%inbvq,me%inbvr,me%inbvs,& + me%iloy,me%iloz,me%iloq,me%ilor,me%ilos,& + me%work_val_1,me%work_val_2,me%work_val_3,me%work_val_4,me%work_val_5,me%work_val_6,& + extrap=me%extrap) + else + iflag = 1_ip + end if + + me%iflag = iflag + + end subroutine evaluate_6d +!***************************************************************************************** + +!***************************************************************************************** +!> +! Error checks for the user-specified knot vector sizes. +! +!@note If more than one is the wrong size, then the `iflag` error code will +! correspond to the one with the highest rank. + + pure subroutine check_knot_vectors_sizes(nx,ny,nz,nq,nr,ns,& + kx,ky,kz,kq,kr,ks,& + tx,ty,tz,tq,tr,ts,iflag) + + implicit none + + integer(ip),intent(in),optional :: nx + integer(ip),intent(in),optional :: ny + integer(ip),intent(in),optional :: nz + integer(ip),intent(in),optional :: nq + integer(ip),intent(in),optional :: nr + integer(ip),intent(in),optional :: ns + integer(ip),intent(in),optional :: kx + integer(ip),intent(in),optional :: ky + integer(ip),intent(in),optional :: kz + integer(ip),intent(in),optional :: kq + integer(ip),intent(in),optional :: kr + integer(ip),intent(in),optional :: ks + real(wp),dimension(:),intent(in),optional :: tx + real(wp),dimension(:),intent(in),optional :: ty + real(wp),dimension(:),intent(in),optional :: tz + real(wp),dimension(:),intent(in),optional :: tq + real(wp),dimension(:),intent(in),optional :: tr + real(wp),dimension(:),intent(in),optional :: ts + integer(ip),intent(out) :: iflag !! 0 if everything is OK + + iflag = 0_ip + + if (present(nx) .and. present(kx) .and. present(tx)) then + if (size(tx,kind=ip)/=(nx+kx)) then + iflag = 501_ip ! tx is not the correct size (nx+kx) + end if + end if + + if (present(ny) .and. present(ky) .and. present(ty)) then + if (size(ty,kind=ip)/=(ny+ky)) then + iflag = 502_ip ! ty is not the correct size (ny+ky) + end if + end if + + if (present(nz) .and. present(kz) .and. present(tz)) then + if (size(tz,kind=ip)/=(nz+kz)) then + iflag = 503_ip ! tz is not the correct size (nz+kz) + end if + end if + + if (present(nq) .and. present(kq) .and. present(tq)) then + if (size(tq,kind=ip)/=(nq+kq)) then + iflag = 504_ip ! tq is not the correct size (nq+kq) + end if + end if + + if (present(nr) .and. present(kr) .and. present(tr)) then + if (size(tr,kind=ip)/=(nr+kr)) then + iflag = 505_ip ! tr is not the correct size (nr+kr) + end if + end if + + if (present(ns) .and. present(ks) .and. present(ts)) then + if (size(ts,kind=ip)/=(ns+ks)) then + iflag = 506_ip ! ts is not the correct size (ns+ks) + end if + end if + + end subroutine check_knot_vectors_sizes +!***************************************************************************************** + +!***************************************************************************************** + end module bspline_oo_module +!***************************************************************************************** diff --git a/examples/bspline/native/bspline_sub_module.f90 b/examples/bspline/native/bspline_sub_module.f90 new file mode 100644 index 000000000..272af1878 --- /dev/null +++ b/examples/bspline/native/bspline_sub_module.f90 @@ -0,0 +1,4733 @@ +!***************************************************************************************** +!> author: Jacob Williams +! license: BSD +! +!### Description +! +! Multidimensional (1D-6D) B-spline interpolation of data on a regular grid. +! Basic pure subroutine interface. +! +!### Notes +! +! This module is based on the B-spline and spline routines from [1]. +! The original Fortran 77 routines were converted to free-form source. +! Some of them are relatively unchanged from the originals, but some have +! been extensively refactored. In addition, new routines for +! 1d, 4d, 5d, and 6d interpolation were also created (these are simply +! extensions of the same algorithm into higher dimensions). +! +!### See also +! * An object-oriented interface can be found in [[bspline_oo_module]]. +! +!### References +! +! 1. DBSPLIN and DTENSBS from the +! [NIST Core Math Library](http://www.nist.gov/itl/math/mcsd-software.cfm). +! Original code is public domain. +! 2. Carl de Boor, "A Practical Guide to Splines", +! Springer-Verlag, New York, 1978. +! 3. Carl de Boor, [Efficient Computer Manipulation of Tensor +! Products](http://dl.acm.org/citation.cfm?id=355831), +! ACM Transactions on Mathematical Software, +! Vol. 5 (1979), p. 173-182. +! 4. D.E. Amos, "Computation with Splines and B-Splines", +! SAND78-1968, Sandia Laboratories, March, 1979. +! 5. Carl de Boor, +! [Package for calculating with B-splines](http://epubs.siam.org/doi/abs/10.1137/0714026), +! SIAM Journal on Numerical Analysis 14, 3 (June 1977), p. 441-472. +! 6. D.E. Amos, "Quadrature subroutines for splines and B-splines", +! Report SAND79-1825, Sandia Laboratories, December 1979. + + module bspline_sub_module + + use bspline_kinds_module, only: wp, ip + use,intrinsic :: iso_fortran_env, only: error_unit + + implicit none + + private + + abstract interface + function b1fqad_func(x) result(f) + !! interface for the input function in [[dbfqad]] + import :: wp + implicit none + real(wp),intent(in) :: x + real(wp) :: f !! f(x) + end function b1fqad_func + end interface + public :: b1fqad_func + + integer(ip),parameter,public :: bspline_order_linear = 2_ip !! spline order `k` parameter + !! (for input to the `db*ink` routines) + !! [order = polynomial degree + 1] + integer(ip),parameter,public :: bspline_order_quadratic = 3_ip !! spline order `k` parameter + !! (for input to the `db*ink` routines) + !! [order = polynomial degree + 1] + integer(ip),parameter,public :: bspline_order_cubic = 4_ip !! spline order `k` parameter + !! (for input to the `db*ink` routines) + !! [order = polynomial degree + 1] + integer(ip),parameter,public :: bspline_order_quartic = 5_ip !! spline order `k` parameter + !! (for input to the `db*ink` routines) + !! [order = polynomial degree + 1] + integer(ip),parameter,public :: bspline_order_quintic = 6_ip !! spline order `k` parameter + !! (for input to the `db*ink` routines) + !! [order = polynomial degree + 1] + integer(ip),parameter,public :: bspline_order_hexic = 7_ip !! spline order `k` parameter + !! (for input to the `db*ink` routines) + !! [order = polynomial degree + 1] + integer(ip),parameter,public :: bspline_order_heptic = 8_ip !! spline order `k` parameter + !! (for input to the `db*ink` routines) + !! [order = polynomial degree + 1] + integer(ip),parameter,public :: bspline_order_octic = 9_ip !! spline order `k` parameter + !! (for input to the `db*ink` routines) + !! [order = polynomial degree + 1] + + interface db1ink + !! 1D initialization routines. + module procedure :: db1ink_default, db1ink_alt, db1ink_alt_2 + end interface + interface db1val + !! 1D evaluation routines. + module procedure :: db1val_default, db1val_alt + end interface + + !main routines: + public :: db1ink, db1val, db1sqad, db1fqad + public :: db2ink, db2val + public :: db3ink, db3val + public :: db4ink, db4val + public :: db5ink, db5val + public :: db6ink, db6val + + public :: get_status_message + + contains +!***************************************************************************************** + +!***************************************************************************************** +!> +! Determines the parameters of a function that interpolates +! the one-dimensional gridded data +! $$ [x(i),\mathrm{fcn}(i)] ~\mathrm{for}~ i=1,..,n_x $$ +! The interpolating function and its derivatives may +! subsequently be evaluated by the function [[db1val]]. +! +!### History +! * Jacob Williams, 10/30/2015 : Created 1D routine. + + pure subroutine db1ink_default(x,nx,fcn,kx,iknot,tx,bcoef,iflag) + + implicit none + + integer(ip),intent(in) :: nx !! Number of \(x\) abcissae + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: fcn !! `(nx)` array of function values to interpolate. `fcn(i)` should + !! contain the function value at the point `x(i)` + integer(ip),intent(in) :: iknot !! knot sequence flag: + !! + !! * 0 = knot sequence chosen by [[db1ink]]. + !! * 1 = knot sequence chosen by user. + real(wp),dimension(:),intent(inout) :: tx !! The `(nx+kx)` knots in the \(x\) direction + !! for the spline interpolant: + !! + !! * If `iknot=0` these are chosen by [[db1ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(out) :: bcoef !! `(nx)` array of coefficients of the b-spline interpolant. + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * 0 = successful execution. + !! * 2 = `iknot` out of range. + !! * 3 = `nx` out of range. + !! * 4 = `kx` out of range. + !! * 5 = `x` not strictly increasing. + !! * 6 = `tx` not non-decreasing. + !! * 700 = `size(x)` \( \ne \) `size(fcn,1)`. + !! * 706 = `size(x)` \( \ne \) `nx`. + !! * 712 = `size(tx)` \( \ne \) `nx+kx`. + !! * 800 = `size(x)` \( \ne \) `size(bcoef,1)`. + + logical :: status_ok + real(wp),dimension(:),allocatable :: work !! work array of dimension `2*kx*(nx+1)` + + !check validity of inputs + + call check_inputs( iknot,& + iflag,& + nx=nx,& + kx=kx,& + x=x,& + f1=fcn,& + bcoef1=bcoef,& + tx=tx,& + status_ok=status_ok) + + if (status_ok) then + + !choose knots + if (iknot == 0_ip) then + call dbknot(x,nx,kx,tx) + end if + + allocate(work(2_ip*kx*(nx+1_ip))) + + !construct b-spline coefficients + call dbtpcf(x,nx,fcn,nx,1_ip,tx,kx,bcoef,work,iflag) + + deallocate(work) + + end if + + end subroutine db1ink_default +!***************************************************************************************** + +!***************************************************************************************** +!> +! Alternate version of [[db1ink_default]], where the boundary conditions can be specified. +! +!### History +! * Jacob Williams, 9/4/2018 : created this routine. +! +!### See also +! * [[dbint4]] -- the main routine that is called here. +! +!@note Currently, this only works for 3rd order (k=4). + + pure subroutine db1ink_alt(x,nx,fcn,kx,ibcl,ibcr,fbcl,fbcr,kntopt,tx,bcoef,iflag) + + implicit none + + real(wp),dimension(:),intent(in) :: x !! \(x\) vector of abscissae of length `nx`, distinct + !! and in increasing order + integer(ip),intent(in) :: nx !! number of data points, \( n_x \ge 2 \) + real(wp),dimension(:),intent(in) :: fcn !! \(y\) vector of ordinates of length `nx` + integer(ip),intent(in) :: kx !! spline order (Currently, this must be `4`) + integer(ip),intent(in) :: ibcl !! selection parameter for left boundary condition: + !! + !! * `ibcl = 1` constrain the first derivative at `x(1)` to `fbcl` + !! * `ibcl = 2` constrain the second derivative at `x(1)` to `fbcl` + integer(ip),intent(in) :: ibcr !! selection parameter for right boundary condition: + !! + !! * `ibcr = 1` constrain first derivative at `x(nx)` to `fbcr` + !! * `ibcr = 2` constrain second derivative at `x(nx)` to `fbcr` + real(wp),intent(in) :: fbcl !! left boundary values governed by `ibcl` + real(wp),intent(in) :: fbcr !! right boundary values governed by `ibcr` + integer(ip),intent(in) :: kntopt !! knot selection parameter: + !! + !! * `kntopt = 1` sets knot multiplicity at `t(4)` and + !! `t(nx+3)` to 4 + !! * `kntopt = 2` sets a symmetric placement of knots + !! about `t(4)` and `t(nx+3)` + real(wp),dimension(:),intent(out) :: tx !! knot array of length `nx+6` + real(wp),dimension(:),intent(out) :: bcoef !! b spline coefficient array of length `nx+2` + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * 0: no errors + !! * 806: [[dbint4]] can only be used when `k=4` + + real(wp),dimension(:,:),allocatable :: w !! work array of dimension `5,nx+2` + integer(ip) :: n !! number of coefficients (n=nx+2) + integer(ip) :: k !! order of spline (k=4) + logical :: status_ok !! status flag for error checking + + real(wp),dimension(3),parameter :: tleft = 0.0_wp !! not used for this case (see [[dbint4]]) + real(wp),dimension(3),parameter :: tright = 0.0_wp !! not used for this case (see [[dbint4]]) + + + if (kx /= 4_ip) then + iflag = 806_ip + else + + call check_inputs( 1_ip,& ! so it will check size of t + iflag,& + nx=nx,& + kx=kx,& + x=x,& + f1=fcn,& + bcoef1=bcoef,& + tx=tx,& + status_ok=status_ok,& + alt=.true.) + + if (status_ok) then + allocate(w(5_ip,nx+2_ip)) + call dbint4(x,fcn,nx,ibcl,ibcr,fbcl,fbcr,kntopt,tleft,tright,tx,bcoef,n,k,w,iflag) + deallocate(w) + end if + + end if + + end subroutine db1ink_alt +!***************************************************************************************** + +!***************************************************************************************** +!> +! Alternate version of [[db1ink_alt]], where the first and +! last 3 knots are specified by the user. +! +!### History +! * Jacob Williams, 9/4/2018 : created this routine. +! +!### See also +! * [[dbint4]] -- the main routine that is called here. +! +!@note Currently, this only works for 3rd order (k=4). + + pure subroutine db1ink_alt_2(x,nx,fcn,kx,ibcl,ibcr,fbcl,fbcr,tleft,tright,tx,bcoef,iflag) + + implicit none + + real(wp),dimension(:),intent(in) :: x !! \(x\) vector of abscissae of length `nx`, distinct + !! and in increasing order + integer(ip),intent(in) :: nx !! number of data points, \( n_x \ge 2 \) + real(wp),dimension(:),intent(in) :: fcn !! \(y\) vector of ordinates of length `nx` + integer(ip),intent(in) :: kx !! spline order (Currently, this must be `4`) + integer(ip),intent(in) :: ibcl !! selection parameter for left boundary condition: + !! + !! * `ibcl = 1` constrain the first derivative at `x(1)` to `fbcl` + !! * `ibcl = 2` constrain the second derivative at `x(1)` to `fbcl` + integer(ip),intent(in) :: ibcr !! selection parameter for right boundary condition: + !! + !! * `ibcr = 1` constrain first derivative at `x(nx)` to `fbcr` + !! * `ibcr = 2` constrain second derivative at `x(nx)` to `fbcr` + real(wp),intent(in) :: fbcl !! left boundary values governed by `ibcl` + real(wp),intent(in) :: fbcr !! right boundary values governed by `ibcr` + real(wp),dimension(3),intent(in) :: tleft !! `t(1:3)` in increasing order supplied by the user. + real(wp),dimension(3),intent(in) :: tright !! `t(nx+4:nx+6)` in increasing order supplied by the user. + real(wp),dimension(:),intent(out) :: tx !! knot array of length `nx+6` + real(wp),dimension(:),intent(out) :: bcoef !! b spline coefficient array of length `nx+2` + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * 0: no errors + !! * 806: [[dbint4]] can only be used when k=4 + + real(wp),dimension(:,:),allocatable :: w !! work array of dimension `5,nx+2` + integer(ip) :: n !! number of coefficients (`n=nx+2`) + integer(ip) :: k !! order of spline (`k=4`) + logical :: status_ok !! status flag for error checking + + integer(ip),parameter :: kntopt = 3 !! use `tleft` and `tright` in [[dbint4]] + + if (kx /= 4_ip) then + iflag = 806_ip + else + + call check_inputs( 1_ip,& ! so it will check size of t + iflag,& + nx=nx,& + kx=kx,& + x=x,& + f1=fcn,& + bcoef1=bcoef,& + tx=tx,& + status_ok=status_ok,& + alt=.true.) + + if (status_ok) then + allocate(w(5,nx+2)) + call dbint4(x,fcn,nx,ibcl,ibcr,fbcl,fbcr,kntopt,tleft,tright,tx,bcoef,n,k,w,iflag) + deallocate(w) + end if + + end if + + end subroutine db1ink_alt_2 +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluates the tensor product piecewise polynomial +! interpolant constructed by the routine [[db1ink]] or one of its +! derivatives at the point `xval`. +! +! To evaluate the interpolant itself, set `idx=0`, +! to evaluate the first partial with respect to `x`, set `idx=1`, and so on. +! +! [[db1val]] returns 0.0 if (`xval`,`yval`) is out of range. that is, if +!```fortran +! xval < tx(1) .or. xval > tx(nx+kx) +!``` +! if the knots `tx` were chosen by [[db1ink]], then this is equivalent to: +!```fortran +! xval < x(1) .or. xval > x(nx)+epsx +!``` +! where +!```fortran +! epsx = 0.1*(x(nx)-x(nx-1)) +!``` +! +! The input quantities `tx`, `nx`, `kx`, and `bcoef` should be +! unchanged since the last call of [[db1ink]]. +! +!### History +! * Jacob Williams, 10/30/2015 : Created 1D routine. + + pure subroutine db1val_default(xval,idx,tx,nx,kx,bcoef,f,iflag,inbvx,w0,extrap) + + implicit none + + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: nx !! the number of interpolation points in \(x\). + !! (same as in last call to [[db1ink]]) + integer(ip),intent(in) :: kx !! order of polynomial pieces in \(x\). + !! (same as in last call to [[db1ink]]) + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + real(wp),dimension(nx+kx),intent(in) :: tx !! sequence of knots defining the piecewise polynomial + !! in the \(x\) direction. (same as in last call to [[db1ink]]) + real(wp),dimension(nx),intent(in) :: bcoef !! the b-spline coefficients computed by [[db1ink]]. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * \( = 0 \) : no errors + !! * \( \ne 0 \) : error + integer(ip),intent(inout) :: inbvx !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + real(wp),dimension(3_ip*kx),intent(inout) :: w0 !! work array + logical,intent(in),optional :: extrap !! if extrapolation is allowed + !! (if not present, default is False) + + f = 0.0_wp + + iflag = check_value(xval,tx,1_ip,extrap); if (iflag/=0_ip) return + + call dbvalu(tx,bcoef,nx,kx,idx,xval,inbvx,w0,iflag,f,extrap) + + end subroutine db1val_default +!***************************************************************************************** + +!***************************************************************************************** +!> +! Alternate version of [[db1val_default]] for use with [[db1ink_alt]] and [[db1ink_alt_2]]. + + pure subroutine db1val_alt(xval,idx,tx,nx,n,kx,bcoef,f,iflag,inbvx,w0,extrap) + + implicit none + + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: nx !! the number of interpolation points in \(x\). + integer(ip),intent(in) :: n !! length of `bcoef`: `nx+2` + integer(ip),intent(in) :: kx !! order of polynomial pieces in \(x\). + !! (same as in last call to [[db1ink]]) + real(wp),dimension(n+kx),intent(in) :: tx !! sequence of knots defining the piecewise polynomial + !! in the \(x\) direction. + real(wp),dimension(n),intent(in) :: bcoef !! the b-spline coefficients computed by [[db1ink]]. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * \( = 0 \) : no errors + !! * \( \ne 0 \) : error + integer(ip),intent(inout) :: inbvx !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + real(wp),dimension(3_ip*kx),intent(inout) :: w0 !! work array + logical,intent(in),optional :: extrap !! if extrapolation is allowed + !! (if not present, default is False) + + f = 0.0_wp + + iflag = check_value(xval,tx,1_ip,extrap); if (iflag/=0_ip) return + + call dbvalu(tx,bcoef,n,kx,idx,xval,inbvx,w0,iflag,f,extrap) + + end subroutine db1val_alt +!***************************************************************************************** + +!***************************************************************************************** +!> +! Computes the integral on `(x1,x2)` of a `kx`-th order b-spline. +! Orders `kx` as high as 20 are permitted by applying a 2, 6, or 10 +! point gauss formula on subintervals of `(x1,x2)` which are +! formed by included (distinct) knots. +! +!### See also +! * [[dbsqad]] -- the core routine. + + pure subroutine db1sqad(tx,bcoef,nx,kx,x1,x2,f,iflag,w0) + + implicit none + + integer(ip),intent(in) :: nx !! length of coefficient array + integer(ip),intent(in) :: kx !! order of b-spline, `1 <= k <= 20` + real(wp),dimension(nx+kx),intent(in) :: tx !! knot array + real(wp),dimension(nx),intent(in) :: bcoef !! b-spline coefficient array + real(wp),intent(in) :: x1 !! left point of quadrature interval in `t(kx) <= x <= t(nx+1)` + real(wp),intent(in) :: x2 !! right point of quadrature interval in `t(kx) <= x <= t(nx+1)` + real(wp),intent(out) :: f !! integral of the b-spline over (`x1`,`x2`) + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * \( = 0 \) : no errors + !! * \( \ne 0 \) : error + real(wp),dimension(3*kx),intent(inout) :: w0 !! work array for [[dbsqad]] + + call dbsqad(tx,bcoef,nx,kx,x1,x2,f,w0,iflag) + + end subroutine db1sqad +!***************************************************************************************** + +!***************************************************************************************** +!> +! Computes the integral on `(x1,x2)` of a product of a +! function `fun` and the `idx`-th derivative of a `kx`-th order b-spline, +! using the b-representation `(tx,bcoef,nx,kx)`, with an adaptive +! 8-point Legendre-Gauss algorithm. +! `(x1,x2)` must be a subinterval of `t(kx) <= x <= t(nx+1)`. +! +!### See also +! * [[dbfqad]] -- the core routine. +! +!@note This one is not pure, because we are not enforcing +! that the user function `fun` be pure. + + subroutine db1fqad(fun,tx,bcoef,nx,kx,idx,x1,x2,tol,f,iflag,w0) + + implicit none + + procedure(b1fqad_func) :: fun !! external function of one argument for the + !! integrand `bf(x)=fun(x)*dbvalu(tx,bcoef,nx,kx,id,x,inbv,work)` + integer(ip),intent(in) :: nx !! length of coefficient array + integer(ip),intent(in) :: kx !! order of b-spline, `kx >= 1` + real(wp),dimension(nx+kx),intent(in):: tx !! knot array + real(wp),dimension(nx),intent(in) :: bcoef !! b-spline coefficient array + integer(ip),intent(in) :: idx !! order of the spline derivative, `0 <= idx <= k-1` + !! `idx=0` gives the spline function + real(wp),intent(in) :: x1 !! left point of quadrature interval in `t(k) <= x <= t(n+1)` + real(wp),intent(in) :: x2 !! right point of quadrature interval in `t(k) <= x <= t(n+1)` + real(wp),intent(in) :: tol !! desired accuracy for the quadrature, suggest + !! `10*dtol < tol <= 0.1` where `dtol` is the maximum + !! of `1.0e-300` and real(wp) unit roundoff for + !! the machine + real(wp),intent(out) :: f !! integral of `bf(x)` on `(x1,x2)` + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * \( = 0 \) : no errors + !! * \( \ne 0 \) : error + real(wp),dimension(3_ip*kx),intent(inout) :: w0 !! work array for [[dbfqad]] + + call dbfqad(fun,tx,bcoef,nx,kx,idx,x1,x2,tol,f,iflag,w0) + + end subroutine db1fqad +!***************************************************************************************** + +!***************************************************************************************** +!> +! Determines the parameters of a function that interpolates +! the two-dimensional gridded data +! $$ [x(i),y(j),\mathrm{fcn}(i,j)] ~\mathrm{for}~ i=1,..,n_x ~\mathrm{and}~ j=1,..,n_y $$ +! The interpolating function and its derivatives may +! subsequently be evaluated by the function [[db2val]]. +! +! The interpolating function is a piecewise polynomial function +! represented as a tensor product of one-dimensional b-splines. the +! form of this function is +! +! $$ s(x,y) = \sum_{i=1}^{n_x} \sum_{j=1}^{n_y} a_{ij} u_i(x) v_j(y) $$ +! +! where the functions \(u_i\) and \(v_j\) are one-dimensional b-spline +! basis functions. the coefficients \( a_{ij} \) are chosen so that +! +! $$ s(x(i),y(j)) = \mathrm{fcn}(i,j) ~\mathrm{for}~ i=1,..,n_x ~\mathrm{and}~ j=1,..,n_y $$ +! +! Note that for each fixed value of \(y\), \( s(x,y) \) is a piecewise +! polynomial function of \(x\) alone, and for each fixed value of \(x\), \( s(x,y) \) +! is a piecewise polynomial function of \(y\) alone. in one dimension +! a piecewise polynomial may be created by partitioning a given +! interval into subintervals and defining a distinct polynomial piece +! on each one. the points where adjacent subintervals meet are called +! knots. each of the functions \(u_i\) and \(v_j\) above is a piecewise +! polynomial. +! +! Users of [[db2ink]] choose the order (degree+1) of the polynomial +! pieces used to define the piecewise polynomial in each of the \(x\) and +! \(y\) directions (`kx` and `ky`). users also may define their own knot +! sequence in \(x\) and \(y\) separately (`tx` and `ty`). if `iflag=0`, however, +! [[db2ink]] will choose sequences of knots that result in a piecewise +! polynomial interpolant with `kx-2` continuous partial derivatives in +! \(x\) and `ky-2` continuous partial derivatives in \(y\). (`kx` knots are taken +! near each endpoint in the \(x\) direction, not-a-knot end conditions +! are used, and the remaining knots are placed at data points if `kx` +! is even or at midpoints between data points if `kx` is odd. the \(y\) +! direction is treated similarly.) +! +! After a call to [[db2ink]], all information necessary to define the +! interpolating function are contained in the parameters `nx`, `ny`, `kx`, +! `ky`, `tx`, `ty`, and `bcoef`. These quantities should not be altered until +! after the last call of the evaluation routine [[db2val]]. +! +!### History +! * Boisvert, Ronald, NBS : 25 may 1982 : Author of original routine. +! * JEC : 000330 modified array declarations. +! * Jacob Williams, 2/24/2015 : extensive refactoring of CMLIB routine. + + pure subroutine db2ink(x,nx,y,ny,fcn,kx,ky,iknot,tx,ty,bcoef,iflag) + + implicit none + + integer(ip),intent(in) :: nx !! Number of \(x\) abcissae + integer(ip),intent(in) :: ny !! Number of \(y\) abcissae + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. Must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. Must be strictly increasing. + real(wp),dimension(:,:),intent(in) :: fcn !! `(nx,ny)` matrix of function values to interpolate. + !! `fcn(i,j)` should contain the function value at the + !! point (`x(i)`,`y(j)`) + integer(ip),intent(in) :: iknot !! knot sequence flag: + !! + !! * 0 = knot sequence chosen by [[db1ink]]. + !! * 1 = knot sequence chosen by user. + real(wp),dimension(:),intent(inout) :: tx !! The `(nx+kx)` knots in the \(x\) direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db2ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: ty !! The `(ny+ky)` knots in the \(y\) direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db2ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:,:),intent(out) :: bcoef !! `(nx,ny)` matrix of coefficients of the b-spline interpolant. + integer(ip),intent(out) :: iflag !! * 0 = successful execution. + !! * 2 = `iknot` out of range. + !! * 3 = `nx` out of range. + !! * 4 = `kx` out of range. + !! * 5 = `x` not strictly increasing. + !! * 6 = `tx` not non-decreasing. + !! * 7 = `ny` out of range. + !! * 8 = `ky` out of range. + !! * 9 = `y` not strictly increasing. + !! * 10 = `ty` not non-decreasing. + !! * 700 = `size(x)` \( \ne \) `size(fcn,1)` + !! * 701 = `size(y)` \( \ne \) `size(fcn,2)` + !! * 706 = `size(x)` \( \ne \) `nx` + !! * 707 = `size(y)` \( \ne \) `ny` + !! * 712 = `size(tx)` \( \ne \) `nx+kx` + !! * 713 = `size(ty)` \( \ne \) `ny+ky` + !! * 800 = `size(x)` \( \ne \) `size(bcoef,1)` + !! * 801 = `size(y)` \( \ne \) `size(bcoef,2)` + + logical :: status_ok + real(wp),dimension(:),allocatable :: temp !! work array of length `nx*ny` + real(wp),dimension(:),allocatable :: work !! work array of length `max(2*kx*(nx+1),2*ky*(ny+1))` + + !check validity of inputs + + call check_inputs( iknot,& + iflag,& + nx=nx,ny=ny,& + kx=kx,ky=ky,& + x=x,y=y,& + tx=tx,ty=ty,& + f2=fcn,& + bcoef2=bcoef,& + status_ok=status_ok) + + if (status_ok) then + + !choose knots + if (iknot == 0_ip) then + call dbknot(x,nx,kx,tx) + call dbknot(y,ny,ky,ty) + end if + + allocate(temp(nx*ny)) + allocate(work(max(2_ip*kx*(nx+1_ip),2_ip*ky*(ny+1_ip)))) + + !construct b-spline coefficients + call dbtpcf(x,nx,fcn, nx,ny,tx,kx,temp, work,iflag) + if (iflag==0_ip) call dbtpcf(y,ny,temp,ny,nx,ty,ky,bcoef,work,iflag) + + deallocate(temp) + deallocate(work) + + end if + + end subroutine db2ink +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluates the tensor product piecewise polynomial +! interpolant constructed by the routine [[db2ink]] or one of its +! derivatives at the point (`xval`,`yval`). +! +! To evaluate the interpolant +! itself, set `idx=idy=0`, to evaluate the first partial with respect +! to `x`, set `idx=1,idy=0`, and so on. +! +! [[db2val]] returns 0.0 if `(xval,yval)` is out of range. that is, if +!```fortran +! xval < tx(1) .or. xval > tx(nx+kx) .or. +! yval < ty(1) .or. yval > ty(ny+ky) +!``` +! if the knots tx and ty were chosen by [[db2ink]], then this is equivalent to: +!```fortran +! xval < x(1) .or. xval > x(nx)+epsx .or. +! yval < y(1) .or. yval > y(ny)+epsy +!``` +! where +!```fortran +! epsx = 0.1*(x(nx)-x(nx-1)) +! epsy = 0.1*(y(ny)-y(ny-1)) +!``` +! +! The input quantities `tx`, `ty`, `nx`, `ny`, `kx`, `ky`, and `bcoef` should be +! unchanged since the last call of [[db2ink]]. +! +!### History +! * Boisvert, Ronald, NBS : 25 may 1982 : Author of original routine. +! * JEC : 000330 modified array declarations. +! * Jacob Williams, 2/24/2015 : extensive refactoring of CMLIB routine. + + pure subroutine db2val(xval,yval,idx,idy,tx,ty,nx,ny,kx,ky,bcoef,f,iflag,inbvx,inbvy,iloy,w1,w0,extrap) + + implicit none + + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idy !! \(y\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: nx !! the number of interpolation points in \(x\). + !! (same as in last call to [[db2ink]]) + integer(ip),intent(in) :: ny !! the number of interpolation points in \(y\). + !! (same as in last call to [[db2ink]]) + integer(ip),intent(in) :: kx !! order of polynomial pieces in \(x\). + !! (same as in last call to [[db2ink]]) + integer(ip),intent(in) :: ky !! order of polynomial pieces in \(y\). + !! (same as in last call to [[db2ink]]) + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + real(wp),intent(in) :: yval !! \(y\) coordinate of evaluation point. + real(wp),dimension(nx+kx),intent(in) :: tx !! sequence of knots defining the piecewise polynomial + !! in the \(x\) direction. + !! (same as in last call to [[db2ink]]) + real(wp),dimension(ny+ky),intent(in) :: ty !! sequence of knots defining the piecewise + !! polynomial in the \(y\) direction. + !! (same as in last call to [[db2ink]]) + real(wp),dimension(nx,ny),intent(in) :: bcoef !! the b-spline coefficients computed by [[db2ink]]. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * \( = 0 \) : no errors + !! * \( \ne 0 \) : error + integer(ip),intent(inout) :: inbvx !! initialization parameter which must be set to 1 + !! the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvy !! initialization parameter which must be set to 1 + !! the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloy !! initialization parameter which must be set to 1 + !! the first time this routine is called, + !! and must not be changed by the user. + real(wp),dimension(ky),intent(inout) :: w1 !! work array + real(wp),dimension(3_ip*max(kx,ky)),intent(inout) :: w0 !! work array + logical,intent(in),optional :: extrap !! if extrapolation is allowed + !! (if not present, default is False) + + integer(ip) :: k, lefty, kcol + + f = 0.0_wp + + iflag = check_value(xval,tx,1_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(yval,ty,2_ip,extrap); if (iflag/=0_ip) return + + call dintrv(ty,ny+ky,yval,iloy,lefty,iflag,extrap); if (iflag/=0_ip) return + + kcol = lefty - ky + do k=1_ip,ky + kcol = kcol + 1_ip + call dbvalu(tx,bcoef(:,kcol),nx,kx,idx,xval,inbvx,w0,iflag,w1(k),extrap) + if (iflag/=0_ip) return !error + end do + + kcol = lefty - ky + 1_ip + call dbvalu(ty(kcol:),w1,ky,ky,idy,yval,inbvy,w0,iflag,f,extrap) + + end subroutine db2val +!***************************************************************************************** + +!***************************************************************************************** +!> +! Determines the parameters of a function that interpolates +! the three-dimensional gridded data +! $$ [x(i),y(j),z(k),\mathrm{fcn}(i,j,k)] ~\mathrm{for}~ +! i=1,..,n_x ~\mathrm{and}~ j=1,..,n_y, ~\mathrm{and}~ k=1,..,n_z $$ +! The interpolating function and +! its derivatives may subsequently be evaluated by the function +! [[db3val]]. +! +! The interpolating function is a piecewise polynomial function +! represented as a tensor product of one-dimensional b-splines. the +! form of this function is +! $$ s(x,y,z) = \sum_{i=1}^{n_x} \sum_{j=1}^{n_y} \sum_{k=1}^{n_z} +! a_{ijk} u_i(x) v_j(y) w_k(z) $$ +! +! where the functions \(u_i\), \(v_j\), and \(w_k\) are one-dimensional b- +! spline basis functions. the coefficients \(a_{ijk}\) are chosen so that: +! +! $$ s(x(i),y(j),z(k)) = \mathrm{fcn}(i,j,k) +! ~\mathrm{for}~ i=1,..,n_x , j=1,..,n_y , k=1,..,n_z $$ +! +! Note that for fixed values of \(y\) and \(z\) \(s(x,y,z)\) is a piecewise +! polynomial function of \(x\) alone, for fixed values of \(x\) and \(z\) \(s(x,y,z)\) +! is a piecewise polynomial function of \(y\) alone, and for fixed +! values of \(x\) and \(y\) \(s(x,y,z)\) is a function of \(z\) alone. in one +! dimension a piecewise polynomial may be created by partitioning a +! given interval into subintervals and defining a distinct polynomial +! piece on each one. the points where adjacent subintervals meet are +! called knots. each of the functions \(u_i\), \(v_j\), and \(w_k\) above is a +! piecewise polynomial. +! +! Users of [[db3ink]] choose the order (degree+1) of the polynomial +! pieces used to define the piecewise polynomial in each of the \(x\), \(y\), +! and \(z\) directions (`kx`, `ky`, and `kz`). users also may define their own +! knot sequence in \(x\), \(y\), \(z\) separately (`tx`, `ty`, and `tz`). if `iflag=0`, +! however, [[db3ink]] will choose sequences of knots that result in a +! piecewise polynomial interpolant with `kx-2` continuous partial +! derivatives in \(x\), `ky-2` continuous partial derivatives in \(y\), and `kz-2` +! continuous partial derivatives in \(z\). (`kx` knots are taken near +! each endpoint in \(x\), not-a-knot end conditions are used, and the +! remaining knots are placed at data points if `kx` is even or at +! midpoints between data points if `kx` is odd. the \(y\) and \(z\) directions +! are treated similarly.) +! +! After a call to [[db3ink]], all information necessary to define the +! interpolating function are contained in the parameters `nx`, `ny`, `nz`, +! `kx`, `ky`, `kz`, `tx`, `ty`, `tz`, and `bcoef`. these quantities should not be +! altered until after the last call of the evaluation routine [[db3val]]. +! +!### History +! * Boisvert, Ronald, NBS : 25 may 1982 : Author of original routine. +! * JEC : 000330 modified array declarations. +! * Jacob Williams, 2/24/2015 : extensive refactoring of CMLIB routine. + + pure subroutine db3ink(x,nx,y,ny,z,nz,fcn,kx,ky,kz,iknot,tx,ty,tz,bcoef,iflag) + + implicit none + + integer(ip),intent(in) :: nx !! number of \(x\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: ny !! number of \(y\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: nz !! number of \(z\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: kx !! The order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! The order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! the order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. must be strictly increasing. + real(wp),dimension(:,:,:),intent(in) :: fcn !! `(nx,ny,nz)` matrix of function values to interpolate. `fcn(i,j,k)` should + !! contain the function value at the point (`x(i)`,`y(j)`,`z(k)`) + integer(ip),intent(in) :: iknot !! knot sequence flag: + !! + !! * 0 = knot sequence chosen by [[db3ink]]. + !! * 1 = knot sequence chosen by user. + real(wp),dimension(:),intent(inout) :: tx !! The `(nx+kx)` knots in the \(x\) direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db3ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: ty !! The `(ny+ky)` knots in the \(y\) direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db3ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: tz !! The `(nz+kz)` knots in the \(z\) direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db3ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:,:,:),intent(out) :: bcoef !! `(nx,ny,nz)` matrix of coefficients of the b-spline interpolant. + integer(ip),intent(out) :: iflag !! * 0 = successful execution. + !! * 2 = `iknot` out of range. + !! * 3 = `nx` out of range. + !! * 4 = `kx` out of range. + !! * 5 = `x` not strictly increasing. + !! * 6 = `tx` not non-decreasing. + !! * 7 = `ny` out of range. + !! * 8 = `ky` out of range. + !! * 9 = `y` not strictly increasing. + !! * 10 = `ty` not non-decreasing. + !! * 11 = `nz` out of range. + !! * 12 = `kz` out of range. + !! * 13 = `z` not strictly increasing. + !! * 14 = `ty` not non-decreasing. + !! * 700 = `size(x) ` \(\ne\) `size(fcn,1)` + !! * 701 = `size(y) ` \(\ne\) `size(fcn,2)` + !! * 702 = `size(z) ` \(\ne\) `size(fcn,3)` + !! * 706 = `size(x) ` \(\ne\) `nx` + !! * 707 = `size(y) ` \(\ne\) `ny` + !! * 708 = `size(z) ` \(\ne\) `nz` + !! * 712 = `size(tx)` \(\ne\) `nx+kx` + !! * 713 = `size(ty)` \(\ne\) `ny+ky` + !! * 714 = `size(tz)` \(\ne\) `nz+kz` + !! * 800 = `size(x) ` \(\ne\) `size(bcoef,1)` + !! * 801 = `size(y) ` \(\ne\) `size(bcoef,2)` + !! * 802 = `size(z) ` \(\ne\) `size(bcoef,3)` + + logical :: status_ok + real(wp),dimension(:),allocatable :: temp !! work array of length `nx*ny*nz` + real(wp),dimension(:),allocatable :: work !! work array of length `max(2*kx*(nx+1), + !! 2*ky*(ny+1),2*kz*(nz+1))` + integer(ip) :: i, j, k, ii !! counter + + ! check validity of input + + call check_inputs( iknot,& + iflag,& + nx=nx,ny=ny,nz=nz,& + kx=kx,ky=ky,kz=kz,& + x=x,y=y,z=z,& + tx=tx,ty=ty,tz=tz,& + f3=fcn,& + bcoef3=bcoef,& + status_ok=status_ok) + + if (status_ok) then + + ! choose knots + if (iknot == 0_ip) then + call dbknot(x,nx,kx,tx) + call dbknot(y,ny,ky,ty) + call dbknot(z,nz,kz,tz) + end if + + allocate(temp(nx*ny*nz)) + allocate(work(max(2_ip*kx*(nx+1_ip),2_ip*ky*(ny+1_ip),2_ip*kz*(nz+1_ip)))) + + ! copy fcn to work in packed for dbtpcf + !temp = reshape( fcn, [nx*ny*nz] ) + ! replaced with loops to avoid stack + ! overflow for large data set: + ii = 0_ip + do k = 1_ip, nz + do j = 1_ip, ny + do i = 1_ip, nx + ii = ii + 1_ip + temp(ii) = fcn(i,j,k) + end do + end do + end do + + ! construct b-spline coefficients + call dbtpcf(x,nx,temp, nx,ny*nz,tx,kx,bcoef,work,iflag) + if (iflag==0_ip) call dbtpcf(y,ny,bcoef,ny,nx*nz,ty,ky,temp, work,iflag) + if (iflag==0_ip) call dbtpcf(z,nz,temp, nz,nx*ny,tz,kz,bcoef,work,iflag) + + deallocate(temp) + deallocate(work) + + end if + + end subroutine db3ink +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluates the tensor product piecewise polynomial +! interpolant constructed by the routine [[db3ink]] or one of its +! derivatives at the point (`xval`,`yval`,`zval`). +! +! To evaluate the +! interpolant itself, set `idx=idy=idz=0`, to evaluate the first +! partial with respect to `x`, set `idx=1`,`idy=idz=0`, and so on. +! +! [[db3val]] returns 0.0 if (`xval`,`yval`,`zval`) is out of range. that is, +!```fortran +! xvaltx(nx+kx) .or. +! yvalty(ny+ky) .or. +! zvaltz(nz+kz) +!``` +! if the knots `tx`, `ty`, and `tz` were chosen by [[db3ink]], then this is +! equivalent to +!```fortran +! xvalx(nx)+epsx .or. +! yvaly(ny)+epsy .or. +! zvalz(nz)+epsz +!``` +! where +!```fortran +! epsx = 0.1*(x(nx)-x(nx-1)) +! epsy = 0.1*(y(ny)-y(ny-1)) +! epsz = 0.1*(z(nz)-z(nz-1)) +!``` +! +! The input quantities `tx`, `ty`, `tz`, `nx`, `ny`, `nz`, `kx`, `ky`, `kz`, and `bcoef` +! should remain unchanged since the last call of [[db3ink]]. +! +!### History +! * Boisvert, Ronald, NBS : 25 may 1982 : Author of original routine. +! * JEC : 000330 modified array declarations. +! * Jacob Williams, 2/24/2015 : extensive refactoring of CMLIB routine. + + pure subroutine db3val(xval,yval,zval,idx,idy,idz,& + tx,ty,tz,& + nx,ny,nz,kx,ky,kz,bcoef,f,iflag,& + inbvx,inbvy,inbvz,iloy,iloz,w2,w1,w0,extrap) + + implicit none + + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idy !! \(y\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idz !! \(z\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: nx !! the number of interpolation points in \(x\). + !! (same as in last call to [[db3ink]]) + integer(ip),intent(in) :: ny !! the number of interpolation points in \(y\). + !! (same as in last call to [[db3ink]]) + integer(ip),intent(in) :: nz !! the number of interpolation points in \(z\). + !! (same as in last call to [[db3ink]]) + integer(ip),intent(in) :: kx !! order of polynomial pieces in \(z\). + !! (same as in last call to [[db3ink]]) + integer(ip),intent(in) :: ky !! order of polynomial pieces in \(y\). + !! (same as in last call to [[db3ink]]) + integer(ip),intent(in) :: kz !! order of polynomial pieces in \(z\). + !! (same as in last call to [[db3ink]]) + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + real(wp),intent(in) :: yval !! \(y\) coordinate of evaluation point. + real(wp),intent(in) :: zval !! \(z\) coordinate of evaluation point. + real(wp),dimension(nx+kx),intent(in) :: tx !! sequence of knots defining the piecewise polynomial + !! in the \(x\) direction. (same as in last call to [[db3ink]]) + real(wp),dimension(ny+ky),intent(in) :: ty !! sequence of knots defining the piecewise polynomial + !! in the \(y\) direction. (same as in last call to [[db3ink]]) + real(wp),dimension(nz+kz),intent(in) :: tz !! sequence of knots defining the piecewise polynomial + !! in the \(z\) direction. (same as in last call to [[db3ink]]) + real(wp),dimension(nx,ny,nz),intent(in) :: bcoef !! the b-spline coefficients computed by [[db3ink]]. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * \( = 0 \) : no errors + !! * \( \ne 0 \) : error + integer(ip),intent(inout) :: inbvx !! initialization parameter which must be + !! set to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvy !! initialization parameter which must be + !! set to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvz !! initialization parameter which must be + !! set to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloy !! initialization parameter which must be + !! set to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloz !! initialization parameter which must be + !! set to 1 the first time this routine is called, + !! and must not be changed by the user. + real(wp),dimension(ky,kz),intent(inout) :: w2 !! work array + real(wp),dimension(kz),intent(inout) :: w1 !! work array + real(wp),dimension(3_ip*max(kx,ky,kz)),intent(inout) :: w0 !! work array + logical,intent(in),optional :: extrap !! if extrapolation is allowed + !! (if not present, default is False) + + integer(ip) :: lefty, leftz, kcoly, kcolz, j, k + + f = 0.0_wp + + iflag = check_value(xval,tx,1_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(yval,ty,2_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(zval,tz,3_ip,extrap); if (iflag/=0_ip) return + + call dintrv(ty,ny+ky,yval,iloy,lefty,iflag,extrap); if (iflag/=0_ip) return + call dintrv(tz,nz+kz,zval,iloz,leftz,iflag,extrap); if (iflag/=0_ip) return + + iflag = 0_ip + + kcolz = leftz - kz + do k=1_ip,kz + kcolz = kcolz + 1_ip + kcoly = lefty - ky + do j=1_ip,ky + kcoly = kcoly + 1_ip + call dbvalu(tx,bcoef(:,kcoly,kcolz),nx,kx,idx,xval,inbvx,w0,iflag,w2(j,k),extrap) + if (iflag/=0_ip) return + end do + end do + + kcoly = lefty - ky + 1_ip + do k=1_ip,kz + call dbvalu(ty(kcoly:),w2(:,k),ky,ky,idy,yval,inbvy,w0,iflag,w1(k),extrap) + if (iflag/=0_ip) return + end do + + kcolz = leftz - kz + 1_ip + call dbvalu(tz(kcolz:),w1,kz,kz,idz,zval,inbvz,w0,iflag,f,extrap) + + end subroutine db3val +!***************************************************************************************** + +!***************************************************************************************** +!> +! Determines the parameters of a function that interpolates +! the four-dimensional gridded data +! $$ [x(i),y(j),z(k),q(l),\mathrm{fcn}(i,j,k,l)] ~\mathrm{for}~ +! i=1,..,n_x ~\mathrm{and}~ j=1,..,n_y, ~\mathrm{and}~ k=1,..,n_z, +! ~\mathrm{and}~ l=1,..,n_q $$ +! The interpolating function and its derivatives may +! subsequently be evaluated by the function [[db4val]]. +! +! See [[db3ink]] header for more details. +! +!### History +! * Jacob Williams, 2/24/2015 : Created this routine. + + pure subroutine db4ink(x,nx,y,ny,z,nz,q,nq,& + fcn,& + kx,ky,kz,kq,& + iknot,& + tx,ty,tz,tq,& + bcoef,iflag) + + implicit none + + integer(ip),intent(in) :: nx !! number of \(x\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: ny !! number of \(y\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: nz !! number of \(z\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: nq !! number of \(q\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: kx !! the order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ). + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! the order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ). + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! the order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ). + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! the order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ). + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. must be strictly increasing. + real(wp),dimension(:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq)` matrix of function values to interpolate. + !! `fcn(i,j,k,q)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`) + integer(ip),intent(in) :: iknot !! knot sequence flag: + !! + !! * 0 = knot sequence chosen by [[db4ink]]. + !! * 1 = knot sequence chosen by user. + real(wp),dimension(:),intent(inout) :: tx !! The `(nx+kx)` knots in the x direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db4ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: ty !! The `(ny+ky)` knots in the y direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db4ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: tz !! The `(nz+kz)` knots in the z direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db4ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: tq !! The `(nq+kq)` knots in the q direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db4ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:,:,:,:),intent(out) :: bcoef !! `(nx,ny,nz,nq)` matrix of coefficients of the b-spline + !! interpolant. + integer(ip),intent(out) :: iflag !! * 0 = successful execution. + !! * 2 = `iknot` out of range. + !! * 3 = `nx` out of range. + !! * 4 = `kx` out of range. + !! * 5 = `x` not strictly increasing. + !! * 6 = `tx` not non-decreasing. + !! * 7 = `ny` out of range. + !! * 8 = `ky` out of range. + !! * 9 = `y` not strictly increasing. + !! * 10 = `ty` not non-decreasing. + !! * 11 = `nz` out of range. + !! * 12 = `kz` out of range. + !! * 13 = `z` not strictly increasing. + !! * 14 = `tz` not non-decreasing. + !! * 15 = `nq` out of range. + !! * 16 = `kq` out of range. + !! * 17 = `q` not strictly increasing. + !! * 18 = `tq` not non-decreasing. + !! * 700 = `size(x)` \( \ne \) `size(fcn,1)` + !! * 701 = `size(y)` \( \ne \) `size(fcn,2)` + !! * 702 = `size(z)` \( \ne \) `size(fcn,3)` + !! * 703 = `size(q)` \( \ne \) `size(fcn,4)` + !! * 706 = `size(x)` \( \ne \) `nx` + !! * 707 = `size(y)` \( \ne \) `ny` + !! * 708 = `size(z)` \( \ne \) `nz` + !! * 709 = `size(q)` \( \ne \) `nq` + !! * 712 = `size(tx`) \( \ne \) `nx+kx` + !! * 713 = `size(ty`) \( \ne \) `ny+ky` + !! * 714 = `size(tz`) \( \ne \) `nz+kz` + !! * 715 = `size(tq`) \( \ne \) `nq+kq` + !! * 800 = `size(x)` \( \ne \) `size(bcoef,1)` + !! * 801 = `size(y)` \( \ne \) `size(bcoef,2)` + !! * 802 = `size(z)` \( \ne \) `size(bcoef,3)` + !! * 803 = `size(q)` \( \ne \) `size(bcoef,4)` + + logical :: status_ok + real(wp),dimension(:),allocatable :: temp !! work array of dimension `nx*ny*nz*nq` + real(wp),dimension(:),allocatable :: work !! work array of dimension `max(2*kx*(nx+1), + !! 2*ky*(ny+1),2*kz*(nz+1),2*kq*(nq+1))` + + ! check validity of input + + call check_inputs( iknot,& + iflag,& + nx=nx,ny=ny,nz=nz,nq=nq,& + kx=kx,ky=ky,kz=kz,kq=kq,& + x=x,y=y,z=z,q=q,& + tx=tx,ty=ty,tz=tz,tq=tq,& + f4=fcn,& + bcoef4=bcoef,& + status_ok=status_ok) + + if (status_ok) then + + ! choose knots + if (iknot == 0_ip) then + call dbknot(x,nx,kx,tx) + call dbknot(y,ny,ky,ty) + call dbknot(z,nz,kz,tz) + call dbknot(q,nq,kq,tq) + end if + + allocate(temp(nx*ny*nz*nq)) + allocate(work(max(2_ip*kx*(nx+1_ip),2_ip*ky*(ny+1_ip),2_ip*kz*(nz+1_ip),2_ip*kq*(nq+1_ip)))) + + ! construct b-spline coefficients + call dbtpcf(x,nx,fcn, nx,ny*nz*nq,tx,kx,temp, work,iflag) + if (iflag==0_ip) call dbtpcf(y,ny,temp, ny,nx*nz*nq,ty,ky,bcoef,work,iflag) + if (iflag==0_ip) call dbtpcf(z,nz,bcoef,nz,nx*ny*nq,tz,kz,temp, work,iflag) + if (iflag==0_ip) call dbtpcf(q,nq,temp, nq,nx*ny*nz,tq,kq,bcoef,work,iflag) + + deallocate(temp) + deallocate(work) + + end if + + end subroutine db4ink +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluates the tensor product piecewise polynomial +! interpolant constructed by the routine [[db4ink]] or one of its +! derivatives at the point (`xval`,`yval`,`zval`,`qval`). +! +! To evaluate the +! interpolant itself, set `idx=idy=idz=idq=0`, to evaluate the first +! partial with respect to `x`, set `idx=1,idy=idz=idq=0`, and so on. +! +! See [[db3val]] header for more information. +! +!### History +! * Jacob Williams, 2/24/2015 : Created this routine. + + pure subroutine db4val(xval,yval,zval,qval,& + idx,idy,idz,idq,& + tx,ty,tz,tq,& + nx,ny,nz,nq,& + kx,ky,kz,kq,& + bcoef,f,iflag,& + inbvx,inbvy,inbvz,inbvq,& + iloy,iloz,iloq,w3,w2,w1,w0,extrap) + + implicit none + + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idy !! \(y\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idz !! \(z\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idq !! \(q\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: nx !! the number of interpolation points in \(x\). + !! (same as in last call to [[db4ink]]) + integer(ip),intent(in) :: ny !! the number of interpolation points in \(y\). + !! (same as in last call to [[db4ink]]) + integer(ip),intent(in) :: nz !! the number of interpolation points in \(z\). + !! (same as in last call to [[db4ink]]) + integer(ip),intent(in) :: nq !! the number of interpolation points in \(q\). + !! (same as in last call to [[db4ink]]) + integer(ip),intent(in) :: kx !! order of polynomial pieces in \(x\). + !! (same as in last call to [[db4ink]]) + integer(ip),intent(in) :: ky !! order of polynomial pieces in \(y\). + !! (same as in last call to [[db4ink]]) + integer(ip),intent(in) :: kz !! order of polynomial pieces in \(z\). + !! (same as in last call to [[db4ink]]) + integer(ip),intent(in) :: kq !! order of polynomial pieces in \(q\). + !! (same as in last call to [[db4ink]]) + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + real(wp),intent(in) :: yval !! \(y\) coordinate of evaluation point. + real(wp),intent(in) :: zval !! \(z\) coordinate of evaluation point. + real(wp),intent(in) :: qval !! \(q\) coordinate of evaluation point. + real(wp),dimension(nx+kx),intent(in) :: tx !! sequence of knots defining the piecewise polynomial + !! in the \(x\) direction. (same as in last call to + !! [[db4ink]]) + real(wp),dimension(ny+ky),intent(in) :: ty !! sequence of knots defining the piecewise polynomial + !! in the \(y\) direction. (same as in last call to + !! [[db4ink]]) + real(wp),dimension(nz+kz),intent(in) :: tz !! sequence of knots defining the piecewise polynomial + !! in the \(z\) direction. (same as in last call to + !! [[db4ink]]) + real(wp),dimension(nq+kq),intent(in) :: tq !! sequence of knots defining the piecewise polynomial + !! in the \(q\) direction. (same as in last call to + !! [[db4ink]]) + real(wp),dimension(nx,ny,nz,nq),intent(in) :: bcoef !! the b-spline coefficients computed by [[db4ink]]. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * \( = 0 \) : no errors + !! * \( \ne 0 \) : error + integer(ip),intent(inout) :: inbvx !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvy !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvz !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvq !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloy !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloz !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloq !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + real(wp),dimension(ky,kz,kq),intent(inout) :: w3 !! work array + real(wp),dimension(kz,kq),intent(inout) :: w2 !! work array + real(wp),dimension(kq),intent(inout) :: w1 !! work array + real(wp),dimension(3_ip*max(kx,ky,kz,kq)),intent(inout) :: w0 !! work array + logical,intent(in),optional :: extrap !! if extrapolation is allowed + !! (if not present, default is False) + + integer(ip) :: lefty, leftz, leftq, & + kcoly, kcolz, kcolq, j, k, q + + f = 0.0_wp + + iflag = check_value(xval,tx,1_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(yval,ty,2_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(zval,tz,3_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(qval,tq,4_ip,extrap); if (iflag/=0_ip) return + + call dintrv(ty,ny+ky,yval,iloy,lefty,iflag,extrap); if (iflag/=0_ip) return + call dintrv(tz,nz+kz,zval,iloz,leftz,iflag,extrap); if (iflag/=0_ip) return + call dintrv(tq,nq+kq,qval,iloq,leftq,iflag,extrap); if (iflag/=0_ip) return + + iflag = 0_ip + + ! x -> y, z, q + kcolq = leftq - kq + do q=1_ip,kq + kcolq = kcolq + 1_ip + kcolz = leftz - kz + do k=1_ip,kz + kcolz = kcolz + 1_ip + kcoly = lefty - ky + do j=1_ip,ky + kcoly = kcoly + 1_ip + call dbvalu(tx,bcoef(:,kcoly,kcolz,kcolq),& + nx,kx,idx,xval,inbvx,w0,iflag,& + w3(j,k,q),extrap) + if (iflag/=0_ip) return + end do + end do + end do + + ! y -> z, q + kcoly = lefty - ky + 1_ip + do q=1_ip,kq + do k=1_ip,kz + call dbvalu(ty(kcoly:),w3(:,k,q),& + ky,ky,idy,yval,inbvy,w0,iflag,& + w2(k,q),extrap) + if (iflag/=0_ip) return + end do + end do + + ! z -> q + kcolz = leftz - kz + 1_ip + do q=1_ip,kq + call dbvalu(tz(kcolz:),w2(:,q),& + kz,kz,idz,zval,inbvz,w0,iflag,& + w1(q),extrap) + if (iflag/=0_ip) return + end do + + ! q + kcolq = leftq - kq + 1_ip + call dbvalu(tq(kcolq:),w1,kq,kq,idq,qval,inbvq,w0,iflag,f,extrap) + + end subroutine db4val +!***************************************************************************************** + +!***************************************************************************************** +!> +! Determines the parameters of a function that interpolates +! the five-dimensional gridded data: +! +! $$ [x(i),y(j),z(k),q(l),r(m),\mathrm{fcn}(i,j,k,l,m)] $$ +! +! for: +! +! $$ i=1,..,n_x ~\mathrm{and}~ j=1,..,n_y, ~\mathrm{and}~ k=1,..,n_z, +! ~\mathrm{and}~ l=1,..,n_q, ~\mathrm{and}~ m=1,..,n_r $$ +! +! The interpolating function and its derivatives may subsequently be evaluated +! by the function [[db5val]]. +! +! See [[db3ink]] header for more details. +! +!### History +! * Jacob Williams, 2/24/2015 : Created this routine. + + pure subroutine db5ink(x,nx,y,ny,z,nz,q,nq,r,nr,& + fcn,& + kx,ky,kz,kq,kr,& + iknot,& + tx,ty,tz,tq,tr,& + bcoef,iflag) + + implicit none + + integer(ip),intent(in) :: nx !! number of \(x\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: ny !! number of \(y\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: nz !! number of \(z\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: nq !! number of \(q\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: nr !! number of \(r\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: kx !! the order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ). + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! the order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ). + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! the order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ). + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! the order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ). + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kr !! the order of spline pieces in \(r\) + !! ( \( 2 \le k_r < n_r \) ). + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. must be strictly increasing. + real(wp),dimension(:),intent(in) :: r !! `(nr)` array of \(r\) abcissae. must be strictly increasing. + real(wp),dimension(:,:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq,nr)` matrix of function values to interpolate. + !! `fcn(i,j,k,q,r)` should contain the function value at the + !! point (`x(i)`,`y(j)`,`z(k)`,`q(l)`,`r(m)`) + integer(ip),intent(in) :: iknot !! knot sequence flag: + !! + !! * 0 = knot sequence chosen by [[db5ink]]. + !! * 1 = knot sequence chosen by user. + real(wp),dimension(:),intent(inout) :: tx !! The `(nx+kx)` knots in the \(x\) direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db5ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: ty !! The `(ny+ky)` knots in the \(y\) direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db5ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: tz !! The `(nz+kz)` knots in the \(z\) direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db5ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: tq !! The `(nq+kq)` knots in the \(q\) direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db5ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: tr !! The `(nr+kr)` knots in the \(r\) direction for the spline + !! interpolant. + !! + !! * If `iknot=0` these are chosen by [[db5ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:,:,:,:,:),intent(out) :: bcoef !! `(nx,ny,nz,nq,nr)` matrix of coefficients of the b-spline + !! interpolant. + integer(ip),intent(out) :: iflag !! * 0 = successful execution. + !! * 2 = `iknot` out of range. + !! * 3 = `nx` out of range. + !! * 4 = `kx` out of range. + !! * 5 = `x` not strictly increasing. + !! * 6 = `tx` not non-decreasing. + !! * 7 = `ny` out of range. + !! * 8 = `ky` out of range. + !! * 9 = `y` not strictly increasing. + !! * 10 = `ty` not non-decreasing. + !! * 11 = `nz` out of range. + !! * 12 = `kz` out of range. + !! * 13 = `z` not strictly increasing. + !! * 14 = `tz` not non-decreasing. + !! * 15 = `nq` out of range. + !! * 16 = `kq` out of range. + !! * 17 = `q` not strictly increasing. + !! * 18 = `tq` not non-decreasing. + !! * 19 = `nr` out of range. + !! * 20 = `kr` out of range. + !! * 21 = `r` not strictly increasing. + !! * 22 = `tr` not non-decreasing. + !! * 700 = `size(x)` \( \ne \) `size(fcn,1)` + !! * 701 = `size(y)` \( \ne \) `size(fcn,2)` + !! * 702 = `size(z)` \( \ne \) `size(fcn,3)` + !! * 703 = `size(q)` \( \ne \) `size(fcn,4)` + !! * 704 = `size(r)` \( \ne \) `size(fcn,5)` + !! * 706 = `size(x)` \( \ne \) `nx` + !! * 707 = `size(y)` \( \ne \) `ny` + !! * 708 = `size(z)` \( \ne \) `nz` + !! * 709 = `size(q)` \( \ne \) `nq` + !! * 710 = `size(r)` \( \ne \) `nr` + !! * 712 = `size(tx)` \( \ne \) `nx+kx` + !! * 713 = `size(ty)` \( \ne \) `ny+ky` + !! * 714 = `size(tz)` \( \ne \) `nz+kz` + !! * 715 = `size(tq)` \( \ne \) `nq+kq` + !! * 716 = `size(tr)` \( \ne \) `nr+kr` + !! * 800 = `size(x)` \( \ne \) `size(bcoef,1)` + !! * 801 = `size(y)` \( \ne \) `size(bcoef,2)` + !! * 802 = `size(z)` \( \ne \) `size(bcoef,3)` + !! * 803 = `size(q)` \( \ne \) `size(bcoef,4)` + !! * 804 = `size(r)` \( \ne \) `size(bcoef,5)` + + logical :: status_ok + real(wp),dimension(:),allocatable :: temp !! work array of length `nx*ny*nz*nq*nr` + real(wp),dimension(:),allocatable :: work !! work array of length `max(2*kx*(nx+1), + !! 2*ky*(ny+1),2*kz*(nz+1),2*kq*(nq+1),2*kr*(nr+1))` + integer(ip) :: i, j, k, l, m, ii !! counter + + ! check validity of input + call check_inputs( iknot,& + iflag,& + nx=nx,ny=ny,nz=nz,nq=nq,nr=nr,& + kx=kx,ky=ky,kz=kz,kq=kq,kr=kr,& + x=x,y=y,z=z,q=q,r=r,& + tx=tx,ty=ty,tz=tz,tq=tq,tr=tr,& + f5=fcn,& + bcoef5=bcoef,& + status_ok=status_ok) + + if (status_ok) then + + ! choose knots + if (iknot == 0_ip) then + call dbknot(x,nx,kx,tx) + call dbknot(y,ny,ky,ty) + call dbknot(z,nz,kz,tz) + call dbknot(q,nq,kq,tq) + call dbknot(r,nr,kr,tr) + end if + + allocate(temp(nx*ny*nz*nq*nr)) + allocate(work(max(2_ip*kx*(nx+1_ip),2_ip*ky*(ny+1_ip),2_ip*kz*(nz+1_ip),2_ip*kq*(nq+1_ip),2_ip*kr*(nr+1_ip)))) + + ! copy fcn to work in packed for dbtpcf + !temp(1:nx*ny*nz*nq*nr) = reshape( fcn, [nx*ny*nz*nq*nr] ) + ! replaced with loops to avoid stack + ! overflow for large data set: + ii = 0_ip + do m = 1_ip, nr + do l = 1_ip, nq + do k = 1_ip, nz + do j = 1_ip, ny + do i = 1_ip, nx + ii = ii + 1_ip + temp(ii) = fcn(i,j,k,l,m) + end do + end do + end do + end do + end do + + ! construct b-spline coefficients + call dbtpcf(x,nx,temp, nx,ny*nz*nq*nr,tx,kx,bcoef,work,iflag) + if (iflag==0_ip) call dbtpcf(y,ny,bcoef, ny,nx*nz*nq*nr,ty,ky,temp, work,iflag) + if (iflag==0_ip) call dbtpcf(z,nz,temp, nz,nx*ny*nq*nr,tz,kz,bcoef,work,iflag) + if (iflag==0_ip) call dbtpcf(q,nq,bcoef, nq,nx*ny*nz*nr,tq,kq,temp, work,iflag) + if (iflag==0_ip) call dbtpcf(r,nr,temp, nr,nx*ny*nz*nq,tr,kr,bcoef,work,iflag) + + deallocate(temp) + deallocate(work) + + end if + + end subroutine db5ink +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluates the tensor product piecewise polynomial +! interpolant constructed by the routine [[db5ink]] or one of its +! derivatives at the point (`xval`,`yval`,`zval`,`qval`,`rval`). +! +! To evaluate the +! interpolant itself, set `idx=idy=idz=idq=idr=0`, to evaluate the first +! partial with respect to `x`, set `idx=1,idy=idz=idq=idr=0,` and so on. +! +! See [[db3val]] header for more information. +! +!### History +! * Jacob Williams, 2/24/2015 : Created this routine. + + pure subroutine db5val(xval,yval,zval,qval,rval,& + idx,idy,idz,idq,idr,& + tx,ty,tz,tq,tr,& + nx,ny,nz,nq,nr,& + kx,ky,kz,kq,kr,& + bcoef,f,iflag,& + inbvx,inbvy,inbvz,inbvq,inbvr,& + iloy,iloz,iloq,ilor,& + w4,w3,w2,w1,w0,extrap) + + implicit none + + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idy !! \(y\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idz !! \(z\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idq !! \(q\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idr !! \(r\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: nx !! the number of interpolation points in \(x\). + !! (same as in last call to [[db5ink]]) + integer(ip),intent(in) :: ny !! the number of interpolation points in \(y\). + !! (same as in last call to [[db5ink]]) + integer(ip),intent(in) :: nz !! the number of interpolation points in \(z\). + !! (same as in last call to [[db5ink]]) + integer(ip),intent(in) :: nq !! the number of interpolation points in \(q\). + !! (same as in last call to [[db5ink]]) + integer(ip),intent(in) :: nr !! the number of interpolation points in \(r\). + !! (same as in last call to [[db5ink]]) + integer(ip),intent(in) :: kx !! order of polynomial pieces in \(x\). + !! (same as in last call to [[db5ink]]) + integer(ip),intent(in) :: ky !! order of polynomial pieces in \(y\). + !! (same as in last call to [[db5ink]]) + integer(ip),intent(in) :: kz !! order of polynomial pieces in \(z\). + !! (same as in last call to [[db5ink]]) + integer(ip),intent(in) :: kq !! order of polynomial pieces in \(q\). + !! (same as in last call to [[db5ink]]) + integer(ip),intent(in) :: kr !! order of polynomial pieces in \(r\). + !! (same as in last call to [[db5ink]]) + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + real(wp),intent(in) :: yval !! \(y\) coordinate of evaluation point. + real(wp),intent(in) :: zval !! \(z\) coordinate of evaluation point. + real(wp),intent(in) :: qval !! \(q\) coordinate of evaluation point. + real(wp),intent(in) :: rval !! \(r\) coordinate of evaluation point. + real(wp),dimension(nx+kx),intent(in) :: tx !! sequence of knots defining the piecewise polynomial + !! in the \(x\) direction. + !! (same as in last call to [[db5ink]]) + real(wp),dimension(ny+ky),intent(in) :: ty !! sequence of knots defining the piecewise polynomial + !! in the \(y\) direction. + !! (same as in last call to [[db5ink]]) + real(wp),dimension(nz+kz),intent(in) :: tz !! sequence of knots defining the piecewise polynomial + !! in the \(z\) direction. + !! (same as in last call to [[db5ink]]) + real(wp),dimension(nq+kq),intent(in) :: tq !! sequence of knots defining the piecewise polynomial + !! in the \(q\) direction. + !! (same as in last call to [[db5ink]]) + real(wp),dimension(nr+kr),intent(in) :: tr !! sequence of knots defining the piecewise polynomial + !! in the \(r\) direction. + !! (same as in last call to [[db5ink]]) + real(wp),dimension(nx,ny,nz,nq,nr),intent(in) :: bcoef !! the b-spline coefficients computed by [[db5ink]]. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * \( = 0 \) : no errors + !! * \( \ne 0 \) : error + integer(ip),intent(inout) :: inbvx !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvy !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvz !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvq !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvr !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloy !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloz !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloq !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: ilor !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + real(wp),dimension(ky,kz,kq,kr),intent(inout) :: w4 !! work array + real(wp),dimension(kz,kq,kr),intent(inout) :: w3 !! work array + real(wp),dimension(kq,kr),intent(inout) :: w2 !! work array + real(wp),dimension(kr),intent(inout) :: w1 !! work array + real(wp),dimension(3_ip*max(kx,ky,kz,kq,kr)),intent(inout) :: w0 !! work array + logical,intent(in),optional :: extrap !! if extrapolation is allowed + !! (if not present, default is False) + + integer(ip) :: lefty, leftz, leftq, leftr, & + kcoly, kcolz, kcolq, kcolr, j, k, q, r + + f = 0.0_wp + + iflag = check_value(xval,tx,1_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(yval,ty,2_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(zval,tz,3_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(qval,tq,4_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(rval,tr,5_ip,extrap); if (iflag/=0_ip) return + + call dintrv(ty,ny+ky,yval,iloy,lefty,iflag,extrap); if (iflag/=0_ip) return + call dintrv(tz,nz+kz,zval,iloz,leftz,iflag,extrap); if (iflag/=0_ip) return + call dintrv(tq,nq+kq,qval,iloq,leftq,iflag,extrap); if (iflag/=0_ip) return + call dintrv(tr,nr+kr,rval,ilor,leftr,iflag,extrap); if (iflag/=0_ip) return + + iflag = 0_ip + + ! x -> y, z, q, r + kcolr = leftr - kr + do r=1_ip,kr + kcolr = kcolr + 1_ip + kcolq = leftq - kq + do q=1_ip,kq + kcolq = kcolq + 1_ip + kcolz = leftz - kz + do k=1_ip,kz + kcolz = kcolz + 1_ip + kcoly = lefty - ky + do j=1_ip,ky + kcoly = kcoly + 1_ip + call dbvalu(tx,bcoef(:,kcoly,kcolz,kcolq,kcolr),& + nx,kx,idx,xval,inbvx,w0,iflag,w4(j,k,q,r),& + extrap) + if (iflag/=0_ip) return + end do + end do + end do + end do + + ! y -> z, q, r + kcoly = lefty - ky + 1_ip + do r=1_ip,kr + do q=1_ip,kq + do k=1_ip,kz + call dbvalu(ty(kcoly:),w4(:,k,q,r),ky,ky,idy,yval,inbvy,& + w0,iflag,w3(k,q,r),extrap) + if (iflag/=0_ip) return + end do + end do + end do + + ! z -> q, r + kcolz = leftz - kz + 1_ip + do r=1_ip,kr + do q=1_ip,kq + call dbvalu(tz(kcolz:),w3(:,q,r),kz,kz,idz,zval,inbvz,& + w0,iflag,w2(q,r),extrap) + if (iflag/=0_ip) return + end do + end do + + ! q -> r + kcolq = leftq - kq + 1_ip + do r=1_ip,kr + call dbvalu(tq(kcolq:),w2(:,r),kq,kq,idq,qval,inbvq,& + w0,iflag,w1(r),extrap) + if (iflag/=0_ip) return + end do + + ! r + kcolr = leftr - kr + 1_ip + call dbvalu(tr(kcolr:),w1,kr,kr,idr,rval,inbvr,w0,iflag,f,extrap) + + end subroutine db5val +!***************************************************************************************** + +!***************************************************************************************** +!> +! Determines the parameters of a function that interpolates +! the six-dimensional gridded data: +! +! $$ [x(i),y(j),z(k),q(l),r(m),s(n),\mathrm{fcn}(i,j,k,l,m,n)] $$ +! +! for: +! +! $$ i=1,..,n_x, j=1,..,n_y, k=1,..,n_z, l=1,..,n_q, m=1,..,n_r, n=1,..,n_s $$ +! +! the interpolating function and its derivatives may subsequently be evaluated +! by the function [[db6val]]. +! +! See [[db3ink]] header for more details. +! +!### History +! * Jacob Williams, 2/24/2015 : Created this routine. + + pure subroutine db6ink(x,nx,y,ny,z,nz,q,nq,r,nr,s,ns,& + fcn,& + kx,ky,kz,kq,kr,ks,& + iknot,& + tx,ty,tz,tq,tr,ts,& + bcoef,iflag) + + implicit none + + integer(ip),intent(in) :: nx !! number of \(x\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: ny !! number of \(y\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: nz !! number of \(z\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: nq !! number of \(q\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: nr !! number of \(r\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: ns !! number of \(s\) abcissae ( \( \ge 3 \) ) + integer(ip),intent(in) :: kx !! the order of spline pieces in \(x\) + !! ( \( 2 \le k_x < n_x \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ky !! the order of spline pieces in \(y\) + !! ( \( 2 \le k_y < n_y \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kz !! the order of spline pieces in \(z\) + !! ( \( 2 \le k_z < n_z \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kq !! the order of spline pieces in \(q\) + !! ( \( 2 \le k_q < n_q \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: kr !! the order of spline pieces in \(r\) + !! ( \( 2 \le k_r < n_r \) ) + !! (order = polynomial degree + 1) + integer(ip),intent(in) :: ks !! the order of spline pieces in \(s\) + !! ( \( 2 \le k_s < n_s \) ) + !! (order = polynomial degree + 1) + real(wp),dimension(:),intent(in) :: x !! `(nx)` array of \(x\) abcissae. + !! must be strictly increasing. + real(wp),dimension(:),intent(in) :: y !! `(ny)` array of \(y\) abcissae. + !! must be strictly increasing. + real(wp),dimension(:),intent(in) :: z !! `(nz)` array of \(z\) abcissae. + !! must be strictly increasing. + real(wp),dimension(:),intent(in) :: q !! `(nq)` array of \(q\) abcissae. + !! must be strictly increasing. + real(wp),dimension(:),intent(in) :: r !! `(nr)` array of \(r\) abcissae. + !! must be strictly increasing. + real(wp),dimension(:),intent(in) :: s !! `(ns)` array of \(s\) abcissae. + !! must be strictly increasing. + real(wp),dimension(:,:,:,:,:,:),intent(in) :: fcn !! `(nx,ny,nz,nq,nr,ns)` matrix of function values to + !! interpolate. `fcn(i,j,k,q,r,s)` should contain the + !! function value at the point + !! (`x(i)`,`y(j)`,`z(k)`,`q(l)`,`r(m)`,`s(n)`) + integer(ip),intent(in) :: iknot !! knot sequence flag: + !! + !! * 0 = knot sequence chosen by [[db6ink]]. + !! * 1 = knot sequence chosen by user. + real(wp),dimension(:),intent(inout) :: tx !! The `(nx+kx)` knots in the \(x\) direction for the + !! spline interpolant. + !! + !! * f `iknot=0` these are chosen by [[db6ink]]. + !! * f `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: ty !! The `(ny+ky)` knots in the \(y\) direction for the + !! spline interpolant. + !! + !! * If `iknot=0` these are chosen by [[db6ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: tz !! The `(nz+kz)` knots in the \(z\) direction for the + !! spline interpolant. + !! + !! * If `iknot=0` these are chosen by [[db6ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: tq !! The `(nq+kq)` knots in the \(q\) direction for the + !! spline interpolant. + !! + !! * If `iknot=0` these are chosen by [[db6ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: tr !! The `(nr+kr)` knots in the \(r\) direction for the + !! spline interpolant. + !! + !! * If `iknot=0` these are chosen by [[db6ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:),intent(inout) :: ts !! The `(ns+ks)` knots in the \(s\) direction for the + !! spline interpolant. + !! + !! * If `iknot=0` these are chosen by [[db6ink]]. + !! * If `iknot=1` these are specified by the user. + !! + !! Must be non-decreasing. + real(wp),dimension(:,:,:,:,:,:),intent(out) :: bcoef !! `(nx,ny,nz,nq,nr,ns)` matrix of coefficients of the + !! b-spline interpolant. + integer(ip),intent(out) :: iflag !! * 0 = successful execution. + !! * 2 = `iknot` out of range. + !! * 3 = `nx` out of range. + !! * 4 = `kx` out of range. + !! * 5 = `x` not strictly increasing. + !! * 6 = `tx` not non-decreasing. + !! * 7 = `ny` out of range. + !! * 8 = `ky` out of range. + !! * 9 = `y` not strictly increasing. + !! * 10 = `ty` not non-decreasing. + !! * 11 = `nz` out of range. + !! * 12 = `kz` out of range. + !! * 13 = `z` not strictly increasing. + !! * 14 = `tz` not non-decreasing. + !! * 15 = `nq` out of range. + !! * 16 = `kq` out of range. + !! * 17 = `q` not strictly increasing. + !! * 18 = `tq` not non-decreasing. + !! * 19 = `nr` out of range. + !! * 20 = `kr` out of range. + !! * 21 = `r` not strictly increasing. + !! * 22 = `tr` not non-decreasing. + !! * 23 = `ns` out of range. + !! * 24 = `ks` out of range. + !! * 25 = `s` not strictly increasing. + !! * 26 = `ts` not non-decreasing. + !! * 700 = `size(x) ` \( \ne \) `size(fcn,1)` + !! * 701 = `size(y) ` \( \ne \) `size(fcn,2)` + !! * 702 = `size(z) ` \( \ne \) `size(fcn,3)` + !! * 703 = `size(q) ` \( \ne \) `size(fcn,4)` + !! * 704 = `size(r) ` \( \ne \) `size(fcn,5)` + !! * 705 = `size(s) ` \( \ne \) `size(fcn,6)` + !! * 706 = `size(x) ` \( \ne \) `nx` + !! * 707 = `size(y) ` \( \ne \) `ny` + !! * 708 = `size(z) ` \( \ne \) `nz` + !! * 709 = `size(q) ` \( \ne \) `nq` + !! * 710 = `size(r) ` \( \ne \) `nr` + !! * 711 = `size(s) ` \( \ne \) `ns` + !! * 712 = `size(tx)` \( \ne \) `nx+kx` + !! * 713 = `size(ty)` \( \ne \) `ny+ky` + !! * 714 = `size(tz)` \( \ne \) `nz+kz` + !! * 715 = `size(tq)` \( \ne \) `nq+kq` + !! * 716 = `size(tr)` \( \ne \) `nr+kr` + !! * 717 = `size(ts)` \( \ne \) `ns+ks` + !! * 800 = `size(x) ` \( \ne \) `size(bcoef,1)` + !! * 801 = `size(y) ` \( \ne \) `size(bcoef,2)` + !! * 802 = `size(z) ` \( \ne \) `size(bcoef,3)` + !! * 803 = `size(q) ` \( \ne \) `size(bcoef,4)` + !! * 804 = `size(r) ` \( \ne \) `size(bcoef,5)` + !! * 805 = `size(s) ` \( \ne \) `size(bcoef,6)` + + logical :: status_ok + real(wp),dimension(:),allocatable :: temp !! work array of size `nx*ny*nz*nq*nr*ns` + real(wp),dimension(:),allocatable :: work !! work array of size `max(2*kx*(nx+1), + !! 2*ky*(ny+1),2*kz*(nz+1),2*kq*(nq+1), + !! 2*kr*(nr+1),2*ks*(ns+1))` + + ! check validity of input + call check_inputs( iknot,& + iflag,& + nx=nx,ny=ny,nz=nz,nq=nq,nr=nr,ns=ns,& + kx=kx,ky=ky,kz=kz,kq=kq,kr=kr,ks=ks,& + x=x,y=y,z=z,q=q,r=r,s=s,& + tx=tx,ty=ty,tz=tz,tq=tq,tr=tr,ts=ts,& + f6=fcn,& + bcoef6=bcoef,& + status_ok=status_ok) + + if (status_ok) then + + ! choose knots + if (iknot == 0_ip) then + call dbknot(x,nx,kx,tx) + call dbknot(y,ny,ky,ty) + call dbknot(z,nz,kz,tz) + call dbknot(q,nq,kq,tq) + call dbknot(r,nr,kr,tr) + call dbknot(s,ns,ks,ts) + end if + + allocate(temp(nx*ny*nz*nq*nr*ns)) + allocate(work(max(2_ip*kx*(nx+1_ip),2_ip*ky*(ny+1_ip),& + 2_ip*kz*(nz+1_ip),2_ip*kq*(nq+1_ip),& + 2_ip*kr*(nr+1_ip),2_ip*ks*(ns+1_ip)))) + + ! construct b-spline coefficients + call dbtpcf(x,nx,fcn, nx,ny*nz*nq*nr*ns,tx,kx,temp, work,iflag) + if (iflag==0_ip) call dbtpcf(y,ny,temp, ny,nx*nz*nq*nr*ns,ty,ky,bcoef,work,iflag) + if (iflag==0_ip) call dbtpcf(z,nz,bcoef,nz,nx*ny*nq*nr*ns,tz,kz,temp, work,iflag) + if (iflag==0_ip) call dbtpcf(q,nq,temp, nq,nx*ny*nz*nr*ns,tq,kq,bcoef,work,iflag) + if (iflag==0_ip) call dbtpcf(r,nr,bcoef,nr,nx*ny*nz*nq*ns,tr,kr,temp, work,iflag) + if (iflag==0_ip) call dbtpcf(s,ns,temp, ns,nx*ny*nz*nq*nr,ts,ks,bcoef,work,iflag) + + deallocate(temp) + deallocate(work) + + end if + + end subroutine db6ink +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluates the tensor product piecewise polynomial +! interpolant constructed by the routine [[db6ink]] or one of its +! derivatives at the point (`xval`,`yval`,`zval`,`qval`,`rval`,`sval`). +! +! To evaluate the +! interpolant itself, set `idx=idy=idz=idq=idr=ids=0`, to evaluate the first +! partial with respect to `x`, set `idx=1,idy=idz=idq=idr=ids=0`, and so on. +! +! See [[db3val]] header for more information. +! +!### History +! * Jacob Williams, 2/24/2015 : Created this routine. + + pure subroutine db6val(xval,yval,zval,qval,rval,sval,& + idx,idy,idz,idq,idr,ids,& + tx,ty,tz,tq,tr,ts,& + nx,ny,nz,nq,nr,ns,& + kx,ky,kz,kq,kr,ks,& + bcoef,f,iflag,& + inbvx,inbvy,inbvz,inbvq,inbvr,inbvs,& + iloy,iloz,iloq,ilor,ilos,& + w5,w4,w3,w2,w1,w0,extrap) + + implicit none + + integer(ip),intent(in) :: idx !! \(x\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idy !! \(y\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idz !! \(z\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idq !! \(q\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: idr !! \(r\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: ids !! \(s\) derivative of piecewise polynomial to evaluate. + integer(ip),intent(in) :: nx !! the number of interpolation points in \(x\). + !! (same as in last call to [[db6ink]]) + integer(ip),intent(in) :: ny !! the number of interpolation points in \(y\). + !! (same as in last call to [[db6ink]]) + integer(ip),intent(in) :: nz !! the number of interpolation points in \(z\). + !! (same as in last call to [[db6ink]]) + integer(ip),intent(in) :: nq !! the number of interpolation points in \(q\). + !! (same as in last call to [[db6ink]]) + integer(ip),intent(in) :: nr !! the number of interpolation points in \(r\). + !! (same as in last call to [[db6ink]]) + integer(ip),intent(in) :: ns !! the number of interpolation points in \(s\). + !! (same as in last call to [[db6ink]]) + integer(ip),intent(in) :: kx !! order of polynomial pieces in \(x\). + !! (same as in last call to [[db6ink]]) + integer(ip),intent(in) :: ky !! order of polynomial pieces in \(y\). + !! (same as in last call to [[db6ink]]) + integer(ip),intent(in) :: kz !! order of polynomial pieces in \(z\). + !! (same as in last call to [[db6ink]]) + integer(ip),intent(in) :: kq !! order of polynomial pieces in \(q\). + !! (same as in last call to [[db6ink]]) + integer(ip),intent(in) :: kr !! order of polynomial pieces in \(r\). + !! (same as in last call to [[db6ink]]) + integer(ip),intent(in) :: ks !! order of polynomial pieces in \(s\). + !! (same as in last call to [[db6ink]]) + real(wp),intent(in) :: xval !! \(x\) coordinate of evaluation point. + real(wp),intent(in) :: yval !! \(y\) coordinate of evaluation point. + real(wp),intent(in) :: zval !! \(z\) coordinate of evaluation point. + real(wp),intent(in) :: qval !! \(q\) coordinate of evaluation point. + real(wp),intent(in) :: rval !! \(r\) coordinate of evaluation point. + real(wp),intent(in) :: sval !! \(s\) coordinate of evaluation point. + real(wp),dimension(nx+kx),intent(in) :: tx !! sequence of knots defining the piecewise polynomial + !! in the \(x\) direction. + !! (same as in last call to [[db6ink]]) + real(wp),dimension(ny+ky),intent(in) :: ty !! sequence of knots defining the piecewise polynomial + !! in the \(y\) direction. + !! (same as in last call to [[db6ink]]) + real(wp),dimension(nz+kz),intent(in) :: tz !! sequence of knots defining the piecewise polynomial + !! in the \(z\) direction. + !! (same as in last call to [[db6ink]]) + real(wp),dimension(nq+kq),intent(in) :: tq !! sequence of knots defining the piecewise polynomial + !! in the \(q\) direction. + !! (same as in last call to [[db6ink]]) + real(wp),dimension(nr+kr),intent(in) :: tr !! sequence of knots defining the piecewise polynomial + !! in the \(r\) direction. + !! (same as in last call to [[db6ink]]) + real(wp),dimension(ns+ks),intent(in) :: ts !! sequence of knots defining the piecewise polynomial + !! in the \(s\) direction. + !! (same as in last call to [[db6ink]]) + real(wp),dimension(nx,ny,nz,nq,nr,ns),intent(in) :: bcoef !! the b-spline coefficients computed by [[db6ink]]. + real(wp),intent(out) :: f !! interpolated value + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * \( = 0 \) : no errors + !! * \( \ne 0 \) : error + integer(ip),intent(inout) :: inbvx !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvy !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvz !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvq !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvr !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: inbvs !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloy !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloz !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: iloq !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: ilor !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + integer(ip),intent(inout) :: ilos !! initialization parameter which must be set + !! to 1 the first time this routine is called, + !! and must not be changed by the user. + real(wp),dimension(ky,kz,kq,kr,ks),intent(inout) :: w5 !! work array + real(wp),dimension(kz,kq,kr,ks),intent(inout) :: w4 !! work array + real(wp),dimension(kq,kr,ks),intent(inout) :: w3 !! work array + real(wp),dimension(kr,ks),intent(inout) :: w2 !! work array + real(wp),dimension(ks),intent(inout) :: w1 !! work array + real(wp),dimension(3_ip*max(kx,ky,kz,kq,kr,ks)),intent(inout) :: w0 !! work array + logical,intent(in),optional :: extrap !! if extrapolation is allowed + !! (if not present, default is False) + + integer(ip) :: lefty,leftz,leftq,leftr,lefts,& + kcoly,kcolz,kcolq,kcolr,kcols,& + j,k,q,r,s + + f = 0.0_wp + + iflag = check_value(xval,tx,1_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(yval,ty,2_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(zval,tz,3_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(qval,tq,4_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(rval,tr,5_ip,extrap); if (iflag/=0_ip) return + iflag = check_value(sval,ts,6_ip,extrap); if (iflag/=0_ip) return + + call dintrv(ty,ny+ky,yval,iloy,lefty,iflag,extrap); if (iflag/=0_ip) return + call dintrv(tz,nz+kz,zval,iloz,leftz,iflag,extrap); if (iflag/=0_ip) return + call dintrv(tq,nq+kq,qval,iloq,leftq,iflag,extrap); if (iflag/=0_ip) return + call dintrv(tr,nr+kr,rval,ilor,leftr,iflag,extrap); if (iflag/=0_ip) return + call dintrv(ts,ns+ks,sval,ilos,lefts,iflag,extrap); if (iflag/=0_ip) return + + iflag = 0_ip + + ! x -> y, z, q, r, s + kcols = lefts - ks + do s=1_ip,ks + kcols = kcols + 1_ip + kcolr = leftr - kr + do r=1_ip,kr + kcolr = kcolr + 1_ip + kcolq = leftq - kq + do q=1_ip,kq + kcolq = kcolq + 1_ip + kcolz = leftz - kz + do k=1_ip,kz + kcolz = kcolz + 1_ip + kcoly = lefty - ky + do j=1_ip,ky + kcoly = kcoly + 1_ip + call dbvalu(tx,bcoef(:,kcoly,kcolz,kcolq,kcolr,kcols),& + nx,kx,idx,xval,inbvx,w0,iflag,& + w5(j,k,q,r,s),extrap) + if (iflag/=0_ip) return + end do + end do + end do + end do + end do + + ! y -> z, q, r, s + kcoly = lefty - ky + 1_ip + do s=1_ip,ks + do r=1_ip,kr + do q=1_ip,kq + do k=1_ip,kz + call dbvalu(ty(kcoly:),w5(:,k,q,r,s),& + ky,ky,idy,yval,inbvy,w0,iflag,& + w4(k,q,r,s),extrap) + if (iflag/=0_ip) return + end do + end do + end do + end do + + ! z -> q, r, s + kcolz = leftz - kz + 1_ip + do s=1_ip,ks + do r=1_ip,kr + do q=1_ip,kq + call dbvalu(tz(kcolz:),w4(:,q,r,s),& + kz,kz,idz,zval,inbvz,w0,iflag,& + w3(q,r,s),extrap) + if (iflag/=0_ip) return + end do + end do + end do + + ! q -> r, s + kcolq = leftq - kq + 1_ip + do s=1_ip,ks + do r=1_ip,kr + call dbvalu(tq(kcolq:),w3(:,r,s),& + kq,kq,idq,qval,inbvq,w0,iflag,& + w2(r,s),extrap) + if (iflag/=0_ip) return + end do + end do + + ! r -> s + kcolr = leftr - kr + 1_ip + do s=1_ip,ks + call dbvalu(tr(kcolr:),w2(:,s),& + kr,kr,idr,rval,inbvr,w0,iflag,& + w1(s),extrap) + if (iflag/=0_ip) return + end do + + ! s + kcols = lefts - ks + 1_ip + call dbvalu(ts(kcols:),w1,ks,ks,ids,sval,inbvs,w0,iflag,f,extrap) + + end subroutine db6val +!***************************************************************************************** + +!***************************************************************************************** +!> +! Checks if the value is withing the range of the knot vectors. +! This is called by the various `db*val` routines. + + pure function check_value(x,t,i,extrap) result(iflag) + + implicit none + + integer(ip) :: iflag !! returns 0 if value is OK, otherwise returns `600+i` + real(wp),intent(in) :: x !! the value to check + integer(ip),intent(in) :: i !! 1=x, 2=y, 3=z, 4=q, 5=r, 6=s + real(wp),dimension(:),intent(in) :: t !! the knot vector + logical,intent(in),optional :: extrap !! if extrapolation is allowed + !! (if not present, default is False) + + logical :: allow_extrapolation !! if extrapolation is allowed + + if (present(extrap)) then + allow_extrapolation = extrap + else + allow_extrapolation = .false. + end if + + if (allow_extrapolation) then + ! in this case all values are OK + iflag = 0_ip + else + if (xt(size(t,kind=ip))) then + iflag = 600_ip + i ! value out of bounds (601, 602, etc.) + else + iflag = 0_ip + end if + end if + + end function check_value +!***************************************************************************************** + +!***************************************************************************************** +!> +! Check the validity of the inputs to the `db*ink` routines. +! Prints warning message if there is an error, +! and also sets iflag and status_ok. +! +! Supports up to 6D: `x`,`y`,`z`,`q`,`r`,`s` +! +!### Notes +! +! The code is new, but the logic is based on the original +! logic in the CMLIB routines `db2ink` and `db3ink`. +! +!### History +! * Jacob Williams, 2/24/2015 : Created this routine. + + pure subroutine check_inputs(iknot,& + iflag,& + nx,ny,nz,nq,nr,ns,& + kx,ky,kz,kq,kr,ks,& + x,y,z,q,r,s,& + tx,ty,tz,tq,tr,ts,& + f1,f2,f3,f4,f5,f6,& + bcoef1,bcoef2,bcoef3,bcoef4,bcoef5,bcoef6,& + alt,& + status_ok) + + implicit none + + integer(ip),intent(in) :: iknot !! = 0 if the `INK` routine is computing the knots. + integer(ip),intent(out) :: iflag + integer(ip),intent(in),optional :: nx,ny,nz,nq,nr,ns + integer(ip),intent(in),optional :: kx,ky,kz,kq,kr,ks + real(wp),dimension(:),intent(in),optional :: x,y,z,q,r,s + real(wp),dimension(:),intent(in),optional :: tx,ty,tz,tq,tr,ts + real(wp),dimension(:),intent(in),optional :: f1,bcoef1 + real(wp),dimension(:,:),intent(in),optional :: f2,bcoef2 + real(wp),dimension(:,:,:),intent(in),optional :: f3,bcoef3 + real(wp),dimension(:,:,:,:),intent(in),optional :: f4,bcoef4 + real(wp),dimension(:,:,:,:,:),intent(in),optional :: f5,bcoef5 + real(wp),dimension(:,:,:,:,:,:),intent(in),optional :: f6,bcoef6 + logical,intent(in),optional :: alt !! using the alt routine where 1st or + !! 2nd deriv is fixed at endpoints + !! [default is False] + logical,intent(out) :: status_ok + + logical :: error + integer :: iex !! extra points for the alt case (in `t` and `bcoef`) + !! [currently, only allowed for the 1D case & `k=4`] + + status_ok = .false. + + iex = 0_ip ! default + if (present(alt)) then + if (alt) iex = 2_ip ! for "alt" mode + end if + + if ((iknot < 0_ip) .or. (iknot > 1_ip)) then + + iflag = 2_ip ! iknot is out of range + + else + + call check('x',nx,kx,x,tx,[3_ip, 4_ip, 5_ip, 6_ip,706_ip,712_ip],iflag,error,iex); if (error) return + call check('y',ny,ky,y,ty,[7_ip, 8_ip, 9_ip,10_ip,707_ip,713_ip],iflag,error,iex); if (error) return + call check('z',nz,kz,z,tz,[11_ip,12_ip,13_ip,14_ip,708_ip,714_ip],iflag,error,iex); if (error) return + call check('q',nq,kq,q,tq,[15_ip,16_ip,17_ip,18_ip,709_ip,715_ip],iflag,error,iex); if (error) return + call check('r',nr,kr,r,tr,[19_ip,20_ip,21_ip,22_ip,710_ip,716_ip],iflag,error,iex); if (error) return + call check('s',ns,ks,s,ts,[23_ip,24_ip,25_ip,26_ip,711_ip,717_ip],iflag,error,iex); if (error) return + + if (present(x) .and. present(f1) .and. present(bcoef1)) then + if (size(x,kind=ip)/=size(f1,1_ip,kind=ip)) then; iflag = 700_ip; return; end if + if (size(x,kind=ip)+iex/=size(bcoef1,1_ip,kind=ip)) then; iflag = 800_ip; return; end if + end if + if (present(x) .and. present(y) .and. present(f2) .and. present(bcoef2)) then + if (size(x,kind=ip)/=size(f2,1_ip,kind=ip)) then; iflag = 700_ip; return; end if + if (size(y,kind=ip)/=size(f2,2_ip,kind=ip)) then; iflag = 701_ip; return; end if + if (size(x,kind=ip)+iex/=size(bcoef2,1_ip,kind=ip)) then; iflag = 800_ip; return; end if + if (size(y,kind=ip)+iex/=size(bcoef2,2_ip,kind=ip)) then; iflag = 801_ip; return; end if + end if + if (present(x) .and. present(y) .and. present(z) .and. present(f3) .and. & + present(bcoef3)) then + if (size(x,kind=ip)/=size(f3,1_ip,kind=ip)) then; iflag = 700_ip; return; end if + if (size(y,kind=ip)/=size(f3,2_ip,kind=ip)) then; iflag = 701_ip; return; end if + if (size(z,kind=ip)/=size(f3,3_ip,kind=ip)) then; iflag = 702_ip; return; end if + if (size(x,kind=ip)+iex/=size(bcoef3,1_ip,kind=ip)) then; iflag = 800_ip; return; end if + if (size(y,kind=ip)+iex/=size(bcoef3,2_ip,kind=ip)) then; iflag = 801_ip; return; end if + if (size(z,kind=ip)+iex/=size(bcoef3,3_ip,kind=ip)) then; iflag = 802_ip; return; end if + end if + if (present(x) .and. present(y) .and. present(z) .and. present(q) .and. & + present(f4) .and. present(bcoef4)) then + if (size(x,kind=ip)/=size(f4,1_ip,kind=ip)) then; iflag = 700_ip; return; end if + if (size(y,kind=ip)/=size(f4,2_ip,kind=ip)) then; iflag = 701_ip; return; end if + if (size(z,kind=ip)/=size(f4,3_ip,kind=ip)) then; iflag = 702_ip; return; end if + if (size(q,kind=ip)/=size(f4,4_ip,kind=ip)) then; iflag = 703_ip; return; end if + if (size(x,kind=ip)+iex/=size(bcoef4,1_ip,kind=ip)) then; iflag = 800_ip; return; end if + if (size(y,kind=ip)+iex/=size(bcoef4,2_ip,kind=ip)) then; iflag = 801_ip; return; end if + if (size(z,kind=ip)+iex/=size(bcoef4,3_ip,kind=ip)) then; iflag = 802_ip; return; end if + if (size(q,kind=ip)+iex/=size(bcoef4,4_ip,kind=ip)) then; iflag = 803_ip; return; end if + end if + if (present(x) .and. present(y) .and. present(z) .and. present(q) .and. & + present(r) .and. present(f5) .and. present(bcoef5)) then + if (size(x,kind=ip)/=size(f5,1_ip,kind=ip)) then; iflag = 700_ip; return; end if + if (size(y,kind=ip)/=size(f5,2_ip,kind=ip)) then; iflag = 701_ip; return; end if + if (size(z,kind=ip)/=size(f5,3_ip,kind=ip)) then; iflag = 702_ip; return; end if + if (size(q,kind=ip)/=size(f5,4_ip,kind=ip)) then; iflag = 703_ip; return; end if + if (size(r,kind=ip)/=size(f5,5_ip,kind=ip)) then; iflag = 704_ip; return; end if + if (size(x,kind=ip)+iex/=size(bcoef5,1_ip,kind=ip)) then; iflag = 800_ip; return; end if + if (size(y,kind=ip)+iex/=size(bcoef5,2_ip,kind=ip)) then; iflag = 801_ip; return; end if + if (size(z,kind=ip)+iex/=size(bcoef5,3_ip,kind=ip)) then; iflag = 802_ip; return; end if + if (size(q,kind=ip)+iex/=size(bcoef5,4_ip,kind=ip)) then; iflag = 803_ip; return; end if + if (size(r,kind=ip)+iex/=size(bcoef5,5_ip,kind=ip)) then; iflag = 804_ip; return; end if + end if + if (present(x) .and. present(y) .and. present(z) .and. present(q) .and. & + present(r) .and. present(s) .and. present(f6) .and. present(bcoef6)) then + if (size(x,kind=ip)/=size(f6,1_ip,kind=ip)) then; iflag = 700_ip; return; end if + if (size(y,kind=ip)/=size(f6,2_ip,kind=ip)) then; iflag = 701_ip; return; end if + if (size(z,kind=ip)/=size(f6,3_ip,kind=ip)) then; iflag = 702_ip; return; end if + if (size(q,kind=ip)/=size(f6,4_ip,kind=ip)) then; iflag = 703_ip; return; end if + if (size(r,kind=ip)/=size(f6,5_ip,kind=ip)) then; iflag = 704_ip; return; end if + if (size(s,kind=ip)/=size(f6,6_ip,kind=ip)) then; iflag = 705_ip; return; end if + if (size(x,kind=ip)+iex/=size(bcoef6,1_ip,kind=ip)) then; iflag = 800_ip; return; end if + if (size(y,kind=ip)+iex/=size(bcoef6,2_ip,kind=ip)) then; iflag = 801_ip; return; end if + if (size(z,kind=ip)+iex/=size(bcoef6,3_ip,kind=ip)) then; iflag = 802_ip; return; end if + if (size(q,kind=ip)+iex/=size(bcoef6,4_ip,kind=ip)) then; iflag = 803_ip; return; end if + if (size(r,kind=ip)+iex/=size(bcoef6,5_ip,kind=ip)) then; iflag = 804_ip; return; end if + if (size(s,kind=ip)+iex/=size(bcoef6,6_ip,kind=ip)) then; iflag = 805_ip; return; end if + + end if + + status_ok = .true. + iflag = 0_ip + + end if + + contains + + pure subroutine check(s,n,k,x,t,ierrs,iflag,error,ik) !! check `t`,`x`,`n`,`k` for validity + + implicit none + + character(len=1),intent(in) :: s !! coordinate string: 'x','y','z','q','r','s' + integer(ip),intent(in),optional :: n !! size of `x` + integer(ip),intent(in),optional :: k !! order + real(wp),dimension(:),intent(in),optional :: x !! abcissae vector + real(wp),dimension(:),intent(in),optional :: t !! knot vector `size(n+k)` + integer(ip),dimension(:),intent(in) :: ierrs !! int error codes for `n`,`k`,`x`,`t`, + !! `size(x)`,`size(t)` checks + integer(ip),intent(out) :: iflag !! status return code + logical,intent(out) :: error !! true if there was an error + integer,intent(in) :: ik !! add this value to k + + integer(ip),dimension(2) :: itmp !! temp integer array + + if (present(n) .and. present(k) .and. present(x) .and. present(t)) then + itmp = [ierrs(1_ip),ierrs(5)] + call check_n('n'//s,n,x,itmp,iflag,error); if (error) return + call check_k('k'//s,k+ik,n,ierrs(2),iflag,error); if (error) return + call check_x(s,n,x,ierrs(3),iflag,error); if (error) return + if (iknot /= 0_ip) then + itmp = [ierrs(4),ierrs(6)] + call check_t('t'//s,n,k+ik,t,itmp,iflag,error); if (error) return + end if + end if + + end subroutine check + + pure subroutine check_n(s,n,x,ierr,iflag,error) + + implicit none + + character(len=*),intent(in) :: s + integer(ip),intent(in) :: n + real(wp),dimension(:),intent(in) :: x !! abcissae vector + integer(ip),dimension(2),intent(in) :: ierr !! [n<3 check, size(x)==n check] + integer(ip),intent(out) :: iflag !! status return code + logical,intent(out) :: error + + if (n < 3_ip) then + iflag = ierr(1_ip) + error = .true. + else + if (size(x)/=n) then + iflag = ierr(2) + error = .true. + else + error = .false. + end if + end if + + end subroutine check_n + + pure subroutine check_k(s,k,n,ierr,iflag,error) + + implicit none + + character(len=*),intent(in) :: s + integer(ip),intent(in) :: k + integer(ip),intent(in) :: n + integer(ip),intent(in) :: ierr + integer(ip),intent(out) :: iflag !! status return code + logical,intent(out) :: error + + if ((k < 2_ip) .or. (k >= n)) then + iflag = ierr + error = .true. + else + error = .false. + end if + + end subroutine check_k + + pure subroutine check_x(s,n,x,ierr,iflag,error) + + implicit none + + character(len=*),intent(in) :: s + integer(ip),intent(in) :: n + real(wp),dimension(:),intent(in) :: x + integer(ip),intent(in) :: ierr + integer(ip),intent(out) :: iflag !! status return code + logical,intent(out) :: error + + integer(ip) :: i + + error = .true. + do i=2_ip,n + if (x(i) <= x(i-1_ip)) then + iflag = ierr + return + end if + end do + error = .false. + + end subroutine check_x + + pure subroutine check_t(s,n,k,t,ierr,iflag,error) + + implicit none + + character(len=*),intent(in) :: s + integer(ip),intent(in) :: n + integer(ip),intent(in) :: k + real(wp),dimension(:),intent(in) :: t + integer(ip),dimension(2),intent(in) :: ierr !! [non-decreasing check, size check] + integer(ip),intent(out) :: iflag !! status return code + logical,intent(out) :: error + + integer(ip) :: i + + error = .true. + + if (size(t)/=(n+k)) then + iflag = ierr(2) + return + end if + + if (iex==0_ip) then ! don't do this for "alt" mode since they haven't been computed yet + do i=2_ip,n + k + if (t(i) < t(i-1_ip)) then + iflag = ierr(1_ip) + return + end if + end do + end if + + error = .false. + + end subroutine check_t + + end subroutine check_inputs +!***************************************************************************************** + +!***************************************************************************************** +!> +! dbknot chooses a knot sequence for interpolation of order k at the +! data points x(i), i=1,..,n. the n+k knots are placed in the array +! t. k knots are placed at each endpoint and not-a-knot end +! conditions are used. the remaining knots are placed at data points +! if n is even and between data points if n is odd. the rightmost +! knot is shifted slightly to the right to insure proper interpolation +! at x(n) (see page 350 of the reference). +! +!### History +! * Jacob Williams, 2/24/2015 : Refactored this routine. + + pure subroutine dbknot(x,n,k,t) + + implicit none + + integer(ip),intent(in) :: n !! dimension of `x` + integer(ip),intent(in) :: k + real(wp),dimension(:),intent(in) :: x + real(wp),dimension(:),intent(out) :: t + + integer(ip) :: i, j, ipj, npj, ip1, jstrt + real(wp) :: rnot + + !put k knots at each endpoint + !(shift right endpoints slightly -- see pg 350 of reference) + rnot = x(n) + 0.1_wp*( x(n)-x(n-1_ip) ) + do j=1_ip,k + t(j) = x(1_ip) + npj = n + j + t(npj) = rnot + end do + + !distribute remaining knots + + if (mod(k,2_ip) == 1_ip) then + + !case of odd k -- knots between data points + + i = (k-1_ip)/2_ip - k + ip1 = i + 1_ip + jstrt = k + 1_ip + do j=jstrt,n + ipj = i + j + t(j) = 0.5_wp*( x(ipj) + x(ipj+1_ip) ) + end do + + else + + !case of even k -- knots at data points + + i = (k/2_ip) - k + jstrt = k+1_ip + do j=jstrt,n + ipj = i + j + t(j) = x(ipj) + end do + + end if + + end subroutine dbknot +!***************************************************************************************** + +!***************************************************************************************** +!> +! dbtpcf computes b-spline interpolation coefficients for nf sets +! of data stored in the columns of the array fcn. the b-spline +! coefficients are stored in the rows of bcoef however. +! each interpolation is based on the n abcissa stored in the +! array x, and the n+k knots stored in the array t. the order +! of each interpolation is k. +! +!### History +! * Jacob Williams, 2/24/2015 : Refactored this routine. + + pure subroutine dbtpcf(x,n,fcn,ldf,nf,t,k,bcoef,work,iflag) + + integer(ip),intent(in) :: n !! dimension of `x` + integer(ip),intent(in) :: nf + integer(ip),intent(in) :: ldf + integer(ip),intent(in) :: k + real(wp),dimension(:),intent(in) :: x + real(wp),dimension(ldf,nf),intent(in) :: fcn + real(wp),dimension(:),intent(in) :: t + real(wp),dimension(nf,n),intent(out) :: bcoef + real(wp),dimension(*),intent(out) :: work !! work array of size >= `2*k*(n+1)` + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * 0: no errors + !! * 301: n should be >0 + + integer(ip) :: i, j, m1, m2, iq, iw + + ! check for null input + + if (nf > 0_ip) then + + ! partition work array + m1 = k - 1_ip + m2 = m1 + k + iq = 1_ip + n + iw = iq + m2*n+1_ip + + ! compute b-spline coefficients + + ! first data set + + call dbintk(x,fcn,t,n,k,work,work(iq),work(iw),iflag) + if (iflag == 0_ip) then + do i=1_ip,n + bcoef(1_ip,i) = work(i) + end do + + ! all remaining data sets by back-substitution + + if (nf == 1_ip) return + do j=2_ip,nf + do i=1_ip,n + work(i) = fcn(i,j) + end do + call dbnslv(work(iq),m2,n,m1,m1,work) + do i=1_ip,n + bcoef(j,i) = work(i) + end do + end do + end if + + else + !write(error_unit,'(A)') 'dbtpcf - n should be >0' + iflag = 301_ip + end if + + end subroutine dbtpcf +!***************************************************************************************** + +!***************************************************************************************** +!> +! dbintk produces the b-spline coefficients, bcoef, of the +! b-spline of order k with knots t(i), i=1,...,n+k, which +! takes on the value y(i) at x(i), i=1,...,n. the spline or +! any of its derivatives can be evaluated by calls to [[dbvalu]]. +! +! the i-th equation of the linear system a*bcoef = b for the +! coefficients of the interpolant enforces interpolation at +! x(i), i=1,...,n. hence, b(i) = y(i), for all i, and a is +! a band matrix with 2k-1 bands if a is invertible. the matrix +! a is generated row by row and stored, diagonal by diagonal, +! in the rows of q, with the main diagonal going into row k. +! the banded system is then solved by a call to dbnfac (which +! constructs the triangular factorization for a and stores it +! again in q), followed by a call to dbnslv (which then +! obtains the solution bcoef by substitution). dbnfac does no +! pivoting, since the total positivity of the matrix a makes +! this unnecessary. the linear system to be solved is +! (theoretically) invertible if and only if +! t(i) < x(i) < t(i+k), for all i. +! equality is permitted on the left for i=1 and on the right +! for i=n when k knots are used at x(1) or x(n). otherwise, +! violation of this condition is certain to lead to an error. +! +!### Error conditions +! +! * improper input +! * singular system of equations +! +!### History +! * splint written by carl de boor [5] +! * dbintk author: amos, d. e., (snla) : date written 800901 +! * revision date 820801 +! * 000330 modified array declarations. (jec) +! * Jacob Williams, 5/10/2015 : converted to free-form Fortran. + + pure subroutine dbintk(x,y,t,n,k,bcoef,q,work,iflag) + + implicit none + + integer(ip),intent(in) :: n !! number of data points, n >= k + real(wp),dimension(n),intent(in) :: x !! vector of length n containing data point abscissa + !! in strictly increasing order. + real(wp),dimension(n),intent(in) :: y !! corresponding vector of length n containing data + !! point ordinates. + real(wp),dimension(*),intent(in) :: t !! knot vector of length n+k + !! since t(1),..,t(k) <= x(1) and t(n+1),..,t(n+k) + !! >= x(n), this leaves only n-k knots (not + !! necessarily x(i) values) interior to (x(1),x(n)) + integer(ip),intent(in) :: k !! order of the spline, k >= 1 + real(wp),dimension(n),intent(out) :: bcoef !! a vector of length n containing the b-spline coefficients + real(wp),dimension(*),intent(out) :: q !! a work vector of length (2*k-1)*n, containing + !! the triangular factorization of the coefficient + !! matrix of the linear system being solved. the + !! coefficients for the interpolant of an + !! additional data set (x(i),yy(i)), i=1,...,n + !! with the same abscissa can be obtained by loading + !! yy into bcoef and then executing + !! call dbnslv(q,2k-1,n,k-1,k-1,bcoef) + real(wp),dimension(*),intent(out) :: work !! work vector of length 2*k + integer(ip),intent(out) :: iflag !! * 0: no errors. + !! * 100: k does not satisfy k>=1. + !! * 101: n does not satisfy n>=k. + !! * 102: x(i) does not satisfy x(i)=1' + iflag = 100_ip + return + end if + + if (n=k' + iflag = 101_ip + return + end if + + jj = n - 1_ip + if (jj/=0_ip) then + do i=1_ip,jj + if (x(i)>=x(i+1_ip)) then + !write(error_unit,'(A)') 'dbintk - x(i) does not satisfy x(i)=ilp1mx) exit + end do + if (.not. found) then + left = left - 1_ip + if (xi>t(left+1_ip)) then + !write(error_unit,'(A)') 'dbintk - some abscissa was not in the support of the'//& + ! ' corresponding basis function and the system is singular' + iflag = 103_ip + return + end if + end if + ! the i-th equation enforces interpolation at xi, hence + ! a(i,j) = b(j,k,t)(xi), all j. only the k entries with j = + ! left-k+1,...,left actually might be nonzero. these k numbers + ! are returned, in bcoef (used for temp.storage here), by the + ! following + call dbspvn(t, k, k, 1_ip, xi, left, bcoef, work, iwork, iflag) + if (iflag/=0_ip) return + + ! we therefore want bcoef(j) = b(left-k+j)(xi) to go into + ! a(i,left-k+j), i.e., into q(i-(left+j)+2*k,(left+j)-k) since + ! a(i+j,j) is to go into q(i+k,j), all i,j, if we consider q + ! as a two-dim. array , with 2*k-1 rows (see comments in + ! dbnfac). in the present program, we treat q as an equivalent + ! one-dimensional array (because of fortran restrictions on + ! dimension statements) . we therefore want bcoef(j) to go into + ! entry + ! i -(left+j) + 2*k + ((left+j) - k-1)*(2*k-1) + ! = i-left+1 + (left -k)*(2*k-1) + (2*k-2)*j + ! of q. + jj = i - left + 1_ip + (left-k)*(k+km1) + do j=1_ip,k + jj = jj + kpkm2 + q(jj) = bcoef(j) + end do + + end do + + ! obtain factorization of a, stored again in q. + call dbnfac(q, k+km1, n, km1, km1, iflag) + + if (iflag==1) then !success + ! solve a*bcoef = y by backsubstitution + do i=1_ip,n + bcoef(i) = y(i) + end do + call dbnslv(q, k+km1, n, km1, km1, bcoef) + iflag = 0_ip + else !failure + !write(error_unit,'(A)') 'dbintk - the system of solver detects a singular system'//& + ! ' although the theoretical conditions for a solution were satisfied' + iflag = 104_ip + end if + + end subroutine dbintk +!***************************************************************************************** + +!***************************************************************************************** +!> +! Returns in w the LU-factorization (without pivoting) of the banded +! matrix a of order nrow with (nbandl + 1 + nbandu) bands or diagonals +! in the work array w . +! +! gauss elimination without pivoting is used. the routine is +! intended for use with matrices a which do not require row inter- +! changes during factorization, especially for the totally +! positive matrices which occur in spline calculations. +! the routine should not be used for an arbitrary banded matrix. +! +!### Work array +! +! **Input** +! +! w array of size (nroww,nrow) contains the interesting +! part of a banded matrix a , with the diagonals or bands of a +! stored in the rows of w , while columns of a correspond to +! columns of w . this is the storage mode used in linpack and +! results in efficient innermost loops. +! explicitly, a has nbandl bands below the diagonal +! + 1 (main) diagonal +! + nbandu bands above the diagonal +! and thus, with middle = nbandu + 1, +! a(i+j,j) is in w(i+middle,j) for i=-nbandu,...,nbandl +! j=1,...,nrow . +! for example, the interesting entries of a (1,2)-banded matrix +! of order 9 would appear in the first 1+1+2 = 4 rows of w +! as follows. +! 13 24 35 46 57 68 79 +! 12 23 34 45 56 67 78 89 +! 11 22 33 44 55 66 77 88 99 +! 21 32 43 54 65 76 87 98 +! +! all other entries of w not identified in this way with an en- +! try of a are never referenced . +! +! **Output** +! +! * if iflag = 1, then +! w contains the lu-factorization of a into a unit lower triangu- +! lar matrix l and an upper triangular matrix u (both banded) +! and stored in customary fashion over the corresponding entries +! of a . this makes it possible to solve any particular linear +! system a*x = b for x by a +! call dbnslv ( w, nroww, nrow, nbandl, nbandu, b ) +! with the solution x contained in b on return . +! * if iflag = 2, then +! one of nrow-1, nbandl,nbandu failed to be nonnegative, or else +! one of the potential pivots was found to be zero indicating +! that a does not have an lu-factorization. this implies that +! a is singular in case it is totally positive . +! +!### History +! * banfac written by carl de boor [5] +! * dbnfac from CMLIB [1] +! * Jacob Williams, 5/10/2015 : converted to free-form Fortran. + + pure subroutine dbnfac(w,nroww,nrow,nbandl,nbandu,iflag) + + integer(ip),intent(in) :: nroww !! row dimension of the work array w. must be >= nbandl + 1 + nbandu. + integer(ip),intent(in) :: nrow !! matrix order + integer(ip),intent(in) :: nbandl !! number of bands of a below the main diagonal + integer(ip),intent(in) :: nbandu !! number of bands of a above the main diagonal + integer(ip),intent(out) :: iflag !! indicating success(=1) or failure (=2) + real(wp),dimension(nroww,nrow),intent(inout) :: w !! work array. See header for details. + + integer(ip) :: i, ipk, j, jmax, k, kmax, middle, midmk, nrowm1 + real(wp) :: factor, pivot + + iflag = 1_ip + middle = nbandu + 1_ip ! w(middle,.) contains the main diagonal of a. + nrowm1 = nrow - 1_ip + + if (nrowm1 < 0_ip) then + iflag = 2_ip + return + else if (nrowm1 == 0_ip) then + if (w(middle,nrow)==0.0_wp) iflag = 2_ip + return + end if + + if (nbandl<=0_ip) then + ! a is upper triangular. check that diagonal is nonzero . + do i=1_ip,nrowm1 + if (w(middle,i)==0.0_wp) then + iflag = 2_ip + return + end if + end do + if (w(middle,nrow)==0.0_wp) iflag = 2_ip + return + end if + + if (nbandu<=0_ip) then + ! a is lower triangular. check that diagonal is nonzero and + ! divide each column by its diagonal. + do i=1_ip,nrowm1 + pivot = w(middle,i) + if (pivot==0.0_wp) then + iflag = 2_ip + return + end if + jmax = min(nbandl,nrow-i) + do j=1_ip,jmax + w(middle+j,i) = w(middle+j,i)/pivot + end do + end do + return + end if + + ! a is not just a triangular matrix. construct lu factorization + do i=1_ip,nrowm1 + ! w(middle,i) is pivot for i-th step . + pivot = w(middle,i) + if (pivot==0.0_wp) then + iflag = 2_ip + return + end if + ! jmax is the number of (nonzero) entries in column i + ! below the diagonal. + jmax = min(nbandl,nrow-i) + ! divide each entry in column i below diagonal by pivot. + do j=1_ip,jmax + w(middle+j,i) = w(middle+j,i)/pivot + end do + ! kmax is the number of (nonzero) entries in row i to + ! the right of the diagonal. + kmax = min(nbandu,nrow-i) + ! subtract a(i,i+k)*(i-th column) from (i+k)-th column + ! (below row i). + do k=1_ip,kmax + ipk = i + k + midmk = middle - k + factor = w(midmk,ipk) + do j=1_ip,jmax + w(midmk+j,ipk) = w(midmk+j,ipk) - w(middle+j,i)*factor + end do + end do + end do + + ! check the last diagonal entry. + if (w(middle,nrow)==0.0_wp) iflag = 2_ip + + end subroutine dbnfac +!***************************************************************************************** + +!***************************************************************************************** +!> +! Companion routine to [[dbnfac]]. it returns the solution x of the +! linear system a*x = b in place of b, given the lu-factorization +! for a in the work array w from dbnfac. +! +! (with \( a = l*u \), as stored in w), the unit lower triangular system +! \( l(u*x) = b \) is solved for \( y = u*x \), and y stored in b. then the +! upper triangular system \(u*x = y \) is solved for x. the calculations +! are so arranged that the innermost loops stay within columns. +! +!### History +! * banslv written by carl de boor [5] +! * dbnslv from SLATEC library [1] +! * Jacob Williams, 5/10/2015 : converted to free-form Fortran. + + pure subroutine dbnslv(w,nroww,nrow,nbandl,nbandu,b) + + integer(ip),intent(in) :: nroww !! describes the lu-factorization of a banded matrix a of order `nrow` + !! as constructed in [[dbnfac]]. + integer(ip),intent(in) :: nrow !! describes the lu-factorization of a banded matrix a of order `nrow` + !! as constructed in [[dbnfac]]. + integer(ip),intent(in) :: nbandl !! describes the lu-factorization of a banded matrix a of order `nrow` + !! as constructed in [[dbnfac]]. + integer(ip),intent(in) :: nbandu !! describes the lu-factorization of a banded matrix a of order `nrow` + !! as constructed in [[dbnfac]]. + real(wp),dimension(nroww,nrow),intent(in) :: w !! describes the lu-factorization of a banded matrix a of + !! order `nrow` as constructed in [[dbnfac]]. + real(wp),dimension(nrow),intent(inout) :: b !! * **in**: right side of the system to be solved + !! * **out**: the solution x, of order nrow + + integer(ip) :: i, j, jmax, middle, nrowm1 + + middle = nbandu + 1_ip + if (nrow/=1_ip) then + + nrowm1 = nrow - 1_ip + if (nbandl/=0_ip) then + + ! forward pass + ! for i=1,2,...,nrow-1, subtract right side(i)*(i-th column of l) + ! from right side (below i-th row). + do i=1_ip,nrowm1 + jmax = min(nbandl,nrow-i) + do j=1_ip,jmax + b(i+j) = b(i+j) - b(i)*w(middle+j,i) + end do + end do + + end if + + ! backward pass + ! for i=nrow,nrow-1,...,1, divide right side(i) by i-th diagonal + ! entry of u, then subtract right side(i)*(i-th column + ! of u) from right side (above i-th row). + if (nbandu<=0_ip) then + ! a is lower triangular. + do i=1_ip,nrow + b(i) = b(i)/w(1_ip,i) + end do + return + end if + + i = nrow + do + b(i) = b(i)/w(middle,i) + jmax = min(nbandu,i-1_ip) + do j=1_ip,jmax + b(i-j) = b(i-j) - b(i)*w(middle-j,i) + end do + i = i - 1_ip + if (i<=1_ip) exit + end do + + end if + + b(1_ip) = b(1_ip)/w(middle,1_ip) + + end subroutine dbnslv +!***************************************************************************************** + +!***************************************************************************************** +!> +! Calculates the value of all (possibly) nonzero basis +! functions at x of order max(jhigh,(j+1)*(index-1)), where t(k) +! <= x <= t(n+1) and j=iwork is set inside the routine on +! the first call when index=1. ileft is such that t(ileft) <= +! x < t(ileft+1). a call to dintrv(t,n+1,x,ilo,ileft,mflag) +! produces the proper ileft. dbspvn calculates using the basic +! algorithm needed in dbspvd. if only basis functions are +! desired, setting jhigh=k and index=1 can be faster than +! calling dbspvd, but extra coding is required for derivatives +! (index=2) and dbspvd is set up for this purpose. +! +! left limiting values are set up as described in dbspvd. +! +!### Error Conditions +! +! * improper input +! +!### History +! * bsplvn written by carl de boor [5] +! * dbspvn author: amos, d. e., (snla) : date written 800901 +! * revision date 820801 +! * 000330 modified array declarations. (jec) +! * Jacob Williams, 2/24/2015 : extensive refactoring of CMLIB routine. + + pure subroutine dbspvn(t,jhigh,k,index,x,ileft,vnikx,work,iwork,iflag) + + implicit none + + real(wp),dimension(*),intent(in) :: t !! knot vector of length `n+k`, where + !! `n` = number of b-spline basis functions + !! `n` = sum of knot multiplicities-`k` + !! dimension `t(ileft+jhigh)` + integer(ip),intent(in) :: jhigh !! order of b-spline, `1 <= jhigh <= k` + integer(ip),intent(in) :: k !! highest possible order + integer(ip),intent(in) :: index !! index = 1 gives basis functions of order `jhigh` + !! = 2 denotes previous entry with `work`, `iwork` + !! values saved for subsequent calls to + !! dbspvn. + real(wp),intent(in) :: x !! argument of basis functions, `t(k) <= x <= t(n+1)` + integer(ip),intent(in) :: ileft !! largest integer such that `t(ileft) <= x < t(ileft+1)` + real(wp),dimension(k),intent(out) :: vnikx !! vector of length `k` for spline values. + real(wp),dimension(*),intent(inout) :: work !! a work vector of length `2*k` + integer(ip),intent(inout) :: iwork !! a work parameter. both `work` and `iwork` contain + !! information necessary to continue for `index = 2`. + !! when `index = 1` exclusively, these are scratch + !! variables and can be used for other purposes. + integer(ip),intent(out) :: iflag !! * 0: no errors + !! * 201: `k` does not satisfy `k>=1` + !! * 202: `jhigh` does not satisfy `1<=jhigh<=k` + !! * 203: `index` is not 1 or 2 + !! * 204: `x` does not satisfy `t(ileft)<=x<=t(ileft+1)` + + integer(ip) :: imjp1, ipj, jp1, jp1ml, l + real(wp) :: vm, vmprev + + ! content of j, deltam, deltap is expected unchanged between calls. + ! work(i) = deltap(i), + ! work(k+i) = deltam(i), i = 1,k + + if (k<1_ip) then + !write(error_unit,'(A)') 'dbspvn - k does not satisfy k>=1' + iflag = 201_ip + return + end if + if (jhigh>k .or. jhigh<1_ip) then + !write(error_unit,'(A)') 'dbspvn - jhigh does not satisfy 1<=jhigh<=k' + iflag = 202_ip + return + end if + if (index<1_ip .or. index>2_ip) then + !write(error_unit,'(A)') 'dbspvn - index is not 1 or 2' + iflag = 203_ip + return + end if + if (xt(ileft+1_ip)) then + !write(error_unit,'(A)') 'dbspvn - x does not satisfy t(ileft)<=x<=t(ileft+1)' + iflag = 204_ip + return + end if + + iflag = 0_ip + + if (index==1_ip) then + iwork = 1_ip + vnikx(1_ip) = 1.0_wp + if (iwork>=jhigh) return + end if + + do + ipj = ileft + iwork + work(iwork) = t(ipj) - x + imjp1 = ileft - iwork + 1_ip + work(k+iwork) = x - t(imjp1) + vmprev = 0.0_wp + jp1 = iwork + 1_ip + do l=1_ip,iwork + jp1ml = jp1 - l + vm = vnikx(l)/(work(l)+work(k+jp1ml)) + vnikx(l) = vm*work(l) + vmprev + vmprev = vm*work(k+jp1ml) + end do + vnikx(jp1) = vmprev + iwork = jp1 + if (iwork>=jhigh) exit + end do + + end subroutine dbspvn +!***************************************************************************************** + +!***************************************************************************************** +!> +! Evaluates the b-representation (`t`,`a`,`n`,`k`) of a b-spline +! at `x` for the function value on `ideriv=0` or any of its +! derivatives on `ideriv=1,2,...,k-1`. right limiting values +! (right derivatives) are returned except at the right end +! point `x=t(n+1)` where left limiting values are computed. the +! spline is defined on `t(k)` \( \le \) `x` \( \le \) `t(n+1)`. +! dbvalu returns a fatal error message when `x` is outside of this +! interval. +! +! To compute left derivatives or left limiting values at a +! knot `t(i)`, replace `n` by `i-1` and set `x=t(i), i=k+1,n+1`. +! +!### Error Conditions +! +! * improper input +! +!### History +! * bvalue written by carl de boor [5] +! * dbvalu author: amos, d. e., (snla) : date written 800901 +! * revision date 820801 +! * 000330 modified array declarations. (jec) +! * Jacob Williams, 2/24/2015 : extensive refactoring of CMLIB routine. + + pure subroutine dbvalu(t,a,n,k,ideriv,x,inbv,work,iflag,val,extrap) + + implicit none + + real(wp),intent(out) :: val !! the interpolated value + integer(ip),intent(in) :: n !! number of b-spline coefficients. + !! (sum of knot multiplicities-`k`) + real(wp),dimension(:),intent(in) :: t !! knot vector of length `n+k` + real(wp),dimension(n),intent(in) :: a !! b-spline coefficient vector of length `n` + integer(ip),intent(in) :: k !! order of the b-spline, `k >= 1` + integer(ip),intent(in) :: ideriv !! order of the derivative, `0 <= ideriv <= k-1`. + !! `ideriv = 0` returns the b-spline value + real(wp),intent(in) :: x !! argument, `t(k) <= x <= t(n+1)` + integer(ip),intent(inout) :: inbv !! an initialization parameter which must be set + !! to 1 the first time [[dbvalu]] is called. + !! `inbv` contains information for efficient processing + !! after the initial call and `inbv` must not + !! be changed by the user. distinct splines require + !! distinct `inbv` parameters. + real(wp),dimension(:),intent(inout) :: work !! work vector of length at least `3*k` + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * 0: no errors + !! * 401: `k` does not satisfy `k` \( \ge \) 1 + !! * 402: `n` does not satisfy `n` \( \ge \) `k` + !! * 403: `ideriv` does not satisfy 0 \( \le \) `ideriv` \(<\) `k` + !! * 404: `x` is not greater than or equal to `t(k)` + !! * 405: `x` is not less than or equal to `t(n+1)` + !! * 406: a left limiting value cannot be obtained at `t(k)` + logical,intent(in),optional :: extrap !! if extrapolation is allowed + !! (if not present, default is False) + + integer(ip) :: i,iderp1,ihi,ihmkmj,ilo,imk,imkpj,ipj,& + ip1,ip1mj,j,jj,j1,j2,kmider,kmj,km1,kpk,mflag + real(wp) :: fkmj + real(wp) :: xt + logical :: extrapolation_allowed !! if extrapolation is allowed + + val = 0.0_wp + + if (k<1_ip) then + iflag = 401_ip ! dbvalu - k does not satisfy k>=1 + return + end if + + if (n=k + return + end if + + if (ideriv<0_ip .or. ideriv>=k) then + iflag = 403_ip ! dbvalu - ideriv does not satisfy 0<=iderivt(n+1_ip)) then + xt = t(n+1_ip) + else + xt = x + end if + else + xt = x + end if + + kmider = k - ideriv + + ! find *i* in (k,n) such that t(i) <= x < t(i+1) + ! (or, <= t(i+1) if t(i) < t(i+1) = t(n+1)). + + km1 = k - 1_ip + call dintrv(t, n+1, xt, inbv, i, mflag) + if (xtt(i)) then + iflag = 405_ip ! dbvalu - x is not less than or equal to t(n+1) + return + end if + + do + if (i==k) then + iflag = 406_ip ! dbvalu - a left limiting value cannot be obtained at t(k) + return + end if + i = i - 1_ip + if (xt/=t(i)) exit + end do + + end if + + ! difference the coefficients *ideriv* times + ! work(i) = aj(i), work(k+i) = dp(i), work(k+k+i) = dm(i), i=1.k + + imk = i - k + do j=1_ip,k + imkpj = imk + j + work(j) = a(imkpj) + end do + + if (ideriv/=0_ip) then + do j=1_ip,ideriv + kmj = k - j + fkmj = real(kmj,wp) + do jj=1_ip,kmj + ihi = i + jj + ihmkmj = ihi - kmj + work(jj) = (work(jj+1_ip)-work(jj))/(t(ihi)-t(ihmkmj))*fkmj + end do + end do + end if + + ! compute value at *x* in (t(i),(t(i+1)) of ideriv-th derivative, + ! given its relevant b-spline coeff. in aj(1),...,aj(k-ideriv). + + if (ideriv/=km1) then + ip1 = i + 1_ip + kpk = k + k + j1 = k + 1_ip + j2 = kpk + 1_ip + do j=1_ip,kmider + ipj = i + j + work(j1) = t(ipj) - x + ip1mj = ip1 - j + work(j2) = x - t(ip1mj) + j1 = j1 + 1_ip + j2 = j2 + 1_ip + end do + iderp1 = ideriv + 1_ip + do j=iderp1,km1 + kmj = k - j + ilo = kmj + do jj=1_ip,kmj + work(jj) = (work(jj+1_ip)*work(kpk+ilo)+work(jj)*& + work(k+jj))/(work(kpk+ilo)+work(k+jj)) + ilo = ilo - 1 + end do + end do + end if + + iflag = 0_ip + val = work(1_ip) + + end subroutine dbvalu +!***************************************************************************************** + +!***************************************************************************************** +!> +! Computes the largest integer `ileft` in 1 \( \le \) `ileft` \( \le \) `lxt` +! such that `xt(ileft)` \( \le \) `x` where `xt(*)` is a subdivision of +! the `x` interval. +! precisely, +! +!```fortran +! if x < xt(1) then ileft=1, mflag=-1 +! if xt(i) <= x < xt(i+1) then ileft=i, mflag=0 +! if xt(lxt) <= x then ileft=lxt, mflag=-2 +!``` +! +! that is, when multiplicities are present in the break point +! to the left of `x`, the largest index is taken for `ileft`. +! +!### History +! * interv written by carl de boor [5] +! * dintrv author: amos, d. e., (snla) : date written 800901 +! * revision date 820801 +! * Jacob Williams, 2/24/2015 : updated to free-form Fortran. +! * Jacob Williams, 2/17/2016 : additional refactoring (eliminated GOTOs). +! * Jacob Williams, 3/4/2017 : added extrapolation option. + + pure subroutine dintrv(xt,lxt,xx,ilo,ileft,mflag,extrap) + + implicit none + + integer(ip),intent(in) :: lxt !! length of the `xt` vector + real(wp),dimension(:),intent(in) :: xt !! a knot or break point vector of length `lxt` + real(wp),intent(in) :: xx !! argument + integer(ip),intent(inout) :: ilo !! an initialization parameter which must be set + !! to 1 the first time the spline array `xt` is + !! processed by dintrv. `ilo` contains information for + !! efficient processing after the initial call and `ilo` + !! must not be changed by the user. distinct splines + !! require distinct `ilo` parameters. + integer(ip),intent(out) :: ileft !! largest integer satisfying `xt(ileft)` \( \le \) `x` + integer(ip),intent(out) :: mflag !! signals when `x` lies out of bounds + logical,intent(in),optional :: extrap !! if extrapolation is allowed + !! (if not present, default is False) + + integer(ip) :: ihi, istep, middle + real(wp) :: x + + x = get_temp_x_for_extrap(xx,xt(1_ip),xt(lxt),extrap) + + ihi = ilo + 1_ip + if ( ihi>=lxt ) then + if ( x>=xt(lxt) ) then + mflag = -2_ip + ileft = lxt + return + end if + if ( lxt<=1 ) then + mflag = -1_ip + ileft = 1_ip + return + end if + ilo = lxt - 1_ip + ihi = lxt + end if + + if ( x>=xt(ihi) ) then + + ! now x >= xt(ilo). find upper bound + istep = 1_ip + do + ilo = ihi + ihi = ilo + istep + if ( ihi>=lxt ) then + if ( x>=xt(lxt) ) then + mflag = -2_ip + ileft = lxt + return + end if + ihi = lxt + else if ( x>=xt(ihi) ) then + istep = istep*2_ip + cycle + end if + exit + end do + + else + + if ( x>=xt(ilo) ) then + mflag = 0_ip + ileft = ilo + return + end if + ! now x <= xt(ihi). find lower bound + istep = 1_ip + do + ihi = ilo + ilo = ihi - istep + if ( ilo<=1_ip ) then + ilo = 1_ip + if ( x +! DBINT4 computes the B representation (`t`,`bcoef`,`n`,`k`) of a +! cubic spline (`k=4`) which interpolates data (`x(i)`,`y(i)`),`i=1,ndata`. +! +! Parameters `ibcl`, `ibcr`, `fbcl`, `fbcr` allow the specification of the spline +! first or second derivative at both `x(1)` and `x(ndata)`. When this data is not specified +! by the problem, it is common practice to use a natural spline by setting second +! derivatives at `x(1)` and `x(ndata)` to zero (`ibcl=ibcr=2`,`fbcl=fbcr=0.0`). +! +! The spline is defined on `t(4) <= x <= t(n+1)` with (ordered) interior knots at +! `x(i)` values where n=ndata+2. The knots `t(1)`,`t(2)`,`t(3)` lie to the left of +! `t(4)=x(1)` and the knots `t(n+2)`, `t(n+3)`, `t(n+4)` lie to the right of `t(n+1)=x(ndata)` +! in increasing order. +! +! * If no extrapolation outside (`x(1)`,`x(ndata)`) is anticipated, the +! knots `t(1)=t(2)=t(3)=t(4)=x(1)` and `t(n+2)=t(n+3)=t(n+4)=t(n+1)=x(ndata)` +! can be specified by `kntopt=1`. +! * `kntopt=2` selects a knot placement for `t(1)`, `t(2)`, `t(3)` to make the +! first 7 knots symmetric about `t(4)=x(1)` and similarly for +! `t(n+2)`, `t(n+3)`, `t(n+4)` about `t(n+1)=x(ndata)`. +! * `kntopt=3` allows the user to make his own selection, in increasing order, +! for `t(1)`, `t(2)`, `t(3)` to the left of `x(1)` and `t(n+2)`, `t(n+3)`, `t(n+4)` to +! the right of x(ndata). +! +! In any case, the interpolation on `t(4) <= x <= t(n+1)` +! by using function [[dbvalu]] is unique for given boundary +! conditions. +! +!### Error conditions +! * improper input +! * singular system of equations +! +!### See also +! * [[dbintk]] +! +!### History +! * Written by D. E. Amos (SNLA), August, 1979. +! * date written 800901 +! * revision date 820801 +! * 000330 Modified array declarations. (JEC) +! * Jacob Williams, 8/30/2018 : refactored to modern Fortran. + + pure subroutine dbint4(x,y,ndata,ibcl,ibcr,fbcl,fbcr,kntopt,tleft,tright,t,bcoef,n,k,w,iflag) + + implicit none + + real(wp),dimension(:),intent(in) :: x !! x vector of abscissae of length `ndata`, distinct + !! and in increasing order + real(wp),dimension(:),intent(in) :: y !! y vector of ordinates of length ndata + integer(ip),intent(in) :: ndata !! number of data points, `ndata >= 2` + integer(ip),intent(in) :: ibcl !! selection parameter for left boundary condition: + !! + !! * `ibcl = 1` constrain the first derivative at `x(1)` to `fbcl` + !! * `ibcl = 2` constrain the second derivative at `x(1)` to `fbcl` + integer(ip),intent(in) :: ibcr !! selection parameter for right boundary condition: + !! + !! * `ibcr = 1` constrain first derivative at `x(ndata)` to `fbcr` + !! * `ibcr = 2` constrain second derivative at `x(ndata)` to `fbcr` + real(wp),intent(in) :: fbcl !! left boundary values governed by `ibcl` + real(wp),intent(in) :: fbcr !! right boundary values governed by `ibcr` + integer(ip),intent(in) :: kntopt !! knot selection parameter: + !! + !! * `kntopt = 1` sets knot multiplicity at `t(4)` and + !! `t(n+1)` to 4 + !! * `kntopt = 2` sets a symmetric placement of knots + !! about `t(4)` and `t(n+1)` + !! * `kntopt = 3` sets `t(i)=tleft(i)` and + !! `t(n+1+i)=tright(i)`,`i=1,3` + real(wp),dimension(3),intent(in) :: tleft !! when `kntopt = 3`: `t(1:3)` in increasing + !! order to be supplied by the user. + real(wp),dimension(3),intent(in) :: tright !! when `kntopt = 3`: `t(n+2:n+4)` in increasing + !! order to be supplied by the user. + real(wp),dimension(:),intent(out) :: t !! knot array of length `n+4` + real(wp),dimension(:),intent(out) :: bcoef !! b spline coefficient array of length `n` + integer(ip),intent(out) :: n !! number of coefficients, `n=ndata+2` + integer(ip),intent(out) :: k !! order of spline, `k=4` + real(wp),dimension(5,ndata+2),intent(inout) :: w !! work array + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * 0: no errors + !! * 2001: `ndata` is less than 2 + !! * 2002: `x` values are not distinct or not ordered + !! * 2003: `ibcl` is not 1 or 2 + !! * 2004: `ibcr` is not 1 or 2 + !! * 2005: `kntopt` is not 1, 2, or 3 + !! * 2006: knot input through `tleft`, `tright` is + !! not ordered properly + !! * 2007: the system of equations is singular + + integer(ip) :: i, ilb, ileft, it, iub, iw, iwp, j, jw, ndm, np, nwrow + real(wp) :: txn, tx1, xl + real(wp),dimension(4,4) :: vnikx + real(wp),dimension(15) :: work !! work array for [[dbspvd]] -- length `(k+1)*(k+2)/2` + + real(wp),parameter :: wdtol = epsilon(1.0_wp) !! d1mach(4) + real(wp),parameter :: tol = sqrt(wdtol) + + if (ndata<2_ip) then + iflag = 2001_ip ! ndata is less than 2 + return + end if + + ndm = ndata - 1_ip + do i=1_ip,ndm + if (x(i)>=x(i+1_ip)) then + iflag = 2002_ip ! x values are not distinct or not ordered + return + end if + end do + + if (ibcl<1_ip .or. ibcl>2_ip) then + iflag = 2003_ip ! ibcl is not 1 or 2 + return + end if + + if (ibcr<1_ip .or. ibcr>2_ip) then + iflag = 2004_ip ! ibcr is not 1 or 2 + return + end if + + if (kntopt<1_ip .or. kntopt>3_ip) then + iflag = 2005_ip ! kntopt is not 1, 2, or 3 + return + end if + + iflag = 0_ip + k = 4_ip + n = ndata + 2_ip + np = n + 1_ip + do i=1_ip,ndata + t(i+3) = x(i) + end do + + select case (kntopt) + case(1_ip) + ! set up knot array with multiplicity 4 at x(1) and x(ndata) + do i=1,3_ip + t(4-i) = x(1) + t(np+i) = x(ndata) + end do + case(2_ip) + !set up knot array with symmetric placement about end points + if (ndata>3) then + tx1 = x(1) + x(1) + txn = x(ndata) + x(ndata) + do i=1,3 + t(4-i) = tx1 - x(i+1) + t(np+i) = txn - x(ndata-i) + end do + else + xl = (x(ndata)-x(1))/3.0_wp + do i=1,3 + t(4-i) = t(5-i) - xl + t(np+i) = t(np+i-1) + xl + end do + end if + case(3_ip) + ! set up knot array less than x(1) and greater than x(ndata) to be + ! supplied by user in tleft & tright when kntopt=3 + t(1:3) = tleft + t(ndata+4:ndata+6) = tright + do i=1,3 + if ((t(4-i)>t(5-i)) .or. (t(np+i)=2) then + do i=2,ndm + ileft = ileft + 1_ip + call dbspvd(t, k, 1_ip, x(i), ileft, 4_ip, vnikx, work, iflag) + if (iflag/=0_ip) return ! error check + do j=1,3 + w(j+1,3+i-j) = vnikx(4-j,1) + end do + bcoef(i+1) = y(i) + end do + end if + + ! set up right interpolation point and right boundary condition for + ! left limits(ileft is associated with t(n)=x(ndata-1)) + it = ibcr + 1_ip + call dbspvd(t, k, it, x(ndata), ileft, 4_ip, vnikx, work, iflag) + if (iflag/=0_ip) return ! error check + jw = 0_ip + if (abs(vnikx(2,1)) +! DBSPVD calculates the value and all derivatives of order +! less than `nderiv` of all basis functions which do not +! (possibly) vanish at `x`. `ileft` is input such that +! `t(ileft) <= x < t(ileft+1)`. A call to [[dintrv]](`t`,`n+1`,`x`, +! `ilo`,`ileft`,`mflag`) will produce the proper `ileft`. The output of +! dbspvd is a matrix `vnikx(i,j)` of dimension at least `(k,nderiv)` +! whose columns contain the `k` nonzero basis functions and +! their `nderiv-1` right derivatives at `x`, `i=1,k, j=1,nderiv`. +! These basis functions have indices `ileft-k+i`, `i=1,k, +! k <= ileft <= n`. The nonzero part of the `i`-th basis +! function lies in `(t(i),t(i+k)), i=1,n)`. +! +! If `x=t(ileft+1)` then `vnikx` contains left limiting values +! (left derivatives) at `t(ileft+1)`. In particular, `ileft = n` +! produces left limiting values at the right end point +! `x=t(n+1)`. To obtain left limiting values at `t(i)`, `i=k+1,n+1`, +! set `x` = next lower distinct knot, call [[dintrv]] to get `ileft`, +! set `x=t(i)`, and then call dbspvd. +! +!### History +! * Written by Carl de Boor and modified by D. E. Amos +! * date written 800901 +! * revision date 820801 +! * 000330 Modified array declarations. (JEC) +! * Jacob Williams, 8/30/2018 : refactored to modern Fortran. +! +!@note `DBSPVD` is the `BSPLVD` routine of the reference. + + pure subroutine dbspvd(t,k,nderiv,x,ileft,ldvnik,vnikx,work,iflag) + + implicit none + + real(wp),dimension(:),intent(in) :: t !! knot vector of length `n+k`, where + !! `n` = number of b-spline basis functions + !! `n` = sum of knot multiplicities-k + integer(ip),intent(in) :: k !! order of the b-spline, `k >= 1` + integer(ip),intent(in) :: nderiv !! number of derivatives = `nderiv-1`, + !! `1 <= nderiv <= k` + real(wp),intent(in) :: x !! argument of basis functions, + !! `t(k) <= x <= t(n+1)` + integer(ip),intent(in) :: ileft !! largest integer such that + !! `t(ileft) <= x < t(ileft+1)` + integer(ip),intent(in) :: ldvnik !! leading dimension of matrix `vnikx` + real(wp),dimension(ldvnik,nderiv),intent(out) :: vnikx !! matrix of dimension at least `(k,nderiv)` + !! containing the nonzero basis functions + !! at `x` and their derivatives columnwise. + real(wp),dimension(*),intent(out) :: work !! a work vector of length `(k+1)*(k+2)/2` + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * 0: no errors + !! * 3001: `k` does not satisfy `k>=1` + !! * 3002: `nderiv` does not satisfy `1<=nderiv<=k` + !! * 3003: `ldvnik` does not satisfy `ldvnik>=k` + + integer(ip) :: i,ideriv,ipkmd,j,jj,jlow,jm,jp1mid,kmd,kp1,l,ldummy,m,mhigh,iwork + real(wp) :: factor, fkmd, v + + ! dimension t(ileft+k), work((k+1)*(k+2)/2) + ! a(i,j) = work(i+j*(j+1)/2), i=1,j+1 j=1,k-1 + ! a(i,k) = work(i+k*(k-1)/2) i=1.k + ! work(1) and work((k+1)*(k+2)/2) are not used. + + if (k<1) then + iflag = 3001_ip ! k does not satisfy k>=1 + return + end if + + if (nderiv<1 .or. nderiv>k) then + iflag = 3002_ip ! nderiv does not satisfy 1<=nderiv<=k + return + end if + + if (ldvnik=k + return + end if + + iflag = 0_ip + + ideriv = nderiv + kp1 = k + 1 + jj = kp1 - ideriv + call dbspvn(t, jj, k, 1_ip, x, ileft, vnikx, work, iwork, iflag) + if (iflag/=0 .or. ideriv==1) return + mhigh = ideriv + do m=2,mhigh + jp1mid = 1 + do j=ideriv,k + vnikx(j,ideriv) = vnikx(jp1mid,1) + jp1mid = jp1mid + 1 + end do + ideriv = ideriv - 1 + jj = kp1 - ideriv + call dbspvn(t, jj, k, 2_ip, x, ileft, vnikx, work, iwork, iflag) + if (iflag/=0) return + end do + + jm = kp1*(kp1+1)/2 + do l = 1,jm + work(l) = 0.0_wp + end do + ! a(i,i) = work(i*(i+3)/2) = 1.0 i = 1,k + l = 2 + j = 0 + do i = 1,k + j = j + l + work(j) = 1.0_wp + l = l + 1 + end do + kmd = k + do m=2,mhigh + kmd = kmd - 1 + fkmd = real(kmd,wp) + i = ileft + j = k + jj = j*(j+1)/2 + jm = jj - j + do ldummy=1,kmd + ipkmd = i + kmd + factor = fkmd/(t(ipkmd)-t(i)) + do l=1,j + work(l+jj) = (work(l+jj)-work(l+jm))*factor + end do + i = i - 1 + j = j - 1 + jj = jm + jm = jm - j + end do + + do i=1,k + v = 0.0_wp + jlow = max(i,m) + jj = jlow*(jlow+1)/2 + do j=jlow,k + v = work(i+jj)*vnikx(j,m) + v + jj = jj + j + 1 + end do + vnikx(i,m) = v + end do + end do + + end subroutine dbspvd +!***************************************************************************************** + +!***************************************************************************************** +!> +! DBSQAD computes the integral on `(x1,x2)` of a `k`-th order +! b-spline using the b-representation `(t,bcoef,n,k)`. orders +! `k` as high as 20 are permitted by applying a 2, 6, or 10 +! point gauss formula on subintervals of `(x1,x2)` which are +! formed by included (distinct) knots. +! +! If orders `k` greater than 20 are needed, use [[dbfqad]] with +! `f(x) = 1`. +! +!### Note +! * The maximum number of significant digits obtainable in +! DBSQAD is the smaller of ~300 and the number of digits +! carried in `real(wp)` arithmetic. +! +!### References +! * D. E. Amos, "Quadrature subroutines for splines and +! B-splines", Report SAND79-1825, Sandia Laboratories, +! December 1979. +! +!### History +! * Author: Amos, D. E., (SNLA) +! * 800901 DATE WRITTEN +! * 890531 Changed all specific intrinsics to generic. (WRB) +! * 890531 REVISION DATE from Version 3.2 +! * 891214 Prologue converted to Version 4.0 format. (BAB) +! * 900315 CALLs to XERROR changed to CALLs to XERMSG. (THJ) +! * 900326 Removed duplicate information from DESCRIPTION section. (WRB) +! * 920501 Reformatted the REFERENCES section. (WRB) +! * Jacob Williams, 9/6/2017 : refactored to modern Fortran. +! Added higher precision coefficients. +! +!@note Extrapolation is not enabled for this routine. + + pure subroutine dbsqad(t,bcoef,n,k,x1,x2,bquad,work,iflag) + + implicit none + + real(wp),dimension(:),intent(in) :: t !! knot array of length `n+k` + real(wp),dimension(:),intent(in) :: bcoef !! b-spline coefficient array of length `n` + integer(ip),intent(in) :: n !! length of coefficient array + integer(ip),intent(in) :: k !! order of b-spline, `1 <= k <= 20` + real(wp),intent(in) :: x1 !! end point of quadrature interval + !! in `t(k) <= x <= t(n+1)` + real(wp),intent(in) :: x2 !! end point of quadrature interval + !! in `t(k) <= x <= t(n+1)` + real(wp),intent(out) :: bquad !! integral of the b-spline over (`x1`,`x2`) + real(wp),dimension(:),intent(inout) :: work !! work vector of length `3*k` + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * 0: no errors + !! * 901: `k` does not satisfy `1<=k<=20` + !! * 902: `n` does not satisfy `n>=k` + !! * 903: `x1` or `x2` or both do + !! not satisfy `t(k)<=x<=t(n+1)` + + integer(ip) :: i,il1,il2,ilo,inbv,jf,left,m,mf,mflag,npk,np1 + real(wp) :: a,aa,b,bb,bma,bpa,c1,gx,q,ta,tb,y1,y2 + real(wp),dimension(5) :: s !! sum + + real(wp),dimension(9),parameter :: gpts = [ & + &0.577350269189625764509148780501957455647601751270126876018602326483977& + &67230293334569371539558574952522520871380513556767665664836499965082627& + &05518373647912161760310773007685273559916067003615583077550051041144223& + &01107628883557418222973945990409015710553455953862673016662179126619796& + &4892168_wp,& + &0.238619186083196908630501721680711935418610630140021350181395164574274& + &93427563984224922442725734913160907222309701068720295545303507720513526& + &28872175189982985139866216812636229030578298770859440976999298617585739& + &46921613621659222233462641640013936777894532787145324672151888999339900& + &0945406150514997832_wp,& + &0.661209386466264513661399595019905347006448564395170070814526705852183& + &49660714310094428640374646145642988837163927514667955734677222538043817& + &23198010093367423918538864300079016299442625145884902455718821970386303& + &22362011735232135702218793618906974301231555871064213101639896769013566& + &1651261150514997832_wp,& + &0.932469514203152027812301554493994609134765737712289824872549616526613& + &50084420019627628873992192598504786367972657283410658797137951163840419& + &21786180750210169211578452038930846310372961174632524612619760497437974& + &07422632089671621172178385230505104744277222209386367655366917903888025& + &2326771150514997832_wp,& + &0.148874338981631210884826001129719984617564859420691695707989253515903& + &61735566852137117762979946369123003116080525533882610289018186437654023& + &16761969968090913050737827720371059070942475859422743249837177174247346& + &21691485290294292900319346665908243383809435507599683357023000500383728& + &0634351_wp,& + &0.433395394129247190799265943165784162200071837656246496502701513143766& + &98907770350122510275795011772122368293504099893794727422475772324920512& + &67741032822086200952319270933462032011328320387691584063411149801129823& + &14148878744320432476641442157678880770848387945248811854979703928792696& + &4254222_wp,& + &0.679409568299024406234327365114873575769294711834809467664817188952558& + &57539507492461507857357048037949983390204739931506083674084257663009076& + &82741718202923543197852846977409718369143712013552962837733153108679126& + &93254495485472934132472721168027426848661712101171203022718105101071880& + &4444161_wp,& + &0.865063366688984510732096688423493048527543014965330452521959731845374& + &75513805556135679072894604577069440463108641176516867830016149345356373& + &92729396890950011571349689893051612072435760480900979725923317923795535& + &73929059587977695683242770223694276591148364371481692378170157259728913& + &9322313_wp,& + &0.973906528517171720077964012084452053428269946692382119231212066696595& + &20323463615962572356495626855625823304251877421121502216860143447777992& + &05409587259942436704413695764881258799146633143510758737119877875210567& + &06745243536871368303386090938831164665358170712568697066873725922944928& + &4383797_wp] + + real(wp),dimension(9),parameter :: gwts = [ & + &1.0_wp,& + &0.467913934572691047389870343989550994811655605769210535311625319963914& + &20162039812703111009258479198230476626878975479710092836255417350295459& + &35635592733866593364825926382559018030281273563502536241704619318259000& + &99756987095900533474080074634376824431808173206369174103416261765346292& + &7888917150514997832_wp,& + &0.360761573048138607569833513837716111661521892746745482289739240237140& + &03783726171832096220198881934794311720914037079858987989027836432107077& + &67872114085818922114502722525757771126000732368828591631602895111800517& + &40813685547074482472486101183259931449817216402425586777526768199930950& + &3106873150514997832_wp,& + &0.171324492379170345040296142172732893526822501484043982398635439798945& + &76054234015464792770542638866975211652206987440430919174716746217597462& + &96492293180314484520671351091683210843717994067668872126692485569940481& + &59429327357024984053433824182363244118374610391205239119044219703570297& + &7497812150514997832_wp,& + &0.295524224714752870173892994651338329421046717026853601354308029755995& + &93821715232927035659579375421672271716440125255838681849078955200582600& + &19363424941869666095627186488841680432313050615358674090830512706638652& + &87483901746874726597515954450775158914556548308329986393605934912382356& + &670244_wp,& + &0.269266719309996355091226921569469352859759938460883795800563276242153& + &43231917927676422663670925276075559581145036869830869292346938114524155& + &64658846634423711656014432259960141729044528030344411297902977067142537& + &53480628460839927657500691168674984281408628886853320804215041950888191& + &6391898_wp,& + &0.219086362515982043995534934228163192458771870522677089880956543635199& + &91065295128124268399317720219278659121687281288763476662690806694756883& + &09211843316656677105269915322077536772652826671027878246851010208832173& + &32006427348325475625066841588534942071161341022729156547776892831330068& + &8702802_wp,& + &0.149451349150580593145776339657697332402556639669427367835477268753238& + &65472663001094594726463473195191400575256104543633823445170674549760147& + &13716011937109528798134828865118770953566439639333773939909201690204649& + &08381561877915752257830034342778536175692764212879241228297015017259084& + &2897331_wp,& + &0.066671344308688137593568809893331792857864834320158145128694881613412& + &06408408710177678550968505887782109005471452041933148750712625440376213& + &93049873169940416344953637064001870112423155043935262424506298327181987& + &18647480566044117862086478449236378557180717569208295026105115288152794& + &421677_wp] + + iflag = 0_ip + bquad = 0.0_wp + + if ( k<1_ip .or. k>20_ip ) then + + iflag = 901_ip ! error return + + else if ( n=t(k) ) then + np1 = n + 1_ip + if ( bb<=t(np1) ) then + if ( aa==bb ) return + npk = n + k + ! selection of 2, 6, or 10 point gauss formula + jf = 0_ip + mf = 1_ip + if ( k>4_ip ) then + jf = 1_ip + mf = 3_ip + if ( k>12_ip ) then + jf = 4_ip + mf = 5_ip + end if + end if + do i = 1_ip , mf + s(i) = 0.0_wp + end do + ilo = 1_ip + inbv = 1_ip + call dintrv(t,npk,aa,ilo,il1,mflag) + call dintrv(t,npk,bb,ilo,il2,mflag) + if ( il2>=np1 ) il2 = n + do left = il1 , il2 + ta = t(left) + tb = t(left+1_ip) + if ( ta/=tb ) then + a = max(aa,ta) + b = min(bb,tb) + bma = 0.5_wp*(b-a) + bpa = 0.5_wp*(b+a) + do m = 1_ip , mf + c1 = bma*gpts(jf+m) + gx = -c1 + bpa + call dbvalu(t,bcoef,n,k,0_ip,gx,inbv,work,iflag,y2) + if (iflag/=0_ip) return + gx = c1 + bpa + call dbvalu(t,bcoef,n,k,0_ip,gx,inbv,work,iflag,y1) + if (iflag/=0_ip) return + s(m) = s(m) + (y1+y2)*bma + end do + end if + end do + q = 0.0_wp + do m = 1_ip , mf + q = q + gwts(jf+m)*s(m) + end do + if ( x1>x2 ) q = -q + bquad = q + return + end if + end if + + iflag = 903_ip ! error return + + end if + + end subroutine dbsqad +!***************************************************************************************** + +!***************************************************************************************** +!> +! dbfqad computes the integral on `(x1,x2)` of a product of a +! function `f` and the `id`-th derivative of a `k`-th order b-spline, +! using the b-representation `(t,bcoef,n,k)`. `(x1,x2)` must be a +! subinterval of `t(k) <= x <= t(n+1)`. an integration routine, +! [[dbsgq8]] (a modification of `gaus8`), integrates the product +! on subintervals of `(x1,x2)` formed by included (distinct) knots +! +!### Reference +! * D. E. Amos, "Quadrature subroutines for splines and +! B-splines", Report SAND79-1825, Sandia Laboratories, +! December 1979. +! +!### History +! * 800901 Amos, D. E., (SNLA) +! * 890531 Changed all specific intrinsics to generic. (WRB) +! * 890531 REVISION DATE from Version 3.2 +! * 891214 Prologue converted to Version 4.0 format. (BAB) +! * 900315 CALLs to XERROR changed to CALLs to XERMSG. (THJ) +! * 900326 Removed duplicate information from DESCRIPTION section. (WRB) +! * 920501 Reformatted the REFERENCES section. (WRB) +! * Jacob Williams, 9/6/2017 : refactored to modern Fortran. Some changes. +! +!@note the maximum number of significant digits obtainable in +! [[dbsqad]] is the smaller of ~300 and the number of digits +! carried in `real(wp)` arithmetic. +! +!@note Extrapolation is not enabled for this routine. + + subroutine dbfqad(f,t,bcoef,n,k,id,x1,x2,tol,quad,iflag,work) + + implicit none + + procedure(b1fqad_func) :: f !! external function of one argument for the + !! integrand `bf(x)=f(x)*dbvalu(t,bcoef,n,k,id,x,inbv,work)` + integer(ip),intent(in) :: n !! length of coefficient array + integer(ip),intent(in) :: k !! order of b-spline, `k >= 1` + real(wp),dimension(n+k),intent(in) :: t !! knot array + real(wp),dimension(n),intent(in) :: bcoef !! coefficient array + integer(ip),intent(in) :: id !! order of the spline derivative, `0 <= id <= k-1` + !! `id=0` gives the spline function + real(wp),intent(in) :: x1 !! left point of quadrature interval in `t(k) <= x <= t(n+1)` + real(wp),intent(in) :: x2 !! right point of quadrature interval in `t(k) <= x <= t(n+1)` + real(wp),intent(in) :: tol !! desired accuracy for the quadrature, suggest + !! `10*dtol < tol <= 0.1` where `dtol` is the maximum + !! of `1.0e-300` and real(wp) unit roundoff for + !! the machine + real(wp),intent(out) :: quad !! integral of `bf(x)` on `(x1,x2)` + real(wp),dimension(:),intent(inout) :: work !! work vector of length `3*k` + integer(ip),intent(out) :: iflag !! status flag: + !! + !! * 0: no errors + !! * 1001: `k` does not satisfy `k>=1` + !! * 1002: `n` does not satisfy `n>=k` + !! * 1003: `d` does not satisfy `0<=id=k ) then + iflag = 1003_ip ! error + else + if ( tol>=min_tol .and. tol<=0.1_wp ) then + aa = min(x1,x2) + bb = max(x1,x2) + if ( aa>=t(k) ) then + np1 = n + 1_ip + if ( bb<=t(np1) ) then + if ( aa==bb ) return + npk = n + k + ilo = 1_ip + call dintrv(t,npk,aa,ilo,il1,mflag) + call dintrv(t,npk,bb,ilo,il2,mflag) + if ( il2>=np1 ) il2 = n + inbv = 1_ip + q = 0.0_wp + do left = il1 , il2 + ta = t(left) + tb = t(left+1_ip) + if ( ta/=tb ) then + a = max(aa,ta) + b = min(bb,tb) + call dbsgq8(f,t,bcoef,n,k,id,a,b,inbv,err,ans,iflag,work) + if ( iflag/=0_ip .and. iflag/=1101_ip ) return + q = q + ans + end if + end do + if ( x1>x2 ) q = -q + quad = q + end if + else + iflag = 1004_ip ! error + end if + else + iflag = 1005_ip ! error + end if + end if + + end subroutine dbfqad +!***************************************************************************************** + +!***************************************************************************************** +!> +! DBSGQ8, a modification of [gaus8](http://netlib.sandia.gov/slatec/src/gaus8.f), +! integrates the product of `fun(x)` by the `id`-th derivative of a spline +! [[dbvalu]] between limits `a` and `b` using an adaptive 8-point Legendre-Gauss +! algorithm. +! +!### See also +! * [[dbfqad]] +! +!### History +! * 800901 Jones, R. E., (SNLA) +! * 890531 Changed all specific intrinsics to generic. (WRB) +! * 890911 Removed unnecessary intrinsics. (WRB) +! * 891214 Prologue converted to Version 4.0 format. (BAB) +! * 900315 CALLs to XERROR changed to CALLs to XERMSG. (THJ) +! * 900326 Removed duplicate information from DESCRIPTION section. (WRB) +! * 900328 Added TYPE section. (WRB) +! * 910408 Updated the AUTHOR section. (WRB) +! * Jacob Williams, 9/6/2017 : refactored to modern Fortran. Some changes. +! Added higher precision coefficients. + + subroutine dbsgq8(fun,xt,bc,n,kk,id,a,b,inbv,err,ans,iflag,work) + + implicit none + + procedure(b1fqad_func) :: fun !! name of external function of one + !! argument which multiplies [[dbvalu]]. + integer(ip),intent(in) :: n !! number of b-coefficients for [[dbvalu]] + integer(ip),intent(in) :: kk !! order of the spline, `kk>=1` + real(wp),dimension(:),intent(in) :: xt !! knot array for [[dbvalu]] + real(wp),dimension(n),intent(in) :: bc !! b-coefficient array for [[dbvalu]] + integer(ip),intent(in) :: id !! Order of the spline derivative, `0<=id<=kk-1` + real(wp),intent(in) :: a !! lower limit of integral + real(wp),intent(in) :: b !! upper limit of integral (may be less than `a`) + integer(ip),intent(inout) :: inbv !! initialization parameter for [[dbvalu]] + real(wp),intent(inout) :: err !! **IN:** is a requested pseudorelative error + !! tolerance. normally pick a value of + !! `abs(err)<1e-3`. `ans` will normally + !! have no more error than `abs(err)` times + !! the integral of the absolute value of + !! `fun(x)*[[dbvalu]]()`. + !! + !! **OUT:** will be an estimate of the absolute + !! error in ans if the input value of `err` + !! was negative. (`err` is unchanged if + !! the input value of `err` was nonnegative.) + !! the estimated error is solely for information + !! to the user and should not be used as a + !! correction to the computed integral. + real(wp),intent(out) :: ans !! computed value of integral + integer(ip),intent(out) :: iflag !! a status code: + !! + !! * 0: `ans` most likely meets requested + !! error tolerance, or `a=b`. + !! * 1101: `a` and `b` are too nearly equal + !! to allow normal integration. + !! `ans` is set to zero. + !! * 1102: `ans` probably does not meet + !! requested error tolerance. + real(wp),dimension(:),intent(inout) :: work !! work vector of length `3*k` for [[dbvalu]] + + integer(ip) :: k,l,lmn,lmx,mxl,nbits,nib,nlmx + real(wp) :: ae,anib,area,c,ce,ee,ef,eps,est,gl,glr,tol,vr,x + integer(ip),dimension(60) :: lr + real(wp),dimension(60) :: aa,hh,vl,gr + + integer(ip),parameter :: i1mach14 = digits(1.0_wp) !! i1mach(14) + real(wp),parameter :: d1mach5 = log10(real(radix(x),wp)) !! d1mach(5) + real(wp),parameter :: ln2 = log(2.0_wp) !! 0.69314718d0 + real(wp),parameter :: sq2 = sqrt(2.0_wp) + integer(ip),parameter :: nlmn = 1 + integer(ip),parameter :: kmx = 5000 + integer(ip),parameter :: kml = 6 + + ! initialize + inbv = 1_ip + iflag = 0_ip + k = i1mach14 + anib = d1mach5*k/0.30102000_wp + nbits = int(anib,ip) + nlmx = min((nbits*5_ip)/8_ip,60_ip) + ans = 0.0_wp + ce = 0.0_wp + + if ( a==b ) then + if ( err<0.0_wp ) err = ce + else + lmx = nlmx + lmn = nlmn + if ( b/=0.0_wp ) then + if ( sign(1.0_wp,b)*a>0.0_wp ) then + c = abs(1.0_wp-a/b) + if ( c<=0.1_wp ) then + if ( c<=0.0_wp ) then + if ( err<0.0_wp ) err = ce + return + else + anib = 0.5_wp - log(c)/ln2 + nib = int(anib,ip) + lmx = min(nlmx,nbits-nib-7_ip) + if ( lmx<1_ip ) then + ! a and b are too nearly equal + ! to allow normal integration + iflag = 1101_ip + if ( err<0.0_wp ) err = ce + return + else + lmn = min(lmn,lmx) + end if + end if + end if + end if + end if + tol = max(abs(err),2.0_wp**(5-nbits))/2.0_wp + if ( err==0.0_wp ) tol = sqrt(epsilon(1.0_wp)) + eps = tol + hh(1_ip) = (b-a)/4.0_wp + aa(1_ip) = a + lr(1_ip) = 1_ip + l = 1_ip + call g8(aa(l)+2.0_wp*hh(l),2.0_wp*hh(l),est,iflag) + if (iflag/=0_ip) return + k = 8_ip + area = abs(est) + ef = 0.5_wp + mxl = 0_ip + end if + + do + ! compute refined estimates, estimate the error, etc. + call g8(aa(l)+hh(l),hh(l),gl,iflag) + if (iflag/=0_ip) return + call g8(aa(l)+3.0_wp*hh(l),hh(l),gr(l),iflag) + if (iflag/=0_ip) return + k = k + 16_ip + area = area + (abs(gl)+abs(gr(l))-abs(est)) + glr = gl + gr(l) + ee = abs(est-glr)*ef + ae = max(eps*area,tol*abs(glr)) + if ( ee>ae ) then + ! consider the left half of this level + if ( k>kmx ) lmx = kml + if ( l>=lmx ) then + mxl = 1_ip + else + l = l + 1_ip + eps = eps*0.5_wp + ef = ef/sq2 + hh(l) = hh(l-1)*0.5_wp + lr(l) = -1_ip + aa(l) = aa(l-1_ip) + est = gl + cycle + end if + end if + ce = ce + (est-glr) + if ( lr(l)<=0_ip ) then + ! proceed to right half at this level + vl(l) = glr + else + ! return one level + vr = glr + do + if ( l<=1_ip ) then + ! exit + ans = vr + if ( (mxl/=0_ip) .and. (abs(ce)>2.0_wp*tol*area) ) then + iflag = 1102_ip + end if + if ( err<0.0_wp ) err = ce + return + else + l = l - 1_ip + eps = eps*2.0_wp + ef = ef*sq2 + if ( lr(l)<=0 ) then + vl(l) = vl(l+1_ip) + vr + exit + else + vr = vl(l+1_ip) + vr + end if + end if + end do + end if + est = gr(l-1_ip) + lr(l) = 1_ip + aa(l) = aa(l) + 4.0_wp*hh(l) + end do + + contains + + subroutine g8(x,h,res,iflag) + + !! 8-point formula. + !! + !!@note Replaced the original double precision abscissa and weight + !! coefficients with the higher precision versions from here: + !! http://pomax.github.io/bezierinfo/legendre-gauss.html + !! So, if `wp` is changed to say, `real128`, more precision + !! can be obtained. These coefficients have about 300 digits. + + implicit none + + real(wp),intent(in) :: x + real(wp),intent(in) :: h + real(wp),intent(out) :: res + integer(ip),intent(out) :: iflag + + real(wp),dimension(8) :: f + real(wp),dimension(8) :: v + + ! abscissa and weight coefficients: + real(wp),parameter :: x1 = & + &0.1834346424956498049394761423601839806667578129129737823171884736992044& + &742215421141160682237111233537452676587642867666089196012523876865683788& + &569995160663568104475551617138501966385810764205532370882654749492812314& + &961247764619363562770645716456613159405134052985058171969174306064445289& + &638150514997832_wp + real(wp),parameter :: x2 = & + &0.5255324099163289858177390491892463490419642431203928577508570992724548& + &207685612725239614001936319820619096829248252608507108793766638779939805& + &395303668253631119018273032402360060717470006127901479587576756241288895& + &336619643528330825624263470540184224603688817537938539658502113876953598& + &879150514997832_wp + real(wp),parameter :: x3 = & + &0.7966664774136267395915539364758304368371717316159648320701702950392173& + &056764730921471519272957259390191974534530973092653656494917010859602772& + &562074621689676153935016290342325645582634205301545856060095727342603557& + &415761265140428851957341933710803722783136113628137267630651413319993338& + &002150514997832_wp + real(wp),parameter :: x4 = & + &0.9602898564975362316835608685694729904282352343014520382716397773724248& + &977434192844394389592633122683104243928172941762102389581552171285479373& + &642204909699700433982618326637346808781263553346927867359663480870597542& + &547603929318533866568132868842613474896289232087639988952409772489387324& + &25615051499783203_wp + real(wp),parameter :: w1 = & + &0.3626837833783619829651504492771956121941460398943305405248230675666867& + &347239066773243660420848285095502587699262967065529258215569895173844995& + &576007862076842778350382862546305771007553373269714714894268328780431822& + &779077846722965535548199601402487767505928976560993309027632737537826127& + &502150514997832_wp + real(wp),parameter :: w2 = & + &0.3137066458778872873379622019866013132603289990027349376902639450749562& + &719421734969616980762339285560494275746410778086162472468322655616056890& + &624276469758994622503118776562559463287222021520431626467794721603822601& + &295276898652509723185157998353156062419751736972560423953923732838789657& + &919150514997832_wp + real(wp),parameter :: w3 = & + &0.2223810344533744705443559944262408844301308700512495647259092892936168& + &145704490408536531423771979278421592661012122181231114375798525722419381& + &826674532090577908613289536840402789398648876004385697202157482063253247& + &195590228631570651319965589733545440605952819880671616779621183704306688& + &233150514997832_wp + real(wp),parameter :: w4 = & + &0.1012285362903762591525313543099621901153940910516849570590036980647401& + &787634707848602827393040450065581543893314132667077154940308923487678731& + &973041136073584690533208824050731976306575729205467961435779467552492328& + &730055025992954089946676810510810729468366466585774650346143712142008566& + &866150514997832_wp + + res = 0.0_wp + + v(1_ip) = x-x1*h + v(2_ip) = x+x1*h + v(3_ip) = x-x2*h + v(4_ip) = x+x2*h + v(5_ip) = x-x3*h + v(6_ip) = x+x3*h + v(7_ip) = x-x4*h + v(8_ip) = x+x4*h + + call dbvalu(xt,bc,n,kk,id,v(1_ip),inbv,work,iflag,f(1_ip)); if (iflag/=0_ip) return + call dbvalu(xt,bc,n,kk,id,v(2_ip),inbv,work,iflag,f(2_ip)); if (iflag/=0_ip) return + call dbvalu(xt,bc,n,kk,id,v(3_ip),inbv,work,iflag,f(3_ip)); if (iflag/=0_ip) return + call dbvalu(xt,bc,n,kk,id,v(4_ip),inbv,work,iflag,f(4_ip)); if (iflag/=0_ip) return + call dbvalu(xt,bc,n,kk,id,v(5_ip),inbv,work,iflag,f(5_ip)); if (iflag/=0_ip) return + call dbvalu(xt,bc,n,kk,id,v(6_ip),inbv,work,iflag,f(6_ip)); if (iflag/=0_ip) return + call dbvalu(xt,bc,n,kk,id,v(7_ip),inbv,work,iflag,f(7_ip)); if (iflag/=0_ip) return + call dbvalu(xt,bc,n,kk,id,v(8_ip),inbv,work,iflag,f(8_ip)); if (iflag/=0_ip) return + + res = h*((w1*(fun(v(1_ip))*f(1_ip) + fun(v(2_ip))*f(2_ip)) + & + w2*(fun(v(3_ip))*f(3_ip) + fun(v(4_ip))*f(4_ip))) + & + (w3*(fun(v(5_ip))*f(5_ip) + fun(v(6_ip))*f(6_ip)) + & + w4*(fun(v(7_ip))*f(7_ip) + fun(v(8_ip))*f(8_ip)))) + + end subroutine g8 + + end subroutine dbsgq8 +!***************************************************************************************** + +!***************************************************************************************** +!> +! Returns the value of `x` to use for computing the interval +! in `t`, depending on if extrapolation is allowed or not. +! +! If extrapolation is allowed and x is < tmin or > tmax, then either +! `tmin` or `tmax - 2.0_wp*spacing(tmax)` is returned. +! Otherwise, `x` is returned. + + pure function get_temp_x_for_extrap(x,tmin,tmax,extrap) result(xt) + + implicit none + + real(wp),intent(in) :: x !! variable value + real(wp),intent(in) :: tmin !! first knot vector element for b-splines + real(wp),intent(in) :: tmax !! last knot vector element for b-splines + real(wp) :: xt !! The value returned (it will either + !! be `tmin`, `x`, or `tmax`) + logical,intent(in),optional :: extrap !! if extrapolation is allowed + !! (if not present, default is False) + + logical :: extrapolation_allowed !! if extrapolation is allowed + + if (present(extrap)) then + extrapolation_allowed = extrap + else + extrapolation_allowed = .false. + end if + + if (extrapolation_allowed) then + if (xtmax) then + ! Put it just inside the upper bound. + ! This is sort of a hack to get + ! extrapolation to work. + xt = tmax - 2.0_wp*spacing(tmax) + else + xt = x + end if + else + xt = x + end if + + end function get_temp_x_for_extrap +!***************************************************************************************** + +!***************************************************************************************** +!> +! Returns a message string associated with the status code. + + pure function get_status_message(iflag) result(msg) + + implicit none + + integer(ip),intent(in) :: iflag !! return code from one of the routines + character(len=:),allocatable :: msg !! status message associated with the flag + + character(len=10) :: istr !! for integer to string conversion + integer(ip) :: istat !! for write statement + + select case (iflag) + + case( 0_ip); msg='Successful execution' + + case( -1_ip); msg='Error in dintrv: x < xt(1_ip)' + case( -2_ip); msg='Error in dintrv: x >= xt(lxt)' + + case( 1_ip); msg='Error in evaluate_*d: class is not initialized' + + case( 2_ip); msg='Error in db*ink: iknot out of range' + case( 3_ip); msg='Error in db*ink: nx out of range' + case( 4_ip); msg='Error in db*ink: kx out of range' + case( 5_ip); msg='Error in db*ink: x not strictly increasing' + case( 6_ip); msg='Error in db*ink: tx not non-decreasing' + case( 7_ip); msg='Error in db*ink: ny out of range' + case( 8_ip); msg='Error in db*ink: ky out of range' + case( 9_ip); msg='Error in db*ink: y not strictly increasing' + case( 10_ip); msg='Error in db*ink: ty not non-decreasing' + case( 11_ip); msg='Error in db*ink: nz out of range' + case( 12_ip); msg='Error in db*ink: kz out of range' + case( 13_ip); msg='Error in db*ink: z not strictly increasing' + case( 14_ip); msg='Error in db*ink: tz not non-decreasing' + case( 15_ip); msg='Error in db*ink: nq out of range' + case( 16_ip); msg='Error in db*ink: kq out of range' + case( 17_ip); msg='Error in db*ink: q not strictly increasing' + case( 18_ip); msg='Error in db*ink: tq not non-decreasing' + case( 19_ip); msg='Error in db*ink: nr out of range' + case( 20_ip); msg='Error in db*ink: kr out of range' + case( 21_ip); msg='Error in db*ink: r not strictly increasing' + case( 22_ip); msg='Error in db*ink: tr not non-decreasing' + case( 23_ip); msg='Error in db*ink: ns out of range' + case( 24_ip); msg='Error in db*ink: ks out of range' + case( 25_ip); msg='Error in db*ink: s not strictly increasing' + case( 26_ip); msg='Error in db*ink: ts not non-decreasing' + case(700_ip); msg='Error in db*ink: size(x) /= size(fcn,1)' + case(701_ip); msg='Error in db*ink: size(y) /= size(fcn,2)' + case(702_ip); msg='Error in db*ink: size(z) /= size(fcn,3)' + case(703_ip); msg='Error in db*ink: size(q) /= size(fcn,4)' + case(704_ip); msg='Error in db*ink: size(r) /= size(fcn,5)' + case(705_ip); msg='Error in db*ink: size(s) /= size(fcn,6)' + case(706_ip); msg='Error in db*ink: size(x) /= nx' + case(707_ip); msg='Error in db*ink: size(y) /= ny' + case(708_ip); msg='Error in db*ink: size(z) /= nz' + case(709_ip); msg='Error in db*ink: size(q) /= nq' + case(710_ip); msg='Error in db*ink: size(r) /= nr' + case(711_ip); msg='Error in db*ink: size(s) /= ns' + case(712_ip); msg='Error in db*ink: size(tx) /= nx+kx' + case(713_ip); msg='Error in db*ink: size(ty) /= ny+ky' + case(714_ip); msg='Error in db*ink: size(tz) /= nz+kz' + case(715_ip); msg='Error in db*ink: size(tq) /= nq+kq' + case(716_ip); msg='Error in db*ink: size(tr) /= nr+kr' + case(717_ip); msg='Error in db*ink: size(ts) /= ns+ks' + case(800_ip); msg='Error in db*ink: size(x) /= size(bcoef,1)' + case(801_ip); msg='Error in db*ink: size(y) /= size(bcoef,2)' + case(802_ip); msg='Error in db*ink: size(z) /= size(bcoef,3)' + case(803_ip); msg='Error in db*ink: size(q) /= size(bcoef,4)' + case(804_ip); msg='Error in db*ink: size(r) /= size(bcoef,5)' + case(805_ip); msg='Error in db*ink: size(s) /= size(bcoef,6)' + + case(806_ip); msg='Error in dbint4: currently, only k=4 can be used' + + case(100_ip); msg='Error in dbintk: k does not satisfy k>=1' + case(101_ip); msg='Error in dbintk: n does not satisfy n>=k' + case(102_ip); msg='Error in dbintk: x(i) does not satisfy x(i) np.int32(0) + + spline.destroy() + assert spline.status_ok() is False diff --git a/examples/bspline/tests/test_procedural_api.py b/examples/bspline/tests/test_procedural_api.py new file mode 100644 index 000000000..9ab4f8e01 --- /dev/null +++ b/examples/bspline/tests/test_procedural_api.py @@ -0,0 +1,274 @@ +"""Procedural B-spline routines checked against SciPy and analytic values.""" + +from __future__ import annotations + +import numpy as np +import pytest + +from examples.bspline.routine_inventory import ORDER_CONSTANTS + +pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] + +CUBIC = np.int32(4) +NOT_A_KNOT = np.int32(0) + + +def _interpolant(bspline_sub, x, fcn): + """Build one cubic interpolant through the procedural entry points.""" + nx = np.int32(x.size) + knots = np.zeros(x.size + int(CUBIC), dtype=np.float64) + bcoef = np.zeros(x.size, dtype=np.float64) + + iflag = bspline_sub.db1ink(x, nx, fcn, CUBIC, NOT_A_KNOT, knots, bcoef) + assert iflag == np.int32(0), bspline_sub.get_status_message(iflag) + return knots, bcoef, nx + + +def _evaluate(bspline_sub, knots, bcoef, nx, point, derivative=0): + work = np.zeros(3 * int(CUBIC), dtype=np.float64) + value, iflag, _inbvx = bspline_sub.db1val( + np.float64(point), + np.int32(derivative), + knots, + nx, + CUBIC, + bcoef, + np.int32(1), + work, + ) + assert iflag == np.int32(0), bspline_sub.get_status_message(iflag) + return value + + +def _multidimensional_inputs(dimension): + """Return a cubic affine interpolant's setup and evaluation arguments.""" + axes = [np.linspace(0.0, 1.0, 5) for _ in range(dimension)] + sizes = [np.int32(axis.size) for axis in axes] + values = np.zeros((5,) * dimension) + for axis, points in enumerate(axes): + shape = [1] * dimension + shape[axis] = points.size + values += points.reshape(shape) + values = np.asfortranarray(values) + knots = [np.zeros(axis.size + int(CUBIC), dtype=np.float64) for axis in axes] + coefficients = np.zeros(values.shape, dtype=np.float64, order="F") + setup_arguments = [] + for axis, size in zip(axes, sizes, strict=True): + setup_arguments.extend((axis, size)) + setup_arguments.extend((values, *(CUBIC,) * dimension, NOT_A_KNOT, *knots, coefficients)) + work_arrays = [ + np.zeros(tuple(int(CUBIC) for _ in range(dimension - index)), dtype=np.float64, order="F") + for index in range(1, dimension) + ] + evaluation_arguments = ( + *(np.float64(0.3),) * dimension, + *(np.int32(0),) * dimension, + *knots, + *sizes, + *(CUBIC,) * dimension, + coefficients, + *(np.int32(1),) * dimension, + *(np.int32(1),) * (dimension - 1), + *work_arrays, + np.zeros(3 * int(CUBIC), dtype=np.float64), + ) + return tuple(setup_arguments), evaluation_arguments + + +def test_spline_order_constants_reach_python(bspline_sub): + for name, expected in ORDER_CONSTANTS.items(): + assert getattr(bspline_sub, name) == np.int32(expected), name + + +def test_generic_interfaces_publish_every_specific_signature(bspline_sub): + """`db1ink` and `db1val` are Fortran generics, so each specific is accepted.""" + assert bspline_sub.db1ink.__doc__.count("db1ink(x:") == 3 + assert bspline_sub.db1val.__doc__.count("db1val(xval:") == 2 + + +def test_db1ink(bspline_sub): + x = np.linspace(0.0, 2.0 * np.pi, 30) + knots = np.zeros(x.size + int(CUBIC), dtype=np.float64) + coefficients = np.zeros(x.size, dtype=np.float64) + + iflag = bspline_sub.db1ink(x, np.int32(x.size), np.sin(x), CUBIC, NOT_A_KNOT, knots, coefficients) + + assert iflag == np.int32(0) + + +def test_db1val(bspline_sub): + x = np.linspace(0.0, 2.0 * np.pi, 30) + knots, coefficients, nx = _interpolant(bspline_sub, x, np.sin(x)) + work = np.zeros(3 * int(CUBIC), dtype=np.float64) + + value, iflag, _inbvx = bspline_sub.db1val( + np.float64(1.2), + np.int32(0), + knots, + nx, + CUBIC, + coefficients, + np.int32(1), + work, + ) + + assert iflag == np.int32(0) + assert value == pytest.approx(np.sin(1.2), abs=1.0e-5) + + +def test_interpolant_reproduces_the_sampled_function(bspline_sub): + x = np.linspace(0.0, 2.0 * np.pi, 30) + knots, bcoef, nx = _interpolant(bspline_sub, x, np.sin(x)) + + for point in np.linspace(0.3, 5.9, 7): + assert _evaluate(bspline_sub, knots, bcoef, nx, point) == pytest.approx(np.sin(point), abs=1.0e-5) + + +def test_interpolant_is_exact_on_a_low_order_polynomial(bspline_sub): + """A cubic spline reproduces a cubic exactly, up to rounding.""" + x = np.linspace(0.0, 1.0, 25) + knots, bcoef, nx = _interpolant(bspline_sub, x, x**3) + + for point in (0.25, 0.5, 0.75): + assert _evaluate(bspline_sub, knots, bcoef, nx, point) == pytest.approx(point**3, abs=1.0e-9) + + +def test_first_derivative_matches_the_analytic_derivative(bspline_sub): + x = np.linspace(0.0, 2.0 * np.pi, 60) + knots, bcoef, nx = _interpolant(bspline_sub, x, np.sin(x)) + + for point in np.linspace(0.5, 5.5, 5): + value = _evaluate(bspline_sub, knots, bcoef, nx, point, derivative=1) + assert value == pytest.approx(np.cos(point), abs=1.0e-4) + + +def test_db1sqad(bspline_sub): + x = np.linspace(0.0, np.pi, 60) + knots, bcoef, nx = _interpolant(bspline_sub, x, np.sin(x)) + work = np.zeros(3 * int(CUBIC), dtype=np.float64) + + value, iflag = bspline_sub.db1sqad(knots, bcoef, nx, CUBIC, np.float64(0.0), np.float64(np.pi), work) + assert iflag == np.int32(0) + assert value == pytest.approx(2.0, abs=1.0e-6) + + +def test_db1fqad(bspline_sub): + x = np.linspace(0.0, np.pi, 60) + knots, coefficients, nx = _interpolant(bspline_sub, x, np.sin(x)) + work = np.zeros(3 * int(CUBIC), dtype=np.float64) + + value, iflag = bspline_sub.db1fqad( + lambda _point: np.float64(1.0), + knots, + coefficients, + nx, + CUBIC, + np.int32(0), + np.float64(0.0), + np.float64(np.pi), + np.float64(1.0e-10), + work, + ) + + assert iflag == np.int32(0) + assert value == pytest.approx(2.0, abs=3.0e-8) + + +def test_db2ink(bspline_sub): + setup_arguments, _evaluation_arguments = _multidimensional_inputs(2) + + assert bspline_sub.db2ink(*setup_arguments) == np.int32(0) + + +def test_db2val(bspline_sub): + setup_arguments, evaluation_arguments = _multidimensional_inputs(2) + assert bspline_sub.db2ink(*setup_arguments) == np.int32(0) + + value, iflag, *_state = bspline_sub.db2val(*evaluation_arguments) + + assert iflag == np.int32(0) + assert value == pytest.approx(0.6, abs=1.0e-12) + + +def test_db3ink(bspline_sub): + setup_arguments, _evaluation_arguments = _multidimensional_inputs(3) + + assert bspline_sub.db3ink(*setup_arguments) == np.int32(0) + + +def test_db3val(bspline_sub): + setup_arguments, evaluation_arguments = _multidimensional_inputs(3) + assert bspline_sub.db3ink(*setup_arguments) == np.int32(0) + + value, iflag, *_state = bspline_sub.db3val(*evaluation_arguments) + + assert iflag == np.int32(0) + assert value == pytest.approx(0.9, abs=1.0e-12) + + +def test_db4ink(bspline_sub): + setup_arguments, _evaluation_arguments = _multidimensional_inputs(4) + + assert bspline_sub.db4ink(*setup_arguments) == np.int32(0) + + +def test_db4val(bspline_sub): + setup_arguments, evaluation_arguments = _multidimensional_inputs(4) + assert bspline_sub.db4ink(*setup_arguments) == np.int32(0) + + value, iflag, *_state = bspline_sub.db4val(*evaluation_arguments) + + assert iflag == np.int32(0) + assert value == pytest.approx(1.2, abs=1.0e-12) + + +def test_db5ink(bspline_sub): + setup_arguments, _evaluation_arguments = _multidimensional_inputs(5) + + assert bspline_sub.db5ink(*setup_arguments) == np.int32(0) + + +def test_db5val(bspline_sub): + setup_arguments, evaluation_arguments = _multidimensional_inputs(5) + assert bspline_sub.db5ink(*setup_arguments) == np.int32(0) + + value, iflag, *_state = bspline_sub.db5val(*evaluation_arguments) + + assert iflag == np.int32(0) + assert value == pytest.approx(1.5, abs=1.0e-12) + + +def test_db6ink(bspline_sub): + setup_arguments, _evaluation_arguments = _multidimensional_inputs(6) + + assert bspline_sub.db6ink(*setup_arguments) == np.int32(0) + + +def test_db6val(bspline_sub): + setup_arguments, evaluation_arguments = _multidimensional_inputs(6) + assert bspline_sub.db6ink(*setup_arguments) == np.int32(0) + + value, iflag, *_state = bspline_sub.db6val(*evaluation_arguments) + + assert iflag == np.int32(0) + assert value == pytest.approx(1.8, abs=1.0e-12) + + +def test_get_status_message(bspline_sub): + message = bspline_sub.get_status_message(np.int32(0)) + + assert isinstance(message, str) + assert message + + +def test_scipy_agrees_with_the_wrapped_interpolant(bspline_sub): + """An independent oracle checks the wrapper rather than the wrapper alone.""" + scipy_interpolate = pytest.importorskip("scipy.interpolate") + + x = np.linspace(0.0, 3.0, 40) + fcn = np.exp(-x) * np.cos(3.0 * x) + knots, bcoef, nx = _interpolant(bspline_sub, x, fcn) + reference = scipy_interpolate.make_interp_spline(x, fcn, k=3) + + for point in np.linspace(0.2, 2.8, 9): + assert _evaluate(bspline_sub, knots, bcoef, nx, point) == pytest.approx(float(reference(point)), abs=1.0e-6) diff --git a/examples/bspline/tests/test_routine_coverage.py b/examples/bspline/tests/test_routine_coverage.py new file mode 100644 index 000000000..a21a710e9 --- /dev/null +++ b/examples/bspline/tests/test_routine_coverage.py @@ -0,0 +1,55 @@ +"""Fail closed when the reviewed BSPLINE-FORTRAN surface or tests drift.""" + +from __future__ import annotations + +import ast +from pathlib import Path + +import pytest + +from ..routine_inventory import ( + ALL_OBJECT_EXPORTS, + ALL_PROCEDURAL_EXPORTS, + ALL_PROCEDURAL_ROUTINES, + EXPLICIT_PROCEDURAL_TEST_NAMES, + PRIK_TESTED_PROCEDURAL_ROUTINES, + PROCEDURAL_ROUTINE_GROUPS, + UNSUPPORTED_PROCEDURAL_ROUTINES, +) + + +pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] +TEST_FILE = Path(__file__).with_name("test_procedural_api.py") + + +def _test_functions() -> dict[str, ast.FunctionDef]: + """Return the explicitly named public-routine tests in this suite.""" + tree = ast.parse(TEST_FILE.read_text(encoding="utf-8"), filename=str(TEST_FILE)) + return { + node.name: node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name.startswith("test_") + } + + +def test_every_public_procedural_routine_has_one_visible_numerical_test(): + functions = _test_functions() + source_text = TEST_FILE.read_text(encoding="utf-8") + + assert len(ALL_PROCEDURAL_ROUTINES) == len(set(ALL_PROCEDURAL_ROUTINES)) + assert set(ALL_PROCEDURAL_ROUTINES) == PRIK_TESTED_PROCEDURAL_ROUTINES + assert UNSUPPORTED_PROCEDURAL_ROUTINES == {} + + for routine, test_name in EXPLICIT_PROCEDURAL_TEST_NAMES.items(): + source = ast.get_source_segment(source_text, functions[test_name]) + assert source is not None + assert f"bspline_sub.{routine}" in source, f"{test_name} does not visibly invoke {routine}" + + +def test_inventory_groups_cover_each_generated_public_export_once(bspline_oo, bspline_sub): + grouped = tuple(routine for group in PROCEDURAL_ROUTINE_GROUPS.values() for routine in group) + object_exports = {name for name in dir(bspline_oo) if not name.startswith("_")} + procedural_exports = {name for name in dir(bspline_sub) if not name.startswith("_")} + + assert grouped == ALL_PROCEDURAL_ROUTINES + assert len(grouped) == len(set(grouped)) + assert object_exports == set(ALL_OBJECT_EXPORTS) + assert procedural_exports == set(ALL_PROCEDURAL_EXPORTS) diff --git a/examples/lapack/README.md b/examples/lapack/README.md index 78402785d..81d4d38ef 100644 --- a/examples/lapack/README.md +++ b/examples/lapack/README.md @@ -4,8 +4,9 @@ Build the complete Reference LAPACK once, wrap it with PRIK and NumPy f2py, and validate a reviewed double-precision surface against SciPy and independent numerical checks. -PRIK wraps all 2,066 discovered procedures. For focused validation, the suite -selects the 127 `float64` routines exposed by SciPy 1.18.0; raw f2py supports +PRIK wraps all 1,936 procedures in the Reference LAPACK default, non-XBLAS +source set. For focused validation, the suite selects the 127 `float64` +routines exposed by SciPy 1.18.0; raw f2py supports 125 of those source interfaces. All 127 selected routines have explicit correctness tests, with no unsupported or skipped routines. @@ -60,16 +61,18 @@ can reuse or adapt either build independently. ```bash export EXAMPLE_WORKSPACE="$PWD" export LAPACK_BUILD_ROOT="$(mktemp -d)" -export LAPACK_SHARED_LIBRARY="$( +LAPACK_SHARED_LIBRARY="$( python -m examples.native_library lapack \ --compiler "$(command -v gfortran)" \ --jobs 8 )" +export LAPACK_SHARED_LIBRARY export LAPACK_MODULE_DIR="$(dirname "$LAPACK_SHARED_LIBRARY")/modules" +export LAPACK_SOURCE_ROOT="$(dirname "$LAPACK_SHARED_LIBRARY")/wrapper_sources" mkdir -p "$LAPACK_BUILD_ROOT/prik/generated" cd "$LAPACK_BUILD_ROOT/prik" -python -m prik "$EXAMPLE_WORKSPACE/examples/lapack/native" \ +python -m prik "$LAPACK_SOURCE_ROOT" \ --out prik_reference_lapack_example \ --out-dir "$LAPACK_BUILD_ROOT/prik/generated" \ --compiler "$(command -v gfortran)" \ @@ -81,7 +84,9 @@ python -m prik "$EXAMPLE_WORKSPACE/examples/lapack/native" \ --wrapper-c-flags="-O0 -g0" ``` -PRIK reads the complete source tree to generate its Python API. +PRIK reads the same default, non-XBLAS source set compiled into the reusable +library. The complete upstream `SRC/` snapshot remains available under +`examples/lapack/native` for provenance and parser inspection. `--no-compile-input-sources` makes it reuse `LAPACK_SHARED_LIBRARY` instead of compiling those native sources again. @@ -146,9 +151,15 @@ Schur decompositions. ## Sources and license -[`native/`](native/) owns 2,062 LAPACK implementation sources: 2,061 from -Netlib LAPACK 3.12.1 plus the project-local `dlamch.f`. BLAS dependencies come -from [`../blas/native/`](../blas/native/) and are not duplicated here. The +[`native/`](native/) owns the complete 2,062-file LAPACK source snapshot: 2,061 +files from Netlib LAPACK 3.12.1 plus the project-local `dlamch.f`. The official +default build excludes the 130 files listed in +[`xblas_sources.txt`](xblas_sources.txt), which require the separately +distributed XBLAS library. The reusable library and PRIK wrapper therefore use +the remaining 1,932 sources. Two required build helpers from upstream +`INSTALL/` live under [`support/`](support/), and BLAS dependencies come from +[`../blas/native/`](../blas/native/). Installed LAPACK and BLAS libraries +provide support routines outside the copied default source set. The audited upstream archive has SHA-256 `37b00c90947488521f475b5a187fff4da4a5cfe61b525efcacf7a97f39a45ec6`. See the [Reference LAPACK site](https://www.netlib.org/lapack/) and its diff --git a/examples/lapack/build_prik.sh b/examples/lapack/build_prik.sh index 6b85ecb38..b3ae5b3f9 100644 --- a/examples/lapack/build_prik.sh +++ b/examples/lapack/build_prik.sh @@ -1,15 +1,17 @@ export EXAMPLE_WORKSPACE="$PWD" export LAPACK_BUILD_ROOT="$(mktemp -d)" -export LAPACK_SHARED_LIBRARY="$( +LAPACK_SHARED_LIBRARY="$( python -m examples.native_library lapack \ --compiler "$(command -v gfortran)" \ --jobs 8 )" +export LAPACK_SHARED_LIBRARY export LAPACK_MODULE_DIR="$(dirname "$LAPACK_SHARED_LIBRARY")/modules" +export LAPACK_SOURCE_ROOT="$(dirname "$LAPACK_SHARED_LIBRARY")/wrapper_sources" mkdir -p "$LAPACK_BUILD_ROOT/prik/generated" cd "$LAPACK_BUILD_ROOT/prik" -python -m prik "$EXAMPLE_WORKSPACE/examples/lapack/native" \ +python -m prik "$LAPACK_SOURCE_ROOT" \ --out prik_reference_lapack_example \ --out-dir "$LAPACK_BUILD_ROOT/prik/generated" \ --compiler "$(command -v gfortran)" \ diff --git a/examples/lapack/ci/full_surface.py b/examples/lapack/ci/full_surface.py index 63b527a10..ea75959e2 100644 --- a/examples/lapack/ci/full_surface.py +++ b/examples/lapack/ci/full_surface.py @@ -2,18 +2,19 @@ from __future__ import annotations +import os from pathlib import Path import pytest -from ..routine_inventory import EXPECTED_LAPACK_PROCEDURES +from ..routine_inventory import EXPECTED_LAPACK_PROCEDURES, EXPECTED_LAPACK_WRAPPED_SOURCE_FILES from examples.lapack.tests.helpers import assert_runtime_smoke from prik.parsers.fortran.parser import parse_fortran_file from prik.preprocessing import PreprocessingConfig, preprocess_source pytestmark = [pytest.mark.fortran_end_to_end, pytest.mark.real_library] -NATIVE_ROOT = Path(__file__).resolve().parents[1] / "native" +NATIVE_ROOT = Path(os.environ["LAPACK_SOURCE_ROOT"]) FORTRAN_SUFFIXES = {".f", ".f90", ".f95", ".f03", ".f08", ".for", ".f77", ".ftn"} PREPROCESSED_FORTRAN_SUFFIXES = {suffix.upper() for suffix in FORTRAN_SUFFIXES} @@ -42,6 +43,9 @@ def _source_procedure_exports() -> set[tuple[str | None, str]]: def test_ci_complete_prik_surface_reuses_example_extension(prik_lapack): expected = _source_procedure_exports() + assert len(tuple(path for path in NATIVE_ROOT.iterdir() if path.suffix.lower() in FORTRAN_SUFFIXES)) == ( + EXPECTED_LAPACK_WRAPPED_SOURCE_FILES + ) assert len(expected) == EXPECTED_LAPACK_PROCEDURES assert all(getattr(prik_lapack, name, None) is not None for name in ("la_constants", "la_xisnan")) diff --git a/examples/lapack/routine_inventory.py b/examples/lapack/routine_inventory.py index 3998db07d..fa5278c1d 100644 --- a/examples/lapack/routine_inventory.py +++ b/examples/lapack/routine_inventory.py @@ -6,7 +6,8 @@ SCIPY_VERSION = "1.18.0" EXPECTED_LAPACK_SOURCE_FILES = 2062 -EXPECTED_LAPACK_PROCEDURES = 2066 +EXPECTED_LAPACK_WRAPPED_SOURCE_FILES = 1932 +EXPECTED_LAPACK_PROCEDURES = 1936 F2PY_SCALAR_WRITEBACK_ROUTINES = frozenset( {"dlarfg", "dlartg", "dgbcon", "dgecon", "dgtcon", "dpocon", "dppcon", "dsycon", "dtrcon"} ) diff --git a/examples/lapack/support/droundup_lwork.f b/examples/lapack/support/droundup_lwork.f new file mode 100644 index 000000000..8df68b0ef --- /dev/null +++ b/examples/lapack/support/droundup_lwork.f @@ -0,0 +1,87 @@ +*> \brief \b DROUNDUP_LWORK +* +* =========== DOCUMENTATION =========== +* +* Online html documentation available at +* http://www.netlib.org/lapack/explore-html/ +* +* Definition: +* =========== +* +* DOUBLE PRECISION FUNCTION DROUNDUP_LWORK( LWORK ) +* +* .. Scalar Arguments .. +* INTEGER LWORK +* .. +* +* +*> \par Purpose: +* ============= +*> +*> \verbatim +*> +*> DROUNDUP_LWORK deals with a subtle bug with returning LWORK as a Float. +*> This routine guarantees it is rounded up instead of down by +*> multiplying LWORK by 1+eps when it is necessary, where eps is the relative machine precision. +*> E.g., +*> +*> float( 9007199254740993 ) == 9007199254740992 +*> float( 9007199254740993 ) * (1.+eps) == 9007199254740994 +*> +*> \return DROUNDUP_LWORK +*> \verbatim +*> DROUNDUP_LWORK >= LWORK. +*> DROUNDUP_LWORK is guaranteed to have zero decimal part. +*> \endverbatim +* +* Arguments: +* ========== +* +*> \param[in] LWORK Workspace size. +* +* Authors: +* ======== +* +*> \author Weslley Pereira, University of Colorado Denver, USA +* +*> \ingroup roundup_lwork +* +*> \par Further Details: +* ===================== +*> +*> \verbatim +*> This routine was inspired in the method `magma_zmake_lwork` from MAGMA. +*> \see https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp +*> \endverbatim +* +* ===================================================================== + DOUBLE PRECISION FUNCTION DROUNDUP_LWORK( LWORK ) +* +* -- LAPACK auxiliary routine -- +* -- LAPACK is a software package provided by Univ. of Tennessee, -- +* -- Univ. of California Berkeley, Univ. of Colorado Denver and NAG Ltd..-- +* +* .. Scalar Arguments .. + INTEGER LWORK +* .. +* +* ===================================================================== +* .. +* .. Intrinsic Functions .. + INTRINSIC EPSILON, DBLE, INT +* .. +* .. Executable Statements .. +* .. + DROUNDUP_LWORK = DBLE( LWORK ) +* + IF( INT( DROUNDUP_LWORK ) .LT. LWORK ) THEN +* Force round up of LWORK + DROUNDUP_LWORK = DROUNDUP_LWORK * + $ ( 1.0D+0 + EPSILON(0.0D+0) ) + ENDIF +* + RETURN +* +* End of DROUNDUP_LWORK +* + END diff --git a/examples/lapack/support/sroundup_lwork.f b/examples/lapack/support/sroundup_lwork.f new file mode 100644 index 000000000..7056ea311 --- /dev/null +++ b/examples/lapack/support/sroundup_lwork.f @@ -0,0 +1,87 @@ +*> \brief \b SROUNDUP_LWORK +* +* =========== DOCUMENTATION =========== +* +* Online html documentation available at +* http://www.netlib.org/lapack/explore-html/ +* +* Definition: +* =========== +* +* REAL FUNCTION SROUNDUP_LWORK( LWORK ) +* +* .. Scalar Arguments .. +* INTEGER LWORK +* .. +* +* +*> \par Purpose: +* ============= +*> +*> \verbatim +*> +*> SROUNDUP_LWORK deals with a subtle bug with returning LWORK as a Float. +*> This routine guarantees it is rounded up instead of down by +*> multiplying LWORK by 1+eps when it is necessary, where eps is the relative machine precision. +*> E.g., +*> +*> float( 16777217 ) == 16777216 +*> float( 16777217 ) * (1.+eps) == 16777218 +*> +*> \return SROUNDUP_LWORK +*> \verbatim +*> SROUNDUP_LWORK >= LWORK. +*> SROUNDUP_LWORK is guaranteed to have zero decimal part. +*> \endverbatim +* +* Arguments: +* ========== +* +*> \param[in] LWORK Workspace size. +* +* Authors: +* ======== +* +*> \author Weslley Pereira, University of Colorado Denver, USA +* +*> \ingroup roundup_lwork +* +*> \par Further Details: +* ===================== +*> +*> \verbatim +*> This routine was inspired in the method `magma_zmake_lwork` from MAGMA. +*> \see https://bitbucket.org/icl/magma/src/master/control/magma_zauxiliary.cpp +*> \endverbatim +* +* ===================================================================== + REAL FUNCTION SROUNDUP_LWORK( LWORK ) +* +* -- LAPACK auxiliary routine -- +* -- LAPACK is a software package provided by Univ. of Tennessee, -- +* -- Univ. of California Berkeley, Univ. of Colorado Denver and NAG Ltd..-- +* +* .. Scalar Arguments .. + INTEGER LWORK +* .. +* +* ===================================================================== +* .. +* .. Intrinsic Functions .. + INTRINSIC EPSILON, REAL, INT +* .. +* .. Executable Statements .. +* .. + SROUNDUP_LWORK = REAL( LWORK ) +* + IF( INT( SROUNDUP_LWORK ) .LT. LWORK ) THEN +* Force round up of LWORK + SROUNDUP_LWORK = SROUNDUP_LWORK * + $ ( 1.0E+0 + EPSILON(0.0E+0) ) + ENDIF +* + RETURN +* +* End of SROUNDUP_LWORK +* + END diff --git a/examples/lapack/tests/test_auxiliary.py b/examples/lapack/tests/test_auxiliary.py index a9f8f7f05..170795fc7 100644 --- a/examples/lapack/tests/test_auxiliary.py +++ b/examples/lapack/tests/test_auxiliary.py @@ -14,10 +14,12 @@ def test_dlamch_reports_float64_machine_epsilon(prik_lapack, scipy_lapack, f2py_lapack): expected = np.finfo(np.float64).eps / 2.0 - prik_value = prik_lapack.dlamch("E") + # DLAMCH declares no intent, so its CMACH selector is returned with the value. + prik_value, prik_cmach = prik_lapack.dlamch("E") f2py_value = f2py_lapack.dlamch(b"E") scipy_value = scipy_lapack.dlamch(b"E") + assert prik_cmach == "E" assert_allclose_float64(prik_value, expected) assert_allclose_float64(f2py_value, expected) assert_allclose_float64(scipy_value, expected) @@ -36,7 +38,9 @@ def test_dlangb_computes_frobenius_norm_of_band_storage(prik_lapack, scipy_lapac f2py_value = f2py_lapack.dlangb(b"F", 3, 1, 1, f2py_ab, np.empty(3, dtype=np.float64)) scipy_value = scipy_lapack.dlangb(b"F", 1, 1, scipy_ab) - assert prik_result[1:] == (3, 1, 1, 3) + # LAPACK declares no intent on its dummies, so the conservative + # intent(inout) default returns every scalar, character selectors included. + assert prik_result[1:] == ("F", 3, 1, 1, 3) assert_allclose_float64(prik_result[0], expected, operation_size=7) assert_allclose_float64(f2py_value, expected, operation_size=7) assert_allclose_float64(scipy_value, expected, operation_size=7) @@ -51,7 +55,9 @@ def test_dlange_computes_one_norm(prik_lapack, scipy_lapack, f2py_lapack): f2py_value = f2py_lapack.dlange(b"1", 3, 2, matrix.copy(order="F"), work.copy()) scipy_value = scipy_lapack.dlange(b"1", matrix.copy(order="F")) - assert prik_result[1:] == (3, 2, 3) + # LAPACK declares no intent on its dummies, so the conservative + # intent(inout) default returns every scalar, character selectors included. + assert prik_result[1:] == ("1", 3, 2, 3) assert_allclose_float64(prik_result[0], expected, operation_size=3) assert_allclose_float64(f2py_value, expected, operation_size=3) assert_allclose_float64(scipy_value, expected, operation_size=3) @@ -68,7 +74,9 @@ def test_dlantr_ignores_unused_triangle_and_unit_diagonal(prik_lapack, scipy_lap f2py_value = f2py_lapack.dlantr(b"F", b"U", b"U", 3, 3, stored.copy(order="F"), work.copy()) scipy_value = scipy_lapack.dlantr(b"F", stored.copy(order="F"), uplo=b"U", diag=b"U") - assert prik_result[1:] == (3, 3, 3) + # LAPACK declares no intent on its dummies, so the conservative + # intent(inout) default returns every scalar, character selectors included. + assert prik_result[1:] == ("F", "U", "U", 3, 3, 3) assert_allclose_float64(prik_result[0], expected, operation_size=6) assert_allclose_float64(f2py_value, expected, operation_size=6) assert_allclose_float64(scipy_value, expected, operation_size=6) @@ -88,7 +96,9 @@ def test_dlarf_applies_householder_reflector_from_left(prik_lapack, scipy_lapack f2py_result = f2py_lapack.dlarf(b"L", 2, 2, vector, 1, tau, f2py_c, np.empty(2)) scipy_c = scipy_lapack.dlarf(vector, tau, original.copy(order="F"), np.empty(2), side=b"L") - assert prik_scalars == (2, 2, 1, tau, 2) + # LAPACK declares no intent on its dummies, so the conservative + # intent(inout) default returns every scalar, character selectors included. + assert prik_scalars == ("L", 2, 2, 1, tau, 2) assert f2py_result is None assert_allclose_float64(prik_c, expected, operation_size=2) assert_allclose_float64(f2py_c, expected, operation_size=2) diff --git a/examples/lapack/tests/test_eigen_generalized.py b/examples/lapack/tests/test_eigen_generalized.py index ca6a4746a..295a6b286 100644 --- a/examples/lapack/tests/test_eigen_generalized.py +++ b/examples/lapack/tests/test_eigen_generalized.py @@ -67,7 +67,9 @@ def test_dgges_computes_generalized_real_schur_form(prik_lapack, scipy_lapack): ) assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[4] == scipy_sdim == 0 + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + assert prik_scalars[7] == scipy_sdim == 0 for s, t, ar, ai, beta, q, z in ( (prik_a, prik_b, prik_ar, prik_ai, prik_beta, prik_vsl, prik_vsr), (scipy_a, scipy_b, scipy_ar, scipy_ai, scipy_beta, scipy_vsl, scipy_vsr), @@ -276,7 +278,9 @@ def test_dsygvx_selects_generalized_eigenpairs(prik_lapack, scipy_lapack, f2py_l assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[9] == scipy_m == 2 + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + assert prik_scalars[12] == scipy_m == 2 _assert_generalized_eigenpairs(a, b, prik_w, prik_z) _assert_generalized_eigenpairs(a, b, f2py_w, f2py_z) _assert_generalized_eigenpairs(a, b, scipy_w, scipy_z) diff --git a/examples/lapack/tests/test_eigen_nonsymmetric.py b/examples/lapack/tests/test_eigen_nonsymmetric.py index 54cd8539d..27f6b44ef 100644 --- a/examples/lapack/tests/test_eigen_nonsymmetric.py +++ b/examples/lapack/tests/test_eigen_nonsymmetric.py @@ -38,7 +38,9 @@ def test_dgebal_preserves_eigenvalues_while_balancing(prik_lapack, scipy_lapack, assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[2:4] == (scipy_lo + 1, scipy_hi + 1) + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + assert prik_scalars[3:5] == (scipy_lo + 1, scipy_hi + 1) expected_eigenvalues = np.sort(np.linalg.eigvals(matrix).real) assert_allclose_float64(np.sort(np.linalg.eigvals(prik_a).real), expected_eigenvalues, operation_size=2) assert_allclose_float64(np.sort(np.linalg.eigvals(f2py_a).real), expected_eigenvalues, operation_size=2) @@ -78,7 +80,9 @@ def test_dgees_computes_real_schur_decomposition(prik_lapack, scipy_lapack): ) assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[3] == scipy_sdim == 0 + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + assert prik_scalars[5] == scipy_sdim == 0 expected_eigenvalues = np.sort(np.linalg.eigvals(matrix).real) for t, vs, wr, wi in ( (prik_t, prik_vs, prik_wr, prik_wi), @@ -251,7 +255,9 @@ def test_dtrsen_reorders_selected_schur_eigenvalue(prik_lapack, scipy_lapack, f2 assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[3] == scipy_m == 1 + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + assert prik_scalars[5] == scipy_m == 1 for t, q, wr, wi in ( (prik_t, prik_q, prik_wr, prik_wi), (f2py_t, f2py_q, f2py_wr, f2py_wi), diff --git a/examples/lapack/tests/test_eigen_symmetric.py b/examples/lapack/tests/test_eigen_symmetric.py index 4395ff0b6..9ceae3a43 100644 --- a/examples/lapack/tests/test_eigen_symmetric.py +++ b/examples/lapack/tests/test_eigen_symmetric.py @@ -156,7 +156,9 @@ def test_dsbevx_selects_all_symmetric_band_eigenpairs(prik_lapack, scipy_lapack, assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[10] == scipy_m == 2 + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + assert prik_scalars[12] == scipy_m == 2 _assert_eigensystem(matrix, prik_w, prik_z) _assert_eigensystem(matrix, f2py_w, f2py_z) _assert_eigensystem(matrix, scipy_w, scipy_z) @@ -216,8 +218,10 @@ def test_dstebz_bisects_tridiagonal_eigenvalues(prik_lapack, scipy_lapack, f2py_ assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - prik_m = int(prik_scalars[6]) - prik_nsplit = int(prik_scalars[7]) + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + prik_m = int(prik_scalars[8]) + prik_nsplit = int(prik_scalars[9]) assert prik_m == scipy_m == 2 assert prik_nsplit == 1 assert_allclose_float64(prik_w[:2], [2.0, 3.0]) @@ -344,7 +348,9 @@ def test_dstemr_computes_robust_tridiagonal_eigenpairs(prik_lapack, scipy_lapack assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[5] == scipy_m == 2 + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + assert prik_scalars[7] == scipy_m == 2 _assert_eigensystem(matrix, prik_w, prik_z) _assert_eigensystem(matrix, f2py_w, f2py_z) _assert_eigensystem(matrix, scipy_w, scipy_z) @@ -427,7 +433,9 @@ def test_dsyev_returns_orthonormal_eigenvectors(prik_lapack, scipy_lapack, f2py_ f2py_result = f2py_lapack.dsyev(b"V", b"U", 2, f2py_vectors, f2py_w, np.empty(16), 16, 0) scipy_w, scipy_vectors, scipy_info = scipy_lapack.dsyev(matrix.copy(order="F"), compute_v=1, lower=0, lwork=16) - assert prik_scalars == (2, 2, 16, 0) + # LAPACK declares no intent on its dummies, so the conservative + # intent(inout) default returns every scalar, character selectors included. + assert prik_scalars == ("V", "U", 2, 2, 16, 0) assert f2py_result is None assert scipy_info == 0 for values, vectors in ( @@ -529,7 +537,9 @@ def test_dsyevr_selects_symmetric_eigenpairs_by_index(prik_lapack, scipy_lapack, assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[8] == scipy_m == 2 + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + assert prik_scalars[10] == scipy_m == 2 _assert_eigensystem(matrix, prik_w, prik_z) _assert_eigensystem(matrix, f2py_w, f2py_z) _assert_eigensystem(matrix, scipy_w, scipy_z) @@ -590,7 +600,9 @@ def test_dsyevx_selects_symmetric_eigenpairs_by_value(prik_lapack, scipy_lapack, assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[8] == scipy_m == 2 + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + assert prik_scalars[10] == scipy_m == 2 _assert_eigensystem(matrix, prik_w, prik_z) _assert_eigensystem(matrix, f2py_w, f2py_z) _assert_eigensystem(matrix, scipy_w, scipy_z) diff --git a/examples/lapack/tests/test_linear_general.py b/examples/lapack/tests/test_linear_general.py index c335b0741..1e06bd75c 100644 --- a/examples/lapack/tests/test_linear_general.py +++ b/examples/lapack/tests/test_linear_general.py @@ -310,7 +310,9 @@ def test_dgetrs_solves_from_native_lu(prik_lapack, scipy_lapack, f2py_lapack): f2py_result = f2py_lapack.dgetrs(b"N", 2, 1, f2py_lu, native_ipiv.copy(), f2py_b, 0) scipy_x, scipy_info = scipy_lapack.dgetrs(scipy_lu, scipy_piv, original_b.copy(order="F"), trans=0) - assert prik_scalars == (2, 1, 2, 2, 0) + # LAPACK declares no intent on its dummies, so the conservative + # intent(inout) default returns every scalar, character selectors included. + assert prik_scalars == ("N", 2, 1, 2, 2, 0) assert f2py_result is None assert scipy_info == 0 assert_allclose_float64(prik_b, expected_x, operation_size=2) diff --git a/examples/lapack/tests/test_linear_positive_definite.py b/examples/lapack/tests/test_linear_positive_definite.py index 5d5386268..000df51a5 100644 --- a/examples/lapack/tests/test_linear_positive_definite.py +++ b/examples/lapack/tests/test_linear_positive_definite.py @@ -194,7 +194,9 @@ def test_dpotrf_reconstructs_spd_matrix(prik_lapack, scipy_lapack, f2py_lapack): f2py_result = f2py_lapack.dpotrf(b"L", 2, f2py_a, 0) scipy_factor, scipy_info = scipy_lapack.dpotrf(stored.copy(order="F"), lower=1, clean=0) - assert prik_scalars == (2, 2, 0) + # LAPACK declares no intent on its dummies, so the conservative + # intent(inout) default returns every scalar, character selectors included. + assert prik_scalars == ("L", 2, 2, 0) assert f2py_result is None assert scipy_info == 0 prik_lower = np.tril(prik_a) @@ -344,7 +346,9 @@ def test_dpstf2_reconstructs_pivoted_cholesky(prik_lapack, scipy_lapack, f2py_la assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[2] == scipy_rank == 1 + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + assert prik_scalars[3] == scipy_rank == 1 assert_allclose_float64(prik_a.T @ prik_a, matrix) assert_allclose_float64(f2py_a.T @ f2py_a, matrix) assert_allclose_float64(scipy_a.T @ scipy_a, matrix) @@ -365,7 +369,9 @@ def test_dpstrf_reconstructs_blocked_pivoted_cholesky(prik_lapack, scipy_lapack, assert f2py_result is None assert prik_scalars[-1] == scipy_info == 0 - assert prik_scalars[2] == scipy_rank == 1 + # Character selectors are returned too, so the projected scalars sit at + # their native-argument positions in the returned tuple. + assert prik_scalars[3] == scipy_rank == 1 assert_allclose_float64(prik_a.T @ prik_a, matrix) assert_allclose_float64(f2py_a.T @ f2py_a, matrix) assert_allclose_float64(scipy_a.T @ scipy_a, matrix) @@ -385,7 +391,9 @@ def test_dsfrk_updates_spd_matrix_in_rfp_storage(prik_lapack, scipy_lapack, f2py scipy_c = scipy_lapack.dsfrk(1, 2, 1.0, matrix, 1.0, np.array([1.0])) assert f2py_result is None - assert prik_scalars == (1, 2, 1.0, 1, 1.0) + # LAPACK declares no intent on its dummies, so the conservative + # intent(inout) default returns every scalar, character selectors included. + assert prik_scalars == ("N", "U", "N", 1, 2, 1.0, 1, 1.0) assert_allclose_float64(prik_c, expected, operation_size=2) assert_allclose_float64(f2py_c, expected, operation_size=2) assert_allclose_float64(scipy_c, expected, operation_size=2) diff --git a/examples/lapack/tests/test_linear_triangular.py b/examples/lapack/tests/test_linear_triangular.py index 4d19dfe8a..3c40b903c 100644 --- a/examples/lapack/tests/test_linear_triangular.py +++ b/examples/lapack/tests/test_linear_triangular.py @@ -35,7 +35,9 @@ def test_dtfsm_solves_with_rfp_triangular_factor(prik_lapack, scipy_lapack, f2py ) assert f2py_result is None - assert prik_scalars == (2, 1, 1.0, 2) + # LAPACK declares no intent on its dummies, so the conservative + # intent(inout) default returns every scalar, character selectors included. + assert prik_scalars == ("N", "L", "U", "N", "N", 2, 1, 1.0, 2) assert_allclose_float64(prik_b, expected, operation_size=2) assert_allclose_float64(f2py_b, expected, operation_size=2) assert_allclose_float64(scipy_x, expected, operation_size=2) diff --git a/examples/lapack/tests/test_routine_coverage.py b/examples/lapack/tests/test_routine_coverage.py index 1ee6c578e..212f7052f 100644 --- a/examples/lapack/tests/test_routine_coverage.py +++ b/examples/lapack/tests/test_routine_coverage.py @@ -12,6 +12,7 @@ EXPLICIT_TEST_NAMES, EXPECTED_LAPACK_PROCEDURES, EXPECTED_LAPACK_SOURCE_FILES, + EXPECTED_LAPACK_WRAPPED_SOURCE_FILES, F2PY_EXPORT_LIMITATIONS, F2PY_FUNCTION_RESULTS, F2PY_SCALAR_WRITEBACK_ROUTINES, @@ -179,7 +180,7 @@ def test_authoritative_native_source_boundary_is_complete_and_unique(): ) stems = {path.stem.lower() for path in sources} assert len(sources) == EXPECTED_LAPACK_SOURCE_FILES - assert EXPECTED_LAPACK_PROCEDURES == EXPECTED_LAPACK_SOURCE_FILES + 4 + assert EXPECTED_LAPACK_PROCEDURES == EXPECTED_LAPACK_WRAPPED_SOURCE_FILES + 4 assert set(ROUTINES) <= stems for routine, spec in ROUTINE_SPECS.items(): assert (NATIVE_ROOT / spec.source_file).is_file(), routine @@ -208,19 +209,6 @@ def test_selected_tests_keep_all_wrapper_calls_visible(): assert missing == {} -def test_documented_coverage_claims_match_inventory(): - """Published claims are derived from the reviewed inventory.""" - readme = " ".join((EXAMPLE_ROOT / "README.md").read_text(encoding="utf-8").split()) - assert len(EXPLICIT_TEST_NAMES) == len(ROUTINES) - assert f"PRIK wraps all {EXPECTED_LAPACK_PROCEDURES:,} discovered procedures" in readme - assert f"the {len(ROUTINES)} `float64` routines" in readme - assert f"raw f2py supports {len(ROUTINES) - len(F2PY_EXPORT_LIMITATIONS)}" in readme - assert f"All {len(EXPLICIT_TEST_NAMES)} selected routines have explicit correctness tests" in readme - assert f"The {len(F2PY_INOUT_ARGUMENTS)} scalar-writeback routines" in readme - assert f"owns {EXPECTED_LAPACK_SOURCE_FILES:,} LAPACK implementation sources" in readme - assert "no unsupported or skipped routines" in readme - - def test_selected_routines_are_exported_by_prik(prik_lapack): """The complete PRIK wrapper must export every selected routine.""" missing = [name for name in ROUTINES if not hasattr(prik_lapack, name)] diff --git a/examples/lapack/tests/test_svd.py b/examples/lapack/tests/test_svd.py index 2ca89b632..76b961e4c 100644 --- a/examples/lapack/tests/test_svd.py +++ b/examples/lapack/tests/test_svd.py @@ -137,7 +137,9 @@ def test_dgesvd_reconstructs_matrix(prik_lapack, scipy_lapack, f2py_lapack): matrix.copy(order="F"), compute_uv=1, full_matrices=1, lwork=32 ) - assert prik_scalars == (3, 2, 3, 3, 2, 32, 0) + # LAPACK declares no intent on its dummies, so the conservative + # intent(inout) default returns every scalar, character selectors included. + assert prik_scalars == ("A", "A", 3, 2, 3, 3, 2, 32, 0) assert f2py_result is None assert scipy_info == 0 for u, values, vt in ( diff --git a/examples/lapack/xblas_sources.txt b/examples/lapack/xblas_sources.txt new file mode 100644 index 000000000..027e1c65a --- /dev/null +++ b/examples/lapack/xblas_sources.txt @@ -0,0 +1,131 @@ +# Reference LAPACK 3.12.1 SRC files enabled only by USE_XBLAS. +cgbrfsx.f +cgbsvxx.f +cgerfsx.f +cgesvxx.f +cherfsx.f +chesvxx.f +cla_gbamv.f +cla_gbrcond_c.f +cla_gbrcond_x.f +cla_gbrfsx_extended.f +cla_gbrpvgrw.f +cla_geamv.f +cla_gercond_c.f +cla_gercond_x.f +cla_gerfsx_extended.f +cla_gerpvgrw.f +cla_heamv.f +cla_hercond_c.f +cla_hercond_x.f +cla_herfsx_extended.f +cla_herpvgrw.f +cla_lin_berr.f +cla_porcond_c.f +cla_porcond_x.f +cla_porfsx_extended.f +cla_porpvgrw.f +cla_syamv.f +cla_syrcond_c.f +cla_syrcond_x.f +cla_syrfsx_extended.f +cla_syrpvgrw.f +cla_wwaddw.f +clarscl2.f +clascl2.f +cporfsx.f +cposvxx.f +csyrfsx.f +csysvxx.f +dgbrfsx.f +dgbsvxx.f +dgerfsx.f +dgesvxx.f +dla_gbamv.f +dla_gbrcond.f +dla_gbrfsx_extended.f +dla_gbrpvgrw.f +dla_geamv.f +dla_gercond.f +dla_gerfsx_extended.f +dla_gerpvgrw.f +dla_lin_berr.f +dla_porcond.f +dla_porfsx_extended.f +dla_porpvgrw.f +dla_syamv.f +dla_syrcond.f +dla_syrfsx_extended.f +dla_syrpvgrw.f +dla_wwaddw.f +dlarscl2.f +dlascl2.f +dporfsx.f +dposvxx.f +dsyrfsx.f +dsysvxx.f +sgbrfsx.f +sgbsvxx.f +sgerfsx.f +sgesvxx.f +sla_gbamv.f +sla_gbrcond.f +sla_gbrfsx_extended.f +sla_gbrpvgrw.f +sla_geamv.f +sla_gercond.f +sla_gerfsx_extended.f +sla_gerpvgrw.f +sla_lin_berr.f +sla_porcond.f +sla_porfsx_extended.f +sla_porpvgrw.f +sla_syamv.f +sla_syrcond.f +sla_syrfsx_extended.f +sla_syrpvgrw.f +sla_wwaddw.f +slarscl2.f +slascl2.f +sporfsx.f +sposvxx.f +ssyrfsx.f +ssysvxx.f +zgbrfsx.f +zgbsvxx.f +zgerfsx.f +zgesvxx.f +zherfsx.f +zhesvxx.f +zla_gbamv.f +zla_gbrcond_c.f +zla_gbrcond_x.f +zla_gbrfsx_extended.f +zla_gbrpvgrw.f +zla_geamv.f +zla_gercond_c.f +zla_gercond_x.f +zla_gerfsx_extended.f +zla_gerpvgrw.f +zla_heamv.f +zla_hercond_c.f +zla_hercond_x.f +zla_herfsx_extended.f +zla_herpvgrw.f +zla_lin_berr.f +zla_porcond_c.f +zla_porcond_x.f +zla_porfsx_extended.f +zla_porpvgrw.f +zla_syamv.f +zla_syrcond_c.f +zla_syrcond_x.f +zla_syrfsx_extended.f +zla_syrpvgrw.f +zla_wwaddw.f +zlarscl2.f +zlascl2.f +zporfsx.f +zposvxx.f +zsyrfsx.f +zsysvxx.f diff --git a/examples/libm/README.md b/examples/libm/README.md new file mode 100644 index 000000000..965bdb828 --- /dev/null +++ b/examples/libm/README.md @@ -0,0 +1,166 @@ +# Wrap the C Standard Math Library with PRIK + +This maintained example wraps 60 reviewed ISO C99 functions from the target's +math library. It generates a target-specific semantic `.pyi`, builds the direct +C wrapper, tests every exported routine, and audits the built surface against +the reviewed inventory. + +Its layout mirrors the other real-library examples: + +- `libm_probe.h` includes the target toolchain's own ``. +- `iso_c99_routines.txt` is the reviewed 60-function allowlist. +- `build_prik.sh` generates the target contract and builds the extension. +- `build_all.sh` exposes the built module on `PYTHONPATH`. +- `routine_inventory.py` groups every public function and names its test. +- `tests/` contains numerical tests and fail-closed surface audits. + +## Requirements + +Install a C compiler, Python development headers, NumPy, and pytest. On Ubuntu: + +```console +sudo apt-get update +sudo apt-get install --yes build-essential python3-dev +python3 -m pip install "numpy>=2" pytest +``` + +Run the remaining commands from the repository root. + +## Quick start + +```bash +source examples/libm/build_all.sh +python3 -m pytest -q examples/libm/tests +``` + +Use `source` so the build paths exported by `build_all.sh` remain available to +the test process. + +## How the build stays portable + +The committed [`libm_probe.h`](libm_probe.h) contains only `#include `. +The generated contract therefore uses the declarations supplied by the active +compiler and platform. [`iso_c99_routines.txt`](iso_c99_routines.txt) selects +the reviewed ISO C99 functions and excludes implementation internals, macros, +constants, and unsupported pointer or string forms. Unknown names fail the +build instead of producing a smaller module silently. + +Private system-header context is still parsed before export selection. That +includes compiler compatibility declarations such as fallback `_Float32` +typedefs; accepting those declarations does not export them or add them to the +direct-wrapper scalar lane. + +The build keeps included headers private with `--include-exposure roots-only`, +then promotes only the allowlisted functions with `--export-symbols`. It also +removes implementation parameter names from the Python API and isolates every +selected C declaration from names already present in Python's headers: + + +```bash +export EXAMPLE_WORKSPACE="$PWD" +export LIBM_BUILD_ROOT="$(mktemp -d)" + +LIBM_COMPILER="${PRIK_LIBM_CC:-cc}" +if ! LIBM_COMPILER_PATH="$(command -v "$LIBM_COMPILER")"; then + echo "libm example: C compiler not found: $LIBM_COMPILER" >&2 + return 1 2>/dev/null || exit 1 +fi +export LIBM_COMPILER_PATH + +mkdir -p "$LIBM_BUILD_ROOT/prik/contract" "$LIBM_BUILD_ROOT/prik/generated" +cd "$LIBM_BUILD_ROOT/prik" + +if ! python3 -m prik generate --pyi --language c \ + "$EXAMPLE_WORKSPACE/examples/libm/libm_probe.h" \ + --compiler "$LIBM_COMPILER_PATH" \ + --std c99 \ + --include-exposure roots-only \ + --export-symbols "$EXAMPLE_WORKSPACE/examples/libm/iso_c99_routines.txt" \ + --out "$LIBM_BUILD_ROOT/prik/contract/libm_api.pyi"; then + return 1 2>/dev/null || exit 1 +fi + +if ! python3 -m prik --language c "$LIBM_BUILD_ROOT/prik/contract/libm_api.pyi" \ + --out prik_reference_libm \ + --out-dir "$LIBM_BUILD_ROOT/prik/generated" \ + --compiler "$LIBM_COMPILER_PATH" \ + --native-library m \ + --positional-only \ + --collision-adapter-all; then + return 1 2>/dev/null || exit 1 +fi +``` + +The public signature uses target-sized NumPy contract types. Exact native C +identities appear only at the native boundary. For example, an LP64 target may +generate: + +```python +@native_call([Arg(0)], result=CLongLong(Return(0))) +def llrint(x: Float64) -> Int64: ... +``` + +Here the native function is declared with a `long long` result and that result +is converted to public `Int64` storage. C `long`, C `int`, and `long double` +tests derive their expected NumPy dtypes from the active target. A native cast +is sparse: it is omitted when the canonical fixed-width typedef already has +the exact source C identity and emitted otherwise. Exact type preservation +handles ABI identity; `--collision-adapter-all` separately prevents a selected +`math.h` declaration such as `remainder` from colliding with a declaration in a +binding header. LTO is not required, so this example does not use `--lto`. + +When C `long double` has the same 64-bit storage width as `double`, as on Apple +ARM64, its public contract is `Float64` and callers pass `numpy.float64`. +Targets with wider `long double` storage use `Float128` and +`numpy.longdouble`. In both cases the native call still retains the exact C +`long double` identity. The generated `.pyi` is the authority for the public +dtype; `CLongDouble` in `@native_call` directs the private scalar conversion and +does not add a second accepted Python dtype. The example tests read the public +annotation instead of independently inferring the choice from NumPy's sizes. + +Macros are intentionally outside the example. Expose a macro through an +ordinary native function when an API needs one. + +The inventory also leaves out `frexp`, `modf`, and `remquo`, whose output +pointers need an authored direction/projection contract, and `nan`, whose +string argument needs authored semantics. Non-ISO Bessel extensions are not +part of the ISO C99 selection. + +## What is validated + +Every inventory entry is visibly invoked by one of four grouped numerical +tests. The audits verify that the generated contract, built module, inventory, +and tests all expose the same 60 functions. + +The numerical oracles are mixed: Python's `math` module where it matches, +independent identities for error and gamma functions, target-aware rounding +checks, tolerance-based transcendental comparisons, exact dtype assertions, +and a fused-rounding check for `fma`. + +Run focused groups with: + +```bash +python3 -m pytest -q examples/libm/tests/test_numerical.py::test_special +python3 -m pytest -q examples/libm/tests/test_numerical.py::test_rounding +python3 -m pytest -q examples/libm/tests/test_numerical.py::test_precision +``` + +## Portability boundary + +The API selection is ISO C99, but build configuration is still target-specific. +`--native-library m` is the conventional Unix link spelling; targets that put +math symbols in a different library should adjust that link item. PRIK fails +when the compiler probe reports a scalar representation outside its supported +contract widths. + +Set `PRIK_LIBM_CC` to select another compiler executable; it defaults to `cc`. +The Real Libraries Portability workflow runs every maintained example on +Linux x86-64, Linux Arm64, macOS Intel, and macOS Arm64. Within those four +machine jobs, libm runs with GCC and Clang on Linux and with Apple Clang and GNU +GCC on macOS. This exercises the target's own declarations, scalar ABI, C +compiler, linker, and math library instead of reusing a contract generated on +another target. Native Windows/MSVC is outside PRIK's current POSIX C build +lane. + +There are no vendored implementation sources or copied prototypes. The +extension parses and calls the math library supplied by the active platform. diff --git a/examples/libm/__init__.py b/examples/libm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/libm/build_all.sh b/examples/libm/build_all.sh new file mode 100644 index 000000000..f994f2b8e --- /dev/null +++ b/examples/libm/build_all.sh @@ -0,0 +1,5 @@ +if ! source examples/libm/build_prik.sh; then + return 1 2>/dev/null || exit 1 +fi +cd "$EXAMPLE_WORKSPACE" +export PYTHONPATH="$LIBM_BUILD_ROOT/prik${PYTHONPATH:+:$PYTHONPATH}" diff --git a/examples/libm/build_prik.sh b/examples/libm/build_prik.sh new file mode 100644 index 000000000..f4128c79f --- /dev/null +++ b/examples/libm/build_prik.sh @@ -0,0 +1,32 @@ +export EXAMPLE_WORKSPACE="$PWD" +export LIBM_BUILD_ROOT="$(mktemp -d)" + +LIBM_COMPILER="${PRIK_LIBM_CC:-cc}" +if ! LIBM_COMPILER_PATH="$(command -v "$LIBM_COMPILER")"; then + echo "libm example: C compiler not found: $LIBM_COMPILER" >&2 + return 1 2>/dev/null || exit 1 +fi +export LIBM_COMPILER_PATH + +mkdir -p "$LIBM_BUILD_ROOT/prik/contract" "$LIBM_BUILD_ROOT/prik/generated" +cd "$LIBM_BUILD_ROOT/prik" + +if ! python3 -m prik generate --pyi --language c \ + "$EXAMPLE_WORKSPACE/examples/libm/libm_probe.h" \ + --compiler "$LIBM_COMPILER_PATH" \ + --std c99 \ + --include-exposure roots-only \ + --export-symbols "$EXAMPLE_WORKSPACE/examples/libm/iso_c99_routines.txt" \ + --out "$LIBM_BUILD_ROOT/prik/contract/libm_api.pyi"; then + return 1 2>/dev/null || exit 1 +fi + +if ! python3 -m prik --language c "$LIBM_BUILD_ROOT/prik/contract/libm_api.pyi" \ + --out prik_reference_libm \ + --out-dir "$LIBM_BUILD_ROOT/prik/generated" \ + --compiler "$LIBM_COMPILER_PATH" \ + --native-library m \ + --positional-only \ + --collision-adapter-all; then + return 1 2>/dev/null || exit 1 +fi diff --git a/examples/libm/conftest.py b/examples/libm/conftest.py new file mode 100644 index 000000000..5b4671d76 --- /dev/null +++ b/examples/libm/conftest.py @@ -0,0 +1,36 @@ +"""Import fixture for the wrapper produced by ``build_all.sh``.""" + +import ast +import importlib +import os +from pathlib import Path + +import numpy as np +import pytest +from numpy import float64 + +float128 = np.longdouble + +_PUBLIC_REAL_TYPES = { + "Float64": float64, + "Float128": float128, +} + + +@pytest.fixture(scope="session") +def libm(): + """Return the already-built PRIK libm module.""" + return importlib.import_module("prik_reference_libm") + + +@pytest.fixture(scope="session") +def public_long_double_type(): + """Return the public scalar type generated for libm ``long double`` calls.""" + contract = Path(os.environ["LIBM_BUILD_ROOT"]) / "prik/contract/libm_api.pyi" + tree = ast.parse(contract.read_text(encoding="utf-8"), filename=str(contract)) + sinl = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "sinl") + annotation = sinl.args.args[0].annotation + + assert isinstance(annotation, ast.Name) + assert annotation.id in _PUBLIC_REAL_TYPES + return _PUBLIC_REAL_TYPES[annotation.id] diff --git a/examples/libm/iso_c99_routines.txt b/examples/libm/iso_c99_routines.txt new file mode 100644 index 000000000..6907e9730 --- /dev/null +++ b/examples/libm/iso_c99_routines.txt @@ -0,0 +1,75 @@ +# Trigonometric +sin +cos +tan +asin +acos +atan +atan2 + +# Hyperbolic +sinh +cosh +tanh +asinh +acosh +atanh + +# Exponential and logarithmic +exp +exp2 +expm1 +log +log2 +log10 +log1p + +# Power and roots +pow +sqrt +cbrt +hypot + +# Rounding, truncation, and remainder +ceil +floor +trunc +round +nearbyint +rint +lrint +llrint +lround +llround +fmod +remainder + +# Floating-point manipulation +copysign +fabs +fdim +fmax +fmin +fma +ldexp +scalbn +scalbln +nextafter +nexttoward +logb +ilogb + +# Error and gamma functions +erf +erfc +tgamma +lgamma + +# Single and extended precision +sinf +cosf +expf +logf +sqrtf +sinl +sqrtl diff --git a/examples/libm/libm_probe.h b/examples/libm/libm_probe.h new file mode 100644 index 000000000..589d39477 --- /dev/null +++ b/examples/libm/libm_probe.h @@ -0,0 +1,7 @@ +#ifndef PRIK_EXAMPLE_LIBM_PROBE_H +#define PRIK_EXAMPLE_LIBM_PROBE_H + +/* Parse the target toolchain's declarations; selection lives in the name file. */ +#include + +#endif diff --git a/examples/libm/routine_inventory.py b/examples/libm/routine_inventory.py new file mode 100644 index 000000000..934957908 --- /dev/null +++ b/examples/libm/routine_inventory.py @@ -0,0 +1,59 @@ +"""Reviewed public libm surface and its explicit test mapping.""" + +from __future__ import annotations + +ROUTINE_GROUPS: dict[str, tuple[str, ...]] = { + "Trigonometric": ("sin", "cos", "tan", "asin", "acos", "atan", "atan2"), + "Hyperbolic": ("sinh", "cosh", "tanh", "asinh", "acosh", "atanh"), + "Exponential and logarithmic": ("exp", "exp2", "expm1", "log", "log2", "log10", "log1p"), + "Power and roots": ("pow", "sqrt", "cbrt", "hypot"), + "Rounding, truncation, and remainder": ( + "ceil", + "floor", + "trunc", + "round", + "nearbyint", + "rint", + "lrint", + "llrint", + "lround", + "llround", + "fmod", + "remainder", + ), + "Floating-point manipulation": ( + "copysign", + "fabs", + "fdim", + "fmax", + "fmin", + "fma", + "ldexp", + "scalbn", + "scalbln", + "nextafter", + "nexttoward", + "logb", + "ilogb", + ), + "Error and gamma functions": ("erf", "erfc", "tgamma", "lgamma"), + "Single and extended precision": ("sinf", "cosf", "expf", "logf", "sqrtf", "sinl", "sqrtl"), +} + +ALL_ROUTINES = tuple(routine for group in ROUTINE_GROUPS.values() for routine in group) +PRIK_TESTED_ROUTINES = frozenset(ALL_ROUTINES) +UNSUPPORTED_ROUTINES: dict[str, str] = {} +EXPLICIT_TEST_NAMES = { + routine: test_name + for test_name, groups in ( + ( + "test_elementary", + ("Trigonometric", "Hyperbolic", "Exponential and logarithmic", "Power and roots"), + ), + ("test_rounding", ("Rounding, truncation, and remainder", "Floating-point manipulation")), + ("test_special", ("Error and gamma functions",)), + ("test_precision", ("Single and extended precision",)), + ) + for group in groups + for routine in ROUTINE_GROUPS[group] +} diff --git a/examples/libm/tests/__init__.py b/examples/libm/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/libm/tests/test_numerical.py b/examples/libm/tests/test_numerical.py new file mode 100644 index 000000000..5917806f2 --- /dev/null +++ b/examples/libm/tests/test_numerical.py @@ -0,0 +1,172 @@ +"""Grouped numerical evidence for the reviewed ISO C99 libm surface.""" + +from __future__ import annotations + +import math + +import numpy as np +import pytest + +pytestmark = pytest.mark.real_library +DOUBLE_TOLERANCE = 1e-12 +FLOAT32_TOLERANCE = 4 * np.finfo(np.float32).eps + + +def test_elementary(libm): + assert np.isclose(libm.sin(np.float64(1.0)), math.sin(1.0), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.cos(np.float64(1.0)), math.cos(1.0), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.tan(np.float64(0.5)), math.tan(0.5), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.asin(np.float64(0.5)), math.asin(0.5), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.acos(np.float64(0.5)), math.acos(0.5), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.atan(np.float64(0.5)), math.atan(0.5), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose( + libm.atan2(np.float64(1.0), np.float64(2.0)), + math.atan2(1.0, 2.0), + rtol=DOUBLE_TOLERANCE, + atol=DOUBLE_TOLERANCE, + ) + assert np.isclose(libm.sinh(np.float64(0.75)), math.sinh(0.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.cosh(np.float64(0.75)), math.cosh(0.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.tanh(np.float64(0.75)), math.tanh(0.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.asinh(np.float64(0.75)), math.asinh(0.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.acosh(np.float64(1.75)), math.acosh(1.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.atanh(np.float64(0.75)), math.atanh(0.75), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.exp(np.float64(1.0)), math.e, rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + + # exp2 is exact on a whole exponent, so no tolerance is needed. + assert libm.exp2(np.float64(10.0)) == 1024.0 + + # expm1 keeps the precision that exp(x) - 1 loses for small x. + assert np.isclose( + libm.expm1(np.float64(1e-9)), + math.expm1(1e-9), + rtol=DOUBLE_TOLERANCE, + atol=DOUBLE_TOLERANCE, + ) + assert libm.expm1(np.float64(1e-9)) != math.exp(1e-9) - 1.0 + + assert np.isclose(libm.log(np.float64(math.e)), 1.0, rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert libm.log2(np.float64(1024.0)) == 10.0 + assert np.isclose(libm.log10(np.float64(1000.0)), 3.0, rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.log1p(np.float64(1e-9)), math.log1p(1e-9), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert libm.pow(np.float64(2.0), np.float64(10.0)) == 1024.0 + assert libm.sqrt(np.float64(144.0)) == 12.0 + assert np.isclose(libm.cbrt(np.float64(27.0)), 3.0, rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert libm.hypot(np.float64(3.0), np.float64(4.0)) == 5.0 + + +def test_precision(libm, public_long_double_type): + result = libm.sinf(np.float32(1.0)) + assert result.dtype == np.float32 + assert np.isclose(result, np.float32(math.sin(1.0)), rtol=FLOAT32_TOLERANCE, atol=0.0) + + result = libm.cosf(np.float32(1.0)) + assert result.dtype == np.float32 + assert np.isclose(result, np.float32(math.cos(1.0)), rtol=FLOAT32_TOLERANCE, atol=0.0) + + result = libm.expf(np.float32(1.0)) + assert result.dtype == np.float32 + assert np.isclose(result, np.float32(math.exp(1.0)), rtol=FLOAT32_TOLERANCE, atol=0.0) + + result = libm.logf(np.float32(math.e)) + assert result.dtype == np.float32 + assert np.isclose(result, 1.0, rtol=1e-6, atol=1e-6) + + result = libm.sqrtf(np.float32(144.0)) + assert result.dtype == np.float32 + assert result == np.float32(12.0) + + result = libm.sinl(public_long_double_type(1.0)) + assert result.dtype == np.dtype(public_long_double_type) + assert np.isclose(result, math.sin(1.0), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + + result = libm.sqrtl(public_long_double_type(2)) + assert result.dtype == np.dtype(public_long_double_type) + assert np.isclose(result, math.sqrt(2.0), rtol=1e-15, atol=1e-15) + + +def test_rounding(libm, public_long_double_type): + assert libm.ceil(np.float64(2.1)) == 3.0 + assert libm.floor(np.float64(2.9)) == 2.0 + assert libm.trunc(np.float64(-2.9)) == -2.0 + + # C `round` breaks ties away from zero, unlike Python's banker's rounding. + assert libm.round(np.float64(2.5)) == 3.0 + assert libm.round(np.float64(-2.5)) == -3.0 + + # nearbyint and rint follow the active floating-point rounding mode. + assert libm.nearbyint(np.float64(2.5)) == libm.rint(np.float64(2.5)) + assert libm.nearbyint(np.float64(-2.5)) == libm.rint(np.float64(-2.5)) + result = libm.rint(np.float64(2.5)) + assert result in {2.0, 3.0} + assert result == libm.nearbyint(np.float64(2.5)) + + result = libm.lrint(np.float64(2.7)) + assert result == np.long(libm.rint(np.float64(2.7))) + assert result.dtype == np.dtype(np.long) + assert libm.llrint(np.float64(2.7)) == np.int64(libm.rint(np.float64(2.7))) + assert libm.llrint(np.float64(-2.7)) == np.int64(libm.rint(np.float64(-2.7))) + + result = libm.lround(np.float64(2.5)) + assert result == np.long(3) + assert result.dtype == np.dtype(np.long) + assert libm.llround(np.float64(2.5)) == np.int64(3) + assert libm.llround(np.float64(-2.5)) == np.int64(-3) + + assert np.isclose( + libm.fmod(np.float64(10.0), np.float64(3.0)), + math.fmod(10.0, 3.0), + rtol=DOUBLE_TOLERANCE, + atol=DOUBLE_TOLERANCE, + ) + + # IEEE remainder rounds the quotient to nearest, so it differs from fmod. + assert np.isclose( + libm.remainder(np.float64(10.0), np.float64(3.0)), + math.remainder(10.0, 3.0), + rtol=DOUBLE_TOLERANCE, + atol=DOUBLE_TOLERANCE, + ) + assert libm.remainder(np.float64(10.0), np.float64(6.0)) == -2.0 + + assert libm.copysign(np.float64(2.0), np.float64(-0.0)) == -2.0 + assert libm.fabs(np.float64(-2.5)) == 2.5 + assert libm.fdim(np.float64(5.0), np.float64(3.0)) == 2.0 + assert libm.fdim(np.float64(3.0), np.float64(5.0)) == 0.0 + assert libm.fmax(np.float64(2.0), np.float64(3.0)) == 3.0 + assert libm.fmin(np.float64(2.0), np.float64(3.0)) == 2.0 + assert libm.fma(np.float64(2.0), np.float64(3.0), np.float64(4.0)) == 10.0 + + # A single rounding keeps the product bits an unfused expression discards. + left, right = 1.0 + 2.0**-52, 1.0 - 2.0**-52 + assert libm.fma(np.float64(left), np.float64(right), np.float64(-1.0)) == -(2.0**-104) + assert left * right - 1.0 == 0.0 + + assert libm.ldexp(np.float64(1.5), np.intc(3)) == 12.0 + assert libm.scalbn(np.float64(1.5), np.intc(3)) == 12.0 + assert libm.scalbln(np.float64(1.5), np.long(3)) == 12.0 + assert libm.nextafter(np.float64(1.0), np.float64(2.0)) == math.nextafter(1.0, 2.0) + assert libm.nexttoward(np.float64(1.0), public_long_double_type(2.0)) == math.nextafter(1.0, 2.0) + assert libm.logb(np.float64(8.0)) == 3.0 + result = libm.ilogb(np.float64(8.0)) + assert result == np.intc(3) + assert result.dtype == np.dtype(np.intc) + + +def test_special(libm): + assert np.isclose(libm.erf(np.float64(0.5)), math.erf(0.5), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + + # erf and erfc are complements, which checks both without a shared oracle. + assert np.isclose( + libm.erf(np.float64(0.7)) + libm.erfc(np.float64(0.7)), + 1.0, + rtol=DOUBLE_TOLERANCE, + atol=DOUBLE_TOLERANCE, + ) + assert np.isclose(libm.erfc(np.float64(0.5)), math.erfc(0.5), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + + # tgamma(n + 1) is n! for a whole argument. + assert libm.tgamma(np.float64(6.0)) == 120.0 + assert np.isclose(libm.tgamma(np.float64(0.5)), math.sqrt(math.pi), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(libm.lgamma(np.float64(5.0)), math.lgamma(5.0), rtol=DOUBLE_TOLERANCE, atol=DOUBLE_TOLERANCE) + assert np.isclose(math.exp(libm.lgamma(np.float64(6.0))), 120.0, rtol=1e-9, atol=1e-9) diff --git a/examples/libm/tests/test_routine_coverage.py b/examples/libm/tests/test_routine_coverage.py new file mode 100644 index 000000000..c84d862ce --- /dev/null +++ b/examples/libm/tests/test_routine_coverage.py @@ -0,0 +1,78 @@ +"""Fail closed when the reviewed libm surface or its tests drift.""" + +from __future__ import annotations + +import ast +import os +from pathlib import Path + +import pytest + +from ..routine_inventory import ( + ALL_ROUTINES, + EXPLICIT_TEST_NAMES, + PRIK_TESTED_ROUTINES, + ROUTINE_GROUPS, + UNSUPPORTED_ROUTINES, +) + +pytestmark = pytest.mark.real_library +TEST_FILES = tuple(sorted(path for path in Path(__file__).parent.glob("test_*.py") if path != Path(__file__))) + + +def _test_sources() -> dict[str, str]: + """Return the source text of every grouped public-routine test.""" + sources: dict[str, str] = {} + for path in TEST_FILES: + text = path.read_text(encoding="utf-8") + tree = ast.parse(text, filename=str(path)) + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name.startswith("test_"): + segment = ast.get_source_segment(text, node) + assert segment is not None + sources[node.name] = segment + return sources + + +def test_every_reviewed_libm_routine_is_visibly_exercised(): + sources = _test_sources() + assert len(ALL_ROUTINES) == len(set(ALL_ROUTINES)) + assert set(ALL_ROUTINES) == PRIK_TESTED_ROUTINES + assert UNSUPPORTED_ROUTINES == {} + + for routine, test_name in EXPLICIT_TEST_NAMES.items(): + source = sources[test_name] + assert f"libm.{routine}(" in source, f"{test_name} does not visibly invoke {routine}" + + +def test_inventory_groups_cover_each_exported_routine_once(libm): + grouped = tuple(routine for group in ROUTINE_GROUPS.values() for routine in group) + exported = {name for name in dir(libm) if not name.startswith("_")} + + assert grouped == ALL_ROUTINES + assert len(grouped) == len(set(grouped)) + assert exported == set(ALL_ROUTINES) + + +def test_build_generated_the_target_contract_from_the_math_h_allowlist(): + contract = Path(os.environ["LIBM_BUILD_ROOT"]) / "prik/contract/libm_api.pyi" + tree = ast.parse(contract.read_text(encoding="utf-8"), filename=str(contract)) + generated = {node.name for node in tree.body if isinstance(node, ast.FunctionDef)} + + assert generated == set(ALL_ROUTINES) + + +def test_reviewed_export_file_matches_the_inventory(): + export_file = Path(__file__).parents[1] / "iso_c99_routines.txt" + selected = tuple( + line.split("#", 1)[0].strip() + for line in export_file.read_text(encoding="utf-8").splitlines() + if line.split("#", 1)[0].strip() + ) + + assert selected == ALL_ROUTINES + + +def test_built_surface_is_positional_only(libm): + with pytest.raises(TypeError, match="keyword"): + libm.atan2(arg0=1.0, arg1=2.0) diff --git a/examples/native_library.py b/examples/native_library.py index 3cefd1053..f62fb831c 100644 --- a/examples/native_library.py +++ b/examples/native_library.py @@ -18,9 +18,11 @@ EXAMPLES_ROOT = Path(__file__).resolve().parent BLAS_SOURCE_ROOT = EXAMPLES_ROOT / "blas" / "native" LAPACK_SOURCE_ROOT = EXAMPLES_ROOT / "lapack" / "native" +LAPACK_SUPPORT_ROOT = EXAMPLES_ROOT / "lapack" / "support" +LAPACK_XBLAS_SOURCE_LIST = EXAMPLES_ROOT / "lapack" / "xblas_sources.txt" NATIVE_CACHE_ENV = "PRIK_REAL_LIBRARY_NATIVE_CACHE_DIR" NATIVE_JOBS_ENV = "PRIK_REAL_LIBRARY_NATIVE_JOBS" -NATIVE_CACHE_VERSION = "copyable-examples-v3-link-dependencies" +NATIVE_CACHE_VERSION = "copyable-examples-v4-default-lapack-sources" NATIVE_MODULE_SOURCE_STEMS = frozenset({"la_constants", "la_xisnan"}) NATIVE_LINK_DEPENDENCIES = { "blas": (), @@ -40,6 +42,7 @@ class NativeLibrary: archive: Path cache_dir: Path module_dir: Path + wrapper_source_root: Path sources: tuple[Path, ...] compiler: str @@ -64,12 +67,40 @@ def compiler_identity(compiler: str) -> str: return f"{Path(compiler).resolve()}: {first_line}" +def _fortran_sources(root: Path) -> tuple[Path, ...]: + return tuple(sorted(path for path in root.iterdir() if path.is_file() and path.suffix.lower() in FORTRAN_SUFFIXES)) + + def library_sources(library: str) -> tuple[Path, ...]: - """Return the authoritative implementation sources for one named library.""" + """Return the authoritative implementation snapshot for one named library.""" if library not in SUPPORTED_LIBRARIES: raise ValueError(f"unknown reference library {library!r}; choose from {', '.join(SUPPORTED_LIBRARIES)}") root = BLAS_SOURCE_ROOT if library == "blas" else LAPACK_SOURCE_ROOT - return tuple(sorted(path for path in root.iterdir() if path.is_file() and path.suffix.lower() in FORTRAN_SUFFIXES)) + return _fortran_sources(root) + + +def _lapack_xblas_source_names() -> frozenset[str]: + names = tuple( + line + for raw_line in LAPACK_XBLAS_SOURCE_LIST.read_text(encoding="utf-8").splitlines() + if (line := raw_line.strip()) and not line.startswith("#") + ) + if len(names) != len(set(names)): + raise RuntimeError(f"duplicate source names in {LAPACK_XBLAS_SOURCE_LIST}") + available = {source.name for source in library_sources("lapack")} + unknown = sorted(set(names) - available) + if unknown: + raise RuntimeError(f"unknown XBLAS-only LAPACK sources: {', '.join(unknown)}") + return frozenset(names) + + +def wrapper_sources(library: str) -> tuple[Path, ...]: + """Return the source surface compiled and exposed by one example wrapper.""" + sources = library_sources(library) + if library == "blas": + return sources + excluded = _lapack_xblas_source_names() + return tuple(source for source in sources if source.name not in excluded) def native_sources(library: str) -> tuple[Path, ...]: @@ -77,8 +108,8 @@ def native_sources(library: str) -> tuple[Path, ...]: if library not in SUPPORTED_LIBRARIES: return library_sources(library) if library == "blas": - return library_sources("blas") - lapack_sources = library_sources("lapack") + return wrapper_sources("blas") + lapack_sources = wrapper_sources("lapack") module_sources = tuple( source for source in ( @@ -91,7 +122,7 @@ def native_sources(library: str) -> tuple[Path, ...]: lapack_rest = tuple(source for source in lapack_sources if source not in module_source_set) lapack_stems = {source.stem.lower() for source in lapack_sources} blas_dependencies = tuple(source for source in library_sources("blas") if source.stem.lower() not in lapack_stems) - return (*module_sources, *lapack_rest, *blas_dependencies) + return (*module_sources, *lapack_rest, *_fortran_sources(LAPACK_SUPPORT_ROOT), *blas_dependencies) def native_cache_root() -> Path: @@ -247,15 +278,51 @@ def _cached_archive(cache_dir: Path, library: str, objects: tuple[Path, ...], ar return archive +def _cached_wrapper_source_root(cache_dir: Path, sources: tuple[Path, ...]) -> Path: + source_root = cache_dir / "wrapper_sources" + complete = cache_dir / "wrapper_sources.complete" + expected_names = {source.name for source in sources} + if len(expected_names) != len(sources): + raise RuntimeError("wrapper source filenames must be unique") + if ( + complete.is_file() + and source_root.is_dir() + and {path.name for path in source_root.iterdir() if path.is_file()} == expected_names + ): + return source_root + + temporary_root = cache_dir / f"wrapper_sources.{os.getpid()}.tmp" + shutil.rmtree(temporary_root, ignore_errors=True) + temporary_root.mkdir() + for source in sources: + (temporary_root / source.name).symlink_to(source.resolve()) + shutil.rmtree(source_root, ignore_errors=True) + temporary_root.rename(source_root) + complete.write_text(f"{NATIVE_CACHE_VERSION}\n", encoding="utf-8") + return source_root + + def _cached_shared_library(cache_dir: Path, library: str, archive: Path, compiler: str) -> Path: - shared_library = cache_dir / f"libprik_full_{library}.so" + suffix = ".dylib" if sys.platform == "darwin" else ".so" + shared_library = cache_dir / f"libprik_full_{library}{suffix}" complete = cache_dir / "shared.complete" if complete.is_file() and shared_library.is_file(): return shared_library temporary_shared = cache_dir / f"{shared_library.name}.{os.getpid()}.tmp" temporary_shared.unlink(missing_ok=True) - subprocess.run( # nosec B603 - explicit compiler and compiled example archive - ( + if sys.platform == "darwin": + command = ( + compiler, + "-dynamiclib", + "-o", + str(temporary_shared), + f"-Wl,-install_name,{shared_library}", + "-Wl,-force_load", + str(archive), + *NATIVE_LINK_DEPENDENCIES[library], + ) + else: + command = ( compiler, "-shared", "-o", @@ -264,7 +331,9 @@ def _cached_shared_library(cache_dir: Path, library: str, archive: Path, compile str(archive), "-Wl,--no-whole-archive", *NATIVE_LINK_DEPENDENCIES[library], - ), + ) + subprocess.run( # nosec B603 - explicit compiler and compiled example archive + command, check=True, ) os.replace(temporary_shared, shared_library) @@ -283,6 +352,7 @@ def build_reference_library( """Build on a cache miss and return one complete reusable native library.""" selected_compiler = compiler or require_tool("gfortran") selected_archiver = archiver or require_tool("ar") + selected_wrapper_sources = wrapper_sources(library) selected_sources = native_sources(library) selected_jobs = jobs if jobs is not None else native_compile_jobs() if selected_jobs < 1: @@ -290,6 +360,7 @@ def build_reference_library( selected_cache_root = (cache_root or native_cache_root()).resolve() cache_dir = selected_cache_root / f"{library}-{_native_cache_key(library, selected_compiler, selected_sources)}" cache_dir.mkdir(parents=True, exist_ok=True) + wrapper_source_root = _cached_wrapper_source_root(cache_dir, selected_wrapper_sources) objects = _cached_objects(cache_dir, selected_sources, selected_compiler, selected_jobs) archive = _cached_archive(cache_dir, library, objects, selected_archiver) shared_library = _cached_shared_library(cache_dir, library, archive, selected_compiler) @@ -299,6 +370,7 @@ def build_reference_library( archive=archive, cache_dir=cache_dir, module_dir=cache_dir / "modules", + wrapper_source_root=wrapper_source_root, sources=selected_sources, compiler=selected_compiler, ) @@ -307,9 +379,10 @@ def build_reference_library( def linker_name(shared_library: Path) -> str: """Return the `-l` name for a shared library produced by this module.""" name = shared_library.name - if not name.startswith("lib") or ".so" not in name: - raise ValueError(f"expected a lib*.so native library, got {shared_library}") - return name[3 : name.index(".so")] + suffix = next((candidate for candidate in (".so", ".dylib") if name.endswith(candidate)), None) + if not name.startswith("lib") or suffix is None: + raise ValueError(f"expected a lib*.so or lib*.dylib native library, got {shared_library}") + return name[3 : -len(suffix)] def main(argv: Sequence[str] | None = None) -> int: diff --git a/mkdocs.yml b/mkdocs.yml index 23f759880..a362ee70d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: PRIK — Bring Native Code to Python -site_description: PRIK generates native Python bindings from Fortran projects, producing importable extensions and editable .pyi contracts for Pythonic APIs. +site_description: PRIK generates native Python bindings for Fortran and C code. site_url: https://pynumlab.github.io/prik/ repo_url: https://github.com/PyNumLab/prik repo_name: GitHub @@ -67,24 +67,14 @@ nav: - Error Handling & Diagnostics: user/guide/error-handling.md - Building the Shared Library: user/guide/building-shared-library.md - Performance: user/performance.md - - Tutorials: - - Overview: user/tutorials/index.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 - - Packaging: user/tutorials/packaging.md - - Scientific Library: user/tutorials/scientific-library.md - Examples: - Overview: user/examples/index.md - BLAS Wrapper: user/examples/blas-wrapper.md - LAPACK Wrapper: user/examples/lapack-wrapper.md - FFTPACK Wrapper: user/examples/fftpack-wrapper.md - MINPACK Wrapper: user/examples/minpack-wrapper.md - - CFD Mini Example: user/examples/cfd-mini-example.md - - MPI Example: user/examples/mpi-example.md - - Object-Oriented Fortran: user/examples/object-oriented-fortran.md - - ODE Solver: user/examples/ode-solver.md - - OpenMP Example: user/examples/openmp-example.md + - BSPLINE-FORTRAN Wrapper: user/examples/bspline-wrapper.md + - libm Wrapper: user/examples/libm-wrapper.md - Recipes: - Build and Import With the Python API: user/examples/recipes/build-and-import-python-api.md - Inspect a Fortran API: user/examples/recipes/inspect-fortran-api.md @@ -94,12 +84,7 @@ nav: - Use Python Inspection APIs: user/examples/recipes/use-python-inspection-apis.md - Use Compiler Preprocessing Options: user/examples/recipes/compiler-preprocessing.md - Troubleshooting: - - Overview: user/troubleshooting/index.md - - Installation Issues: user/troubleshooting/installation-issues.md - Compiler Issues: user/troubleshooting/compiler-issues.md - - Build Issues: user/troubleshooting/build-issues.md - - Runtime Issues: user/troubleshooting/runtime-issues.md - - Platform-Specific Issues: user/troubleshooting/platform-specific-issues.md - FAQ: user/faq/index.md - Reference: - Overview: user/reference/index.md @@ -120,6 +105,7 @@ nav: - Configuration Files: user/reference/configuration-files.md - Language Support: - Overview: user/language-support/index.md + - C: user/language-support/c-support.md - Feature Matrix: user/language-support/feature-matrix.md - Developer Documentation: - Overview: developer/index.md @@ -153,7 +139,6 @@ nav: # PRIK_C_DOCS: - Deferred C Parser Reference: developer/deferred/c-parser.md - Roadmaps: - Overview: developer/roadmap/index.md - - Native Entrypoint and Adapter Adoption: developer/roadmap/native-entrypoint-adoption-checklist.md - Language-First Test Suite and Fortran Cleanup: developer/roadmap/fortran-test-suite-cleanup-checklist.md - Documentation Content: developer/roadmap/documentation-content-checklist.md - Semantic .pyi Wrapper: developer/roadmap/semantic-pyi-wrapper-checklist.md diff --git a/prik/__init__.py b/prik/__init__.py index 0c6d75ef3..448399c66 100644 --- a/prik/__init__.py +++ b/prik/__init__.py @@ -11,6 +11,7 @@ __version__ = _distribution_version("prik") _BUILD_EXPORTS = { + "build_c_extension", "build_fortran_extension", "build_pyi_extension", "build_pyi_extension_from_manifest", @@ -27,6 +28,7 @@ def __getattr__(name: str): __all__ = ( "__version__", + "build_c_extension", "build_fortran_extension", "build_pyi_extension", "build_pyi_extension_from_manifest", diff --git a/prik/cli.py b/prik/cli.py index 7b71ff83d..f9ebe6253 100644 --- a/prik/cli.py +++ b/prik/cli.py @@ -14,10 +14,10 @@ from prik.parsers.c.cli import attach_preprocessing_recipe, expand_c_paths, format_c_report, parse_c_report from prik.parsers.c.models import CParseError from prik.parsers.c.parser import CParser -from prik.parsers.fortran.cli import _format_report +from prik.parsers.fortran.cli import _format_report, _limit_items from prik.parsers.fortran.models import FortranParseError from prik.parsers.fortran.parser import FortranParser -from prik.semantics.c2ir import c_project_to_semantic_modules +from prik.semantics.c2ir import c_project_to_semantic_modules, select_c_export_functions from prik.semantics.fortran2ir import fortran_file_to_semantic_modules from prik.preprocessing.probes.c_types import ( CStandardTypeProbeError, @@ -27,7 +27,12 @@ FortranTypeProbeReport, probe_fortran_type_expressions_cached, ) -from prik.pipeline.type_mapping_report import c_type_mapping_markdown, fortran_type_mapping_markdown +from prik.pipeline.type_mapping_report import ( + c_type_mapping_report, + expression_probe_markdown, + fortran_type_mapping_report, + type_mapping_markdown, +) from prik.preprocessing import ( PreprocessingConfig, PreprocessingError, @@ -121,7 +126,11 @@ " python3 -m prik parse points.f90 --show-vars --print-limit 50\n" "\n" " C header as JSON:\n" - " python3 -m prik parse path/to/api.h --language c --json\n\n" + " python3 -m prik parse path/to/api.h --language c --json\n" + "\n" + " --json picks the format, --out picks the destination:\n" + " python3 -m prik parse points.f90 --out report.txt\n" + " python3 -m prik parse points.f90 --json --out report.json\n\n" f"{_POINTS_EXAMPLE_HELP}" ) _SEMANTICS_HELP_EPILOG = ( @@ -132,8 +141,15 @@ " C header:\n" " python3 -m prik semantics path/to/api.h --language c\n" "\n" - " Save semantic IR:\n" - " python3 -m prik semantics points.f90 --out semantics.json\n\n" + " Shorten a large human-readable summary:\n" + " python3 -m prik semantics points.f90 --print-limit 20\n" + "\n" + " Complete semantic IR as JSON on standard output:\n" + " python3 -m prik semantics points.f90 --json\n" + "\n" + " --json picks the format, --out picks the destination:\n" + " python3 -m prik semantics points.f90 --out summary.txt\n" + " python3 -m prik semantics points.f90 --json --out semantics.json\n\n" f"{_POINTS_EXAMPLE_HELP}" ) _GENERATE_HELP_EPILOG = ( @@ -150,13 +166,20 @@ ) _PROBE_HELP_EPILOG = ( f"{_HELP_DIVIDER}\n\n" - " Basic target probes:\n" + " Target datatype mapping table:\n" " python3 -m prik probe --language fortran --compiler gfortran-13\n" " python3 -m prik probe --language c --compiler gcc-13\n" "\n" - " Human-readable mapping table:\n" + " Complete measured report as JSON:\n" + " python3 -m prik probe --language fortran --compiler gfortran-13 --json\n" + "\n" + " Measure specific Fortran expressions in either format:\n" " python3 -m prik probe --language fortran --compiler gfortran-13 \\\n" - " --format markdown\n" + ' --expr "selected_real_kind(15,307)"\n' + "\n" + " --json picks the format, --out picks the destination:\n" + " python3 -m prik probe --language c --compiler cc --out types.md\n" + " python3 -m prik probe --language c --compiler cc --json --out types.json\n" "\n" " Probe flags that change default kinds:\n" " python3 -m prik probe --language fortran --compiler gfortran-13 \\\n" @@ -359,10 +382,18 @@ def _parse_report(paths: list[str], preprocessing: PreprocessingConfig | None = return out -def _convert_c_project(project, *, c_standard_type_report: dict[str, object] | None): - if c_standard_type_report is None: - return c_project_to_semantic_modules(project) - return c_project_to_semantic_modules(project, standard_type_report=c_standard_type_report) +def _convert_c_project( + project, + *, + c_standard_type_report: dict[str, object] | None, + export_symbols: tuple[str, ...] | None = None, +): + modules = ( + c_project_to_semantic_modules(project) + if c_standard_type_report is None + else c_project_to_semantic_modules(project, standard_type_report=c_standard_type_report) + ) + return modules if export_symbols is None else select_c_export_functions(modules, export_symbols) def _c_standard_type_report( @@ -408,6 +439,8 @@ class _SemanticPipelineContext: fortran_type_probe_runner: list[str] | None = None fortran_type_probe_cache_dir: str | None = None refresh_fortran_type_probe: bool = False + assume_intent_in_scalars: bool = False + export_symbols: tuple[str, ...] | None = None @dataclass(frozen=True) @@ -441,6 +474,8 @@ def _converted_semantic_files( fortran_type_probe_runner: list[str] | None = None, fortran_type_probe_cache_dir: str | None = None, refresh_fortran_type_probe: bool = False, + assume_intent_in_scalars: bool = False, + export_symbols: tuple[str, ...] | None = None, ) -> list[tuple[Path, list[object]]]: context = _SemanticPipelineContext( paths=paths, @@ -454,6 +489,8 @@ def _converted_semantic_files( fortran_type_probe_runner=fortran_type_probe_runner, fortran_type_probe_cache_dir=fortran_type_probe_cache_dir, refresh_fortran_type_probe=refresh_fortran_type_probe, + assume_intent_in_scalars=assume_intent_in_scalars, + export_symbols=export_symbols, ) pipeline = _SOURCE_SEMANTIC_PIPELINES[language] parsed = pipeline.parser(context) @@ -470,6 +507,8 @@ def _semantic_report( fortran_type_probe_runner: list[str] | None = None, fortran_type_probe_cache_dir: str | None = None, refresh_fortran_type_probe: bool = False, + assume_intent_in_scalars: bool = False, + export_symbols: tuple[str, ...] | None = None, ) -> dict[str, dict]: preprocessing = preprocessing or PreprocessingConfig() converted_files = _converted_semantic_files( @@ -481,6 +520,8 @@ def _semantic_report( fortran_type_probe_runner=fortran_type_probe_runner, fortran_type_probe_cache_dir=fortran_type_probe_cache_dir, refresh_fortran_type_probe=refresh_fortran_type_probe, + assume_intent_in_scalars=assume_intent_in_scalars, + export_symbols=export_symbols, ) return _semantic_payload_for_converted_files(converted_files) @@ -525,7 +566,11 @@ def _convert_c_semantic_sources( c_standard_type_report = _c_standard_type_report(context.preprocessing) modules_by_source = { module.origin.native_name: [module] - for module in _convert_c_project(parsed_sources.parsed, c_standard_type_report=c_standard_type_report) + for module in _convert_c_project( + parsed_sources.parsed, + c_standard_type_report=c_standard_type_report, + export_symbols=context.export_symbols, + ) } return [(path, modules_by_source[str(path)]) for path in parsed_sources.source_paths] @@ -567,6 +612,7 @@ def _convert_fortran_semantic_sources( standalone_module_name=p.stem, compile_time_values=compile_time_values, wrapped_derived_types=wrapped_derived_types, + assume_intent_in_scalars=context.assume_intent_in_scalars, **({"type_facts": type_facts} if type_facts is not None else {}), ) converted_files.append((p, modules)) @@ -587,6 +633,7 @@ def _convert_fortran_semantic_sources( def _semantic_payload_for_converted_files(converted_files) -> dict[str, dict]: from prik.pipeline.pyi import emit_module_stubs + from prik.printers import emit_module out: dict[str, dict] = {} available_modules = [module for _p, modules in converted_files for module in modules] @@ -595,6 +642,17 @@ def _semantic_payload_for_converted_files(converted_files) -> dict[str, dict]: if _is_fortran_semantic_file(modules): out[str(p)] = _fortran_contract_payload(Path(p), modules, available_modules) continue + if _is_c_semantic_file(modules): + # A generated C starter contract preserves raw source facts, even + # for a form that the direct-only wrapper policy will later block. + # ``--pyi`` is contract extraction, not wrapper planning. + module_stubs = {module.name: emit_module(module).strip() for module in modules} + out[str(p)] = { + "semantic_modules": [asdict(module) for module in modules], + "pyi": "\n\n".join(module_stubs.values()).strip(), + "pyi_modules": module_stubs, + } + continue stubs = emit_module_stubs(modules, available_modules=available_modules) module_stubs = {module.name: stubs[module.name] for module in modules} out[str(p)] = { @@ -612,6 +670,11 @@ def _is_fortran_semantic_file(modules) -> bool: return any(getattr(getattr(module, "origin", None), "source_language", None) == "fortran" for module in modules) +def _is_c_semantic_file(modules) -> bool: + """Return whether modules came from C source contract extraction.""" + return any(getattr(getattr(module, "origin", None), "source_language", None) == "c" for module in modules) + + def _fortran_contract_payload(path: Path, modules, available_modules) -> dict[str, object]: from prik.pipeline.pyi import emit_module_stubs @@ -839,6 +902,10 @@ def _path_is_fortran_source(path: str) -> bool: return Path(path).suffix.lower() in _FORTRAN_SOURCE_SUFFIXES +def _path_is_c_source(path: str) -> bool: + return Path(path).suffix.lower() == ".c" + + def _path_is_pyi_contract(path: str) -> bool: return Path(path).suffix.lower() == ".pyi" @@ -859,7 +926,9 @@ def _native_link_options_used(args: argparse.Namespace) -> bool: return bool( getattr(args, "no_compile_input_sources", False) or getattr(args, "native_fortran_sources", None) + or getattr(args, "native_c_sources", None) or getattr(args, "native_compile_flags", None) + or getattr(args, "native_c_compile_flags", None) or getattr(args, "native_objects", None) or getattr(args, "native_libraries", None) or getattr(args, "native_link_items", None) @@ -901,14 +970,25 @@ def _validate_pyi_wrapper_options(args: argparse.Namespace, parser: argparse.Arg parser.error("A .pyi wrapper build accepts exactly one entry contract") if getattr(args, "no_compile_input_sources", False): parser.error("--no-compile-input-sources applies only to source-driven wrapper builds") + if getattr(args, "assume_intent_in_scalars", False): + parser.error( + "--assume-intent-in-scalars interprets a missing Fortran intent; a semantic .pyi contract " + "already states its own results, so edit the contract instead" + ) + if getattr(args, "export_symbols", None): + parser.error( + "--export-symbols selects declarations while reading C source; a semantic .pyi contract " + "already states its public functions" + ) if not ( getattr(args, "native_fortran_sources", None) + or getattr(args, "native_c_sources", None) or getattr(args, "native_objects", None) or getattr(args, "native_libraries", None) or getattr(args, "native_link_items", None) ): parser.error( - "A .pyi wrapper build requires --native-fortran-sources, --native-objects, " + "A .pyi wrapper build requires --native-fortran-sources, --native-c-sources, --native-objects, " "--native-library, or --native-link-item" ) @@ -934,27 +1014,42 @@ def _validate_manifest_wrapper_options(args: argparse.Namespace, parser: argpars ) if _native_link_options_used(args): parser.error("--build-manifest replays saved native inputs; do not pass native build flags") - if getattr(args, "strict_wrapper_names", False) or _wrapper_compile_options_used(args): + if ( + getattr(args, "strict_wrapper_names", False) + or getattr(args, "assume_intent_in_scalars", False) + or getattr(args, "export_symbols", None) + or _wrapper_compile_options_used(args) + ): parser.error("--build-manifest replays saved wrapper behavior and compiler flags") def _validate_source_wrapper_options(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None: + language = args.language + label = "C" if language == "c" else "Fortran" + source_check = _path_is_c_source if language == "c" else _path_is_fortran_source if not args.paths: - parser.error("A wrapper build expects at least one Fortran source, source directory, or semantic .pyi contract") - unsupported = [path for path in args.paths if not Path(path).is_dir() and not _path_is_fortran_source(path)] + parser.error( + f"A wrapper build expects at least one {label} source, source directory, or semantic .pyi contract" + ) + unsupported = [path for path in args.paths if not Path(path).is_dir() and not source_check(path)] if unsupported: parser.error( - "A wrapper build expects recognized Fortran source suffixes or one semantic .pyi contract; " + f"A wrapper build expects recognized {label} source suffixes or one semantic .pyi contract; " f"unsupported input: {unsupported[0]}" ) - empty_directories = [path for path in args.paths if Path(path).is_dir() and not _collect_extensions(Path(path))] + collect = (lambda path: sorted(path.rglob("*.c"))) if language == "c" else _collect_extensions + empty_directories = [path for path in args.paths if Path(path).is_dir() and not collect(Path(path))] if empty_directories: - parser.error(f"A wrapper build found no recognized Fortran sources under: {empty_directories[0]}") + parser.error(f"A wrapper build found no recognized {label} sources under: {empty_directories[0]}") if not getattr(args, "no_compile_input_sources", False): return - if not (getattr(args, "native_fortran_sources", None) or _prebuilt_native_link_input_used(args)): + if not ( + getattr(args, "native_fortran_sources", None) + or getattr(args, "native_c_sources", None) + or _prebuilt_native_link_input_used(args) + ): parser.error( - "--no-compile-input-sources requires --native-fortran-sources, --native-objects, " + "--no-compile-input-sources requires --native-fortran-sources, --native-c-sources, --native-objects, " "--native-library, or --native-link-item" ) @@ -974,8 +1069,6 @@ def _validate_wrapper_out(args: argparse.Namespace, parser: argparse.ArgumentPar def _validate_wrapper_build_options(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None: if not _is_wrapper_build(args): return - if args.language != "fortran": - parser.error("Compiled wrappers and generate --sources/--makefile currently require --language fortran") if args.command == "generate" and args.out is not None: parser.error("generate --sources/--makefile uses --out-dir, not --out") if args.command == "build": @@ -994,13 +1087,59 @@ def _validate_wrapper_build_options(args: argparse.Namespace, parser: argparse.A def _validate_c_main_options(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None: if args.language != "c": + if getattr(args, "export_symbols", None): + parser.error("--export-symbols is supported only with --language c") return - if args.command == "build": - parser.error("C input supports parse, semantics, and generate --pyi; compiled C wrappers are not implemented") if args.command == "parse" and args.show_vars: parser.error("--show-vars is Fortran-only and is not supported for --language c") +def _read_c_export_symbols(path: str | Path) -> tuple[str, ...]: + """Read one fail-closed C function allowlist from a UTF-8 text file.""" + source = Path(path) + try: + lines = source.read_text(encoding="utf-8").splitlines() + except (OSError, UnicodeError) as exc: + raise ValueError(f"Cannot read --export-symbols file {source}: {exc}") from exc + + symbols = [] + locations: dict[str, int] = {} + for line_number, raw_line in enumerate(lines, start=1): + symbol = raw_line.split("#", 1)[0].strip() + if not symbol: + continue + valid = ( + symbol.isascii() + and (symbol[0].isalpha() or symbol[0] == "_") + and all(character.isalnum() or character == "_" for character in symbol) + ) + if not valid: + raise ValueError(f"Invalid C identifier in --export-symbols file {source}:{line_number}: {symbol!r}") + previous = locations.get(symbol) + if previous is not None: + raise ValueError( + f"Repeated C function name in --export-symbols file {source}:{line_number}: " + f"{symbol!r} first appeared on line {previous}" + ) + locations[symbol] = line_number + symbols.append(symbol) + if not symbols: + raise ValueError(f"--export-symbols file contains no C function names: {source}") + return tuple(symbols) + + +def _complete_c_export_symbol_options(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None: + """Resolve the CLI file once for every downstream semantic/build path.""" + path = getattr(args, "export_symbols", None) + args._resolved_export_symbols = None + if path is None: + return + try: + args._resolved_export_symbols = _read_c_export_symbols(path) + except ValueError as exc: + parser.error(str(exc)) + + def _validate_output_options(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None: if args.print_limit is not None and args.print_limit < 0: parser.error("--print-limit must be >= 0") @@ -1022,15 +1161,42 @@ def _validate_pyi_generation_options(args: argparse.Namespace, parser: argparse. parser.error(f"generate --pyi cannot use {', '.join(invalid)}") +def _validate_semantic_stage_source_inputs(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None: + """Require source-stage commands to receive at least one source file. + + Wrapper builds separately accept a semantic ``.pyi`` contract. The + ``semantics`` and ``generate --pyi`` commands instead create their output + from native source, so filtering a contract out of their source list must + be a diagnostic rather than an empty report. + """ + if not (args.semantics or args.pyi): + return + + source_suffixes = _SOURCE_SUFFIXES_BY_LANGUAGE[args.language] + unsupported = tuple( + Path(raw) for raw in args.paths if not Path(raw).is_dir() and Path(raw).suffix.lower() not in source_suffixes + ) + command = "semantics" if args.semantics else "generate --pyi" + if unsupported: + parser.error( + f"{command} expects recognized {args.language} source suffixes; unsupported input: {unsupported[0]}" + ) + + if not _source_paths_for_semantic_pipeline(args.paths, language=args.language): + parser.error(f"{command} found no recognized {args.language} sources in the supplied inputs") + + def _validate_main_options(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int | None: if not args.paths and getattr(args, "build_manifest", None) is None: parser.error("Source input is required unless --build-manifest is used") _validate_pyi_generation_options(args, parser) + _validate_semantic_stage_source_inputs(args, parser) _validate_wrapper_build_options(args, parser) _validate_c_main_options(args, parser) _validate_output_options(args, parser) + _complete_c_export_symbol_options(args, parser) return args.print_limit @@ -1048,6 +1214,10 @@ def _semantic_stage_options( options: dict[str, object] = {"language": args.language} if c_standard_type_report is not None: options["c_standard_type_report"] = c_standard_type_report + if getattr(args, "assume_intent_in_scalars", False): + options["assume_intent_in_scalars"] = True + if getattr(args, "_resolved_export_symbols", None) is not None: + options["export_symbols"] = args._resolved_export_symbols return options @@ -1099,6 +1269,10 @@ def _cli_native_compile_flags(raw_flags: list[str] | None) -> tuple[str, ...]: return _cli_compiler_flags(raw_flags, option_name="--native-compile-flags") +def _cli_native_c_compile_flags(raw_flags: list[str] | None) -> tuple[str, ...]: + return _cli_compiler_flags(raw_flags, option_name="--native-c-compile-flags") + + def _positive_compile_jobs(value: str) -> int: try: jobs = int(value) @@ -1122,6 +1296,17 @@ def _cli_wrapper_c_flags(raw_flags: list[str] | None) -> tuple[str, ...]: return _cli_compiler_flags(raw_flags, option_name="--wrapper-c-flags") +def _with_link_time_optimization(flags: tuple[str, ...], args) -> tuple[str, ...]: + """Append ``-flto`` when the build asked for link-time optimization. + + Requested flags follow the compiler profile, so this adds LTO without + replacing the selected optimization profile. + """ + if not getattr(args, "lto", False) or "-flto" in flags: + return flags + return (*flags, "-flto") + + def _wrapper_shared_library_alias_path(result, raw_out: str | None) -> Path: if raw_out in (None, ""): return Path.cwd() / f"{result.module_name}.so" @@ -1226,7 +1411,12 @@ def _run_stage_reports_with_diagnostics(args: argparse.Namespace, preprocessing: def _run_wrap_build(args: argparse.Namespace, preprocessing: PreprocessingConfig): - from prik.pipeline.build import build_fortran_extension, build_pyi_extension, build_pyi_extension_from_manifest + from prik.pipeline.build import ( + build_c_extension, + build_fortran_extension, + build_pyi_extension, + build_pyi_extension_from_manifest, + ) def record_total_build_time(elapsed: float) -> None: args._verbose_total_build_time = elapsed @@ -1237,6 +1427,7 @@ def record_total_build_time(elapsed: float) -> None: args.build_manifest, output_name=_wrapper_output_name(args), input_compiler=getattr(args, "compiler", None), + input_c_compiler=getattr(args, "compiler", None), include_dirs=getattr(args, "include_dirs", None), makefile=getattr(args, "makefile", False), generate_sources=getattr(args, "generate_sources", False), @@ -1250,8 +1441,16 @@ def record_total_build_time(elapsed: float) -> None: result = build_pyi_extension( args.paths[0], input_compiler=preprocessing.compiler or "gfortran", + input_c_compiler=(preprocessing.compiler or "cc") if args.language == "c" else "cc", + native_language=args.language, native_fortran_sources=getattr(args, "native_fortran_sources", None), - native_fortran_flags=_cli_native_compile_flags(getattr(args, "native_compile_flags", None)), + native_fortran_flags=_with_link_time_optimization( + _cli_native_compile_flags(getattr(args, "native_compile_flags", None)), args + ), + native_c_sources=getattr(args, "native_c_sources", None), + native_c_flags=_with_link_time_optimization( + _cli_native_c_compile_flags(getattr(args, "native_c_compile_flags", None)), args + ), native_objects=getattr(args, "native_objects", None), native_libraries=_cli_native_libraries(getattr(args, "native_libraries", None)), native_link_items=_cli_native_link_items(getattr(args, "native_link_items", None)), @@ -1260,13 +1459,61 @@ def record_total_build_time(elapsed: float) -> None: output_name=_wrapper_output_name(args), output_dir=getattr(args, "out_dir", None), strict_wrapper_names=getattr(args, "strict_wrapper_names", False), + collision_adapters=getattr(args, "collision_adapters", None), + collision_adapter_all=getattr(args, "collision_adapter_all", False), + positional_only=getattr(args, "positional_only", False), makefile=getattr(args, "makefile", False), generate_sources=getattr(args, "generate_sources", False), jobs=getattr(args, "jobs", None), verbose=1 if getattr(args, "verbose", False) else 0, wrapper_compiler_debug=getattr(args, "wrapper_compiler_debug", False), - wrapper_fortran_flags=_cli_wrapper_fortran_flags(getattr(args, "wrapper_fortran_flags", None)), - wrapper_c_flags=_cli_wrapper_c_flags(getattr(args, "wrapper_c_flags", None)), + wrapper_fortran_flags=_with_link_time_optimization( + _cli_wrapper_fortran_flags(getattr(args, "wrapper_fortran_flags", None)), args + ), + wrapper_c_flags=_with_link_time_optimization( + _cli_wrapper_c_flags(getattr(args, "wrapper_c_flags", None)), args + ), + _on_total_build_time=total_build_time_reporter, + ) + return _copy_wrapper_shared_library_alias(args, result) + + if args.language == "c": + result = build_c_extension( + args.paths, + output_dir=getattr(args, "out_dir", None), + output_name=_wrapper_output_name(args), + input_c_compiler=preprocessing.compiler or "cc", + preprocessing=preprocessing, + export_symbols=getattr(args, "_resolved_export_symbols", None), + input_compiler="gfortran", + native_c_sources=getattr(args, "native_c_sources", None), + native_c_flags=_with_link_time_optimization( + _cli_native_c_compile_flags(getattr(args, "native_c_compile_flags", None)), args + ), + native_fortran_sources=getattr(args, "native_fortran_sources", None), + native_fortran_flags=_with_link_time_optimization( + _cli_native_compile_flags(getattr(args, "native_compile_flags", None)), args + ), + native_objects=getattr(args, "native_objects", None), + native_libraries=_cli_native_libraries(getattr(args, "native_libraries", None)), + native_link_items=_cli_native_link_items(getattr(args, "native_link_items", None)), + native_library_dirs=getattr(args, "native_library_dirs", None), + native_include_dirs=_cli_build_include_dirs(args), + strict_wrapper_names=getattr(args, "strict_wrapper_names", False), + collision_adapters=getattr(args, "collision_adapters", None), + collision_adapter_all=getattr(args, "collision_adapter_all", False), + positional_only=getattr(args, "positional_only", False), + makefile=getattr(args, "makefile", False), + generate_sources=getattr(args, "generate_sources", False), + jobs=getattr(args, "jobs", None), + verbose=1 if getattr(args, "verbose", False) else 0, + wrapper_compiler_debug=getattr(args, "wrapper_compiler_debug", False), + wrapper_fortran_flags=_with_link_time_optimization( + _cli_wrapper_fortran_flags(getattr(args, "wrapper_fortran_flags", None)), args + ), + wrapper_c_flags=_with_link_time_optimization( + _cli_wrapper_c_flags(getattr(args, "wrapper_c_flags", None)), args + ), _on_total_build_time=total_build_time_reporter, ) return _copy_wrapper_shared_library_alias(args, result) @@ -1277,9 +1524,19 @@ def record_total_build_time(elapsed: float) -> None: output_name=_wrapper_output_name(args), preprocessing=preprocessing, strict_wrapper_names=getattr(args, "strict_wrapper_names", False), + collision_adapters=getattr(args, "collision_adapters", None), + collision_adapter_all=getattr(args, "collision_adapter_all", False), + positional_only=getattr(args, "positional_only", False), + assume_intent_in_scalars=getattr(args, "assume_intent_in_scalars", False), compile_input_sources=not getattr(args, "no_compile_input_sources", False), native_fortran_sources=getattr(args, "native_fortran_sources", None), - native_fortran_flags=_cli_native_compile_flags(getattr(args, "native_compile_flags", None)), + native_fortran_flags=_with_link_time_optimization( + _cli_native_compile_flags(getattr(args, "native_compile_flags", None)), args + ), + native_c_sources=getattr(args, "native_c_sources", None), + native_c_flags=_with_link_time_optimization( + _cli_native_c_compile_flags(getattr(args, "native_c_compile_flags", None)), args + ), native_objects=getattr(args, "native_objects", None), native_libraries=_cli_native_libraries(getattr(args, "native_libraries", None)), native_link_items=_cli_native_link_items(getattr(args, "native_link_items", None)), @@ -1290,8 +1547,12 @@ def record_total_build_time(elapsed: float) -> None: jobs=getattr(args, "jobs", None), verbose=1 if getattr(args, "verbose", False) else 0, wrapper_compiler_debug=getattr(args, "wrapper_compiler_debug", False), - wrapper_fortran_flags=_cli_wrapper_fortran_flags(getattr(args, "wrapper_fortran_flags", None)), - wrapper_c_flags=_cli_wrapper_c_flags(getattr(args, "wrapper_c_flags", None)), + wrapper_fortran_flags=_with_link_time_optimization( + _cli_wrapper_fortran_flags(getattr(args, "wrapper_fortran_flags", None)), args + ), + wrapper_c_flags=_with_link_time_optimization( + _cli_wrapper_c_flags(getattr(args, "wrapper_c_flags", None)), args + ), _on_total_build_time=total_build_time_reporter, ) return _copy_wrapper_shared_library_alias(args, result) @@ -1324,6 +1585,116 @@ def _run_wrap_build_with_diagnostics(args: argparse.Namespace, preprocessing: Pr return None +def _semantic_rank_text(rank: int) -> str: + """Render an argument rank as an index suffix, or nothing for a scalar.""" + return f"[{','.join([':'] * rank)}]" if rank > 0 else "" + + +def _semantic_argument_text(argument: dict) -> str: + """Render one completed semantic argument for the human report. + + The mode reflects the policy decision the wrapper will implement, not the + declared Fortran intent, and ownership appears only when it is not the + ordinary borrowed case. + """ + semantic_type = argument.get("semantic_type") or {} + ownership = semantic_type.get("ownership") or {} + dtype = semantic_type.get("dtype") or semantic_type.get("name") or "?" + parts = [f"{dtype}{_semantic_rank_text(int(semantic_type.get('rank') or 0))}"] + if ownership.get("ownership") and ownership["ownership"] != "borrowed": + parts.append(str(ownership["ownership"])) + parts.append("inout" if ownership.get("mutable") else "in") + if argument.get("optional"): + parts.append("optional") + return f"{argument.get('name', '?')}: {' '.join(parts)}" + + +def _semantic_function_line(function: dict) -> str: + """Render one semantic function signature line.""" + arguments = ", ".join(_semantic_argument_text(item) for item in function.get("arguments") or []) + return_type = function.get("return_type") or {} + result = f" -> {return_type.get('dtype') or return_type.get('name')}" if return_type else "" + return f" - {function.get('name', '?')}({arguments}){result}" + + +def _semantic_module_lines(module: dict, print_limit: int | None) -> list[str]: + """Render one semantic module block with its functions and classes.""" + functions = module.get("functions") or [] + classes = module.get("classes") or [] + variables = module.get("variables") or [] + lines = [ + f" - module {module.get('name', '?')} " + f"(functions={len(functions)}, classes={len(classes)}, variables={len(variables)})" + ] + if functions: + lines.append(f" Functions: {len(functions)}") + visible, hidden = _limit_items(functions, print_limit) + lines.extend(_semantic_function_line(function) for function in visible) + if hidden > 0: + lines.append(f" ... {hidden} more functions") + if classes: + lines.append(f" Classes: {len(classes)}") + visible, hidden = _limit_items(classes, print_limit) + for item in visible: + fields = len(item.get("fields") or []) + methods = len(item.get("methods") or []) + lines.append(f" - class {item.get('name', '?')} (fields={fields}, methods={methods})") + if hidden > 0: + lines.append(f" ... {hidden} more classes") + return lines + + +def _format_semantic_report(semantic_report: dict[str, dict], *, print_limit: int | None = None) -> str: + """Format the per-file semantic IR report as a stable, human-readable tree. + + This is the default ``semantics`` rendering; ``--json`` remains the + complete record. Each argument shows its semantic dtype, rank, ownership, + and mutability, which are the policy decisions a parse report cannot show. + """ + lines: list[str] = [] + for fname, payload in semantic_report.items(): + lines.append(f"File: {fname}") + modules = payload.get("semantic_modules") or [] + lines.append(f" Semantic modules: {len(modules)}") + visible, hidden = _limit_items(modules, print_limit) + for module in visible: + lines.extend(_semantic_module_lines(module, print_limit)) + if hidden > 0: + lines.append(f" ... {hidden} more modules") + lines.append("") + return "\n".join(lines).rstrip() + + +def _format_main_report( + args: argparse.Namespace, + payload: dict, + parse_payload: dict[str, dict] | None, + semantic_payload: dict[str, dict] | None, + print_limit: int | None, +) -> str: + """Render the active stage selection in the requested format. + + ``--json`` selects the complete record for every stage; otherwise each + stage renders its own human-readable report. The result is identical + whether it is printed or written with ``--out``. + """ + if args.json: + return json.dumps(payload, indent=2) + if args.pyi: + return _format_pyi_report(semantic_payload or {}) + if args.semantics: + return _format_semantic_report(semantic_payload or {}, print_limit=print_limit) + if args.parse: + if args.language == "c": + return format_c_report(parse_payload or {}, print_limit=print_limit) + return _format_report( + parse_payload or {}, + show_vars=args.show_vars or args.vars_limit is not None, + print_limit=print_limit, + ) + return json.dumps(payload, indent=2) + + def _select_main_payload(args: argparse.Namespace, parse_payload, semantic_payload): if args.parse: return parse_payload or {} @@ -1451,36 +1822,54 @@ def _write_json_output(args: argparse.Namespace, payload: dict) -> None: Path(fname).with_suffix(".json").write_text(json.dumps({fname: report}, indent=2), encoding="utf-8") +def _write_text_output( + args: argparse.Namespace, + payload: dict, + parse_payload: dict[str, dict] | None, + semantic_payload: dict[str, dict] | None, + print_limit: int | None, +) -> None: + """Write the human-readable report to ``--out``. + + With a path the whole report is written there; with no path each input + source receives a sibling ``.txt`` file holding only its own report. + """ + if args.out: + text = _format_main_report(args, payload, parse_payload, semantic_payload, print_limit) + Path(args.out).write_text(text + "\n", encoding="utf-8") + return + for fname, report in payload.items(): + one_file = {fname: report} + text = _format_main_report(args, one_file, one_file, one_file, print_limit) + Path(fname).with_suffix(".txt").write_text(text + "\n", encoding="utf-8") + + def _write_main_output( args: argparse.Namespace, parser: argparse.ArgumentParser, payload: dict, + parse_payload: dict[str, dict] | None, semantic_payload: dict[str, dict] | None, + print_limit: int | None, ) -> bool: + """Write the selected format to ``--out``, or report that stdout owns it. + + ``--out`` chooses only the destination: the rendered content is whatever + ``--json`` and the active stage already selected. + """ if args.out is None: return False if args.json and args.pyi: parser.error("--out cannot be used with both --json and --pyi") if args.pyi: _write_pyi_output(args, semantic_payload or {}) - else: + elif args.json: _write_json_output(args, payload) + else: + _write_text_output(args, payload, parse_payload, semantic_payload, print_limit) return True -def _print_parse_output(args: argparse.Namespace, parse_payload: dict, print_limit: int | None) -> None: - if args.language == "c": - print(format_c_report(parse_payload, print_limit=print_limit)) - return - print( - _format_report( - parse_payload, - show_vars=args.show_vars or args.vars_limit is not None, - print_limit=print_limit, - ) - ) - - def _print_main_output( args: argparse.Namespace, payload: dict, @@ -1488,12 +1877,11 @@ def _print_main_output( semantic_payload: dict[str, dict] | None, print_limit: int | None, ) -> None: + text = _format_main_report(args, payload, parse_payload, semantic_payload, print_limit) if args.pyi and not args.json: - print_pyi_output(_format_pyi_report(semantic_payload or {})) - elif args.parse and not (args.semantics or args.json or args.pyi): - _print_parse_output(args, parse_payload or {}, print_limit) - else: - print(json.dumps(payload, indent=2)) + print_pyi_output(text) + return + print(text) def _print_wrap_build_output(args: argparse.Namespace, result) -> None: @@ -1754,6 +2142,32 @@ def _add_include_exposure_options( ) +def _add_semantic_interpretation_options( + parser: argparse.ArgumentParser, + *, + group_title: str = "semantic interpretation options", +) -> None: + """Add options that change how source facts are read into semantic IR. + + These belong to every command that produces semantic IR, because they + change the IR itself rather than a later wrapper or build choice. + """ + group = parser.add_argument_group(group_title) + group.add_argument( + "--assume-intent-in-scalars", + action="store_true", + help=( + "Treat a primitive scalar dummy that declares no intent as intent(in) instead of the " + "conservative intent(inout) default, so its value is not returned; a declared intent always wins" + ), + ) + group.add_argument( + "--export-symbols", + metavar="FILE", + help="Select exact reachable C functions from a UTF-8 name file; C semantic commands only", + ) + + def _add_wrapper_behavior_options( parser: argparse.ArgumentParser, *, @@ -1804,7 +2218,7 @@ def _add_native_compilation_options(group: argparse._ArgumentGroup) -> None: group.add_argument( "--no-compile-input-sources", action="store_true", - help="Read positional Fortran sources without compiling them; require an explicit native implementation", + help="Read positional sources without compiling them; require an explicit native implementation", ) group.add_argument( "--native-fortran-sources", @@ -1814,6 +2228,14 @@ def _add_native_compilation_options(group: argparse._ArgumentGroup) -> None: metavar="PATH", help="Additional Fortran sources to compile without exposing them in the Python API", ) + group.add_argument( + "--native-c-sources", + dest="native_c_sources", + action="extend", + nargs="+", + metavar="PATH", + help="Additional C sources to compile without exposing them in the Python API", + ) group.add_argument( "--native-compile-flags", dest="native_compile_flags", @@ -1822,6 +2244,14 @@ def _add_native_compilation_options(group: argparse._ArgumentGroup) -> None: metavar="FLAG", help='Native compiler flags (for example, "-O3 -fopenmp")', ) + group.add_argument( + "--native-c-compile-flags", + dest="native_c_compile_flags", + action="extend", + nargs="+", + metavar="FLAG", + help='C implementation compiler flags (for example, "-O3 -std=c11")', + ) def _add_extension_link_options(group: argparse._ArgumentGroup) -> None: @@ -1857,6 +2287,29 @@ def _add_extension_link_options(group: argparse._ArgumentGroup) -> None: metavar="DIR", help="Library search and runtime directories", ) + group.add_argument( + "--lto", + action="store_true", + help="Add -flto to generated and native compilation and to the extension link", + ) + group.add_argument( + "--collision-adapter", + dest="collision_adapters", + action="extend", + nargs="+", + metavar="NAME", + help="Call native symbol NAME through a forwarder defined outside the binding unit", + ) + group.add_argument( + "--collision-adapter-all", + action="store_true", + help="Call every direct C symbol through a forwarder, not only selected names", + ) + group.add_argument( + "--positional-only", + action="store_true", + help="Expose wrappers whose arguments are all required as positional-only arg0..argN", + ) def _add_output_options( @@ -1904,13 +2357,20 @@ def _add_diagnostic_controls(group: argparse._ArgumentGroup, *, allow_verbose: b "build_manifest": None, "no_compile_input_sources": False, "native_fortran_sources": None, + "native_c_sources": None, "native_compile_flags": None, + "native_c_compile_flags": None, "jobs": None, "native_objects": None, "native_libraries": None, "native_link_items": None, "native_library_dirs": None, "strict_wrapper_names": False, + "lto": False, + "collision_adapters": None, + "collision_adapter_all": False, + "positional_only": False, + "assume_intent_in_scalars": False, "wrapper_compiler_debug": False, "wrapper_fortran_flags": None, "wrapper_c_flags": None, @@ -1924,6 +2384,7 @@ def _add_diagnostic_controls(group: argparse._ArgumentGroup, *, allow_verbose: b "compile_commands": None, "public_includes": None, "private_includes": None, + "export_symbols": None, } @@ -1937,13 +2398,13 @@ def _add_build_arguments(parser: argparse.ArgumentParser) -> None: _add_paths( positional_group, metavar="INPUT", - help_text="Fortran source file(s), one source directory, or exactly one semantic .pyi contract", + help_text="Fortran or C source file(s), one source directory, or exactly one semantic .pyi contract", ) input_group = parser.add_argument_group("input selection") _add_language_option( input_group, - choices=("fortran",), - help_text="Input language (default: fortran)", + choices=("fortran", "c"), + help_text="Input language (default: fortran; use c for direct C wrappers)", ) parser.set_defaults(language="fortran") _add_build_manifest_option(input_group) @@ -1961,11 +2422,12 @@ def _add_build_arguments(parser: argparse.ArgumentParser) -> None: _add_preprocessing_options( parser, - languages=("fortran",), + languages=("fortran", "c"), group_title="compiler options", - compiler_help="Compiler used throughout the extension build (default: gfortran)", + compiler_help="Fortran or C compiler used throughout the extension build (default: gfortran or cc)", include_help="Add a compiler include search directory; repeat as needed", ) + _add_semantic_interpretation_options(parser) _add_wrapper_behavior_options(parser, group_title="wrapper options") native_group = parser.add_argument_group("native options") _add_native_compilation_options(native_group) @@ -2042,6 +2504,43 @@ def _add_top_level_arguments(parser: argparse.ArgumentParser) -> None: metavar="NAME", help=("Link against NAME; for example, --native-library openblas passes -lopenblas to the linker"), ) + build_group.add_argument( + "--lto", + action="store_true", + help=( + "Add -flto to generated and native compilation and to the extension link, " + "so a collision adapter can be inlined away" + ), + ) + build_group.add_argument( + "--collision-adapter", + dest="collision_adapters", + action="extend", + nargs="+", + metavar="NAME", + help=( + "Call native symbol NAME through a forwarder in a separate translation unit, " + "so the binding never declares a name Python.h already declares" + ), + ) + build_group.add_argument( + "--collision-adapter-all", + action="store_true", + help="Apply --collision-adapter to every direct C symbol", + ) + build_group.add_argument( + "--positional-only", + action="store_true", + help=( + "Expose wrappers whose arguments are all required as positional-only, naming them " + "arg0..argN so a native declaration's parameter names stay out of the Python API" + ), + ) + build_group.add_argument( + "--assume-intent-in-scalars", + action="store_true", + help="Treat a scalar dummy with no declared intent as intent(in), so its value is not returned", + ) build_group.add_argument( "--verbose", action="store_true", @@ -2121,7 +2620,7 @@ def _parse_parser(argv: list[str]) -> argparse.ArgumentParser: _add_output_options( output_group, json_help="Print the parse report as JSON instead of human-readable text", - out_help="Write combined JSON to PATH; with no PATH, write one .json file beside each input source", + out_help="Write the report to PATH; with no PATH, write one file beside each input source", out_metavar="PATH", ) diagnostic_group = parser.add_argument_group("diagnostic options") @@ -2158,11 +2657,19 @@ def _semantics_parser(argv: list[str]) -> argparse.ArgumentParser: include_help="Add a preprocessing include search directory; repeat as needed", ) _add_include_exposure_options(parser, group_title="C include options") + _add_semantic_interpretation_options(parser) + report_group = parser.add_argument_group("report options") + report_group.add_argument( + "--print-limit", + type=int, + metavar="N", + help="Show at most N items in each repeated human-readable report section", + ) output_group = parser.add_argument_group("output options") _add_output_options( output_group, - allow_json=False, - out_help=("Write combined JSON to PATH; with no PATH, write one .json file beside each input source"), + json_help="Print the semantic report as JSON instead of human-readable text", + out_help=("Write the report to PATH; with no PATH, write one file beside each input source"), out_metavar="PATH", ) diagnostic_group = parser.add_argument_group("diagnostic options") @@ -2220,6 +2727,7 @@ def _generate_parser(argv: list[str]) -> argparse.ArgumentParser: include_help="Add an include search directory; repeat as needed", ) _add_include_exposure_options(parser, group_title="C include options") + _add_semantic_interpretation_options(parser) _add_wrapper_behavior_options(parser, group_title="wrapper options") native_group = parser.add_argument_group("native options") _add_native_compilation_options(native_group) @@ -2259,12 +2767,6 @@ def _probe_parser(argv: list[str]) -> argparse.ArgumentParser: required=True, help="Native or cross compiler used to build the probe", ) - target.add_argument( - "--format", - choices=("json", "markdown"), - default="json", - help="Output measured JSON facts or a Markdown type mapping table", - ) target.add_argument( "--expr", "--expression", @@ -2272,7 +2774,7 @@ def _probe_parser(argv: list[str]) -> argparse.ArgumentParser: action="append", default=[], metavar="EXPR", - help="Evaluate a Fortran integer expression in JSON output; repeat as needed", + help="Measure a Fortran integer expression instead of the mapping table; repeat as needed", ) compiler = parser.add_argument_group("execution options") compiler.add_argument( @@ -2325,6 +2827,11 @@ def _probe_parser(argv: list[str]) -> argparse.ArgumentParser: compiler.add_argument("--cache-dir", metavar="DIR", help="Read and write reusable probe results under DIR") compiler.add_argument("--refresh", action="store_true", help="Ignore reusable results and probe again") output = parser.add_argument_group("output options") + output.add_argument( + "--json", + action="store_true", + help="Print the measured report as JSON instead of the human-readable table", + ) output.add_argument("--out", metavar="PATH", help="Write the probe report to PATH instead of standard output") diagnostic = parser.add_argument_group("diagnostic options") _add_diagnostic_controls(diagnostic) @@ -2356,19 +2863,14 @@ def _argv_uses_option(argv: list[str], option: str) -> bool: return any(value == option or value.startswith(f"{option}=") for value in argv) -def _probe_output(args: argparse.Namespace) -> str: - target_options = { - "runner": args.runner or None, - "cache_dir": args.cache_dir, - "refresh": args.refresh, - } - if args.format == "markdown": - unsupported = bool(args.include_dirs or args.defines or args.undefs or args.std or args.expressions) - if unsupported: - raise ValueError("--format markdown accepts compiler, compiler arguments, runner, cache, and refresh only") - generator = c_type_mapping_markdown if args.language == "c" else fortran_type_mapping_markdown - return generator(compiler=args.compiler, compiler_args=args.compiler_args, **target_options) +def _probe_expression_output(args: argparse.Namespace, target_options: dict[str, object]) -> str: + """Measure the requested Fortran expressions and render the chosen format. + Preprocessing options apply here because each expression is compiled from + generated source. The measured report is the record; Markdown converts it. + """ + if args.language == "c": + raise ValueError("--expr is supported only for --language fortran") config = PreprocessingConfig( mode="compiler", compiler=args.compiler, @@ -2378,13 +2880,45 @@ def _probe_output(args: argparse.Namespace) -> str: std=args.std, compiler_args=args.compiler_args, ) - if args.language == "c": - if args.expressions: - raise ValueError("--expr is supported only for --language fortran") - report = probe_c_standard_types_cached(config, **target_options) - else: - report = probe_fortran_type_expressions_cached(config, args.expressions, **target_options) - return json.dumps(report.to_dict(), indent=2) + report = probe_fortran_type_expressions_cached(config, args.expressions, **target_options) + if args.json: + return json.dumps(report.to_dict(), indent=2) + return expression_probe_markdown(report) + + +def _probe_mapping_output(args: argparse.Namespace, target_options: dict[str, object]) -> str: + """Measure the standard type mapping table and render the chosen format. + + The mapping inventory is fixed, so preprocessing options cannot affect it + and are rejected instead of silently ignored. The measured report is the + record; Markdown converts it. + """ + if args.include_dirs or args.defines or args.undefs or args.std: + raise ValueError( + "the type mapping report accepts compiler, compiler arguments, runner, cache, " + "and refresh only; add --expr to probe preprocessed expressions" + ) + builder = c_type_mapping_report if args.language == "c" else fortran_type_mapping_report + report = builder(compiler=args.compiler, compiler_args=args.compiler_args, **target_options) + if args.json: + return json.dumps(report, indent=2) + return type_mapping_markdown(report) + + +def _probe_output(args: argparse.Namespace) -> str: + """Select the probe report and serialize it in the requested format. + + ``--expr`` selects the measured expression report; without it the standard + type mapping table is measured. Both reports support both formats. + """ + target_options = { + "runner": args.runner or None, + "cache_dir": args.cache_dir, + "refresh": args.refresh, + } + if args.expressions: + return _probe_expression_output(args, target_options) + return _probe_mapping_output(args, target_options) def _run_probe_command(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int: @@ -2430,7 +2964,7 @@ def main(argv: list[str] | None = None) -> int: return 1 parse_payload, semantic_payload = reports payload = _select_main_payload(args, parse_payload, semantic_payload) - if _write_main_output(args, parser, payload, semantic_payload): + if _write_main_output(args, parser, payload, parse_payload, semantic_payload, print_limit): return 0 _print_main_output(args, payload, parse_payload, semantic_payload, print_limit) return 0 diff --git a/prik/codegen/__init__.py b/prik/codegen/__init__.py index 8aa81a5ef..89de8abdf 100644 --- a/prik/codegen/__init__.py +++ b/prik/codegen/__init__.py @@ -20,9 +20,11 @@ CExpressionStatement, CFunction, CFunctionPrototype, + CGoto, CHeader, CIf, CInclude, + CLabel, CMacroDefinition, CMethodDefEntry, CMethodDefTable, @@ -63,9 +65,11 @@ "CExpressionStatement", "CFunction", "CFunctionPrototype", + "CGoto", "CHeader", "CIf", "CInclude", + "CLabel", "CMacroDefinition", "CMethodDefEntry", "CMethodDefTable", diff --git a/prik/codegen/c/binding.py b/prik/codegen/c/binding.py index 16e116e69..674b3014c 100644 --- a/prik/codegen/c/binding.py +++ b/prik/codegen/c/binding.py @@ -63,9 +63,11 @@ CFunction, CFunctionPointerType, CFunctionPrototype, + CGoto, CHeader, CIf, CInclude, + CLabel, CMacroDefinition, CMethodDefEntry, CMethodDefTable, @@ -80,6 +82,7 @@ CodeExpression, ) from prik.codegen.overloads import OverloadPlanQueries +from prik.naming.native_symbols import COLLISION_ADAPTER_STORAGE from prik.planning.models import ( ArrayHandoffPlan, ArgumentTransferPlan, @@ -91,6 +94,7 @@ DerivedHandoffPlan, DerivedMemberPathPlan, DerivedTypePlan, + DirectCABITypePlan, FunctionPlan, LifecycleActionPlan, ModulePlan, @@ -109,7 +113,7 @@ OverloadPlan, ResultPlan, ) -from prik.codegen.primitive_scalar_types import PrimitiveScalarTypeRegistry +from prik.codegen.primitive_scalar_types import NativeCArrayStorageRegistry, PrimitiveScalarTypeRegistry from prik.codegen.visitor import ClassVisitor @@ -161,6 +165,24 @@ class _COverloadDispatch: public: bool +_BINDING_GETTER_SUMMARIES = { + ModuleGetterAction.CONSTANT_VALUE: "The value is a constant placed in the module dictionary at import.", + ModuleGetterAction.NATIVE_CONSTANT_VALUE: "Builds a Python object from the compiler-evaluated constant.", + ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE: "Copies the parameter array into one read-only NumPy array.", + ModuleGetterAction.DIRECT_VALUE: "Builds a Python scalar from the current native value.", + ModuleGetterAction.CHARACTER_VALUE: "Decodes the fixed-width native characters into a Python str.", + ModuleGetterAction.NULLABLE_SNAPSHOT: "Returns a detached copy, or None when the native value holds nothing.", + ModuleGetterAction.BORROWED_ARRAY_VIEW: "Wraps the native storage in a live NumPy array without copying.", + ModuleGetterAction.DERIVED_OBJECT: "Returns the generated wrapper object for the native value.", +} + +_BINDING_SETTER_SUMMARIES = { + SetterAction.WRITE_THROUGH: "Validates the incoming object and writes it into native storage.", + SetterAction.REJECT_REPLACEMENT: "Replacement is rejected; the attribute is read-only.", + SetterAction.OMIT: "No setter is exposed.", +} + + class CBindingGenerator(ClassVisitor): """Build the CPython C half of a wrapper from validated binding-plan views. @@ -173,6 +195,7 @@ class CBindingGenerator(ClassVisitor): _SHARD_MIN_FUNCTIONS = 128 _SHARD_TARGET_FUNCTIONS = 32 + _SHARED_OUTPUT_CLEANUP_MIN_RESULTS = 4 def require_supported(self, plan: ModulePlan) -> None: """Preflight primitive spellings needed by an already-validated plan. @@ -381,9 +404,66 @@ def binding_modules(self, plan: ModulePlan) -> tuple[CModule, ...]: """ module = self.binding_module(plan) function_groups = self._binding_function_shards(plan) - if not function_groups: - return (module,) - return self._sharded_binding_modules(plan, module, function_groups) + modules = (module,) if not function_groups else self._sharded_binding_modules(plan, module, function_groups) + adapters = self._collision_adapter_module(plan) + return (*modules, adapters) if adapters is not None else modules + + def _collision_adapter_module(self, plan: ModulePlan) -> CModule | None: + """Build the translation unit that forwards collision-adapted symbols. + + The unit deliberately includes no Python header, so its declaration of + each native symbol is the only one in scope and cannot conflict with a + declaration ``Python.h`` would otherwise have brought in. + """ + adapted = self._collision_adapted_functions(plan) + if not adapted: + return None + return CModule( + name=f"{plan.binding.owner_path}_adapters", + includes=( + CInclude("stdint.h"), + CInclude("stdbool.h"), + CInclude("complex.h"), + CInclude("stddef.h"), + ), + declarations=tuple(self._collision_adapter_native_prototype(function) for function in adapted), + functions=tuple(self._collision_adapter_function(function) for function in adapted), + ) + + def _collision_adapted_functions(self, plan: ModulePlan) -> tuple[FunctionPlan, ...]: + """Return one function per adapted symbol, in stable emission order. + + Several Python callables may name the same native symbol, so the + forwarder is defined once per symbol rather than once per callable. + """ + adapted: dict[str, FunctionPlan] = {} + for function in self._functions(plan): + symbol = function.entrypoint.collision_adapter_symbol + if symbol is not None: + adapted.setdefault(symbol, function) + return tuple(adapted.values()) + + def _collision_adapter_native_prototype(self, plan: FunctionPlan) -> CFunctionPrototype: + """Declare the native symbol under its own name inside the adapter unit.""" + return replace(self._entrypoint_prototype(plan), name=plan.entrypoint.symbol_name) + + def _collision_adapter_function(self, plan: FunctionPlan) -> CFunction: + """Define the forwarder the binding calls in place of the native symbol.""" + prototype = self._entrypoint_prototype(plan) + call = CodeExpression( + f"({plan.entrypoint.symbol_name})({', '.join(parameter.name for parameter in prototype.parameters)})" + ) + body = (CExpressionStatement(call),) if prototype.return_type == "void" else (CReturn(call),) + return CFunction( + name=prototype.name, + return_type=prototype.return_type, + parameters=prototype.parameters, + body=body, + # A hidden forwarder is not part of the extension's exported ABI, so + # link-time optimization may inline it and drop the definition. An + # exported one is interposable and must survive the link. + storage=COLLISION_ADAPTER_STORAGE, + ) def _sharded_binding_modules( self, @@ -649,11 +729,9 @@ def requires_native_support(self, plan: ModulePlan) -> bool: return ( bool(tuple(self._variables(plan))) or any(function.arguments or function.results for function in self._functions(plan)) - or any( - field.object_kind is ObjectKind.NUMPY_ARRAY - for derived in self._derived_types(plan) - for field in derived.fields - ) + # Every published component converts through the bundled helpers, so a + # type whose module exposes only `bind(C)` procedures still needs them. + or any(derived.fields for derived in self._derived_types(plan)) ) def _module_needs_allocator(self, plan: ModulePlan) -> bool: @@ -697,6 +775,11 @@ def _module_includes( CInclude("stdint.h"), CInclude("stdbool.h"), CInclude("complex.h"), + # A preserved direct-C declaration may spell a standard typedef such + # as ``size_t`` or ``ptrdiff_t``, so the entrypoint prototype needs + # its defining header rather than whatever ``Python.h`` happens to + # pull in on one platform. + *((CInclude("stddef.h"),) if self._module_declares_direct_c_entrypoints(plan) else ()), *((CInclude("stdatomic.h"),) if self._module_uses_derived_origin_ops(plan) else ()), *((CInclude("string.h"),) if self._module_uses_memory_copy(plan) else ()), *( @@ -709,6 +792,15 @@ def _module_includes( CInclude(f"{plan.binding.owner_path}_wrapper.h", system=False), ) + @staticmethod + def _module_declares_direct_c_entrypoints(plan: ModulePlan) -> bool: + """Return whether any planned entrypoint carries preserved C declarations.""" + return any( + function.entrypoint.direct_c_abi is not None + for namespace in plan.namespaces + for function in namespace.functions + ) + def _module_uses_string_values(self, plan: ModulePlan) -> bool: """Return whether binding conversion needs C string helpers.""" return any( @@ -2359,6 +2451,7 @@ def _direct_field_bridge_prototype_entries(self, plan: ModulePlan) -> tuple[CFun return tuple( self._generated_support_procedure_entrypoint_prototype(operation) for derived in self._derived_types(plan) + if not derived.abstract for field in derived.fields for operation in self._generated_support_procedure_entrypoints_for( f"{derived.owner_path}.{field.name}", "field:direct:" @@ -2428,6 +2521,7 @@ def _direct_field_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, return tuple( function for derived in self._derived_types(plan) + if not derived.abstract for field in derived.fields for function in self._direct_field_functions(derived, field) ) @@ -5362,11 +5456,28 @@ def _native_array_capsule_release_name(plan: ArgumentTransferPlan | ResultPlan) owner = re.sub(r"\W", "_", plan.owner_path).casefold() return f"prik_release_native_handle_{owner}" + @staticmethod + def _documented(functions: tuple[CFunction, ...], *doc: str) -> tuple[CFunction, ...]: + """Attach explanatory prose to generated functions that carry none.""" + return tuple(function if function.doc else replace(function, doc=doc) for function in functions) + def _visit_ModuleVariablePlan(self, plan: ModuleVariablePlan) -> tuple[CFunction, ...]: """Lower binding-owned getter and setter actions into C functions.""" + # The binding facet names the Python attribute and the C symbols it + # calls; the native Fortran variable belongs to the bridge facet and is + # deliberately not read here. + name = plan.binding.python_names[0] return ( - *self._lower_module_getter(plan), - *self._lower_module_setter(plan), + *self._documented( + self._lower_module_getter(plan), + f"Read module attribute '{name}'.", + _BINDING_GETTER_SUMMARIES.get(plan.binding.getter_action, ""), + ), + *self._documented( + self._lower_module_setter(plan), + f"Assign module attribute '{name}'.", + _BINDING_SETTER_SUMMARIES.get(plan.binding.setter_action, ""), + ), ) def _lower_module_getter(self, plan: ModuleVariablePlan) -> tuple[CFunction, ...]: @@ -5381,6 +5492,8 @@ def _lower_module_getter(self, plan: ModuleVariablePlan) -> tuple[CFunction, ... return self._lower_module_getter_constant_value(plan) case ModuleGetterAction.DIRECT_VALUE: return self._lower_module_getter_direct_value(plan) + case ModuleGetterAction.CHARACTER_VALUE: + return self._lower_module_getter_character_value(plan) case ModuleGetterAction.NULLABLE_SNAPSHOT: return self._lower_module_getter_nullable_snapshot(plan) case ModuleGetterAction.BORROWED_ARRAY_VIEW: @@ -5423,8 +5536,85 @@ def _lower_module_getter_direct_value(self, plan: ModuleVariablePlan) -> tuple[C ), ) + def _module_character_length(self, plan: ModuleVariablePlan) -> int: + """Return the declared width one character module accessor copies.""" + length = plan.character_length + if length is None or length <= 0: + raise ValueError(f"Character module variable {plan.owner_path!r} has no declared length") + return length + + def _lower_module_getter_character_value(self, plan: ModuleVariablePlan) -> tuple[CFunction, ...]: + """Copy one fixed native character module variable into an independent Python string.""" + length = self._module_character_length(plan) + return ( + CFunction( + self._module_getter_name(plan), + "PyObject *", + storage="static", + body=( + CDeclaration(f"value[{length + 1}]", "char"), + CExpressionStatement(CodeExpression(f"{self._module_bridge_getter_name(plan)}(value)")), + CExpressionStatement(CodeExpression(f"value[{length}] = '\\0'")), + CReturn(CodeExpression(f'PyUnicode_DecodeUTF8(value, {length}, "strict")')), + ), + ), + ) + + def _lower_module_setter_character_value(self, plan: ModuleVariablePlan) -> tuple[CFunction, ...]: + """Validate and copy one exact-width Python string into native module storage. + + The setter reports failure with ``-1`` rather than ``NULL``: a module + attribute assignment is an ``int`` slot, not a returned object. + """ + length = self._module_character_length(plan) + name = plan.binding.python_names[0] + return ( + CFunction( + self._module_setter_name(plan), + "int", + parameters=(CParameter("value_obj", "PyObject *"),), + storage="static", + body=( + CIf( + CodeExpression("!PyUnicode_Check(value_obj)"), + body=( + CExpressionStatement( + CodeExpression( + f'PyErr_SetString(PyExc_TypeError, "Expected str for module variable {name}")' + ) + ), + CReturn(CodeExpression("-1")), + ), + ), + CDeclaration("value_length", "Py_ssize_t", CodeExpression("0")), + CDeclaration( + "value", + "const char *", + CodeExpression("PyUnicode_AsUTF8AndSize(value_obj, &value_length)"), + ), + CIf(CodeExpression("value == NULL"), body=(CReturn(CodeExpression("-1")),)), + CIf( + CodeExpression(f"value_length != {length} || (Py_ssize_t)strlen(value) != value_length"), + body=( + CExpressionStatement( + CodeExpression( + f'PyErr_SetString(PyExc_TypeError, "Module variable {name} must encode to ' + f'exactly {length} bytes without embedded NUL")' + ) + ), + CReturn(CodeExpression("-1")), + ), + ), + CExpressionStatement(CodeExpression(f"{self._module_bridge_setter_name(plan)}(value)")), + CReturn(CodeExpression("0")), + ), + ), + ) + def _lower_module_getter_nullable_snapshot(self, plan: ModuleVariablePlan) -> tuple[CFunction, ...]: """Return None or a detached Python copy from a nullable native snapshot.""" + if plan.datatype_family is DatatypeFamily.STRING: + return self._lower_module_getter_nullable_character_snapshot(plan) scalar_type = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) return ( CFunction( @@ -5457,13 +5647,69 @@ def _lower_module_getter_nullable_snapshot(self, plan: ModuleVariablePlan) -> tu ), ) + def _lower_module_getter_nullable_character_snapshot( + self, + plan: ModuleVariablePlan, + ) -> tuple[CFunction, ...]: + """Decode one nullable detached character snapshot, or report absence. + + An unallocated descriptor and a failed allocation are different + outcomes: the first is ``None``, the second is a ``MemoryError``, and + only the reported width separates them. + """ + return ( + CFunction( + self._module_getter_name(plan), + "PyObject *", + storage="static", + body=( + CDeclaration("length", "int64_t", CodeExpression("0")), + CDeclaration( + "data", + "void *", + CodeExpression(f"{self._module_bridge_getter_name(plan)}(&length)"), + ), + CIf( + CodeExpression("data == NULL"), + body=( + CIf( + CodeExpression("length > 0"), + body=(CReturn(CodeExpression("PyErr_NoMemory()")),), + ), + CExpressionStatement(CodeExpression("Py_RETURN_NONE")), + ), + ), + CDeclaration( + "result", + "PyObject *", + CodeExpression('PyUnicode_DecodeUTF8((const char *)data, (Py_ssize_t)length, "strict")'), + ), + CExpressionStatement(CodeExpression("free(data)")), + CReturn(CodeExpression("result")), + ), + ), + ) + def _lower_module_getter_borrowed_array_view(self, plan: ModuleVariablePlan) -> tuple[CFunction, ...]: """Create one live Fortran-ordered NumPy alias over fixed module storage.""" array = plan.array if array is None or array.rank is None: raise ValueError(f"Module array view {plan.owner_path!r} has no fixed rank") - scalar = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) + # A character element is a fixed-width bytes dtype whose width the + # Fortran variable reports, so it carries an itemsize instead of naming + # a NumPy scalar type macro. + character = plan.datatype_family is DatatypeFamily.STRING + if character: + element_size = "itemsize" + numpy_type = "NPY_STRING" + numpy_itemsize = "(int)itemsize" + else: + scalar = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) + element_size = f"sizeof({scalar.c_spelling})" + numpy_type = str(scalar.numpy_type_macro) + numpy_itemsize = "0" owner = self._module_native_array_owner_name(plan) + width = ("itemsize",) if character else () extents = tuple(f"extent_{axis}" for axis in range(array.rank)) strides = "strides" return ( @@ -5472,12 +5718,13 @@ def _lower_module_getter_borrowed_array_view(self, plan: ModuleVariablePlan) -> "PyObject *", storage="static", body=( - *(CDeclaration(name, "int64_t", CodeExpression("0")) for name in extents), + *(CDeclaration(name, "int64_t", CodeExpression("0")) for name in (*width, *extents)), CDeclaration( "data", "void *", CodeExpression( - f"{self._module_bridge_getter_name(plan)}({', '.join(f'&{name}' for name in extents)})" + f"{self._module_bridge_getter_name(plan)}" + f"({', '.join(f'&{name}' for name in (*width, *extents))})" ), ), CDeclaration( @@ -5486,7 +5733,7 @@ def _lower_module_getter_borrowed_array_view(self, plan: ModuleVariablePlan) -> CodeExpression("{" + ", ".join(extents) + "}"), ), CDeclaration(f"{strides}[{array.rank}]", "npy_intp"), - CExpressionStatement(CodeExpression(f"{strides}[0] = (npy_intp)sizeof({scalar.c_spelling})")), + CExpressionStatement(CodeExpression(f"{strides}[0] = (npy_intp){element_size}")), *( CExpressionStatement( CodeExpression(f"{strides}[{axis}] = {strides}[{axis - 1}] * dimensions[{axis - 1}]") @@ -5497,8 +5744,8 @@ def _lower_module_getter_borrowed_array_view(self, plan: ModuleVariablePlan) -> "result", "PyObject *", CodeExpression( - f"PyArray_New(&PyArray_Type, {array.rank}, dimensions, {scalar.numpy_type_macro}, " - f"{strides}, data, 0, NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_ALIGNED | " + f"PyArray_New(&PyArray_Type, {array.rank}, dimensions, {numpy_type}, " + f"{strides}, data, {numpy_itemsize}, NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_ALIGNED | " "NPY_ARRAY_WRITEABLE, NULL)" ), ), @@ -5798,6 +6045,8 @@ def _lower_module_setter(self, plan: ModuleVariablePlan) -> tuple[CFunction, ... def _lower_module_setter_write_through(self, plan: ModuleVariablePlan) -> tuple[CFunction, ...]: """Return a Python-to-native scalar write-through helper.""" + if plan.binding.setter_converts_characters: + return self._lower_module_setter_character_value(plan) scalar_type = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) return ( CFunction( @@ -5853,11 +6102,12 @@ def _visit_FunctionPlan(self, plan: FunctionPlan) -> CFunction: output_nodes = self._output_nodes(plan, context) return CFunction( name=self._binding_function_name(plan), + doc=self._binding_function_doc(plan), return_type="PyObject *", - parameters=self._binding_parameters(), + parameters=self._binding_parameters(plan), storage="static", body=( - self._keyword_declaration(plan), + *self._keyword_declarations(plan), *argument_declarations, *alias_declarations, *self._callback_context_declarations(plan), @@ -5929,6 +6179,20 @@ def _binding_conversion_order(self, plan: FunctionPlan) -> tuple[ArgumentTransfe except KeyError as error: raise ValueError(f"Unknown binding argument conversion owner {error.args[0]!r}") from None + def _binding_function_doc(self, plan: FunctionPlan) -> tuple[str, ...]: + """Describe one CPython wrapper: its Python name and the symbol it calls. + + A reader opening the generated binding sees the Python entry point and + the native symbol it reaches without cross-referencing the plan. + """ + lines = [ + f"Python callable '{plan.binding.python_name}'.", + f"Calls the native entrypoint '{plan.entrypoint.symbol_name}'.", + ] + if plan.binding.release_gil: + lines.append("Releases the GIL around the native call.") + return tuple(lines) + def _visit_ArgumentTransferPlan( self, plan: ArgumentTransferPlan, @@ -6357,6 +6621,26 @@ def _lower_argument_required_scalar_value( if scalar_type.numpy_type_macro is None: raise ValueError(f"Unsupported scalar input type {plan.semantic_type_name!r}") names = context.arguments[plan.owner_path] + storage_type = plan.native_storage_c_type or scalar_type.c_spelling + if storage_type != scalar_type.c_spelling: + converted_name = f"{names.value_name}_converted" + return ( + CDeclaration(names.object_name, "PyObject *"), + CDeclaration(converted_name, scalar_type.c_spelling), + CDeclaration(names.value_name, storage_type), + self._scalar_exact_unpack_statement( + scalar_type, + names.object_name, + converted_name, + ( + f'PyErr_Format(PyExc_TypeError, "Expected an argument of type ' + f"{scalar_type.python_type_name} for argument {plan.binding.python_name}. " + f"Received \", Py_TYPE({names.object_name})->tp_name)" + ), + "NULL", + ), + CExpressionStatement(CodeExpression(f"{names.value_name} = ({storage_type}){converted_name}")), + ) return ( CDeclaration(names.object_name, "PyObject *"), CDeclaration(names.value_name, scalar_type.c_spelling), @@ -6467,11 +6751,17 @@ def _required_string_validation_nodes( CodeExpression(f"{payload_name} = PyUnicode_AsUTF8AndSize({names.object_name}, &{names.length_name})") ), CExpressionStatement(CodeExpression(f"if ({payload_name} == NULL) return NULL")), - CExpressionStatement( - CodeExpression( - f"if ((Py_ssize_t)strlen({payload_name}) != {names.length_name}) {{ " - f'PyErr_SetString(PyExc_TypeError, "Argument {plan.binding.python_name} cannot contain ' - 'embedded NUL"); return NULL; }' + *( + () + if plan.character_allows_embedded_nul + else ( + CExpressionStatement( + CodeExpression( + f"if ((Py_ssize_t)strlen({payload_name}) != {names.length_name}) {{ " + f'PyErr_SetString(PyExc_TypeError, "Argument {plan.binding.python_name} cannot contain ' + 'embedded NUL"); return NULL; }' + ) + ), ) ), ] @@ -6568,7 +6858,7 @@ def _lower_argument_required_array_actual( prefix = names.value_name array_object = f"(PyArrayObject *){names.object_name}" direct_nodes = ( - self._array_validation_statement(plan, names), + self._array_validation_statement(plan, names, object_kind_checked=True), *self._array_shape_checks(plan, context, array_object), *self._array_extraction_nodes(plan, names, array_object), ) @@ -6713,6 +7003,8 @@ def _array_validation_statement( self, plan: ArgumentTransferPlan, names: _CArgumentNames, + *, + object_kind_checked: bool = False, ) -> CExpressionStatement: """Call compact validation with selectors from the completed plan.""" handoff = plan.array @@ -6721,9 +7013,12 @@ def _array_validation_statement( numpy_type, python_type = self._array_dtype_selectors(plan, handoff) minimum_rank, maximum_rank = self._array_rank_bounds(handoff) layout = self._array_layout_selector(handoff) + helper = "prik_array_validate_ndarray" if object_kind_checked else "prik_array_validate" + value = f"(PyArrayObject *){names.object_name}" if object_kind_checked else names.object_name return CExpressionStatement( CodeExpression( - f"if (prik_array_validate({names.object_name}, {numpy_type}, {minimum_rank}, {maximum_rank}, " + f"if ({helper}({value}, {numpy_type}, " + f"{minimum_rank}, {maximum_rank}, " f'{layout}, {int(handoff.contiguous is True)}, {int(plan.binding.writable)}, "{python_type}", ' f'"{plan.binding.python_name}") < 0) return NULL' ) @@ -6737,6 +7032,17 @@ def _array_dtype_selectors( """Return compact helper dtype selectors from completed array facts.""" if plan.datatype_family is DatatypeFamily.STRING: return "NPY_STRING", f"numpy.bytes_[{handoff.itemsize}]" + return CBindingGenerator._numeric_array_dtype_selectors(plan) + + @staticmethod + def _numeric_array_dtype_selectors(plan: ArgumentTransferPlan) -> tuple[str, str]: + """Return canonical or policy-selected exact native NumPy storage.""" + if plan.binding.native_array_element_c_type is not None: + native = NativeCArrayStorageRegistry.type_for( + plan.binding.native_array_element_c_type, + plan.semantic_type_name, + ) + return native.numpy_type_macro, native.python_type_name scalar_type = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) if scalar_type.numpy_type_macro is None or scalar_type.python_type_name is None: raise ValueError(f"Unsupported array element type {plan.semantic_type_name!r}") @@ -6841,18 +7147,21 @@ def _array_extraction_nodes( CExpressionStatement(CodeExpression(f"{names.runtime_rank_name} = (int64_t)PyArray_NDIM({array})")) ) if handoff.itemsize_role is not None: - nodes.extend( - ( - CExpressionStatement(CodeExpression(f"{names.itemsize_name} = (int64_t)PyArray_ITEMSIZE({array})")), + nodes.append( + CExpressionStatement(CodeExpression(f"{names.itemsize_name} = (int64_t)PyArray_ITEMSIZE({array})")) + ) + # An assumed width accepts whatever the caller's array declares; only + # a stated width is checked against it. + if handoff.itemsize is not None: + nodes.append( CExpressionStatement( CodeExpression( f"if ({names.itemsize_name} != {handoff.itemsize}) {{ PyErr_SetString(PyExc_TypeError, " f'"Argument {plan.binding.python_name} must have NumPy bytes dtype itemsize ' f'{handoff.itemsize}"); return NULL; }}' ) - ), + ) ) - ) if handoff.flatten_python_storage: nodes.extend(self._flat_array_extraction_nodes(handoff, names, array)) return tuple(nodes) @@ -7022,19 +7331,16 @@ def _lower_argument_required_scalar_storage( context: _CFunctionContext, ) -> tuple[CDeclaration | CExpressionStatement, ...]: """Validate and borrow one rank-zero NumPy scalar data address.""" - scalar_type = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) - if scalar_type.numpy_type_macro is None: - raise ValueError(f"Unsupported scalar storage type {plan.semantic_type_name!r}") + numpy_type, expected = self._numeric_array_dtype_selectors(plan) names = context.arguments[plan.owner_path] array = f"(PyArrayObject *){names.object_name}" - expected = scalar_type.python_type_name nodes = [ CDeclaration(names.object_name, "PyObject *"), CDeclaration(names.value_name, "void *", CodeExpression("NULL")), CExpressionStatement( CodeExpression( f"if (!PyArray_Check({names.object_name}) || PyArray_TYPE({array}) != " - f"{scalar_type.numpy_type_macro} || PyArray_NDIM({array}) != 0) {{ " + f"{numpy_type} || PyArray_NDIM({array}) != 0) {{ " f'PyErr_Format(PyExc_TypeError, "Expected a rank-zero numpy.ndarray of type ' f"{expected} for argument {plan.binding.python_name}. Received \", " f"Py_TYPE({names.object_name})->tp_name); return NULL; }}" @@ -7074,12 +7380,18 @@ def _lower_argument_required_string_storage( plan: ArgumentTransferPlan, context: _CFunctionContext, ) -> tuple[CDeclaration | CExpressionStatement, ...]: - """Validate and borrow one rank-zero fixed-width NumPy bytes buffer.""" - if plan.character_length is None or plan.character_length <= 0: - raise ValueError(f"String storage {plan.owner_path!r} is missing a fixed length") + """Validate and borrow one rank-zero NumPy bytes buffer. + + A declared capacity is checked against the array's itemsize. An + assumed capacity accepts any ``S`` width, because the caller's buffer + states its own size and the binding passes that storage untouched. + """ + if plan.character_length is not None and plan.character_length <= 0: + raise ValueError(f"String storage {plan.owner_path!r} has a non-positive length") names = context.arguments[plan.owner_path] array = f"(PyArrayObject *){names.object_name}" length = plan.character_length + expected = f"S{length}" if length is not None else "S" return ( CDeclaration(names.object_name, "PyObject *"), CDeclaration(names.value_name, "void *", CodeExpression("NULL")), @@ -7087,17 +7399,23 @@ def _lower_argument_required_string_storage( CodeExpression( f"if (!PyArray_Check({names.object_name}) || PyArray_TYPE({array}) != NPY_STRING || " f"PyArray_NDIM({array}) != 0) {{ " - f'PyErr_Format(PyExc_TypeError, "Expected a rank-zero numpy.ndarray with dtype S{length} ' + f'PyErr_Format(PyExc_TypeError, "Expected a rank-zero numpy.ndarray with dtype {expected} ' f"for argument {plan.binding.python_name}. Received \", " f"Py_TYPE({names.object_name})->tp_name); return NULL; }}" ) ), - CExpressionStatement( - CodeExpression( - f"if (PyArray_ITEMSIZE({array}) != {length}) {{ " - f'PyErr_SetString(PyExc_TypeError, "Argument {plan.binding.python_name} must use itemsize ' - f'{length}"); return NULL; }}' + *( + ( + CExpressionStatement( + CodeExpression( + f"if (PyArray_ITEMSIZE({array}) != {length}) {{ " + f'PyErr_SetString(PyExc_TypeError, "Argument {plan.binding.python_name} must use itemsize ' + f'{length}"); return NULL; }}' + ) + ), ) + if length is not None + else () ), CExpressionStatement( CodeExpression( @@ -7855,15 +8173,17 @@ def _visit_ResultPlan( *, context: _CFunctionContext, failure_cleanup: tuple[str, ...] = (), + failure_label: str | None = None, ) -> tuple[CExpressionStatement | CDeclaration | CIf, ...]: """Lower one result through its completed binding action.""" - return self._lower_result(plan, context, failure_cleanup) + return self._lower_result(plan, context, failure_cleanup, failure_label) def _lower_result( self, plan: ResultPlan, context: _CFunctionContext, failure_cleanup: tuple[str, ...], + failure_label: str | None, ) -> tuple[CExpressionStatement | CDeclaration | CIf, ...]: """Dispatch one completed binding result action explicitly.""" if plan.scalar_descriptor is not None: @@ -7877,7 +8197,7 @@ def _lower_result( return self._lower_result_fixed_string(plan, context, failure_cleanup) case ObjectKind.SCALAR: if plan.binding.codegen_action is CodegenAction.DIRECT_VALUE: - return self._lower_result_direct_value(plan, context, failure_cleanup) + return self._lower_result_direct_value(plan, context, failure_cleanup, failure_label) raise ValueError( f"Unsupported C scalar result action for {plan.owner_path!r}: {plan.binding.codegen_action!r}" ) @@ -8580,15 +8900,17 @@ def _lower_result_direct_value( plan: ResultPlan, context: _CFunctionContext, failure_cleanup: tuple[str, ...], + failure_label: str | None = None, ) -> tuple[CExpressionStatement | CDeclaration | CIf, ...]: """Lower result direct value from the supplied completed binding records without inferring semantic policy.""" - return self._lower_result_value(plan, context, failure_cleanup) + return self._lower_result_value(plan, context, failure_cleanup, failure_label) def _lower_result_value( self, plan: ResultPlan, context: _CFunctionContext, failure_cleanup: tuple[str, ...], + failure_label: str | None = None, ) -> tuple[CExpressionStatement | CDeclaration | CIf, ...]: """Convert one native result into its binding-owned Python consumer.""" scalar_type = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) @@ -8596,18 +8918,27 @@ def _lower_result_value( python_name = context.python_results.get(plan.owner_path) if scalar_type.python_result_kind is None or python_name is None: raise ValueError(f"Unsupported scalar result type {plan.semantic_type_name!r}") + converted_name = native_name + conversion = () + if plan.entrypoint.native_scalar_c_type is not None: + converted_name = f"{native_name}_contract" + conversion = ( + CDeclaration( + converted_name, + scalar_type.c_spelling, + CodeExpression(f"({scalar_type.c_spelling}){native_name}"), + ), + ) return ( + *conversion, CDeclaration( python_name, "PyObject *", - CodeExpression(self._scalar_result_expression(scalar_type, f"&{native_name}")), + CodeExpression(self._scalar_result_expression(scalar_type, f"&{converted_name}")), ), CIf( CodeExpression(f"{python_name} == NULL"), - body=( - *(CExpressionStatement(CodeExpression(f"Py_DECREF({name})")) for name in failure_cleanup), - CReturn(CodeExpression("NULL")), - ), + body=self._output_failure_nodes(failure_cleanup, failure_label), ), ) @@ -8649,16 +8980,32 @@ def _combined_output_nodes( self, plan: FunctionPlan, context: _CFunctionContext, - ) -> tuple[CDeclaration | CExpressionStatement | CIf | CReturn, ...]: + ) -> tuple[CDeclaration | CExpressionStatement | CGoto | CIf | CLabel | CReturn, ...]: """Convert every public output once, then aggregate by completed position.""" published, ordinary_writebacks, derived_results, scalar_results = self._output_conversion_groups(plan) + output_count = sum(len(group) for group in (published, ordinary_writebacks, derived_results, scalar_results)) + shared_cleanup = output_count >= self._SHARED_OUTPUT_CLEANUP_MIN_RESULTS converted: list[str] = [] nodes = [] + def failure_label() -> str | None: + """Name the suffix that owns the already-converted prefix.""" + if not shared_cleanup or not converted: + return None + return self._output_cleanup_label(len(converted)) + # Published temporaries are converted first so every later failure owns # an ordinary Python reference that can be released uniformly. for action in published: - nodes.extend(self._writeback_value_nodes(plan, action, context, tuple(converted))) + nodes.extend( + self._writeback_value_nodes( + plan, + action, + context, + tuple(converted), + failure_label=failure_label(), + ) + ) converted.append(context.python_results[action.owner_path]) for position, result in enumerate(derived_results): @@ -8667,15 +9014,57 @@ def _combined_output_nodes( converted.append(context.python_results[result.owner_path]) for result in scalar_results: - nodes.extend(self.visit(result, context=context, failure_cleanup=tuple(converted))) + nodes.extend( + self.visit( + result, + context=context, + failure_cleanup=tuple(converted), + failure_label=failure_label(), + ) + ) converted.append(context.python_results[result.owner_path]) for action in ordinary_writebacks: - nodes.extend(self._writeback_value_nodes(plan, action, context, tuple(converted))) + nodes.extend( + self._writeback_value_nodes( + plan, + action, + context, + tuple(converted), + failure_label=failure_label(), + ) + ) converted.append(context.python_results[action.owner_path]) - ordered = tuple(context.python_results[owner] for owner, _position in self._output_owners(plan)) - nodes.extend(self._python_result_aggregation_nodes(ordered, context)) + # A ``Hidden`` result is lowered exactly like a published one so that + # every release the ordinary path performs still happens; only the + # Python object it produced is dropped instead of being aggregated. + for result in plan.results: + if not result.python_returned: + nodes.append( + CExpressionStatement(CodeExpression(f"Py_DECREF({context.python_results[result.owner_path]})")) + ) + if shared_cleanup: + nodes.append( + CExpressionStatement(CodeExpression(f"{context.python_results[result.owner_path]} = NULL")) + ) + hidden_owners = {result.owner_path for result in plan.results if not result.python_returned} + ordered = tuple( + context.python_results[owner] + for owner, _position in self._output_owners(plan) + if owner not in hidden_owners + ) + aggregate_failure_label = self._output_cleanup_label(len(converted)) if shared_cleanup and converted else None + nodes.extend( + self._python_result_aggregation_nodes( + ordered, + context, + failure_cleanup=tuple(converted), + failure_label=aggregate_failure_label, + ) + ) + if shared_cleanup: + nodes.extend(self._output_cleanup_chain(tuple(converted))) return tuple(nodes) def _output_conversion_groups( @@ -8701,6 +9090,7 @@ def _mixed_string_writeback_nodes( action: LifecycleActionPlan, context: _CFunctionContext, converted: tuple[str, ...], + failure_label: str | None = None, ) -> tuple: """Convert one projected fixed string without terminating aggregation.""" source = self._argument_for_role(plan, action.source_role) @@ -8708,11 +9098,13 @@ def _mixed_string_writeback_nodes( raise ValueError(f"Mixed output {action.owner_path!r} is not a fixed string") names = context.arguments[source.owner_path] target = context.python_results[action.owner_path] - cleanup = tuple(CExpressionStatement(CodeExpression(f"Py_DECREF({name})")) for name in converted) conversion = CExpressionStatement( CodeExpression(f'{target} = Py_BuildValue("s", (const char *){names.value_name})') ) - failure = CIf(CodeExpression(f"{target} == NULL"), body=(*cleanup, CReturn(CodeExpression("NULL")))) + failure = CIf( + CodeExpression(f"{target} == NULL"), + body=self._output_failure_nodes(converted, failure_label), + ) if source.binding.optional_mode is OptionalMode.REQUIRED: return ( CDeclaration(target, "PyObject *", CodeExpression("NULL")), @@ -9002,21 +9394,27 @@ def _python_result_aggregation_nodes( self, converted: tuple[str, ...], context: _CFunctionContext, - ) -> tuple[CDeclaration | CExpressionStatement | CIf | CReturn, ...]: + *, + failure_cleanup: tuple[str, ...] | None = None, + failure_label: str | None = None, + ) -> tuple[CDeclaration | CExpressionStatement | CGoto | CIf | CReturn, ...]: """Return one object directly or assemble ordered tuple ownership.""" + if not converted: + # Every output was hidden, so the call publishes nothing. The macro + # increfs before returning; a bare ``Py_None`` would leak a + # decrement onto the singleton. + return (CExpressionStatement(CodeExpression("Py_RETURN_NONE")),) if len(converted) == 1: return (CReturn(CodeExpression(converted[0])),) aggregate = context.python_result_name if aggregate is None: raise ValueError("Multiple Python results have no aggregate binding role") + cleanup = converted if failure_cleanup is None else failure_cleanup return ( CDeclaration(aggregate, "PyObject *", CodeExpression(f"PyTuple_New({len(converted)})")), CIf( CodeExpression(f"{aggregate} == NULL"), - body=( - *(CExpressionStatement(CodeExpression(f"Py_DECREF({name})")) for name in converted), - CReturn(CodeExpression("NULL")), - ), + body=self._output_failure_nodes(cleanup, failure_label), ), *( CExpressionStatement(CodeExpression(f"PyTuple_SET_ITEM({aggregate}, {position}, {name})")) @@ -9038,6 +9436,10 @@ def _entrypoint_call_statement(self, plan: FunctionPlan, context: _CFunctionCont or direct_result.object_kind is not ObjectKind.SCALAR or direct_result.scalar_descriptor is not None ): + direct_c_result = plan.entrypoint.direct_c_abi.result if plan.entrypoint.direct_c_abi is not None else None + if direct_c_result is not None and direct_c_result.converts_to_contract_storage: + contract_type = PrimitiveScalarTypeRegistry.type_for(direct_result.semantic_type_name) + call = f"({contract_type.c_spelling}){call}" expression = f"{context.result_name} = {call}" else: raise ValueError(f"Scalar result {direct_result.owner_path!r} has no completed direct-result ABI") @@ -9091,7 +9493,7 @@ def _lower_status_error_runtime_error( context, ) transformation_cleanup = self._binding_transformation_cleanup_nodes(plan, context) - if policy.message_role is None: + if policy.message_role is None and policy.message_argument is None: return ( CIf( condition, @@ -9108,24 +9510,96 @@ def _lower_status_error_runtime_error( ), ), ) - message_name = context.native_outputs[policy.message_role] - message_object = f"{message_name}_obj" - return ( - CIf( - CodeExpression(f"{message_name} == NULL"), - body=( - CExpressionStatement(CodeExpression("PyErr_NoMemory()")), - *transformation_cleanup, - *derived_cleanup, - CReturn(CodeExpression("NULL")), + message_capacity: str | None = None + if policy.message_argument is not None: + # The caller supplied the buffer, so the binding neither owns nor + # frees it; it only reads what the native call left behind. The read + # is bounded by the caller's own capacity because a native writer is + # not obliged to terminate: Fortran blank-pads fixed-length + # character storage and never writes a NUL. + names = context.arguments[policy.message_argument] + message_name = names.value_name + message_plan = next( + argument for argument in plan.arguments if argument.owner_path == policy.message_argument + ) + message_capacity = ( + f"PyArray_ITEMSIZE((PyArrayObject *){names.object_name})" + if message_plan.binding.codegen_action is CodegenAction.IN_PLACE_ARGUMENT + else names.length_name + ) + binding_owned = True + else: + message_name = context.native_outputs[policy.message_role] + # A binding-owned buffer is never NULL and is never freed here; only + # the adapter's owned-allocation protocol hands back memory the + # binding owns. + binding_owned = any( + result.character_capacity is not None and result.native_result_role == policy.message_role + for result in plan.entrypoint.results + ) + # A hidden message occupies fixed-length native character storage, + # which Fortran blank-pads to the declared width. Bounding the read + # by that width drops the padding instead of reporting it. + if policy.message_character_length is not None: + message_capacity = str(policy.message_character_length) + # A visible argument already owns ``_obj`` for its Python object, + # so the exception string needs a distinct local there. + message_object = f"{message_name}_status_text" if policy.message_argument is not None else f"{message_name}_obj" + if binding_owned: + # Nothing needs freeing, so the Python string is built only on the + # failure path instead of on every successful call. + message_value = ( + f"PyUnicode_FromString((const char *){message_name})" + if message_capacity is None + else (f"prik_status_message_text((const char *){message_name}, (Py_ssize_t)({message_capacity}))") + ) + return ( + CIf( + condition, + body=( + CDeclaration( + message_object, + "PyObject *", + CodeExpression(message_value), + ), + CIf( + CodeExpression(f"{message_object} == NULL"), + body=(*transformation_cleanup, *derived_cleanup, CReturn(CodeExpression("NULL"))), + ), + CExpressionStatement(CodeExpression(f"PyErr_SetObject(PyExc_RuntimeError, {message_object})")), + CExpressionStatement(CodeExpression(f"Py_DECREF({message_object})")), + *transformation_cleanup, + *derived_cleanup, + CReturn(CodeExpression("NULL")), + ), ), + ) + return ( + *( + () + if binding_owned + else ( + CIf( + CodeExpression(f"{message_name} == NULL"), + body=( + CExpressionStatement(CodeExpression("PyErr_NoMemory()")), + *transformation_cleanup, + *derived_cleanup, + CReturn(CodeExpression("NULL")), + ), + ), + ) ), CDeclaration( message_object, "PyObject *", - CodeExpression(f"PyUnicode_FromString((const char *){message_name})"), + CodeExpression( + f"PyUnicode_FromString((const char *){message_name})" + if message_capacity is None + else f"prik_status_message_text((const char *){message_name}, (Py_ssize_t)({message_capacity}))" + ), ), - CExpressionStatement(CodeExpression(f"free({message_name})")), + *(() if binding_owned else (CExpressionStatement(CodeExpression(f"free({message_name})")),)), CIf( CodeExpression(f"{message_object} == NULL"), body=(*transformation_cleanup, *derived_cleanup, CReturn(CodeExpression("NULL"))), @@ -9149,6 +9623,8 @@ def _writeback_value_nodes( action: LifecycleActionPlan, context: _CFunctionContext, converted: tuple[str, ...], + *, + failure_label: str | None = None, ) -> tuple[CDeclaration | CExpressionStatement | CIf, ...]: """Convert one planned writeback without terminating output aggregation.""" if action.binding is None: @@ -9160,8 +9636,20 @@ def _writeback_value_nodes( return self._identity_writeback_value_nodes(source, action, context, converted) if action.binding.codegen_action is CodegenAction.COPY_IN_OUT: if action.binding.datatype_family is DatatypeFamily.STRING: - return self._mixed_string_writeback_nodes(plan, action, context, converted) - return self._scalar_writeback_value_nodes(source, action, context, converted) + return self._mixed_string_writeback_nodes( + plan, + action, + context, + converted, + failure_label=failure_label, + ) + return self._scalar_writeback_value_nodes( + source, + action, + context, + converted, + failure_label=failure_label, + ) raise ValueError(f"Unsupported C writeback action for {action.owner_path!r}: {action.binding.codegen_action!r}") def _identity_writeback_value_nodes( @@ -9205,18 +9693,28 @@ def _scalar_writeback_value_nodes( action: LifecycleActionPlan, context: _CFunctionContext, converted: tuple[str, ...], + *, + failure_label: str | None = None, ) -> tuple[CDeclaration | CExpressionStatement | CIf, ...]: """Convert one mutated scalar storage value for combined aggregation.""" names = context.arguments[source.owner_path] scalar_type = PrimitiveScalarTypeRegistry.type_for(action.binding.semantic_type_name) target = context.python_results[action.owner_path] - cleanup = tuple(CExpressionStatement(CodeExpression(f"Py_DECREF({name})")) for name in converted) + value_name, contract_conversion = self._scalar_writeback_contract_storage(source, names, scalar_type) conversion = CExpressionStatement( - CodeExpression(f"{target} = {self._scalar_result_expression(scalar_type, f'&{names.value_name}')}") + CodeExpression(f"{target} = {self._scalar_result_expression(scalar_type, f'&{value_name}')}") + ) + failure = CIf( + CodeExpression(f"{target} == NULL"), + body=self._output_failure_nodes(converted, failure_label), ) - failure = CIf(CodeExpression(f"{target} == NULL"), body=(*cleanup, CReturn(CodeExpression("NULL")))) if source.entrypoint.descriptor_output_presence_role is None: - return (CDeclaration(target, "PyObject *", CodeExpression("NULL")), conversion, failure) + return ( + CDeclaration(target, "PyObject *", CodeExpression("NULL")), + *contract_conversion, + conversion, + failure, + ) return ( CDeclaration(target, "PyObject *", CodeExpression("NULL")), CIf( @@ -9225,7 +9723,26 @@ def _scalar_writeback_value_nodes( CExpressionStatement(CodeExpression("Py_INCREF(Py_None)")), CExpressionStatement(CodeExpression(f"{target} = Py_None")), ), - else_body=(conversion, failure), + else_body=(*contract_conversion, conversion, failure), + ), + ) + + @staticmethod + def _scalar_writeback_contract_storage( + source: ArgumentTransferPlan, + names: _CArgumentNames, + scalar_type, + ) -> tuple[str, tuple[CDeclaration, ...]]: + """Convert an exact native scalar local back to public contract storage.""" + storage_type = source.native_storage_c_type or scalar_type.c_spelling + if storage_type == scalar_type.c_spelling: + return names.value_name, () + contract_name = f"{names.value_name}_contract" + return contract_name, ( + CDeclaration( + contract_name, + scalar_type.c_spelling, + CodeExpression(f"({scalar_type.c_spelling}){names.value_name}"), ), ) @@ -9434,6 +9951,12 @@ def _argument_context_names(self, argument: ArgumentTransferPlan) -> _CArgumentN f"{local}_polymorphic", ) + def _keyword_declarations(self, plan: FunctionPlan) -> tuple[CDeclaration, ...]: + """Return the keyword table one wrapper needs, or nothing when it takes none.""" + if not plan.binding.accepts_keyword_arguments: + return () + return (self._keyword_declaration(plan),) + def _keyword_declaration(self, plan: FunctionPlan) -> CDeclaration: """Build keyword declaration from the supplied completed binding records; emitted nodes only project completed binding actions.""" keywords = ", ".join( @@ -9452,6 +9975,8 @@ def _parse_statement(self, plan: FunctionPlan, context: _CFunctionContext) -> CE units = "O" * len(required) + ("|" if optional else "") + "O" * len(optional) targets = ", ".join(f"&{context.arguments[item.owner_path].object_name}" for item in arguments) suffix = f", {targets}" if targets else "" + if not plan.binding.accepts_keyword_arguments: + return CExpressionStatement(CodeExpression(f'if (!PyArg_ParseTuple(args, "{units}"{suffix})) return NULL')) return CExpressionStatement( CodeExpression(f'if (!PyArg_ParseTupleAndKeywords(args, kwargs, "{units}", kwlist{suffix})) return NULL') ) @@ -9559,11 +10084,18 @@ def _native_output_declarations( ) ) continue + if result.character_capacity is not None: + # One extra byte so a callee that terminates its own output + # cannot write past the buffer the contract asked for. + declarations.append( + CDeclaration(f"{name}[{result.character_capacity + 1}]", "char", CodeExpression("{0}")) + ) + continue if result.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY, ObjectKind.DERIVED_TYPE}: declarations.append(CDeclaration(name, "void *", CodeExpression("NULL"))) continue scalar_type = PrimitiveScalarTypeRegistry.type_for(result.semantic_type_name) - declarations.append(CDeclaration(name, scalar_type.c_spelling)) + declarations.append(CDeclaration(name, result.native_scalar_c_type or scalar_type.c_spelling)) return tuple(declarations) def _native_call_setup_nodes( @@ -9826,6 +10358,39 @@ def _decref_names(names: tuple[str, ...]) -> tuple[CExpressionStatement, ...]: """Release already-created Python result objects on a later failure.""" return tuple(CExpressionStatement(CodeExpression(f"Py_DECREF({name})")) for name in names) + def _output_failure_nodes( + self, + names: tuple[str, ...], + failure_label: str | None, + ) -> tuple[CExpressionStatement | CGoto | CReturn, ...]: + """Exit one failed output conversion through inline or shared cleanup.""" + if failure_label is not None: + return (CGoto(failure_label),) + return (*self._decref_names(names), CReturn(CodeExpression("NULL"))) + + @staticmethod + def _output_cleanup_label(converted_count: int) -> str: + """Name the cleanup suffix for one successfully converted prefix.""" + if converted_count < 1: + raise ValueError("Output cleanup labels require at least one converted result") + return f"prik_output_cleanup_{converted_count}" + + def _output_cleanup_chain( + self, + converted: tuple[str, ...], + ) -> tuple[CLabel | CExpressionStatement | CReturn, ...]: + """Release a converted prefix through one fallthrough cleanup chain.""" + nodes: list[CLabel | CExpressionStatement | CReturn] = [] + for count in range(len(converted), 0, -1): + nodes.extend( + ( + CLabel(self._output_cleanup_label(count)), + CExpressionStatement(CodeExpression(f"Py_XDECREF({converted[count - 1]})")), + ) + ) + nodes.append(CReturn(CodeExpression("NULL"))) + return tuple(nodes) + @staticmethod def _is_owned_native_array_result(result: ResultPlan | NativeEntrypointResultPlan) -> bool: """Return whether one result owns persistent standard-descriptor storage.""" @@ -9875,6 +10440,8 @@ def _entrypoint_parameter_values( values.append(names.present_name) if argument.entrypoint.descriptor_output_role is not None: values.extend((f"&{names.value_name}", f"&{self._descriptor_output_present_name(names)}")) + if slot.native_scalar_c_type is not None and slot.passing is EntrypointPassingConvention.C_VALUE: + values[0] = f"({slot.native_scalar_c_type}){values[0]}" return tuple(values) if parameter.source_kind == "projected_slot": return self._projected_slot_values( @@ -10003,6 +10570,8 @@ def _entrypoint_hidden_result_values( f"&{name}_itemsize", *(f"&{name}_extent_{axis}" for axis in range(rank)), ) + if result.character_capacity is not None: + return (name,) values = [name if self._is_owned_native_array_result(result) else f"&{name}"] if result.scalar_descriptor is not None: values.append(f"&{name}_present") @@ -10080,6 +10649,12 @@ def _scalar_entrypoint_argument_values( if plan.entrypoint.optional_mode is not OptionalMode.REQUIRED: return (names.nullable_name,) if plan.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: + if plan.entrypoint.pass_character_length: + # Assumed-capacity storage reports the caller's own itemsize. + return ( + names.value_name, + f"(int64_t)PyArray_ITEMSIZE((PyArrayObject *){names.object_name})", + ) return (names.value_name,) if passing is EntrypointPassingConvention.C_VALUE: return (names.value_name,) @@ -10146,6 +10721,17 @@ def _entrypoint_prototype(self, plan: FunctionPlan) -> CFunctionPrototype: for group in sorted(plan.entrypoint.parameters, key=lambda item: item.position) for parameter in self._entrypoint_parameter_declarations(plan, group) ) + direct_c_abi = plan.entrypoint.direct_c_abi + if direct_c_abi is not None: + if len(parameters) != len(direct_c_abi.parameters): + raise ValueError( + f"Direct C entrypoint {plan.owner_path!r} has {len(parameters)} planned parameters " + f"but {len(direct_c_abi.parameters)} preserved C declarations" + ) + parameters = tuple( + CParameter(parameter.name, self._direct_c_abi_declaration_type(abi_type)) + for parameter, abi_type in zip(parameters, direct_c_abi.parameters, strict=True) + ) return CFunctionPrototype( self._entrypoint_function_name(plan), self._entrypoint_return_type(plan), @@ -10212,8 +10798,27 @@ def _default_native_array_bridge_prototypes(self, plan: ModulePlan) -> tuple[CFu ) ) + @staticmethod + def _direct_c_abi_declaration_type(abi_type: DirectCABITypePlan) -> str: + """Render one planned direct-C declaration type. + + A preserved source spelling is emitted verbatim so the generated + prototype stays compatible with the user's declaration. A source-free + contract preserves none, so the backend composes the canonical spelling + from the completed scalar identity and pointer depth. + """ + if abi_type.source_spelling: + return abi_type.source_spelling + canonical = PrimitiveScalarTypeRegistry.type_for(abi_type.scalar_type_name).c_spelling + return f"{canonical} {'*' * abi_type.pointer_depth}" if abi_type.pointer_depth else canonical + def _entrypoint_return_type(self, plan: FunctionPlan) -> str: """Return the direct entrypoint result type, or void for subroutines.""" + direct_c_abi = plan.entrypoint.direct_c_abi + if direct_c_abi is not None: + if direct_c_abi.result is None: + return "void" + return self._direct_c_abi_declaration_type(direct_c_abi.result) result = self._direct_result(plan) if result is None: return "void" @@ -10316,6 +10921,8 @@ def _ordinary_entrypoint_argument_parameters( parameters.append(CParameter(f"{name}_present", "void *")) return tuple(parameters) if argument.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: + if argument.entrypoint.pass_character_length: + return (CParameter(name, "void *"), CParameter(f"{name}_length", "int64_t")) return (CParameter(name, "void *"),) scalar_type = self._scalar_entrypoint_argument_type(argument, passing=passing) if argument.entrypoint.pass_descriptor_presence: @@ -10425,6 +11032,10 @@ def _entrypoint_result_parameters(self, result: NativeEntrypointResultPlan) -> t *(CParameter(f"{name}_extent_{axis}", "int64_t *") for axis in range(rank)), ) return (CParameter(name, "CFI_cdesc_t *"),) + if result.character_capacity is not None: + # Direct C: the binding owns the buffer, so the callee receives a + # plain ``char *`` rather than the adapter's owned-allocation slot. + return (CParameter(name, "char *"),) if result.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY, ObjectKind.DERIVED_TYPE}: return (CParameter(name, "void **"),) scalar_type = PrimitiveScalarTypeRegistry.type_for(result.semantic_type_name).c_spelling @@ -10503,7 +11114,7 @@ def _binding_prototype(self, plan: FunctionPlan, *, external: bool = False) -> C return CFunctionPrototype( self._binding_function_name(plan), "PyObject *", - self._binding_parameters(), + self._binding_parameters(plan), None if external else "static", ) @@ -10800,7 +11411,7 @@ def _method_table(self, module: ModulePlan, namespace: NamespacePlan) -> CMethod CMethodDefEntry( function.binding.python_name, self._binding_function_name(function), - "METH_VARARGS | METH_KEYWORDS", + self._binding_method_flags(function), function.binding.docstring, ) for function in namespace.functions @@ -10820,6 +11431,13 @@ def _method_table(self, module: ModulePlan, namespace: NamespacePlan) -> CMethod ), ) + @staticmethod + def _binding_method_flags(plan: FunctionPlan) -> str: + """Return the CPython call convention selected for one wrapper.""" + if plan.binding.accepts_keyword_arguments: + return "METH_VARARGS | METH_KEYWORDS" + return "METH_VARARGS" + def _overload_method_entries(self, namespace: NamespacePlan) -> tuple[CMethodDefEntry, ...]: """Install public module dispatchers and private class dispatchers.""" return tuple( @@ -10842,7 +11460,13 @@ def _namespace_overload_dispatches(namespace: NamespacePlan) -> tuple[_COverload for surface in namespace.classes: constructor = surface.constructor.overload if constructor is not None and id(constructor) not in seen: - dispatches.append(_COverloadDispatch(constructor, receiver=True, public=False)) + # A constructor overload whose candidates are type-bound takes the + # receiver; one whose candidates are functions returning the type + # -- a Fortran `interface ` -- does not. + constructor_receiver = bool( + constructor.candidate_passed_objects and constructor.candidate_passed_objects[0] + ) + dispatches.append(_COverloadDispatch(constructor, receiver=constructor_receiver, public=False)) seen.add(id(constructor)) for overload in surface.overloads: if id(overload) in seen: @@ -11268,6 +11892,7 @@ def _direct_field_method_names(self, namespace: NamespacePlan) -> tuple[str, ... return tuple( self._derived_field_method_name(derived, field, action) for derived in namespace.derived_types + if not derived.abstract for field in derived.fields for action in self._field_method_actions(field) ) @@ -11647,16 +12272,30 @@ def _module_constant_array_declarations( array = variable.array if array is None or array.rank is None or array.rank <= 0: raise ValueError(f"Module parameter array {variable.owner_path!r} has no fixed array plan") - scalar_type = PrimitiveScalarTypeRegistry.type_for(variable.semantic_type_name) + # A character element is a fixed-width bytes dtype whose width is the + # Fortran element length. The parameter reports that length itself, so + # a `len=*` declaration works the same as a declared one. + character = variable.datatype_family is DatatypeFamily.STRING + itemsize_name = f"{value_name}_itemsize" + if character: + allocation = ( + f"(PyObject *)PyArray_New(&PyArray_Type, {array.rank}, {{dimensions}}, NPY_STRING, " + f"NULL, NULL, (int){itemsize_name}, NPY_ARRAY_F_CONTIGUOUS | NPY_ARRAY_WRITEABLE, NULL)" + ) + else: + scalar_type = PrimitiveScalarTypeRegistry.type_for(variable.semantic_type_name) + allocation = f"(PyObject *)PyArray_EMPTY({array.rank}, {{dimensions}}, {scalar_type.numpy_type_macro}, 1)" + width_names = (itemsize_name,) if character else () extent_names = tuple(f"{value_name}_extent_{axis}" for axis in range(array.rank)) dimensions = f"{value_name}_dimensions" + reported = (*width_names, *extent_names) return ( - *(CDeclaration(extent, "int64_t", CodeExpression("0")) for extent in extent_names), + *(CDeclaration(name, "int64_t", CodeExpression("0")) for name in reported), CDeclaration( value_name, "void *", CodeExpression( - f"{self._module_bridge_getter_name(variable)}({', '.join(f'&{extent}' for extent in extent_names)})" + f"{self._module_bridge_getter_name(variable)}({', '.join(f'&{name}' for name in reported)})" ), ), CExpressionStatement( @@ -11670,9 +12309,7 @@ def _module_constant_array_declarations( CDeclaration( object_name, "PyObject *", - CodeExpression( - f"(PyObject *)PyArray_EMPTY({array.rank}, {dimensions}, {scalar_type.numpy_type_macro}, 1)" - ), + CodeExpression(allocation.format(dimensions=dimensions)), ), CExpressionStatement(CodeExpression(f"if ({object_name} == NULL) {{ Py_DECREF(mod); return NULL; }}")), CExpressionStatement( @@ -11718,21 +12355,25 @@ def _lower_module_literal_complex(self, value: object) -> str: number = complex(value) return f"({number.real!r} + {number.imag!r} * I)" - def _binding_parameters(self) -> tuple[CParameter, ...]: + def _binding_parameters(self, plan: FunctionPlan | None = None) -> tuple[CParameter, ...]: """Build binding parameters from the supplied local lowering values; emitted nodes only project completed binding actions.""" - return ( - CParameter("self", "PyObject *"), - CParameter("args", "PyObject *"), - CParameter("kwargs", "PyObject *"), - ) + parameters = (CParameter("self", "PyObject *"), CParameter("args", "PyObject *")) + if plan is not None and not plan.binding.accepts_keyword_arguments: + return parameters + return (*parameters, CParameter("kwargs", "PyObject *")) def _binding_function_name(self, plan: FunctionPlan) -> str: """Return the binding-local binding function name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"wrap_{plan.symbol_name}" def _entrypoint_function_name(self, plan: FunctionPlan) -> str: - """Return the shared C-ABI function symbol selected by planning.""" - return plan.entrypoint.symbol_name + """Return the symbol the binding declares and calls for one entrypoint. + + Planning selects a collision-adapter forwarder when the binding must + not declare the native symbol itself; the forwarder is defined in the + separate adapter translation unit built by :meth:`binding_modules`. + """ + return plan.entrypoint.collision_adapter_symbol or plan.entrypoint.symbol_name def _module_getter_name(self, plan: ModuleVariablePlan) -> str: """Return the binding-local module getter name derived from the supplied completed binding records; this helper preserves completed policy.""" diff --git a/prik/codegen/c/python_surface.py b/prik/codegen/c/python_surface.py index 558d06c34..7be5ee62e 100644 --- a/prik/codegen/c/python_surface.py +++ b/prik/codegen/c/python_surface.py @@ -277,18 +277,33 @@ def _bound_constructor_python_lines(self, surface: ClassSurfacePlan) -> tuple[st return tuple(lines) def _overloaded_constructor_python_lines(self, surface: ClassSurfacePlan) -> tuple[str, ...]: - """Dispatch one completed constructor overload after owner allocation.""" + """Dispatch one completed constructor overload. + + A type-bound candidate initializes an instance the wrapper allocates + first. A candidate that returns the type -- the specifics of a Fortran + `interface ` -- produces the instance itself, so the dispatch + happens in ``__new__`` and the returned object is the new value. + """ overload = surface.constructor.overload if overload is None: raise ValueError(f"Overloaded constructor {surface.owner_path!r} has no overload plan") + if overload.candidate_passed_objects and overload.candidate_passed_objects[0]: + return ( + " def __new__(cls, *args, **kwargs):", + f" return {CBindingNames.class_create_method(surface)}()", + *self._class_overload_python_lines( + overload, + constructor=True, + docstring=surface.constructor.docstring, + ), + ) + dispatch = CBindingNames.overload_dispatch_method(overload) return ( " def __new__(cls, *args, **kwargs):", - f" return {CBindingNames.class_create_method(surface)}()", - *self._class_overload_python_lines( - overload, - constructor=True, - docstring=surface.constructor.docstring, - ), + f" {surface.constructor.docstring!r}", + f" return {dispatch}(*args, **kwargs)", + " def __init__(self, *args, **kwargs):", + " pass", ) def _class_method_python_lines(self, method: ClassMethodPlan) -> tuple[str, ...]: @@ -476,9 +491,14 @@ def _derived_property_python_lines(field: DerivedFieldPlan) -> tuple[str, ...]: return tuple(lines) def _direct_type_ops_literal(self, derived: DerivedTypePlan) -> str: - """Return the operation dictionary for directly owned native storage.""" + """Return the operation dictionary for directly owned native storage. + + An abstract type publishes no accessor of its own, so its dictionary is + empty; each concrete extension supplies one for every component it + inherits. + """ entries = [] - for field in derived.fields: + for field in () if derived.abstract else derived.fields: entries.append(f"'{field.name}_get': {CBindingNames.derived_field_method(derived, field, 'get')}") if field.setter_action is SetterAction.WRITE_THROUGH: entries.append(f"'{field.name}_set': {CBindingNames.derived_field_method(derived, field, 'set')}") diff --git a/prik/codegen/docstrings.py b/prik/codegen/docstrings.py index 827545739..7007fab34 100644 --- a/prik/codegen/docstrings.py +++ b/prik/codegen/docstrings.py @@ -9,7 +9,8 @@ from __future__ import annotations -from prik.policy.ownership import OwnershipOwner, SetterAction, TransferMode +from prik.codegen.primitive_scalar_types import NativeCArrayStorageRegistry +from prik.policy.ownership import OwnershipOwner, PythonBarrierAction, SetterAction, TransferMode from prik.policy.models import ( ClassConstructorKind, EntrypointOptionalityAction, @@ -676,7 +677,9 @@ def _documented_outputs( for argument in arguments if argument.projects_result and argument.result_position is not None } - by_position.update((result.result_position, result) for result in results) + # A ``Hidden`` result is written by the native call but never published, + # so it is not part of the documented Python signature. + by_position.update((result.result_position, result) for result in results if result.python_returned) return tuple(by_position[position] for position in sorted(by_position)) def _result_summary(self, outputs: tuple[ArgumentTransferPlan | ResultPlan, ...]) -> str: @@ -716,6 +719,7 @@ def _argument_lines(self, argument: ArgumentTransferPlan) -> tuple[str, ...]: nullable = optional or argument.binding.nullable lines = [f"{argument.binding.python_name} : {self._type(argument, nullable=nullable, signature=False)}"] lines.extend(self._array_lines(argument.array)) + lines.extend(self._native_c_array_storage_lines(argument)) 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: @@ -724,6 +728,15 @@ def _argument_lines(self, argument: ArgumentTransferPlan) -> tuple[str, ...]: lines.append(f" Descriptor ownership: {argument.native_array_handle.descriptor_ownership.value}.") return tuple(lines) + @staticmethod + def _native_c_array_storage_lines(argument: ArgumentTransferPlan) -> tuple[str, ...]: + """Document an exact NumPy dtype already selected by completed policy.""" + c_type = argument.binding.native_array_element_c_type + if c_type is None: + return () + storage = NativeCArrayStorageRegistry.type_for(c_type, argument.semantic_type_name) + return (f" Accepts exact {storage.python_type_name} element storage for the native C {c_type} pointer.",) + def _output_lines( self, output: ArgumentTransferPlan | ResultPlan, @@ -791,8 +804,9 @@ def _mutation_lines(argument: ArgumentTransferPlan) -> tuple[str, ...]: """Describe completed native mutation and copy-return projection behavior. Non-mutating arguments produce no note. Copy returns, projected - updates, and in-place storage each retain their established wording; - the helper does not infer mutability from datatype or intent. + updates, call-local scalar addresses, and in-place storage each retain + their established wording; the helper does not infer mutability from + datatype or intent. """ if not argument.mutates_native: return () @@ -803,6 +817,14 @@ def _mutation_lines(argument: ArgumentTransferPlan) -> tuple[str, ...]: ) if argument.projects_result: return (" Native code may update this value; the updated value is returned.",) + if argument.binding.python_action is PythonBarrierAction.SCALAR_VALUE: + # A Python scalar has no caller storage to write through: the + # completed plan converts it into a call-local native value, so a + # native write lands in that temporary and is never read back. + return ( + " Native code may update its call-local copy.", + " The update is not visible in Python.", + ) return (" Native code may update the supplied storage in place.",) def _raise_lines( diff --git a/prik/codegen/fortran/bridge.py b/prik/codegen/fortran/bridge.py index d4626ee25..650c5b3b1 100644 --- a/prik/codegen/fortran/bridge.py +++ b/prik/codegen/fortran/bridge.py @@ -9,6 +9,7 @@ from __future__ import annotations +from collections.abc import Iterable, Mapping from dataclasses import replace import re @@ -41,6 +42,7 @@ ExternalDeclarationMode, ModuleGetterAction, ModuleObjectAccessMechanism, + CharacterLocalRelease, NativeArrayDescriptorKind, NativeArrayDescriptorInterop, NativeArrayDefaultConstruction, @@ -79,6 +81,7 @@ ArgumentTransferPlan, CallbackHandoffPlan, CallbackTransferPlan, + CharacterLocalPlan, ClassSurfacePlan, DatatypeFamily, DeclarationCallablePlan, @@ -106,6 +109,63 @@ from prik.codegen.visitor import ClassVisitor +_MODULE_GETTER_SUMMARIES = { + ModuleGetterAction.CONSTANT_VALUE: "The value is a compile-time constant materialized by the binding.", + ModuleGetterAction.NATIVE_CONSTANT_VALUE: "Returns the compiler-evaluated constant by value.", + ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE: ( + "Copies the parameter array into persistent storage and reports its width and extents." + ), + ModuleGetterAction.DIRECT_VALUE: "Returns the variable's current value.", + ModuleGetterAction.CHARACTER_VALUE: "Copies the characters into a fixed-width byte buffer.", + ModuleGetterAction.NULLABLE_SNAPSHOT: ( + "Copies the value into C-owned storage, or reports a null pointer when it holds nothing." + ), + ModuleGetterAction.BORROWED_ARRAY_VIEW: "Returns the array's address plus its width and extents, without copying.", + ModuleGetterAction.DERIVED_OBJECT: "Returns the address of the derived object.", +} + +_MODULE_ASSIGNMENT_SUMMARIES = { + AssignmentMode.NONE: "No native assignment is generated.", + AssignmentMode.VALUE_COPY: "Copies the incoming value into the variable.", + AssignmentMode.ALIAS: "Points the variable at the incoming storage.", +} + + +def _plan_semantic_type_names(node: object, _seen: set[int] | None = None) -> frozenset[str]: + """Collect every ``semantic_type_name`` reachable from a completed plan node. + + The walk is exhaustive by construction rather than by enumerating plan + shapes, so a lowering mechanism that starts carrying a new scalar cannot + silently lose the ``iso_c_binding`` import that spells it. + """ + seen = set() if _seen is None else _seen + if id(node) in seen: + return frozenset() + seen.add(id(node)) + names: set[str] = set() + if isinstance(node, str | bytes): + return frozenset() + if isinstance(node, Mapping): + for key, value in node.items(): + names |= _plan_semantic_type_names(key, seen) + names |= _plan_semantic_type_names(value, seen) + return frozenset(names) + if isinstance(node, Iterable): + for item in node: + names |= _plan_semantic_type_names(item, seen) + return frozenset(names) + fields = getattr(node, "__dataclass_fields__", None) + if fields is None: + return frozenset() + for field_name in fields: + value = getattr(node, field_name, None) + if field_name == "semantic_type_name" and isinstance(value, str): + names.add(value) + else: + names |= _plan_semantic_type_names(value, seen) + return frozenset(names) + + class FortranBridgeGenerator(ClassVisitor): """Build the Fortran half of a wrapper from validated bridge-plan views. @@ -194,6 +254,11 @@ def _visit_ModulePlan(self, plan: ModulePlan) -> FortranModule: self._derived_owner_paths = { derived.backend_symbol: derived.owner_path for derived in self._derived_types(plan) } + # An abstract native type has no instances of its own, so an adapter + # reaches one only through a concrete extension's address. + self._abstract_backend_symbols = frozenset( + derived.backend_symbol for derived in self._derived_types(plan) if derived.abstract + ) if plan.bridge is None: raise ValueError(f"Fortran lowering requires a bridge plan for {plan.owner_path!r}") self._bridge_allocatable_holder_owner_paths = frozenset(plan.bridge.allocatable_holder_type_owner_paths) @@ -386,6 +451,17 @@ def _support_procedure_fortran_type(value: NativeEntrypointABIValuePlan) -> str if value.kind is NativeEntrypointABIValueKind.DESCRIPTOR: if value.semantic_type_name is None: raise ValueError(f"Generated-support descriptor {value.role!r} has no element type") + if value.semantic_type_name == "String": + if value.descriptor_kind is NativeArrayDescriptorKind.POINTER: + # A bind(C) pointer character dummy has to declare deferred + # length. Pointer assignment takes the length from the + # target, so a declared-width array still associates. + return "character(kind=c_char, len=:)" + # An allocatable descriptor dummy accepts a deferred-length + # actual only when it declares one, so the width the array + # declares is spelled. + length = ":" if value.character_length is None else str(value.character_length) + return f"character(kind=c_char, len={length})" return PrimitiveScalarTypeRegistry.type_for(value.semantic_type_name).fortran_spelling try: return types[value.kind] @@ -486,6 +562,7 @@ def _visit_FunctionPlan( *self._string_address_finalizers(plan), *self._direct_result_finalizers(plan), *self._native_output_finalizers(plan), + *self._character_local_release_finalizers(plan), ) # Stage 3: wrap native execution in derived-result and carrier lifecycles. call_body = self._derived_result_execution(plan, result_name, native_body) @@ -496,6 +573,7 @@ def _visit_FunctionPlan( ) return FortranFunction( name=entrypoint_name, + doc=self._entrypoint_doc(plan, entrypoint_name), parameters=parameters, result_name=result_name, result_type=result_type, @@ -516,6 +594,7 @@ def _visit_FunctionPlan( *self._derived_result_allocation_declarations(plan), ), body=( + *self._character_local_initializers(plan), *self._descriptor_initializers(plan), *self._required_descriptor_initializers(plan), *self._logical_scalar_argument_initializers(plan), @@ -989,19 +1068,27 @@ def _derived_call_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclara declarations = [FortranDeclaration("prik_derived_ready", "logical")] for argument in arguments: name = argument.entrypoint.parameter_name - native_type = f"type({self._derived_native_alias(argument.derived.backend_symbol)})" + abstract = argument.derived.backend_symbol in self._abstract_backend_symbols + declaration_kind = "class" if abstract else "type" + native_type = f"{declaration_kind}({self._derived_native_alias(argument.derived.backend_symbol)})" declarations.extend( ( FortranDeclaration(name, native_type, ("pointer",)), - FortranDeclaration( - f"{name}_allocatable_holder", - f"type({self._allocatable_holder_type_name(argument.derived.backend_symbol)})", - ("pointer",), - ), - FortranDeclaration( - f"{name}_pointer_holder", - f"type({self._pointer_holder_type_name(argument.derived.backend_symbol)})", - ("pointer",), + *( + () + if abstract + else ( + FortranDeclaration( + f"{name}_allocatable_holder", + f"type({self._allocatable_holder_type_name(argument.derived.backend_symbol)})", + ("pointer",), + ), + FortranDeclaration( + f"{name}_pointer_holder", + f"type({self._pointer_holder_type_name(argument.derived.backend_symbol)})", + ("pointer",), + ), + ) ), FortranDeclaration(f"{name}_call_pointer", native_type, ("pointer",)), FortranDeclaration(f"{name}_transaction_address", "type(c_ptr)"), @@ -1334,8 +1421,16 @@ def _derived_transaction_acquisition( acquisition = FortranSelectCase( CodeExpression(f"bound_{name}_access"), ( - FortranCase(5, self._one_derived_transaction_acquisition(argument, allocatable=True)), - FortranCase(6, self._one_derived_transaction_acquisition(argument, allocatable=False)), + *( + (FortranCase(5, self._one_derived_transaction_acquisition(argument, allocatable=True)),) + if self._uses_allocatable_holder(argument) + else () + ), + *( + (FortranCase(6, self._one_derived_transaction_acquisition(argument, allocatable=False)),) + if self._uses_pointer_holder(argument) + else () + ), FortranCase(None, ()), ), ) @@ -1563,18 +1658,20 @@ def _derived_argument_output_and_cleanup(self, argument: ArgumentTransferPlan) - if argument.entrypoint.descriptor_output_role is not None: nodes.append(self._derived_argument_output_finalizer(argument)) else: - nodes.extend( - ( + if self._uses_allocatable_holder(argument): + nodes.append( FortranIf( CodeExpression(f"{name}_created .and. bound_{name}_access == 3_c_int"), body=(FortranDeallocate(f"{name}_allocatable_holder"),), - ), + ) + ) + if self._uses_pointer_holder(argument): + nodes.append( FortranIf( CodeExpression(f"{name}_created .and. bound_{name}_access == 4_c_int"), body=(FortranDeallocate(f"{name}_pointer_holder"),), - ), + ) ) - ) return tuple(nodes) def _derived_argument_output_finalizer(self, argument: ArgumentTransferPlan) -> FortranIf: @@ -1973,12 +2070,34 @@ def _owned_native_array_result_operation_name( def _visit_ModuleVariablePlan(self, plan: ModuleVariablePlan) -> tuple[FortranFunction, ...]: """Lower bridge-owned getter and setter actions into procedures.""" if plan.bridge.native_getter_action is ModuleGetterAction.NATIVE_ARRAY_HANDLE: - return self._lower_module_native_array_operations(plan) + return self._documented( + self._lower_module_native_array_operations(plan), + f"Runtime handle operations for native module variable '{plan.bridge.native_name}'.", + "Each is one operation the generated Python handle calls.", + ) return ( - *self._lower_module_getter(plan), - *self._lower_module_setter(plan), + *self._documented( + self._lower_module_getter(plan), + f"Read native module variable '{plan.bridge.native_name}'.", + _MODULE_GETTER_SUMMARIES.get(plan.bridge.native_getter_action, ""), + ), + *self._documented( + self._lower_module_setter(plan), + f"Write native module variable '{plan.bridge.native_name}'.", + _MODULE_ASSIGNMENT_SUMMARIES.get(plan.bridge.native_assignment, ""), + ), ) + @staticmethod + def _documented(procedures: tuple[FortranFunction, ...], *doc: str) -> tuple[FortranFunction, ...]: + """Attach explanatory prose to generated procedures that carry none. + + The text is emitted as leading comments so a reader opening the + generated module can tell what each procedure is for without + reconstructing it from the wrapper plan. + """ + return tuple(procedure if procedure.doc else replace(procedure, doc=doc) for procedure in procedures) + def _lower_module_getter(self, plan: ModuleVariablePlan) -> tuple[FortranFunction, ...]: """Dispatch one completed bridge getter action explicitly.""" action = plan.bridge.native_getter_action @@ -1991,6 +2110,8 @@ def _lower_module_getter(self, plan: ModuleVariablePlan) -> tuple[FortranFunctio return self._lower_module_getter_constant_array_value(plan) case ModuleGetterAction.DIRECT_VALUE: return self._lower_module_getter_direct_value(plan) + case ModuleGetterAction.CHARACTER_VALUE: + return self._lower_module_getter_character_value(plan) case ModuleGetterAction.NULLABLE_SNAPSHOT: return self._lower_module_getter_nullable_snapshot(plan) case ModuleGetterAction.BORROWED_ARRAY_VIEW: @@ -2533,7 +2654,7 @@ def _module_native_array_descriptor_operation(self, plan: ModuleVariablePlan) -> parameters=( FortranParameter( "descriptor", - self._module_native_array_element_type(plan), + self._module_pointer_dummy_element_type(plan), ("pointer", self._array_dimension_attribute(handle.array.rank), "intent(out)"), ), ), @@ -2623,7 +2744,7 @@ def _module_native_array_associate_operation(self, plan: ModuleVariablePlan) -> parameters=( FortranParameter( "source", - self._module_native_array_element_type(plan), + self._module_pointer_dummy_element_type(plan), ("pointer", self._array_dimension_attribute(handle.array.rank), "intent(in)"), ), ), @@ -2672,11 +2793,49 @@ def _module_native_array_presence_expression(self, plan: ModuleVariablePlan) -> return f"{intrinsic}({self._native_variable_name(plan)})" def _module_native_array_element_type(self, plan: ModuleVariablePlan) -> str: - """Return one numeric or deferred-character module-array element type.""" + """Return one numeric or character module-array element type. + + An allocatable or pointer dummy accepts a deferred-length actual only + when it declares one itself, so a declared-length character array + spells its own width rather than always deferring it. + """ + if plan.datatype_family is DatatypeFamily.STRING: + length = ":" if plan.character_length is None else str(plan.character_length) + return f"character(kind=c_char, len={length})" + return PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name).fortran_spelling + + def _module_pointer_dummy_element_type(self, plan: ModuleVariablePlan) -> str: + """Return the element type of one module pointer dummy. + + A ``bind(C)`` pointer character dummy has to declare deferred length. + Pointer assignment then takes the length from the target, so a module + array that declares its own width still associates through it. + """ if plan.datatype_family is DatatypeFamily.STRING: return "character(kind=c_char, len=:)" return PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name).fortran_spelling + def _module_descriptor_consumer_value_declaration( + self, + plan: ModuleVariablePlan, + rank: int, + ) -> tuple[str, tuple[str, ...]]: + """Return the type and attributes of one descriptor-consumer value dummy. + + A ``bind(C)`` allocatable character dummy has to declare deferred + length, while argument association requires the actual to be deferred + exactly when the dummy is. A module array that declares its own width + satisfies neither together, so it travels as an assumed-length + assumed-shape dummy whose descriptor still carries the element length. + The runtime never reaches this operation while the array is + unallocated: ``AllocatableArray.to_numpy`` and ``shape`` both return + early on ``allocated``. + """ + dimension = self._array_dimension_attribute(rank) + if plan.datatype_family is DatatypeFamily.STRING and plan.character_length is not None: + return "character(kind=c_char, len=*)", (dimension, "intent(in)") + return self._module_native_array_element_type(plan), ("allocatable", dimension, "intent(in)") + def _module_native_array_operation_name(self, plan: ModuleVariablePlan, operation) -> str: """Return one planner-owned module native-array operation symbol.""" return self._generated_support_procedure_entrypoint( @@ -2697,6 +2856,56 @@ def _lower_module_getter_direct_value(self, plan: ModuleVariablePlan) -> tuple[F ), ) + def _module_character_length(self, plan: ModuleVariablePlan) -> int: + """Return the declared width one character module accessor copies.""" + length = plan.character_length + if length is None or length <= 0: + raise ValueError(f"Character module variable {plan.owner_path!r} has no declared length") + return length + + def _lower_module_getter_character_value(self, plan: ModuleVariablePlan) -> tuple[FortranFunction, ...]: + """Copy one fixed native character module variable into a C byte buffer. + + A character value has no by-value C ABI, so it travels the same + fixed-width buffer a character field already uses. + """ + length = self._module_character_length(plan) + name = self._module_bridge_getter_name(plan) + return ( + FortranFunction( + name=name, + parameters=( + FortranParameter("value", "character(kind=c_char)", (f"dimension({length})", "intent(out)")), + ), + bind_name=name, + body=( + FortranAssignment("value", CodeExpression(f"transfer({self._native_variable_name(plan)}, value)")), + ), + is_subroutine=True, + ), + ) + + def _lower_module_setter_character_value(self, plan: ModuleVariablePlan) -> tuple[FortranFunction, ...]: + """Copy one exact-width C byte buffer into a native character module variable.""" + length = self._module_character_length(plan) + name = self._module_bridge_setter_name(plan) + return ( + FortranFunction( + name=name, + parameters=( + FortranParameter("value", "character(kind=c_char)", (f"dimension({length})", "intent(in)")), + ), + bind_name=name, + body=( + FortranAssignment( + self._native_variable_name(plan), + CodeExpression(f"transfer(value, {self._native_variable_name(plan)})"), + ), + ), + is_subroutine=True, + ), + ) + def _lower_module_getter_constant_array_value(self, plan: ModuleVariablePlan) -> tuple[FortranFunction, ...]: """Copy one compiler-owned parameter array into persistent bridge storage. @@ -2707,16 +2916,26 @@ def _lower_module_getter_constant_array_value(self, plan: ModuleVariablePlan) -> array = plan.array if array is None or array.rank is None or array.rank <= 0: raise ValueError(f"Module parameter array {plan.owner_path!r} has no fixed array plan") - scalar_type = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) name = self._module_bridge_getter_name(plan) native = self._native_variable_name(plan) snapshot = "parameter_snapshot" extents = tuple(f"extent_{axis}" for axis in range(array.rank)) + character = plan.datatype_family is DatatypeFamily.STRING + # A character parameter always knows its own width, even when the + # declaration spells `len=*` and takes it from an initializer prik does + # not evaluate, so the element length is read from the parameter itself. + element_type = ( + f"character(kind=c_char, len=len({native}))" + if character + else PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name).fortran_spelling + ) + width = ("itemsize",) if character else () return ( FortranFunction( name=name, parameters=tuple( - FortranParameter(extent, "integer(c_int64_t)", ("intent(out)",)) for extent in extents + FortranParameter(reported, "integer(c_int64_t)", ("intent(out)",)) + for reported in (*width, *extents) ), result_name="result", result_type="type(c_ptr)", @@ -2725,7 +2944,7 @@ def _lower_module_getter_constant_array_value(self, plan: ModuleVariablePlan) -> declarations=( FortranDeclaration( snapshot, - scalar_type.fortran_spelling, + element_type, ("allocatable", "target", "save", self._array_dimension_attribute(array.rank)), ), FortranDeclaration("allocation_status", "integer(c_int)"), @@ -2747,6 +2966,10 @@ def _lower_module_getter_constant_array_value(self, plan: ModuleVariablePlan) -> CodeExpression("allocation_status == 0_c_int"), body=( FortranAssignment(snapshot, CodeExpression(native)), + *( + FortranAssignment("itemsize", CodeExpression(f"len({native}, kind=c_int64_t)")) + for _ in width + ), *( FortranAssignment( extent, @@ -2771,23 +2994,29 @@ def _lower_module_getter_borrowed_array_view( raise ValueError(f"Module array view {plan.owner_path!r} has no fixed rank") name = self._module_bridge_getter_name(plan) native = self._native_variable_name(plan) + # A character element reports the width its own declaration carries, + # for the same reason a copied parameter does: the length belongs to + # the Fortran variable, not to anything the binding can restate. + width = ("itemsize",) if plan.datatype_family is DatatypeFamily.STRING else () + extents = tuple(f"extent_{axis}" for axis in range(array.rank)) return ( FortranFunction( name=name, parameters=tuple( - FortranParameter(f"extent_{axis}", "integer(c_int64_t)", ("intent(out)",)) - for axis in range(array.rank) + FortranParameter(reported, "integer(c_int64_t)", ("intent(out)",)) + for reported in (*width, *extents) ), result_name="result", result_type="type(c_ptr)", bind_name=name, body=( + *(FortranAssignment("itemsize", CodeExpression(f"len({native}, kind=c_int64_t)")) for _ in width), *( FortranAssignment( - f"extent_{axis}", + extent, CodeExpression(f"int(size({native}, {axis + 1}), c_int64_t)"), ) - for axis in range(array.rank) + for axis, extent in enumerate(extents) ), FortranAssignment("result", CodeExpression(f"c_loc({native})")), ), @@ -2800,7 +3029,69 @@ def _lower_module_getter_nullable_snapshot( ) -> tuple[FortranFunction, ...]: """Return a nullable detached snapshot through C-owned storage.""" presence = "allocated" if plan.entrypoint.descriptor_kind == "allocatable" else "associated" - return self._lower_nullable_module_getter(plan, f"{presence}({self._native_variable_name(plan)})") + condition = f"{presence}({self._native_variable_name(plan)})" + if plan.datatype_family is DatatypeFamily.STRING: + return self._lower_nullable_character_module_getter(plan, condition) + return self._lower_nullable_module_getter(plan, condition) + + def _lower_nullable_character_module_getter( + self, + plan: ModuleVariablePlan, + condition: str, + ) -> tuple[FortranFunction, ...]: + """Build one nullable detached character snapshot with its runtime width. + + A descriptor character has no width until it is allocated, so the + length travels beside the copied bytes rather than being known here. + """ + name = self._module_bridge_getter_name(plan) + native = self._native_variable_name(plan) + return ( + FortranFunction( + name=name, + parameters=(FortranParameter("length", "integer(c_int64_t)", ("intent(out)",)),), + result_name="result", + result_type="type(c_ptr)", + bind_name=name, + declarations=(FortranDeclaration("copy", "character(kind=c_char)", ("pointer", "dimension(:)")),), + body=( + FortranAssignment("result", CodeExpression("c_null_ptr")), + FortranAssignment("length", CodeExpression("0_c_int64_t")), + FortranIf( + CodeExpression(condition), + body=( + FortranAssignment("length", CodeExpression(f"len({native}, kind=c_int64_t)")), + FortranAssignment( + "result", + CodeExpression("c_malloc(max(1_c_size_t, int(length, c_size_t)))"), + ), + FortranIf( + CodeExpression("c_associated(result)"), + body=( + FortranCall( + "c_f_pointer", + ( + CodeExpression("result"), + CodeExpression("copy"), + CodeExpression("[length]"), + ), + ), + FortranIf( + CodeExpression("length > 0_c_int64_t"), + body=( + FortranAssignment( + "copy(1:length)", + CodeExpression(f"transfer({native}, copy(1:length))"), + ), + ), + ), + ), + ), + ), + ), + ), + ), + ) def _lower_nullable_module_getter( self, @@ -2853,6 +3144,8 @@ def _lower_module_setter(self, plan: ModuleVariablePlan) -> tuple[FortranFunctio return self._lower_module_setter_none(plan) case AssignmentMode.VALUE_COPY: return self._lower_module_setter_value_copy(plan) + case AssignmentMode.CHARACTER_COPY: + return self._lower_module_setter_character_value(plan) raise ValueError(f"Unsupported Fortran module setter assignment for {plan.owner_path!r}: {action!r}") def _lower_module_setter_none(self, _plan: ModuleVariablePlan) -> tuple[FortranFunction, ...]: @@ -2873,6 +3166,44 @@ def _lower_module_setter_value_copy(self, plan: ModuleVariablePlan) -> tuple[For ), ) + def _entrypoint_doc(self, plan: FunctionPlan, entrypoint_name: str) -> tuple[str, ...]: + """Describe one adapter: who calls it, what it calls, and what it converts. + + The adapter exists because the original procedure is not callable + across the C ABI as declared, so the summary names the conversions that + difference forces rather than restating the signature. + """ + # Only bridge and entrypoint facts are read here: the Python-visible + # name belongs to the binding facet, which this generator never reads. + lines = [ + f"Adapter for native procedure '{plan.bridge.native_name}'.", + f"Exported to the binding as the C symbol '{entrypoint_name}'.", + ] + work = self._entrypoint_doc_conversions(plan) + if work: + lines.append(f"Converts: {'; '.join(work)}.") + return tuple(lines) + + def _entrypoint_doc_conversions(self, plan: FunctionPlan) -> tuple[str, ...]: + """Summarize the conversions this adapter performs, in argument order.""" + notes: list[str] = [] + for argument in plan.arguments: + name = argument.entrypoint.parameter_name + if argument.entrypoint.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER: + local = argument.bridge.character_local if argument.bridge is not None else None + attribute = local.descriptor_kind.value if local and local.descriptor_kind else "fixed-length" + article = "an" if attribute[0] in "aeiou" else "a" + notes.append(f"'{name}' byte buffer into {article} {attribute} character local") + elif argument.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: + notes.append(f"'{name}' buffer into a Fortran array actual") + elif argument.entrypoint.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: + notes.append(f"'{name}' native descriptor") + for result in plan.results: + if result.scalar_descriptor is not None: + role = "updated value" if result.updates_argument else "descriptor result" + notes.append(f"copies out the {role} for '{result.owner_path.rsplit('.', 1)[-1]}'") + return tuple(notes) + def _visit_ArgumentTransferPlan(self, plan: ArgumentTransferPlan) -> tuple[FortranParameter, ...]: """Lower one argument through the completed optional-mode action.""" return self._lower_argument(plan) @@ -3047,7 +3378,18 @@ def _lower_argument_required(self, plan: ArgumentTransferPlan) -> tuple[FortranP case ArgumentHandoffMode.TYPED_REFERENCE: return self._lower_argument_required_typed_reference(plan) case ArgumentHandoffMode.OPAQUE_ADDRESS: - return self._lower_argument_required_opaque_address(plan) + return ( + *self._lower_argument_required_opaque_address(plan), + *( + ( + FortranParameter( + f"{plan.entrypoint.parameter_name}_length", "integer(c_int64_t)", ("value",) + ), + ) + if plan.entrypoint.pass_character_length + else () + ), + ) case ArgumentHandoffMode.CHARACTER_BUFFER: return self._lower_argument_string_value(plan) raise ValueError(f"Unsupported Fortran argument handoff for {plan.owner_path!r}: {mode!r}") @@ -3465,6 +3807,11 @@ def _native_result_expression_invocation( self._owned_direct_array_result_collector_name(), (CodeExpression(expression), CodeExpression("result")), ) + if self._uses_allocatable_character_result_collector(direct_result): + return FortranCall( + self._allocatable_character_result_collector_name(), + (CodeExpression(expression), CodeExpression("result_value")), + ) if self._uses_pointer_result_assignment(direct_result): return FortranPointerAssignment(result_name, CodeExpression(expression)) return FortranAssignment(result_name, CodeExpression(expression)) @@ -4333,8 +4680,14 @@ def _array_element_fortran_type(self, argument: ArgumentTransferPlan) -> str: """Return the completed primitive or fixed-width character element type.""" array = argument.array if argument.datatype_family is DatatypeFamily.STRING: - if array is None or array.itemsize is None or array.itemsize <= 0: - raise ValueError(f"Character array {argument.owner_path!r} has no fixed itemsize") + if array is None: + raise ValueError(f"Character array {argument.owner_path!r} has no shape plan") + if array.itemsize is None: + # Every element of the caller's array shares one width, which + # the ABI already reports beside the buffer. + return f"character(kind=c_char, len={argument.entrypoint.parameter_name}_itemsize)" + if array.itemsize <= 0: + raise ValueError(f"Character array {argument.owner_path!r} has a non-positive itemsize") return f"character(kind=c_char, len={array.itemsize})" return PrimitiveScalarTypeRegistry.type_for(argument.semantic_type_name).fortran_spelling @@ -4411,11 +4764,21 @@ def _string_address_arguments(self, plan: FunctionPlan) -> tuple[ArgumentTransfe and argument.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION ) - def _string_address_length(self, plan: ArgumentTransferPlan) -> int: - """Return the fixed extent already completed in the shared plan.""" + def _string_address_length(self, plan: ArgumentTransferPlan) -> str: + """Return the extent expression completed in the shared plan. + + A declared width is spelled as a literal. Assumed-capacity storage has + no compile-time width, so the plan asks for the caller's itemsize + alongside the address and the extent names that runtime dummy. + """ + if plan.entrypoint.pass_character_length: + # NumPy-backed storage reports the caller's own itemsize. + return f"{plan.entrypoint.parameter_name}_length" + # A raw address carries no measurable width, so the contract's is all + # there is. if plan.character_length is None or plan.character_length <= 0: raise ValueError(f"String address {plan.owner_path!r} is missing a fixed character length") - return plan.character_length + return str(plan.character_length) # String value bridge storage. def _string_value_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, ...]: @@ -4432,11 +4795,57 @@ def _string_value_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclara "character(kind=c_char)", ("pointer", "dimension(:)"), ), - FortranDeclaration(name, f"character(kind=c_char, len={name}_length)"), + self._string_value_declaration(argument, name), ) ) + if self._retains_character_local_seed(argument): + declarations.append(self._string_value_declaration(argument, f"{name}_seed")) return tuple(declarations) + @staticmethod + def _character_local(plan: ArgumentTransferPlan) -> CharacterLocalPlan: + """Return the completed adapter-local character storage for one input.""" + local = plan.bridge.character_local + if local is None: + raise ValueError(f"String input {plan.owner_path!r} is missing completed character-local policy") + return local + + @classmethod + def _retains_character_local_seed(cls, plan: ArgumentTransferPlan) -> bool: + """Report whether the adapter keeps a second pointer to the storage it allocated. + + A pointer dummy the native procedure may reassociate makes the dummy an + unreliable handle on that allocation, so the completed release action + asks for a seed pointer to compare against afterwards. + """ + return cls._character_local(plan).release is CharacterLocalRelease.DEALLOCATE_IF_RETAINED + + @classmethod + def _string_value_declaration(cls, plan: ArgumentTransferPlan, name: str) -> FortranDeclaration: + """Declare the native character local selected by completed bridge policy. + + The C ABI is a byte buffer and a length whatever the dummy declares, so + only the local changes: an ``allocatable`` or ``pointer`` dummy needs a + local carrying the same attribute, and a deferred-length dummy is not + interoperable at all, so no ``bind(C)`` interface could declare it. + + A descriptor local also takes its fixed length from the plan rather than + from the runtime length beside the buffer. Neither length is deferred + there, so the standard requires the actual and the dummy to agree, and + the declared length is what lets the compiler check that they do. + """ + local = cls._character_local(plan) + if local.deferred_length: + length = ":" + elif local.descriptor_kind is not None and plan.character_length is not None: + length = str(plan.character_length) + else: + length = f"{plan.entrypoint.parameter_name}_length" + spelling = f"character(kind=c_char, len={length})" + if local.descriptor_kind is None: + return FortranDeclaration(name, spelling) + return FortranDeclaration(name, spelling, (local.descriptor_kind.value,)) + def _string_value_initializers( self, plan: FunctionPlan, @@ -4456,15 +4865,19 @@ def _string_value_initializers( def _string_value_initializer_nodes( self, plan: ArgumentTransferPlan, - ) -> tuple[FortranCall | FortranAssignment, ...]: + ) -> tuple[FortranCall | FortranAssignment | FortranAllocate | FortranPointerAssignment, ...]: """Associate and materialize one present string payload.""" name = plan.entrypoint.parameter_name + local = self._character_local(plan) extent = f"{name}_length + 1" if plan.bridge.codegen_action is CodegenAction.COPY_IN_OUT else f"{name}_length" source = ( f"{name}_bytes(1:{name}_length)" if plan.bridge.codegen_action is CodegenAction.COPY_IN_OUT else f"{name}_bytes" ) + # A deferred-length local has no length until it is allocated, so its + # mold spells the width instead of naming storage that does not exist. + mold = f"repeat(' ', {name}_length)" if local.deferred_length else name return ( FortranCall( "c_f_pointer", @@ -4474,9 +4887,41 @@ def _string_value_initializer_nodes( CodeExpression(f"[{extent}]"), ), ), - FortranAssignment(name, CodeExpression(f"transfer({source}, {name})")), + *self._character_local_allocation_nodes(plan, name), + FortranAssignment(name, CodeExpression(f"transfer({source}, {mold})")), + *self._character_local_seed_nodes(plan, name), ) + def _character_local_allocation_nodes( + self, + plan: ArgumentTransferPlan, + name: str, + ) -> tuple[FortranAllocate, ...]: + """Allocate the adapter local that intrinsic assignment cannot establish. + + Assignment allocates a deferred-length allocatable on its own, so only a + pointer local, and a fixed-length allocatable whose mold would otherwise + be unallocated storage, need an explicit allocation first. + """ + local = self._character_local(plan) + if local.descriptor_kind is None: + return () + if local.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE and local.deferred_length: + return () + if local.deferred_length: + return (FortranAllocate(f"character(kind=c_char, len={name}_length) :: {name}"),) + return (FortranAllocate(name),) + + def _character_local_seed_nodes( + self, + plan: ArgumentTransferPlan, + name: str, + ) -> tuple[FortranPointerAssignment, ...]: + """Record the allocation a reassociable pointer local started out holding.""" + if not self._retains_character_local_seed(plan): + return () + return (FortranPointerAssignment(f"{name}_seed", CodeExpression(name)),) + def _string_value_finalizers( self, plan: FunctionPlan, @@ -4500,6 +4945,54 @@ def _string_value_finalizers( raise ValueError(f"Unsupported Fortran string finalizer for {argument.owner_path!r}: {action!r}") return tuple(nodes) + def _character_local_initializers(self, plan: FunctionPlan) -> tuple[FortranPointerAssignment, ...]: + """Disassociate pointer character locals before any presence branch runs. + + An absent optional argument never reaches the allocation, so without + this the local's association status stays undefined and both the + copy-out test and the release test read it. + """ + nodes = [] + for argument in plan.arguments: + if argument.entrypoint.handoff_mode is not ArgumentHandoffMode.CHARACTER_BUFFER: + continue + local = self._character_local(argument) + if local.descriptor_kind is not NativeArrayDescriptorKind.POINTER: + continue + name = argument.entrypoint.parameter_name + nodes.append(FortranPointerAssignment(name, CodeExpression("null()"))) + if self._retains_character_local_seed(argument): + nodes.append(FortranPointerAssignment(f"{name}_seed", CodeExpression("null()"))) + return tuple(nodes) + + def _character_local_release_finalizers(self, plan: FunctionPlan) -> tuple[FortranIf | FortranDeallocate, ...]: + """Free the character locals the adapter allocated, after every value is read. + + Only a pointer local is adapter-owned storage; an allocatable local is + released by the compiler. A read-only pointer dummy cannot change its + association, so its allocation is always the one still in hand. An + update dummy may have been reassociated or deallocated by the native + procedure, so the adapter frees its allocation only while the dummy + still identifies it, leaving native-owned storage untouched. + """ + nodes: list[FortranIf | FortranDeallocate] = [] + for argument in plan.arguments: + if argument.entrypoint.handoff_mode is not ArgumentHandoffMode.CHARACTER_BUFFER: + continue + release = self._character_local(argument).release + if release is CharacterLocalRelease.NONE: + continue + name = argument.entrypoint.parameter_name + # An absent optional argument skipped the allocation entirely, so + # every release is guarded by what the local actually holds. + condition = ( + f"associated({name})" + if release is CharacterLocalRelease.DEALLOCATE + else f"associated({name}, {name}_seed)" + ) + nodes.append(FortranIf(CodeExpression(condition), body=(FortranDeallocate(name),))) + return tuple(nodes) + def _lower_argument_string_copyback( self, plan: ArgumentTransferPlan, @@ -4682,6 +5175,46 @@ def _native_output_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclar f"Unsupported native-output bridge data action for {slot.owner_path!r}: " f"{slot.adapter.bridge_data_action!r}" ) + declarations.extend(self._argument_update_declarations(plan)) + return tuple(declarations) + + def _argument_update_results( + self, + plan: FunctionPlan, + ) -> tuple[tuple[ResultPlan, ArgumentTransferPlan], ...]: + """Pair each argument-update result with the input storage it returns. + + A character descriptor update has no result call slot: the native + procedure receives the adapter's call-local input and may reallocate or + reassociate it, so the copied-out value is read from that same local. + """ + arguments = {argument.owner_path: argument for argument in plan.arguments} + pairs = [] + for result in sorted(plan.results, key=lambda item: item.result_position): + if not result.updates_argument: + continue + argument = arguments.get(result.owner_path) + if argument is None: + raise ValueError(f"Argument update {result.owner_path!r} has no completed input transfer") + if result.scalar_descriptor is None: + raise ValueError(f"Argument update {result.owner_path!r} has no completed descriptor result") + pairs.append((result, argument)) + return tuple(pairs) + + @staticmethod + def _argument_update_names(result: ResultPlan, argument: ArgumentTransferPlan) -> tuple[str, str]: + """Return the planned output-group name and the input local it reads.""" + name = result.entrypoint.parameter_name + if name is None: + raise ValueError(f"Argument update {result.owner_path!r} has no entrypoint parameter name") + return name, argument.entrypoint.parameter_name + + def _argument_update_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, ...]: + """Declare detached-copy storage for every completed argument update.""" + declarations = [] + for result, argument in self._argument_update_results(plan): + name, value_name = self._argument_update_names(result, argument) + declarations.extend(self._scalar_descriptor_copy_declarations(result, name, value_name=value_name)) return tuple(declarations) def _direct_result_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, ...]: @@ -4879,23 +5412,39 @@ def _scalar_descriptor_copy_declarations( self, result: ResultPlan | NativeEntrypointProjectedSlotPlan, name: str, + *, + value_name: str | None = None, ) -> tuple[FortranDeclaration, ...]: - """Declare helper-local storage selected by a scalar descriptor plan.""" + """Declare helper-local storage selected by a scalar descriptor plan. + + ``value_name`` names storage another facet already declares, as an + argument update does with its call-local input; only the detached copy + pointer is then declared here. + """ descriptor = result.scalar_descriptor if descriptor is None: return () attribute = "allocatable" if descriptor.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE else "pointer" - value_name = f"{name}_value" copy_name = f"{name}_copy" if result.object_kind is ObjectKind.STRING: + copy = FortranDeclaration(copy_name, "character(kind=c_char)", ("pointer", "dimension(:)")) + if value_name is not None: + return (copy,) + # An allocatable or pointer dummy accepts a deferred-length actual + # only when it declares one itself, so the local mirrors the + # completed length instead of always deferring it. + length = ":" if result.character_length is None else str(result.character_length) return ( - FortranDeclaration(value_name, "character(kind=c_char, len=:)", (attribute,)), - FortranDeclaration(copy_name, "character(kind=c_char)", ("pointer", "dimension(:)")), + FortranDeclaration(f"{name}_value", f"character(kind=c_char, len={length})", (attribute,)), + copy, ) scalar_type = PrimitiveScalarTypeRegistry.type_for(result.semantic_type_name) + copy = FortranDeclaration(copy_name, scalar_type.fortran_spelling, ("pointer",)) + if value_name is not None: + return (copy,) return ( - FortranDeclaration(value_name, scalar_type.fortran_spelling, (attribute,)), - FortranDeclaration(copy_name, scalar_type.fortran_spelling, ("pointer",)), + FortranDeclaration(f"{name}_value", scalar_type.fortran_spelling, (attribute,)), + copy, ) # Ordinary-array result storage. @@ -5061,9 +5610,66 @@ def _owned_direct_native_array_result_finalizers( 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): + if result is None: return () - return (self._owned_direct_array_result_collector(result),) + if self._uses_owned_direct_array_result_collector(plan): + return (self._owned_direct_array_result_collector(result),) + if self._uses_allocatable_character_result_collector(result): + return (self._allocatable_character_result_collector(result),) + return () + + @classmethod + def _uses_allocatable_character_result_collector(cls, result: ResultPlan | None) -> bool: + """Return whether a direct character result travels through the move helper. + + Whether the storage may be absent is a completed policy fact, exactly as + it is for an owned array result; this only selects the lowering it asks + for. + """ + descriptor = result.scalar_descriptor if result is not None else None + return bool( + result is not None + and descriptor is not None + and result.object_kind is ObjectKind.STRING + and descriptor.may_be_unallocated + ) + + @staticmethod + def _allocatable_character_result_collector_name() -> str: + """Return the fixed internal helper name for collecting allocatable character results.""" + return "prik_collect_allocatable_character_result" + + @classmethod + def _allocatable_character_result_collector(cls, result: ResultPlan) -> FortranFunction: + """Move an allocatable character function result without assigning it directly. + + Intrinsic assignment reads the result, which is not permitted when the + function left it unallocated. Receiving it through an allocatable dummy + makes allocation a testable fact, so an unallocated result becomes the + Python ``None`` the descriptor contract already describes rather than a + read of storage that was never established. + """ + length = ":" if result.character_length is None else str(result.character_length) + element_type = f"character(kind=c_char, len={length})" + return FortranFunction( + name=cls._allocatable_character_result_collector_name(), + parameters=( + FortranParameter("value", element_type, ("allocatable",)), + FortranParameter("result", element_type, ("allocatable", "intent(out)")), + ), + body=( + FortranIf( + CodeExpression("allocated(value)"), + body=( + FortranCall( + "move_alloc", + (CodeExpression("value"), CodeExpression("result")), + ), + ), + ), + ), + is_subroutine=True, + ) def _owned_direct_array_result_collector(self, result: ResultPlan) -> FortranFunction: """Move a GNU allocatable function result without the crashing assignment path.""" @@ -5246,18 +5852,34 @@ def _native_output_finalizers( f"Unsupported native-output bridge data action for {slot.owner_path!r}: " f"{slot.adapter.bridge_data_action!r}" ) + nodes.extend(self._argument_update_finalizers(plan)) + return tuple(nodes) + + def _argument_update_finalizers(self, plan: FunctionPlan) -> tuple[FortranAssignment | FortranIf, ...]: + """Copy every reallocated call-local input into its C-owned output group.""" + nodes: list[FortranAssignment | FortranIf] = [] + for result, argument in self._argument_update_results(plan): + name, value_name = self._argument_update_names(result, argument) + nodes.extend(self._scalar_descriptor_copy_nodes(result, name, value_name=value_name)) return tuple(nodes) def _scalar_descriptor_copy_nodes( self, result: ResultPlan | NativeEntrypointProjectedSlotPlan, name: str, + *, + value_name: str | None = None, ) -> tuple[FortranAssignment | FortranIf, ...]: - """Copy one present scalar descriptor payload into C-owned storage.""" + """Copy one present scalar descriptor payload into C-owned storage. + + ``value_name`` overrides the native local read after the call, which an + argument update points at the call-local input the native procedure may + have reallocated. + """ descriptor = result.scalar_descriptor if descriptor is None: return () - value_name = f"{name}_value" + value_name = value_name or f"{name}_value" copy_name = f"{name}_copy" present = "allocated" if descriptor.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE else "associated" initializers: list[FortranAssignment | FortranIf] = [ @@ -5812,6 +6434,11 @@ def _uses_allocatable_holder(argument: ArgumentTransferPlan) -> bool: """Return whether the module plan requires the allocatable holder for one native derived identity.""" return FortranBridgeGenerator._uses_holder(argument, DerivedActualAccess.ALLOCATABLE_HOLDER) + @staticmethod + def _uses_pointer_holder(argument: ArgumentTransferPlan) -> bool: + """Return whether the completed matrix keeps the pointer holder for one carrier.""" + return FortranBridgeGenerator._uses_holder(argument, DerivedActualAccess.POINTER_HOLDER) + @staticmethod def _uses_holder(argument: ArgumentTransferPlan, access: DerivedActualAccess) -> bool: """Return whether one completed derived matrix includes a holder row.""" @@ -5836,6 +6463,7 @@ def _direct_field_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFunc return tuple( procedure for derived in self._derived_types(plan) + if not derived.abstract for field in derived.fields for procedure in self._planned_support_procedures( f"{derived.owner_path}.{field.name}", @@ -7533,11 +8161,7 @@ def _module_descriptor_callback_interface( name=self._module_descriptor_callback_interface_name(plan), imports=(self._iso_symbol(plan.semantic_type_name), "c_ptr"), parameters=( - FortranParameter( - "value", - self._module_native_array_element_type(plan), - ("allocatable", self._array_dimension_attribute(handle.array.rank), "intent(in)"), - ), + FortranParameter("value", *self._module_descriptor_consumer_value_declaration(plan, handle.array.rank)), FortranParameter("context", "type(c_ptr)", ("value",)), ), is_subroutine=True, @@ -8062,10 +8686,17 @@ def _iso_symbol(self, semantic_type_name: str) -> str: "Int16": "c_int16_t", "Int32": "c_int32_t", "Int64": "c_int64_t", + "UInt8": "c_int8_t", + "UInt16": "c_int16_t", + "UInt32": "c_int32_t", + "UInt64": "c_int64_t", + "SizeT": "c_size_t", "Float32": "c_float", "Float64": "c_double", + "Float128": "c_long_double", "Complex64": "c_float_complex", "Complex128": "c_double_complex", + "Complex256": "c_long_double_complex", "String": "c_char", } return symbols[semantic_type_name] @@ -8093,12 +8724,27 @@ def _iso_c_symbols(self, plan: ModulePlan) -> tuple[str, ...]: "c_size_t", "c_sizeof", ] + symbols.extend(self._extended_precision_iso_symbols(plan)) if self._uses_c_function_pointer_symbols(plan): symbols.extend(("c_funptr", "c_f_procpointer")) if self._uses_derived_interop_symbols(plan): symbols.extend(("c_funloc", "c_funptr", "c_f_procpointer")) return tuple(dict.fromkeys(symbols)) + def _extended_precision_iso_symbols(self, plan: ModulePlan) -> tuple[str, ...]: + """Return the extended-precision kind imports the completed plan actually spells. + + ``c_long_double`` is imported only when the plan uses it, so an ordinary + module's generated bridge keeps the import list it already had. + """ + names = _plan_semantic_type_names(plan) + symbols = [] + if "Float128" in names: + symbols.append("c_long_double") + if "Complex256" in names: + symbols.append("c_long_double_complex") + return tuple(symbols) + def _uses_c_function_pointer_symbols(self, plan: ModulePlan) -> bool: """Return whether completed module or field descriptor actions require C procedure-pointer support.""" module_descriptors = any( diff --git a/prik/codegen/nodes.py b/prik/codegen/nodes.py index 168b530d3..54fd96dc2 100644 --- a/prik/codegen/nodes.py +++ b/prik/codegen/nodes.py @@ -163,6 +163,20 @@ class CExpressionStatement(StageRecord): expression: CodeExpression +@dataclass +class CGoto(StageRecord): + """C jump to a function-local cleanup label.""" + + label: str + + +@dataclass +class CLabel(StageRecord): + """C function-local label used by shared cleanup paths.""" + + name: str + + @dataclass class CAllowThreadsBegin(StageRecord): """Release the CPython GIL immediately before one native call.""" @@ -178,8 +192,8 @@ class CIf(StageRecord): """C conditional with recursively printable statement bodies.""" condition: CodeExpression - body: tuple[CDeclaration | CExpressionStatement | CIf | CFor | CReturn, ...] = () - else_body: tuple[CDeclaration | CExpressionStatement | CIf | CFor | CReturn, ...] = () + body: tuple[CDeclaration | CExpressionStatement | CGoto | CIf | CFor | CReturn, ...] = () + else_body: tuple[CDeclaration | CExpressionStatement | CGoto | CIf | CFor | CReturn, ...] = () @dataclass @@ -230,6 +244,8 @@ class CFunction(StageRecord): body: tuple[ CDeclaration | CExpressionStatement + | CGoto + | CLabel | CAllowThreadsBegin | CAllowThreadsEnd | CIf @@ -240,6 +256,7 @@ class CFunction(StageRecord): ..., ] = () storage: str | None = None + doc: tuple[str, ...] = () @dataclass @@ -450,6 +467,7 @@ class FortranFunction(StageRecord): ] = () is_subroutine: bool = False internal_procedures: tuple[FortranFunction, ...] = () + doc: tuple[str, ...] = () @dataclass diff --git a/prik/codegen/primitive_scalar_types.py b/prik/codegen/primitive_scalar_types.py index 432994dc0..13629e1e7 100644 --- a/prik/codegen/primitive_scalar_types.py +++ b/prik/codegen/primitive_scalar_types.py @@ -11,14 +11,77 @@ from __future__ import annotations from collections.abc import Mapping -from dataclasses import replace +from dataclasses import dataclass, replace from types import MappingProxyType from typing import ClassVar from prik.codegen.nodes import BackendScalarType +from prik.contracts import NATIVE_C_SCALAR_CASTS from prik.semantics.scalar_types import BOOLEAN_SEMANTIC_TYPE_NAMES +@dataclass(frozen=True) +class NativeCArrayStorageType: + """Exact NumPy storage corresponding to one native C element type.""" + + numpy_type_macro: str + python_type_name: str + + +class NativeCArrayStorageRegistry: + """Resolve a completed exact C element identity into NumPy C storage. + + Policy decides that an array requires exact native storage. This registry + owns only the backend spellings used to validate that storage; it never + promotes a scalar marker into array policy or selects a nearby dtype. + """ + + _BY_CONTRACT_NAME: ClassVar[Mapping[str, NativeCArrayStorageType]] = MappingProxyType( + { + "CSignedChar": NativeCArrayStorageType("NPY_BYTE", "numpy.byte"), + "CUnsignedChar": NativeCArrayStorageType("NPY_UBYTE", "numpy.ubyte"), + "CShort": NativeCArrayStorageType("NPY_SHORT", "numpy.short"), + "CUnsignedShort": NativeCArrayStorageType("NPY_USHORT", "numpy.ushort"), + "CInt": NativeCArrayStorageType("NPY_INT", "numpy.intc"), + "CUnsignedInt": NativeCArrayStorageType("NPY_UINT", "numpy.uintc"), + "CLong": NativeCArrayStorageType("NPY_LONG", "numpy.long"), + "CUnsignedLong": NativeCArrayStorageType("NPY_ULONG", "numpy.ulong"), + "CLongLong": NativeCArrayStorageType("NPY_LONGLONG", "numpy.longlong"), + "CUnsignedLongLong": NativeCArrayStorageType("NPY_ULONGLONG", "numpy.ulonglong"), + "CFloat": NativeCArrayStorageType("NPY_FLOAT", "numpy.single"), + "CDouble": NativeCArrayStorageType("NPY_DOUBLE", "numpy.double"), + "CLongDouble": NativeCArrayStorageType("NPY_LONGDOUBLE", "numpy.longdouble"), + "CFloatComplex": NativeCArrayStorageType("NPY_CFLOAT", "numpy.csingle"), + "CDoubleComplex": NativeCArrayStorageType("NPY_CDOUBLE", "numpy.cdouble"), + "CLongDoubleComplex": NativeCArrayStorageType("NPY_CLONGDOUBLE", "numpy.clongdouble"), + } + ) + TYPES: ClassVar[Mapping[str, NativeCArrayStorageType]] = MappingProxyType( + {NATIVE_C_SCALAR_CASTS[name]: storage for name, storage in _BY_CONTRACT_NAME.items()} + ) + _CHAR_TYPES: ClassVar[Mapping[str, NativeCArrayStorageType]] = MappingProxyType( + { + "Int8": NativeCArrayStorageType("NPY_BYTE", "numpy.byte"), + "UInt8": NativeCArrayStorageType("NPY_UBYTE", "numpy.ubyte"), + } + ) + + @classmethod + def type_for(cls, c_spelling: str, semantic_type_name: str) -> NativeCArrayStorageType: + """Return exact NumPy storage or fail instead of reinterpreting a buffer.""" + if c_spelling == "_Bool": + raise ValueError("C _Bool has no exact NumPy array storage type") + if c_spelling == "char": + try: + return cls._CHAR_TYPES[semantic_type_name] + except KeyError: + raise ValueError(f"C char array storage requires Int8 or UInt8, not {semantic_type_name!r}") from None + try: + return cls.TYPES[c_spelling] + except KeyError: + raise ValueError(f"Unsupported exact native C array element type {c_spelling!r}") from None + + class NumpyDtypeRegistry: """Project resolved semantic dtypes into emitted NumPy expressions.""" @@ -139,6 +202,50 @@ class PrimitiveScalarTypeRegistry: python_module_result_kind="numpy", cfi_type_spelling="CFI_type_size_t", ), + "UInt8": BackendScalarType( + semantic_name="UInt8", + c_spelling="uint8_t", + fortran_spelling="integer(c_int8_t)", + python_parse_unit="O", + numpy_type_macro="NPY_UINT8", + python_result_kind="numpy", + python_type_name=NumpyDtypeRegistry.expression_for("UInt8"), + python_module_result_kind="numpy", + cfi_type_spelling="CFI_type_int8_t", + ), + "UInt16": BackendScalarType( + semantic_name="UInt16", + c_spelling="uint16_t", + fortran_spelling="integer(c_int16_t)", + python_parse_unit="O", + numpy_type_macro="NPY_UINT16", + python_result_kind="numpy", + python_type_name=NumpyDtypeRegistry.expression_for("UInt16"), + python_module_result_kind="numpy", + cfi_type_spelling="CFI_type_int16_t", + ), + "UInt32": BackendScalarType( + semantic_name="UInt32", + c_spelling="uint32_t", + fortran_spelling="integer(c_int32_t)", + python_parse_unit="O", + numpy_type_macro="NPY_UINT32", + python_result_kind="numpy", + python_type_name=NumpyDtypeRegistry.expression_for("UInt32"), + python_module_result_kind="numpy", + cfi_type_spelling="CFI_type_int32_t", + ), + "UInt64": BackendScalarType( + semantic_name="UInt64", + c_spelling="uint64_t", + fortran_spelling="integer(c_int64_t)", + python_parse_unit="O", + numpy_type_macro="NPY_UINT64", + python_result_kind="numpy", + python_type_name=NumpyDtypeRegistry.expression_for("UInt64"), + python_module_result_kind="numpy", + cfi_type_spelling="CFI_type_int64_t", + ), "Float32": BackendScalarType( semantic_name="Float32", c_spelling="float", @@ -161,6 +268,17 @@ class PrimitiveScalarTypeRegistry: python_module_result_kind="numpy", cfi_type_spelling="CFI_type_double", ), + "Float128": BackendScalarType( + semantic_name="Float128", + c_spelling="long double", + fortran_spelling="real(c_long_double)", + python_parse_unit="O", + numpy_type_macro="NPY_LONGDOUBLE", + python_result_kind="numpy", + python_type_name=NumpyDtypeRegistry.expression_for("Float128"), + python_module_result_kind="numpy", + cfi_type_spelling="CFI_type_long_double", + ), "Complex64": BackendScalarType( semantic_name="Complex64", c_spelling="float complex", @@ -183,6 +301,17 @@ class PrimitiveScalarTypeRegistry: python_module_result_kind="numpy", cfi_type_spelling="CFI_type_double_Complex", ), + "Complex256": BackendScalarType( + semantic_name="Complex256", + c_spelling="long double complex", + fortran_spelling="complex(c_long_double_complex)", + python_parse_unit="O", + numpy_type_macro="NPY_CLONGDOUBLE", + python_result_kind="numpy", + python_type_name=NumpyDtypeRegistry.expression_for("Complex256"), + python_module_result_kind="numpy", + cfi_type_spelling="CFI_type_long_double_Complex", + ), } @classmethod @@ -195,6 +324,8 @@ def type_for(cls, semantic_type_name: str) -> BackendScalarType: __all__ = ( + "NativeCArrayStorageRegistry", + "NativeCArrayStorageType", "NumpyDtypeRegistry", "PrimitiveScalarTypeRegistry", ) diff --git a/prik/compiler/README.md b/prik/compiler/README.md index db11de094..de5088aab 100644 --- a/prik/compiler/README.md +++ b/prik/compiler/README.md @@ -89,5 +89,5 @@ policy completion. Those decisions happen before generated sources reach this pa - Pipeline package guide: `docs/developer/packages/pipeline.md` - Quality and static checks: `docs/developer/workflows/quality-assurance.md` - Source navigation: `docs/developer/codebase-map.md`, `docs/developer/feature-to-code-map.md` -- Build-mode tests: `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py` -- Runtime ABI tests: `tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py` +- Build-mode tests: `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py` +- Runtime ABI tests: `tests/fortran/infrastructure/building/end_to_end/test_runtime_compatibility.py` diff --git a/prik/compiler/compiler_profiles.py b/prik/compiler/compiler_profiles.py index ca55623ec..58fdef46f 100644 --- a/prik/compiler/compiler_profiles.py +++ b/prik/compiler/compiler_profiles.py @@ -127,7 +127,7 @@ def _language( "gcc", "mpicc", debug_flags=("-g", "-O0"), - release_flags=("-O3", "-funroll-loops", "-DNDEBUG"), + release_flags=("-O3", "-DNDEBUG"), general_flags=("-fPIC",), standard_flags=("-std=c99",), openmp={"flags": ("-fopenmp",), "libs": ("gomp",)}, @@ -137,7 +137,7 @@ def _language( "g++", "mpic++", debug_flags=("-g", "-O0"), - release_flags=("-O3", "-funroll-loops"), + release_flags=("-O3",), general_flags=("-fPIC",), standard_flags=("--std=c++20",), openmp={"flags": ("-fopenmp",), "libs": ("gomp",)}, @@ -147,7 +147,7 @@ def _language( "gfortran", "mpif90", debug_flags=("-fcheck=bounds", "-g", "-O0"), - release_flags=("-O3", "-funroll-loops", "-DNDEBUG"), + release_flags=("-O3", "-DNDEBUG"), general_flags=("-fPIC", "-cpp"), optional_general_flags=("-ftrampoline-impl=heap",), standard_flags=("-std=f2003",), @@ -160,7 +160,7 @@ def _language( "icx", "mpiicx", debug_flags=("-g", "-O0"), - release_flags=("-O3", "-funroll-loops", "-DNDEBUG"), + release_flags=("-O3", "-DNDEBUG"), general_flags=("-fPIC",), standard_flags=("-std=c99",), openmp={"flags": ("-qopenmp",)}, @@ -170,7 +170,7 @@ def _language( "icpx", "mpiicpx", debug_flags=("-g", "-O0"), - release_flags=("-O3", "-funroll-loops"), + release_flags=("-O3",), general_flags=("-fPIC",), standard_flags=("--std=c++20",), openmp={"flags": ("-qopenmp",)}, @@ -180,7 +180,7 @@ def _language( "ifx", "mpiifx", debug_flags=("-check", "bounds", "-g", "-O0"), - release_flags=("-O3", "-funroll-loops", "-DNDEBUG"), + release_flags=("-O3", "-DNDEBUG"), general_flags=("-fPIC", "-fpp"), standard_flags=("-std=f2003",), module_output_flag="-module", @@ -192,7 +192,7 @@ def _language( "pgcc", "pgcc", debug_flags=("-g", "-O0"), - release_flags=("-O3", "-Munroll", "-DNDEBUG"), + release_flags=("-O3", "-DNDEBUG"), general_flags=("-fPIC",), standard_flags=("-std=c99",), openmp={"flags": ("-mp",)}, @@ -202,7 +202,7 @@ def _language( "pgfortran", "pgfortran", debug_flags=("-Mbounds", "-g", "-O0"), - release_flags=("-O3", "-Munroll", "-DNDEBUG"), + release_flags=("-O3", "-DNDEBUG"), general_flags=("-fPIC", "-cpp"), standard_flags=("-Mstandard",), module_output_flag="-module", @@ -214,7 +214,7 @@ def _language( "nvc", "mpicc", debug_flags=("-g", "-O0"), - release_flags=("-O3", "-Munroll", "-DNDEBUG"), + release_flags=("-O3", "-DNDEBUG"), general_flags=("-fPIC",), standard_flags=("-std=c99",), openmp={"flags": ("-mp",)}, @@ -224,7 +224,7 @@ def _language( "nvc++", "mpic++", debug_flags=("-g", "-O0"), - release_flags=("-O3", "-Munroll"), + release_flags=("-O3",), general_flags=("-fPIC",), standard_flags=("--std=c++20",), openmp={"flags": ("-mp",)}, @@ -234,7 +234,7 @@ def _language( "nvfortran", "mpifort", debug_flags=("-Mbounds", "-g", "-O0"), - release_flags=("-O3", "-Munroll", "-DNDEBUG"), + release_flags=("-O3", "-DNDEBUG"), general_flags=("-fPIC", "-cpp"), standard_flags=("-Mstandard",), module_output_flag="-module", @@ -249,7 +249,7 @@ def _language( "clang", "mpicc", debug_flags=("-g", "-O0"), - release_flags=("-O3", "-funroll-loops", "-DNDEBUG"), + release_flags=("-O3", "-DNDEBUG"), general_flags=("-fPIC",), standard_flags=("-std=c99",), openmp=_CLANG_OPENMP, @@ -259,7 +259,7 @@ def _language( "clang++", "mpic++", debug_flags=("-g", "-O0"), - release_flags=("-O3", "-funroll-loops"), + release_flags=("-O3",), general_flags=("-fPIC",), standard_flags=("--std=c++20",), openmp=_CLANG_OPENMP, @@ -303,6 +303,28 @@ def _toolchain(**languages: dict[str, object]) -> dict[str, dict[str, object]]: ("ifx", "intel", "icx"), ) +_C_COMPILER_FAMILIES = ( + ("nvc", "nvidia"), + ("pgcc", "PGI"), + ("gcc", "GNU"), + ("clang", "LLVM"), + ("icx", "intel"), + ("icc", "intel"), +) + +# Ordered banner markers, most specific vendor first: an Intel or NVIDIA driver +# is Clang- or LLVM-derived and says so, and GNU's banner names its foundation +# rather than the ``gcc`` program when it was invoked as ``cc``. +_C_VERSION_BANNER_FAMILIES = ( + ("intel(r)", ("icx", "intel")), + ("nvidia", ("nvc", "nvidia")), + ("nvc ", ("nvc", "nvidia")), + ("pgcc ", ("pgcc", "PGI")), + ("clang", ("clang", "LLVM")), + ("free software foundation", ("gcc", "GNU")), + ("gcc", ("gcc", "GNU")), +) + def fortran_compiler_family(executable: str) -> tuple[str, str, str]: """Return the compiler token, profile, and matching C executable name.""" @@ -314,6 +336,53 @@ def fortran_compiler_family(executable: str) -> tuple[str, str, str]: raise ValueError(f"Unknown Fortran compiler family for {executable!r}; expected one of: {supported}") +def c_compiler_family(executable: str) -> tuple[str, str]: + """Return the C-driver token and compiler profile for ``executable``. + + C-only extension builds deliberately choose this route instead of treating + a C executable as a misspelled Fortran driver. Mixed-language builds keep + using :func:`fortran_compiler_family`, because the Fortran runtime then + owns the final link driver. + + A vendor-named driver is recognized from its name alone. A generic POSIX + name such as ``cc`` names no vendor, so callers resolve it with + :func:`c_compiler_family_from_version` instead of guessing one. + """ + family = c_compiler_family_from_name(executable) + if family is not None: + return family + raise ValueError(f"Unknown C compiler family for {executable!r}; expected one of: {_supported_c_tokens()}") + + +def c_compiler_family_from_name(executable: str) -> tuple[str, str] | None: + """Return the C family a vendor-named executable states, if its name states one.""" + name = Path(executable).name + for token, vendor in _C_COMPILER_FAMILIES: + if re.search(rf"(?:^|-){re.escape(token)}(?:-|$)", name): + return token, vendor + return None + + +def c_compiler_family_from_version(version_text: str) -> tuple[str, str] | None: + """Return the C family a compiler's own version banner identifies. + + ``cc`` is a POSIX name for whichever C compiler the platform installs, and + on some platforms it is a real program rather than a link to a vendor-named + one. The banner is the compiler's own statement of what it is, so it + settles the vendor without assuming a platform default. + """ + banner = version_text.casefold() + for marker, family in _C_VERSION_BANNER_FAMILIES: + if marker in banner: + return family + return None + + +def _supported_c_tokens() -> str: + """Return the recognized C driver names for a diagnostic.""" + return ", ".join(token for token, _vendor in _C_COMPILER_FAMILIES) + + if __name__ == "__main__": token, vendor, c_executable = fortran_compiler_family("/opt/toolchain/bin/gfortran-13") fortran_profile = available_compilers[vendor]["fortran"] diff --git a/prik/compiler/compilers.py b/prik/compiler/compilers.py index 0e8087678..6c0130e26 100644 --- a/prik/compiler/compilers.py +++ b/prik/compiler/compilers.py @@ -20,7 +20,14 @@ import threading import warnings -from prik.compiler.compiler_profiles import available_compilers, fortran_compiler_family, vendors +from prik.compiler.compiler_profiles import ( + available_compilers, + c_compiler_family, + c_compiler_family_from_name, + c_compiler_family_from_version, + fortran_compiler_family, + vendors, +) from prik.compiler.objects import ObjectFile __all__ = ("Compiler", "get_condaless_search_path") @@ -82,6 +89,63 @@ def from_fortran_executable( executables={"fortran": resolved_fortran, "c": resolved_c}, ) + @classmethod + def from_c_executable( + cls, + executable: str = "cc", + *, + debug: bool = False, + execute_commands: bool = True, + search_path: str | None = None, + ) -> Compiler: + """Create a C-only toolchain without inventing a Fortran dependency.""" + resolved_c = shutil.which(executable, path=search_path) + if resolved_c is None: + raise FileNotFoundError(f"Could not find compiler executable: {executable}") + _token, vendor = cls._c_family(resolved_c) + return cls( + vendor, + debug=debug, + execute_commands=execute_commands, + search_path=search_path, + executables={"c": resolved_c}, + ) + + @classmethod + def _c_family(cls, resolved_c: str) -> tuple[str, str]: + """Identify one C driver from its name, or from its own version banner. + + A generic POSIX name such as ``cc`` may be a real program rather than a + link to a vendor-named driver, so the name alone cannot classify it. + Asking the compiler keeps the vendor a measured fact instead of a + platform guess. + """ + family = c_compiler_family_from_name(str(Path(resolved_c).resolve())) or c_compiler_family_from_name( + str(resolved_c) + ) + if family is not None: + return family + family = c_compiler_family_from_version(cls._version_banner(resolved_c)) + if family is not None: + return family + # Reuse the established diagnostic, now that neither route identified it. + return c_compiler_family(str(Path(resolved_c).resolve())) + + @staticmethod + @cache + def _version_banner(executable: str) -> str: + """Return a compiler's ``--version`` output, or empty text when it fails.""" + try: + completed = subprocess.run( + (executable, "--version"), + capture_output=True, + text=True, + check=False, + ) + except OSError: + return "" + return f"{completed.stdout}\n{completed.stderr}" + def __init__( self, vendor: str, diff --git a/prik/contracts/__init__.py b/prik/contracts/__init__.py index f7f56674a..507cb40e6 100644 --- a/prik/contracts/__init__.py +++ b/prik/contracts/__init__.py @@ -7,6 +7,7 @@ from __future__ import annotations +from abc import abstractmethod as abstractmethod from typing import Annotated as Annotated, Any as Any, Final as Final import numpy as np @@ -227,12 +228,65 @@ def apply(target): PointerAssociation = _expression PointerPolicy = _expression Range = _expression +Hidden = _expression Return = _expression SourceName = _expression Transfer = _expression Value = _expression Work = _expression + +NATIVE_C_SCALAR_CASTS: Final[dict[str, str]] = { + "CBool": "_Bool", + "CChar": "char", + "CSignedChar": "signed char", + "CUnsignedChar": "unsigned char", + "CShort": "short", + "CUnsignedShort": "unsigned short", + "CInt": "int", + "CUnsignedInt": "unsigned int", + "CLong": "long", + "CUnsignedLong": "unsigned long", + "CLongLong": "long long", + "CUnsignedLongLong": "unsigned long long", + "CFloat": "float", + "CDouble": "double", + "CLongDouble": "long double", + "CFloatComplex": "float _Complex", + "CDoubleComplex": "double _Complex", + "CLongDoubleComplex": "long double _Complex", +} + +CBool = _expression +CChar = _expression +CSignedChar = _expression +CUnsignedChar = _expression +CShort = _expression +CUnsignedShort = _expression +CInt = _expression +CUnsignedInt = _expression +CLong = _expression +CUnsignedLong = _expression +CLongLong = _expression +CUnsignedLongLong = _expression +CFloat = _expression +CDouble = _expression +CLongDouble = _expression +CFloatComplex = _expression +CDoubleComplex = _expression +CLongDoubleComplex = _expression + + +def abstract(target): + """Mark a contract class as an abstract native type. + + A class carrying this marker cannot be constructed: the native type is + declared ``abstract``, so only its concrete extensions have instances. It + is returned unchanged so the contract stays an ordinary Python stub. + """ + return target + + bind = _decorator nogil = _decorator native_abi = _decorator @@ -270,6 +324,7 @@ def apply(target): "Bool64", "Bounded", "Byte", + *NATIVE_C_SCALAR_CASTS, "CAnonymous", "CAnonymousMember", "CEnum", @@ -332,6 +387,8 @@ def apply(target): "Void", "Work", "WrappedType", + "abstract", + "abstractmethod", "bind", "nogil", "native_abi", @@ -341,6 +398,7 @@ def apply(target): "prototype", "pure", "private", + "Hidden", "raises", "standalone", } diff --git a/prik/naming/native_symbols.py b/prik/naming/native_symbols.py index e4e011de1..1be3bffd5 100644 --- a/prik/naming/native_symbols.py +++ b/prik/naming/native_symbols.py @@ -6,6 +6,11 @@ import zlib +COLLISION_ADAPTER_PREFIX = "prik_collision_adapter_" +# Every compiler PRIK profiles accepts the GNU visibility attribute. +COLLISION_ADAPTER_STORAGE = '__attribute__((visibility("hidden")))' + + class NativeSymbolNames: """Create stable backend symbols within native compiler limits.""" @@ -17,6 +22,16 @@ def compact(owner_path: str, preferred: str, *, limit: int = 27) -> str: prefix_length = max(1, limit - len(digest) - 1) return f"{readable[:prefix_length]}_{digest}" + @staticmethod + def collision_adapter(symbol_name: str) -> str: + """Return the forwarder symbol that stands in for one native symbol. + + The binding calls this name instead of ``symbol_name`` so its own + declaration cannot collide with a declaration of the same identifier + that ``Python.h`` already brought into the binding translation unit. + """ + return f"{COLLISION_ADAPTER_PREFIX}{symbol_name}" + if __name__ == "__main__": owner = "geometry.point.coordinates" diff --git a/prik/parsers/c/README.md b/prik/parsers/c/README.md index 991010222..0e08639b3 100644 --- a/prik/parsers/c/README.md +++ b/prik/parsers/c/README.md @@ -28,7 +28,7 @@ not own preprocessing. - User recipe: `docs/user/examples/recipes/inspect-c-api.md` - Source navigation: `docs/developer/codebase-map.md`, `docs/developer/feature-to-code-map.md` - Parser tests: `tests/c/fixtures/parser/` -- Semantic handoff tests: `tests/c/semantics/conversion/` +- Semantic handoff tests: `tests/c/infrastructure/semantic_ir/semantics/` Runtime C-input wrapping is future backend work. Keep C docs clear about the current boundary: parse, semantic IR, and `.pyi` are implemented; diff --git a/prik/parsers/c/parser.py b/prik/parsers/c/parser.py index c20e92840..1a5c0f833 100644 --- a/prik/parsers/c/parser.py +++ b/prik/parsers/c/parser.py @@ -61,7 +61,7 @@ parser inputs. Executable walkthroughs live in -``tests/c/parsing/test_c_parser_developer_tutorial.py``. +``tests/c/infrastructure/execution_examples/test_c_parser_developer_tutorial.py``. """ from __future__ import annotations @@ -171,9 +171,18 @@ "_Decimal64": "_xd64", "_Decimal128": "_xd128", } -_COMPILER_KEYWORD_NORMALIZATIONS.update(_EXTENDED_SCALAR_NORMALIZATIONS) +_FALLBACK_FLOAT_TYPEDEF_SPELLINGS = { + spelling for spelling in _EXTENDED_SCALAR_NORMALIZATIONS if spelling.startswith("_Float") +} +_COMPILER_KEYWORD_NORMALIZATIONS.update( + { + spelling: normalized + for spelling, normalized in _EXTENDED_SCALAR_NORMALIZATIONS.items() + if spelling not in _FALLBACK_FLOAT_TYPEDEF_SPELLINGS + } +) _EXTENDED_SCALAR_SPELLINGS = {normalized: spelling for spelling, normalized in _EXTENDED_SCALAR_NORMALIZATIONS.items()} -_EXTENDED_SCALAR_WORDS = set(_EXTENDED_SCALAR_SPELLINGS) +_EXTENDED_SCALAR_WORDS = set(_EXTENDED_SCALAR_SPELLINGS) | _FALLBACK_FLOAT_TYPEDEF_SPELLINGS _TAG_KINDS = {"struct", "union", "enum"} _UNSUPPORTED_DECLARATION_MARKERS = ( "__attribute__", @@ -1742,6 +1751,22 @@ def _find_matching_delimiter( return index return None + def _starts_fallback_float_typedef_declarator( + self, + text: str, + word: str, + end: int, + *, + consumed_type: bool, + ) -> bool: + """Recognize ``_FloatN`` as a fallback typedef name after a complete type.""" + if not consumed_type or word not in _FALLBACK_FLOAT_TYPEDEF_SPELLINGS: + return False + if "typedef" not in _IDENTIFIER_RE.findall(text[:end]): + return False + suffix_start = self._skip_whitespace(text, end) + return suffix_start >= len(text) or text[suffix_start] in "[,(=;" + def _split_declaration_specifiers(self, text: str) -> tuple[str, str]: """Split a declaration into specifier prefix and declarator tail. @@ -1785,6 +1810,13 @@ def _split_declaration_specifiers(self, text: str) -> tuple[str, str]: continue if self._canonical_primitive_word(word) in _PRIMITIVE_WORDS or word in _EXTENDED_SCALAR_WORDS: + if self._starts_fallback_float_typedef_declarator( + text, + word, + end, + consumed_type=consumed_type, + ): + break consumed_type = True index = end spec_end = end @@ -2131,6 +2163,73 @@ def _is_knr_definition(self, segment: CTopLevelSegment, parameters_text: str) -> return False return all(re.fullmatch(r"[A-Za-z_]\w*", item.strip()) for item in top_level_split(stripped, ",")) + def _old_style_signature_name(self, line: str) -> re.Match[str] | None: + """Return a possible K&R function name from one declaration line.""" + text = line.strip() + if text.startswith("#"): + return None + parameter_bounds = self._find_parameter_list(text) + if parameter_bounds is None: + return None + open_index, close_index = parameter_bounds + before_parameters = text[:open_index].strip() + name_match = self._last_identifier(before_parameters) + if name_match is None or name_match.group(0) in {"if", "for", "while", "switch"}: + return None + return_spec = before_parameters[: name_match.start()].strip() + if not return_spec or "(" in return_spec or ")" in return_spec: + return None + + parameters_text = text[open_index + 1 : close_index].strip() + if not parameters_text or parameters_text == "void": + return None + parameters = [part.strip() for part in parameters_text.split(",")] + if not parameters or not all(re.fullmatch(r"[A-Za-z_]\w*", part) for part in parameters): + return None + if any(self._unambiguously_names_parameter_type(part) for part in parameters): + return None + return name_match + + def _unambiguously_names_parameter_type(self, word: str) -> bool: + """Return whether one bare parameter token is a builtin type, not a K&R name.""" + return self._canonical_primitive_word(word) in _PRIMITIVE_WORDS or word in _EXTENDED_SCALAR_WORDS + + @staticmethod + def _has_old_style_declaration_tail(stripped_lines: list[str], index: int) -> bool: + """Recognize declarations or a body following a possible K&R signature.""" + saw_old_style_declaration = False + for follow in stripped_lines[index + 1 :]: + stripped = follow.strip() + if not stripped: + continue + if stripped.startswith("{"): + return True + if stripped.endswith(";"): + saw_old_style_declaration = True + continue + break + return saw_old_style_declaration + + @staticmethod + def _raise_old_style_definition_error( + line: str, + index: int, + name_match: re.Match[str], + line_mappings, + filename: str | None, + ) -> None: + """Raise the stable K&R diagnostic at its original source location.""" + mapping = line_mappings[index] if index < len(line_mappings) else None + source_line = mapping.source_line if mapping is not None and mapping.source_line is not None else line + raise CParseError( + "K&R style function definitions are not supported", + filename=mapping.filename if mapping is not None else filename, + line_number=mapping.line if mapping is not None else index + 1, + column=max(line.find(name_match.group(0)) + 1, 1), + source_line=source_line, + code="CPARSE_UNSUPPORTED_KNR_DEFINITION", + ) + def _raise_for_unsupported_old_style_definitions( self, source: str, @@ -2151,65 +2250,10 @@ def _raise_for_unsupported_old_style_definitions( ) for index, line in enumerate(stripped_lines): - text = line.strip() - if text.startswith("#"): - continue - parameter_bounds = self._find_parameter_list(text) - if parameter_bounds is None: - continue - open_index, close_index = parameter_bounds - before_parameters = text[:open_index].strip() - name_match = self._last_identifier(before_parameters) - if name_match is None: - continue - if name_match.group(0) in {"if", "for", "while", "switch"}: - continue - return_spec = before_parameters[: name_match.start()].strip() - if not return_spec or "(" in return_spec or ")" in return_spec: - continue - - parameters_text = text[open_index + 1 : close_index].strip() - if not parameters_text or parameters_text == "void": + name_match = self._old_style_signature_name(line) + if name_match is None or not self._has_old_style_declaration_tail(stripped_lines, index): continue - - parameters = [part.strip() for part in parameters_text.split(",")] - if not parameters or not all(re.fullmatch(r"[A-Za-z_]\w*", part) for part in parameters): - continue - - saw_old_style_declaration = False - for follow in stripped_lines[index + 1 :]: - stripped = follow.strip() - if not stripped: - continue - if stripped.startswith("{"): - mapping = line_mappings[index] if index < len(line_mappings) else None - source_line = ( - mapping.source_line if mapping is not None and mapping.source_line is not None else line - ) - raise CParseError( - "K&R style function definitions are not supported", - filename=mapping.filename if mapping is not None else filename, - line_number=mapping.line if mapping is not None else index + 1, - column=max(line.find(name_match.group(0)) + 1, 1), - source_line=source_line, - code="CPARSE_UNSUPPORTED_KNR_DEFINITION", - ) - if stripped.endswith(";"): - saw_old_style_declaration = True - continue - break - - if saw_old_style_declaration: - mapping = line_mappings[index] if index < len(line_mappings) else None - source_line = mapping.source_line if mapping is not None and mapping.source_line is not None else line - raise CParseError( - "K&R style function definitions are not supported", - filename=mapping.filename if mapping is not None else filename, - line_number=mapping.line if mapping is not None else index + 1, - column=max(line.find(name_match.group(0)) + 1, 1), - source_line=source_line, - code="CPARSE_UNSUPPORTED_KNR_DEFINITION", - ) + self._raise_old_style_definition_error(line, index, name_match, line_mappings, filename) def _prototype_style(self, parameters_text: str) -> str: """Classify empty `()` versus prototype-style parameter lists.""" diff --git a/prik/parsers/fortran/README.md b/prik/parsers/fortran/README.md index f33b816c4..516d69c9f 100644 --- a/prik/parsers/fortran/README.md +++ b/prik/parsers/fortran/README.md @@ -23,9 +23,9 @@ re-export parser functions or models. - Package reference: `docs/developer/packages/parsers.md` - User recipe: `docs/user/examples/recipes/inspect-fortran-api.md` - Source navigation: `docs/developer/codebase-map.md`, `docs/developer/feature-to-code-map.md` -- Parser tests: `tests/fortran/source_parsing/parsing/` -- Fixture suite: `tests/fortran/source_parsing/parsing/test_fortran_fixture_suite.py` -- Semantic handoff tests: `tests/fortran/semantic_ir/semantics/` +- Parser tests: `tests/fortran/infrastructure/parsing/` +- Fixture suite: `tests/fortran/infrastructure/parsing/test_fortran_fixture_suite.py` +- Semantic handoff tests: `tests/fortran/infrastructure/semantic_ir/semantics/` Parser support alone does not establish native binding support. Wrapper features need semantic lowering, completed policy, codegen, compilation, and diff --git a/prik/parsers/fortran/models.py b/prik/parsers/fortran/models.py index 067437f3b..3279e1141 100644 --- a/prik/parsers/fortran/models.py +++ b/prik/parsers/fortran/models.py @@ -355,6 +355,8 @@ class FortranDerivedType: attributes: list[str] = field(default_factory=list) procedure_bindings: list[dict] = field(default_factory=list) generic_bindings: list[dict] = field(default_factory=list) + component_visibility: str = "public" + binding_visibility: str = "public" @dataclass diff --git a/prik/parsers/fortran/parser.py b/prik/parsers/fortran/parser.py index 146bd3465..cf9024d22 100644 --- a/prik/parsers/fortran/parser.py +++ b/prik/parsers/fortran/parser.py @@ -114,6 +114,16 @@ rejected by the slicer validation. """ + +def _binding_visibility(attributes: list[str], default_visibility: str) -> str: + """Return a type-bound binding's accessibility from its attributes and the type default.""" + if "private" in attributes: + return "private" + if "public" in attributes: + return "public" + return default_visibility + + _REGEX: dict[str, re.Pattern[str]] = { "type": re.compile( r"^(integer|real|complex|logical|character|double\s+(?:precision|complex))\b\s*(\([^)]*\))?\s*(.*)$", @@ -144,10 +154,14 @@ re.IGNORECASE, ), "legacy_parameter": re.compile(r"^parameter\s*\(\s*(?P.*)\s*\)$", re.IGNORECASE), + "construct_name": re.compile(r"^[A-Za-z_]\w*\s*:(?!:)\s*(?P.+)$"), "derived_type": re.compile(r"^type\s*(?P(?:,\s*[^:]+)?)::\s*(?P\w+)(?:\s*\([^)]*\))?$", re.IGNORECASE), "type_field": re.compile(r"^type\s*\(\s*(?P\w+(?:\s*\([^)]*\))?)\s*\)\s*(?P.*)$", re.IGNORECASE), "class_field": re.compile(r"^class\s*\(\s*(?P\w+(?:\s*\([^)]*\))?)\s*\)\s*(?P.*)$", re.IGNORECASE), - "procedure_binding": re.compile(r"^procedure\s*(?:,\s*[^:]*)?::\s*(?P.*)$", re.IGNORECASE), + "procedure_binding": re.compile( + r"^procedure\s*(?:\(\s*(?P\w+)\s*\))?\s*(?:,\s*[^:]*)?::\s*(?P.*)$", + re.IGNORECASE, + ), "procedure_dummy": re.compile(r"^procedure\s*\(\s*(?P\w+)\s*\)\s*(?P.*)$", re.IGNORECASE), "module": re.compile(r"^module\s+(?P\w+)\s*$", re.IGNORECASE), "submodule": re.compile(r"^submodule\s*\(\s*(?P[^)]+?)\s*\)\s*(?P\w+)\s*$", re.IGNORECASE), @@ -1170,6 +1184,11 @@ def is_executable_statement_start(cls, line: str) -> bool: stripped = labeled.group("body").strip() if not stripped: return False + named_construct = _REGEX["construct_name"].match(stripped) + if named_construct: + stripped = named_construct.group("body").strip() + if not stripped: + return False lowered = stripped.lower() if cls.is_openmp_directive(stripped): return not cls.is_openmp_declarative_directive(stripped) @@ -3651,7 +3670,8 @@ def _parse_type_spec_line( if "sequence" not in dtype.attributes: dtype.attributes.append("sequence") return - if stripped.lower() == "private": + if stripped.lower() in {"private", "public"}: + dtype.component_visibility = stripped.lower() return if self._source_unit_scanner.is_openmp_declarative_directive(stripped): raise FortranParseError( @@ -3661,6 +3681,7 @@ def _parse_type_spec_line( source_line=source_line, code="PARSE_UNSUPPORTED_OPENMP_DIRECTIVE", ) + field_count = len(dtype.fields) parsed = self._helper_parse_declaration_line( stripped, scope, @@ -3671,6 +3692,7 @@ def _parse_type_spec_line( parse_character_star=False, ) if parsed: + self._apply_default_component_visibility(dtype, stripped, first_new_field=field_count) return if "::" not in stripped and not self._source_unit_scanner.looks_like_declaration_or_spec(stripped): _raise_invalid_fortran_syntax_line( @@ -3688,6 +3710,28 @@ def _parse_type_spec_line( code="PARSE_UNSUPPORTED_DECLARATION", ) + @staticmethod + def _apply_default_component_visibility( + dtype: FortranDerivedType, + declaration: str, + *, + first_new_field: int, + ) -> None: + """Apply a type's component-accessibility default to newly parsed components. + + A component keeps the accessibility written on its own declaration; the + `private` or `public` statement in the type's specification part only + supplies the default for components that do not state one. + """ + if dtype.component_visibility != "private": + return + attribute_text = declaration.split("::", 1)[0].lower() if "::" in declaration else "" + if re.search(r"\bpublic\b", attribute_text): + return + for component in dtype.fields[first_new_field:]: + if component.visibility == "public": + component.visibility = "private" + def _parse_derived_type_contains_line( self, line: str, @@ -3698,14 +3742,23 @@ def _parse_derived_type_contains_line( source_line: str | None = None, ) -> None: """Parse type-bound procedure and generic bindings after `contains`.""" + if line.strip().lower() in {"private", "public"}: + dtype.binding_visibility = line.strip().lower() + return + proc_binding = _REGEX["procedure_binding"].match(line) if proc_binding: binding_names = split_csv(proc_binding.group("names")) dtype.methods.extend(binding_names) left = line.split("::", 1)[0] attrs = [a.strip().lower() for a in split_csv(left.split(",", 1)[1] if "," in left else "")] + visibility = _binding_visibility(attrs, dtype.binding_visibility) + interface_name = proc_binding.group("iface") for name in binding_names: - dtype.procedure_bindings.append({"name": name, "attrs": attrs}) + binding = {"name": name, "attrs": attrs, "visibility": visibility} + if interface_name: + binding["interface"] = interface_name + dtype.procedure_bindings.append(binding) return if line.lower().startswith("generic") and "::" in line and "=>" in line: @@ -3714,7 +3767,14 @@ def _parse_derived_type_contains_line( attrs = [a.strip().lower() for a in split_csv(attr_txt)] if attr_txt else [] lhs, rhs_txt = [x.strip() for x in right.split("=>", 1)] rhs = [r.strip() for r in split_csv(rhs_txt)] - dtype.generic_bindings.append({"name": lhs, "targets": rhs, "attrs": attrs}) + dtype.generic_bindings.append( + { + "name": lhs, + "targets": rhs, + "attrs": attrs, + "visibility": _binding_visibility(attrs, dtype.binding_visibility), + } + ) return if re.match(r"^final\s*::\s*[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*\s*$", line, re.IGNORECASE): diff --git a/prik/pipeline/README.md b/prik/pipeline/README.md index 236173b14..ab50ec15d 100644 --- a/prik/pipeline/README.md +++ b/prik/pipeline/README.md @@ -7,7 +7,7 @@ native compiler mechanisms. | File | Owns | | --- | --- | | `pyi.py` | Semantic `.pyi` loading, package assembly, and reference reconciliation. | -| `type_mapping_report.py` | Compiler-target facts converted through semantic IR and backend NumPy projection into inspection Markdown. | +| `type_mapping_report.py` | Compiler-target facts converted through semantic IR and backend NumPy projection into a measured inspection record, rendered as Markdown on request. | | `wrapper.py` | One completed-plan-to-rendered-wrapper generation workflow. | | `build.py` | Generated-source output, native compilation, linking, and extension results. | diff --git a/prik/pipeline/build.py b/prik/pipeline/build.py index 716f814bb..e43a4b8f8 100644 --- a/prik/pipeline/build.py +++ b/prik/pipeline/build.py @@ -1,7 +1,8 @@ """Orchestrate source-first and contract-first extension builds. The public boundary is the build records plus ``build_fortran_extension()``, -``build_pyi_extension()``, and ``build_pyi_extension_from_manifest()``. Each +``build_c_extension()``, ``build_pyi_extension()``, and +``build_pyi_extension_from_manifest()``. Each entrypoint prepares semantic input, completes policy, plans and renders a wrapper, materializes its sources, prepares native inputs, then returns a ``WrapperBuildResult`` after compilation, source-only output, or Makefile @@ -17,8 +18,9 @@ from __future__ import annotations -from collections.abc import Callable, Iterable +from collections.abc import Callable, Iterable, Mapping from concurrent.futures import Future, ThreadPoolExecutor +from copy import deepcopy from dataclasses import dataclass, field, replace from importlib.util import module_from_spec, spec_from_file_location import json @@ -32,6 +34,8 @@ from prik.compiler.objects import ObjectFile from prik.compiler.compilers import Compiler, get_condaless_search_path from prik.compiler.native_support import install_native_support +from prik.parsers.c import parse_c_file +from prik.parsers.c.cli import attach_preprocessing_recipe from prik.parsers.fortran.parser import parse_fortran_project from prik.preprocessing.probes.fortran_types import ( evaluate_fortran_type_facts, @@ -39,15 +43,21 @@ resolve_fortran_logical_storage_types, ) from prik.preprocessing import PreprocessingConfig, preprocess_source +from prik.preprocessing.source import run_compiler_preprocessor_with_recipe +from prik.preprocessing.probes.c_types import probe_c_standard_types +from prik.pipeline.pyi import emit_module_stubs from prik.pipeline.wrapper import GeneratedSource, GeneratedWrapper, WrapperGenerator from prik.semantics.fortran2ir import ( collect_fortran_type_storage_requirements, collect_semantic_compile_time_requirements, fortran_project_to_semantic_modules, ) +from prik.semantics.c2ir import CToIRConverter, c_file_to_semantic_modules, select_c_export_functions +from prik.semantics.metadata import EXPLICIT_C_EXPORT_METADATA from prik.semantics.models import ( PYTHON_EXPORTS_METADATA, PYTHON_EXPORTS_PREPARED_METADATA, + RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA, ProcedureOverloadSet, SemanticClass, SemanticFunction, @@ -63,6 +73,7 @@ native_array_handle_build_requirements, ) from prik.policy.completion import complete_semantic_policies +from prik.policy.models import FunctionWrapperPolicy, NativeEntrypointAction from prik.pipeline.pyi import _PyiSemanticModuleCache from prik.semantics.pyi_metadata import PYI_LOADED_METADATA from prik.planning import NativeGeneratedCodeGroupPlan, WrapperPlanner @@ -71,7 +82,7 @@ _DEFAULT_BUILD_DIR_NAME = "__prik__" _BUILD_MANIFEST_NAME = "prik-build.json" -_BUILD_MANIFEST_SCHEMA_VERSION = 3 +_BUILD_MANIFEST_SCHEMA_VERSION = 4 _FORTRAN_SOURCE_SUFFIXES = {".f", ".f03", ".f08", ".f77", ".f90", ".f95", ".for", ".ftn"} _C_SOURCE_SUFFIXES = {".c"} _NATIVE_PATH_LINK_KINDS = frozenset({"object", "archive", "shared_library"}) @@ -90,6 +101,37 @@ _GENERATED_WRAPPER_NATIVE_SUPPORT_IMPORTS = { "binding_support": ("binding_support/prik_binding",), } +# A wrapper build must not silently publish a smaller API than its source +# declares, so every parser warning that skips a top-level declaration is an +# error here even though inspection routes report it and continue. +_UNMODELED_C_DECLARATION_DIAGNOSTIC_CODES = frozenset( + { + "C_UNSUPPORTED_DECLARATION", + "C_UNSUPPORTED_DECLARATOR", + "C_UNMODELED_COMPILER_EXTENSION", + } +) +_INTRINSIC_C_DIRECT_DIAGNOSTIC_CODES = frozenset( + { + "C_DIRECT_CALLBACK", + "C_DIRECT_ARRAY_DECLARATOR", + "C_DIRECT_POINTER_DEPTH", + "C_DIRECT_POINTER_RESULT", + "C_DIRECT_VARIADIC_FUNCTION", + "C_DIRECT_TRANSLATION_UNIT_LOCAL_SYMBOL", + "C_DIRECT_UNSUPPORTED_CALLING_CONVENTION", + "C_DIRECT_UNSUPPORTED_QUALIFIER", + "C_DIRECT_NULLABLE_POINTER", + "C_DIRECT_RAW_ADDRESS", + "C_DIRECT_BOOL_ARRAY", + "C_DIRECT_CONST_POINTER_OUTPUT", + "C_DIRECT_ARRAY_RANK", + "C_DIRECT_ARRAY_CONTRACT", + "C_DIRECT_ARRAY_PASSING", + "C_DIRECT_ARRAY_TRANSFORMATION", + "C_DIRECT_ARRAY_ORDER", + } +) # Build configuration, timing, and mode validation @@ -233,6 +275,7 @@ class NativePrebuiltArtifact: path: Path kind: str + language: str | None = None def __post_init__(self) -> None: """Validate the artifact kind and normalize its path field. @@ -244,14 +287,19 @@ def __post_init__(self) -> None: """ if self.kind not in _NATIVE_PATH_LINK_KINDS: raise ValueError(f"Unsupported native artifact kind: {self.kind!r}") + if self.language not in {None, "c", "fortran"}: + raise ValueError(f"Unsupported native artifact language: {self.language!r}") object.__setattr__(self, "path", Path(self.path)) def to_dict(self) -> dict[str, object]: """Return the artifact kind and string path for JSON serialization.""" - return { + record = { "kind": self.kind, "path": str(self.path), } + if self.language is not None: + record["language"] = self.language + return record @dataclass(frozen=True) @@ -267,6 +315,7 @@ class NativeLinkItem: kind: str value: Path | str + language: str | None = None def __post_init__(self) -> None: """Validate ``kind`` and normalize its value to a path or string. @@ -276,6 +325,8 @@ def __post_init__(self) -> None: """ if self.kind not in _NATIVE_LINK_KINDS: raise ValueError(f"Unsupported native link item kind: {self.kind!r}") + if self.language not in {None, "c", "fortran"}: + raise ValueError(f"Unsupported native link-item language: {self.language!r}") if self.kind in _NATIVE_PATH_LINK_KINDS: object.__setattr__(self, "value", Path(self.value)) else: @@ -288,10 +339,13 @@ def to_dict(self) -> dict[str, object]: arguments use ``argument``. The record itself remains unchanged. """ if self.kind in _NATIVE_PATH_LINK_KINDS: - return { + record = { "kind": self.kind, "path": str(self.value), } + if self.language is not None: + record["language"] = self.language + return record if self.kind == "named_library": return { "kind": self.kind, @@ -467,6 +521,97 @@ def _default_preprocessing_config() -> PreprocessingConfig: ) +def _default_c_preprocessing_config(compiler: str) -> PreprocessingConfig: + """Create the default compiler-backed C preprocessing configuration. + + A C wrapper build calls this only when a caller did not provide a + ``PreprocessingConfig``. It returns a fresh configuration so a build + cannot modify shared default lists. + """ + return PreprocessingConfig( + mode="compiler", + compiler=compiler, + defines=[], + include_dirs=[], + ) + + +def _wrapped_c_translation_unit(module: SemanticModule) -> SemanticModule: + """Return one C module holding only the declarations its own file wrote. + + Preprocessing expands system headers into the translation unit, and their + declarations stay as inspection facts with their recorded provenance. The + editable contract written beside a build describes the API that build + generated, so it keeps only what the wrapped file declared and stays + reusable as the input of the next build. + """ + + def is_owned(node) -> bool: + if node.metadata.get(EXPLICIT_C_EXPORT_METADATA): + return True + location = node.origin.source_location + filename = location.get("filename") if isinstance(location, dict) else None + return not (isinstance(filename, str) and filename != module.origin.native_name) + + return replace( + module, + functions=[function for function in module.functions if is_owned(function)], + variables=[variable for variable in module.variables if is_owned(variable)], + classes=[semantic_class for semantic_class in module.classes if is_owned(semantic_class)], + ) + + +def _parse_c_wrapper_source(path: Path, preprocessing: PreprocessingConfig): + """Parse one C implementation source for a wrapper build. + + Compiler-backed preprocessing expands the translation unit before parsing, + so ordinary directives and macro-defined declarations reach the wrapper on + the same terms as the inspection routes. A declaration the parser could + not model is raised here instead of silently disappearing from the public + API of a build that promises to fail closed. + """ + if preprocessing.uses_compiler: + source, recipe = run_compiler_preprocessor_with_recipe(path, language="c", config=preprocessing) + parsed = parse_c_file( + source, + filename=str(path), + include_dirs=preprocessing.include_dirs, + preprocessing="compiler", + ) + # Include exposure needs the recipe: without it every declaration + # expanded from a system header would be published as public API. + attach_preprocessing_recipe(parsed, recipe.to_dict()) + else: + parsed = parse_c_file(path, filename=str(path), include_dirs=preprocessing.include_dirs) + _reject_unmodeled_c_declarations(parsed, path) + return parsed + + +def _reject_unmodeled_c_declarations(parsed, path: Path) -> None: + """Raise when the C parser could not model a declaration written in ``path``. + + Only the wrapped translation unit's own declarations are decided here. + Preprocessed system headers keep their recorded provenance and stay + inspection facts, because the wrapper never exposes them. + """ + owned = str(path) + dropped = [ + diagnostic + for diagnostic in parsed.diagnostics + if diagnostic.code in _UNMODELED_C_DECLARATION_DIAGNOSTIC_CODES + and str(getattr(diagnostic.location, "filename", "") or owned) == owned + ] + if not dropped: + return + details = "; ".join( + f"{diagnostic.code} at {owned}:{diagnostic.location.line}: {diagnostic.message}" for diagnostic in dropped + ) + raise ValueError( + f"C_DIRECT_UNMODELED_DECLARATION: a wrapper build cannot silently drop or reinterpret " + f"a declaration it cannot model: {details}" + ) + + def _fortran_source_for_pipeline(path: Path, preprocessing: PreprocessingConfig) -> str: """Read one source path in the form required by the Fortran parser. @@ -493,19 +638,29 @@ def _new_compiler( execute_commands: bool = True, debug: bool = False, input_compiler: str | None = None, + input_c_compiler: str | None = None, + requires_fortran: bool = True, ) -> Compiler: """Create the compiler configured for generated wrapper code. - ``input_compiler`` overrides the default ``gfortran`` executable; - ``execute_commands`` selects real compilation versus command recording, - and ``debug`` enables the compiler's debug configuration. The returned - compiler has a Conda-free search path and has not run any commands yet. - """ - return Compiler.from_fortran_executable( - input_compiler or "gfortran", + A plan with any Fortran object uses the selected Fortran driver and its + paired C compiler. A C-only plan uses ``input_c_compiler`` directly, so it + neither discovers nor requires a Fortran compiler. The choice comes from + explicit build-language records, never source suffixes. + """ + search_path = get_condaless_search_path("verbose") + if requires_fortran: + return Compiler.from_fortran_executable( + input_compiler or "gfortran", + debug=debug, + execute_commands=execute_commands, + search_path=search_path, + ) + return Compiler.from_c_executable( + input_c_compiler or "cc", debug=debug, execute_commands=execute_commands, - search_path=get_condaless_search_path("verbose"), + search_path=search_path, ) @@ -562,6 +717,46 @@ def _generated_source_output_path(output_dir: Path, path: Path) -> Path: return output_dir / path +BUILD_CONTRACT_DIRECTORY_NAME = "contracts" + + +def _write_build_contract_package( + source_modules: tuple[SemanticModule, ...], + output_dir: Path, + *, + verbose: bool | int = False, +) -> tuple[Path, ...]: + """Write the editable semantic contract for one build beside its artifacts. + + Every build leaves the contract that describes the API it just generated, so + reshaping the Python surface never needs a separate `generate --pyi` run. + The package lives in its own directory inside the build output so its + ``__init__.pyi`` cannot make the build directory look like a Python package. + """ + if not source_modules: + return () + try: + stubs = emit_module_stubs(source_modules) + except (ValueError, KeyError) as error: + # The extension is already built; a contract that cannot be rendered is + # reported rather than allowed to fail the build behind it. + _print_verbose_step(verbose, f"Skip contract package: {error}") + return () + package_dir = output_dir / BUILD_CONTRACT_DIRECTORY_NAME + package_dir.mkdir(parents=True, exist_ok=True) + written = [] + for module_name, text in stubs.items(): + path = package_dir / f"{module_name}.pyi" + path.write_text(f"{text}\n", encoding="utf-8") + _print_verbose_step(verbose, f"Write semantic contract: {path}") + written.append(path) + root = package_dir / "__init__.pyi" + root.write_text("".join(f"from . import {name}\n" for name in sorted(stubs)), encoding="utf-8") + _print_verbose_step(verbose, f"Write semantic contract package: {root}") + written.append(root) + return tuple(written) + + def _write_generated_wrapper_sources( rendered: GeneratedWrapper, output_dir: Path, @@ -704,48 +899,61 @@ def _generated_wrapper_link_language( return binding_objects[-1].language +def _native_plan_link_languages(plan: NativeBuildPlan) -> tuple[str, ...]: + """Return every explicitly recorded language that participates in linking.""" + return tuple( + dict.fromkeys( + ( + *(unit.language for unit in plan.compilation_units), + *(artifact.language for artifact in plan.prebuilt_artifacts if artifact.language is not None), + *(item.language for item in plan.link_items if item.language is not None), + ) + ) + ) + + @dataclass(frozen=True) class _CompiledObject: - """Store the recorded compiler command and elapsed time for one object.""" + """Store the elapsed time for one completed object compilation.""" - command: tuple[str, ...] | None elapsed: float -def _compile_one_object(compiler: Compiler, object_file: ObjectFile) -> _CompiledObject: - """Compile one object and return its command record plus elapsed time. +def _compile_one_object( + compiler: Compiler, + object_file: ObjectFile, + *, + verbose: bool | int, +) -> _CompiledObject: + """Compile one object and return its elapsed time. The supplied ``compiler`` performs the compile and may create the object - file. A tuple command is retained for Makefile generation; other compiler - return values are represented as ``None``. + file. """ started = time.perf_counter() - command = compiler.compile_object(object_file, verbose=False) - return _CompiledObject( - command=command if isinstance(command, tuple) else None, - elapsed=time.perf_counter() - started, - ) + compiler.compile_object(object_file, verbose=verbose) + return _CompiledObject(elapsed=time.perf_counter() - started) + +def _report_compilation_timing(result: _CompiledObject, *, verbose: bool | int) -> None: + """Print the completion timing for one verbose object compilation. -def _report_compiled_object( + The compiler prints its command before starting it. This report records the + elapsed time after a successful compilation. + """ + if not verbose: + return + _print_verbose_timing(verbose, result.elapsed) + + +def _announce_object_compilation( object_file: ObjectFile, - result: _CompiledObject, *, label: str, verbose: bool | int, ) -> None: - """Print verbose diagnostics for one completed object compilation. - - Receives the object and timing record produced by ``_compile_one_object``. - When ``verbose`` is false it changes nothing; otherwise it writes the - labelled source-to-object mapping, command, and duration to standard out. - """ - if not verbose: - return + """Print one object boundary before its compiler command can execute.""" _print_verbose_step(verbose, f"{label}: {object_file.source} -> {object_file.object_path}") - if result.command is not None: - print(shlex.join(result.command)) - _print_verbose_timing(verbose, result.elapsed) def _compile_object_stage( @@ -757,39 +965,49 @@ def _compile_object_stage( ) -> None: """Compile one named object group and expose that boundary in verbose logs.""" for object_file in object_files: - result = _compile_one_object(compiler, object_file) - _report_compiled_object(object_file, result, label=label, verbose=verbose) + _announce_object_compilation(object_file, label=label, verbose=verbose) + result = _compile_one_object(compiler, object_file, verbose=verbose) + _report_compilation_timing(result, verbose=verbose) def _submit_object_stage( executor: ThreadPoolExecutor, compiler: Compiler, object_files: Iterable[ObjectFile], + *, + label: str, + verbose: bool | int, ) -> tuple[tuple[ObjectFile, Future[_CompiledObject]], ...]: - """Submit one independent compilation group to an executor. + """Announce and submit one independent compilation group to an executor. - Each input object produces one ``(object_file, future)`` pair. The helper - schedules work but does not wait for it or report verbose output. + Each command is announced before submission; the compiler then prints its + replayable argv immediately before execution in the worker. """ - return tuple( - (object_file, executor.submit(_compile_one_object, compiler, object_file)) for object_file in object_files - ) + pending = [] + for object_file in object_files: + _announce_object_compilation(object_file, label=label, verbose=verbose) + pending.append( + ( + object_file, + executor.submit(_compile_one_object, compiler, object_file, verbose=verbose), + ) + ) + return tuple(pending) def _finish_object_stage( pending: Iterable[tuple[ObjectFile, Future[_CompiledObject]]], *, - label: str, verbose: bool | int, ) -> None: """Wait for a submitted compilation group and report each result. - ``pending`` comes from ``_submit_object_stage``. Calling ``future.result`` - propagates compiler failures; successful objects are reported in input - order when verbose output is enabled. + ``pending`` comes from ``_submit_object_stage``. Calling ``future.result`` + propagates compiler failures; successful objects report completion timing + in input order when verbose output is enabled. """ - for object_file, future in pending: - _report_compiled_object(object_file, future.result(), label=label, verbose=verbose) + for _, future in pending: + _report_compilation_timing(future.result(), verbose=verbose) def _compile_extension_objects( @@ -813,13 +1031,31 @@ def _compile_extension_objects( return with ThreadPoolExecutor(max_workers=jobs, thread_name_prefix="prik-compile") as executor: - binding_futures = _submit_object_stage(executor, compiler, bindings) + binding_futures = _submit_object_stage( + executor, + compiler, + bindings, + label="Compile binding source", + verbose=verbose, + ) for batch in native_groups: - native_futures = _submit_object_stage(executor, compiler, batch) - _finish_object_stage(native_futures, label="Compile native source", verbose=verbose) - bridge_futures = _submit_object_stage(executor, compiler, bridges) - _finish_object_stage(bridge_futures, label="Compile bridge source", verbose=verbose) - _finish_object_stage(binding_futures, label="Compile binding source", verbose=verbose) + native_futures = _submit_object_stage( + executor, + compiler, + batch, + label="Compile native source", + verbose=verbose, + ) + _finish_object_stage(native_futures, verbose=verbose) + bridge_futures = _submit_object_stage( + executor, + compiler, + bridges, + label="Compile bridge source", + verbose=verbose, + ) + _finish_object_stage(bridge_futures, verbose=verbose) + _finish_object_stage(binding_futures, verbose=verbose) def _build_generated_wrapper_extension( @@ -888,7 +1124,10 @@ def _build_generated_wrapper_extension( bridge_objects, binding_objects, native_objects=tuple(native_dependencies), - required_languages=rendered.required_link_languages, + required_languages=( + *rendered.required_link_languages, + *_native_plan_link_languages(resolved_native_build_plan), + ), ), objects=(*tuple(native_dependencies), *bridge_objects, *binding_objects), link_args=tuple(native_link_args), @@ -974,9 +1213,14 @@ def _render_wrapper_plan( module: SemanticModule, *, progress: Callable[[str, float | None], None] | None = None, + collision_adapters: Iterable[str] = (), + collision_adapter_all: bool = False, ) -> GeneratedWrapper: """Render one policy-completed module through the canonical generator.""" - plan = WrapperPlanner().build(module) + plan = WrapperPlanner( + collision_adapters=collision_adapters, + collision_adapter_all=collision_adapter_all, + ).build(module) return WrapperGenerator().generate(plan, progress=progress) @@ -985,12 +1229,25 @@ def _generate_wrapper( *, strict_wrapper_names: bool, verbose: bool | int = False, + collision_adapters: Iterable[str] = (), + collision_adapter_all: bool = False, + positional_only: bool = False, ) -> GeneratedWrapper: """Complete policy and generate the one production wrapper representation.""" + collision_adapter_names = tuple(collision_adapters) _print_verbose_step(verbose, "Complete wrapper policies") policy_started = time.perf_counter() - complete_semantic_policies(module, strict_wrapper_names=strict_wrapper_names) + complete_semantic_policies( + module, + strict_wrapper_names=strict_wrapper_names, + positional_only=positional_only, + ) _print_verbose_timing(verbose, time.perf_counter() - policy_started) + _validate_collision_adapter_selection( + module, + collision_adapters=collision_adapter_names, + collision_adapter_all=collision_adapter_all, + ) def render_progress(label: str, elapsed: float | None) -> None: """Translate generator progress events into this build's verbose output. @@ -1004,7 +1261,107 @@ def render_progress(label: str, elapsed: float | None) -> None: return _print_verbose_timing(verbose, elapsed) - return _render_wrapper_plan(module, progress=render_progress) + return _render_wrapper_plan( + module, + progress=render_progress, + collision_adapters=collision_adapter_names, + collision_adapter_all=collision_adapter_all, + ) + + +def _validate_collision_adapter_selection( + module: SemanticModule, + *, + collision_adapters: Iterable[str], + collision_adapter_all: bool, +) -> None: + """Reject named collision-adapter selections that no C symbol can satisfy. + + ``--collision-adapter-all`` names nothing, so it selects whatever is + eligible and stays silent about the rest; an explicitly named symbol that + is unknown or ineligible is a mistake worth stopping the build for. + """ + requested = frozenset(collision_adapters) + if not requested: + return + missing = sorted(requested - _direct_c_entrypoint_symbols(module)) + if missing: + raise ValueError( + "Collision adapters require existing direct C symbols; unknown or ineligible names: " + ", ".join(missing) + ) + + +def _direct_c_entrypoint_symbols(module: SemanticModule) -> frozenset[str]: + """Return every entrypoint symbol reached through a C-source direct call. + + Only a C-source operation carries the exact C declaration plan the adapter + unit reconstructs, so a Fortran ``bind(C)`` procedure is not eligible even + though it also reaches a direct entrypoint. + """ + functions = list(module.functions) + for overload_set in module.overload_sets: + functions.extend(overload_set.procedures) + classes = list(module.classes) + for semantic_class in classes: + functions.extend(semantic_class.methods) + for overload_set in semantic_class.overload_sets: + functions.extend(overload_set.procedures) + classes.extend(semantic_class.classes) + return frozenset( + policy.entrypoint_symbol + for policy in (function.metadata.get(RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA) for function in functions) + if isinstance(policy, FunctionWrapperPolicy) + and policy.entrypoint_action is NativeEntrypointAction.DIRECT_C_ABI + and policy.direct_c_abi is not None + ) + + +def _preflight_intrinsic_c_direct_policy( + modules: Iterable[SemanticModule], + *, + strict_wrapper_names: bool, +) -> None: + """Raise C-lane blockers before target ABI probing or output creation. + + Target-sized arithmetic types deliberately remain unresolved until the C + probe runs. This dry policy pass therefore raises only form-intrinsic + diagnostics such as callbacks, pointer results, or raw addresses. It keeps + those unsupported forms away from the compiler while a supported + ``int``/``size_t`` operation acquires its target ABI facts normally. + """ + try: + complete_semantic_policies( + deepcopy(list(modules)), + strict_wrapper_names=strict_wrapper_names, + ) + except ValueError as error: + if any(code in str(error) for code in _INTRINSIC_C_DIRECT_DIAGNOSTIC_CODES) or ( + "C_DIRECT_UNRESOLVED_PRIMITIVE_ABI" in str(error) and _c_direct_aggregate_contract_requested(modules) + ): + raise + + +def _c_direct_aggregate_contract_requested(modules: Iterable[SemanticModule]) -> bool: + """Return whether a C contract passes a declared aggregate at its boundary.""" + for module in modules: + aggregate_names = { + semantic_class.name + for semantic_class in module.classes + if semantic_class.metadata.get("c_kind") in {"struct", "union"} + } + if not aggregate_names: + continue + for function in module.functions: + boundary_types = (function.return_type, *(argument.semantic_type for argument in function.arguments)) + if any( + semantic_type is not None + and ( + semantic_type.name in aggregate_names or semantic_type.metadata.get("c_kind") in {"struct", "union"} + ) + for semantic_type in boundary_types + ): + return True + return False # Native source compilation scheduling @@ -1015,6 +1372,7 @@ def _source_compile_object( output_dir: Path, *, object_stem: str, + language: str, flags: Iterable[str] = (), include_dirs: Iterable[Path] = (), ) -> ObjectFile: @@ -1022,14 +1380,16 @@ def _source_compile_object( Uses ``object_stem`` beneath ``output_dir`` to avoid collisions, preserves the supplied flags, and appends the output directory to include paths so - later sources can locate generated Fortran module files. It returns only - an ``ObjectFile`` description and does not compile it. + later Fortran sources can locate generated module files. ``language`` is + the caller-declared build language, preserved in the object record without + consulting its filename. It returns only an ``ObjectFile`` description and + does not compile it. """ target = output_dir / f"{object_stem}.o" return ObjectFile( source=source_path, object_path=target, - language="fortran", + language=language, flags=tuple(flags), include_dirs=(*tuple(include_dirs), output_dir), ) @@ -1181,6 +1541,27 @@ def _source_paths(sources: str | Path | Iterable[str | Path]) -> tuple[Path, ... return tuple(dict.fromkeys(paths)) +def _c_source_paths(sources: str | Path | Iterable[str | Path]) -> tuple[Path, ...]: + """Validate and expand explicit C implementation sources in stable order.""" + inputs = (Path(sources),) if isinstance(sources, str | Path) else tuple(Path(source) for source in sources) + if not inputs: + raise ValueError("wrapper build requires at least one C source file or directory") + paths: list[Path] = [] + for path in inputs: + if path.is_dir(): + discovered = sorted(candidate for candidate in path.rglob("*.c") if candidate.is_file()) + if not discovered: + raise ValueError(f"No recognized C sources found under: {path}") + paths.extend(discovered) + continue + if not path.is_file(): + raise FileNotFoundError(f"C source not found: {path}") + if path.suffix.lower() not in _C_SOURCE_SUFFIXES: + raise ValueError(f"Unrecognized C source suffix: {path}") + paths.append(path) + return tuple(dict.fromkeys(paths)) + + def _wrapper_output_paths(output_dir: str | Path | None) -> tuple[Path, Path]: """Return build and extension directories owned by one wrapper invocation.""" if output_dir is not None: @@ -1208,6 +1589,13 @@ def _pyi_entry_path(contract: str | Path) -> Path: return path +def _native_contract_language(value: str) -> str: + """Validate the explicit ABI language selected for a semantic contract.""" + if value not in {"c", "fortran"}: + raise ValueError(f"Semantic .pyi native language must be 'c' or 'fortran', not {value!r}") + return value + + @dataclass(frozen=True) class _PyiContractBundle: """Keep one resolved ``.pyi`` import graph and its native contract leaves.""" @@ -1223,7 +1611,9 @@ class _NativeBuildInputs: """Hold validated native source, artifact, include, and link input groups.""" source_paths: tuple[Path, ...] - source_flags: tuple[str, ...] + source_languages: tuple[str, ...] + fortran_source_flags: tuple[str, ...] + c_source_flags: tuple[str, ...] artifact_paths: tuple[Path, ...] libraries: tuple[str, ...] explicit_link_items: tuple[NativeLinkItem, ...] @@ -1238,6 +1628,8 @@ class _NativeBuildInputs: def _pyi_contract_bundle( entry: Path, + *, + native_language: str = "fortran", ) -> _PyiContractBundle: """Load one semantic contract graph and retain its native declaration leaves. @@ -1248,9 +1640,9 @@ def _pyi_contract_bundle( """ # Load the complete relative-import graph through one semantic-module cache. module_cache = _PyiSemanticModuleCache() - discovered = {entry, *_discover_pyi_imports(entry, module_cache)} + discovered = {entry, *_discover_pyi_imports(entry, module_cache, native_language=native_language)} sorted_paths = tuple(sorted(discovered)) - loaded_modules = module_cache.paths_to_semantic_modules(sorted_paths) + loaded_modules = module_cache.paths_to_semantic_modules(sorted_paths, native_language=native_language) modules_by_path = dict(zip(sorted_paths, loaded_modules, strict=True)) _validate_pyi_bundle_placement(entry, modules_by_path) _apply_pyi_python_exports(entry, modules_by_path) @@ -1345,7 +1737,12 @@ def _namespace_imported_pyi_paths(entry: Path, modules_by_path: dict[Path, Seman return namespace_imports -def _discover_pyi_imports(root: Path, module_cache: _PyiSemanticModuleCache | None = None) -> tuple[Path, ...]: +def _discover_pyi_imports( + root: Path, + module_cache: _PyiSemanticModuleCache | None = None, + *, + native_language: str = "fortran", +) -> tuple[Path, ...]: """Resolve every relative semantic ``.pyi`` import reachable from ``root``. Reuses an optional semantic-module cache, follows only relative imports, @@ -1357,7 +1754,7 @@ def _discover_pyi_imports(root: Path, module_cache: _PyiSemanticModuleCache | No pending = [root] while pending: path = pending.pop() - module = module_cache.file_to_semantic_module(path) + module = module_cache.file_to_semantic_module(path, native_language=native_language) for dependency in _relative_pyi_dependencies(path, module): if dependency in discovered or dependency == root: continue @@ -1704,7 +2101,7 @@ def _native_build_plan( ) produced_object_set = set(produced_objects) explicit_path_artifacts = tuple( - NativePrebuiltArtifact(path=Path(item.value), kind=item.kind) + NativePrebuiltArtifact(path=Path(item.value), kind=item.kind, language=item.language) for item in link_items if item.kind in _NATIVE_PATH_LINK_KINDS and Path(item.value) not in produced_object_set ) @@ -1713,8 +2110,8 @@ def _native_build_plan( NativeCompilationUnit( source=source_path, object_path=source_object.object_path, - language="fortran", - module_dir=module_dir, + language=source_object.language, + module_dir=module_dir if source_object.language == "fortran" else None, include_dirs=include_dirs, flags=tuple(source_object.flags), ) @@ -1781,7 +2178,10 @@ def _coerce_native_link_items(items: Iterable[NativeLinkItem | dict[str, object] path = item.get("path") if not isinstance(path, str | Path): raise ValueError(f"{kind!r} native link item requires a path") - result.append(NativeLinkItem(kind, path)) + language = item.get("language") + if language is not None and language not in {"c", "fortran"}: + raise ValueError("native link-item language must be 'c' or 'fortran'") + result.append(NativeLinkItem(kind, path, language=language)) elif kind == "named_library": name = item.get("name") if not isinstance(name, str): @@ -1816,6 +2216,8 @@ def _native_build_inputs( *, native_fortran_sources: Iterable[str | Path] | None, native_fortran_flags: Iterable[str] | None, + native_c_sources: Iterable[str | Path] | None, + native_c_flags: Iterable[str] | None, native_objects: Iterable[str | Path] | None, native_libraries: Iterable[str] | None, native_link_items: Iterable[NativeLinkItem | dict[str, object]] | None, @@ -1831,8 +2233,12 @@ def _native_build_inputs( with no native implementation input before any generated code is compiled. """ # Validate independent source, artifact, and explicit-link inputs first. - source_paths = _existing_paths(native_fortran_sources, kind="Native Fortran source") - source_flags = tuple(str(flag) for flag in (native_fortran_flags or ())) + fortran_source_paths = _existing_paths(native_fortran_sources, kind="Native Fortran source") + c_source_paths = _existing_paths(native_c_sources, kind="Native C source") + source_paths = (*fortran_source_paths, *c_source_paths) + source_languages = (*(("fortran",) * len(fortran_source_paths)), *(("c",) * len(c_source_paths))) + fortran_source_flags = tuple(str(flag) for flag in (native_fortran_flags or ())) + c_source_flags = tuple(str(flag) for flag in (native_c_flags or ())) artifact_paths = _existing_paths(native_objects, kind="Native artifact") libraries = tuple(native_libraries or ()) explicit_link_items = _coerce_native_link_items(native_link_items) @@ -1866,7 +2272,9 @@ def _native_build_inputs( ) return _NativeBuildInputs( source_paths=source_paths, - source_flags=source_flags, + source_languages=source_languages, + fortran_source_flags=fortran_source_flags, + c_source_flags=c_source_flags, artifact_paths=artifact_paths, libraries=libraries, explicit_link_items=explicit_link_items, @@ -1884,7 +2292,7 @@ def _native_include_dirs(inputs: _NativeBuildInputs, *, output_path: Path) -> tu caller include directories, and parents of linked artifacts. Returns the paths without creating directories or changing ``inputs``. """ - module_include_dirs = (output_path,) if inputs.source_paths else () + module_include_dirs = (output_path,) if "fortran" in inputs.source_languages else () inferred_include_dirs = _unique_paths((*inputs.artifact_paths, *inputs.link_item_paths)) return _unique_paths( ( @@ -1895,6 +2303,13 @@ def _native_include_dirs(inputs: _NativeBuildInputs, *, output_path: Path) -> tu ) +def _native_inputs_require_fortran(inputs: _NativeBuildInputs) -> bool: + """Return whether explicit native language records require a Fortran driver.""" + return "fortran" in inputs.source_languages or any( + item.language == "fortran" for item in (*inputs.explicit_link_items, *(inputs.complete_link_items or ())) + ) + + def _source_object_stems(source_paths: tuple[Path, ...]) -> tuple[str, ...]: """Return collision-free object stems for an ordered source-path sequence. @@ -1932,10 +2347,16 @@ def _native_source_objects( source_path, output_path, object_stem=object_stem, - flags=inputs.source_flags, + language=language, + flags=inputs.fortran_source_flags if language == "fortran" else inputs.c_source_flags, include_dirs=include_dirs, ) - for source_path, object_stem in zip(inputs.source_paths, _source_object_stems(inputs.source_paths), strict=True) + for source_path, language, object_stem in zip( + inputs.source_paths, + inputs.source_languages, + _source_object_stems(inputs.source_paths), + strict=True, + ) ) @@ -1974,7 +2395,7 @@ def _prepare_native_build_plan( library_dirs=inputs.library_dirs, explicit_include_dirs=inputs.explicit_include_dirs, include_dirs=include_dirs, - module_dir=output_path if source_objects else None, + module_dir=output_path if any(source.language == "fortran" for source in source_objects) else None, ) _validate_native_link_paths(plan) return source_objects, plan @@ -2012,10 +2433,13 @@ def _manifest_link_item(item: NativeLinkItem, *, base: Path) -> dict[str, object modified. """ if item.kind in _NATIVE_PATH_LINK_KINDS: - return { + record = { "kind": item.kind, "path": _manifest_path(Path(item.value), base=base), } + if item.language is not None: + record["language"] = item.language + return record if item.kind == "named_library": return { "kind": item.kind, @@ -2048,10 +2472,13 @@ def _manifest_native_plan(plan: NativeBuildPlan, *, base: Path) -> dict[str, obj ], "produced_objects": [_manifest_path(path, base=base) for path in plan.produced_objects], "prebuilt_artifacts": [ - { - "kind": artifact.kind, - "path": _manifest_path(artifact.path, base=base), - } + ( + { + "kind": artifact.kind, + "path": _manifest_path(artifact.path, base=base), + } + | ({"language": artifact.language} if artifact.language is not None else {}) + ) for artifact in plan.prebuilt_artifacts ], "module_dirs": [_manifest_path(path, base=base) for path in plan.module_dirs], @@ -2105,7 +2532,13 @@ def _pyi_build_manifest( strict_wrapper_names: bool, requested_output_name: str | None, input_compiler: str, + input_c_compiler: str, + native_language: str, + collision_adapters: tuple[str, ...], + collision_adapter_all: bool, + positional_only: bool, native_fortran_flags: tuple[str, ...], + native_c_flags: tuple[str, ...], wrapper_compiler_debug: bool, wrapper_fortran_flags: tuple[str, ...], wrapper_c_flags: tuple[str, ...], @@ -2129,6 +2562,10 @@ def _pyi_build_manifest( "extension": { "requested_name": requested_output_name, "module_name": module_name, + "native_language": native_language, + "collision_adapters": list(collision_adapters), + "collision_adapter_all": collision_adapter_all, + "positional_only": positional_only, }, "output": { "output_dir": _manifest_path(output_dir, base=manifest_dir), @@ -2138,7 +2575,9 @@ def _pyi_build_manifest( "compiler": { "vendor": "GNU", "input_executable": input_compiler, + "input_c_executable": input_c_compiler, "fortran_flags": list(native_fortran_flags), + "c_flags": list(native_c_flags), "wrapper_compiler_debug": wrapper_compiler_debug, "wrapper_fortran_flags": list(wrapper_fortran_flags), "wrapper_c_flags": list(wrapper_c_flags), @@ -2168,7 +2607,13 @@ def _with_pyi_manifest( strict_wrapper_names: bool, requested_output_name: str | None, input_compiler: str, + input_c_compiler: str, + native_language: str, + collision_adapters: tuple[str, ...], + collision_adapter_all: bool, + positional_only: bool, native_fortran_flags: tuple[str, ...], + native_c_flags: tuple[str, ...], wrapper_compiler_debug: bool, wrapper_fortran_flags: tuple[str, ...], wrapper_c_flags: tuple[str, ...], @@ -2183,7 +2628,13 @@ def _with_pyi_manifest( strict_wrapper_names=strict_wrapper_names, requested_output_name=requested_output_name, input_compiler=input_compiler, + input_c_compiler=input_c_compiler, + native_language=native_language, + collision_adapters=collision_adapters, + collision_adapter_all=collision_adapter_all, + positional_only=positional_only, native_fortran_flags=native_fortran_flags, + native_c_flags=native_c_flags, wrapper_compiler_debug=wrapper_compiler_debug, wrapper_fortran_flags=wrapper_fortran_flags, wrapper_c_flags=wrapper_c_flags, @@ -2271,7 +2722,10 @@ def _native_link_item_from_manifest(item: object, *, base: Path) -> NativeLinkIt path = item.get("path") if not isinstance(path, str): raise ValueError(f"Wrapper build manifest {kind!r} link item is missing path") - return NativeLinkItem(kind, _resolve_manifest_path(path, base=base)) + language = item.get("language") + if language is not None and language not in {"c", "fortran"}: + raise ValueError("Wrapper build manifest native link-item language must be 'c' or 'fortran'") + return NativeLinkItem(kind, _resolve_manifest_path(path, base=base), language=language) if kind == "named_library": name = item.get("name") if not isinstance(name, str): @@ -2293,13 +2747,13 @@ def _manifest_link_items(section: dict[str, object], *, base: Path) -> tuple[Nat return tuple(_native_link_item_from_manifest(item, base=base) for item in value) -def _manifest_compilation_sources(section: dict[str, object], *, base: Path) -> tuple[Path, ...]: - """Return Fortran source paths recorded by a manifest's compilation units. - - Validates the unit-list shape and rejects source languages this replay path - cannot rebuild. Paths are resolved relative to ``base`` and returned in - recorded order without checking their current existence. - """ +def _manifest_compilation_sources( + section: dict[str, object], + *, + base: Path, + language: str, +) -> tuple[Path, ...]: + """Return one explicit-language source group recorded by a manifest.""" value = section.get("compilation_units", ()) if not isinstance(value, list): raise ValueError("Wrapper build manifest field 'compilation_units' must be a list") @@ -2307,9 +2761,11 @@ def _manifest_compilation_sources(section: dict[str, object], *, base: Path) -> for unit in value: if not isinstance(unit, dict) or not isinstance(unit.get("source"), str): raise ValueError("Wrapper build manifest compilation units must include source paths") - if unit.get("language") != "fortran": + unit_language = unit.get("language") + if unit_language not in {"c", "fortran"}: raise ValueError(f"Unsupported manifest native source language: {unit.get('language')!r}") - sources.append(_resolve_manifest_path(unit["source"], base=base)) + if unit_language == language: + sources.append(_resolve_manifest_path(unit["source"], base=base)) return tuple(sources) @@ -2396,11 +2852,22 @@ def _command_source(command: tuple[str, ...]) -> str | None: return None -def _command_language(command: tuple[str, ...]) -> str | None: - """Infer ``fortran`` or ``c`` from a command's detected source suffix.""" +def _command_language( + command: tuple[str, ...], + *, + source_languages: Mapping[str, str] | None = None, +) -> str | None: + """Return a recorded source language, using suffixes only for generated files. + + Native inputs retain their explicit language in ``ObjectFile`` records. + Generated wrapper files predate that record and keep their conventional + suffix fallback solely for selecting a Makefile recipe. + """ source = _command_source(command) if source is None: return None + if source_languages is not None and (language := source_languages.get(str(Path(source)))) is not None: + return language return "fortran" if Path(source).suffix.lower() in _FORTRAN_SOURCE_SUFFIXES else "c" @@ -2420,14 +2887,19 @@ def _make_shell_literal(text: str) -> str: return text.replace("$", "$$") -def _make_recipe(command: tuple[str, ...], working_directory: Path) -> str: +def _make_recipe( + command: tuple[str, ...], + working_directory: Path, + *, + source_languages: Mapping[str, str], +) -> str: """Convert one recorded compiler command into an overridable Make recipe. Selects the Fortran, C, or shared-linker variable from the command and separates compiler-fixed arguments from caller-overridable flag variables. The returned tab-prefixed recipe runs from ``working_directory``. """ - language = _command_language(command) + language = _command_language(command, source_languages=source_languages) if "-shared" in command: compiler_var, flags_var = "PRIK_LD", "PRIK_LDFLAGS" elif language == "fortran": @@ -2442,14 +2914,22 @@ def _make_recipe(command: tuple[str, ...], working_directory: Path) -> str: return f"\tcd {directory} && $({compiler_var}) {before_output} $({flags_var}) {output_and_after}".rstrip() -def _compiler_executable(commands: tuple[tuple[str, ...], ...], *, language: str | None, shared: bool) -> str: +def _compiler_executable( + commands: tuple[tuple[str, ...], ...], + *, + language: str | None, + shared: bool, + source_languages: Mapping[str, str], +) -> str: """Find the recorded compiler executable for one Makefile variable. Searches commands by source language or shared-link status and returns a conservative GNU compiler default when no matching command was recorded. """ for command in commands: - if ("-shared" in command) == shared and (shared or _command_language(command) == language): + if ("-shared" in command) == shared and ( + shared or _command_language(command, source_languages=source_languages) == language + ): return command[0] return "gfortran" if language == "fortran" or shared else "gcc" @@ -2476,15 +2956,16 @@ def _write_build_makefile( _absolute_command_path(_command_output(command), working_directory) for command in compile_commands ) makefile_path = path.resolve() + source_languages = {str(Path(source_object.source)): source_object.language for source_object in source_objects} # Preserve compiler selection while leaving caller-overridable flags empty. lines = [ "# Generated by prik. Edit variables or override them on the make command line.", "# User Fortran sources are conservatively chained in supplied order.", "# Generated bridge and C binding objects may be built in parallel with make -j.", - f"FC := {_make_shell_literal(shlex.quote(_compiler_executable(commands, language='fortran', shared=False)))}", - f"CC := {_make_shell_literal(shlex.quote(_compiler_executable(commands, language='c', shared=False)))}", - f"PRIK_LD := {_make_shell_literal(shlex.quote(_compiler_executable(commands, language=None, shared=True)))}", + f"FC := {_make_shell_literal(shlex.quote(_compiler_executable(commands, language='fortran', shared=False, source_languages=source_languages)))}", + f"CC := {_make_shell_literal(shlex.quote(_compiler_executable(commands, language='c', shared=False, source_languages=source_languages)))}", + f"PRIK_LD := {_make_shell_literal(shlex.quote(_compiler_executable(commands, language=None, shared=True, source_languages=source_languages)))}", "PRIK_FFLAGS ?=", "PRIK_CFLAGS ?=", "PRIK_LDFLAGS ?=", @@ -2503,25 +2984,31 @@ def _write_build_makefile( if previous_user_output is not None: dependencies.append(previous_user_output) previous_user_output = output - elif _command_language(command) == "fortran": + elif _command_language(command, source_languages=source_languages) == "fortran": dependencies.extend(user_outputs) dependency_text = " ".join(_make_target(dependency) for dependency in dict.fromkeys(dependencies)) lines.extend( [ f"{_make_target(output)}: {dependency_text}", - _make_recipe(command, working_directory), + _make_recipe(command, working_directory, source_languages=source_languages), "", ] ) - all_link_dependencies = tuple(dict.fromkeys((*compile_outputs, *extra_dependencies))) + # Extra link items are recorded as build-relative paths. Resolving them the + # same way as compile outputs keeps one spelling per object, so a produced + # object is not also demanded under a second name that has no rule. + resolved_extra_dependencies = tuple( + _absolute_command_path(dependency, working_directory) for dependency in extra_dependencies + ) + all_link_dependencies = tuple(dict.fromkeys((*compile_outputs, *resolved_extra_dependencies))) object_dependencies = " ".join(_make_target(output) for output in all_link_dependencies) # Link, rebuild, and cleanup rules share the recorded artifact paths. lines.extend( [ f"{_make_target(link_output)}: {object_dependencies}", - _make_recipe(link_command, working_directory), + _make_recipe(link_command, working_directory, source_languages=source_languages), "", "rebuild:", f"\t$(MAKE) -f {_make_target(makefile_path)} clean", @@ -2644,7 +3131,8 @@ def _fortran_wrapper_module( fortran_type_probe_runner: list[str] | None, fortran_type_probe_cache_dir: str | Path | None, refresh_fortran_type_probe: bool, -) -> tuple[object, SemanticModule]: + assume_intent_in_scalars: bool = False, +) -> tuple[object, SemanticModule, tuple[SemanticModule, ...]]: """Parse Fortran sources, resolve type facts, and form one wrapper module.""" # Preprocess and parse the complete source project. preprocessed_sources = { @@ -2676,10 +3164,11 @@ def _fortran_wrapper_module( parsed, compile_time_values=compile_time_values, type_facts=type_facts, + assume_intent_in_scalars=assume_intent_in_scalars, ) _apply_source_python_exports(modules) module_name = _validated_wrapper_module_name(output_name, source_paths[0].stem) - return parsed, _merge_wrapper_modules(modules, name=module_name) + return parsed, _merge_wrapper_modules(modules, name=module_name), tuple(modules) def _complete_pyi_fortran_boolean_types( @@ -2717,6 +3206,49 @@ def _complete_pyi_fortran_boolean_types( semantic_type.origin.source_type = native_types[boolean_storage_bits(semantic_type.name)] +def _complete_pyi_c_standard_types( + modules: list[SemanticModule], + *, + compiler: str, + compiler_args: Iterable[str], +) -> None: + """Attach compiler-probed C spellings to target-sized contract scalars. + + A source-free contract identifies C through ``native_language='c'``; this + pass resolves only the target-sized names whose storage cannot be inferred + from the contract spelling itself. The public ``Int`` or ``SizeT`` name + remains intact while policy consumes the measured dtype and exact C + declaration spelling. + """ + spellings = {"Int": "int", "UInt": "unsigned int", "SizeT": "size_t"} + semantic_types = [semantic_type for module in modules for semantic_type in _module_semantic_types(module)] + if not any(semantic_type.name in spellings for semantic_type in semantic_types): + return + report = probe_c_standard_types( + PreprocessingConfig(mode="compiler", compiler=compiler, compiler_args=list(compiler_args)) + ) + converter = CToIRConverter(standard_type_report=report) + for module in modules: + for semantic_type in _module_semantic_types(module): + spelling = spellings.get(semantic_type.name) + if spelling is None: + continue + fact = report.types.get(spelling) + resolved = converter._semantic_type_from_standard_fact(fact) if isinstance(fact, dict) else None + if resolved is None: + raise ValueError(f"C semantic contract type {semantic_type.name!r} has no supported target ABI") + semantic_type.dtype = resolved + semantic_type.metadata.update( + { + "c_type_fact": dict(fact), + "c_type_fact_source": "compiler_probe", + "c_abi_spelling": spelling, + } + ) + semantic_type.origin.source_language = "c" + semantic_type.origin.source_type = spelling + + # Public build entry points @@ -2727,6 +3259,10 @@ def build_fortran_extension( output_name: str | None = None, preprocessing: PreprocessingConfig | None = None, strict_wrapper_names: bool = False, + collision_adapters: Iterable[str] | None = None, + collision_adapter_all: bool = False, + positional_only: bool = False, + assume_intent_in_scalars: bool = False, fortran_type_report=None, fortran_type_probe_runner: list[str] | None = None, fortran_type_probe_cache_dir: str | Path | None = None, @@ -2734,6 +3270,8 @@ def build_fortran_extension( compile_input_sources: bool = True, native_fortran_sources: Iterable[str | Path] | None = None, native_fortran_flags: Iterable[str] | None = None, + native_c_sources: Iterable[str | Path] | None = None, + native_c_flags: Iterable[str] | None = None, native_objects: Iterable[str | Path] | None = None, native_libraries: Iterable[str] | None = None, native_link_items: Iterable[NativeLinkItem | dict[str, object]] | None = None, @@ -2780,6 +3318,13 @@ def build_fortran_extension( strict_wrapper_names Reject generated Python names that cannot be represented without a strict naming decision. + assume_intent_in_scalars + Treat a primitive scalar dummy that declares no ``intent`` as + ``intent(in)`` rather than applying the conservative ``intent(inout)`` + default, so its value is not projected as a Python result. Rank-zero + ``character`` dummies follow the same rule. A declared ``intent`` is + always honored, and arrays, derived-type objects, and allocatable or + pointer scalars are unaffected. fortran_type_report, fortran_type_probe_runner, fortran_type_probe_cache_dir, refresh_fortran_type_probe Optional controls for compiler-probed Fortran type facts used while @@ -2788,8 +3333,9 @@ def build_fortran_extension( Compile ``sources`` as native implementation inputs. Set false only when their implementation is supplied separately as objects, libraries, link items, or ``native_fortran_sources``. - native_fortran_sources, native_fortran_flags - Additional implementation sources and their compiler flags. + native_fortran_sources, native_fortran_flags, native_c_sources, native_c_flags + Additional explicit Fortran or C implementation sources and their + language-specific compiler flags. native_objects, native_libraries, native_link_items, native_library_dirs, native_include_dirs Existing artifacts, ``-l`` names, ordered linker records, and search @@ -2839,6 +3385,8 @@ def build_fortran_extension( native_inputs = _native_build_inputs( native_fortran_sources=implementation_source_paths, native_fortran_flags=native_fortran_flags, + native_c_sources=native_c_sources, + native_c_flags=native_c_flags, native_objects=native_objects, native_libraries=native_libraries, native_link_items=native_link_items, @@ -2846,10 +3394,10 @@ def build_fortran_extension( native_library_dirs=native_library_dirs, native_include_dirs=native_include_dirs, ) - type_probe_preprocessing = _type_probe_preprocessing(preprocessing, native_inputs.source_flags) + type_probe_preprocessing = _type_probe_preprocessing(preprocessing, native_inputs.fortran_source_flags) # 2. Parse source, resolve target facts, and assemble semantic IR. - parsed, module = _fortran_wrapper_module( + parsed, module, source_modules = _fortran_wrapper_module( source_paths, preprocessing=preprocessing, type_probe_preprocessing=type_probe_preprocessing, @@ -2858,13 +3406,18 @@ def build_fortran_extension( fortran_type_probe_runner=fortran_type_probe_runner, fortran_type_probe_cache_dir=fortran_type_probe_cache_dir, refresh_fortran_type_probe=refresh_fortran_type_probe, + assume_intent_in_scalars=assume_intent_in_scalars, ) # 3. Complete wrapper policy and generate the canonical wrapper. + collision_adapter_names = tuple(collision_adapters or ()) generated_wrapper = _generate_wrapper( module, strict_wrapper_names=strict_wrapper_names, verbose=verbose, + collision_adapters=collision_adapter_names, + collision_adapter_all=collision_adapter_all, + positional_only=positional_only, ) # 4. Prepare native compilation, dependency batches, and link inputs. @@ -2902,6 +3455,162 @@ def build_fortran_extension( source_objects=native_source_objects, extra_dependencies=_link_item_paths(native_build_plan.link_items), ) + _write_build_contract_package(source_modules, output_path, verbose=verbose) + _report_total_build_time( + verbose, + time.perf_counter() - build_started, + on_total_build_time=_on_total_build_time, + ) + return result + + +def build_c_extension( + sources: str | Path | Iterable[str | Path], + *, + output_dir: str | Path | None = None, + output_name: str | None = None, + input_c_compiler: str = "cc", + preprocessing: PreprocessingConfig | None = None, + c_type_report=None, + c_type_probe_runner: list[str] | None = None, + export_symbols: Iterable[str] | None = None, + native_c_sources: Iterable[str | Path] | None = None, + native_c_flags: Iterable[str] | None = None, + native_fortran_sources: Iterable[str | Path] | None = None, + native_fortran_flags: Iterable[str] | None = None, + input_compiler: str = "gfortran", + native_objects: Iterable[str | Path] | None = None, + native_libraries: Iterable[str] | None = None, + native_link_items: Iterable[NativeLinkItem | dict[str, object]] | None = None, + native_library_dirs: Iterable[str | Path] | None = None, + native_include_dirs: Iterable[str | Path] | None = None, + strict_wrapper_names: bool = False, + collision_adapters: Iterable[str] | None = None, + collision_adapter_all: bool = False, + positional_only: bool = False, + makefile: bool = False, + generate_sources: bool = False, + jobs: int | None = None, + verbose: bool | int = False, + wrapper_compiler_debug: bool = False, + wrapper_fortran_flags: Iterable[str] | None = None, + wrapper_c_flags: Iterable[str] | None = None, + _on_total_build_time: Callable[[float], None] | None = None, +) -> WrapperBuildResult: + """Build a direct-only C extension from explicit C implementation sources. + + C declarations are parsed from ``sources`` and converted using a probe of + ``input_c_compiler``. Their C ABI facts select the direct binding route; + unsupported operations raise a documented completed-policy diagnostic + before planning, generated files, or compiler commands. A selected genuine + identifier collision may use a separate C forwarder translation unit. + ``export_symbols`` restricts semantic conversion to those exact reachable + C functions and can explicitly select declarations from included headers. + ``native_c_sources`` adds separately compiled C inputs, while explicit + Fortran inputs are supported only as ordinary link dependencies. + + ``preprocessing`` supplies the C preprocessing configuration used to expand + ``sources`` before parsing; the default runs ``input_c_compiler``. Without + it a source containing any directive other than ``#include`` could not be + parsed at all. + """ + generation_only, compile_jobs = _resolve_build_mode( + makefile=makefile, + generate_sources=generate_sources, + jobs=jobs, + verbose=verbose, + ) + build_started = time.perf_counter() + selected_exports = None if export_symbols is None else tuple(export_symbols) + source_paths = _c_source_paths(sources) + output_path, shared_library_output_path = _wrapper_output_paths(output_dir) + supplemental_c_paths = tuple(Path(path) for path in (native_c_sources or ())) + native_inputs = _native_build_inputs( + native_fortran_sources=native_fortran_sources, + native_fortran_flags=native_fortran_flags, + native_c_sources=(*source_paths, *supplemental_c_paths), + native_c_flags=native_c_flags, + native_objects=native_objects, + native_libraries=native_libraries, + native_link_items=native_link_items, + complete_native_link_items=None, + native_library_dirs=native_library_dirs, + native_include_dirs=native_include_dirs, + ) + preprocessing = preprocessing or _default_c_preprocessing_config(input_c_compiler) + parsed_sources = tuple(_parse_c_wrapper_source(path, preprocessing) for path in source_paths) + # Fail forms that are intrinsically outside the primitive lane before the + # ABI probe, generated files, or native build commands. A supported source + # may still need the probe to resolve target-sized arithmetic facts. + preflight_modules = tuple(c_file_to_semantic_modules(parsed)[0] for parsed in parsed_sources) + if selected_exports is not None: + preflight_modules = tuple(select_c_export_functions(preflight_modules, selected_exports)) + _preflight_intrinsic_c_direct_policy( + preflight_modules, + strict_wrapper_names=strict_wrapper_names, + ) + c_report = c_type_report or probe_c_standard_types( + PreprocessingConfig( + mode="compiler", + compiler=input_c_compiler, + compiler_args=list(native_inputs.c_source_flags), + ), + runner=c_type_probe_runner, + ) + source_modules = tuple( + c_file_to_semantic_modules(parsed, standard_type_report=c_report)[0] for parsed in parsed_sources + ) + if selected_exports is not None: + source_modules = tuple(select_c_export_functions(source_modules, selected_exports)) + module_name = _validated_wrapper_module_name(output_name, source_paths[0].stem) + module = _merge_wrapper_modules(list(source_modules), name=module_name) + generated_wrapper = _generate_wrapper( + module, + strict_wrapper_names=strict_wrapper_names, + verbose=verbose, + collision_adapters=collision_adapters or (), + collision_adapter_all=collision_adapter_all, + positional_only=positional_only, + ) + output_path.mkdir(parents=True, exist_ok=True) + native_source_objects, native_build_plan = _prepare_native_build_plan(native_inputs, output_path=output_path) + wrapper_fortran_flags = _compiler_flags(wrapper_fortran_flags) + wrapper_c_flags = _compiler_flags(wrapper_c_flags) + compiler = _new_compiler( + execute_commands=not generation_only, + debug=wrapper_compiler_debug, + input_compiler=input_compiler, + input_c_compiler=input_c_compiler, + requires_fortran=_native_inputs_require_fortran(native_inputs), + ) + result = _build_generated_wrapper_extension( + generated_wrapper, + output_dir=output_path, + shared_library_output_dir=shared_library_output_path, + sources=source_paths, + native_build_plan=native_build_plan, + native_dependencies=native_source_objects, + native_compile_batches=_serial_compile_batches(native_source_objects), + native_link_args=_generated_wrapper_native_link_args(native_build_plan), + wrapper_fortran_flags=wrapper_fortran_flags, + wrapper_c_flags=wrapper_c_flags, + compiler=compiler, + compile_jobs=1 if generation_only else compile_jobs, + verbose=verbose, + ) + result = _finalize_build_mode( + result, + makefile=makefile, + generate_sources=generate_sources, + compiler=compiler, + source_objects=native_source_objects, + extra_dependencies=_link_item_paths(native_build_plan.link_items), + ) + _write_build_contract_package( + tuple(_wrapped_c_translation_unit(module) for module in source_modules), + output_path, + verbose=verbose, + ) _report_total_build_time( verbose, time.perf_counter() - build_started, @@ -2914,8 +3623,12 @@ def build_pyi_extension( contract: str | Path, *, input_compiler: str = "gfortran", + input_c_compiler: str = "cc", + native_language: str = "fortran", native_fortran_sources: Iterable[str | Path] | None = None, native_fortran_flags: Iterable[str] | None = None, + native_c_sources: Iterable[str | Path] | None = None, + native_c_flags: Iterable[str] | None = None, native_objects: Iterable[str | Path] | None = None, native_libraries: Iterable[str] | None = None, native_link_items: Iterable[NativeLinkItem | dict[str, object]] | None = None, @@ -2924,6 +3637,9 @@ def build_pyi_extension( output_name: str | None = None, output_dir: str | Path | None = None, strict_wrapper_names: bool = False, + collision_adapters: Iterable[str] | None = None, + collision_adapter_all: bool = False, + positional_only: bool = False, makefile: bool = False, generate_sources: bool = False, jobs: int | None = None, @@ -2953,11 +3669,16 @@ def build_pyi_extension( contract Existing semantic ``.pyi`` entry file. Its relative-import graph is loaded as one contract bundle. - input_compiler - Fortran compiler executable used for generated bridge code and optional - native source compilation. - native_fortran_sources, native_fortran_flags - Existing implementation source paths to compile and their flags. + input_compiler, input_c_compiler + Explicit Fortran and C compiler executables. A build with any Fortran + object uses the Fortran link driver; a C-only build uses the C driver. + native_language + Explicit ABI language of the source-free semantic contract: ``"fortran"`` + (the default) or ``"c"``. It is not inferred from filenames, compiler + executables, missing Fortran sources, or ``@native_abi("c")``. + native_fortran_sources, native_fortran_flags, native_c_sources, native_c_flags + Existing implementation source paths to compile and their + language-specific compiler flags. native_objects, native_libraries, native_link_items, native_library_dirs, native_include_dirs Existing artifacts, ``-l`` names, ordered linker records, and search @@ -3003,10 +3724,13 @@ def build_pyi_extension( # 1. Load the contract graph and collect native implementation inputs. entry = _pyi_entry_path(contract) - bundle = _pyi_contract_bundle(entry) + native_language = _native_contract_language(native_language) + bundle = _pyi_contract_bundle(entry, native_language=native_language) native_inputs = _native_build_inputs( native_fortran_sources=native_fortran_sources, native_fortran_flags=native_fortran_flags, + native_c_sources=native_c_sources, + native_c_flags=native_c_flags, native_objects=native_objects, native_libraries=native_libraries, native_link_items=native_link_items, @@ -3016,24 +3740,39 @@ def build_pyi_extension( ) output_path, shared_library_output_path = _wrapper_output_paths(output_dir) - output_path.mkdir(parents=True, exist_ok=True) wrapper_fortran_flags = _compiler_flags(wrapper_fortran_flags) wrapper_c_flags = _compiler_flags(wrapper_c_flags) # 2. Assemble semantic IR, complete policy, and generate the wrapper. modules = list(bundle.modules) - _complete_pyi_fortran_boolean_types( - modules, - compiler=input_compiler, - compiler_args=(*native_inputs.source_flags, *wrapper_fortran_flags), - ) + if native_language == "fortran": + _complete_pyi_fortran_boolean_types( + modules, + compiler=input_compiler, + compiler_args=(*native_inputs.fortran_source_flags, *wrapper_fortran_flags), + ) + else: + _preflight_intrinsic_c_direct_policy( + modules, + strict_wrapper_names=strict_wrapper_names, + ) + _complete_pyi_c_standard_types( + modules, + compiler=input_c_compiler, + compiler_args=(*native_inputs.c_source_flags, *wrapper_c_flags), + ) module_name = _validated_wrapper_module_name(output_name, _bundle_output_name(bundle)) module = _merge_wrapper_modules(modules, name=module_name) + collision_adapter_names = tuple(collision_adapters or ()) generated_wrapper = _generate_wrapper( module, strict_wrapper_names=strict_wrapper_names, verbose=verbose, + collision_adapters=collision_adapter_names, + collision_adapter_all=collision_adapter_all, + positional_only=positional_only, ) + output_path.mkdir(parents=True, exist_ok=True) # 3. Prepare native compilation and link inputs before selecting the compiler. native_source_objects, native_build_plan = _prepare_native_build_plan(native_inputs, output_path=output_path) @@ -3041,6 +3780,8 @@ def build_pyi_extension( execute_commands=not generation_only, debug=wrapper_compiler_debug, input_compiler=input_compiler, + input_c_compiler=input_c_compiler, + requires_fortran=_native_inputs_require_fortran(native_inputs) or native_language == "fortran", ) native_array_build_requirements = native_array_handle_build_requirements(module) @@ -3066,7 +3807,13 @@ def build_pyi_extension( strict_wrapper_names=strict_wrapper_names, requested_output_name=output_name, input_compiler=input_compiler, - native_fortran_flags=native_inputs.source_flags, + input_c_compiler=input_c_compiler, + native_language=native_language, + collision_adapters=collision_adapter_names, + collision_adapter_all=collision_adapter_all, + positional_only=positional_only, + native_fortran_flags=native_inputs.fortran_source_flags, + native_c_flags=native_inputs.c_source_flags, wrapper_compiler_debug=wrapper_compiler_debug, wrapper_fortran_flags=wrapper_fortran_flags, wrapper_c_flags=wrapper_c_flags, @@ -3105,6 +3852,7 @@ def build_pyi_extension_from_manifest( *, output_name: str | None = None, input_compiler: str | None = None, + input_c_compiler: str | None = None, include_dirs: Iterable[str | Path] | None = None, makefile: bool = False, generate_sources: bool = False, @@ -3123,10 +3871,10 @@ def build_pyi_extension_from_manifest( ---------- manifest Existing ``prik-build.json`` produced by a semantic ``.pyi`` build. - output_name, input_compiler, include_dirs - Optional replay overrides for the extension name, compiler executable, - and additional native include directories. All other build choices are - restored from the manifest. + output_name, input_compiler, input_c_compiler, include_dirs + Optional replay overrides for the extension name, Fortran or C compiler + executable, and additional native include directories. All other build + choices are restored from the manifest. makefile, generate_sources, jobs, verbose Output mode and compilation controls with the same meanings as :func:`build_pyi_extension`. @@ -3166,6 +3914,10 @@ def build_pyi_extension_from_manifest( requested_name = output_name if output_name is not None else extension_section.get("requested_name") if requested_name is not None and not isinstance(requested_name, str): raise ValueError("Wrapper build manifest extension.requested_name must be a string or null") + native_language = _native_contract_language(_manifest_string(extension_section, "native_language")) + collision_adapters = _manifest_string_list(extension_section, "collision_adapters") + collision_adapter_all = _manifest_bool(extension_section, "collision_adapter_all") + positional_only = _manifest_bool(extension_section, "positional_only") # 2. Restore native include paths and compiler selection from the manifest. manifest_module_dirs = _manifest_path_list(native_section, "module_dirs", base=base) @@ -3178,18 +3930,28 @@ def build_pyi_extension_from_manifest( selected_input_compiler = input_compiler if selected_input_compiler is None: selected_input_compiler = _manifest_string(compiler_section, "input_executable") + selected_input_c_compiler = input_c_compiler + if selected_input_c_compiler is None: + selected_input_c_compiler = _manifest_string(compiler_section, "input_c_executable") # 3. Delegate execution to the regular `.pyi` build path. result = build_pyi_extension( _resolve_manifest_path(entry_contract, base=base), input_compiler=selected_input_compiler, - native_fortran_sources=_manifest_compilation_sources(native_section, base=base), + input_c_compiler=selected_input_c_compiler, + native_language=native_language, + native_fortran_sources=_manifest_compilation_sources(native_section, base=base, language="fortran"), native_fortran_flags=_manifest_string_list(compiler_section, "fortran_flags"), + native_c_sources=_manifest_compilation_sources(native_section, base=base, language="c"), + native_c_flags=_manifest_string_list(compiler_section, "c_flags"), native_include_dirs=native_include_dirs, native_library_dirs=_manifest_path_list(native_section, "library_dirs", base=base), output_name=requested_name, output_dir=output_path, strict_wrapper_names=strict_wrapper_names, + collision_adapters=collision_adapters, + collision_adapter_all=collision_adapter_all, + positional_only=positional_only, makefile=makefile, generate_sources=generate_sources, jobs=jobs, diff --git a/prik/pipeline/pyi.py b/prik/pipeline/pyi.py index 49e0315bd..ef5f2fb94 100644 --- a/prik/pipeline/pyi.py +++ b/prik/pipeline/pyi.py @@ -109,9 +109,11 @@ def emit_module_stubs( """Complete and render semantic modules plus opaque dependencies. Inputs are deep-copied before dependency insertion and policy completion, - so callers retain their original semantic modules. The returned mapping is - keyed by module name and is normally written into a generated contract - package by a pipeline stage. + so callers retain their original semantic modules. C-source modules are + starter-contract extraction: they preserve parser facts without invoking + direct-wrapper policy, which is only required by a build request. The + returned mapping is keyed by module name and is normally written into a + generated contract package by a pipeline stage. """ source_modules = _module_list(modules) emitted_modules: dict[str, SemanticModule] = {} @@ -128,7 +130,7 @@ def emit_module_stubs( existing = {cls.name for cls in target.classes} target.classes.extend(cls for cls in dependency.classes if cls.name not in existing) - complete_semantic_policies(emitted_modules.values()) + complete_semantic_policies(module for module in emitted_modules.values() if module.origin.source_language != "c") return { module_name: emit_module( module, diff --git a/prik/pipeline/type_mapping_report.py b/prik/pipeline/type_mapping_report.py index 1d38863cc..f5d9de7df 100644 --- a/prik/pipeline/type_mapping_report.py +++ b/prik/pipeline/type_mapping_report.py @@ -1,17 +1,21 @@ """Orchestrate target-specific native-to-semantic-to-NumPy reports. The public functions combine compiler probes, the normal semantic converters, -and codegen's NumPy projection catalogue before rendering Markdown. This is a +and codegen's NumPy projection catalogue into one measured record. This is a cross-stage inspection pipeline, not a probe implementation or an alternative -datatype conversion path. ``c_type_mapping_markdown()`` and -``fortran_type_mapping_markdown()`` are the report boundaries; ``main()`` is -their standalone command-line adapter. +datatype conversion path. ``c_type_mapping_report()`` and +``fortran_type_mapping_report()`` are the report boundaries, and every text +format converts one of their records: ``type_mapping_markdown()`` renders the +mapping table and ``expression_probe_markdown()`` renders a measured ``--expr`` +probe. Both output formats therefore describe identical measurements. +``main()`` is their standalone command-line adapter. """ from __future__ import annotations import argparse -from collections.abc import Sequence +from collections.abc import Mapping, Sequence +from dataclasses import asdict import platform from prik.codegen.primitive_scalar_types import NumpyDtypeRegistry @@ -42,7 +46,11 @@ from prik.preprocessing import PreprocessingConfig from prik.preprocessing.probes.c_types import probe_c_standard_types_cached -from prik.preprocessing.probes.fortran_types import evaluate_fortran_type_facts, probe_fortran_type_expressions_cached +from prik.preprocessing.probes.fortran_types import ( + FortranTypeProbeReport, + evaluate_fortran_type_facts, + probe_fortran_type_expressions_cached, +) # C report inventory. @@ -180,22 +188,23 @@ def target_profile() -> str: return f"{platform.system().lower()}-{machine}" -def c_type_mapping_markdown( +def c_type_mapping_report( *, compiler: str = "cc", compiler_args: Sequence[str] = (), runner: Sequence[str] | None = None, cache_dir: str | None = None, refresh: bool = False, -) -> str: - """Render the modeled C native-to-semantic-to-NumPy mapping for one target. +) -> dict[str, object]: + """Measure the modeled C native-to-semantic-to-NumPy mapping for one target. Use this inspection report when documenting or checking how the selected compiler represents the supported C primitive and standard-library types. Compiler arguments and an optional runner select a native or cross target; cache options are forwarded to the existing C ABI probe. The returned - Markdown contains the target profile and one row per supported C spelling. - Probe and semantic-conversion failures propagate to the caller. + record contains the target profile and one entry per supported C spelling; + pass it to :func:`type_mapping_markdown` for the table. Probe and + semantic-conversion failures propagate to the caller. """ # Measure target ABI facts once for every C spelling in this fixed report. report = probe_c_standard_types_cached( @@ -207,32 +216,33 @@ def c_type_mapping_markdown( # Reuse the C semantic converter to project each measured native type. converter = CToIRConverter(standard_type_report=report) - rows = [] + mapping_entries = [] for spelling, ctype in _C_TYPES: semantic_type = converter.visit(ctype, as_type=True) fact = report.types[spelling] - rows.append((spelling, _c_fact_text(fact), _semantic_text(semantic_type), _numpy_dtype(semantic_type.dtype))) + mapping_entries.append(_mapping_entry(spelling, fact, _c_fact_text(fact), semantic_type)) - # Render the stable documentation table after all target conversion is complete. - return _markdown_table("C type", rows) + # Return the measured record; text formats convert it afterwards. + return _mapping_report("c", mapping_entries, report) -def fortran_type_mapping_markdown( +def fortran_type_mapping_report( *, compiler: str = "gfortran", compiler_args: Sequence[str] = (), runner: Sequence[str] | None = None, cache_dir: str | None = None, refresh: bool = False, -) -> str: - """Render the supported Fortran native-to-semantic-to-NumPy mapping for one target. +) -> dict[str, object]: + """Measure the supported Fortran native-to-semantic-to-NumPy mapping for one target. Use this inspection report to show how the selected compiler and flags map the maintained modern and legacy intrinsic spellings. It probes only compiler-dependent storage expressions, models fixed legacy storage and - character code units directly, then returns a Markdown table. Compiler, - runner, and cache options use the existing Fortran probe path; its failures - and semantic-conversion failures propagate to the caller. + character code units directly, then returns a measured record for + :func:`type_mapping_markdown`. Compiler, runner, and cache options use the + existing Fortran probe path; its failures and semantic-conversion failures + propagate to the caller. """ # Associate every maintained spelling with its converter key and probe expression. key_converter = FortranToIRConverter() @@ -274,31 +284,29 @@ def fortran_type_mapping_markdown( ] converter = FortranToIRConverter(type_facts=evaluate_fortran_type_facts(config, requirements, report=report)) - # Convert every displayed spelling with the shared target facts, then render it. - rows = [] + # Convert every displayed spelling with the shared target facts, then record it. + mapping_entries = [] for spelling, variable, key, _expression in entries: semantic_type = converter.visit(variable) - rows.append( - ( - spelling, - _fortran_fact_text(semantic_type, key), - _semantic_text(semantic_type), - _numpy_dtype(semantic_type.dtype), - ) - ) - return _markdown_table("Fortran type", rows) + fact = _fortran_target_fact(semantic_type, key) + mapping_entries.append(_mapping_entry(spelling, fact, _fortran_fact_text(fact), semantic_type)) + return _mapping_report("fortran", mapping_entries, report) -def _fortran_fact_text(semantic_type, key: tuple[str, str | None]) -> str: - """Format one Fortran row's target-storage description. +def _fortran_target_fact(semantic_type, key: tuple[str, str | None]) -> dict[str, object]: + """Return one Fortran spelling's measured target-storage record. Character entries intentionally bypass compiler metadata because the report models their eight-bit code unit directly. Every other entry consumes the converter metadata populated from the shared Fortran probe facts. """ if key[0] == "character": - return "8-bit storage" - fact = semantic_type.metadata["fortran_type_fact"] + return {"bits": 8} + return dict(semantic_type.metadata["fortran_type_fact"]) + + +def _fortran_fact_text(fact: Mapping[str, object]) -> str: + """Format one measured Fortran storage record for a Markdown table cell.""" return f"{fact['bits']}-bit storage" @@ -350,20 +358,86 @@ def _numpy_dtype(semantic_dtype: str | None) -> str: return expression -def _markdown_table(native_header: str, rows: list[tuple[str, str, str, str]]) -> str: - """Render ordered native, target, semantic, and NumPy rows as Markdown. +def _mapping_entry( + native: str, + target_fact: Mapping[str, object], + native_fact_text: str, + semantic_type, +) -> dict[str, object]: + """Build one serializable native-to-semantic-to-NumPy mapping entry. + + ``target_fact`` keeps the structured measurement so JSON consumers read + numbers rather than parsing prose, while the display fields carry the exact + strings the Markdown table renders. Semantic identity and NumPy projection + are read from the converted type so both formats agree by construction. + """ + return { + "native": native, + "target_fact": dict(target_fact), + "native_fact": native_fact_text, + "semantic_dtype": _semantic_text(semantic_type), + "numpy_dtype": _numpy_dtype(semantic_type.dtype), + } + + +def _mapping_report(language: str, entries: list[dict[str, object]], probe) -> dict[str, object]: + """Wrap ordered mapping entries in the serializable report envelope. + + Entries stay in their supported-display order, and ``report`` names the + record shape so machine consumers can tell a mapping table from a measured + expression probe without inspecting the payload. The originating probe's + recipe and generated source travel with the report so a JSON reader can + reproduce the measurement. + """ + return { + "report": "type_mapping", + "language": language, + "target_profile": target_profile(), + "types": entries, + "recipe": asdict(probe.recipe), + "source_text": probe.source_text, + } + + +_NATIVE_HEADER = {"c": "C type", "fortran": "Fortran type"} + + +def type_mapping_markdown(report: Mapping[str, object]) -> str: + """Render one measured type-mapping report as its Markdown table. - Native rows must already be in their supported-display order. The helper - adds the local target-profile heading and does not escape or reorder row - content, preserving the generated documentation snapshot format. + This is the only Markdown path for the mapping report: callers measure with + :func:`c_type_mapping_report` or :func:`fortran_type_mapping_report` and + convert the same record here, so the table can never drift from the JSON + form. Entries render in report order without escaping or reordering. """ + native_header = _NATIVE_HEADER[str(report["language"])] lines = [ - f"Target profile: `{target_profile()}`", + f"Target profile: `{report['target_profile']}`", "", f"| {native_header} | Native target fact | Semantic dtype | NumPy dtype |", "| --- | --- | --- | --- |", ] - lines.extend(f"| `{native}` | {fact} | `{semantic}` | `{numpy}` |" for native, fact, semantic, numpy in rows) + lines.extend( + f"| `{entry['native']}` | {entry['native_fact']} | `{entry['semantic_dtype']}` | `{entry['numpy_dtype']}` |" + for entry in report["types"] + ) + return "\n".join(lines) + + +def expression_probe_markdown(report: FortranTypeProbeReport) -> str: + """Render one measured Fortran expression probe as a Markdown table. + + Use this to read a ``--expr`` probe in the same shape as the mapping table. + Values render in measurement order; the compiler recipe and generated + program stay in the JSON form, which remains the complete record. + """ + lines = [ + f"Compiler: `{report.recipe.compiler}`", + "", + "| Fortran expression | Measured value |", + "| --- | --- |", + ] + lines.extend(f"| `{expression}` | {value} |" for expression, value in report.values.items()) return "\n".join(lines) @@ -392,16 +466,18 @@ def main(argv: list[str] | None = None) -> int: "refresh": args.refresh, } if args.language == "c": - print(c_type_mapping_markdown(compiler=args.compiler or "cc", **options)) + print(type_mapping_markdown(c_type_mapping_report(compiler=args.compiler or "cc", **options))) else: - print(fortran_type_mapping_markdown(compiler=args.compiler or "gfortran", **options)) + print(type_mapping_markdown(fortran_type_mapping_report(compiler=args.compiler or "gfortran", **options))) return 0 __all__ = ( - "c_type_mapping_markdown", - "fortran_type_mapping_markdown", + "c_type_mapping_report", + "expression_probe_markdown", + "fortran_type_mapping_report", "target_profile", + "type_mapping_markdown", ) @@ -415,7 +491,9 @@ def main(argv: list[str] | None = None) -> int: if compiler is None: raise SystemExit("The direct type-mapping example requires cc on PATH.") with tempfile.TemporaryDirectory(prefix="prik-type-mapping-example-") as cache_dir: - markdown = c_type_mapping_markdown(compiler=compiler, cache_dir=cache_dir, refresh=True) + markdown = type_mapping_markdown( + c_type_mapping_report(compiler=compiler, cache_dir=cache_dir, refresh=True) + ) print(next(line for line in markdown.splitlines() if line.startswith("| `int` |"))) else: raise SystemExit(main()) diff --git a/prik/pipeline/wrapper.py b/prik/pipeline/wrapper.py index bdd7b4962..5fcc5c512 100644 --- a/prik/pipeline/wrapper.py +++ b/prik/pipeline/wrapper.py @@ -241,6 +241,7 @@ def generate( started = time.perf_counter() c_modules = self._c_generator.binding_modules(plan) c_sources = tuple(self._c_printer.doprint(module) for module in c_modules) + c_module_names = tuple(module.name for module in c_modules) if progress is not None: progress("Generate binding source", time.perf_counter() - started) @@ -269,6 +270,7 @@ def generate( return self._generated_wrapper( plan.owner_path, c_sources, + c_module_names, c_header_source, fortran_source, native_support_keys=(("binding_support",) if self._c_generator.requires_native_support(plan) else ()), @@ -1355,7 +1357,9 @@ def _module_write_through_setter_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Validate one scalar module write-through setter.""" diagnostics = [] - if plan.bridge.native_assignment is not AssignmentMode.VALUE_COPY: + # A character write copies a byte buffer rather than a value, but it is + # the same write-through contract; every other mechanism is rejected. + if plan.bridge.native_assignment not in {AssignmentMode.VALUE_COPY, AssignmentMode.CHARACTER_COPY}: diagnostics.append( self._diagnostic(plan.owner_path, "invalid-module-native-assignment", plan.bridge.native_assignment) ) @@ -1436,6 +1440,7 @@ def _function_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnost ), *self._duplicate_role_diagnostics(plan), *self._available_role_diagnostics(plan), + *self._argument_update_diagnostics(plan), *self._binding_conversion_order_diagnostics(plan), *self._function_output_diagnostics(plan), *self._string_result_aggregation_diagnostics(plan), @@ -1462,6 +1467,42 @@ def _function_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnost diagnostics.extend(self._string_writeback_diagnostics(plan)) return tuple(diagnostics) + def _argument_update_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnostic, ...]: + """Require every argument-update result to pair with a descriptor string input. + + The replaced value is read back from the adapter local its argument + converts, so that pairing must exist and must be the completed + allocatable or pointer character-buffer input. A result without it + would publish whichever storage the adapter happened to declare, which + compiles and imports while returning the pre-call value. + """ + arguments = {argument.owner_path: argument for argument in plan.arguments} + diagnostics = [] + for result in plan.results: + if not result.updates_argument: + continue + argument = arguments.get(result.owner_path) + if argument is None: + diagnostics.append(self._diagnostic(result.owner_path, "missing-update-result-argument", None)) + continue + if not argument.projects_character_descriptor_update: + diagnostics.append( + self._diagnostic(result.owner_path, "invalid-update-result-argument", argument.owner_path) + ) + if argument.entrypoint.handoff_mode is not ArgumentHandoffMode.CHARACTER_BUFFER: + diagnostics.append( + self._diagnostic( + result.owner_path, + "invalid-update-result-argument-handoff", + argument.entrypoint.handoff_mode.value, + ) + ) + if any(action.owner_path == result.owner_path for action in plan.writeback_actions): + diagnostics.append( + self._diagnostic(result.owner_path, "unexpected-update-result-writeback", result.result_position) + ) + return tuple(diagnostics) + @staticmethod def _adapter_slots(plan: FunctionPlan) -> tuple[NativeEntrypointProjectedSlotPlan, ...]: """Return ordered projected slots that carry an adapter facet.""" @@ -1565,12 +1606,15 @@ def _entrypoint_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagno def _expected_entrypoint_parameter_groups(plan: FunctionPlan) -> tuple[tuple[str, str], ...]: """Return the C-ABI parameter groups required by completed transfer facts.""" argument_owners = {argument.owner_path for argument in plan.arguments} + updated_owners = {result.owner_path for result in plan.results if result.updates_argument} groups: list[tuple[str, str]] = [] for slot in sorted(plan.entrypoint.projected_slots, key=lambda item: item.native_position): if slot.source_kind == "result": groups.append((slot.owner_path, "hidden_result")) elif slot.owner_path in argument_owners: groups.append((slot.owner_path, "argument")) + if slot.owner_path in updated_owners: + groups.append((slot.owner_path, "hidden_result")) else: groups.append((slot.owner_path, "projected_slot")) groups.extend( @@ -1662,6 +1706,7 @@ def _entrypoint_result_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPla expected_owners = { *(slot.owner_path for slot in plan.entrypoint.projected_slots if slot.source_kind == "result"), *(result.owner_path for result in plan.results if result.source_kind == "direct_return"), + *(result.owner_path for result in plan.results if result.updates_argument), } extra = tuple( result.owner_path for result in plan.entrypoint.results if result.owner_path not in expected_owners @@ -3742,7 +3787,9 @@ def _array_itemsize_diagnostics( if array is None: return () if plan.datatype_family is DatatypeFamily.STRING: - if array.itemsize is None or array.itemsize <= 0 or array.itemsize_role is None: + # The role is mandatory because the runtime width always crosses; + # the literal is optional, because a contract may leave it assumed. + if array.itemsize_role is None or (array.itemsize is not None and array.itemsize <= 0): return (self._diagnostic(plan.owner_path, "invalid-array-itemsize", array.itemsize),) return () if array.itemsize is not None or array.itemsize_role is not None: @@ -4023,9 +4070,14 @@ def _string_address_length_diagnostics( plan: ArgumentTransferPlan, label: str, ) -> tuple[WrapperPlanDiagnostic, ...]: - """Require one fixed plan length and prohibit a runtime length ABI role.""" + """Require a plan length and prohibit a runtime length ABI role. + + Assumed-capacity rank-zero storage states no width, so the plan instead + records that the caller's itemsize travels beside the address. + """ diagnostics = [] - if plan.character_length is None or plan.character_length <= 0: + assumed_capacity = plan.character_length is None and plan.entrypoint.pass_character_length + if not assumed_capacity and (plan.character_length is None or plan.character_length <= 0): diagnostics.append( self._diagnostic(plan.owner_path, f"invalid-string-{label}-length", plan.character_length) ) @@ -4052,7 +4104,7 @@ def _string_codegen_diagnostics(self, plan: ArgumentTransferPlan) -> tuple[Wrapp diagnostics.append(self._diagnostic(plan.owner_path, "invalid-string-copy-reason", plan.bridge.copy_reason)) if action is CodegenAction.COPY_IN_OUT: diagnostics.extend(self._string_replacement_diagnostics(plan)) - elif plan.projects_result: + elif plan.projects_result and not plan.projects_character_descriptor_update: diagnostics.append( self._diagnostic(plan.owner_path, "call-local-string-projects-result", plan.result_position) ) @@ -4207,7 +4259,9 @@ def _result_diagnostics( plan.bridge.codegen_action, ) ) - if plan.source_kind == "direct_return": + if plan.updates_argument: + diagnostics.extend(self._update_result_diagnostics(plan, function_slots)) + elif plan.source_kind == "direct_return": diagnostics.extend(self._direct_result_diagnostics(plan)) elif plan.source_kind == "hidden_output": diagnostics.extend(self._hidden_result_diagnostics(plan, function_slots)) @@ -4337,10 +4391,6 @@ def _scalar_descriptor_family_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, "invalid-scalar-descriptor-runtime-length", descriptor.runtime_length) ) - if descriptor.runtime_length and plan.character_length is not None: - diagnostics.append( - self._diagnostic(plan.owner_path, "fixed-length-scalar-descriptor-result", plan.character_length) - ) return tuple(diagnostics) def _scalar_descriptor_ownership_diagnostics( @@ -4391,10 +4441,19 @@ def _scalar_descriptor_source_diagnostics( self, plan: ResultPlan, ) -> tuple[WrapperPlanDiagnostic, ...]: - """Validate exact hidden-slot sharing or direct-result independence.""" + """Validate exact hidden-slot sharing or direct-result independence. + + An argument update shares the Python-visible input's slot, which carries + that input's own handoff rather than a descriptor, so its descriptor is + owned by the result record alone. + """ descriptor = plan.scalar_descriptor if descriptor is None: return () + if plan.updates_argument: + if plan.projected_call_slot is None or plan.projected_call_slot.scalar_descriptor is not None: + return (self._diagnostic(plan.owner_path, "inconsistent-update-descriptor-native-slot", None),) + return () if plan.source_kind == "hidden_output": if plan.projected_call_slot is None or plan.projected_call_slot.scalar_descriptor is not descriptor: return (self._diagnostic(plan.owner_path, "inconsistent-scalar-descriptor-native-slot", None),) @@ -4570,6 +4629,39 @@ def _direct_result_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagn ) return tuple(diagnostics) + def _update_result_diagnostics( + self, + plan: ResultPlan, + function_slots: dict[int, NativeEntrypointProjectedSlotPlan], + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate one result that returns a Python-visible argument's new value. + + Unlike a hidden output, this result has no native slot of its own: it + shares the input slot of the argument it updates, and that slot describes + the input handoff. The checks therefore require the shared slot to be + that Python argument's slot at the same result position, and require the + descriptor that carries the reallocated storage to be present. Its + pairing with a descriptor character input is validated once per + function by :meth:`_argument_update_diagnostics`. + """ + slot = plan.projected_call_slot + if slot is None: + return (self._diagnostic(plan.owner_path, "missing-update-result-native-slot", None),) + diagnostics = [] + if slot.source_kind == "result" or slot.python_position is None: + diagnostics.append(self._diagnostic(plan.owner_path, "invalid-update-result-native-slot", slot.source_kind)) + if slot.result_position != plan.result_position: + diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-result-position", slot.result_position)) + if function_slots.get(slot.native_position) is not slot: + diagnostics.append( + self._diagnostic(plan.owner_path, "inconsistent-function-result-slot", slot.native_position) + ) + if plan.scalar_descriptor is None: + diagnostics.append(self._diagnostic(plan.owner_path, "missing-update-result-descriptor", None)) + if plan.bridge is None: + diagnostics.append(self._diagnostic(plan.owner_path, "missing-update-result-adapter-facet", None)) + return tuple(diagnostics) + def _hidden_result_diagnostics( self, plan: ResultPlan, @@ -5479,6 +5571,7 @@ def _expected_available_roles(self, plan: FunctionPlan) -> tuple[str, ...]: *self._argument_extent_roles(plan.arguments), *self._argument_descriptor_output_roles(plan.arguments), *self._native_slot_roles(plan.entrypoint.projected_slots, "result"), + *self._update_result_roles(plan.results), *self._direct_result_roles(plan.results), *self._declaration_callable_roles(plan.declaration_callables), ) @@ -5516,6 +5609,11 @@ def _direct_result_roles(results: tuple[ResultPlan, ...]) -> tuple[str, ...]: result.entrypoint.native_result_role for result in results if result.source_kind == "direct_return" ) + @staticmethod + def _update_result_roles(results: tuple[ResultPlan, ...]) -> tuple[str, ...]: + """Return native result roles produced beside a Python-visible argument.""" + return tuple(result.entrypoint.native_result_role for result in results if result.updates_argument) + @staticmethod def _declaration_callable_roles( declarations: tuple[DeclarationCallablePlan, ...], @@ -5556,6 +5654,7 @@ def _generated_wrapper( self, module_name: str, c_sources: tuple[str, ...], + c_module_names: tuple[str, ...], c_header: str, fortran_source: str | None, native_support_keys: tuple[str, ...], @@ -5565,16 +5664,14 @@ def _generated_wrapper( ) -> GeneratedWrapper: """Package rendered source text with the filenames owned by build integration. - Binding translation-unit paths preserve the primary file followed by - zero-padded worker shards. The returned wrapper places bridge, C + Each binding translation unit is named for the C module it renders, so + the primary file is followed by its zero-padded worker shards and then + any collision-adapter unit. The returned wrapper places bridge, C sources, and header text in that stable order; this helper does not write files or freeze the newly assembled source records. """ # Name bridge, binding, and header files before pairing each with rendered text. - binding_sources = ( - Path(f"{module_name}_wrapper.c"), - *(Path(f"{module_name}_wrapper_{index:03d}.c") for index in range(1, len(c_sources))), - ) + binding_sources = tuple(Path(f"{name}.c") for name in c_module_names) bridge_sources = tuple( dict.fromkeys( Path(path) diff --git a/prik/planning/entrypoints.py b/prik/planning/entrypoints.py index 2f13ab3b5..1761f833f 100644 --- a/prik/planning/entrypoints.py +++ b/prik/planning/entrypoints.py @@ -34,6 +34,7 @@ ArgumentTransferPlan, CallbackHandoffPlan, CallbackTransferPlan, + DatatypeFamily, DerivedFieldPlan, DerivedMemberPathPlan, DerivedTypePlan, @@ -335,6 +336,7 @@ def _descriptor_parameter( pointer_depth=1, semantic_type_name=semantic_type_name, rank=handle.array.rank, + character_length=handle.array.itemsize if semantic_type_name == "String" else None, descriptor_kind=handle.descriptor_kind, intent=intent, ) @@ -568,6 +570,11 @@ def _derived_field_operations( ) -> tuple[GeneratedSupportProcedureEntrypointPlan, ...]: operations = [] for derived in self.derived_types: + # An abstract type has no instance to address, so it publishes no + # accessor of its own; each concrete extension already generates one + # for every component it inherits. + if derived.abstract: + continue for field in derived.fields: operations.extend(self._field_operations(derived, field, "direct")) for variable in self.variables: @@ -906,17 +913,46 @@ def _primary_module_variable_operations(self, variable): ModuleGetterAction.BORROWED_ARRAY_VIEW, ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE, }: - parameters = tuple( - self._int64_parameter(f"extent_{axis}", reference=True, intent="out") - for axis in range(variable.array.rank) + # A character element reports the width its own declaration + # carries, which for a parameter may come from an initializer. + width = ( + (self._int64_parameter("itemsize", reference=True, intent="out"),) + if variable.datatype_family is DatatypeFamily.STRING + else () + ) + parameters = ( + *width, + *( + self._int64_parameter(f"extent_{axis}", reference=True, intent="out") + for axis in range(variable.array.rank) + ), ) result = self._opaque_result() + elif ( + variable.bridge.native_getter_action is ModuleGetterAction.NULLABLE_SNAPSHOT + and variable.datatype_family is DatatypeFamily.STRING + ): + parameters = (self._int64_parameter("length", reference=True, intent="out"),) + result = self._opaque_result() elif variable.bridge.native_getter_action in { ModuleGetterAction.NULLABLE_SNAPSHOT, ModuleGetterAction.DERIVED_OBJECT, }: parameters = () result = self._opaque_result() + elif variable.bridge.native_getter_action is ModuleGetterAction.CHARACTER_VALUE: + # A character value has no by-value C ABI, so it copies out + # through the same fixed-width buffer a character field uses. + parameters = ( + self._value( + "value", + NativeEntrypointABIValueKind.CHARACTER, + pointer_depth=1, + character_length=variable.character_length, + intent="out", + ), + ) + result = self._void_result() else: parameters = () result = self._scalar_result(variable.semantic_type_name) @@ -930,12 +966,23 @@ def _primary_module_variable_operations(self, variable): ) ) if variable.entrypoint.setter_role is not None: + if variable.bridge.native_getter_action is ModuleGetterAction.CHARACTER_VALUE: + value = self._value( + "value", + NativeEntrypointABIValueKind.CHARACTER, + pointer_depth=1, + const=True, + character_length=variable.character_length, + intent="in", + ) + else: + value = self._scalar_parameter(variable.semantic_type_name) operations.append( self._operation( variable.owner_path, "module:set", f"bind_c_set_{variable.symbol_name}", - (self._scalar_parameter(variable.semantic_type_name),), + (value,), ) ) return tuple(operations) diff --git a/prik/planning/models.py b/prik/planning/models.py index d87c802ea..8210b8b83 100644 --- a/prik/planning/models.py +++ b/prik/planning/models.py @@ -67,6 +67,7 @@ ModuleGetterAction, ModuleObjectAccessMechanism, NativeArrayDescriptorInterop, + CharacterLocalRelease, NativeArrayDescriptorKind, NativeArrayDescriptorOwnership, NativeArrayDefaultConstruction, @@ -186,6 +187,31 @@ class GeneratedSupportProcedureEntrypointPlan(StageRecord): implementation_owner: GeneratedSupportProcedureImplementationOwner +@dataclass +class DirectCABITypePlan(StageRecord): + """One exact C declaration type copied from completed policy.""" + + source_spelling: str | None + scalar_type_name: str | None + pointer_depth: int + qualifiers: tuple[str, ...] + const: bool + # Scalar values whose native declaration differs from canonical contract + # storage are converted at the call boundary. Exact NumPy storage already + # has the native representation, so its completed decision remains false. + converts_to_contract_storage: bool = False + + +@dataclass +class DirectCABIPlan(StageRecord): + """Direct C ABI declaration facts consumed by binding lowering only.""" + + calling_convention: str + result_transport: str + result: DirectCABITypePlan | None + parameters: tuple[DirectCABITypePlan, ...] + + # ============================================================================ # Derived types and generated class surfaces # ============================================================================ @@ -314,6 +340,8 @@ class DerivedTypePlan(StageRecord): finalizers: tuple[str, ...] bind_c: bool sequence: bool + abstract: bool = False + deferred_bindings: tuple[str, ...] = () @dataclass @@ -566,6 +594,20 @@ class NativeArrayHandlePlan(StageRecord): default_handle: NativeArrayDefaultHandlePlan +@dataclass +class CharacterLocalPlan(StageRecord): + """Describe the adapter-local storage one scalar character value needs. + + The C ABI does not change with the native attribute; the adapter local + does. ``descriptor_kind`` is the attribute the local carries, and + ``release`` names the deallocation the adapter still owns afterwards. + """ + + descriptor_kind: NativeArrayDescriptorKind | None + deferred_length: bool + release: CharacterLocalRelease + + @dataclass class ScalarDescriptorResultPlan(StageRecord): """Describe a nullable rank-zero descriptor result and its copy/release contract.""" @@ -576,6 +618,7 @@ class ScalarDescriptorResultPlan(StageRecord): copy_reason: str release_owner: OwnershipOwner presence_role: str + may_be_unallocated: bool = False @dataclass @@ -612,6 +655,13 @@ class BindingStatusErrorPlan(StageRecord): message_role: str | None success: int exception_kind: PythonExceptionKind + # Owner path of the visible Python argument whose caller-supplied buffer + # carries the message. Mutually exclusive with ``message_role``, which names + # a projected native output the binding itself materialized. + message_argument: str | None = None + # Declared capacity of a hidden message, which bounds the binding's read of + # fixed-length native character storage. + message_character_length: int | None = None @dataclass @@ -686,6 +736,7 @@ class BindingModuleVariablePlan(StageRecord): setter_action: SetterAction initializer: Any constant_value: Any + setter_converts_characters: bool = False @dataclass @@ -729,6 +780,7 @@ class ModuleVariablePlan(StageRecord): array: ArrayHandoffPlan | None native_array_handle: NativeArrayHandlePlan | None derived: DerivedModuleObjectPlan | None = None + character_length: int | None = None docstring: str | None = None @@ -747,6 +799,9 @@ class BindingFunctionPlan(StageRecord): status_error: BindingStatusErrorPlan | None argument_conversion_order: tuple[str, ...] public: bool = True + # A positional-only binding parses its arguments from the call tuple alone, + # so it declares no keyword list and installs no METH_KEYWORDS entry. + accepts_keyword_arguments: bool = True @dataclass @@ -772,6 +827,11 @@ class NativeEntrypointFunctionPlan(StageRecord): parameters: tuple[NativeEntrypointParameterPlan, ...] results: tuple[NativeEntrypointResultPlan, ...] projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...] + direct_c_abi: DirectCABIPlan | None = None + # A selected symbol is reached through a forwarder defined in a separate + # translation unit that never includes Python.h, so the binding's own + # declaration of ``symbol_name`` cannot collide with a header declaration. + collision_adapter_symbol: str | None = None @dataclass @@ -819,6 +879,7 @@ class BindingArgumentPlan(StageRecord): nullable: bool writable: bool descriptor_boundary: bool + native_array_element_c_type: str | None = None @dataclass @@ -855,6 +916,7 @@ class BridgeArgumentPlan(StageRecord): codegen_action: CodegenAction data_action: BridgeDataAction copy_reason: str | None + character_local: CharacterLocalPlan | None = None @dataclass @@ -884,6 +946,12 @@ class NativeEntrypointResultPlan(StageRecord): native_array_handle: NativeArrayHandlePlan | None scalar_descriptor: ScalarDescriptorResultPlan | None passing: EntrypointPassingConvention + native_scalar_c_type: str | None = None + updates_argument: bool = False + # Set only on a direct-C hidden character output: the binding owns a buffer + # of this many bytes and passes ``char *``. A bridged route leaves it None + # and keeps the adapter's owned-allocation protocol. + character_capacity: int | None = None @dataclass @@ -946,6 +1014,7 @@ class NativeEntrypointProjectedSlotPlan(StageRecord): value_kind: str symbolic_role: str object_kind: ObjectKind | None + native_scalar_c_type: str | None = None scalar_logical_abi: ScalarLogicalABI = ScalarLogicalABI.NOT_APPLICABLE scalar_native_type: str | None = None array_logical_abi: ArrayLogicalABI = ArrayLogicalABI.NOT_APPLICABLE @@ -1168,6 +1237,24 @@ class ArgumentTransferPlan(StageRecord): bridge: BridgeArgumentPlan | None projected_call_slot: NativeEntrypointProjectedSlotPlan transformations: tuple[TransformationPlan, ...] = () + native_storage_c_type: str | None = None + character_allows_embedded_nul: bool = False + + @property + def projects_character_descriptor_update(self) -> bool: + """Report whether this input also returns its replaced value as a result. + + The completed bridge fact ``character_local`` belongs only to a + call-local ``allocatable`` or ``pointer`` character input, so an input + that also occupies a Python result position returns through the matching + ``updates_argument`` result rather than through argument writeback. + """ + return bool( + self.bridge is not None + and self.bridge.character_local is not None + and self.bridge.character_local.descriptor_kind is not None + and self.projects_result + ) @dataclass @@ -1178,6 +1265,11 @@ class ResultPlan(StageRecord): share the corresponding function-wide projected slot. Binding, entrypoint, and bridge facets hold their completed projection, transport, and production choices. + + ``updates_argument`` carries the completed policy fact that this result is + the reallocated value of a Python-visible ``character(len=:), allocatable`` + argument. Such a result shares that argument's native call slot instead of + owning a result slot of its own. """ owner_path: str @@ -1185,6 +1277,9 @@ class ResultPlan(StageRecord): datatype_family: DatatypeFamily source_kind: str result_position: int + # False for a ``Hidden`` slot: the native call still produces the value, but + # the binding builds no Python object from it. + python_returned: bool character_length: int | None object_kind: ObjectKind ownership_owner: OwnershipOwner @@ -1202,6 +1297,7 @@ class ResultPlan(StageRecord): scalar_descriptor: ScalarDescriptorResultPlan | None = None derived: DerivedHandoffPlan | None = None transformations: tuple[TransformationPlan, ...] = () + updates_argument: bool = False @dataclass diff --git a/prik/planning/planner.py b/prik/planning/planner.py index e4f02f21d..8388a664d 100644 --- a/prik/planning/planner.py +++ b/prik/planning/planner.py @@ -12,7 +12,7 @@ from __future__ import annotations from collections import Counter, defaultdict -from collections.abc import Mapping +from collections.abc import Iterable, Mapping from dataclasses import dataclass, replace from types import MappingProxyType @@ -47,6 +47,7 @@ NativeArrayActualPolicy, NativeArrayDefaultConstruction, NativeArrayDefaultHandlePolicy, + CharacterLocalPolicy, NativeArrayHandleWrapperPolicy, NativeDescriptorHandoffABI, NativeDescriptorHandoffPolicy, @@ -60,6 +61,7 @@ TransformationPolicy, WritebackPhase, NativeEntrypointAction, + DirectCABIPolicy, ) from prik.policy.construction import ( completed_class_surface_policy, @@ -68,7 +70,7 @@ completed_module_variable_policy, ) from prik.policy.exports import PythonExportPolicy -from prik.policy.ownership import NativeBarrierAction, SetterAction +from prik.policy.ownership import AssignmentMode, NativeBarrierAction, SetterAction from prik.planning.models import ( ArrayHandoffPlan, ArgumentTransferPlan, @@ -118,6 +120,8 @@ NativeEntrypointArgumentPlan, NativeEntrypointCallbackPlan, NativeEntrypointFunctionPlan, + DirectCABIPlan, + DirectCABITypePlan, NativeEntrypointModulePlan, NativeEntrypointModuleVariablePlan, NativeEntrypointParameterPlan, @@ -134,6 +138,7 @@ ProcedurePrototypePlan, ProcedurePrototypeResultPlan, ResultPlan, + CharacterLocalPlan, ScalarDescriptorResultPlan, TransformationPlan, ) @@ -153,11 +158,17 @@ "Int16": DatatypeFamily.INTEGER, "Int32": DatatypeFamily.INTEGER, "Int64": DatatypeFamily.INTEGER, + "UInt8": DatatypeFamily.INTEGER, + "UInt16": DatatypeFamily.INTEGER, + "UInt32": DatatypeFamily.INTEGER, + "UInt64": DatatypeFamily.INTEGER, "SizeT": DatatypeFamily.INTEGER, "Float32": DatatypeFamily.REAL, "Float64": DatatypeFamily.REAL, + "Float128": DatatypeFamily.REAL, "Complex64": DatatypeFamily.COMPLEX, "Complex128": DatatypeFamily.COMPLEX, + "Complex256": DatatypeFamily.COMPLEX, "String": DatatypeFamily.STRING, } @@ -281,6 +292,22 @@ class WrapperPlanner(ClassVisitor): code generator validates and freezes it. """ + def __init__( + self, + *, + collision_adapters: Iterable[str] = (), + collision_adapter_all: bool = False, + ) -> None: + """Record which native symbols the binding reaches through a forwarder. + + ``collision_adapters`` names individual native symbols; + ``collision_adapter_all`` selects every direct C entrypoint. Only a + direct C symbol is eligible, because a generated bridge symbol is + already PRIK-owned and cannot collide with a header declaration. + """ + self._collision_adapters = frozenset(collision_adapters) + self._collision_adapter_all = collision_adapter_all + def visit(self, node, *args, **kwargs): """Project one completed policy record through its named handler.""" return self._visit(node, *args, **kwargs) @@ -290,6 +317,21 @@ def _visit_not_supported(node): """Reject inputs outside the completed semantic-policy vocabulary.""" raise TypeError(f"WrapperPlanner does not support completed policy {type(node).__name__}") + def _collision_adapter_symbol(self, policy: FunctionWrapperPolicy) -> str | None: + """Return the forwarder symbol selected for one entrypoint, or ``None``. + + Only a C-source direct entrypoint is eligible: it alone carries the + exact C declaration the adapter unit must reconstruct. A Fortran + ``bind(C)`` procedure keeps its backend-projected prototype, and a + generated bridge symbol is PRIK-owned and cannot collide. + """ + if policy.entrypoint_action is not NativeEntrypointAction.DIRECT_C_ABI or policy.direct_c_abi is None: + return None + symbol_name = policy.entrypoint_symbol + if not (self._collision_adapter_all or symbol_name in self._collision_adapters): + return None + return NativeSymbolNames.collision_adapter(symbol_name) + def build(self, module: models.SemanticModule) -> ModulePlan: """Build an editable wrapper plan from one policy-completed module. @@ -582,6 +624,8 @@ def _derived_type_plan( finalizers=policy.finalizers, bind_c=policy.bind_c, sequence=policy.sequence, + abstract=policy.abstract, + deferred_bindings=policy.deferred_bindings, ) # Generated class surfaces compose Phase 8 types and ordinary function plans. @@ -1098,6 +1142,7 @@ def _module_variable_plan( setter_action=policy.setter_action, initializer=policy.initializer, constant_value=policy.constant_value, + setter_converts_characters=policy.native_assignment is AssignmentMode.CHARACTER_COPY, ), entrypoint=NativeEntrypointModuleVariablePlan( descriptor_kind=policy.descriptor_kind, @@ -1110,6 +1155,7 @@ def _module_variable_plan( native_getter_action=policy.getter_action, native_assignment=policy.native_assignment, ), + character_length=policy.character_length, array=self._array_plan(policy.array, policy.owner_path), native_array_handle=self._native_array_handle_plan(policy.native_array_handle, policy.owner_path), derived=( @@ -1163,7 +1209,11 @@ def _function_plan( projected_slots = self._projected_slot_plans(policy) arguments = self._argument_plans(policy, projected_slots) results = self._result_plans(policy, projected_slots) - entrypoint_results = self._entrypoint_result_plans(results, projected_slots) + entrypoint_results = self._entrypoint_result_plans( + results, + projected_slots, + direct_c_abi=policy.direct_c_abi is not None, + ) declaration_callables = tuple(self._declaration_callable_plan(item) for item in policy.declaration_callables) status_error = self._status_error_plan(policy.status_error, projected_slots) @@ -1181,6 +1231,7 @@ def _function_plan( status_error=status_error, argument_conversion_order=self._binding_argument_conversion_order(arguments), public=public, + accepts_keyword_arguments=policy.accepts_keyword_arguments, ), entrypoint=NativeEntrypointFunctionPlan( symbol_name=( @@ -1196,6 +1247,8 @@ def _function_plan( ), results=entrypoint_results, projected_slots=projected_slots, + direct_c_abi=self._direct_c_abi_plan(policy.direct_c_abi), + collision_adapter_symbol=self._collision_adapter_symbol(policy), ), bridge=( BridgeFunctionPlan( @@ -1225,6 +1278,29 @@ def _function_plan( release_actions=release_actions, ) + @staticmethod + def _direct_c_abi_plan(policy: DirectCABIPolicy | None) -> DirectCABIPlan | None: + """Project already-completed exact C ABI facts without reinterpreting them.""" + if policy is None: + return None + + def project(value): + return DirectCABITypePlan( + source_spelling=value.source_spelling, + scalar_type_name=value.scalar_type_name, + pointer_depth=value.pointer_depth, + qualifiers=value.qualifiers, + const=value.const, + converts_to_contract_storage=value.converts_to_contract_storage, + ) + + return DirectCABIPlan( + calling_convention=policy.calling_convention, + result_transport=policy.result_transport, + result=project(policy.result) if policy.result is not None else None, + parameters=tuple(project(item) for item in policy.parameters), + ) + @staticmethod def _binding_argument_conversion_order( arguments: tuple[ArgumentTransferPlan, ...], @@ -1253,12 +1329,17 @@ def _entrypoint_parameter_plans( ) -> tuple[NativeEntrypointParameterPlan, ...]: """Record C-ABI parameter groups in emitted call/prototype order.""" argument_owners = {argument.owner_path for argument in arguments} + updated_owners = {result.owner_path for result in results if result.updates_argument} groups: list[tuple[str, str, int | None]] = [] for slot in sorted(projected_slots, key=lambda item: item.native_position): if slot.source_kind == "result": groups.append((slot.owner_path, "hidden_result", slot.native_position)) elif slot.owner_path in argument_owners: groups.append((slot.owner_path, "argument", slot.native_position)) + # A string update returns through its own output group, placed + # directly after the input it belongs to. + if slot.owner_path in updated_owners: + groups.append((slot.owner_path, "hidden_result", slot.native_position)) else: groups.append((slot.owner_path, "projected_slot", slot.native_position)) groups.extend( @@ -1292,24 +1373,40 @@ def _entrypoint_result_plans( self, results: tuple[ResultPlan, ...], projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...], + *, + direct_c_abi: bool = False, ) -> tuple[NativeEntrypointResultPlan, ...]: """Collect every C-ABI result, including binding-private status outputs.""" public = {result.owner_path: result.entrypoint for result in results} - hidden = tuple( - public.get(slot.owner_path) or self._entrypoint_result_plan_from_slot(slot) - for slot in sorted(projected_slots, key=lambda item: item.native_position) - if slot.source_kind == "result" - ) + hidden_items = [] + for slot in sorted(projected_slots, key=lambda item: item.native_position): + if slot.source_kind != "result": + continue + result = public.get(slot.owner_path) + if result is None: + result = self._entrypoint_result_plan_from_slot(slot, direct_c_abi=direct_c_abi) + hidden_items.append(result) + hidden = tuple(hidden_items) + # A string update produces no result slot of its own; its output group + # travels beside the Python-visible argument it updates. + updates = tuple(result.entrypoint for result in results if result.updates_argument) direct = tuple(result.entrypoint for result in results if result.source_kind == "direct_return") - return (*hidden, *direct) + return (*hidden, *updates, *direct) @staticmethod def _entrypoint_result_plan_from_slot( slot: NativeEntrypointProjectedSlotPlan, + *, + direct_c_abi: bool = False, ) -> NativeEntrypointResultPlan: """Project one non-public hidden output into the shared C-ABI result view.""" if slot.semantic_type_name is None or slot.datatype_family is None or slot.object_kind is None: raise ValueError(f"Hidden entrypoint result {slot.owner_path!r} has incomplete type facts") + character_capacity = ( + slot.character_length + if direct_c_abi and slot.semantic_type_name == "String" and slot.character_length + else None + ) return NativeEntrypointResultPlan( owner_path=slot.owner_path, parameter_name=slot.native_name.casefold(), @@ -1325,6 +1422,8 @@ def _entrypoint_result_plan_from_slot( native_array_handle=slot.native_array_handle, scalar_descriptor=slot.scalar_descriptor, passing=slot.passing, + native_scalar_c_type=slot.native_scalar_c_type, + character_capacity=character_capacity, ) @staticmethod @@ -1453,6 +1552,7 @@ def _projected_slot_plans( value_kind=slot_policy.value_kind, symbolic_role=role, object_kind=slot_policy.object_kind, + native_scalar_c_type=slot_policy.native_scalar_c_type, scalar_logical_abi=slot_policy.scalar_logical_abi, scalar_native_type=slot_policy.scalar_native_type, array_logical_abi=slot_policy.array_logical_abi, @@ -1582,6 +1682,8 @@ def _visit_ArgumentPolicy( bridge=(self._bridge_argument_plan(policy) if projected_slot.adapter is not None else None), projected_call_slot=projected_slot, transformations=tuple(self.visit(item) for item in policy.transformations), + native_storage_c_type=policy.native_storage_c_type, + character_allows_embedded_nul=policy.character_allows_embedded_nul, ) def _callback_handoff_plan( @@ -1755,6 +1857,7 @@ def _binding_argument_plan( nullable=policy.nullable, writable=policy.writable, descriptor_boundary=policy.descriptor_boundary, + native_array_element_c_type=policy.native_array_element_c_type, ) def _entrypoint_argument_plan( @@ -1787,6 +1890,17 @@ def _entrypoint_argument_plan( ), ) + @staticmethod + def _character_local_plan(policy: CharacterLocalPolicy | None) -> CharacterLocalPlan | None: + """Mechanically project the completed adapter-local character storage.""" + if policy is None: + return None + return CharacterLocalPlan( + descriptor_kind=policy.descriptor_kind, + deferred_length=policy.deferred_length, + release=policy.release, + ) + @staticmethod def _bridge_argument_plan(policy: ArgumentPolicy) -> BridgeArgumentPlan: """Project adapter-local conversion and original-dummy facts.""" @@ -1796,6 +1910,7 @@ def _bridge_argument_plan(policy: ArgumentPolicy) -> BridgeArgumentPlan: codegen_action=policy.codegen_action, data_action=policy.bridge_data_action, copy_reason=policy.bridge_copy_reason, + character_local=WrapperPlanner._character_local_plan(policy.character_local), ) @staticmethod @@ -1893,6 +2008,7 @@ def _visit_ResultPolicy( semantic_type_name=policy.semantic_type_name, datatype_family=datatype_family, source_kind=policy.source_kind, + python_returned=policy.python_returned, result_position=policy.result_position, character_length=policy.character_length, object_kind=policy.ownership.kind, @@ -1914,7 +2030,7 @@ def _visit_ResultPolicy( ), entrypoint=NativeEntrypointResultPlan( owner_path=policy.owner_path, - parameter_name=(policy.native_name.casefold() if policy.native_name is not None else None), + parameter_name=self._result_parameter_name(policy), source_kind=policy.source_kind, result_position=policy.result_position, native_result_role=native_role, @@ -1927,6 +2043,8 @@ def _visit_ResultPolicy( native_array_handle=native_array_handle, scalar_descriptor=scalar_descriptor, passing=policy.entrypoint_passing, + native_scalar_c_type=(projected_slot.native_scalar_c_type if projected_slot is not None else None), + updates_argument=policy.updates_argument, ), bridge=( BridgeResultPlan( @@ -1942,8 +2060,24 @@ def _visit_ResultPolicy( projected_call_slot=projected_slot, scalar_descriptor=scalar_descriptor, transformations=tuple(self.visit(item) for item in policy.transformations), + updates_argument=policy.updates_argument, ) + @staticmethod + def _result_parameter_name(policy: ResultPolicy) -> str | None: + """Return the C-ABI parameter name reserved for one result group. + + A hidden output owns its dummy's name. A result that updates a + Python-visible argument instead crosses the boundary beside that + argument's own input parameters, so its output group takes the + ``_output`` suffix already used for descriptor copyout rather than + colliding with the input's own name and length. + """ + if policy.native_name is None: + return None + name = policy.native_name.casefold() + return f"{name}_output" if policy.updates_argument else name + def _result_array_plan( self, policy: ResultPolicy, @@ -1974,8 +2108,14 @@ def _result_scalar_descriptor_plan( policy: ResultPolicy, projected_slot: NativeEntrypointProjectedSlotPlan | None, ) -> ScalarDescriptorResultPlan | None: - """Reuse exact hidden descriptor state or project one direct result.""" - if projected_slot is not None: + """Reuse exact hidden descriptor state or project this result's own record. + + A hidden output owns a dedicated result slot that already carries the + completed descriptor, and both views must stay identical. A string + update instead shares its Python-visible argument's input slot, so its + descriptor comes from the result policy that owns it. + """ + if projected_slot is not None and projected_slot.source_kind == "result": return projected_slot.scalar_descriptor return self._scalar_descriptor_result_plan(policy.scalar_descriptor, policy.owner_path) @@ -2055,6 +2195,7 @@ def _scalar_descriptor_result_plan( copy_reason=policy.copy_reason, release_owner=policy.release_owner, presence_role=f"{owner_path}:present", + may_be_unallocated=policy.may_be_unallocated, ) # Native-array-handle planning. @@ -2322,8 +2463,12 @@ def _array_runtime_rank_role(self, policy: ArrayHandoffPolicy, owner_path: str) return f"{owner_path}:rank" if policy.rank is None else None def _array_itemsize_role(self, policy: ArrayHandoffPolicy, owner_path: str) -> str | None: - """Name the itemsize role only for fixed-width character arrays.""" - return f"{owner_path}:itemsize" if policy.itemsize is not None else None + """Name the itemsize role for every character array. + + The width crosses at runtime whether or not the contract declared it, + because each element of the caller's array shares one itemsize. + """ + return f"{owner_path}:itemsize" if policy.character else None def _array_layout_roles( self, @@ -2346,9 +2491,14 @@ def _status_error_plan( if policy is None: return None roles = {slot.owner_path: slot.symbolic_role for slot in projected_slots} + # A visible message is read through its Python argument, so it has no + # projected slot to name. + visible_message = policy.message is not None and policy.message.python_position is not None try: status_role = roles[policy.status.owner_path] - message_role = roles[policy.message.owner_path] if policy.message is not None else None + message_role = ( + roles[policy.message.owner_path] if policy.message is not None and not visible_message else None + ) except KeyError as error: raise ValueError(f"Completed native status output {error.args[0]!r} has no native-call slot") from None return BindingStatusErrorPlan( @@ -2356,6 +2506,10 @@ def _status_error_plan( message_role=message_role, success=policy.success, exception_kind=policy.exception_kind, + message_argument=policy.message.owner_path if visible_message else None, + message_character_length=( + policy.message.character_length if policy.message is not None and not visible_message else None + ), ) def _planned_bridge_slot( @@ -2392,6 +2546,7 @@ def _available_roles( *self._argument_extent_roles(arguments), *self._argument_descriptor_output_roles(arguments), *self._native_result_roles(projected_slots), + *self._update_result_roles(results), *self._direct_result_roles(results), *self._declaration_callable_roles(declaration_callables), ) @@ -2490,6 +2645,10 @@ def _direct_result_roles(self, results: tuple[ResultPlan, ...]) -> tuple[str, .. result.entrypoint.native_result_role for result in results if result.source_kind == "direct_return" ) + def _update_result_roles(self, results: tuple[ResultPlan, ...]) -> tuple[str, ...]: + """Return roles produced by results that update a Python-visible argument.""" + return tuple(result.entrypoint.native_result_role for result in results if result.updates_argument) + def _datatype_family(self, semantic_type_name: str) -> DatatypeFamily: """Copy the backend-relevant family of one supported semantic type.""" try: diff --git a/prik/policy/completion.py b/prik/policy/completion.py index 27fd280eb..33425982f 100644 --- a/prik/policy/completion.py +++ b/prik/policy/completion.py @@ -21,6 +21,7 @@ ObjectKind, SetterAction, default_ownership_policy, + is_character_descriptor_update, ownership_context_for_argument, ) from prik.semantics.ownership_metadata import OWNERSHIP_POLICY_METADATA, POINTER_POLICY_METADATA @@ -29,6 +30,7 @@ ADDRESS_ROLE_PROJECTION, ADDRESS_ROLE_RAW, BIND_TARGET_METADATA, + EXPLICIT_C_EXPORT_METADATA, MAYBE_UNALLOCATED_METADATA, OPTIONAL_ABSENT_HANDLE_METADATA, PROJECTED_OUTPUT_METADATA, @@ -104,6 +106,7 @@ def complete_semantic_policies( semantic_ir: models.SemanticModule | Iterable[models.SemanticModule], *, strict_wrapper_names: bool = False, + positional_only: bool = False, ) -> list[models.SemanticModule]: """Complete policy decisions for semantic modules after parser-to-IR conversion. @@ -111,8 +114,10 @@ def complete_semantic_policies( either one module or any iterable of modules, mutates each in place, and returns an ordered list of those same objects for pipeline chaining. ``strict_wrapper_names`` is forwarded to export and class-surface policy - validation. Invalid or incomplete semantic contracts raise ``ValueError`` - rather than leaving a lower stage to choose a fallback. + validation. ``positional_only`` completes a keyword-free Python surface + where every argument is required. Invalid or incomplete semantic contracts + raise ``ValueError`` rather than leaving a lower stage to choose a + fallback. This shared post-IR boundary completes entry export reachability, ownership, transfer, destruction, mutability/writeback, projection, nullability, @@ -129,9 +134,118 @@ def complete_semantic_policies( # Resolve all remaining ownership and wrapper-facing semantic choices. _complete_ownership_policies(module, strict_wrapper_names=strict_wrapper_names) + _reject_ineligible_direct_c_operations(module) + if positional_only: + _complete_positional_only_surface(module) return modules +def _complete_positional_only_surface(module: models.SemanticModule) -> None: + """Complete a keyword-free Python surface for one module. + + A positional-only callable exposes no argument names, so the names a native + declaration happens to use -- reserved spellings such as ``__x``, or none at + all -- stop being part of the Python API. Policy therefore renames the + visible arguments to their position and records that the binding takes no + keywords. A function with an optional argument keeps keywords, because + skipping one still requires naming the rest. + """ + if module.overload_sets or any(semantic_class.overload_sets for semantic_class in module.classes): + raise ValueError("A positional-only surface does not support overload sets, which dispatch on keywords") + declarations = [*module.functions] + declarations.extend(method for semantic_class in module.classes for method in semantic_class.methods) + for function in declarations: + policy = function.metadata.get(models.RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA) + if not isinstance(policy, FunctionWrapperPolicy) or not _accepts_positional_only_call(policy): + continue + function.metadata[models.RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] = replace( + policy, + arguments=tuple( + replace(argument, python_name=f"arg{argument.python_position}") for argument in policy.arguments + ), + accepts_keyword_arguments=False, + ) + + +def _accepts_positional_only_call(policy: FunctionWrapperPolicy) -> bool: + """Report whether every visible argument of one function must be supplied.""" + return all(argument.optional_mode in _REQUIRED_ARGUMENT_MODES for argument in policy.arguments) + + +_REQUIRED_ARGUMENT_MODES = frozenset({OptionalMode.REQUIRED, OptionalMode.REQUIRED_DESCRIPTOR}) + + +_C_DIRECT_DIAGNOSTIC_PREFIX = "C_DIRECT_" + + +def _reject_ineligible_direct_c_operations(module: models.SemanticModule) -> None: + """Raise C primitive-lane diagnostics before wrapper planning can begin. + + The direct-only C lane has no adapter to fall back to, so a declaration of + the wrapped translation unit that this lane cannot reach is an error rather + than a silently omitted export. That covers module variables and class + surfaces too, because a C module has no generated accessor route for them. + A blocker every language shares -- an unexported concrete procedure behind + an overload set, for example -- is left to planning. + """ + declarations = [*module.functions] + declarations.extend(procedure for group in module.overload_sets for procedure in group.procedures) + declarations.extend(method for semantic_class in module.classes for method in semantic_class.methods) + for function in declarations: + if not _is_wrapped_c_declaration(module, function): + continue + policy = function.metadata.get(models.RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA) + if not isinstance(policy, FunctionWrapperPolicy) or policy.supported: + continue + if not any(blocker.startswith(_C_DIRECT_DIAGNOSTIC_PREFIX) for blocker in policy.blockers): + # A shared policy fact such as an unexported concrete procedure is + # not a C lane limitation. Planning already decides those the same + # way it does for Fortran, so only this lane's own diagnostics stop + # the build here. + continue + details = "; ".join(policy.blockers) + raise ValueError(f"C direct operation {policy.owner_path!r} is unsupported before wrapper planning: {details}") + for variable in module.variables: + if not _is_wrapped_c_declaration(module, variable): + continue + raise ValueError( + f"C direct operation '{module.name}.{variable.name}' is unsupported before wrapper planning: " + f"{_c_module_variable_blocker(variable)}" + ) + for semantic_class in module.classes: + if not _is_wrapped_c_declaration(module, semantic_class): + continue + raise ValueError( + f"C direct operation '{module.name}.{semantic_class.name}' is unsupported before wrapper planning: " + f"C_DIRECT_AGGREGATE_TYPE:{semantic_class.name}" + ) + + +def _c_module_variable_blocker(variable: models.SemanticVariable) -> str: + """Name the post-Goal-3 C surface one module variable would require.""" + if variable.origin.source_kind == "enum_constant": + return f"C_DIRECT_ENUM_CONSTANT:{variable.name}" + if variable.origin.source_kind == "macro": + return f"C_DIRECT_MACRO_CONSTANT:{variable.name}" + return f"C_DIRECT_NATIVE_GLOBAL_STATE:{variable.name}" + + +def _is_wrapped_c_declaration(module: models.SemanticModule, node) -> bool: + """Return whether one C declaration belongs to the wrapped translation unit. + + A declaration expanded from an include is normally inspection-only. An + export-symbol selection marks the exact included functions the user chose, + making those declarations part of the direct C surface without changing + their source provenance. + """ + if node.origin.source_language != "c": + return False + if node.metadata.get(EXPLICIT_C_EXPORT_METADATA): + return True + filename = node.origin.source_location.get("filename") if isinstance(node.origin.source_location, dict) else None + return not (isinstance(filename, str) and filename != module.origin.native_name) + + # Entry export reachability @@ -465,11 +579,15 @@ def _complete_class_method_policies( """ type_bound_targets = _type_bound_target_names(module_functions) module_targets = {str(function.native_name or function.name) for function in module_functions} + private_module_targets = { + str(function.native_name or function.name) for function in module_functions if function.visibility == "private" + } for semantic_class in class_nodes: _complete_one_class_method_policy( semantic_class, type_bound_targets, module_targets, + private_module_targets, derived_types, polymorphic_variants, ) @@ -488,6 +606,7 @@ def _complete_one_class_method_policy( semantic_class: models.SemanticClass, type_bound_targets: set[str], module_targets: set[str], + private_module_targets: set[str], derived_types: dict[tuple[str, str], DerivedTypePolicy], polymorphic_variants: dict[tuple[str, str], tuple[tuple[str, str], ...]], ) -> None: @@ -522,6 +641,7 @@ def _complete_one_class_method_policy( derived, type_bound_targets, module_targets, + private_module_targets, derived_types, polymorphic_variants, ) @@ -584,6 +704,7 @@ def _complete_class_overload_methods( derived: DerivedTypePolicy, type_bound_targets: set[str], module_targets: set[str], + private_module_targets: set[str], derived_types: dict[tuple[str, str], DerivedTypePolicy], polymorphic_variants: dict[tuple[str, str], tuple[tuple[str, str], ...]], ) -> None: @@ -603,6 +724,7 @@ def _complete_class_overload_methods( generic_bindings, type_bound_targets, module_targets, + private_module_targets, derived_types, polymorphic_variants, ) @@ -615,6 +737,7 @@ def _complete_one_class_overload_method( generic_bindings: dict[str, str], type_bound_targets: set[str], module_targets: set[str], + private_module_targets: set[str], derived_types: dict[tuple[str, str], DerivedTypePolicy], polymorphic_variants: dict[tuple[str, str], tuple[tuple[str, str], ...]], ) -> None: @@ -636,12 +759,20 @@ def _complete_one_class_overload_method( else None, ) overload_kind = str(procedure.metadata.get(models.OVERLOAD_KIND_METADATA, "generic")) + # An overload dispatches through a native generic only when its own name is + # one. `__init__` is a Python name with no native counterpart, so a + # constructor candidate falls back to the specific procedure it selects -- + # or, when that specific is private and therefore unreachable by name, to + # the constructor generic Fortran names for the type itself. + dispatches_through_overload_name = overload_kind != "generic" and overload.name != "__init__" + if not bind_target and overload.name == "__init__" and native_name in private_module_targets: + bind_target = derived.native_type_name native_dispatch_name = ( str(bind_target) if bind_target else ( str(procedure.metadata.get(models.FORTRAN_GENERIC_NAME_METADATA, overload.name)) - if overload_kind != "generic" + if dispatches_through_overload_name else None ) ) @@ -888,8 +1019,23 @@ def extends(candidate: tuple[str, str], base: tuple[str, str]) -> bool: return candidate == base or any(extends(parent, base) for parent in bases.get(candidate, ())) identities = tuple(surface.type_identity for surface in surfaces) + # An abstract type has no instance, so it is never the dynamic type a caller + # can supply; it stays a dispatch base without becoming one of its own cases. + abstract_identities = { + surface.type_identity + for semantic_class, surface in zip(class_nodes, surfaces, strict=False) + if any( + str(attribute).casefold() == "abstract" + for attribute in semantic_class.metadata.get("fortran_type_attributes", ()) + ) + } return { - base: tuple(candidate for candidate in reversed(identities) if extends(candidate, base)) for base in identities + base: tuple( + candidate + for candidate in reversed(identities) + if extends(candidate, base) and candidate not in abstract_identities + ) + for base in identities } @@ -947,6 +1093,7 @@ def _complete_function( ownership_context_for_argument(function, argument), owner_path=f"{owner_path}.{argument.name}", ) + _complete_update_result_ownership(argument) 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()) @@ -989,11 +1136,11 @@ def _complete_native_status_error_policy(function: models.SemanticFunction, owne message_name = raw_policy.get("message") message = None if message_name is not None: - message = _native_status_output(function, owner_path, message_name, subject="message") + message = _native_status_output(function, owner_path, message_name, subject="message", allow_visible=True) if message.rank != 0 or message.semantic_type_name != "String": raise ValueError( f"Function {function.name!r} raises message target {message.name!r} " - "must be a scalar string hidden output" + "must be a scalar string hidden output or visible argument" ) if message.owner_path == status.owner_path: raise ValueError(f"Function {function.name!r} raises status and message targets must be distinct") @@ -1012,30 +1159,29 @@ def _native_status_output( output_name: object, *, subject: str, + allow_visible: bool = False, ) -> NativeStatusOutputPolicy: - """Return one completed hidden output selected by a runtime policy.""" + """Return one completed output selected by a runtime policy. + + A status is always a hidden projected output. A message may instead name a + visible argument, which lets the caller supply the buffer the native code + writes into; the declared storage then carries its own capacity. + """ + noun = "a hidden output or visible argument" if allow_visible else "a hidden output" if not isinstance(output_name, str) or not output_name: - raise ValueError(f"Function {function.name!r} raises {subject} target must name a hidden output") - mappings = tuple( - mapping - for mapping in function.projection - if ( - mapping.python_position is None - and isinstance(mapping.result_position, int) - and output_name in {mapping.python_name, mapping.native_name} - ) - ) - if len(mappings) != 1: - raise ValueError(f"Function {function.name!r} raises {subject} target must name a hidden output") - mapping = mappings[0] + raise ValueError(f"Function {function.name!r} raises {subject} target must name {noun}") + mapping = _sole_status_output_mapping(function, output_name, allow_visible=allow_visible) + if mapping is None or not isinstance(mapping.native_position, int): + raise ValueError(f"Function {function.name!r} raises {subject} target must name {noun}") argument = next((item for item in function.arguments if item.name == mapping.python_name), None) - if argument is None or not isinstance(mapping.native_position, int): - raise ValueError(f"Function {function.name!r} raises {subject} target must name a hidden output") + if argument is None: + raise ValueError(f"Function {function.name!r} raises {subject} target must name {noun}") + visible = isinstance(mapping.python_position, int) decision = argument.metadata.get(models.RESOLVED_OWNERSHIP_POLICY_METADATA) - if not isinstance(decision, OwnershipDecision) or not _is_compatible_status_handoff(decision): + if not isinstance(decision, OwnershipDecision) or not _is_compatible_status_handoff(decision, visible=visible): raise ValueError( f"Function {function.name!r} raises {subject} target {output_name!r} " - "has no compatible completed hidden-output handoff" + f"has no compatible completed {'visible-argument' if visible else 'hidden-output'} handoff" ) semantic_type = argument.semantic_type return NativeStatusOutputPolicy( @@ -1047,11 +1193,55 @@ def _native_status_output( semantic_type_name=semantic_type.name, rank=int(semantic_type.rank or 0), character_length=_fixed_character_length(semantic_type), + python_position=mapping.python_position if visible else None, ) -def _is_compatible_status_handoff(decision: OwnershipDecision) -> bool: - """Report whether a hidden scalar/string result has a valid status handoff action.""" +def _is_status_output_mapping(mapping: models.ProjectionMapping, *, allow_visible: bool) -> bool: + """Report whether one mapping projects a status output this policy accepts. + + A hidden projected output carries a result position and no Python position. + A visible argument is accepted only where the caller may supply the buffer. + """ + if mapping.python_position is None and isinstance(mapping.result_position, int): + return True + return allow_visible and isinstance(mapping.python_position, int) + + +def _sole_status_output_mapping( + function: models.SemanticFunction, + output_name: str, + *, + allow_visible: bool, +) -> models.ProjectionMapping | None: + """Return the one projection mapping named by a status target, if unambiguous.""" + mappings = tuple( + mapping + for mapping in function.projection + if output_name in {mapping.python_name, mapping.native_name} + and _is_status_output_mapping(mapping, allow_visible=allow_visible) + ) + return mappings[0] if len(mappings) == 1 else None + + +_VISIBLE_STATUS_STRING_ACTIONS = frozenset( + { + # A caller-supplied NumPy bytes buffer the native code writes in place. + CodegenAction.IN_PLACE_ARGUMENT, + # A borrowed Python ``str`` payload; the contract states what C expects. + CodegenAction.CALL_LOCAL_INPUT, + } +) + + +def _is_compatible_status_handoff(decision: OwnershipDecision, *, visible: bool = False) -> bool: + """Report whether a scalar/string argument has a valid status handoff action.""" + if visible: + return bool( + decision.kind is ObjectKind.STRING + and decision.python_visible + and decision.codegen_action in _VISIBLE_STATUS_STRING_ACTIONS + ) expected_action = { ObjectKind.SCALAR: CodegenAction.DIRECT_VALUE, ObjectKind.STRING: CodegenAction.COPY_OUT, @@ -1436,9 +1626,7 @@ def _native_array_handle_operations( return () if descriptor_kind == "allocatable": operations = {"allocated", "to_numpy"} - if handle_kind in {"borrowed_module_descriptor", "borrowed_field_descriptor", "owned_result_descriptor"} or ( - context.is_argument and context.writes_argument - ): + if _handle_releases_its_own_storage(handle_kind, context): operations.add("deallocate") if not _is_deferred_character_array(semantic_type): operations.add("resize") @@ -1447,13 +1635,33 @@ def _native_array_handle_operations( pointer_policy = _pointer_policy_metadata(semantic_type) if _pointer_policy_allows_allocate(pointer_policy): operations.add("allocate") - if _pointer_policy_allows_deallocate(pointer_policy): + if _handle_releases_its_own_storage(handle_kind, context) or _pointer_policy_allows_deallocate(pointer_policy): operations.add("deallocate") if _pointer_policy_allows_resize(pointer_policy): operations.add("resize") return tuple(sorted(operations)) +def _handle_releases_its_own_storage(handle_kind: str, context: OwnershipContext) -> bool: + """Report whether one handle exposes manual release of the storage it names. + + Releasing is offered wherever the equivalent Fortran is an ordinary + ``deallocate`` on the same entity: a result the wrapper received, a module + or field descriptor, and a mutable argument. A read-only input is excluded, + because freeing storage the caller supplied is not the caller's intent. + + The operation is manual in both handle families. prik never releases native + storage on its own, so withholding the operation does not protect anything; + it only removes the caller's ability to free storage the native procedure + handed over, which is exactly what a Fortran caller would deallocate. + """ + return handle_kind in { + "borrowed_module_descriptor", + "borrowed_field_descriptor", + "owned_result_descriptor", + } or (context.is_argument and context.writes_argument) + + def _is_deferred_character_array(semantic_type: models.SemanticType) -> bool: """Return whether shape mutation also requires a runtime character length.""" return semantic_type.name == "String" and semantic_type.metadata.get("fortran_character_length") == ":" @@ -1741,6 +1949,36 @@ def _complete_variable( _complete_native_array_handle_variable_policy(variable, context) +def _complete_update_result_ownership(argument: models.SemanticArgument) -> None: + """Complete the projected result facet of one caller-supplied string update. + + A ``character(len=:), allocatable, intent(inout)`` dummy owns two decisions: + the argument facet already resolved above converts the caller's ``str`` into + call-local native storage, and this facet describes the freshly allocated + value the native procedure leaves behind. The result facet is resolved from + the same native output context an ``intent(out)`` dummy uses, so every later + stage validates and lowers it exactly like one hidden descriptor output. + Arguments outside that lane keep a single decision. + """ + argument.metadata.pop(models.RESOLVED_UPDATE_RESULT_OWNERSHIP_POLICY_METADATA, None) + decision = argument.metadata.get(models.RESOLVED_OWNERSHIP_POLICY_METADATA) + if not isinstance(decision, OwnershipDecision): + return + if not is_character_descriptor_update(argument.semantic_type.metadata, decision): + return + argument.metadata[models.RESOLVED_UPDATE_RESULT_OWNERSHIP_POLICY_METADATA] = ( + default_ownership_policy.decide_semantic_variable( + argument, + OwnershipContext.argument( + reads_argument=False, + writes_argument=True, + projects_result=True, + python_visible=False, + ), + ) + ) + + def _complete_accessor_policies(variable: models.SemanticVariable, context: OwnershipContext) -> None: """Attach resolved getter and setter decisions, then refresh descriptor-handle facts.""" variable.metadata[models.RESOLVED_GETTER_OWNERSHIP_POLICY_METADATA] = ( diff --git a/prik/policy/construction.py b/prik/policy/construction.py index c8834d52b..85cb16626 100644 --- a/prik/policy/construction.py +++ b/prik/policy/construction.py @@ -14,14 +14,19 @@ from collections.abc import Mapping from dataclasses import dataclass, replace +import numpy + from immutabledict import immutabledict +from prik.contracts import NATIVE_C_SCALAR_CASTS from prik.naming import NamingPolicy from prik.semantics import models from prik.semantics.metadata import ( ADDRESS_ROLE_METADATA, ADDRESS_ROLE_RAW, BIND_TARGET_METADATA, + NATIVE_C_SCALAR_CAST_METADATA, + NULLABLE_ANNOTATION_METADATA, SCALAR_STORAGE_CATEGORY, SUPPRESS_DEFAULT_CONSTRUCTOR_METADATA, ) @@ -43,6 +48,10 @@ SetterAction, StorageMode, TransferMode, + character_descriptor_kind, + declared_character_length, + is_character_descriptor_update, + uses_deferred_character_length, ) from prik.policy.models import ( FIXED_STRING_RESULT_COPY_REASON, @@ -57,6 +66,8 @@ LOGICAL_SCALAR_KIND_COPY_REASON, LOGICAL_ARRAY_KIND_COPY_REASON, NativeEntrypointAction, + DirectCABITypePolicy, + DirectCABIPolicy, EntrypointPassingConvention, EntrypointOptionalityAction, EntrypointProjectionAction, @@ -113,6 +124,8 @@ OverloadCandidatePolicy, OverloadPolicy, ClassSurfacePolicy, + CharacterLocalPolicy, + CharacterLocalRelease, NativeArrayDescriptorKind, NativeArrayHandleKind, NativeDescriptorHandoffABI, @@ -167,23 +180,52 @@ "Int16", "Int32", "Int64", + "UInt8", + "UInt16", + "UInt32", + "UInt64", + "SizeT", "Float32", "Float64", + "Float128", "Complex64", "Complex128", + "Complex256", } ) +# Two 128-bit reals differ only in mantissa width: x87 extended precision and +# IEEE binary128 share a storage size. Whether either is representable depends +# on the build target's ``long double``, so the decision reads measured facts +# rather than the source language. +_EXTENDED_PRECISION_SCALAR_TYPES = frozenset({"Float128", "Complex256"}) + +# Binding-owned extent, length, presence, and workspace slots record the Python +# position of the argument they are derived from, but they never transport it. +_DERIVED_NATIVE_CALL_SLOT_KINDS = frozenset({"computed", "work"}) + +# Qualifiers describe the native view of a pointee, not the call-local storage +# that receives a converted Python value, so they are dropped whole rather than +# by substring, which would corrupt a spelling that merely contains one. +_C_POINTEE_QUALIFIER_WORDS = frozenset({"const", "restrict", "__restrict", "__restrict__", "volatile", "_Atomic"}) + _NUMPY_DTYPE_NAMES = { **dict.fromkeys(BOOLEAN_SEMANTIC_TYPE_NAMES, "bool"), "Int8": "int8", "Int16": "int16", "Int32": "int32", "Int64": "int64", + "UInt8": "uint8", + "UInt16": "uint16", + "UInt32": "uint32", + "UInt64": "uint64", + "SizeT": "uintp", "Float32": "float32", "Float64": "float64", + "Float128": "longdouble", "Complex64": "complex64", "Complex128": "complex128", + "Complex256": "clongdouble", } @@ -377,18 +419,15 @@ def build_derived_type_policy( str(attribute).casefold() for attribute in semantic_class.metadata.get("fortran_type_attributes", ()) } deferred_bindings = tuple(semantic_class.metadata.get("fortran_deferred_bindings", ())) + abstract = "abstract" in type_attributes blockers = tuple( [*(f"field {name!r} is missing completed derived-field policy" for name in missing)] + [reason for field in fields for reason in field.blockers] + ( - ["abstract derived types need a non-instantiable Python class policy"] - if "abstract" in type_attributes + [f"deferred type-bound procedure {name!r} needs a declaring abstract type" for name in deferred_bindings] + if not abstract else [] ) - + [ - f"deferred type-bound procedure {name!r} needs an override and dispatch policy" - for name in deferred_bindings - ] ) exports = completed_python_exports(semantic_class, semantic_class.name) native_type_name = str(semantic_class.native_name or semantic_class.name) @@ -407,6 +446,8 @@ def build_derived_type_policy( sequence=bool(semantic_class.metadata.get("fortran_sequence")), supported=not blockers, blockers=blockers, + abstract=abstract, + deferred_bindings=deferred_bindings, ) @@ -559,7 +600,28 @@ def _class_constructor_policy( owner_path: str, derived: DerivedTypePolicy, ) -> tuple[ConstructorPolicy, tuple[str, ...]]: - """Select exactly one constructor surface from the semantic contract.""" + """Select exactly one constructor surface from the semantic contract. + + An abstract native type has no constructor at all: Fortran forbids an + instance of it, so the generated class exposes its inherited surface while + only a concrete extension can be created. + """ + if derived.abstract: + return ( + ConstructorPolicy( + kind=ClassConstructorKind.ABSENT, + fields=(), + target_owner_path=None, + overload_name=None, + call=None, + lifecycle=(), + rejection_message=( + f"{semantic_class.name} is an abstract native type and cannot be instantiated; " + "create one of its concrete extensions instead" + ), + ), + (), + ) bound = tuple( method for method in semantic_class.methods @@ -997,6 +1059,7 @@ def _module_variable_policy_base( "native_module": str(variable.origin.native_scope or module_name), "semantic_type_name": variable.semantic_type.name, "rank": int(variable.semantic_type.rank or 0), + "character_length": _character_length(variable.semantic_type), } @@ -1126,7 +1189,7 @@ def _constant_array_module_variable_blockers( blockers.append("module parameter array is not public") if array is None or array.rank is None or array.rank <= 0 or len(array.shape) != array.rank: blockers.append("module parameter array requires one concrete fixed rank") - if variable.semantic_type.name not in _PLAN_PRIMITIVE_SCALAR_TYPES: + if variable.semantic_type.name not in _PLAN_PRIMITIVE_SCALAR_TYPES | {"String"}: blockers.append("module parameter array requires a primitive numeric element type") expected_getter = ( getter is not None @@ -1168,7 +1231,7 @@ def _scalar_module_variable_policy( getter_action=getter_action, getter=getter, setter_action=setter.setter_action if setter is not None else SetterAction.OMIT, - native_assignment=_scalar_module_native_assignment(setter), + native_assignment=_scalar_module_native_assignment(setter, variable), setter=setter, descriptor_kind=descriptor_kind, initializer=( @@ -1194,7 +1257,7 @@ def _ordinary_array_module_variable_blockers( blockers = [] if array.rank is None or array.rank <= 0 or len(array.shape) != array.rank: blockers.append("ordinary module array requires one concrete fixed rank") - if variable.semantic_type.name not in _PLAN_PRIMITIVE_SCALAR_TYPES: + if variable.semantic_type.name not in _PLAN_PRIMITIVE_SCALAR_TYPES | {"String"}: blockers.append("ordinary module array requires a primitive numeric element type") if not variable.semantic_type.metadata.get("aliased"): blockers.append("ordinary module array requires addressable Aliased target storage") @@ -1248,6 +1311,9 @@ def completed_function_wrapper_policy(function: models.SemanticFunction) -> Func f"Semantic function {function.name!r} is missing completed wrapper policy; " "run complete_semantic_policies before wrapper planning" ) + if not policy.supported: + details = "; ".join(policy.blockers) or "unsupported wrapper policy" + raise ValueError(f"Semantic function {policy.owner_path!r} has unsupported wrapper policy: {details}") if policy.entrypoint_action is None: raise ValueError(f"Semantic function {policy.owner_path!r} is missing completed native entrypoint action") if policy.entrypoint_action is NativeEntrypointAction.DIRECT_C_ABI and not policy.entrypoint_symbol: @@ -1265,9 +1331,6 @@ def completed_function_wrapper_policy(function: models.SemanticFunction) -> Func raise ValueError( f"Semantic function {policy.owner_path!r} has incomplete native entrypoint slots {incomplete_slots}" ) - if not policy.supported: - details = "; ".join(policy.blockers) or "unsupported wrapper policy" - raise ValueError(f"Semantic function {policy.owner_path!r} has unsupported wrapper policy: {details}") return policy @@ -1617,6 +1680,13 @@ def build_function_wrapper_policy( # Complete result representation and declaration call targets, then bind # every array-extent producer to its immutable role. results, result_blockers = _result_policies(context) + if function.origin.source_language == "c": + arguments, results, native_call_slots = _normalize_c_direct_scalar_identities( + function, + arguments, + results, + native_call_slots, + ) declaration_callables = _function_declaration_callable_policies(function, owner_path) arguments, results, native_call_slots = _complete_function_array_extent_policies( function, @@ -1631,6 +1701,8 @@ def build_function_wrapper_policy( arguments, native_call_slots, ) + if function.origin.source_language == "c": + arguments, native_call_slots = _complete_c_direct_array_layouts(arguments, native_call_slots) # Record ordered writeback, cleanup, and ownership-transfer lifecycle work. writeback_actions, lifecycle_blockers = _lifecycle_policies(arguments) cleanup_actions, release_actions = _derived_result_lifecycle_policies(results) @@ -1664,6 +1736,17 @@ def build_function_wrapper_policy( slots=native_call_slots, ) ) + # Only a C-source operation carries an exact C declaration plan. A Fortran + # ``bind(C)`` procedure keeps its established backend-projected prototype, + # which is the only route that can lower strings, derived objects, and + # callbacks through the shared direct entrypoint. + direct_c_abi = ( + _completed_direct_c_abi_policy(function, arguments, results, native_call_slots) + if entrypoint_action is NativeEntrypointAction.DIRECT_C_ABI and function.origin.source_language == "c" + else None + ) + if function.origin.source_language == "c": + blockers = (*blockers, *entrypoint_diagnostics) return FunctionWrapperPolicy( owner_path=owner_path, python_exports=completed_python_exports(function, function.name), @@ -1698,6 +1781,7 @@ def build_function_wrapper_policy( entrypoint_action=entrypoint_action, entrypoint_symbol=entrypoint_symbol, entrypoint_diagnostics=entrypoint_diagnostics, + direct_c_abi=direct_c_abi, ) @@ -1712,7 +1796,7 @@ def _complete_function_entrypoint_route( ) -> tuple[ list[ArgumentPolicy], tuple[NativeCallSlotPolicy, ...], - NativeEntrypointAction, + NativeEntrypointAction | None, str, tuple[str, ...], ]: @@ -1725,12 +1809,19 @@ def _complete_function_entrypoint_route( results=results, slots=slots, ) + is_c_operation = function.origin.source_language == "c" entrypoint_action = ( NativeEntrypointAction.DIRECT_C_ABI if not entrypoint_diagnostics + else None + if is_c_operation else NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER ) - arguments = [_complete_entrypoint_argument_route(argument, entrypoint_action) for argument in arguments] + # C policy errors deliberately have no adapter action. The preliminary + # route below exists only to finish the policy record; planning rejects the + # completed unsupported record before it can emit an artifact. + completed_action = entrypoint_action or NativeEntrypointAction.DIRECT_C_ABI + arguments = [_complete_entrypoint_argument_route(argument, completed_action) for argument in arguments] optionality_by_position = {argument.native_position: argument.entrypoint_optionality for argument in arguments} slots = tuple( replace(slot, entrypoint_optionality=optionality_by_position[slot.native_position]) @@ -1746,6 +1837,109 @@ def _complete_function_entrypoint_route( return arguments, slots, entrypoint_action, entrypoint_symbol, entrypoint_diagnostics +def _normalize_c_direct_scalar_identities( + function: models.SemanticFunction, + arguments: list[ArgumentPolicy], + results: tuple[ResultPolicy, ...], + slots: tuple[NativeCallSlotPolicy, ...], +) -> tuple[list[ArgumentPolicy], tuple[ResultPolicy, ...], tuple[NativeCallSlotPolicy, ...]]: + """Copy target-resolved C scalar identities into completed lowering policy. + + C's public ``Int`` spelling intentionally survives semantic conversion, + while the measured storage identity (for example ``Int32``) is what the + shared NumPy and binding path consumes. This is policy normalization, not + backend inference; source spelling remains in ``c_abi`` provenance. + """ + # Argument identity is keyed by name: a route-neutral projection may + # reorder native slots, so a positional map would resolve one argument's + # Python conversion against another argument's declared type. + by_name = {argument.name: _c_direct_scalar_name(argument.semantic_type) for argument in function.arguments} + semantic_by_name = {argument.name: argument for argument in function.arguments} + slots_by_name = {slot.python_name: slot for slot in slots if slot.python_name is not None} + normalized_arguments = [ + replace( + argument, + semantic_type_name=by_name.get(argument.name) or argument.semantic_type_name, + native_storage_c_type=( + _c_direct_argument_storage_type( + function, + argument.native_position, + semantic_argument=semantic_by_name.get(argument.name), + ) + or ( + slots_by_name[argument.name].native_scalar_c_type + if argument.name in slots_by_name and slots_by_name[argument.name].value_kind == "addr" + else None + ) + ), + native_array_element_c_type=( + slots_by_name[argument.name].native_scalar_c_type + if argument.ownership.kind is ObjectKind.NUMPY_ARRAY and argument.name in slots_by_name + else None + ), + # A C payload is bytes plus whatever length the contract passes. + # Refusing an embedded NUL would impose a terminator convention + # that belongs to the C author, not to PRIK. + character_allows_embedded_nul=argument.semantic_type_name == "String", + ) + for argument in arguments + ] + return_name = _c_direct_scalar_name(function.return_type) + normalized_results = tuple( + replace( + result, + semantic_type_name=return_name, + direct_result_abi=DirectResultABI.NATIVE_SCALAR, + bridge_data_action=BridgeDataAction.DIRECT_TRANSFER, + bridge_copy_reason=None, + ) + if result.source_kind == "direct_return" and return_name is not None + else result + for result in results + ) + # Only a slot that transports one visible argument inherits that argument's + # identity. A binding-owned extent, length, presence, or literal slot owns + # its own completed type and must keep it. + normalized_slots = tuple( + replace(slot, semantic_type_name=by_name.get(slot.python_name) or slot.semantic_type_name) + if slot.python_name is not None + else slot + for slot in slots + ) + return normalized_arguments, normalized_results, normalized_slots + + +def _complete_c_direct_array_layouts( + arguments: list[ArgumentPolicy], + slots: tuple[NativeCallSlotPolicy, ...], +) -> tuple[list[ArgumentPolicy], tuple[NativeCallSlotPolicy, ...]]: + """Select C-contiguous NumPy buffer validation for direct C arrays. + + A semantic array contract is an author-selected view of a one-level C + pointer. The C entrypoint receives only its first element, so policy owns + rank, concrete-shape, writable, and C-layout requirements before planning. + """ + completed = [] + arrays_by_position: dict[int, ArrayHandoffPolicy] = {} + for argument in arguments: + if argument.rank <= 0 or argument.array is None: + completed.append(argument) + continue + array = replace(argument.array, order="ORDER_C", native_order="ORDER_C", contiguous=True) + actual = argument.native_array_actual + if actual is not None: + actual = replace(actual, order="C", require_contiguous=True) + completed.append(replace(argument, array=array, native_array_actual=actual)) + arrays_by_position[argument.native_position] = array + completed_slots = tuple( + replace(slot, array=arrays_by_position[slot.native_position]) + if slot.native_position in arrays_by_position + else slot + for slot in slots + ) + return completed, completed_slots + + def _complete_entrypoint_argument_route( argument: ArgumentPolicy, action: NativeEntrypointAction, @@ -1755,7 +1949,20 @@ def _complete_entrypoint_argument_route( return replace( argument, entrypoint_pass_character_length=( - uses_adapter and argument.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER + uses_adapter + and ( + argument.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER + # Rank-zero NumPy string storage always reports the caller's + # itemsize beside the address, declared width or not, so the + # adapter has one shape to receive. A raw string address is the + # exception: the caller hands over a bare integer with no Python + # object to measure, so its width can only be the declared one. + or ( + argument.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS + and argument.semantic_type_name == "String" + and argument.native_barrier_action is NativeBarrierAction.PASS_STORAGE_ADDRESS + ) + ) ), entrypoint_pass_array_metadata=(uses_adapter and argument.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER), entrypoint_pass_descriptor_presence=(uses_adapter and argument.optional_mode is OptionalMode.DESCRIPTOR), @@ -1845,7 +2052,14 @@ def _argument_entrypoint_passing( """Complete one C parameter transport from already completed boundary facts.""" if callback is not None: return EntrypointPassingConvention.RUNTIME_HANDLE - direct_c_abi = function.origin.source_language == "fortran" and function.origin.native_abi == "c" + if function.origin.source_language == "c" and _c_source_pointer_depth_for_argument(function, argument) == 1: + # Source C's conservative ``T *`` default is one scalar local whose + # address crosses the direct entrypoint. Array promotion is an edited + # semantic contract and arrives below through ARRAY_BUFFER instead. + return EntrypointPassingConvention.POINTER_REFERENCE + direct_c_abi = (function.origin.source_language == "fortran" and function.origin.native_abi == "c") or ( + function.origin.source_language == "c" + ) if boundary.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: return EntrypointPassingConvention.C_DESCRIPTOR_POINTER if argument.optional: @@ -1879,7 +2093,9 @@ def _argument_entrypoint_optionality( return EntrypointOptionalityAction.NULL_C_DESCRIPTOR_POINTER if _argument_passes_by_value(argument, slot): return EntrypointOptionalityAction.ADAPTER_SIDE_FORTRAN_OMISSION - if function.origin.source_language == "fortran" and function.origin.native_abi == "c": + if (function.origin.source_language == "fortran" and function.origin.native_abi == "c") or ( + function.origin.source_language == "c" + ): return EntrypointOptionalityAction.NULL_POINTER return EntrypointOptionalityAction.ADAPTER_SIDE_FORTRAN_OMISSION @@ -1986,7 +2202,9 @@ def _direct_c_abi_ineligibility( results: tuple[ResultPolicy, ...], slots: tuple[NativeCallSlotPolicy, ...], ) -> tuple[str, ...]: - """Return central reasons an operation must keep its generated Fortran adapter.""" + """Return completed direct-route blockers without choosing an adapter.""" + if function.origin.source_language == "c": + return _direct_c_operation_ineligibility(function, arguments=arguments, results=results, slots=slots) if function.origin.source_language != "fortran" or function.origin.native_abi != "c": return ("original procedure has no Fortran C ABI fact",) @@ -2006,6 +2224,442 @@ def _direct_c_abi_ineligibility( return tuple(dict.fromkeys(reasons)) +def _direct_c_array_ineligibility(argument: ArgumentPolicy) -> tuple[str, ...]: + """Validate the selected one-level C-pointer NumPy-array mechanism.""" + reasons = [] + if argument.rank < 1 or argument.rank > 15: + reasons.append(f"C_DIRECT_ARRAY_RANK:{argument.name}") + if argument.handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER or argument.array is None: + reasons.append(f"C_DIRECT_ARRAY_CONTRACT:{argument.name}") + if argument.entrypoint_passing is not EntrypointPassingConvention.POINTER_REFERENCE: + reasons.append(f"C_DIRECT_ARRAY_PASSING:{argument.name}") + if argument.native_array_handle is not None or argument.derived is not None or argument.callback is not None: + reasons.append(f"C_DIRECT_ARRAY_CONTRACT:{argument.name}") + if argument.transformations: + reasons.append(f"C_DIRECT_ARRAY_TRANSFORMATION:{argument.name}") + if argument.entrypoint_optionality is not EntrypointOptionalityAction.REQUIRED: + reasons.append(f"C_DIRECT_NULLABLE_POINTER:{argument.name}") + if argument.array is not None and argument.array.order != "ORDER_C": + reasons.append(f"C_DIRECT_ARRAY_ORDER:{argument.name}") + return tuple(dict.fromkeys(reasons)) + + +def _direct_c_operation_ineligibility( + function: models.SemanticFunction, + *, + arguments: tuple[ArgumentPolicy, ...], + results: tuple[ResultPolicy, ...], + slots: tuple[NativeCallSlotPolicy, ...], +) -> tuple[str, ...]: + """Return fail-closed blockers for the initial direct-only C lane.""" + semantic_arguments = {argument.name: argument for argument in function.arguments} + reasons: list[str] = [ + *_direct_c_callable_ineligibility(function), + *( + reason + for argument in arguments + for reason in _direct_c_argument_source_ineligibility( + function, + argument, + semantic_arguments[argument.name], + ) + ), + ] + if function.return_type is not None and function.return_type.metadata.get("c_type_fact_source") == "fallback": + reasons.append("C_DIRECT_UNPROBED_PRIMITIVE_ABI:return") + for argument in arguments: + if argument.native_array_element_c_type == "_Bool": + reasons.append(f"C_DIRECT_BOOL_ARRAY:{argument.name}") + if _is_c_string_argument(argument): + reasons.extend(_direct_c_string_ineligibility(argument)) + elif argument.rank > 0: + reasons.extend(_direct_c_array_ineligibility(argument)) + else: + reasons.extend(_direct_argument_ineligibility(argument)) + for result in results: + if result.semantic_type_name == "String": + # Only argument character contracts are adopted. A projected string + # result would need the owned-allocation protocol the Fortran + # adapter provides, and C has no adapter to allocate it. + reasons.append(f"C_DIRECT_UNSUPPORTED_STRING_RESULT:{result.owner_path.rsplit('.', 1)[-1]}") + reasons.extend(_direct_result_ineligibility(result)) + for slot in slots: + reasons.extend( + _direct_slot_ineligibility( + slot, + # Only a slot that transports one visible argument carries an + # adopted C character contract; a hidden output does not. + character_representation_is_binding_owned=slot.python_name is not None, + ) + ) + return tuple(dict.fromkeys(reasons)) + + +def _is_c_string_argument(argument: ArgumentPolicy) -> bool: + """Return whether one completed C argument carries a character contract.""" + return argument.semantic_type_name == "String" + + +def _direct_c_string_ineligibility(argument: ArgumentPolicy) -> tuple[str, ...]: + """Validate the adopted rank-zero C character forms. + + A C ``char *`` is a pointer to bytes; the terminator convention belongs to + the C author. ``String`` hands over Python's own NUL-terminated buffer for + a read-only input, and rank-zero string storage hands over the caller's + NumPy bytes untouched. Anything else stays fail-closed. + """ + reasons = [] + if argument.rank != 0: + reasons.append(f"C_DIRECT_UNSUPPORTED_STRING_CONTRACT:{argument.name}") + if argument.handoff_mode not in {ArgumentHandoffMode.CHARACTER_BUFFER, ArgumentHandoffMode.OPAQUE_ADDRESS}: + reasons.append(f"C_DIRECT_UNSUPPORTED_STRING_CONTRACT:{argument.name}") + if argument.entrypoint_passing is not EntrypointPassingConvention.POINTER_REFERENCE: + reasons.append(f"C_DIRECT_UNSUPPORTED_STRING_CONTRACT:{argument.name}") + if argument.entrypoint_optionality is not EntrypointOptionalityAction.REQUIRED: + reasons.append(f"C_DIRECT_NULLABLE_POINTER:{argument.name}") + if argument.transformations or argument.derived is not None or argument.callback is not None: + reasons.append(f"C_DIRECT_UNSUPPORTED_STRING_CONTRACT:{argument.name}") + if argument.writable and argument.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: + # A borrowed Python payload is immutable and may be interned, so only + # caller-owned NumPy storage may be written through. + reasons.append(f"C_DIRECT_IMMUTABLE_STRING_WRITEBACK:{argument.name}") + return tuple(dict.fromkeys(reasons)) + + +def _direct_c_callable_ineligibility(function: models.SemanticFunction) -> tuple[str, ...]: + """Return the direct-C blockers owned by one operation's own declaration.""" + raw_abi = function.metadata.get("c_abi") + source_abi = raw_abi if isinstance(raw_abi, dict) else {} + result_facts = source_abi.get("result") if isinstance(source_abi.get("result"), dict) else {} + reasons = [] + if isinstance(raw_abi, dict): + if source_abi.get("calling_convention") != "c": + reasons.append("C_DIRECT_UNSUPPORTED_CALLING_CONVENTION") + if source_abi.get("variadic"): + reasons.append("C_DIRECT_VARIADIC_FUNCTION") + if "static" in function.metadata.get("storage", ()): + reasons.append("C_DIRECT_TRANSLATION_UNIT_LOCAL_SYMBOL") + if function.origin.native_abi not in {None, "c"}: + reasons.append("C_DIRECT_UNSUPPORTED_CALLING_CONVENTION") + if function.return_type is not None: + if _c_direct_scalar_name(function.return_type) is None: + reasons.append("C_DIRECT_UNRESOLVED_PRIMITIVE_ABI:return") + if _c_source_pointer_depth(function, result=True) > 0: + reasons.append("C_DIRECT_POINTER_RESULT") + if {"volatile", "_Atomic"} & set(result_facts.get("qualifiers", ())): + reasons.append("C_DIRECT_UNSUPPORTED_QUALIFIER:return") + return tuple(reasons) + + +def _direct_c_argument_source_ineligibility( + function: models.SemanticFunction, + argument: ArgumentPolicy, + semantic_argument: models.SemanticArgument, +) -> tuple[str, ...]: + """Return the direct-C blockers one argument's preserved source facts prove.""" + semantic_type = semantic_argument.semantic_type + source_type = _c_source_type_facts(function, argument.native_position) + pointer_depth = int(source_type.get("pointer_depth", _c_pointer_depth(semantic_type))) + storage = semantic_type.storage + reasons = [] + if semantic_type.name == "CFunctionPointer" or source_type.get("has_function_pointer"): + reasons.append(f"C_DIRECT_CALLBACK:{argument.name}") + if source_type.get("has_array_declarator"): + reasons.append(f"C_DIRECT_ARRAY_DECLARATOR:{argument.name}") + if {"volatile", "_Atomic"} & set(source_type.get("qualifiers", ())): + reasons.append(f"C_DIRECT_UNSUPPORTED_QUALIFIER:{argument.name}") + if pointer_depth > 1: + reasons.append(f"C_DIRECT_POINTER_DEPTH:{argument.name}") + if _argument_declares_nullable_c_pointer(argument, semantic_type): + reasons.append(f"C_DIRECT_NULLABLE_POINTER:{argument.name}") + if storage is not None and storage.metadata.get("address_role") == "raw": + reasons.append(f"C_DIRECT_RAW_ADDRESS:{argument.name}") + if _c_direct_scalar_name(semantic_type) is None and not _is_c_string_argument(argument): + reasons.append(f"C_DIRECT_UNRESOLVED_PRIMITIVE_ABI:{argument.name}") + if argument.rank > 0 and semantic_type.name in {"Bool", "Bool8"}: + reasons.append(f"C_DIRECT_BOOL_ARRAY:{argument.name}") + if pointer_depth and source_type.get("const") and _argument_requests_native_write(argument): + reasons.append(f"C_DIRECT_CONST_POINTER_OUTPUT:{argument.name}") + if semantic_type.metadata.get("c_type_fact_source") == "fallback": + reasons.append(f"C_DIRECT_UNPROBED_PRIMITIVE_ABI:{argument.name}") + return tuple(reasons) + + +def _argument_declares_nullable_c_pointer(argument: ArgumentPolicy, semantic_type: models.SemanticType) -> bool: + """Return whether one C argument was written as a nullable value. + + Subscripted storage loses its ``| None`` spelling during conversion, so the + recorded annotation fact stands in for it. + """ + return bool( + argument.nullable + or argument.optional + or " | None" in semantic_type.name + or semantic_type.metadata.get(NULLABLE_ANNOTATION_METADATA) + ) + + +def _argument_requests_native_write(argument: ArgumentPolicy) -> bool: + """Return whether a completed contract expects native writes to be visible.""" + return bool(argument.writable or argument.projects_result or argument.array_copy_out) + + +def _c_direct_scalar_name(semantic_type: models.SemanticType | None) -> str | None: + """Return the resolved lowering identity without replacing public C spelling.""" + if semantic_type is None: + return None + candidate = semantic_type.dtype or semantic_type.name + return str(candidate) if candidate in _PLAN_PRIMITIVE_SCALAR_TYPES else None + + +def _c_source_type_facts(function: models.SemanticFunction, native_position: int) -> dict[str, object]: + raw_abi = function.metadata.get("c_abi") + if not isinstance(raw_abi, dict): + return {} + parameters = raw_abi.get("parameters") + if not isinstance(parameters, list) or not 0 <= native_position < len(parameters): + return {} + value = parameters[native_position] + return dict(value) if isinstance(value, dict) else {} + + +def _c_source_pointer_depth(function: models.SemanticFunction, *, result: bool) -> int: + raw_abi = function.metadata.get("c_abi") + if not isinstance(raw_abi, dict): + return _c_pointer_depth(function.return_type) if result else 0 + value = raw_abi.get("result") if result else None + return int(value.get("pointer_depth", 0)) if isinstance(value, dict) else 0 + + +def _c_source_pointer_depth_for_argument( + function: models.SemanticFunction, + argument: models.SemanticArgument, +) -> int: + """Return one source C parameter's preserved pointer depth.""" + position = argument.metadata.get("native_position") + if not isinstance(position, int): + return 0 + return int(_c_source_type_facts(function, position).get("pointer_depth", 0)) + + +def _c_direct_argument_storage_type( + function: models.SemanticFunction, + native_position: int, + *, + semantic_argument: models.SemanticArgument | None = None, +) -> str | None: + """Return the policy-selected local C scalar type for a source C argument. + + The source declaration remains the direct entrypoint prototype, while this + spelling owns scalar storage passed through a pointer. Matching it avoids + aliasing a normalized ``int64_t`` local as a distinct source type such as + ``long long``. A type written through a typedef resolves to its underlying + builtin spelling, because the binding cannot declare a name that only the + user's headers define. ``const`` and ``restrict`` qualify the native view, + not the temporary that receives Python input before the call. + """ + resolved = _c_typedef_resolved_spelling( + semantic_argument.semantic_type if semantic_argument is not None else None, + pointer_depth=0, + const=False, + ) + if resolved is not None: + return resolved + source_type = _c_source_type_facts(function, native_position) + spelling = source_type.get("source_spelling") + if not isinstance(spelling, str): + return None + base = spelling.split("*", maxsplit=1)[0] + words = [word for word in base.split() if word not in _C_POINTEE_QUALIFIER_WORDS] + return " ".join(words) or None + + +def _c_pointer_depth(semantic_type: models.SemanticType | None) -> int: + return int(semantic_type.storage.pointer_depth) if semantic_type is not None and semantic_type.storage else 0 + + +def _completed_direct_c_abi_policy( + function: models.SemanticFunction, + arguments: list[ArgumentPolicy], + results: tuple[ResultPolicy, ...], + slots: tuple[NativeCallSlotPolicy, ...], +) -> DirectCABIPolicy: + """Copy the selected C declaration facts into immutable policy output.""" + raw_abi = function.metadata.get("c_abi") + source_abi = raw_abi if isinstance(raw_abi, dict) else {} + parameter_source = source_abi.get("parameters") if isinstance(source_abi.get("parameters"), list) else [] + semantic_arguments_by_name = {argument.name: argument for argument in function.arguments} + argument_policies_by_name = {argument.name: argument for argument in arguments} + + def slot_semantic_type(slot: NativeCallSlotPolicy) -> models.SemanticType | None: + """Return the declared type of the argument one slot transports. + + A binding-owned extent, length, presence, or literal slot names no + argument and keeps its own completed identity, so it returns ``None`` + rather than borrowing the type of the argument it was derived from. + """ + if slot.python_name is None: + return None + semantic_argument = semantic_arguments_by_name.get(slot.python_name) + return semantic_argument.semantic_type if semantic_argument is not None else None + + parameters = tuple( + _direct_c_abi_type_policy( + parameter_source[slot.native_position] + if slot.native_position < len(parameter_source) and isinstance(parameter_source[slot.native_position], dict) + else None, + semantic_type=slot_semantic_type(slot), + semantic_type_name=slot.semantic_type_name, + pointer_depth=(0 if slot.entrypoint_passing is EntrypointPassingConvention.C_VALUE else 1), + # A hidden output slot is storage the callee writes into. + writes_output=slot.source_kind == "result", + native_scalar_c_type=slot.native_scalar_c_type, + converts_to_contract_storage=( + slot.native_scalar_c_type is not None + and ( + slot.python_name not in argument_policies_by_name + or argument_policies_by_name[slot.python_name].native_array_element_c_type is None + ) + ), + ) + for slot in sorted(slots, key=lambda item: item.native_position) + ) + direct_result = next((result for result in results if result.source_kind == "direct_return"), None) + result_source = source_abi.get("result") if isinstance(source_abi.get("result"), dict) else None + result = ( + _direct_c_abi_type_policy( + result_source, + semantic_type=function.return_type, + semantic_type_name=None, + pointer_depth=0, + native_scalar_c_type=_native_scalar_c_type(function.return_type), + ) + if direct_result is not None and function.return_type is not None + else None + ) + return DirectCABIPolicy( + calling_convention=str(source_abi.get("calling_convention", "c")), + result_transport="value" if result is not None else "void", + result=result, + parameters=parameters, + ) + + +def _direct_c_abi_type_policy( + source: dict[str, object] | None, + *, + semantic_type: models.SemanticType | None, + semantic_type_name: str | None, + pointer_depth: int, + writes_output: bool = False, + native_scalar_c_type: str | None = None, + converts_to_contract_storage: bool | None = None, +) -> DirectCABITypePolicy: + """Normalize preserved source facts or the canonical source-free C form.""" + if semantic_type_name == "String": + return _direct_c_character_abi_type_policy(source, semantic_type=semantic_type, writes_output=writes_output) + scalar_name = _c_direct_scalar_name(semantic_type) or semantic_type_name + if scalar_name is None: + raise ValueError("C direct ABI policy requires a resolved primitive scalar") + if scalar_name not in _PLAN_PRIMITIVE_SCALAR_TYPES: + raise ValueError(f"C direct ABI policy requires a supported scalar, not {scalar_name!r}") + source = source or {} + source_pointer_depth = int(source.get("pointer_depth", pointer_depth)) + contract_spelling = semantic_type.metadata.get("c_abi_spelling") if semantic_type is not None else None + # A source-free contract preserves no declaration text, so policy records + # only the resolved identity and leaves the backend spelling to the C + # binding generator that owns scalar projection. + native_spelling = None + if native_scalar_c_type is not None: + native_spelling = ( + f"{native_scalar_c_type} {'*' * source_pointer_depth}" if source_pointer_depth else native_scalar_c_type + ) + preserved = ( + source.get("source_spelling") or native_spelling or (contract_spelling if not source_pointer_depth else None) + ) + qualifiers = tuple(str(item) for item in source.get("qualifiers", ())) + const = bool(source.get("const", False)) + declarable = _c_typedef_resolved_spelling(semantic_type, pointer_depth=source_pointer_depth, const=const) + return DirectCABITypePolicy( + source_spelling=declarable or (str(preserved) if preserved else None), + scalar_type_name=scalar_name, + pointer_depth=source_pointer_depth, + qualifiers=qualifiers, + const=const, + converts_to_contract_storage=( + native_scalar_c_type is not None if converts_to_contract_storage is None else converts_to_contract_storage + ), + ) + + +def _direct_c_character_abi_type_policy( + source: dict[str, object] | None, + *, + semantic_type: models.SemanticType | None, + writes_output: bool = False, +) -> DirectCABITypePolicy: + """Return the exact C declaration for one rank-zero character contract. + + A borrowed Python payload is read-only, so it is declared ``const char *``. + Caller-owned NumPy storage may be written by the callee and is declared + ``char *``. The contract states which one it is; PRIK never infers it from + a C declaration it cannot see. + """ + source = source or {} + mutable = writes_output or bool( + semantic_type is not None and semantic_type.storage is not None and semantic_type.storage.mutable + ) + preserved = source.get("source_spelling") + spelling = str(preserved) if isinstance(preserved, str) and preserved else ("char *" if mutable else "const char *") + return DirectCABITypePolicy( + source_spelling=spelling, + scalar_type_name="String", + pointer_depth=int(source.get("pointer_depth", 1)), + qualifiers=tuple(str(item) for item in source.get("qualifiers", ())), + const=bool(source.get("const", not mutable)), + ) + + +def _native_scalar_c_type(semantic_type: models.SemanticType | None) -> str | None: + """Resolve one semantic native-call cast marker to its exact C spelling.""" + marker = semantic_type.metadata.get(NATIVE_C_SCALAR_CAST_METADATA) if semantic_type is not None else None + return NATIVE_C_SCALAR_CASTS.get(marker) if isinstance(marker, str) else None + + +def _c_typedef_resolved_spelling( + semantic_type: models.SemanticType | None, + *, + pointer_depth: int, + const: bool, +) -> str | None: + """Return the underlying builtin spelling of a type written through a typedef. + + The generated binding declares the entrypoint prototype itself, so a + typedef name that only the user's own headers define cannot appear there. + A typedef is exactly its underlying type, so substituting the probed + builtin spelling preserves width, signedness, and representation instead of + choosing a nearby one; the typedef chain stays recorded on the semantic + type as provenance. + """ + if semantic_type is None or not semantic_type.metadata.get("c_typedefs"): + return None + primitive = semantic_type.metadata.get("c_primitive") or _c_underlying_type_spelling(semantic_type) + if not isinstance(primitive, str) or not primitive: + return None + declared = f"const {primitive}" if const else primitive + return f"{declared} {'*' * pointer_depth}" if pointer_depth else declared + + +def _c_underlying_type_spelling(semantic_type: models.SemanticType) -> str | None: + """Return the probed builtin spelling recorded for a standard C typedef.""" + for key in ("c_standard_type_fact", "c_type_fact"): + fact = semantic_type.metadata.get(key) + underlying = fact.get("underlying_c_type") if isinstance(fact, dict) else None + if isinstance(underlying, str) and underlying: + return underlying + return None + + def _direct_operation_ineligibility( function: models.SemanticFunction, *, @@ -2112,16 +2766,27 @@ def _direct_result_ineligibility(result: ResultPolicy) -> tuple[str, ...]: return tuple(reasons) -def _direct_slot_ineligibility(slot: NativeCallSlotPolicy) -> tuple[str, ...]: - """Return direct-route blockers owned by one completed call projection.""" +def _direct_slot_ineligibility( + slot: NativeCallSlotPolicy, + *, + character_representation_is_binding_owned: bool = False, +) -> tuple[str, ...]: + """Return direct-route blockers owned by one completed call projection. + + A Fortran character actual needs adapter-side representation work beyond a + single element. A C character contract does not: the binding itself hands + over the caller's bytes, so its caller sets + ``character_representation_is_binding_owned``. + """ reasons = [] if slot.projection_action is EntrypointProjectionAction.BLOCKED: reasons.append(f"native-call slot {slot.native_position} has no binding projection action") if slot.entrypoint_passing is EntrypointPassingConvention.BLOCKED: reasons.append(f"native-call slot {slot.native_position} has no C passing convention") - if slot.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION and not ( - slot.semantic_type_name == "String" and slot.character_length == 1 - ): + character_slot = slot.semantic_type_name == "String" and ( + character_representation_is_binding_owned or slot.character_length == 1 + ) + if slot.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION and not character_slot: reasons.append(f"native-call slot {slot.native_position} requires adapter representation work") return tuple(reasons) @@ -2266,8 +2931,21 @@ def _native_call_slot_for_python_position( slots: tuple[NativeCallSlotPolicy, ...], python_position: int, ) -> NativeCallSlotPolicy | None: - """Find the completed native-call slot owned by one visible argument.""" - return next((slot for slot in slots if slot.python_position == python_position), None) + """Find the completed native-call slot owned by one visible argument. + + A binding-owned producer such as ``Arg(i).shape[0]`` or ``Len(Arg(i))`` + records the Python position of the argument it measures, so it is skipped + here. Matching the first position-equal slot would otherwise hand an array + its own extent slot and lower the buffer as a by-value scalar. + """ + return next( + ( + slot + for slot in slots + if slot.python_position == python_position and slot.source_kind not in _DERIVED_NATIVE_CALL_SLOT_KINDS + ), + None, + ) def _argument_policy( @@ -2393,6 +3071,7 @@ def _argument_policy( python_visible=decision.python_visible, result_position=boundary.result_position, character_length=_character_length(argument.semantic_type), + character_local=_character_local_policy(argument.semantic_type, decision), array=array_policy, native_array_actual=_native_array_actual_policy(argument, decision, array_policy), native_array_handle=_native_array_handle_wrapper_policy( @@ -2646,9 +3325,19 @@ def _direct_result_policy(context: _FunctionPolicyContext) -> _ResultPolicyCandi function.metadata.get(models.RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA), result_path, ) - scalar_descriptor = _scalar_descriptor_result_policy(return_type, decision) + scalar_descriptor = _scalar_descriptor_result_policy( + return_type, + decision, + may_be_unallocated=_scalar_descriptor_kind(return_type) == "allocatable", + ) blockers = list(_result_blockers(return_type, decision)) - if scalar_descriptor is not None and scalar_descriptor.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE: + if ( + scalar_descriptor is not None + and scalar_descriptor.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE + and decision.kind is not ObjectKind.STRING + ): + # A character result is moved out through an allocatable dummy, which + # makes allocation testable; other scalars have no such completed move. blockers.append( "direct allocatable scalar function results cannot preserve unallocated state; " "use an allocatable hidden output projection" @@ -2748,6 +3437,11 @@ def _hidden_result_policies(context: _FunctionPolicyContext) -> tuple[_ResultPol return tuple(policies) +def _update_result_ownership(argument: models.SemanticArgument) -> OwnershipDecision | None: + """Return the completed result facet of one caller-supplied string update, if any.""" + return _ownership_decision(argument, models.RESOLVED_UPDATE_RESULT_OWNERSHIP_POLICY_METADATA) + + def _hidden_result_projection_index( function: models.SemanticFunction, ) -> dict[str, models.ProjectionMapping]: @@ -2774,7 +3468,7 @@ def _hidden_result_ownership( argument: models.SemanticArgument, suppressed_outputs: frozenset[str], ) -> OwnershipDecision | None: - """Return ownership only when an argument is an exposed hidden result. + """Return ownership only when an argument is an exposed native output. The helper receives one possible native output dummy and the owner paths reserved by runtime status handling. Source parsing may originally have @@ -2784,8 +3478,18 @@ def _hidden_result_ownership( reserved path. For example, hidden ``value`` returns its decision, while hidden ``status`` returns ``None`` when ``module.proc.status`` appears in ``suppressed_outputs``. + + A caller-supplied string descriptor update is the one shape whose + result facet is a second completed decision rather than the argument's own, + so a Python-visible argument reaches this stage through + ``RESOLVED_UPDATE_RESULT_OWNERSHIP_POLICY_METADATA``. That facet is an + ordinary native output: it carries ``python_visible=False`` and is completed, + validated, and lowered exactly like an ``intent(out)`` descriptor result. """ - decision = _ownership_decision(argument, models.RESOLVED_OWNERSHIP_POLICY_METADATA) + decision = _update_result_ownership(argument) or _ownership_decision( + argument, + models.RESOLVED_OWNERSHIP_POLICY_METADATA, + ) if decision is None or not (decision.projects_result and not decision.python_visible): return None if f"{context.owner_path}.{argument.name}" in suppressed_outputs: @@ -2896,6 +3600,7 @@ def _hidden_result_candidate( character_length=_character_length(argument.semantic_type), array=_array_handoff_policy(argument.semantic_type), source_kind="hidden_output", + python_returned=not argument.metadata.get(models.HIDDEN_NATIVE_OUTPUT_METADATA), native_name=mapping.native_name or argument.name, native_position=mapping.native_position, result_position=int(mapping.result_position), @@ -2907,6 +3612,7 @@ def _hidden_result_candidate( if native_array_handle is not None or scalar_descriptor is not None else EntrypointPassingConvention.OUTPUT_STORAGE ), + updates_argument=_update_result_ownership(argument) is not None, ), tuple(blockers), ) @@ -3134,6 +3840,7 @@ def _projected_argument_slot( python_name=mapping.python_name or argument.name, native_name=mapping.native_name or argument.name, value_kind=value_kind, + native_scalar_c_type=NATIVE_C_SCALAR_CASTS.get(mapping.native_cast), native_barrier_action=native_barrier_action, codegen_action=codegen_action, bridge_data_action=bridge_data_action, @@ -3221,6 +3928,7 @@ def _hidden_result_native_call_slot_policy( python_name=mapping.python_name, native_name=mapping.native_name or f"result_{native_position}", value_kind=mapping.value_kind, + native_scalar_c_type=NATIVE_C_SCALAR_CASTS.get(mapping.native_cast), native_barrier_action=NativeBarrierAction.BLOCKED, codegen_action=CodegenAction.BLOCKED, bridge_data_action=BridgeDataAction.BLOCKED, @@ -3241,6 +3949,7 @@ def _hidden_result_native_call_slot_policy( python_name=argument.name, native_name=mapping.native_name or argument.name, value_kind=mapping.value_kind, + native_scalar_c_type=NATIVE_C_SCALAR_CASTS.get(mapping.native_cast), native_barrier_action=NativeBarrierAction.BLOCKED, codegen_action=CodegenAction.BLOCKED, bridge_data_action=BridgeDataAction.BLOCKED, @@ -3287,6 +3996,7 @@ def _hidden_result_native_call_slot_policy( python_name=argument.name, native_name=mapping.native_name or argument.name, value_kind=mapping.value_kind, + native_scalar_c_type=NATIVE_C_SCALAR_CASTS.get(mapping.native_cast), native_barrier_action=decision.native_barrier_action, codegen_action=decision.codegen_action, bridge_data_action=bridge_data_action, @@ -3614,6 +4324,15 @@ def _derived_object_storage( return DerivedObjectStorage.DIRECT +# An abstract type has no instances of its own. Every origin that would declare +# storage of that exact type -- a wrapper-owned holder, or a module variable -- +# has nothing to hold, so only a plain concrete object address stays reachable. +# The adapter converts that address to the extension's own type and passes it to +# the `class(...)` dummy through the polymorphic discriminator. +_ABSTRACT_REACHABLE_STORAGES = frozenset({DerivedObjectStorage.DIRECT}) +_ABSTRACT_INCOMPATIBLE_STORAGES = frozenset(DerivedObjectStorage) - _ABSTRACT_REACHABLE_STORAGES + + def _derived_call_policy( argument: models.SemanticArgument, decision: OwnershipDecision, @@ -3627,8 +4346,19 @@ def _derived_call_policy( argument.semantic_type, native_value=_native_by_value_argument(argument), ) + abstract_dummy = bool(argument.semantic_type.metadata.get("fortran_abstract_type")) cases = tuple( _derived_call_case(category, storage, projects_result=decision.projects_result) + if not (abstract_dummy and storage in _ABSTRACT_INCOMPATIBLE_STORAGES) + else _derived_incompatible_case( + storage, + "abstract-owner-storage", + ( + f"{argument.semantic_type.name} is an abstract type; a " + f"{storage.value.replace('_', ' ')} actual would declare storage of that exact " + "type, which has no instance. Pass a concrete extension instead." + ), + ) for storage in DerivedObjectStorage ) writeback = { @@ -3991,6 +4721,10 @@ def _scalar_or_string_argument_shape_blockers( string_value = _is_plan_string_value_type(argument.semantic_type) if not (_is_first_lane_scalar_type(argument.semantic_type) or string_value): blockers.append(f"argument {argument.name!r} is not a first-lane primitive scalar") + precision_blocker = _extended_precision_blocker(argument.semantic_type) + if precision_blocker is not None: + blockers.append(f"argument {argument.name!r}: {precision_blocker}") + blockers.extend(_character_descriptor_blockers(argument, decision)) if not decision.python_visible: blockers.append(f"argument {argument.name!r} is not Python-visible") expected_kind = ObjectKind.STRING if string_value else ObjectKind.SCALAR @@ -4362,7 +5096,11 @@ def _string_value_boundary_blockers( f"argument {argument.name!r} string action is {decision.codegen_action.value}, " "not a call-local input or copy-in/out replacement" ) - if decision.codegen_action is CodegenAction.CALL_LOCAL_INPUT and decision.projects_result: + if ( + decision.codegen_action is CodegenAction.CALL_LOCAL_INPUT + and decision.projects_result + and not is_character_descriptor_update(argument.semantic_type.metadata, decision) + ): blockers.append(f"argument {argument.name!r} call-local string input unexpectedly projects a result") if decision.codegen_action is CodegenAction.COPY_IN_OUT: blockers.extend(_string_replacement_blockers(argument, decision)) @@ -4420,7 +5158,11 @@ def _string_address_ownership_blockers( ) -> tuple[str, ...]: """Validate ownership shared by fixed storage and raw-address forms.""" blockers = [] - if _character_length(argument.semantic_type) is None: + if _character_length(argument.semantic_type) is None and expected_storage is not StorageMode.ALIAS: + # Rank-zero string storage may leave the capacity assumed: the caller's + # NumPy buffer carries its own itemsize, which the binding hands to the + # boundary beside the address. Other address forms still need a + # declared length. blockers.append(f"argument {argument.name!r} {label} requires a fixed positive character length") if decision.owner is not OwnershipOwner.CALLER: blockers.append(f"argument {argument.name!r} {label} owner is {decision.owner.value}, not caller") @@ -4496,7 +5238,14 @@ def _argument_projection_blockers( argument: models.SemanticArgument, decision: OwnershipDecision, ) -> tuple[str, ...]: - """Return projected-result action blockers for one argument.""" + """Return projected-result action blockers for one argument. + + A projected argument normally replaces or mutates caller-visible storage. + The deferred-length string update instead keeps a call-local input and + returns the reallocated value through its own completed result facet. + """ + if is_character_descriptor_update(argument.semantic_type.metadata, decision): + return () if decision.projects_result and decision.codegen_action not in { CodegenAction.COPY_IN_OUT, CodegenAction.IN_PLACE_ARGUMENT, @@ -4601,6 +5350,9 @@ def _scalar_result_blockers( blockers.append(f"result has blocked ownership policy: {decision.blocker or decision.reason}") if not _is_first_lane_scalar_type(semantic_type): blockers.append("result is not a first-lane primitive scalar") + precision_blocker = _extended_precision_blocker(semantic_type) + if precision_blocker is not None: + blockers.append(f"result: {precision_blocker}") if decision.kind is not ObjectKind.SCALAR: blockers.append(f"result policy kind is {decision.kind.value}, not scalar") if decision.codegen_action is not CodegenAction.DIRECT_VALUE: @@ -4903,7 +5655,7 @@ def _fixed_string_result_ownership_blockers( if (decision.boundary_storage_mode or decision.storage_mode) is not StorageMode.STACK: blockers.append(f"{label} boundary storage is not stack") if decision.nullable: - blockers.append(f"{label} is nullable outside deferred string results") + blockers.append(f"{label} is nullable outside descriptor string results") return tuple(blockers) @@ -4934,9 +5686,16 @@ def _result_position_blockers( results: tuple[ResultPolicy, ...], arguments: list[ArgumentPolicy] | tuple[ArgumentPolicy, ...] = (), ) -> tuple[str, ...]: - """Require native results and visible writebacks to cover one public order.""" + """Require native results and visible writebacks to cover one public order. + + A deferred-length string update contributes its position through the result + facet that carries the reallocated value, so counting the argument again + would report a duplicate for one public output. + """ positions = tuple(result.result_position for result in results) + tuple( - argument.result_position for argument in arguments if argument.projects_result + argument.result_position + for argument in arguments + if argument.projects_result and not argument.projects_character_descriptor_update ) if not positions: return () @@ -4959,6 +5718,11 @@ def _function_shape_blockers( blockers.append("function locals are outside the first scalar lane") if function.contracts: blockers.append("function contracts are outside the first scalar lane") + has_native_c_scalar_cast = any(mapping.native_cast is not None for mapping in function.projection) or bool( + function.return_type is not None and function.return_type.metadata.get(NATIVE_C_SCALAR_CAST_METADATA) + ) + if has_native_c_scalar_cast and function.origin.source_language != "c": + blockers.append("native C scalar casts require a C native contract") return tuple(blockers) @@ -4988,19 +5752,109 @@ def _runtime_status_plan_blockers(policy: NativeStatusErrorPolicy | None) -> tup blockers = [] if policy.status.semantic_type_name != "Int32": blockers.append("native status error projection requires an Int32 status in the current plan lane") - if policy.message is not None and policy.message.character_length is None: + if ( + policy.message is not None + and policy.message.character_length is None + and policy.message.python_position is None + ): + # Only a hidden message is allocated by the binding, so only a hidden + # message needs the contract to state the width. A visible argument + # brings its own storage. blockers.append("native status error message requires a fixed positive character length") return tuple(blockers) +def _has_deferred_character_length(semantic_type: models.SemanticType) -> bool: + """Return whether one character value declares a deferred length parameter.""" + return uses_deferred_character_length(semantic_type.metadata) + + +def _character_local_policy( + semantic_type: models.SemanticType, + decision: OwnershipDecision, +) -> CharacterLocalPolicy | None: + """Complete the adapter-local storage one caller-supplied character input needs. + + The binding always hands the adapter a byte buffer and a length, so the + only open decision is the Fortran local that buffer is materialized into. + A dummy with no descriptor attribute keeps a fixed-length local; an + ``allocatable`` or ``pointer`` dummy needs a local carrying the same + attribute, and a ``pointer`` local is adapter-allocated storage the adapter + must also release. + """ + if int(semantic_type.rank or 0) != 0 or semantic_type.name != "String": + return None + plain = CharacterLocalPolicy( + descriptor_kind=None, + deferred_length=False, + release=CharacterLocalRelease.NONE, + ) + if decision.codegen_action is CodegenAction.COPY_IN_OUT: + # A replacement writes back through the caller's own buffer, so its + # local is the fixed-length storage that buffer already sizes. + return plain + if decision.codegen_action is not CodegenAction.CALL_LOCAL_INPUT: + return None + descriptor = character_descriptor_kind(semantic_type.metadata) + if descriptor is None: + return None if _has_deferred_character_length(semantic_type) else plain + return CharacterLocalPolicy( + descriptor_kind=NativeArrayDescriptorKind(descriptor), + deferred_length=_has_deferred_character_length(semantic_type), + release=_character_local_release(descriptor, decision), + ) + + +def _character_local_release(descriptor: str, decision: OwnershipDecision) -> CharacterLocalRelease: + """Return who frees the adapter-local character storage after the call. + + An ``allocatable`` local is released by the compiler when the adapter + returns. A ``pointer`` local is storage the adapter allocated itself: a + read-only dummy cannot change its association, so the adapter always frees + it, while an update dummy may be reassociated or deallocated by the native + procedure and is freed only while it still identifies that allocation. + """ + if descriptor != "pointer": + return CharacterLocalRelease.NONE + if decision.projects_result: + return CharacterLocalRelease.DEALLOCATE_IF_RETAINED + return CharacterLocalRelease.DEALLOCATE + + +def _character_descriptor_blockers( + argument: models.SemanticArgument, + decision: OwnershipDecision, +) -> tuple[str, ...]: + """Restrict descriptor and deferred-length character arguments to completed lanes. + + A Python-visible ``allocatable`` or ``pointer`` character dummy is wrapped + as one call-local input, optionally paired with the projected result an + update returns. Any other action has no completed conversion, so it stops + here instead of reaching an adapter with nothing to build. A deferred + length additionally requires one of those attributes, because + ``character(len=:)`` is not a declarable local without it. + """ + semantic_type = argument.semantic_type + if int(semantic_type.rank or 0) != 0 or semantic_type.name != "String": + return () + descriptor = character_descriptor_kind(semantic_type.metadata) + deferred = _has_deferred_character_length(semantic_type) + if not (descriptor or deferred): + return () + label = f"argument {argument.name!r}" + if descriptor is None: + return (f"{label} is a deferred-length character argument without an allocatable or pointer attribute",) + if decision.codegen_action is not CodegenAction.CALL_LOCAL_INPUT: + return ( + f"{label} is an {descriptor} character argument with action {decision.codegen_action.value}; " + "only a call-local input, alone or with a projected update result, is wrapped", + ) + return () + + def _character_length(semantic_type: models.SemanticType) -> int | None: """Return a positive fixed Fortran character length, normalizing accepted metadata spellings.""" - value = semantic_type.metadata.get("fortran_character_length") - if isinstance(value, int) and not isinstance(value, bool) and value > 0: - return value - if isinstance(value, str) and value.strip().isdigit() and int(value.strip()) > 0: - return int(value.strip()) - return None + return declared_character_length(semantic_type.metadata) def _lifecycle_policies( @@ -5012,6 +5866,10 @@ def _lifecycle_policies( for argument in arguments: if not argument.projects_result: continue + # A string descriptor update publishes its value through the + # projected descriptor result, so it owns no writeback phase. + if argument.projects_character_descriptor_update: + continue if argument.result_position is None: blockers.append(f"argument {argument.name!r} writeback is missing a result position") continue @@ -5086,6 +5944,46 @@ def _is_first_lane_scalar_type(semantic_type: models.SemanticType) -> bool: ) +def _target_long_double_mantissa_bits() -> int: + """Return the build target's ``long double`` mantissa width, implicit bit included.""" + return int(numpy.finfo(numpy.longdouble).nmant) + 1 + + +def _measured_mantissa_bits(semantic_type: models.SemanticType) -> int | None: + """Return the compiler-measured mantissa width recorded for one scalar. + + The C probe reports ``precision_bits`` from ``LDBL_MANT_DIG`` and the + Fortran probe reports ``digits``; both count the implicit bit. A contract + that declares the type without a source language carries neither. + """ + for fact_key, field in (("c_type_fact", "precision_bits"), ("fortran_type_fact", "digits")): + fact = semantic_type.metadata.get(fact_key) + if isinstance(fact, Mapping): + measured = fact.get(field) + if isinstance(measured, int) and measured > 0: + return int(measured) + return None + + +def _extended_precision_blocker(semantic_type: models.SemanticType) -> str | None: + """Refuse an extended-precision scalar whose measured format the target cannot hold. + + ``Float128`` names the target's ``long double``, which NumPy exposes as + ``longdouble``. A source declaring a wider mantissa -- Fortran ``real(16)`` + on a target whose ``long double`` is x87 extended precision -- has no NumPy + representation, and its storage size cannot reveal that on its own. + """ + if semantic_type.name not in _EXTENDED_PRECISION_SCALAR_TYPES: + return None + measured = _measured_mantissa_bits(semantic_type) + if measured is None or measured == _target_long_double_mantissa_bits(): + return None + return ( + f"{semantic_type.name} declares a {measured}-bit mantissa but this target's long double " + f"provides {_target_long_double_mantissa_bits()} bits" + ) + + def _is_scalar_storage_type(semantic_type: models.SemanticType) -> bool: """Report whether a type carries rank-zero array-backed scalar storage metadata.""" storage = semantic_type.storage @@ -5146,6 +6044,7 @@ def _scalar_descriptor_result_policy( decision: OwnershipDecision, *, descriptor_kind: str | None = None, + may_be_unallocated: bool = False, ) -> ScalarDescriptorResultPolicy | None: """Project one completed nullable rank-zero descriptor copy policy.""" if decision.kind is ObjectKind.DERIVED_TYPE: @@ -5161,6 +6060,7 @@ def _scalar_descriptor_result_policy( nullable=decision.nullable, copy_reason=SCALAR_DESCRIPTOR_RESULT_COPY_REASON, release_owner=OwnershipOwner.PYTHON, + may_be_unallocated=may_be_unallocated, ) @@ -5752,11 +6652,20 @@ def _scalar_module_getter_blockers( """Validate one completed scalar or literal-string getter.""" blockers = [] literal_string = _is_binding_literal_string(variable, getter_action) - if not (_is_first_lane_scalar_type(variable.semantic_type) or literal_string): + character_value = getter_action is ModuleGetterAction.CHARACTER_VALUE + # A descriptor character module variable reaches Python through the same + # nullable snapshot a descriptor scalar uses, carrying a runtime width. + character_snapshot = ( + getter_action is ModuleGetterAction.NULLABLE_SNAPSHOT and variable.semantic_type.name == "String" + ) + string_getter = literal_string or character_value + if not (_is_first_lane_scalar_type(variable.semantic_type) or string_getter or character_snapshot): blockers.append("module variable is not a primitive rank-zero scalar") - expected_getter_kind = ObjectKind.STRING if literal_string else ObjectKind.SCALAR + if character_value and _character_length(variable.semantic_type) is None: + blockers.append("character module variable requires one declared length") + expected_getter_kind = ObjectKind.STRING if string_getter else ObjectKind.SCALAR supported_getter_actions = ( - {CodegenAction.COPY_OUT} if literal_string else {CodegenAction.DIRECT_VALUE, CodegenAction.SNAPSHOT_COPY} + {CodegenAction.COPY_OUT} if string_getter else {CodegenAction.DIRECT_VALUE, CodegenAction.SNAPSHOT_COPY} ) if getter is None: blockers.append("module variable is missing completed getter policy") @@ -5831,10 +6740,13 @@ def _scalar_module_setter_blockers( return ("scalar constant must omit native setter assignment",) return () if setter.setter_action is SetterAction.WRITE_THROUGH: - if setter.assignment_mode is not AssignmentMode.VALUE_COPY: + if setter.assignment_mode not in {AssignmentMode.VALUE_COPY, AssignmentMode.CHARACTER_COPY}: return ("write-through scalar setter requires value-copy native assignment",) - if setter.python_barrier_action is not PythonBarrierAction.SCALAR_VALUE: - return ("write-through scalar setter requires scalar-value Python conversion",) + expected_python_action = ( + PythonBarrierAction.STRING_VALUE if setter.kind is ObjectKind.STRING else PythonBarrierAction.SCALAR_VALUE + ) + if setter.python_barrier_action is not expected_python_action: + return (f"write-through scalar setter requires {expected_python_action.value} Python conversion",) return () if setter.setter_action is SetterAction.REJECT_REPLACEMENT: if descriptor_kind is None: @@ -5855,9 +6767,24 @@ def _scalar_module_getter_action( return ModuleGetterAction.CONSTANT_VALUE if getter is not None and getter.codegen_action is CodegenAction.SNAPSHOT_COPY and getter.nullable: return ModuleGetterAction.NULLABLE_SNAPSHOT + if _is_fixed_length_character_scalar(variable): + # A character value cannot cross the C ABI by value, so it copies + # through a fixed-width byte buffer the way a character field does. + return ModuleGetterAction.CHARACTER_VALUE return ModuleGetterAction.DIRECT_VALUE +def _is_fixed_length_character_scalar(variable: models.SemanticVariable) -> bool: + """Return whether one module variable is a rank-zero declared-length character.""" + semantic_type = variable.semantic_type + return bool( + semantic_type.name == "String" + and int(semantic_type.rank or 0) == 0 + and _character_length(semantic_type) is not None + and character_descriptor_kind(semantic_type.metadata) is None + ) + + def _source_parameter_needs_native_getter(variable: models.SemanticVariable) -> bool: """Return whether a source parameter value remains compiler-owned.""" return bool( @@ -5872,10 +6799,17 @@ def _source_parameter_needs_native_getter(variable: models.SemanticVariable) -> def _scalar_module_native_assignment( setter: OwnershipDecision | None, + variable: models.SemanticVariable, ) -> AssignmentMode: - """Project the completed native setter action for bridge lowering.""" + """Project the completed native setter action for bridge lowering. + + A character value has no by-value C ABI, so its write is a distinct native + mechanism rather than the same value copy a numeric scalar uses. + """ if setter is None or setter.setter_action is not SetterAction.WRITE_THROUGH: return AssignmentMode.NONE + if setter.assignment_mode is AssignmentMode.VALUE_COPY and _is_fixed_length_character_scalar(variable): + return AssignmentMode.CHARACTER_COPY return setter.assignment_mode @@ -6363,6 +7297,7 @@ def _array_handoff_policy(semantic_type: models.SemanticType) -> ArrayHandoffPol flatten_python_storage=_array_handoff_flattens_python_storage(array), flat_axis=_array_handoff_flat_axis(array), itemsize=_array_handoff_itemsize(semantic_type), + character=semantic_type.name == "String", category=array.category, extent_references=tuple(declaration_extent_references(item) for item in shape), ) @@ -6438,8 +7373,10 @@ def _is_phase6_ordinary_array_type(semantic_type: models.SemanticType) -> bool: storage = semantic_type.storage array = storage.array if storage is not None else None scalar_storage = _is_scalar_storage_array_policy(array_policy) + # A character array may leave its width assumed: every element of a NumPy + # ``S`` array shares one itemsize, which already travels beside the buffer. 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 + semantic_type.name == "String" and not scalar_storage ) supported_rank = array_policy.rank is None or 1 <= array_policy.rank <= 15 or scalar_storage return bool( @@ -6501,6 +7438,7 @@ def _raw_array_handoff_policy(semantic_type: models.SemanticType) -> ArrayHandof native_order=order, contiguous=True, itemsize=_character_length(semantic_type) if semantic_type.name == "String" else None, + character=semantic_type.name == "String", category="raw_address", extent_references=tuple(declaration_extent_references(item) for item in shape), ) diff --git a/prik/policy/models.py b/prik/policy/models.py index 5676b95f1..642f8e328 100644 --- a/prik/policy/models.py +++ b/prik/policy/models.py @@ -63,6 +63,31 @@ class NativeEntrypointAction(str, Enum): GENERATED_FORTRAN_ADAPTER = "generated_fortran_adapter" +@dataclass(frozen=True) +class DirectCABITypePolicy: + """One preserved C declaration type selected before wrapper planning.""" + + source_spelling: str | None + scalar_type_name: str | None + pointer_depth: int + qualifiers: tuple[str, ...] + const: bool + # Scalar values whose native declaration differs from canonical contract + # storage are converted at the call boundary. Exact NumPy storage already + # has the native representation, so its completed decision remains false. + converts_to_contract_storage: bool = False + + +@dataclass(frozen=True) +class DirectCABIPolicy: + """Exact direct-C function ABI facts owned by post-IR policy.""" + + calling_convention: str + result_transport: str + result: DirectCABITypePolicy | None + parameters: tuple[DirectCABITypePolicy, ...] + + class EntrypointPassingConvention(str, Enum): """Completed C-boundary transport for one parameter or result.""" @@ -263,6 +288,7 @@ class ModuleGetterAction(str, Enum): NATIVE_CONSTANT_VALUE = "native_constant_value" NATIVE_CONSTANT_ARRAY_VALUE = "native_constant_array_value" DIRECT_VALUE = "direct_value" + CHARACTER_VALUE = "character_value" NULLABLE_SNAPSHOT = "nullable_snapshot" BORROWED_ARRAY_VIEW = "borrowed_array_view" NATIVE_ARRAY_HANDLE = "native_array_handle" @@ -578,6 +604,8 @@ class DerivedTypePolicy: sequence: bool supported: bool blockers: tuple[str, ...] = () + abstract: bool = False + deferred_bindings: tuple[str, ...] = () @dataclass(frozen=True) @@ -682,6 +710,19 @@ class NativeArrayDescriptorKind(str, Enum): POINTER = "pointer" +class CharacterLocalRelease(str, Enum): + """Completed release responsibility for one adapter-local character value. + + ``NONE`` covers a plain or ``allocatable`` local, which the compiler frees + when the adapter returns. A ``pointer`` local is storage the adapter itself + allocated, so it names when the adapter must free it again. + """ + + NONE = "none" + DEALLOCATE = "deallocate" + DEALLOCATE_IF_RETAINED = "deallocate_if_retained" + + class NativeArrayHandleKind(str, Enum): """Completed native handle owner/use category.""" @@ -838,10 +879,13 @@ class NativeStatusOutputPolicy: name: str native_name: str native_position: int - result_position: int + result_position: int | None semantic_type_name: str rank: int character_length: int | None = None + # A visible message names a buffer the caller supplied, so the binding + # reads it through the argument instead of a projected native output. + python_position: int | None = None @dataclass(frozen=True) @@ -875,6 +919,7 @@ class ModuleVariablePolicy: constant_value: Any supported: bool blockers: tuple[str, ...] = () + character_length: int | None = None array: ArrayHandoffPolicy | None = None native_array_handle: NativeArrayHandleWrapperPolicy | None = None derived: DerivedModuleObjectPolicy | None = None @@ -907,6 +952,9 @@ class ArrayHandoffPolicy: flatten_python_storage: bool = False flat_axis: int | None = None itemsize: int | None = None + # Whether the buffer holds characters. A character array always reports its + # width at runtime, so the role exists even when ``itemsize`` is assumed. + character: bool = False category: str | None = None extent_references: tuple[tuple[str, ...], ...] = () extent_reference_roles: tuple[tuple[str, ...], ...] = () @@ -1053,15 +1101,36 @@ class NativeArrayHandleWrapperPolicy: default_handle: NativeArrayDefaultHandlePolicy +@dataclass(frozen=True) +class CharacterLocalPolicy: + """Completed adapter-local storage for one scalar character value. + + The C ABI is the same for every scalar character argument: a byte buffer + and a length. What differs is the Fortran local the adapter must build + before the original dummy accepts it, so this records the attribute and + length kind that local carries and who releases it. + """ + + descriptor_kind: NativeArrayDescriptorKind | None + deferred_length: bool + release: CharacterLocalRelease + + @dataclass(frozen=True) class ScalarDescriptorResultPolicy: - """Completed nullable rank-zero descriptor result copy contract.""" + """Completed nullable rank-zero descriptor result copy contract. + + ``may_be_unallocated`` marks a result whose storage the native procedure is + not obliged to establish, so reading it directly is not permitted and the + value has to be moved out through a dummy that can test allocation first. + """ descriptor_kind: NativeArrayDescriptorKind runtime_length: bool nullable: bool copy_reason: str release_owner: OwnershipOwner + may_be_unallocated: bool = False @dataclass(frozen=True) @@ -1153,6 +1222,7 @@ class ArgumentPolicy: python_visible: bool result_position: int | None character_length: int | None + character_local: CharacterLocalPolicy | None = None array: ArrayHandoffPolicy | None = None native_array_actual: NativeArrayActualPolicy | None = None native_array_handle: NativeArrayHandleWrapperPolicy | None = None @@ -1168,11 +1238,37 @@ class ArgumentPolicy: entrypoint_pass_descriptor_presence: bool = False entrypoint_pass_derived_transaction: bool = False entrypoint_pass_callback_parameter: bool = False + native_storage_c_type: str | None = None + native_array_element_c_type: str | None = None + character_allows_embedded_nul: bool = False + + @property + def projects_character_descriptor_update(self) -> bool: + """Report whether this argument returns its replaced value as a projected result. + + ``character_local`` carries a descriptor kind only for a call-local + ``allocatable`` or ``pointer`` character input, so an input that also + occupies a Python result position is the update lane. Its output + travels as one descriptor-backed result rather than as argument + writeback. + """ + return bool( + self.character_local is not None + and self.character_local.descriptor_kind is not None + and self.projects_result + ) @dataclass(frozen=True) class ResultPolicy: - """Completed wrapper policy for one native result.""" + """Completed wrapper policy for one native result. + + ``updates_argument`` marks the one shape whose native output storage is also + a Python-visible argument: a ``character(len=:), allocatable`` update whose + caller supplies a ``str`` and receives the reallocated value. Its producer + is the argument's own native call slot, so stages that pair a hidden output + with a dedicated result slot must consult this fact instead. + """ owner_path: str semantic_type_name: str @@ -1189,6 +1285,9 @@ class ResultPolicy: character_length: int | None = None array: ArrayHandoffPolicy | None = None source_kind: str = "direct_return" + # Declared by a ``Hidden`` slot: the native call produces it exactly like + # any other output, but the binding never builds a Python value from it. + python_returned: bool = True native_name: str | None = None native_position: int | None = None result_position: int = 0 @@ -1197,6 +1296,7 @@ class ResultPolicy: derived: DerivedHandoffPolicy | None = None transformations: tuple[TransformationPolicy, ...] = () entrypoint_passing: EntrypointPassingConvention = EntrypointPassingConvention.BLOCKED + updates_argument: bool = False @dataclass(frozen=True) @@ -1219,6 +1319,7 @@ class NativeCallSlotPolicy: bridge_data_action: BridgeDataAction bridge_copy_reason: str | None object_kind: ObjectKind | None + native_scalar_c_type: str | None = None scalar_logical_abi: ScalarLogicalABI = ScalarLogicalABI.NOT_APPLICABLE scalar_native_type: str | None = None array_logical_abi: ArrayLogicalABI = ArrayLogicalABI.NOT_APPLICABLE @@ -1275,6 +1376,12 @@ class FunctionWrapperPolicy: entrypoint_action: NativeEntrypointAction | None = None entrypoint_symbol: str = "" entrypoint_diagnostics: tuple[str, ...] = () + direct_c_abi: DirectCABIPolicy | None = None + # A positional-only surface takes no keyword arguments, so its argument + # names are not part of the Python API. Policy renames them to ``arg0`` + # upward, because a native declaration's parameter names are an + # implementation detail that need not agree across targets. + accepts_keyword_arguments: bool = True if __name__ == "__main__": diff --git a/prik/policy/ownership.py b/prik/policy/ownership.py index d5234e4a7..a0c3dc047 100644 --- a/prik/policy/ownership.py +++ b/prik/policy/ownership.py @@ -264,12 +264,15 @@ class AssignmentMode(str, Enum): Values: ``NONE`` emits no native assignment. ``VALUE_COPY`` copies the incoming - value into existing native storage. ``ALIAS`` associates the + value into existing native storage. ``CHARACTER_COPY`` copies an + incoming fixed-width byte buffer into existing native character + storage, which has no by-value C ABI. ``ALIAS`` associates the destination with existing storage rather than copying it. """ NONE = "none" VALUE_COPY = "value_copy" + CHARACTER_COPY = "character_copy" ALIAS = "alias" @@ -455,6 +458,7 @@ def dispatch(self, target: Any, subject: Any, decision: OwnershipDecision, *args "Char", "Complex64", "Complex128", + "Complex256", "Float16", "Float32", "Float64", @@ -464,6 +468,7 @@ def dispatch(self, target: Any, subject: Any, decision: OwnershipDecision, *args "Int16", "Int32", "Int64", + "SizeT", "UInt", "UInt8", "UInt16", @@ -620,6 +625,75 @@ def ownership_context_for_argument(function: Any, argument: Any) -> OwnershipCon ) +def uses_deferred_character_length(metadata: Mapping[str, Any] | None) -> bool: + """Return whether one character value declares a deferred length parameter. + + A ``character(len=:)`` dummy is not interoperable, so no ``bind(C)`` + interface can declare it and the generated Fortran adapter must build the + local the native dummy requires. Assumed length (``character(len=*)``) is + a different form and stays fixed-length here. + """ + return bool(metadata) and metadata.get("fortran_character_length") == ":" + + +def declared_character_length(metadata: Mapping[str, Any] | None) -> int | None: + """Return a positive fixed Fortran character length, normalizing accepted spellings. + + A deferred (``:``) or assumed (``*``) length is not a declared width and + returns ``None``, as does any spelling that is not a positive integer. + """ + value = (metadata or {}).get("fortran_character_length") + if isinstance(value, bool) or value is None: + return None + if isinstance(value, int): + return value if value > 0 else None + text = str(value).strip() + return int(text) if text.isdigit() and int(text) > 0 else None + + +def _has_declared_character_length(variable: Any) -> bool: + """Return whether one character variable declares a positive fixed width.""" + metadata = getattr(getattr(variable, "semantic_type", None), "metadata", None) + return declared_character_length(metadata) is not None + + +def character_descriptor_kind(metadata: Mapping[str, Any] | None) -> str | None: + """Return the ``allocatable`` or ``pointer`` attribute one character value declares. + + The attribute belongs to the native dummy, not to the C ABI: a scalar + character argument still crosses as a byte buffer and a length either way. + What it decides is the adapter local, which must carry the same attribute + before the original dummy will accept it. + """ + values = metadata or {} + if values.get("fortran_allocatable"): + return "allocatable" + if values.get("fortran_pointer"): + return "pointer" + return None + + +def is_character_descriptor_update( + metadata: Mapping[str, Any] | None, + decision: OwnershipDecision, +) -> bool: + """Report whether one completed argument decision is the string update lane. + + The lane is the shape that is both caller-supplied and returns native + storage the procedure may have replaced: an ``allocatable`` or ``pointer`` + character dummy that native code reads and may reallocate or reassociate. + Its input stays an ordinary call-local character buffer, so the replaced + value needs a second completed decision for the projected result facet. + """ + return bool( + character_descriptor_kind(metadata) + and decision.kind is ObjectKind.STRING + and decision.codegen_action is CodegenAction.CALL_LOCAL_INPUT + and decision.projects_result + and decision.python_visible + ) + + def _is_native_array_handle_facts(facts: _StorageFacts) -> bool: """Return whether completed storage facts identify an array descriptor handle.""" metadata = facts.metadata or {} @@ -927,7 +1001,7 @@ def decide_semantic_setter( assignment_mode=( AssignmentMode.ALIAS if storage.storage_mode is StorageMode.ALIAS else AssignmentMode.VALUE_COPY ), - setter_action=self._setter_action(storage, incoming, context), + setter_action=self._setter_action(storage, incoming, context, variable), ) @staticmethod @@ -935,6 +1009,7 @@ def _setter_action( storage: OwnershipDecision, incoming: OwnershipDecision, context: OwnershipContext, + variable: Any = None, ) -> SetterAction: """Select Python setter exposure from completed storage and incoming policy. @@ -948,6 +1023,15 @@ def _setter_action( return SetterAction.WRITE_THROUGH if storage.kind is ObjectKind.STRING and context.is_field: return SetterAction.WRITE_THROUGH + # A character module variable is written through the same fixed-width + # buffer a field uses, so it needs a declared width to write into. + # An assumed length has none, and keeps the rejecting setter. + if ( + storage.kind is ObjectKind.STRING + and context.is_module_variable + and _has_declared_character_length(variable) + ): + return SetterAction.WRITE_THROUGH if storage.kind is ObjectKind.DERIVED_TYPE and context.is_module_variable: return SetterAction.REJECT_REPLACEMENT if storage.kind is ObjectKind.DERIVED_TYPE and incoming.transfer is TransferMode.CALL_LOCAL: @@ -1328,6 +1412,9 @@ def _string_descriptor_decision( return None storage = StorageMode.HEAP if facts.allocatable else StorageMode.ALIAS + update = OwnershipPolicyResolver._character_descriptor_update_decision(facts, context, storage) + if update is not None: + return update if context.is_result: return OwnershipDecision( ObjectKind.STRING, @@ -1357,6 +1444,48 @@ def _string_descriptor_decision( ) return None + @staticmethod + def _character_descriptor_update_decision( + facts: _StorageFacts, + context: OwnershipContext, + storage: StorageMode, + ) -> OwnershipDecision | None: + """Return update policy for one caller-supplied allocatable or pointer string. + + A descriptor dummy that native code both reads and may replace cannot + travel as one caller buffer in each direction: an ``allocatable`` dummy + may be reallocated to a length the caller never sized, and a ``pointer`` + dummy may be reassociated with storage the caller never supplied. The + input therefore stays an ordinary call-local character buffer and the + projected result carries whatever the dummy holds afterwards, so the + Python caller supplies a ``str`` and receives the replaced value. A + character dummy with no descriptor attribute keeps its copy-in/copy-out + replacement, whose single buffer is wide enough by construction. + """ + if not ( + context.is_argument + and (facts.allocatable or facts.pointer) + and context.reads_argument + and context.writes_argument + and context.projects_result + and context.python_visible + ): + return None + return OwnershipDecision( + ObjectKind.STRING, + OwnershipOwner.CALLER, + TransferMode.CALL_LOCAL, + DestructionPolicy.CALL_LOCAL, + storage_mode=storage, + boundary_storage_mode=storage, + mutates_native=True, + projects_result=True, + reason=( + "string descriptor update converts one call-local input and returns " + "the replaced value through a projected descriptor result" + ), + ) + @staticmethod def _scalar_string_storage_decision(context: OwnershipContext) -> OwnershipDecision: """Return aliasing or call-local policy for rank-zero mutable character storage.""" @@ -2061,7 +2190,14 @@ def _pointer_argument_blocker( # handoff policy. Their association and writeback rules do not # use the older scalar/array descriptor projection lane below. return None - supported_scalar_write = facts.rank == 0 and decision.descriptor_boundary and context.projects_result + # A rank-zero pointer write is completed either as a hidden descriptor + # output or as the caller-supplied string update, which returns the + # possibly reassociated value through its own projected result. + supported_scalar_write = ( + facts.rank == 0 + and context.projects_result + and (decision.descriptor_boundary or decision.kind is ObjectKind.STRING) + ) if context.writes_argument and not supported_scalar_write: return "pointer output and reassociation code generation is not implemented" if not context.writes_argument and decision.transfer is not TransferMode.CALL_LOCAL: diff --git a/prik/preprocessing/README.md b/prik/preprocessing/README.md index ad24f1ac6..686d91394 100644 --- a/prik/preprocessing/README.md +++ b/prik/preprocessing/README.md @@ -35,9 +35,9 @@ extension. `prik.compiler` supplies reusable compiler mechanisms; ## Tests And Docs -- `tests/c/preprocessing/` -- `tests/c/probes/` -- `tests/fortran/source_preprocessing/preprocessing/` +- `tests/c/infrastructure/preprocessing/` +- `tests/c/data_types/probes/` +- `tests/fortran/infrastructure/preprocessing/` - `tests/fortran/data_types/probes/` - `docs/developer/packages/preprocessing.md` - `docs/developer/codebase-map.md` diff --git a/prik/preprocessing/probes/c_types.py b/prik/preprocessing/probes/c_types.py index 32ddb3c33..edafdaad6 100644 --- a/prik/preprocessing/probes/c_types.py +++ b/prik/preprocessing/probes/c_types.py @@ -177,6 +177,16 @@ def build_c_standard_type_probe_source() -> str: _Alignof(type) * (size_t)CHAR_BIT, \ precision, max_exp) +#define PRIK_PRINT_COMPLEX(name, type, precision, max_exp) \ + printf("\"" name "\":{\"header\":\"\",\"available\":true," \ + "\"kind\":\"arithmetic\",\"underlying_c_type\":\"%s\"," \ + "\"bits\":%zu,\"alignment_bits\":%zu,\"precision_bits\":%d," \ + "\"max_binary_exponent\":%d}", \ + PRIK_BASE_TYPE((type)0), \ + sizeof(type) * (size_t)CHAR_BIT, \ + _Alignof(type) * (size_t)CHAR_BIT, \ + precision, max_exp) + int main(void) { printf("{\"types\":{"); PRIK_PRINT_ARITHMETIC("_Bool", "", _Bool); @@ -209,18 +219,60 @@ def build_c_standard_type_probe_source() -> str: printf(","); PRIK_PRINT_REAL("long double", long double, LDBL_MANT_DIG, LDBL_MAX_EXP); printf(","); - PRIK_PRINT_ARITHMETIC("float _Complex", "", float _Complex); + PRIK_PRINT_COMPLEX("float _Complex", float _Complex, FLT_MANT_DIG, FLT_MAX_EXP); printf(","); - PRIK_PRINT_ARITHMETIC("double _Complex", "", double _Complex); + PRIK_PRINT_COMPLEX("double _Complex", double _Complex, DBL_MANT_DIG, DBL_MAX_EXP); printf(","); - PRIK_PRINT_ARITHMETIC("long double _Complex", "", long double _Complex); + PRIK_PRINT_COMPLEX("long double _Complex", long double _Complex, LDBL_MANT_DIG, LDBL_MAX_EXP); printf(","); PRIK_PRINT_ARITHMETIC("size_t", "stddef.h", size_t); printf(","); +#ifdef INT8_MAX + PRIK_PRINT_ARITHMETIC("int8_t", "stdint.h", int8_t); +#else + printf("\"int8_t\":{\"header\":\"stdint.h\",\"available\":false}"); +#endif + printf(","); +#ifdef INT16_MAX + PRIK_PRINT_ARITHMETIC("int16_t", "stdint.h", int16_t); +#else + printf("\"int16_t\":{\"header\":\"stdint.h\",\"available\":false}"); +#endif + printf(","); +#ifdef INT32_MAX + PRIK_PRINT_ARITHMETIC("int32_t", "stdint.h", int32_t); +#else + printf("\"int32_t\":{\"header\":\"stdint.h\",\"available\":false}"); +#endif + printf(","); +#ifdef INT64_MAX + PRIK_PRINT_ARITHMETIC("int64_t", "stdint.h", int64_t); +#else + printf("\"int64_t\":{\"header\":\"stdint.h\",\"available\":false}"); +#endif + printf(","); +#ifdef UINT8_MAX + PRIK_PRINT_ARITHMETIC("uint8_t", "stdint.h", uint8_t); +#else + printf("\"uint8_t\":{\"header\":\"stdint.h\",\"available\":false}"); +#endif + printf(","); +#ifdef UINT16_MAX + PRIK_PRINT_ARITHMETIC("uint16_t", "stdint.h", uint16_t); +#else + printf("\"uint16_t\":{\"header\":\"stdint.h\",\"available\":false}"); +#endif + printf(","); #ifdef UINT32_MAX PRIK_PRINT_ARITHMETIC("uint32_t", "stdint.h", uint32_t); #else printf("\"uint32_t\":{\"header\":\"stdint.h\",\"available\":false}"); +#endif + printf(","); +#ifdef UINT64_MAX + PRIK_PRINT_ARITHMETIC("uint64_t", "stdint.h", uint64_t); +#else + printf("\"uint64_t\":{\"header\":\"stdint.h\",\"available\":false}"); #endif printf(","); PRIK_PRINT_ARITHMETIC("time_t", "time.h", time_t); diff --git a/prik/preprocessing/probes/fortran_types.py b/prik/preprocessing/probes/fortran_types.py index cbf35a65f..da299065a 100644 --- a/prik/preprocessing/probes/fortran_types.py +++ b/prik/preprocessing/probes/fortran_types.py @@ -52,6 +52,44 @@ _SAFE_EXPRESSION_RE = re.compile(r"^[A-Za-z0-9_+\-*/().,= :]+$") _TOKEN_RE = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\b") +_PROBE_INTRINSIC_NAMES = frozenset( + { + # Numeric inquiry and kind-selection intrinsics that may appear in a + # constant kind or size expression. + "bit_size", + "digits", + "epsilon", + "huge", + "kind", + "len", + "maxexponent", + "minexponent", + "precision", + "radix", + "range", + "selected_char_kind", + "selected_int_kind", + "selected_real_kind", + "size", + "storage_size", + "tiny", + # Conversion and reduction intrinsics used to combine the above. + "abs", + "ceiling", + "floor", + "int", + "max", + "min", + "mod", + "modulo", + "nint", + "real", + # Constant operands that may appear as intrinsic arguments. + "false", + "true", + } +) + _ISO_FORTRAN_ENV_NAMES = { "int8", "int16", @@ -180,7 +218,7 @@ def fortran_type_probe_expressions( seen: set[str] = set() for item in requirements: expression = str(item.get("expression") or "").strip() - if not expression: + if not expression or not probe_can_resolve_expression(expression): continue key = expression.lower() if key in seen: @@ -190,6 +228,20 @@ def fortran_type_probe_expressions( return expressions +def probe_can_resolve_expression(expression: str) -> bool: + """Return whether the standalone probe program can evaluate ``expression``. + + The probe is a self-contained program: it can import intrinsic modules but + cannot ``use`` a module from the project being analyzed, whose compiled + interface does not exist yet. An expression naming a symbol declared + elsewhere in the project — a `wp` or `ip` kind parameter, for example — is + therefore left for the requirement report rather than compiled into a + program that cannot resolve it. + """ + known = _PROBE_INTRINSIC_NAMES | _ISO_FORTRAN_ENV_NAMES | _ISO_C_BINDING_NAMES + return all(token.lower() in known for token in _TOKEN_RE.findall(expression)) + + def build_fortran_type_probe_source(expressions: Sequence[str]) -> str: """Build free-form Fortran source that prints integer expression results. @@ -718,8 +770,15 @@ def evaluate_fortran_type_facts( incomplete. """ requirement_list = list(requirements) - expressions = [str(item.get("expression") or "").strip() for item in requirement_list] - expressions = [expression for expression in expressions if expression] + expressions = [ + text + for item in requirement_list + for text in ( + str(item.get("expression") or "").strip(), + str(item.get("precision_expression") or "").strip(), + ) + if text + ] if not expressions: return {} active_report = _report_for_expressions( @@ -742,12 +801,22 @@ def evaluate_fortran_type_facts( base_type = str(item.get("base_type") or "").lower() raw_kind = item.get("kind") kind = None if raw_kind is None else str(raw_kind).lower() - facts[(base_type, kind)] = { + fact: dict[str, object] = { "base_type": base_type, "kind": kind, "bits": value, "expression": expression, } + precision_expression = str(item.get("precision_expression") or "").strip() + if precision_expression: + digits = _value_for_expression(active_report.values, precision_expression) + if digits is None: + raise FortranTypeProbeError( + f"Fortran type probe report is missing required expression {precision_expression!r}" + ) + fact["digits"] = digits + fact["precision_expression"] = precision_expression + facts[(base_type, kind)] = fact return facts diff --git a/prik/printers/c.py b/prik/printers/c.py index cce308070..189d5e1de 100644 --- a/prik/printers/c.py +++ b/prik/printers/c.py @@ -8,6 +8,8 @@ from __future__ import annotations +import textwrap + from prik.codegen.nodes import ( CAllowThreadsBegin, CAllowThreadsEnd, @@ -20,9 +22,11 @@ CFunction, CFunctionPointerType, CFunctionPrototype, + CGoto, CHeader, CIf, CInclude, + CLabel, CMacroDefinition, CMethodDefEntry, CMethodDefTable, @@ -96,7 +100,8 @@ def _visit_CFunction(self, node: CFunction) -> str: """Render one C function definition with each body statement indented.""" prefix = f"{node.storage} " if node.storage else "" body = "\n".join(self._indented(self.visit(statement)) for statement in node.body) - return f"{prefix}{self._signature(node.return_type, node.name, node.parameters)} {{\n{body}\n}}" + doc = "".join(f"/* {chunk} */\n" for line in node.doc for chunk in (textwrap.wrap(line, width=96) or [""])) + return f"{doc}{prefix}{self._signature(node.return_type, node.name, node.parameters)} {{\n{body}\n}}" def _visit_CFunctionPrototype(self, node: CFunctionPrototype) -> str: """Render one C prototype using the shared signature renderer.""" @@ -287,6 +292,14 @@ def _visit_CExpressionStatement(self, node: CExpressionStatement) -> str: """Render one C expression statement and add its terminating semicolon.""" return f"{node.expression.text};" + def _visit_CGoto(self, node: CGoto) -> str: + """Render one jump to a function-local cleanup label.""" + return f"goto {node.label};" + + def _visit_CLabel(self, node: CLabel) -> str: + """Render one function-local cleanup label.""" + return f"{node.name}:" + def _visit_CAllowThreadsBegin(self, _node: CAllowThreadsBegin) -> str: """Render the opening CPython thread-release macro without a semicolon.""" return "Py_BEGIN_ALLOW_THREADS" diff --git a/prik/printers/fortran.py b/prik/printers/fortran.py index 0c1114be5..f6f5d7f2b 100644 --- a/prik/printers/fortran.py +++ b/prik/printers/fortran.py @@ -9,6 +9,8 @@ import re +import textwrap + from prik.codegen.nodes import ( FortranAllocate, FortranAssignment, @@ -224,11 +226,33 @@ def _visit_FortranModule(self, node: FortranModule) -> str: lines.extend(self._indented(self.visit(declaration)) for declaration in node.declarations) lines.extend(self._indented(self.visit(interface)) for interface in node.interfaces if not interface.abstract) lines.append("contains") - lines.extend(self._indented(self.visit(procedure)) for procedure in node.procedures) + for procedure in node.procedures: + # One blank line before each procedure keeps a long generated module + # scannable; without it every procedure abuts the previous `end`. + lines.append("") + lines.append(self._indented(self.visit(procedure))) lines.append(f"end module {node.name}") - lines.extend(self.visit(procedure) for procedure in node.standalone_procedures) + for procedure in node.standalone_procedures: + lines.append("") + lines.append(self.visit(procedure)) return "\n".join(lines) + @staticmethod + def _doc_comment_lines(doc: tuple[str, ...]) -> list[str]: + """Render one procedure's explanatory prose as wrapped Fortran line comments. + + Free-form Fortran caps a line at 132 columns, and a generated procedure + is indented inside its module, so prose is wrapped well short of that + rather than emitted as one long line. + """ + lines: list[str] = [] + for entry in doc: + if not entry: + lines.append("!") + continue + lines.extend(f"! {chunk}" for chunk in textwrap.wrap(entry, width=96) or [""]) + return lines + def _visit_FortranUse(self, node: FortranUse) -> str: """Render one Fortran use statement and wrap a long ONLY list.""" if node.only: @@ -249,7 +273,7 @@ def _visit_FortranFunction(self, node: FortranFunction) -> str: optional internal procedures in Fortran's required source order. """ signature = self._function_signature(node) - lines = [signature, *self._fortran_function_specification(node)] + lines = [*self._doc_comment_lines(node.doc), signature, *self._fortran_function_specification(node)] lines.extend(self._indented(self.visit(statement)) for statement in node.body) if node.internal_procedures: lines.append("contains") diff --git a/prik/printers/pyi.py b/prik/printers/pyi.py index ea7355561..22c8d8a4f 100644 --- a/prik/printers/pyi.py +++ b/prik/printers/pyi.py @@ -30,7 +30,9 @@ ADDRESS_ROLE_PROJECTION, ADDRESS_ROLE_RAW, BIND_TARGET_METADATA, + DEFERRED_BINDING_METADATA, MAYBE_UNALLOCATED_METADATA, + NATIVE_C_SCALAR_CAST_METADATA, NATIVE_PROJECTION_METADATA, OPTIONAL_ABSENT_HANDLE_METADATA, SCALAR_STORAGE_CATEGORY, @@ -50,6 +52,7 @@ PROTOTYPE_INTENT_METADATA, PROTOTYPE_REF_METADATA, RUNTIME_RELEASE_GIL_METADATA, + HIDDEN_NATIVE_OUTPUT_METADATA, RUNTIME_STATUS_ERROR_METADATA, ProjectionMapping, ProcedureOverloadSet, @@ -77,6 +80,12 @@ _FLAT_DIMENSION_PRINT_SENTINEL = "@prik.Flat" +# Type attributes the contract states through its own vocabulary rather than +# through `native_type`: `public` is the default accessibility, `private` has a +# marker, and `abstract` has one too. +_IMPLIED_TYPE_ATTRIBUTES = frozenset({"public", "private", "abstract"}) + + @dataclass(frozen=True) class _PyiEmissionContext: """Own all state accumulated while rendering one semantic node tree.""" @@ -388,6 +397,11 @@ def _visit_ProcedureOverloadSet( indent = "" generic = self._overload_generic_argument(candidate, overload_set.name) if in_class else "" bind_target = candidate.metadata.get(BIND_TARGET_METADATA) + if self._constructor_binds_its_own_type(overload_set.name, bind_target, context): + # A constructor's native generic is named for its type, so the + # class already states the target the way an unrenamed method + # states its own. + bind_target = None if candidate.origin.native_abi == "c" and candidate.origin.native_symbol: bind_target = ( candidate.origin.native_symbol @@ -420,6 +434,8 @@ def _visit_SemanticClass( decorators = [] if self._is_private(cls): decorators.append(f"@{context.contract('private')}") + if self._is_abstract(cls): + decorators.append(f"@{context.contract('abstract')}") native_type = self._native_type_decorator(cls, context) if native_type: decorators.append(native_type) @@ -436,12 +452,23 @@ def _class_base_text(base: str, context: _PyiEmissionContext) -> str: """Return an imported contract base name or a user base name.""" return context.contract_type(base) + @staticmethod + def _is_abstract(cls: SemanticClass) -> bool: + """Return whether the native type is declared ``abstract``.""" + return any( + str(attribute).casefold() == "abstract" for attribute in cls.metadata.get("fortran_type_attributes", ()) + ) + @staticmethod def _native_type_decorator(cls: SemanticClass, context: _PyiEmissionContext) -> str: """Emit native derived-type metadata when the class needs it.""" if cls.origin.source_language != "fortran" or cls.origin.source_kind != "derived_type": return "" - attributes = tuple(str(item) for item in cls.metadata.get("fortran_type_attributes", ())) + attributes = tuple( + str(item) + for item in cls.metadata.get("fortran_type_attributes", ()) + if str(item).casefold() not in _IMPLIED_TYPE_ATTRIBUTES + ) finalizers = tuple(str(item) for item in cls.metadata.get("fortran_final_procedures", ())) parts = [] if attributes: @@ -525,17 +552,23 @@ def _semantic_base_type( semantic_type: SemanticType, context: _PyiEmissionContext, *, - include_deferred_length: bool = False, + shape_follows: bool = False, ) -> str: - """Return the semantic dtype including fixed character length.""" + """Return the semantic dtype, spelling a ``String`` character length. + + A ``String`` annotation carries its length in the first subscription and + its shape, if any, in the second. A scalar assumed length is the bare + ``String`` shorthand; when a shape subscription follows, the length slot + is always spelled so the two are never confused. + """ if semantic_type.name != "String": return context.contract_type(semantic_type.name) length = semantic_type.metadata.get("fortran_character_length") string = context.contract("String") if length is None or str(length) in {"", "*"}: - return string + return f"{string}[...]" if shape_follows else string if str(length) == ":": - return f"{string}[:]" if include_deferred_length else string + return f"{string}[:]" return f"{string}[{length}]" @staticmethod @@ -567,7 +600,7 @@ def _address_target_type( base_type = self._semantic_base_type( semantic_type, context, - include_deferred_length=semantic_type.rank > 0, + shape_follows=semantic_type.rank > 0, ) if semantic_type.rank <= 0: return base_type @@ -587,11 +620,9 @@ def _emit_array_type( storage = semantic_type.storage array = storage.array if storage is not None else None if array is not None and array.category == SCALAR_STORAGE_CATEGORY: - return f"{self._semantic_base_type(semantic_type, context, include_deferred_length=True)}[()]" + return f"{self._semantic_base_type(semantic_type, context, shape_follows=True)}[()]" dimensions = self._array_dimensions(semantic_type, array, context) - base = ( - f"{self._semantic_base_type(semantic_type, context, include_deferred_length=True)}[{', '.join(dimensions)}]" - ) + base = f"{self._semantic_base_type(semantic_type, context, shape_follows=True)}[{', '.join(dimensions)}]" metadata = self._array_annotation_metadata(array, context) if metadata: @@ -851,7 +882,7 @@ def _prototype_argument_inner_type( and storage.array is not None and storage.array.category == SCALAR_STORAGE_CATEGORY ): - return self._semantic_base_type(semantic_type, context, include_deferred_length=True) + return self._semantic_base_type(semantic_type, context, shape_follows=False) if storage is not None and storage.kind in {"reference", "address", "pointer"}: return self._address_target_type(semantic_type, context) return self._visit(semantic_type, context) @@ -1265,8 +1296,16 @@ def _class_constructor( cls: SemanticClass, context: _PyiEmissionContext, ) -> str: - """Handle class constructor for the current generation context.""" - if cls.origin.source_language != "fortran": + """Handle class constructor for the current generation context. + + An abstract native type has no constructor: the type cannot be + instantiated, so the contract states no ``__init__`` for it. + """ + if cls.origin.source_language != "fortran" or self._is_abstract(cls): + return "" + if any(overload.name == "__init__" for overload in cls.overload_sets): + # A generic constructor supplies every accepted signature, so the + # keyword-field form is not part of this class's surface. return "" arguments = [ self._constructor_argument(field, context) for field in cls.fields if self._constructor_accepts_field(field) @@ -1775,15 +1814,34 @@ def _projected_return_annotation( return parts[0] return f"tuple[{', '.join(parts)}]" + @staticmethod + def _unreturned_output_names(func: SemanticFunction) -> frozenset[str]: + """Name the native outputs that never reach the Python return value. + + These are declared as ``Hidden`` slots: either the contract said so + directly, or ``@raises`` consumes them into an exception. Both spell the + same fact, so both emit the same way. + """ + names = {argument.name for argument in func.arguments if argument.metadata.get(HIDDEN_NATIVE_OUTPUT_METADATA)} + policy = func.metadata.get(RUNTIME_STATUS_ERROR_METADATA) + if isinstance(policy, dict): + names.update( + str(policy[key]) for key in ("status", "message") if isinstance(policy.get(key), str) and policy[key] + ) + return frozenset(names) + @staticmethod def _projected_return_arguments(func: SemanticFunction) -> list[tuple[int, SemanticArgument, bool]]: """Handle projected return arguments for the current generation context.""" by_name = {arg.name: arg for arg in func.arguments} + consumed = PyiPrinter._unreturned_output_names(func) returned = [] for mapping in func.projection: if mapping.result_position is None: continue arg_name = mapping.python_name or mapping.native_name + if arg_name in consumed: + continue arg = by_name.get(arg_name) if arg is not None: returned.append( @@ -1979,7 +2037,7 @@ def _decorators( decorators.append(f"{indent}@{context.contract('standalone')}") if not func.metadata.get(OVERLOAD_TARGET_METADATA) and self._requires_native_call(func): decorators.append( - f"{indent}{self._native_call(self._pyi_projection(func), context, self._native_result_projection(func))}" + f"{indent}{self._native_call(self._pyi_projection(func), context, self._native_result_projection(func), func)}" ) if isinstance(policy := func.metadata.get(RUNTIME_STATUS_ERROR_METADATA), dict): decorators.append(f"{indent}{self._raises(policy, context)}") @@ -1999,10 +2057,14 @@ def _identity_decorators( ) -> list[str]: """Emit visibility, method-kind, native-ABI, and link-name markers.""" decorators = [] - if self._is_private(func): + # A constructor is published or absent; the accessibility of the + # specific it selects is that procedure's own fact, not the class's. + if self._is_private(func) and emitted_name != "__init__": decorators.append(f"{indent}@{context.contract('private')}") if isinstance(func, SemanticMethod) and func.is_static: decorators.append(f"{indent}@staticmethod") + if func.metadata.get(DEFERRED_BINDING_METADATA): + decorators.append(f"{indent}@{context.contract('abstractmethod')}") is_native_c_abi = func.origin.source_language == "fortran" and func.origin.native_abi == "c" is_overload = bool(func.metadata.get(OVERLOAD_TARGET_METADATA)) if is_native_c_abi and not is_overload: @@ -2014,6 +2076,20 @@ def _identity_decorators( decorators.append(f"{indent}@{context.contract('bind')}({json.dumps(str(bind_target))})") return decorators + @staticmethod + def _constructor_binds_its_own_type( + overload_name: str, + bind_target: object | None, + context: _PyiEmissionContext, + ) -> bool: + """Return whether a constructor's link name simply repeats its class name.""" + return bool( + bind_target + and overload_name == "__init__" + and context.public_namespace + and str(bind_target).casefold() == str(context.public_namespace[-1]).casefold() + ) + @staticmethod def _bind_target( func: SemanticFunction, @@ -2149,7 +2225,16 @@ def _with_descriptor_projections( @staticmethod def _native_result_projection(func: SemanticFunction) -> ProjectionMapping | None: - """Return the explicit native scalar descriptor function-result mapping.""" + """Return an exact scalar cast or descriptor function-result mapping.""" + native_cast = ( + func.return_type.metadata.get(NATIVE_C_SCALAR_CAST_METADATA) if func.return_type is not None else None + ) + if isinstance(native_cast, str): + return ProjectionMapping( + result_position=0, + value={"kind": "return", "position": 0}, + native_cast=native_cast, + ) descriptor = PyiPrinter._scalar_descriptor_kind(func.return_type) if descriptor is None: return None @@ -2218,34 +2303,60 @@ def _native_call( projection: list[ProjectionMapping], context: _PyiEmissionContext, native_result: ProjectionMapping | None = None, + func: SemanticFunction | None = None, ) -> str: """Handle native call for the current generation context.""" entries = ", ".join( - self._native_projection_entry(mapping, context) + self._native_projection_entry(mapping, context, func) for mapping in sorted( projection, key=lambda item: item.native_position if item.native_position is not None else -1 ) ) suffix = "" if native_result is not None: - suffix = f", result={self._native_projection_value(native_result, context)}" + suffix = f", result={self._native_projection_entry(native_result, context)}" return f"@{context.contract('native_call')}([{entries}]{suffix})" def _native_projection_entry( self, mapping: ProjectionMapping, context: _PyiEmissionContext, + func: SemanticFunction | None = None, ) -> str: """Handle native projection entry for the current generation context.""" if mapping.value_kind: return self._native_projection_value(mapping, context) if mapping.python_position is not None: - return f"{context.contract('Arg')}({mapping.python_position})" - if mapping.result_position is not None: + rendered = f"{context.contract('Arg')}({mapping.python_position})" + elif (hidden := self._hidden_projection_entry(mapping, context, func)) is not None: + rendered = hidden + elif mapping.result_position is not None: if mapping.native_name: - return f"{context.contract('Return')}({mapping.native_name!r}, {mapping.result_position})" - return f"{context.contract('Return')}({mapping.result_position})" - raise ValueError("native_call cannot represent a native-only projection entry") + rendered = f"{context.contract('Return')}({mapping.native_name!r}, {mapping.result_position})" + else: + rendered = f"{context.contract('Return')}({mapping.result_position})" + else: + raise ValueError("native_call cannot represent a native-only projection entry") + if mapping.native_cast is not None: + return f"{context.contract(mapping.native_cast)}({rendered})" + return rendered + + def _hidden_projection_entry( + self, + mapping: ProjectionMapping, + context: _PyiEmissionContext, + func: SemanticFunction | None, + ) -> str | None: + """Spell one decorator-consumed output as a typed ``Hidden`` slot.""" + if func is None or mapping.result_position is None: + return None + name = mapping.python_name or mapping.native_name + if name not in self._unreturned_output_names(func): + return None + argument = next((item for item in func.arguments if item.name == name), None) + if argument is None: + return None + return f"{context.contract('Hidden')}({name!r}, {self._visit(argument.semantic_type, context)})" def _native_projection_value( self, @@ -2254,7 +2365,10 @@ def _native_projection_value( ) -> str: """Handle native projection value for the current generation context.""" if mapping.value_kind == "addr": - return f"{context.contract('Addr')}({self._native_value_ref(mapping.value, context)})" + value = self._native_value_ref(mapping.value, context) + if mapping.native_cast is not None: + value = f"{context.contract(mapping.native_cast)}({value})" + return f"{context.contract('Addr')}({value})" if mapping.value_kind == "value": return f"{context.contract('Value')}({self._native_value_ref(mapping.value, context)})" if mapping.value_kind in {"allocatable", "pointer"}: @@ -2321,6 +2435,8 @@ def _requires_native_call(func: SemanticFunction) -> bool: return True if PyiPrinter._scalar_descriptor_kind(func.return_type) is not None: return True + if func.return_type is not None and func.return_type.metadata.get(NATIVE_C_SCALAR_CAST_METADATA): + return True if any(PyiPrinter._scalar_descriptor_kind(argument.semantic_type) is not None for argument in func.arguments): return True if func.metadata.get(NATIVE_PROJECTION_METADATA) and any( @@ -2352,6 +2468,8 @@ def _is_assignment_passed_object_return(func: SemanticFunction, mapping: Project @staticmethod def _requires_explicit_projection_mapping(mapping: ProjectionMapping) -> bool: """Return whether requires explicit projection mapping.""" + if mapping.native_cast is not None: + return True if mapping.value_kind: return True if mapping.result_position is not None: diff --git a/prik/runtime/native_support/prik_binding.h b/prik/runtime/native_support/prik_binding.h index 7bd91977d..2e7ed8f80 100644 --- a/prik/runtime/native_support/prik_binding.h +++ b/prik/runtime/native_support/prik_binding.h @@ -10,6 +10,7 @@ #include #include #include +#include #include "numpy_version.h" @@ -74,6 +75,26 @@ typedef struct { #endif /* Release descriptor payload and storage at most once while retaining the record. */ +/* Build a Python string from caller-supplied status-message storage. + + The read never passes ``capacity`` because a native writer is not obliged to + terminate. When it did terminate, the bytes are taken exactly as written; + when it did not, the storage is fixed-length padded (Fortran blank-pads + ``character(len=n)``), so trailing blanks and NULs are dropped. */ +static inline PyObject *prik_status_message_text(const char *bytes, Py_ssize_t capacity) +{ + const char *terminator = (const char *)memchr(bytes, 0, (size_t)capacity); + Py_ssize_t length = capacity; + if (terminator != NULL) { + return PyUnicode_FromStringAndSize(bytes, (Py_ssize_t)(terminator - bytes)); + } + while (length > 0 && (bytes[length - 1] == ' ' || bytes[length - 1] == '\0')) { + length -= 1; + } + return PyUnicode_FromStringAndSize(bytes, length); +} + + static inline void prik_native_array_handle_release(prik_native_array_handle *handle) { void *descriptor; @@ -381,8 +402,8 @@ PRIK_NO_INLINE static int prik_array_actual_unpack( * generated wrapper supplies completed policy selectors and retains its * call-local shape and ABI-field lowering. */ -static inline int prik_array_validate( - PyObject *value, +static inline int prik_array_validate_ndarray( + PyArrayObject *array, int numpy_type, int minimum_rank, int maximum_rank, @@ -392,7 +413,6 @@ static inline int prik_array_validate( const char *python_type, const char *argument_name) { - PyArrayObject *array; int axis; int rank; const char *expected_order; @@ -405,16 +425,6 @@ static inline int prik_array_validate( PyErr_SetString(PyExc_RuntimeError, "prik generated invalid NumPy-array validation selectors"); return -1; } - if (!PyArray_Check(value)) { - PyErr_Format( - PyExc_TypeError, - "Expected a compatible numpy.ndarray of dtype %s for argument %s. Received ", - python_type, - argument_name, - Py_TYPE(value)->tp_name); - return -1; - } - array = (PyArrayObject *)value; rank = PyArray_NDIM(array); if (PyArray_TYPE(array) != numpy_type || rank < minimum_rank || rank > maximum_rank) { PyErr_Format( @@ -422,7 +432,7 @@ static inline int prik_array_validate( "Expected a compatible numpy.ndarray of dtype %s for argument %s. Received ", python_type, argument_name, - Py_TYPE(value)->tp_name); + Py_TYPE((PyObject *)array)->tp_name); return -1; } if (layout == PRIK_ARRAY_LAYOUT_POSITIVE_STRIDED_F) { @@ -480,6 +490,39 @@ static inline int prik_array_validate( return 0; } +/* Validate an arbitrary Python argument before entering the shared ndarray core. */ +static inline int prik_array_validate( + PyObject *value, + int numpy_type, + int minimum_rank, + int maximum_rank, + int layout, + int require_contiguous, + int require_writeable, + const char *python_type, + const char *argument_name) +{ + if (!PyArray_Check(value)) { + PyErr_Format( + PyExc_TypeError, + "Expected a compatible numpy.ndarray of dtype %s for argument %s. Received ", + python_type, + argument_name, + Py_TYPE(value)->tp_name); + return -1; + } + return prik_array_validate_ndarray( + (PyArrayObject *)value, + numpy_type, + minimum_rank, + maximum_rank, + layout, + require_contiguous, + require_writeable, + python_type, + argument_name); +} + /* Exact typed scalar input conversion. A mismatch deliberately sets no error. */ static inline int prik_bool_unpack_exact(PyObject *value, bool *destination) { @@ -524,10 +567,17 @@ static inline int prik_int32_unpack_exact(PyObject *value, int32_t *destination) static inline int prik_int64_unpack_exact(PyObject *value, int64_t *destination) { +#if NPY_SIZEOF_LONG == 8 + if (!PyArray_IsScalar(value, Long)) { + return -1; + } + *destination = (int64_t)PyArrayScalar_VAL(value, Long); +#else if (!PyArray_IsScalar(value, Int64)) { return -1; } *destination = (int64_t)PyArrayScalar_VAL(value, Int64); +#endif return 0; } @@ -790,6 +840,268 @@ static inline PyObject *prik_complex128_to_numpy(const double complex *value) return result; } +/* Unsigned-integer and extended-precision scalar conversions. + + NumPy dropped the ``Intp`` scalar tag, so ``size_t`` selects the fixed-width + tag that matches the target's pointer width instead. */ + +static inline int prik_uint8_unpack_exact(PyObject *value, uint8_t *destination) +{ + if (!PyArray_IsScalar(value, UByte)) { + return -1; + } + *destination = (uint8_t)PyArrayScalar_VAL(value, UByte); + return 0; +} + +static inline int prik_uint8_unpack(PyObject *value, uint8_t *destination) +{ + if (PyArray_IsScalar(value, UByte)) { + PyArray_ScalarAsCtype(value, destination); + } else { + *destination = (uint8_t)PyLong_AsUnsignedLong(value); + } + return PyErr_Occurred() == NULL ? 0 : -1; +} + +static inline PyObject *prik_uint8_to_python(const uint8_t *value) +{ + return PyLong_FromUnsignedLong(*value); +} + +static inline PyObject *prik_uint8_to_numpy(const uint8_t *value) +{ + PyObject *result = PyArrayScalar_New(UByte); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, UByte, (npy_uint8)*value); + } + return result; +} + +static inline int prik_uint16_unpack_exact(PyObject *value, uint16_t *destination) +{ + if (!PyArray_IsScalar(value, UShort)) { + return -1; + } + *destination = (uint16_t)PyArrayScalar_VAL(value, UShort); + return 0; +} + +static inline int prik_uint16_unpack(PyObject *value, uint16_t *destination) +{ + if (PyArray_IsScalar(value, UShort)) { + PyArray_ScalarAsCtype(value, destination); + } else { + *destination = (uint16_t)PyLong_AsUnsignedLong(value); + } + return PyErr_Occurred() == NULL ? 0 : -1; +} + +static inline PyObject *prik_uint16_to_python(const uint16_t *value) +{ + return PyLong_FromUnsignedLong(*value); +} + +static inline PyObject *prik_uint16_to_numpy(const uint16_t *value) +{ + PyObject *result = PyArrayScalar_New(UShort); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, UShort, (npy_uint16)*value); + } + return result; +} + +static inline int prik_uint32_unpack_exact(PyObject *value, uint32_t *destination) +{ + if (!PyArray_IsScalar(value, UInt)) { + return -1; + } + *destination = (uint32_t)PyArrayScalar_VAL(value, UInt); + return 0; +} + +static inline int prik_uint32_unpack(PyObject *value, uint32_t *destination) +{ + if (PyArray_IsScalar(value, UInt)) { + PyArray_ScalarAsCtype(value, destination); + } else { + *destination = (uint32_t)PyLong_AsUnsignedLong(value); + } + return PyErr_Occurred() == NULL ? 0 : -1; +} + +static inline PyObject *prik_uint32_to_python(const uint32_t *value) +{ + return PyLong_FromUnsignedLong(*value); +} + +static inline PyObject *prik_uint32_to_numpy(const uint32_t *value) +{ + PyObject *result = PyArrayScalar_New(UInt); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, UInt, (npy_uint32)*value); + } + return result; +} + +static inline int prik_uint64_unpack_exact(PyObject *value, uint64_t *destination) +{ +#if NPY_SIZEOF_LONG == 8 + if (!PyArray_IsScalar(value, ULong)) { + return -1; + } + *destination = (uint64_t)PyArrayScalar_VAL(value, ULong); +#else + if (!PyArray_IsScalar(value, ULongLong)) { + return -1; + } + *destination = (uint64_t)PyArrayScalar_VAL(value, ULongLong); +#endif + return 0; +} + +static inline int prik_uint64_unpack(PyObject *value, uint64_t *destination) +{ + if (PyArray_IsScalar(value, ULongLong)) { + PyArray_ScalarAsCtype(value, destination); + } else { + *destination = (uint64_t)PyLong_AsUnsignedLongLong(value); + } + return PyErr_Occurred() == NULL ? 0 : -1; +} + +static inline PyObject *prik_uint64_to_python(const uint64_t *value) +{ + return PyLong_FromUnsignedLongLong(*value); +} + +static inline PyObject *prik_uint64_to_numpy(const uint64_t *value) +{ + PyObject *result = PyArrayScalar_New(ULongLong); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, ULongLong, (npy_uint64)*value); + } + return result; +} + +static inline int prik_uintp_unpack_exact(PyObject *value, size_t *destination) +{ +#if NPY_SIZEOF_INTP == 8 + if (!PyArray_IsScalar(value, ULongLong)) { + return -1; + } + *destination = (size_t)PyArrayScalar_VAL(value, ULongLong); +#else + if (!PyArray_IsScalar(value, UInt)) { + return -1; + } + *destination = (size_t)PyArrayScalar_VAL(value, UInt); +#endif + return 0; +} + +static inline int prik_uintp_unpack(PyObject *value, size_t *destination) +{ +#if NPY_SIZEOF_INTP == 8 + if (PyArray_IsScalar(value, ULongLong)) { +#else + if (PyArray_IsScalar(value, UInt)) { +#endif + PyArray_ScalarAsCtype(value, destination); + } else { + *destination = (size_t)PyLong_AsUnsignedLongLong(value); + } + return PyErr_Occurred() == NULL ? 0 : -1; +} + +static inline PyObject *prik_uintp_to_python(const size_t *value) +{ + return PyLong_FromUnsignedLongLong((unsigned long long)*value); +} + +static inline PyObject *prik_uintp_to_numpy(const size_t *value) +{ +#if NPY_SIZEOF_INTP == 8 + PyObject *result = PyArrayScalar_New(ULongLong); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, ULongLong, (npy_uint64)*value); + } +#else + PyObject *result = PyArrayScalar_New(UInt); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, UInt, (npy_uint32)*value); + } +#endif + return result; +} + +static inline int prik_longdouble_unpack_exact(PyObject *value, long double *destination) +{ + if (!PyArray_IsScalar(value, LongDouble)) { + return -1; + } + *destination = (long double)PyArrayScalar_VAL(value, LongDouble); + return 0; +} + +static inline int prik_longdouble_unpack(PyObject *value, long double *destination) +{ + if (PyArray_IsScalar(value, LongDouble)) { + PyArray_ScalarAsCtype(value, destination); + } else { + *destination = (long double)PyFloat_AsDouble(value); + } + return PyErr_Occurred() == NULL ? 0 : -1; +} + +static inline PyObject *prik_longdouble_to_python(const long double *value) +{ + return PyFloat_FromDouble((double)*value); +} + +static inline PyObject *prik_longdouble_to_numpy(const long double *value) +{ + PyObject *result = PyArrayScalar_New(LongDouble); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, LongDouble, (npy_longdouble)*value); + } + return result; +} + +static inline int prik_clongdouble_unpack_exact(PyObject *value, long double complex *destination) +{ + if (!PyArray_IsScalar(value, CLongDouble)) { + return -1; + } + *destination = (long double complex)PyArrayScalar_VAL(value, CLongDouble); + return 0; +} + +static inline int prik_clongdouble_unpack(PyObject *value, long double complex *destination) +{ + if (PyArray_IsScalar(value, CLongDouble)) { + PyArray_ScalarAsCtype(value, destination); + } else { + Py_complex parsed = PyComplex_AsCComplex(value); + *destination = (long double)parsed.real + (long double)parsed.imag * _Complex_I; + } + return PyErr_Occurred() == NULL ? 0 : -1; +} + +static inline PyObject *prik_clongdouble_to_python(const long double complex *value) +{ + return PyComplex_FromDoubles((double)creall(*value), (double)cimagl(*value)); +} + +static inline PyObject *prik_clongdouble_to_numpy(const long double complex *value) +{ + PyObject *result = PyArrayScalar_New(CLongDouble); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, CLongDouble, (npy_clongdouble)*value); + } + return result; +} + /* Release a bridge-owned allocation transferred through a NumPy base capsule. */ static inline void prik_release_owned_memory(PyObject *capsule) { diff --git a/prik/semantics/README.md b/prik/semantics/README.md index f8493f18b..16682e618 100644 --- a/prik/semantics/README.md +++ b/prik/semantics/README.md @@ -110,6 +110,6 @@ completion remains the next shared stage after those converters produce - Source navigation: `docs/developer/codebase-map.md`, `docs/developer/feature-to-code-map.md` - Architecture: `docs/developer/architecture.md` - Semantics package guide: `docs/developer/packages/semantics.md` -- Semantic tests: `tests/fortran/semantic_ir/semantics/` -- `.pyi` tests: `tests/fortran/semantic_pyi_format/` +- Semantic tests: `tests/fortran/infrastructure/semantic_ir/semantics/` +- `.pyi` tests: `tests/fortran/infrastructure/semantic_pyi/` - Wrapper behavior that reaches the typed plan: `tests/fortran/` diff --git a/prik/semantics/__init__.py b/prik/semantics/__init__.py index 958c9d33b..f9959d159 100644 --- a/prik/semantics/__init__.py +++ b/prik/semantics/__init__.py @@ -24,6 +24,7 @@ c_project_to_semantic_modules, c_struct_to_semantic_class, c_type_to_semantic_type, + select_c_export_functions, ) from .pyi2ir import convert_pyi_to_ir @@ -43,4 +44,5 @@ "fortran_module_to_semantic_module", "fortran_project_to_semantic_modules", "resolve_semantic_compile_time_values", + "select_c_export_functions", ) diff --git a/prik/semantics/c2ir.py b/prik/semantics/c2ir.py index fe72810ea..962b33154 100644 --- a/prik/semantics/c2ir.py +++ b/prik/semantics/c2ir.py @@ -9,10 +9,13 @@ from __future__ import annotations import ast +from collections.abc import Iterable import re from pathlib import Path from typing import Any +from prik.contracts import NATIVE_C_SCALAR_CASTS +from prik.semantics.metadata import EXPLICIT_C_EXPORT_METADATA, NATIVE_C_SCALAR_CAST_METADATA from prik.semantics.scalar_types import BOOLEAN_STORAGE_BITS from prik.parsers.c.models import ( @@ -77,6 +80,7 @@ _IDENTIFIER_RE = re.compile(r"[^0-9A-Za-z_]+") _C_IDENTIFIER_TOKEN_RE = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\b") +_C_EXPORT_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z") _C_INTEGER_LITERAL_SUFFIX_RE = re.compile(r"(?&|^~()\s]+") @@ -151,6 +155,33 @@ CLongDoubleComplex: "long double _Complex", } +_PRIMITIVE_NATIVE_CAST_NAMES = { + primitive: next(name for name, spelling in NATIVE_C_SCALAR_CASTS.items() if spelling == c_spelling) + for primitive, c_spelling in _PRIMITIVE_TYPE_FACT_NAMES.items() +} + +_CANONICAL_C_TYPE_FACT_NAMES = { + "Bool": "_Bool", + "Bool8": "_Bool", + "Bool16": "_Bool", + "Bool32": "_Bool", + "Bool64": "_Bool", + "Int8": "int8_t", + "Int16": "int16_t", + "Int32": "int32_t", + "Int64": "int64_t", + "UInt8": "uint8_t", + "UInt16": "uint16_t", + "UInt32": "uint32_t", + "UInt64": "uint64_t", + "Float32": "float", + "Float64": "double", + "Float128": "long double", + "Complex64": "float _Complex", + "Complex128": "double _Complex", + "Complex256": "long double _Complex", +} + _STANDARD_TYPE_FALLBACKS = { "bool": "Bool", "size_t": "SizeT", @@ -368,6 +399,18 @@ def _visit_CFunction(self, function: CFunction) -> SemanticFunction: "specifiers": list(function.specifiers), "prototype_style": function.prototype_style, "is_definition": function.is_definition, + # This is source provenance, not a wrapper decision. Policy + # consumes it later to choose an exact direct C declaration or a + # documented blocker before planning starts. + "c_abi": { + "calling_convention": "c", + "variadic": function.is_variadic, + "result": self._c_abi_type_facts(function.result_type), + "parameters": [ + self._c_abi_type_facts(parameter.declared_type or parameter.type, name=parameter.name) + for parameter in function.parameters + ], + }, } return SemanticFunction( name=function.name, @@ -380,6 +423,7 @@ def _visit_CFunction(self, function: CFunction) -> SemanticFunction: native_name=parameter.name or argument.name, native_position=index, python_position=index, + native_cast=argument.semantic_type.metadata.get(NATIVE_C_SCALAR_CAST_METADATA), ) for index, (parameter, argument) in enumerate(zip(function.parameters, arguments, strict=False)) ], @@ -711,7 +755,10 @@ def _visit_CFunctionType(self, type_: CFunctionType, **_context) -> SemanticType return self._callback_placeholder(type_) def _visit_CUnknownType(self, type_: CUnknownType, *, owner: str | None = None, **_context) -> SemanticType: - """Preserve an unresolved C spelling as an explicit semantic type.""" + """Resolve a probed standard typedef or preserve an unknown C spelling.""" + standard = self._standard_semantic_type(type_.spelling) + if standard is not None: + return standard return self._unresolved_type(type_.spelling, owner=owner, source_type=self._type_text(type_)) def _visit_CVoid(self, type_: CVoid, **_context) -> SemanticType: @@ -772,6 +819,9 @@ def _primitive_type(self, type_: CType, *, owner: str | None) -> SemanticType: metadata["c_primitive"] = "int" metadata["c_type_fact"] = fact metadata["c_type_fact_source"] = fact_source + native_cast = self._required_native_scalar_cast(type_, dtype) + if native_cast is not None: + metadata[NATIVE_C_SCALAR_CAST_METADATA] = native_cast return SemanticType( name=semantic_name, dtype=dtype, @@ -779,6 +829,45 @@ def _primitive_type(self, type_: CType, *, owner: str | None) -> SemanticType: origin=origin, ) + def _required_native_scalar_cast(self, type_: CType, semantic_name: str) -> str | None: + """Return the exact C primitive marker when canonical storage is a distinct C type.""" + if not self.standard_type_facts: + return None + primitive_name = _PRIMITIVE_TYPE_FACT_NAMES.get(type(type_)) + native_cast = _PRIMITIVE_NATIVE_CAST_NAMES.get(type(type_)) + canonical_name = _CANONICAL_C_TYPE_FACT_NAMES.get(semantic_name) + if primitive_name is None or native_cast is None or canonical_name is None: + return None + source_fact = self.standard_type_facts.get(primitive_name) + canonical_fact = self.standard_type_facts.get(canonical_name) + if not isinstance(source_fact, dict) or not isinstance(canonical_fact, dict): + return None + source_spelling = self._underlying_c_type(primitive_name) + canonical_spelling = self._underlying_c_type(canonical_name) + return None if self._compatible_c_scalar_spelling(source_spelling, canonical_spelling) else native_cast + + def _underlying_c_type(self, name: str) -> str: + fact = self.standard_type_facts.get(name) + if isinstance(fact, dict): + underlying = fact.get("underlying_c_type") + if isinstance(underlying, str) and underlying: + return underlying + return name + + @staticmethod + def _compatible_c_scalar_spelling(left: str, right: str) -> bool: + """Compare equivalent builtin spellings without collapsing distinct integer types.""" + aliases = { + "bool": "_Bool", + "signed": "int", + "signed int": "int", + "unsigned": "unsigned int", + "float complex": "float _Complex", + "double complex": "double _Complex", + "long double complex": "long double _Complex", + } + return aliases.get(left, left) == aliases.get(right, right) + def _return_type(self, type_: CType, *, owner: str) -> SemanticType | None: """Convert a function result, using ``None`` for by-value C ``void``.""" if isinstance(type_, CVoid): @@ -1674,6 +1763,36 @@ def _type_text(type_: CType) -> str: return type_.reference_name return type(type_).__name__ + @classmethod + def _c_abi_type_facts(cls, type_: CType, *, name: str | None = None) -> dict[str, object]: + """Return the exact source facts needed by direct-C policy. + + A declaration name is removed only from the final declarator position; + the preserved spelling retains all qualifiers, pointer levels, and C + complex/typedef spelling. Arrays and function pointers stay marked as + source facts so policy can reject them without lowering a partial ABI. + """ + source_spelling = cls._type_text(type_) + if name: + source_spelling = re.sub( + rf"\b{re.escape(name)}\b(?=\s*(?:\[[^]]*\]\s*)*$)", + "", + source_spelling, + ).strip() + components = list(type_.components) if isinstance(type_, CComposedType) else [type_] + pointer_components = [component for component in components if isinstance(component, CPointer)] + qualifiers = tuple( + qualifier.spelling for component in components for qualifier in getattr(component, "qualifiers", ()) + ) + return { + "source_spelling": source_spelling, + "pointer_depth": len(pointer_components), + "qualifiers": qualifiers, + "const": "const" in qualifiers, + "has_array_declarator": any(isinstance(component, CArray) for component in components), + "has_function_pointer": any(isinstance(component, CFunctionType) for component in components), + } + @staticmethod def _type_metadata(type_: CType) -> dict[str, Any]: """Return parser model kind and direct qualifier facts for a semantic type origin.""" @@ -1827,6 +1946,126 @@ def c_project_to_semantic_modules( return CToIRConverter(standard_type_report=standard_type_report).visit(project) +def select_c_export_functions( + modules: Iterable[SemanticModule], + symbols: Iterable[str], +) -> list[SemanticModule]: + """Restrict C semantic IR to an exact, fail-closed function allowlist. + + The selection happens after ordinary include exposure has recorded source + provenance and before policy completion. Selected functions receive one + explicit-export marker so a declaration from an included system header is + intentionally treated as part of the wrapped translation unit. Every + other declaration category is removed from the selected semantic surface. + """ + selected_modules = list(modules) + requested = _validated_c_export_symbols(symbols) + functions_by_symbol, non_function_symbols = _c_export_candidates(selected_modules) + _validate_c_export_resolution(requested, functions_by_symbol, non_function_symbols) + selected = set(requested) + for module in selected_modules: + _apply_c_export_selection(module, selected) + return selected_modules + + +def _validated_c_export_symbols(symbols: Iterable[str]) -> tuple[str, ...]: + """Return unique C identifiers or raise one request-level diagnostic.""" + requested = tuple(symbols) + if not requested: + raise ValueError("C export-symbol selection requires at least one function name") + invalid = [symbol for symbol in requested if _C_EXPORT_IDENTIFIER_RE.fullmatch(symbol) is None] + seen: set[str] = set() + repeated = [] + for symbol in requested: + if symbol in seen and symbol not in repeated: + repeated.append(symbol) + seen.add(symbol) + problems = tuple( + problem + for problem in ( + _c_export_problem("invalid C identifiers", invalid), + _c_export_problem("repeated names", repeated), + ) + if problem is not None + ) + if problems: + raise ValueError("C export-symbol selection failed: " + "; ".join(problems)) + return requested + + +def _c_export_problem(label: str, names: Iterable[str]) -> str | None: + """Format one populated export-selection problem category.""" + values = tuple(names) + return f"{label}: {', '.join(values)}" if values else None + + +def _c_export_candidates( + modules: Iterable[SemanticModule], +) -> tuple[dict[str, list[SemanticFunction]], set[str]]: + """Index reachable functions and names from all other declaration kinds.""" + functions_by_symbol: dict[str, list[SemanticFunction]] = {} + non_function_symbols: set[str] = set() + for module in modules: + for function in module.functions: + symbol = _c_function_symbol(function) + functions_by_symbol.setdefault(symbol, []).append(function) + for declaration in (*module.variables, *module.classes, *module.prototypes, *module.overload_sets): + if symbol := _c_non_function_symbol(declaration): + non_function_symbols.add(symbol) + return functions_by_symbol, non_function_symbols + + +def _c_function_symbol(function: SemanticFunction) -> str: + """Return the exact native lookup key for one C semantic function.""" + return str(function.origin.native_name or function.native_name or function.name) + + +def _c_non_function_symbol(declaration: object) -> str | None: + """Return one non-function declaration name when it has one.""" + name = getattr(declaration, "name", None) + origin = getattr(declaration, "origin", None) + native_name = getattr(origin, "native_name", None) + return str(native_name or name) if native_name or name else None + + +def _validate_c_export_resolution( + requested: tuple[str, ...], + functions_by_symbol: dict[str, list[SemanticFunction]], + non_function_symbols: set[str], +) -> None: + """Fail unless every requested name identifies exactly one function.""" + missing = [ + symbol for symbol in requested if symbol not in functions_by_symbol and symbol not in non_function_symbols + ] + non_functions = [ + symbol for symbol in requested if symbol not in functions_by_symbol and symbol in non_function_symbols + ] + ambiguous = [symbol for symbol in requested if len(functions_by_symbol.get(symbol, ())) > 1] + problems = tuple( + problem + for problem in ( + _c_export_problem("unknown names", missing), + _c_export_problem("non-function names", non_functions), + _c_export_problem("ambiguous function names", ambiguous), + ) + if problem is not None + ) + if problems: + raise ValueError("C export-symbol selection failed: " + "; ".join(problems)) + + +def _apply_c_export_selection(module: SemanticModule, selected: set[str]) -> None: + """Promote selected functions and clear every other declaration category.""" + module.functions = [function for function in module.functions if _c_function_symbol(function) in selected] + for function in module.functions: + function.visibility = "public" + function.metadata[EXPLICIT_C_EXPORT_METADATA] = True + module.prototypes = [] + module.overload_sets = [] + module.classes = [] + module.variables = [] + + def c_project_to_semantic_module( project: CProject, *, @@ -1855,6 +2094,7 @@ def c_project_to_semantic_module( "c_project_to_semantic_modules", "c_struct_to_semantic_class", "c_type_to_semantic_type", + "select_c_export_functions", ) diff --git a/prik/semantics/fortran2ir.py b/prik/semantics/fortran2ir.py index 120a94084..85999dd01 100644 --- a/prik/semantics/fortran2ir.py +++ b/prik/semantics/fortran2ir.py @@ -47,6 +47,8 @@ ) from prik.semantics.ownership_metadata import set_ownership_metadata from prik.semantics.metadata import ( + CONSTRUCTOR_SPECIFIC_METADATA, + DEFERRED_BINDING_METADATA, BIND_TARGET_METADATA, OPTIONAL_ABSENT_HANDLE_METADATA, PROJECTED_OUTPUT_METADATA, @@ -270,6 +272,7 @@ def __init__( compile_time_values: dict[str, int | str] | None = None, wrapped_derived_types: Iterable[tuple[str, str]] | None = None, type_facts: dict[tuple[str, str | None], dict[str, object]] | None = None, + assume_intent_in_scalars: bool = False, ): """Configure parser-fact conversion without performing any conversion. @@ -278,7 +281,15 @@ def __init__( ``wrapped_derived_types`` marks imported types with generated wrappers; and ``type_facts`` supplies compiler-measured storage facts. Inputs are normalized into lookup-friendly forms and retained for later visitors. + + ``assume_intent_in_scalars`` replaces the conservative ``intent(inout)`` + default with ``intent(in)`` for primitive scalar dummies that declare no + ``intent`` at all. It is a caller assertion about sources that predate + the attribute, not a fact derived from the source, so it stays off by + default and never applies to a declared ``intent``. """ + self.assume_intent_in_scalars = bool(assume_intent_in_scalars) + self._abstract_type_names: set[str] = set() self.type_map = FORTRAN_TYPE_MAP if type_map is None else type_map self.compile_time_values = _normalize_compile_time_values(compile_time_values) self.wrapped_derived_types = { @@ -449,6 +460,8 @@ def _convert_variable_type( metadata["fortran_allocatable"] = True if getattr(var, "polymorphic", False): metadata["fortran_polymorphic"] = True + if semantic_name.casefold() in self._abstract_type_names: + metadata["fortran_abstract_type"] = True if getattr(var, "target", False): metadata["aliased"] = True metadata["fortran_target"] = True @@ -528,7 +541,11 @@ def _visit_FortranArgument( derived_type_context=derived_type_context, declaration_arrays=declaration_arrays, ) - access = self._argument_access(arg, semantic_type) + access = self._argument_access( + arg, + semantic_type, + assume_intent_in_scalars=self.assume_intent_in_scalars, + ) self._complete_argument_storage(arg, semantic_type, access=access) self._apply_argument_ownership(semantic_type, writes_argument=access[1]) @@ -946,7 +963,11 @@ def _visit_FortranProcedureSignature( native_name=proc.name, arguments=arguments, return_type=return_type, - projection=self._procedure_projection(proc, arguments), + projection=self._procedure_projection( + proc, + arguments, + assume_intent_in_scalars=self.assume_intent_in_scalars, + ), metadata=metadata, visibility=visibility, origin=SemanticOrigin( @@ -966,6 +987,7 @@ def _visit_FortranDerivedType( procedure_lookup: dict[str, SemanticFunction] | None = None, *, derived_type_context: _DerivedTypeContext | None = None, + prototype_lookup: dict[str, SemanticFunction] | None = None, ) -> SemanticClass: """Convert a Fortran derived type into fields, bound methods, and overload sets. @@ -974,11 +996,12 @@ def _visit_FortranDerivedType( declaration facts for later semantic and printing stages. """ lookup = procedure_lookup or {} + prototypes = prototype_lookup or {} context = derived_type_context or _DerivedTypeContext( module=dtype.module, local_types=frozenset({dtype.name.lower()}), ) - methods = self._bound_methods(dtype, lookup) + methods = self._bound_methods(dtype, lookup, prototypes) overload_sets = self._bound_overload_sets(dtype, methods) type_attributes = list(dict.fromkeys(str(attr).casefold() for attr in dtype.attributes)) metadata = { @@ -1044,6 +1067,14 @@ def _derived_type_component_fact(field: FortranArgument) -> dict[str, object]: "target": field.target, } + def _record_abstract_type_names(self, module: FortranModule) -> None: + """Remember which of the module's derived types are declared abstract.""" + self._abstract_type_names |= { + str(dtype.name).casefold() + for dtype in module.derived_types + if any(str(attribute).casefold() == "abstract" for attribute in dtype.attributes) + } + def _visit_FortranModule( self, module: FortranModule, @@ -1058,6 +1089,7 @@ def _visit_FortranModule( later policy completion owns wrapper behavior decisions. """ context = self._module_derived_type_context(module) + self._record_abstract_type_names(module) callback_interfaces = { **(callback_interfaces or {}), **self._callback_interface_lookup(module), @@ -1102,11 +1134,12 @@ def _visit_FortranModule( dtype, procedure_lookup=procedure_lookup, derived_type_context=context, + prototype_lookup={prototype.name.casefold(): prototype for prototype in prototypes}, ) for dtype in module.derived_types ] - for semantic_cls in semantic_classes: - semantic_cls.visibility = self._symbol_visibility(module, semantic_cls.name) + for semantic_cls, dtype in zip(semantic_classes, module.derived_types, strict=True): + semantic_cls.visibility = self._derived_type_visibility(module, dtype) self._record_class_declaration_callables( semantic_cls, self._declaration_callable_context( @@ -1403,6 +1436,7 @@ def _with_additional_wrapped_types( compile_time_values=self.compile_time_values, wrapped_derived_types=merged, type_facts=self.type_facts, + assume_intent_in_scalars=self.assume_intent_in_scalars, ) converter._known_procedures = set(self._known_procedures) return converter @@ -1422,6 +1456,7 @@ def _with_additional_known_procedures( compile_time_values=self.compile_time_values, wrapped_derived_types=self.wrapped_derived_types, type_facts=self.type_facts, + assume_intent_in_scalars=self.assume_intent_in_scalars, ) converter._known_procedures = merged return converter @@ -2099,16 +2134,42 @@ def _apply_pointer_result_policy(semantic_type: SemanticType) -> None: def _argument_access( arg: FortranArgument | FortranVariable, semantic_type: SemanticType, + *, + assume_intent_in_scalars: bool = False, ) -> tuple[bool, bool]: - """Return parser-provided read/write facts or the established conservative default.""" + """Return parser-provided read/write facts or the established conservative default. + + A declared ``intent`` always wins; ``assume_intent_in_scalars`` only + chooses which default an undeclared ``intent`` receives, and only for + the scalars whose replacement value would otherwise be projected as a + Python result. + """ reads = getattr(arg, "reads_argument", None) writes = getattr(arg, "writes_argument", None) if reads is None or writes is None: - if semantic_type.name == "String" and semantic_type.rank == 0: + if assume_intent_in_scalars and FortranToIRConverter._assumed_input_scalar(semantic_type): return True, False return True, True return bool(reads), bool(writes) + @staticmethod + def _assumed_input_scalar(semantic_type: SemanticType | None) -> bool: + """Return whether an undeclared ``intent`` on this dummy may be assumed ``intent(in)``. + + This covers exactly the rank-zero values whose replacement would + otherwise be projected as a Python result: primitive scalars and + non-descriptor character scalars. Descriptor scalars keep the + conservative default because their result is a nullable snapshot + rather than a replacement value. + """ + return bool( + FortranToIRConverter._is_primitive_scalar_replacement(semantic_type) + or ( + FortranToIRConverter._is_scalar_character(semantic_type) + and not FortranToIRConverter._is_scalar_descriptor(semantic_type) + ) + ) + @staticmethod def _argument_has_writable_storage(argument: SemanticArgument) -> bool: """Return whether semantic ownership or storage marks an argument writable.""" @@ -2124,6 +2185,7 @@ def _bound_methods( self, dtype: FortranDerivedType, procedure_lookup: dict[str, SemanticFunction], + prototype_lookup: dict[str, SemanticFunction] | None = None, ) -> list[SemanticMethod]: """Project resolved type-bound procedure bindings into semantic methods. @@ -2140,14 +2202,21 @@ def _bound_methods( binding_name, target_name = self._procedure_binding_names(binding["name"]) proc = procedure_lookup.get(target_name.casefold()) if proc is None: + deferred = self._deferred_bound_method(binding, binding_name, prototype_lookup or {}) + if deferred is not None: + methods.append(deferred) continue binding_attributes = tuple(binding.get("attrs", ())) attrs = set(binding_attributes) - visibility = proc.visibility - if "private" in attrs: + declared_visibility = binding.get("visibility") + if declared_visibility in {"private", "public"}: + visibility = str(declared_visibility) + elif "private" in attrs: visibility = "private" elif "public" in attrs: visibility = "public" + else: + visibility = proc.visibility is_static = "nopass" in attrs passed_object_name, passed_object_position = self._passed_object_argument(proc, binding_attributes) proc.metadata["fortran_type_bound_target"] = True @@ -2213,11 +2282,22 @@ def _module_overload_sets( overload_sets.append(ProcedureOverloadSet(interface.name)) continue if self._is_procedure_generic_name(interface.name): - if interface.name.casefold() in class_map: - raise ValueError( - f"Fortran semantic conversion cannot represent generic constructor " - f"{module.name}.{interface.name!s}; constructor projection is not implemented" - ) + constructor_class = class_map.get(interface.name.casefold()) + if constructor_class is not None: + # An interface named for a derived type is that type's + # constructor, so its specifics become the class's own + # `__init__` overload set rather than a module generic. + constructor_set = self._normal_overload_set("__init__", procedures) + target_lookup = procedure_lookup | inline_lookup + for target_name, candidate in zip(target_names, constructor_set.procedures, strict=True): + if target_lookup[target_name.casefold()].visibility == "private": + # A private specific is unreachable by name; the type + # name is public and resolves to the same procedure. + candidate.native_name = interface.name + candidate.metadata[BIND_TARGET_METADATA] = interface.name + self._merge_overload_sets(constructor_class.overload_sets, [constructor_set]) + self._mark_constructor_specifics(procedures, procedure_lookup, interface.name) + continue overload_set = self._normal_overload_set(interface.name, procedures) target_lookup = procedure_lookup | inline_lookup for target_name, candidate in zip(target_names, overload_set.procedures, strict=True): @@ -2308,6 +2388,23 @@ def _apply_assignment_projection_to_originals( if original is not None: original.projection = self._assignment_projection(original, 0) + @staticmethod + def _mark_constructor_specifics( + procedures: list[SemanticFunction], + procedure_lookup: dict[str, SemanticFunction], + type_name: str, + ) -> None: + """Hide the module functions a generic constructor selects between. + + Each specific stays reachable as the constructor's native target, but it + is no longer published as a separate module procedure: the type name is + the public spelling the source chose for it. + """ + for procedure in procedures: + original = procedure_lookup.get((procedure.native_name or procedure.name).casefold()) + if original is not None: + original.metadata[CONSTRUCTOR_SPECIFIC_METADATA] = type_name + @staticmethod def _merge_overload_sets( overload_sets: list[ProcedureOverloadSet], @@ -2662,6 +2759,50 @@ def _passed_object_argument( f"Type-bound procedure {proc.name!r} declares pass({pass_name}), but that dummy argument is not present" ) + @staticmethod + def _deferred_bound_method( + binding: dict, + binding_name: str, + prototype_lookup: dict[str, SemanticFunction], + ) -> SemanticMethod | None: + """Project a deferred type-bound binding from its declared interface. + + A deferred binding names an interface instead of an implementation, so + the method carries that signature and no native target. Every concrete + extension supplies the override that a caller actually reaches. + """ + interface_name = binding.get("interface") + if not interface_name: + return None + prototype = prototype_lookup.get(str(interface_name).casefold()) + if prototype is None: + return None + attributes = tuple(binding.get("attrs", ())) + passed_object_name, passed_object_position = FortranToIRConverter._passed_object_argument( + prototype, + attributes, + ) + # A prototype spells a subroutine's absent result as the "None" semantic + # type; a method states the same absence by carrying no result at all. + return_type = prototype.return_type + if return_type is not None and return_type.name == "None": + return_type = None + return SemanticMethod( + name=binding_name, + native_name="", + arguments=list(prototype.arguments), + return_type=return_type, + visibility=str(binding.get("visibility", "public")), + is_static="nopass" in set(attributes), + passed_object_name=passed_object_name, + passed_object_position=passed_object_position, + binding_attributes=attributes, + metadata={ + DEFERRED_BINDING_METADATA: True, + "fortran_deferred_interface": str(interface_name), + }, + ) + @staticmethod def _procedure_binding_names(name: str) -> tuple[str, str]: """Split a Fortran binding ``local => target`` spelling into both names.""" @@ -2723,6 +2864,8 @@ def _is_hidden_output_argument( def _procedure_projection( proc: FortranProcedureSignature, arguments: list[SemanticArgument], + *, + assume_intent_in_scalars: bool = False, ) -> list[ProjectionMapping]: """Build native-to-Python argument and result mappings for one procedure. @@ -2737,7 +2880,11 @@ def _procedure_projection( result_position = 1 if proc.result is not None else 0 for native_position, native_arg in enumerate(proc.arguments): arg = by_name[native_arg.name] - reads_argument, writes_argument = FortranToIRConverter._argument_access(native_arg, arg.semantic_type) + reads_argument, writes_argument = FortranToIRConverter._argument_access( + native_arg, + arg.semantic_type, + assume_intent_in_scalars=assume_intent_in_scalars, + ) is_output = writes_argument and not reads_argument is_replacement = reads_argument and writes_argument is_allocatable_replacement = is_replacement and FortranToIRConverter._is_allocatable_array( @@ -2897,6 +3044,21 @@ def _standalone_module_name(parsed_file: FortranFile) -> str: return Path(parsed_file.filename).stem return "standalone" + @staticmethod + def _derived_type_visibility(module: FortranModule, dtype: FortranDerivedType) -> str: + """Resolve a derived type's accessibility, preferring its own declaration. + + ``type, public ::`` and ``type, private ::`` state the type's own + accessibility, so they win over a module-level ``public``/``private`` + default and over the module's accessibility lists. + """ + attributes = {str(attribute).lower() for attribute in getattr(dtype, "attributes", ())} + if "private" in attributes: + return "private" + if "public" in attributes: + return "public" + return FortranToIRConverter._symbol_visibility(module, dtype.name) + @staticmethod def _symbol_visibility(module: FortranModule, symbol_name: str) -> str: """Resolve explicit private/public lists before the module default visibility.""" @@ -3116,6 +3278,26 @@ def fortran_type_storage_expression(base_type: str, kind: str | None = None) -> return f"storage_size({constructor})" +def fortran_type_precision_expression(base_type: str, kind: str | None = None) -> str | None: + """Return the Fortran ``digits`` expression for one floating-point type. + + Storage width alone cannot separate two 128-bit reals: x87 extended + precision and IEEE binary128 share a size and differ only in mantissa + width. Integer and logical types have no such ambiguity and return ``None``. + + Example: + >>> fortran_type_precision_expression("real", "16") + 'digits(real(0.0,kind=16))' + """ + base = str(base_type).lower() + if base not in {"real", "complex"}: + return None + # ``digits`` rejects a complex argument, and a complex kind's components + # are reals of the same kind, so both bases query the real constructor. + constructor = "real(0.0)" if kind is None else f"real(0.0,kind={kind})" + return f"digits({constructor})" + + def collect_fortran_type_storage_requirements( parsed, *, @@ -3151,6 +3333,7 @@ def collect_fortran_type_storage_requirements( "base_type": key[0], "kind": key[1], "expression": fortran_type_storage_expression(*key), + "precision_expression": fortran_type_precision_expression(*key), "unit": context.get("unit"), "symbol": context.get("symbol"), } @@ -3350,6 +3533,7 @@ def _converter_for( compile_time_values: dict[str, int | str] | None = None, wrapped_derived_types: Iterable[tuple[str, str]] | None = None, type_facts: dict[tuple[str, str | None], dict[str, object]] | None = None, + assume_intent_in_scalars: bool = False, ) -> FortranToIRConverter: """Return the shared default converter or an isolated configured converter. @@ -3357,12 +3541,18 @@ def _converter_for( conversion input creates a new instance so per-call compile-time values and facts never leak into unrelated conversions. """ - if compile_time_values is None and wrapped_derived_types is None and type_facts is None: + if ( + compile_time_values is None + and wrapped_derived_types is None + and type_facts is None + and not assume_intent_in_scalars + ): return _DEFAULT_CONVERTER return FortranToIRConverter( compile_time_values=compile_time_values, wrapped_derived_types=wrapped_derived_types, type_facts=type_facts, + assume_intent_in_scalars=assume_intent_in_scalars, ) @@ -3375,6 +3565,7 @@ def fortran_module_to_semantic_module( compile_time_values: dict[str, int | str] | None = None, wrapped_derived_types: Iterable[tuple[str, str]] | None = None, type_facts: dict[tuple[str, str | None], dict[str, object]] | None = None, + assume_intent_in_scalars: bool = False, ) -> SemanticModule: """Convert one parsed Fortran module into a :class:`SemanticModule`. @@ -3394,7 +3585,12 @@ def fortran_module_to_semantic_module( >>> fortran_module_to_semantic_module(parsed).functions[0].arguments[0].semantic_type.name 'Float64' """ - converter = _converter_for(compile_time_values, wrapped_derived_types, type_facts) + converter = _converter_for( + compile_time_values, + wrapped_derived_types, + type_facts, + assume_intent_in_scalars=assume_intent_in_scalars, + ) return converter.visit(converter.first_module(module)) @@ -3405,6 +3601,7 @@ def fortran_file_to_semantic_modules( compile_time_values: dict[str, int | str] | None = None, wrapped_derived_types: Iterable[tuple[str, str]] | None = None, type_facts: dict[tuple[str, str | None], dict[str, object]] | None = None, + assume_intent_in_scalars: bool = False, ) -> list[SemanticModule]: """Convert every module and standalone procedure group in one parsed file. @@ -3417,7 +3614,12 @@ def fortran_file_to_semantic_modules( >>> [module.name for module in fortran_file_to_semantic_modules(parsed)] ['standalone'] """ - return _converter_for(compile_time_values, wrapped_derived_types, type_facts).visit( + return _converter_for( + compile_time_values, + wrapped_derived_types, + type_facts, + assume_intent_in_scalars=assume_intent_in_scalars, + ).visit( parsed_file, standalone_module_name=standalone_module_name, ) @@ -3428,6 +3630,7 @@ def fortran_project_to_semantic_modules( *, compile_time_values: dict[str, int | str] | None = None, type_facts: dict[tuple[str, str | None], dict[str, object]] | None = None, + assume_intent_in_scalars: bool = False, ) -> list[SemanticModule]: """Convert an ordered parsed Fortran project with project-wide type context. @@ -3441,7 +3644,11 @@ def fortran_project_to_semantic_modules( >>> [module.name for module in fortran_project_to_semantic_modules(project)] ['math'] """ - return _converter_for(compile_time_values, type_facts=type_facts).visit(project) + return _converter_for( + compile_time_values, + type_facts=type_facts, + assume_intent_in_scalars=assume_intent_in_scalars, + ).visit(project) if __name__ == "__main__": diff --git a/prik/semantics/metadata.py b/prik/semantics/metadata.py index ba8f633c0..e3888b676 100644 --- a/prik/semantics/metadata.py +++ b/prik/semantics/metadata.py @@ -10,8 +10,13 @@ SCALAR_STORAGE_CATEGORY = "scalar_storage" SUPPRESS_DEFAULT_CONSTRUCTOR_METADATA = "suppress_default_constructor" USER_PRIVATE_METADATA = "user_private" +DEFERRED_BINDING_METADATA = "deferred_binding" +CONSTRUCTOR_SPECIFIC_METADATA = "constructor_specific" NATIVE_PROJECTION_METADATA = "native_projection" +NATIVE_C_SCALAR_CAST_METADATA = "native_c_scalar_cast" +EXPLICIT_C_EXPORT_METADATA = "explicit_c_export" 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" +NULLABLE_ANNOTATION_METADATA = "nullable_annotation" diff --git a/prik/semantics/models.py b/prik/semantics/models.py index 31b8b14f0..993d4dbfc 100644 --- a/prik/semantics/models.py +++ b/prik/semantics/models.py @@ -290,6 +290,7 @@ class ProjectionMapping: result_position: int | None = None value_kind: str = "" value: Any = None + native_cast: str | None = None # ============================================================ @@ -398,8 +399,10 @@ class ProcedureOverloadSet: PYTHON_EXPORTS_METADATA = "python_exports" PYTHON_EXPORTS_PREPARED_METADATA = "python_exports_prepared" POLICY_COMPLETION_PREPARED_METADATA = "policy_completion_prepared" +HIDDEN_NATIVE_OUTPUT_METADATA = "hidden_native_output" RESOLVED_OWNERSHIP_POLICY_METADATA = "resolved_ownership_policy" RESOLVED_RETURN_OWNERSHIP_POLICY_METADATA = "resolved_return_ownership_policy" +RESOLVED_UPDATE_RESULT_OWNERSHIP_POLICY_METADATA = "resolved_update_result_ownership_policy" RESOLVED_CLASS_INSTANCE_POLICY_METADATA = "resolved_class_instance_policy" RESOLVED_CLASS_SELF_POLICY_METADATA = "resolved_class_self_policy" RESOLVED_DERIVED_FIELD_POLICY_METADATA = "resolved_derived_field_policy" @@ -542,6 +545,7 @@ def _projection_key( mapping.result_position, mapping.value_kind, _native_projection_value_key(mapping.value, name_map), + mapping.native_cast, ) for mapping in projection if _requires_explicit_projection_mapping(mapping) @@ -549,6 +553,8 @@ def _projection_key( def _requires_explicit_projection_mapping(mapping: ProjectionMapping) -> bool: + if mapping.native_cast is not None: + return True if mapping.value_kind: return True if mapping.result_position is not None: diff --git a/prik/semantics/native_contract.py b/prik/semantics/native_contract.py index 8012033fb..6c44b3940 100644 --- a/prik/semantics/native_contract.py +++ b/prik/semantics/native_contract.py @@ -48,61 +48,73 @@ def prepare_pyi_native_contract(modules: Iterable[SemanticModule]) -> list[Seman def _prepare_module(module: SemanticModule) -> None: native_scope = module.name - module.origin.source_language = "fortran" + native_language = module.origin.source_language or "fortran" + if native_language not in {"c", "fortran"}: + raise ValueError(f"Unsupported semantic .pyi native language: {native_language!r}") + module.origin.source_language = native_language module.origin.native_name = native_scope module.origin.native_scope = native_scope module.origin.source_kind = "module" for variable in module.variables: - _set_origin(variable, native_scope, "variable") + _set_origin(variable, native_scope, "variable", native_language=native_language) for function in module.functions: - _prepare_function(function, native_scope) + _prepare_function(function, native_scope, native_language=native_language) for prototype in module.prototypes: - _prepare_prototype(prototype, native_scope) + _prepare_prototype(prototype, native_scope, native_language=native_language) for overload_set in module.overload_sets: for procedure in overload_set.procedures: - _prepare_function(procedure, native_scope) + _prepare_function(procedure, native_scope, native_language=native_language) for semantic_class in module.classes: - _prepare_class(semantic_class, native_scope) + _prepare_class(semantic_class, native_scope, native_language=native_language) for semantic_type in _module_semantic_types(module): - semantic_type.origin.source_language = "fortran" + semantic_type.origin.source_language = native_language -def _set_origin(node, native_scope: str | None, source_kind: str) -> None: - node.origin.source_language = "fortran" +def _set_origin(node, native_scope: str | None, source_kind: str, *, native_language: str) -> None: + node.origin.source_language = native_language node.origin.native_name = node.origin.native_name or getattr(node, "native_name", None) or node.name node.origin.native_scope = native_scope node.origin.source_kind = source_kind -def _prepare_function(function: SemanticFunction, native_scope: str) -> None: +def _prepare_function(function: SemanticFunction, native_scope: str, *, native_language: str) -> None: + # Preserve the established Fortran distinction: a declaration already + # marked as Fortran with no scope is external, while an ordinary `.pyi` + # declaration receives its contract module scope. C has no generated + # module adapter and therefore always preserves the user symbol directly. is_external = function.origin.source_language == "fortran" and function.origin.native_scope is None function_scope = None if is_external else native_scope source_kind = "function" if function.return_type is not None else "subroutine" - _set_origin(function, function_scope, source_kind) + _set_origin(function, function_scope, source_kind, native_language=native_language) function.origin.native_name = function.native_name or function.name for argument in function.arguments: - _set_origin(argument, function.origin.native_name, "argument") + _set_origin(argument, function.origin.native_name, "argument", native_language=native_language) -def _prepare_prototype(prototype: SemanticPrototype, native_scope: str) -> None: - _set_origin(prototype, native_scope, "prototype") +def _prepare_prototype(prototype: SemanticPrototype, native_scope: str, *, native_language: str) -> None: + _set_origin(prototype, native_scope, "prototype", native_language=native_language) prototype.origin.native_name = prototype.native_name or prototype.name for argument in prototype.arguments: - _set_origin(argument, prototype.origin.native_name, "prototype_argument") + _set_origin( + argument, + prototype.origin.native_name, + "prototype_argument", + native_language=native_language, + ) -def _prepare_class(semantic_class: SemanticClass, native_scope: str) -> None: - _set_origin(semantic_class, native_scope, "derived_type") +def _prepare_class(semantic_class: SemanticClass, native_scope: str, *, native_language: str) -> None: + _set_origin(semantic_class, native_scope, "derived_type", native_language=native_language) for field in semantic_class.fields: - _set_origin(field, native_scope, "field") + _set_origin(field, native_scope, "field", native_language=native_language) for method in semantic_class.methods: - _prepare_function(method, native_scope) + _prepare_function(method, native_scope, native_language=native_language) for overload_set in semantic_class.overload_sets: for procedure in overload_set.procedures: - _prepare_function(procedure, native_scope) + _prepare_function(procedure, native_scope, native_language=native_language) for nested in semantic_class.classes: - _prepare_class(nested, native_scope) + _prepare_class(nested, native_scope, native_language=native_language) def native_contract_issues(module: SemanticModule) -> list[NativeContractIssue]: diff --git a/prik/semantics/pyi2ir.py b/prik/semantics/pyi2ir.py index 62b1484d0..e0f5dc9f2 100644 --- a/prik/semantics/pyi2ir.py +++ b/prik/semantics/pyi2ir.py @@ -20,7 +20,7 @@ from copy import deepcopy from dataclasses import dataclass, field -from prik.contracts import CONTRACT_SYMBOLS, CONTRACT_TYPE_NAMES +from prik.contracts import CONTRACT_SYMBOLS, CONTRACT_TYPE_NAMES, NATIVE_C_SCALAR_CASTS from prik.utilities.declaration_expressions import ( declaration_expression_calls, is_declaration_expression_helper, @@ -37,8 +37,11 @@ ADDRESS_ROLE_PROJECTION, ADDRESS_ROLE_RAW, BIND_TARGET_METADATA, + DEFERRED_BINDING_METADATA, MAYBE_UNALLOCATED_METADATA, + NATIVE_C_SCALAR_CAST_METADATA, NATIVE_PROJECTION_METADATA, + NULLABLE_ANNOTATION_METADATA, OPTIONAL_ABSENT_HANDLE_METADATA, PROJECTED_OUTPUT_METADATA, SCALAR_STORAGE_CATEGORY, @@ -50,6 +53,7 @@ from prik.semantics.models import ( EXTERNAL_TYPE_REF_METADATA, + HIDDEN_NATIVE_OUTPUT_METADATA, FORTRAN_GENERIC_NAME_METADATA, OVERLOAD_KIND_METADATA, OVERLOAD_TARGET_METADATA, @@ -164,6 +168,8 @@ class _Decorators: error_status_policy: dict[str, object] | None = None prototype: bool = False pure: bool = False + abstract: bool = False + abstract_method: bool = False @dataclass @@ -176,6 +182,10 @@ class _PendingOverload: generic_name: str | None = None +#: Sentinel for a projected result this comparison does not reconstruct. +_UNCOMPARED_PROJECTED_RETURN = object() + + class _PyiAstParser: """Stateful AST visitor that builds one semantic module from a contract. @@ -195,6 +205,10 @@ def __init__(self, *, module_name: str, source: str = "", native_language: str = if native_language not in {"c", "fortran"}: raise ValueError(f"Unsupported semantic .pyi native language: {native_language!r}") self.module = SemanticModule(name=module_name, origin=SemanticOrigin(source_language=native_language)) + # Types declared by ``Hidden(name, T)`` slots, keyed by the mapping they + # came from. They are consumed while the owning callable is built and + # never reach the semantic model. + self._hidden_output_types: dict[int, SemanticType] = {} self.source = source self.native_language = native_language self._pending_overloads: list[_PendingOverload] = [] @@ -431,6 +445,7 @@ def class_def( *, visibility: str, native_type: dict[str, object] | None = None, + abstract: bool = False, ) -> SemanticClass: """Convert one class AST node, its body, and supported native metadata. @@ -445,15 +460,19 @@ def class_def( raise ValueError("Direct constructor bindings replace the generated field constructor; remove one __init__") base_classes = [self.base_class_name(base) for base in node.bases] origin = self._origin( - source_language="fortran" if body.constructor_from_fields or native_type is not None else None, + source_language=( + "fortran" if body.constructor_from_fields or native_type is not None or abstract else None + ), user_private=visibility == "private", ) if not body.constructor_from_fields: origin.metadata[SUPPRESS_DEFAULT_CONSTRUCTOR_METADATA] = True metadata = self._class_metadata(base_classes) + if abstract: + metadata["fortran_type_attributes"] = [*metadata.get("fortran_type_attributes", []), "abstract"] if native_type is not None: - attributes = list(native_type.get("attributes", ())) + attributes = [*metadata.get("fortran_type_attributes", []), *native_type.get("attributes", ())] metadata["fortran_type_attributes"] = attributes normalized_attributes = {str(item).strip().casefold().replace(" ", "") for item in attributes} if "bind(c)" in normalized_attributes: @@ -632,6 +651,7 @@ def method_def( has_native_call: bool = False, release_gil: bool = False, error_status_policy: dict[str, object] | None = None, + deferred: bool = False, ) -> SemanticMethod: """Convert a class stub into a semantic method declaration. @@ -648,6 +668,10 @@ def method_def( drop_untyped_self=True, ) metadata = {BIND_TARGET_METADATA: native_name} if native_name is not None else {} + if deferred: + if native_name is not None: + raise ValueError("A deferred binding has no native target; remove its bind decorator") + metadata[DEFERRED_BINDING_METADATA] = True if has_native_call: metadata[NATIVE_PROJECTION_METADATA] = True passed_object_name, passed_object_position = self._complete_method_passed_object( @@ -846,6 +870,8 @@ def _apply_decorator(self, parsed: _Decorators, node: ast.expr, *, context: str) "native_type": self._apply_native_type_decorator, "prototype": self._apply_prototype_decorator, "pure": self._apply_pure_decorator, + "abstract": self._apply_abstract_decorator, + "abstractmethod": self._apply_abstract_method_decorator, "raises": self._apply_raises_decorator, } handler = next((value for name, value in handlers.items() if self.matches_name(target, name)), None) @@ -853,6 +879,37 @@ def _apply_decorator(self, parsed: _Decorators, node: ast.expr, *, context: str) raise ValueError(f"Unsupported {context} decorator: {ast.unparse(node)!r}") handler(parsed, node, context) + @staticmethod + def _reject_private_constructor(declaration_name: str, visibility: str) -> None: + """Refuse an accessibility marker that a constructor cannot express.""" + if declaration_name == "__init__" and visibility == "private": + raise ValueError( + "A constructor is published or absent; remove @private from __init__. " + "Mark the specific procedure it selects private instead." + ) + + @staticmethod + def _apply_abstract_decorator(parsed: _Decorators, node: ast.expr, context: str) -> None: + """Mark a class as an abstract native type that cannot be constructed.""" + if isinstance(node, ast.Call): + raise ValueError("abstract does not accept arguments") + if context != "class": + raise ValueError("abstract is only valid on a class declaration") + if parsed.abstract: + raise ValueError("Duplicate abstract decorator") + parsed.abstract = True + + @staticmethod + def _apply_abstract_method_decorator(parsed: _Decorators, node: ast.expr, context: str) -> None: + """Mark a type-bound declaration as a deferred binding with no native target.""" + if isinstance(node, ast.Call): + raise ValueError("abstractmethod does not accept arguments") + if context == "class": + raise ValueError("abstractmethod is only valid on a method declaration") + if parsed.abstract_method: + raise ValueError("Duplicate abstractmethod decorator") + parsed.abstract_method = True + @staticmethod def _apply_prototype_decorator(parsed: _Decorators, node: ast.expr, context: str) -> None: """Mark a module-level declaration as an exact native interface.""" @@ -996,8 +1053,15 @@ def native_call(self, node: ast.Call) -> tuple[list[ProjectionMapping], Projecti return projection, native_result def native_result_projection(self, node: ast.AST) -> ProjectionMapping: - """Parse the nullable scalar descriptor returned by a native function.""" + """Parse an exact scalar cast or nullable descriptor native result.""" mapping = self.native_projection_entry(node, native_position=-1) + if mapping.native_cast is not None: + if mapping.result_position is None or mapping.python_position is not None or mapping.value_kind: + raise ValueError("native_call scalar result expects CScalar(Return(0))") + mapping.native_position = None + if mapping.result_position != 0: + raise ValueError("native scalar function result must map to Python result slot 0") + return mapping if mapping.value_kind in {"allocatable", "pointer"} and mapping.python_position is not None: raise ValueError("native_call result must reference Return(i), not Arg(i)") if mapping.value_kind not in {"allocatable", "pointer"} or mapping.result_position is None: @@ -1181,11 +1245,16 @@ def _validate_overload_signature( form. A class overload may instead expose a projected bound-object return; every other mismatch raises ``ValueError``. """ - visible_declaration_arguments = [_PyiAstParser._visible_overload_argument(arg) for arg in declaration.arguments] - visible_call_arguments = [_PyiAstParser._visible_overload_argument(arg) for arg in call_arguments] + projected_arguments = _PyiAstParser._projected_overload_arguments(target, call_arguments) + declared_arguments = _PyiAstParser._projected_overload_arguments(declaration, declaration.arguments) + visible_declaration_arguments = [_PyiAstParser._visible_overload_argument(arg) for arg in declared_arguments] + visible_call_arguments = [_PyiAstParser._visible_overload_argument(arg) for arg in projected_arguments] + target_return = _PyiAstParser._projected_overload_return_type(target) if visible_declaration_arguments == visible_call_arguments and ( _PyiAstParser._visible_overload_type(declaration.return_type) == _PyiAstParser._visible_overload_type(target.return_type) + or target_return is _UNCOMPARED_PROJECTED_RETURN + or _PyiAstParser._matches_projected_return(declaration.return_type, target_return) or _PyiAstParser._matches_bound_projection_return(declaration, target, bound_position) ): return @@ -1194,6 +1263,65 @@ def _validate_overload_signature( f"specific procedure {target.native_name or target.name!r}" ) + @staticmethod + def _matches_projected_return(declared, target_return) -> bool: + """Compare a declared result with a target's, ignoring result ownership.""" + declared_type = _PyiAstParser._visible_overload_type(declared) + target_type = _PyiAstParser._visible_overload_type(target_return) + if declared_type is None or target_type is None: + return declared_type == target_type + expected = deepcopy(target_type) + expected.ownership = deepcopy(declared_type.ownership) + return declared_type == expected + + @staticmethod + def _projected_overload_arguments( + function: SemanticFunction, + arguments: list[SemanticArgument], + ) -> list[SemanticArgument]: + """Return only the arguments one projected signature still accepts. + + An output the projection turns into a result is not part of the public + signature, whether it is a native output argument on the specific or a + further returned value the declaration states. + """ + hidden = { + mapping.native_name + for mapping in function.projection + if mapping.python_position is None and mapping.result_position is not None + } + if not hidden: + return list(arguments) + return [argument for argument in arguments if argument.name not in hidden] + + @staticmethod + def _projected_overload_return_type(target: SemanticFunction): + """Return the result a projected target presents, or the uncompared marker. + + A projection that supplies exactly one result replaces an absent native + return with that argument's type. Several results compose a tuple the + declaration states directly, which this comparison does not rebuild. + """ + results = [mapping for mapping in target.projection if mapping.result_position is not None] + if not results: + return target.return_type + if target.return_type is not None or len(results) != 1: + # Several results compose a tuple the declaration states directly, + # and its extra members arrive as `return_position` arguments that + # the comparison above has already set aside. + return _UNCOMPARED_PROJECTED_RETURN + by_name = {argument.name: argument for argument in target.arguments} + projected = by_name.get(results[0].native_name) + if projected is None: + return _UNCOMPARED_PROJECTED_RETURN + # A projected output is declared as a native output argument; as a result + # it is an ordinary returned value, so its argument-passing storage is + # not part of the public type the declaration states. + returned = deepcopy(projected.semantic_type) + if returned.rank == 0 and returned.storage is not None and returned.storage.kind in {"address", "reference"}: + returned.storage = None + return returned + @staticmethod def _visible_overload_argument(argument: SemanticArgument) -> SemanticArgument: """Copy one overload argument with its type normalized for public comparison.""" @@ -1253,18 +1381,33 @@ def _class_overload_bound_position( ) -> int | None: """Locate the unique native wrapped-object argument for a class overload. - Static methods need no bound object. Instance methods must match one - target argument whose type is the owning class and whose removal leaves - the declared Python arguments in order; ambiguity is an error. + Static methods need no bound object. A constructor candidate produces + the object instead of receiving one, so a specific whose result is the + owning class has no bound argument either. Every other instance method + must match one target argument whose type is the owning class and whose + removal leaves the declared Python arguments in order; ambiguity is an + error. """ if isinstance(declaration, SemanticMethod) and declaration.is_static: return None - remaining_names = [argument.name for argument in declaration.arguments] + if ( + declaration.name == "__init__" + and target.return_type is not None + and target.return_type.name.casefold() == owner.name.casefold() + ): + return None + # Compare public signatures: an output either side projects into a result + # is not one of the arguments a caller supplies. + declared_names = [ + argument.name + for argument in _PyiAstParser._projected_overload_arguments(declaration, declaration.arguments) + ] + visible_target_arguments = _PyiAstParser._projected_overload_arguments(target, target.arguments) matching = [ index for index, argument in enumerate(target.arguments) if argument.semantic_type.name.casefold() == owner.name.casefold() - and [arg.name for pos, arg in enumerate(target.arguments) if pos != index] == remaining_names + and [item.name for item in visible_target_arguments if item is not argument] == declared_names ] if len(matching) == 1: return matching[0] @@ -1398,6 +1541,8 @@ def native_projection_entry(self, node: ast.AST, native_position: int) -> Projec return self.native_address_projection_entry(node, native_position) descriptor = self.contract_name(node.func) + if descriptor in NATIVE_C_SCALAR_CASTS: + return self.native_scalar_cast_projection_entry(node, native_position, descriptor) if descriptor == "Value": return self.native_value_projection_entry(node, native_position) if descriptor in {"Allocatable", "Pointer"}: @@ -1410,6 +1555,23 @@ def native_projection_entry(self, node: ast.AST, native_position: int) -> Projec helper = self.required_name(node.func) return self._native_helper_projection_entry(helper, node, native_position) + def native_scalar_cast_projection_entry( + self, + node: ast.Call, + native_position: int, + native_cast: str, + ) -> ProjectionMapping: + """Attach one exact C scalar identity to an argument or result reference.""" + if len(node.args) != 1 or node.keywords: + raise ValueError(f"{native_cast} expects one Arg(...) or Return(...) reference") + mapping = self.native_projection_entry(node.args[0], native_position) + if mapping.native_cast is not None: + raise ValueError("native_call scalar casts cannot be nested") + if mapping.value_kind: + raise ValueError(f"{native_cast} expects Arg(...) or Return(...), not a projection wrapper") + mapping.native_cast = native_cast + return mapping + def native_value_projection_entry( self, node: ast.Call, @@ -1466,6 +1628,7 @@ def _native_helper_projection_entry( "Len": self._native_len_projection_entry, "IsPresent": self._native_is_present_projection_entry, "Work": self._native_work_projection_entry, + "Hidden": self._native_hidden_projection_entry, } try: handler = handlers[helper] @@ -1499,6 +1662,22 @@ def _native_return_projection_entry(node: ast.Call, native_position: int) -> Pro result_position=int(ast.literal_eval(position_arg)), ) + def _native_hidden_projection_entry(self, node: ast.Call, native_position: int) -> ProjectionMapping: + """Parse ``Hidden(name, T)`` into an output the Python signature never shows. + + A hidden output is produced by the native call but consumed by a + decorator such as ``@raises``, so it declares its own type here instead + of occupying a slot in the return annotation. + """ + if len(node.args) != 2: + raise ValueError("Hidden expects a name and a type") + name = str(ast.literal_eval(node.args[0])) + if not name: + raise ValueError("Hidden requires a non-empty output name") + mapping = ProjectionMapping(native_name=name, native_position=native_position) + self._hidden_output_types[id(mapping)] = self.semantic_type(node.args[1]) + return mapping + @staticmethod def _native_pass_projection_entry(node: ast.Call, native_position: int) -> ProjectionMapping: """Parse ``Pass()`` as the temporary passed-object mapping for a method.""" @@ -1590,11 +1769,19 @@ def native_address_projection_entry(self, node: ast.Call, native_position: int) raise ValueError("Addr projection expects one Arg(...), Return(...), or Work(...) reference") if self._addr_depth(node.func) != 1: raise ValueError("native_call address projection only supports Addr(...)") - value = self.native_value_ref(node.args[0]) + native_cast = None + reference = node.args[0] + if isinstance(reference, ast.Call) and self.contract_name(reference.func) in NATIVE_C_SCALAR_CASTS: + native_cast = self.contract_name(reference.func) + if len(reference.args) != 1 or reference.keywords: + raise ValueError(f"{native_cast} expects one Arg(...) or Return(...) reference") + reference = reference.args[0] + value = self.native_value_ref(reference) mapping = ProjectionMapping( native_position=native_position, value_kind="addr", value=value, + native_cast=native_cast, ) if value["kind"] == "arg": mapping.python_position = int(value["position"]) @@ -1724,6 +1911,8 @@ def semantic_type(self, node: ast.expr) -> SemanticType: unimported contract spellings raise ``ValueError``. """ self._reject_unimported_contract_type(node) + if self.contract_name(node) in NATIVE_C_SCALAR_CASTS: + raise ValueError("Native C scalar names are valid only inside @native_call") optional_item = self._optional_union_item(node) if optional_item is not None: semantic_type = self.semantic_type(optional_item) @@ -1743,11 +1932,8 @@ def semantic_type(self, node: ast.expr) -> SemanticType: raise ValueError(f"Unsupported semantic type call: {ast.unparse(node)!r}") if isinstance(node, ast.Subscript) and self.matches_name(node.value, "String"): - if self._string_subscript_is_array_dimensions(node): - raise ValueError( - "String[:] is ambiguous; use String for scalar non-fixed length, " - "String[:][:] for an array of non-fixed strings, or String[n] for fixed length" - ) + # One subscription after String is always the character length; an + # array adds its shape as a second subscription. return self._character_type(node) if self.is_subscript_of(node, "Allocatable"): return self._descriptor_type(node, "Allocatable") @@ -1849,7 +2035,7 @@ def array_type(self, node: ast.Subscript) -> SemanticType: """Load a bracketed scalar type as an array or fixed-length character contract.""" if isinstance(node.value, ast.Subscript): if self.matches_name(node.value.value, "String"): - semantic_type = self._character_type(node.value, allow_deferred_length=True) + semantic_type = self._character_type(node.value) return self._array_type_from_dimensions( semantic_type.name, self.array_dimension_texts(node), @@ -1867,13 +2053,6 @@ def array_type(self, node: ast.Subscript) -> SemanticType: self.array_dimension_texts(node), ) - def _string_subscript_is_array_dimensions(self, node: ast.Subscript) -> bool: - """Return whether ``String[...]`` is an array contract, not a length.""" - return any( - isinstance(item, ast.Slice) or (isinstance(item, ast.Constant) and item.value is Ellipsis) - for item in self.subscript_items(node) - ) - def array_dimension_texts(self, node: ast.Subscript) -> list[str]: """Return normalized source dimension spellings from a bracketed type AST.""" items = self.subscript_items(node) @@ -2037,22 +2216,26 @@ def _flat_array_order(source_shape: list[str], rank: int | None) -> str | None: return None return "ORDER_C" if source_shape.index("*") == 0 else "ORDER_F" - def _character_type(self, node: ast.Subscript, *, allow_deferred_length: bool = False) -> SemanticType: - """Load a fixed or allowed deferred ``String`` length annotation.""" + def _character_type(self, node: ast.Subscript) -> SemanticType: + """Load the character length from one ``String[...]`` subscription. + + A ``String`` annotation carries its length in the first subscription and + its shape, if any, in the second. ``String[8]`` and ``String[n]`` are + explicit lengths, ``String[:]`` is a deferred length established by + allocation, and ``String[...]`` is the assumed length that bare + ``String`` also spells. + """ items = self.subscript_items(node) - if len(items) != 1 or (isinstance(items[0], ast.Constant) and items[0].value is Ellipsis): - raise ValueError("Fixed character types use String[length]; use String for non-fixed length") - if isinstance(items[0], ast.Slice): - length = self.dimension_text(items[0]) - if allow_deferred_length and length == ":": - return SemanticType( - name="String", - dtype="String", - metadata={"fortran_character_length": ":"}, - ) + if len(items) != 1: + raise ValueError("Character length uses one subscription: String[8], String[n], String[:], or String[...]") + if isinstance(items[0], ast.Constant) and items[0].value is Ellipsis: + return SemanticType(name="String", dtype="String", metadata={"fortran_character_length": "*"}) + if isinstance(items[0], ast.Slice) and not self._is_deferred_length_slice(node, items[0]): + raw_items = self._source_dimension_items(node) + spelling = raw_items[0].strip() if raw_items and len(raw_items) == 1 else self.dimension_text(items[0]) raise ValueError( - "String[:] is ambiguous; use String for scalar non-fixed length, " - "String[:][:] for an array of non-fixed strings, or String[n] for fixed length" + f"String[{spelling}] is not a character length; use String[:] for a deferred " + "length and a second subscription for array shape" ) length = self.dimension_text(items[0]) return SemanticType( @@ -2061,6 +2244,20 @@ def _character_type(self, node: ast.Subscript, *, allow_deferred_length: bool = metadata={"fortran_character_length": length}, ) + def _is_deferred_length_slice(self, node: ast.Subscript, item: ast.Slice) -> bool: + """Report whether one ``String[...]`` slice spells exactly the deferred length ``:``. + + Python parses ``[:]`` and ``[::]`` into the same AST, so the original + contract text decides: only a bare colon is a deferred length, while a + strided spelling belongs to the shape subscription. + """ + if not (item.lower is None and item.upper is None and item.step is None): + return False + raw_items = self._source_dimension_items(node) + if raw_items is None or len(raw_items) != 1: + return True + return raw_items[0].strip() == ":" + def apply_annotation_metadata(self, semantic_type: SemanticType, node: ast.expr) -> None: """Apply one ``Annotated`` metadata AST item to a semantic type in place. @@ -2777,6 +2974,7 @@ def _callable_parts( optional_return_positions=optional_return_positions, ) self._validate_callable_descriptor_return(return_type, native_result) + self._apply_hidden_native_outputs(return_type, returned_args, projection) return_type, returned_args = self._apply_native_call_returns(return_type, returned_args, projection) return_type = self._apply_native_result_projection(return_type, native_result) @@ -2852,7 +3050,7 @@ def _optional_native_return_positions( for mapping in projection if mapping.result_position is not None and mapping.python_position is None } - if native_result is None or native_result.result_position is None: + if native_result is None or native_result.result_position is None or native_result.native_cast is not None: return positions if native_result.result_position in positions: raise ValueError( @@ -2895,14 +3093,22 @@ def _callable_argument( raise ValueError( f"Scalar descriptor argument {arg.arg!r} must use a nullable annotation such as Float64 | None" ) - elif (optional_annotation := self._optional_union_item(annotation)) is not None: + nullable_annotation = False + if not nullable_descriptor and (optional_annotation := self._optional_union_item(annotation)) is not None: optional_type = self.semantic_type(optional_annotation) if self.contract_name(optional_annotation) is None and native_array_descriptor_kind(optional_type) is None: annotation = optional_annotation + nullable_annotation = True visibility, semantic_type, original_name = self.visible_type( annotation, allow_optional_absent_handle=True, ) + if nullable_annotation: + # Unwrapping keeps the established storage contract, but the author + # did write '| None'. Recording it lets a language whose direct + # route cannot express a nullable actual reject the form instead of + # silently building a non-nullable one. + semantic_type.metadata[NULLABLE_ANNOTATION_METADATA] = True self._validate_optional_native_array_handle_argument(arg, default, semantic_type) writable = self._type_uses_writable_storage(semantic_type) semantic_type.ownership.mutable = writable @@ -2998,6 +3204,9 @@ def _apply_native_result_projection( return return_type if return_type is None: raise ValueError("native_call result requires a native function result in Python result slot 0") + if native_result.native_cast is not None: + return_type.metadata[NATIVE_C_SCALAR_CAST_METADATA] = native_result.native_cast + return return_type if not return_type.metadata.pop(_PYI_OPTIONAL_RETURN_METADATA, False): raise ValueError("native scalar descriptor function result must use a nullable T | None annotation") self._apply_scalar_descriptor_kind(return_type, native_result.value_kind) @@ -3019,6 +3228,44 @@ def _validate_stub_callable(node: ast.FunctionDef) -> None: if not (isinstance(body, ast.Expr) and isinstance(body.value, ast.Constant) and body.value.value is Ellipsis): raise ValueError(f"Unsupported function header: {_node_text(node)!r}") + def _apply_hidden_native_outputs( + self, + return_type: SemanticType | None, + returned_args: list[SemanticArgument], + projection: list[ProjectionMapping], + ) -> None: + """Turn ``Hidden(name, T)`` slots into projected outputs after the visible ones. + + The result slots the annotation already claimed keep their positions, so + hidden outputs take the next free ones and reach the rest of the + pipeline exactly as an annotated projected result would. + """ + hidden = [mapping for mapping in projection if id(mapping) in self._hidden_output_types] + if not hidden: + return + claimed = [mapping.result_position for mapping in projection] + claimed.extend(argument.metadata.get("return_position") for argument in returned_args) + # A direct return owns result slot 0 even though no mapping names it, so + # a hidden output must never claim that slot and displace it. + if return_type is not None: + claimed.append(0) + next_position = max((position for position in claimed if isinstance(position, int)), default=-1) + 1 + for mapping in hidden: + semantic_type = self._hidden_output_types.pop(id(mapping)) + _PyiAstParser._mark_projected_output(semantic_type) + mapping.result_position = next_position + returned_args.append( + SemanticArgument( + name=mapping.native_name, + semantic_type=semantic_type, + metadata={ + "return_position": next_position, + HIDDEN_NATIVE_OUTPUT_METADATA: True, + }, + ) + ) + next_position += 1 + @staticmethod def _apply_projected_returns(semantic_args: list[SemanticArgument], returned_args: list[SemanticArgument]) -> None: """Merge ``Returns`` outputs into native arguments and mark their storage writable.""" @@ -3269,7 +3516,9 @@ def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: has_native_call=decorators.has_native_call, release_gil=decorators.release_gil, error_status_policy=decorators.error_status_policy, + deferred=decorators.abstract_method, ) + self.parser._reject_private_constructor(node.name, decorators.visibility) if node.name == "__init__" and decorators.bind_target is not None and decorators.overload_target is None: self.has_bound_constructor = True if decorators.overload_target is not None: @@ -3331,6 +3580,7 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: node, visibility=decorators.visibility, native_type=decorators.native_type, + abstract=decorators.abstract, ) ) @@ -3395,6 +3645,7 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: node, visibility=decorators.visibility, native_type=decorators.native_type, + abstract=decorators.abstract, ) ) diff --git a/pyproject.toml b/pyproject.toml index f8e9fb788..70e9a6c88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -126,8 +126,10 @@ extend-exclude = [ "tests/c/fixtures/pyi", "tests/fortran/*/end_to_end/fixtures", "tests/fortran/*/pipeline/fixtures", - "tests/fortran/pyi_contracts/*/end_to_end/fixtures", - "tests/fortran/semantic_pyi_format/pipeline/fixtures", + "tests/fortran/infrastructure/building/end_to_end/fixtures", + "tests/fortran/infrastructure/building/pipeline/fixtures", + "tests/fortran/infrastructure/semantic_pyi/contracts/*/end_to_end/fixtures", + "tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures", "prik.egg-info", ] @@ -165,12 +167,14 @@ exclude_dirs = ["tests", "docs", "prik.egg-info"] [tool.vulture] paths = ["prik", "tests"] exclude = [ - "tests/c/fixtures/pyi/", - "tests/fortran/*/end_to_end/fixtures/", - "tests/fortran/*/pipeline/fixtures/", - "tests/fortran/pyi_contracts/*/end_to_end/fixtures/", - "tests/fortran/semantic_pyi_format/pipeline/fixtures/", - "prik.egg-info/", + "*/tests/c/fixtures/pyi/*", + "*/tests/fortran/*/end_to_end/fixtures/*", + "*/tests/fortran/*/pipeline/fixtures/*", + "*/tests/fortran/infrastructure/building/end_to_end/fixtures/*", + "*/tests/fortran/infrastructure/building/pipeline/fixtures/*", + "*/tests/fortran/infrastructure/semantic_pyi/contracts/*/end_to_end/fixtures/*", + "*/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/*", + "*/prik.egg-info/*", ] min_confidence = 80 sort_by_size = true diff --git a/tests/README.md b/tests/README.md index 6772de29e..bd91c2748 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,15 +1,16 @@ # Test Suite Map -Product-behavior tests are organized language first. Fortran tests are then -organized by documented feature and pipeline stage: +Product-behavior tests are organized language first. Within a language, +documented language features use a feature-first, stage-second layout: ```text -tests/fortran/// +tests//// ``` -Documentation is the top-level `tests/docs/` feature. Only other genuinely -internal product behavior mirrors its production package below -`tests/fortran/infrastructure/`. Maintainer tooling has the independent +Parsing, preprocessing, command-line handling, semantic IR and `.pyi` +conversion, build orchestration, and other cross-feature mechanisms are +infrastructure. They live below `tests//infrastructure/`, even when +they also have user documentation. Maintainer tooling has the independent `tests/tools/` owner, while exceptional automation-safety checks live under `tests/workflows/`. Generated C and CPython binding code used by a Fortran wrapper remains evidence @@ -74,15 +75,17 @@ semantic tests preserve names, imports, and native callable provenance; the arrays policy tests classify dependency roles and unsupported native calls; and the arrays end-to-end tests compile representative dimensions, inquiry forms, reductions, conditionals, powers, and logical-kind arrays. Contract-batch -reconciliation belongs with `tests/fortran/semantic_pyi_format/`, where -editable `.pyi` imports and prototypes are exercised. - -Public cross-feature capabilities have explicit owners: -`source_parsing/`, `source_preprocessing/`, `command_line_interface/`, and -`semantic_ir/`. Only internal frameworks with no honest public-capability owner -belong under `tests/fortran/infrastructure/`. A user-visible behavior stays -with its feature even when its test crosses several pipeline stages. Minimized -real-world parser interactions belong under `source_parsing/parsing/`; full +reconciliation belongs with `tests/fortran/infrastructure/semantic_pyi/`, +where editable `.pyi` imports and prototypes are exercised. + +Cross-feature mechanisms have explicit infrastructure owners. `parsing/`, +`preprocessing/`, `cli/`, `semantic_ir/`, `semantic_pyi/`, and `building/` own +shared pipeline behavior; the remaining owners mirror their production package +(`policy/`, `codegen/`, `printers/`, `naming/`, `pipeline/`, `runtime/`, +`utilities/`). `tests/fortran/README.md` and `tests/c/README.md` carry the +complete per-language tables. A user-visible language behavior stays with its +feature even when its test crosses several pipeline stages. Minimized +real-world parser interactions belong under `infrastructure/parsing/`; full third-party snapshots are temporary analysis inputs, not permanent fixtures. ## Independent suite gates @@ -117,7 +120,7 @@ selection: - `toolchain_smoke` selects only the bounded portable compiler-profile subset declared by `tests/fortran/conftest.py`. -The smoke suite is eight exact nodes reused from ordinary feature end-to-end +The smoke suite is eight exact nodes reused from ordinary Fortran end-to-end tests. Strict mode requires a resolved compiler, rejects skips and xfails, and prints the selected nodes with their mechanism and compilation fixture: @@ -157,15 +160,16 @@ CLI/API diagnostic test only when propagation is itself public behavior. Feature-local fixtures live below their feature; cross-feature helpers require an explicit infrastructure owner. -After choosing feature ownership, place genuinely internal mechanisms under -their owning production package when that makes the invariant easier to find: +First decide whether the invariant is a language feature or a cross-feature +mechanism. For a cross-feature mechanism, place it under its infrastructure +owner when that makes the invariant easier to find: ```text tests/fortran/infrastructure//test_.py ``` For example, `prik/policy/ownership.py` uses -`infrastructure/semantics/test_ownership.py`, while +`infrastructure/policy/test_ownership.py`, while `prik/planning/planner.py` uses `infrastructure/codegen/test_planner.py`; language source printers use `infrastructure/printers/` and the wrapper orchestrator uses `infrastructure/pipeline/test_wrapper_generator.py`. diff --git a/tests/c/README.md b/tests/c/README.md index 6f150a380..bcfdb7d16 100644 --- a/tests/c/README.md +++ b/tests/c/README.md @@ -4,22 +4,31 @@ CPython binding code used to implement a Fortran wrapper remains under the owning Fortran feature. -C receives a mechanical quarantine during the language-first migration. Move -existing C parsing, probes, preprocessing, semantic conversion, pipeline, CLI -dispatch, property tests, fixtures, and helpers without redesigning their -behavior. Preserve node IDs where path changes permit, parameters, markers, -skips, xfails, and fixture contents. +C language features use the same feature-first, stage-second shape as Fortran: + +```text +tests/c/// +``` + +Parsing, preprocessing, command-line handling, semantic IR and `.pyi` +conversion, and other cross-feature mechanisms live under +`tests/c/infrastructure/`. Preserve node IDs where path changes permit, +parameters, markers, skips, xfails, and fixture contents. The quarantined owners are: | Owner | Scope | | --- | --- | -| `cli/` | C-input command dispatch and C-specific argument/output contracts | -| `parsing/` | C lexer, parser, project, corpus, fixture, and public-entrypoint behavior | -| `probes/` | C compiler type probes | -| `preprocessing/` | C recipes, dependencies, mappings, execution, and diagnostics | -| `semantics/conversion/` | C parser model and C semantic `.pyi` conversion | -| `pipeline/` | C source/generated-contract parity | +| `data_types//` | C scalar type facts and compiler type probes | +| `functions//` | C function declarations and their semantic projection | +| `records//` | C structs, unions, and typedefs | +| `enumerations//` | C enum syntax and semantic projection | +| `infrastructure/cli/` | C-input command dispatch and C-specific argument/output contracts | +| `infrastructure/parsing/` | C lexer, parser, project, corpus, fixture, and public-entrypoint behavior | +| `infrastructure/preprocessing/` | C recipes, dependencies, mappings, execution, and diagnostics | +| `infrastructure/semantic_ir/` | C parser-model conversion to semantic IR | +| `infrastructure/semantic_pyi/` | C semantic `.pyi` conversion and source/generated-contract parity | +| `infrastructure/execution_examples/` | Executable C parser walkthroughs kept runnable as documentation | | `fixtures/native/` | C source and include inputs | | `fixtures/parser/` | C parser snapshots and update commands | | `fixtures/pyi/` | checked C generated-contract packages | diff --git a/tests/c/_support/cli.py b/tests/c/_support/cli.py index ffc35c3b7..fa2ecb5d7 100644 --- a/tests/c/_support/cli.py +++ b/tests/c/_support/cli.py @@ -29,6 +29,7 @@ def _main_args(**overrides): "include_exposure": "reachable-project", "public_includes": [], "private_includes": [], + "export_symbols": None, "show_vars": False, "print_limit": None, "vars_limit": None, diff --git a/tests/c/_support/fixture_outputs.py b/tests/c/_support/fixture_outputs.py index 652cebb05..3e9187648 100644 --- a/tests/c/_support/fixture_outputs.py +++ b/tests/c/_support/fixture_outputs.py @@ -11,10 +11,9 @@ from prik.preprocessing import PreprocessingConfig, preprocess_source from prik.semantics.c2ir import c_project_to_semantic_module from prik.printers import emit_module +from tests.c._support.paths import C_DATA_DIR, C_ROOT -C_ROOT = Path(__file__).resolve().parents[1] -C_DATA_DIR = C_ROOT / "fixtures" / "native" GENERAL_C_DIR = C_DATA_DIR / "general" C_PYI_FIXTURE_DIR = C_ROOT / "fixtures" / "pyi" / "general" C_SOURCE_SUFFIXES = {".c", ".h", ".i"} diff --git a/tests/c/_support/paths.py b/tests/c/_support/paths.py new file mode 100644 index 000000000..15bbefb40 --- /dev/null +++ b/tests/c/_support/paths.py @@ -0,0 +1,13 @@ +"""Directory anchors for tests that read a file owned by another directory. + +Computing `Path(__file__).parents[N]` couples a test to its own depth in the +tree, so moving it silently resolves the path to the wrong directory instead of +failing. Import the anchor that names what is wanted. +""" + +from pathlib import Path + +C_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = C_ROOT.parents[1] +C_DATA_DIR = C_ROOT / "fixtures" / "native" +PARSER_FIXTURE_ROOT = C_ROOT / "fixtures" / "parser" diff --git a/tests/c/_support/runtime.py b/tests/c/_support/runtime.py new file mode 100644 index 000000000..4c81270eb --- /dev/null +++ b/tests/c/_support/runtime.py @@ -0,0 +1,13 @@ +"""C-owned helpers for imported direct-C extension assertions.""" + +from types import ModuleType + + +def sole_native_module(module): + """Return the only generated native child module, when one is present.""" + children = [ + value + for value in vars(module).values() + if isinstance(value, ModuleType) and value.__name__.startswith(f"{module.__name__}.") + ] + return children[0] if len(children) == 1 else module diff --git a/tests/c/semantics/conversion/_support.py b/tests/c/_support/semantic_conversion.py similarity index 100% rename from tests/c/semantics/conversion/_support.py rename to tests/c/_support/semantic_conversion.py diff --git a/tests/c/probes/test_c_types.py b/tests/c/data_types/probes/test_c_types.py similarity index 95% rename from tests/c/probes/test_c_types.py rename to tests/c/data_types/probes/test_c_types.py index a9869fc6e..45872e9d6 100644 --- a/tests/c/probes/test_c_types.py +++ b/tests/c/data_types/probes/test_c_types.py @@ -45,9 +45,10 @@ def test_c_standard_type_probe_source_queries_standard_headers_without_layout_cl assert "PRIK_PRINT_CHAR()" in source assert 'PRIK_PRINT_ARITHMETIC("unsigned long"' in source assert 'PRIK_PRINT_REAL("long double"' in source - assert 'PRIK_PRINT_ARITHMETIC("long double _Complex"' in source + assert 'PRIK_PRINT_COMPLEX("long double _Complex"' in source assert 'PRIK_PRINT_ARITHMETIC("int"' in source assert 'PRIK_PRINT_ARITHMETIC("size_t"' in source + assert 'PRIK_PRINT_ARITHMETIC("int64_t"' in source assert 'PRIK_PRINT_ARITHMETIC("uint32_t"' in source assert 'PRIK_PRINT_ARITHMETIC("time_t"' in source assert "sizeof(FILE *)" in source @@ -204,6 +205,21 @@ def test_c_standard_type_probe_reports_semantic_facts_from_native_compiler(): assert uint32_t["signed"] is False assert uint32_t["bits"] == 32 + for name, signed, bits in ( + ("int8_t", True, 8), + ("int16_t", True, 16), + ("int32_t", True, 32), + ("int64_t", True, 64), + ("uint8_t", False, 8), + ("uint16_t", False, 16), + ("uint64_t", False, 64), + ): + fact = report.types[name] + if fact["available"]: + assert fact["kind"] == "integer" + assert fact["signed"] is signed + assert fact["bits"] == bits + time_t = report.types["time_t"] assert time_t["available"] is True assert time_t["semantic_category"] in { diff --git a/tests/c/semantics/conversion/test_types_and_constants.py b/tests/c/data_types/semantics/test_types_and_constants.py similarity index 99% rename from tests/c/semantics/conversion/test_types_and_constants.py rename to tests/c/data_types/semantics/test_types_and_constants.py index bc13ed17f..3103ad132 100644 --- a/tests/c/semantics/conversion/test_types_and_constants.py +++ b/tests/c/data_types/semantics/test_types_and_constants.py @@ -51,7 +51,7 @@ c_struct_to_semantic_class, c_type_to_semantic_type, ) -from tests.c.semantics.conversion._support import ( +from tests.c._support.semantic_conversion import ( _assert_c_origin, _assert_unsupported_type, _function, diff --git a/tests/c/enumerations/parsing/test_c_enum_syntax.py b/tests/c/enumerations/parsing/test_c_enum_syntax.py new file mode 100644 index 000000000..307154d8b --- /dev/null +++ b/tests/c/enumerations/parsing/test_c_enum_syntax.py @@ -0,0 +1,40 @@ +"""C enum declaration parser tests.""" + + +def test_enum_constants_preserve_explicit_implicit_and_symbolic_values(): + from prik.parsers.c import parse_c_file + + parsed = parse_c_file( + """ +enum status { + STATUS_OK = 0, + STATUS_WARN, + STATUS_ERROR = 10, + STATUS_NEXT = STATUS_ERROR + 1 +}; +""", + filename="enum.h", + ) + + assert [(item.name, item.value) for item in parsed.enums[0].constants] == [ + ("STATUS_OK", "0"), + ("STATUS_WARN", None), + ("STATUS_ERROR", "10"), + ("STATUS_NEXT", "STATUS_ERROR + 1"), + ] + + +def test_typedef_enum_and_trailing_tag_variable_are_separate_objects(): + from prik.parsers.c import CEnum, CStruct, parse_c_file + + parsed = parse_c_file( + "typedef enum { FLAG_NONE = 0, FLAG_READ = 1 } flag_t;\nstruct point { int x; } origin;\n", + filename="tag_declarators.h", + ) + + assert parsed.enums[0].anonymous_id + assert isinstance(parsed.typedefs[0].type, CEnum) + assert parsed.typedefs[0].type is parsed.enums[0] + assert parsed.variables[0].name == "origin" + assert isinstance(parsed.variables[0].type, CStruct) + assert parsed.variables[0].type is parsed.structs[0] diff --git a/tests/c/enumerations/semantics/test_c_enum_semantics.py b/tests/c/enumerations/semantics/test_c_enum_semantics.py new file mode 100644 index 000000000..bc32d4778 --- /dev/null +++ b/tests/c/enumerations/semantics/test_c_enum_semantics.py @@ -0,0 +1,170 @@ +"""C enum conversion into the semantic IR.""" + +from dataclasses import asdict + +from prik.printers import emit_module +from prik.parsers.c import parse_c_file, parse_c_project +from prik.parsers.c.models import ( + CMacro, +) +from prik.pipeline.pyi import pyi_text_to_semantic_module as parse_pyi_text +from prik.semantics.c2ir import ( + CToIRConverter, + c_file_to_semantic_module, + c_file_to_semantic_modules, + c_project_to_semantic_module, + c_project_to_semantic_modules, +) +from prik.semantics.models import ( + SemanticVariable, +) +from tests.c._support.semantic_conversion import ( + _assert_c_origin, + _function, +) + + +def test_c2ir_converts_enum_constants_and_simple_macro_constants(): + parsed = parse_c_file( + """ +enum status { STATUS_OK = 0, STATUS_WARN, STATUS_ERROR = 10 }; +""", + filename="constants.h", + ) + parsed.macros = [CMacro(name="API_VERSION", value="3")] + module = c_file_to_semantic_modules(parsed)[0] + + constants = {var.name: var for var in module.variables} + assert constants["API_VERSION"].default_value == "3" + assert constants["API_VERSION"].semantic_type.constraints[0].name == "Constant" + assert constants["STATUS_WARN"].default_value == "1" + assert constants["STATUS_ERROR"].default_value == "10" + api_version = constants["API_VERSION"] + assert isinstance(api_version, SemanticVariable) + assert api_version.semantic_type.name == "Int32" + assert api_version.semantic_type.dtype == "Int32" + assert [asdict(constraint) for constraint in api_version.semantic_type.constraints] == [ + {"name": "Constant", "arguments": []} + ] + _assert_c_origin( + api_version.origin, + native_name="API_VERSION", + source_kind="macro", + ) + status_ok = constants["STATUS_OK"] + assert module.classes == [] + assert status_ok.semantic_type.name == "Int" + assert status_ok.semantic_type.dtype == "Int32" + assert status_ok.semantic_type.metadata["enum_name"] == "status" + assert status_ok.semantic_type.metadata["c_kind"] == "enum" + assert status_ok.semantic_type.metadata["c_enum"] == "enum status" + assert status_ok.semantic_type.metadata["c_underlying_type"] == "Int" + assert status_ok.semantic_type.coercions == [] + _assert_c_origin( + status_ok.origin, + native_name="STATUS_OK", + native_scope="enum status", + source_kind="enum_constant", + source_location={ + "filename": "constants.h", + "line": 2, + "column": 1, + "source_line": "enum status { STATUS_OK = 0, STATUS_WARN, STATUS_ERROR = 10 };", + }, + ) + + +def test_c2ir_names_anonymous_typedef_enums_and_keeps_enumerators_unscoped(): + source = "typedef enum { FLAG_NONE = 0, FLAG_READ = 1 } flag_t; flag_t get_flags(void);" + parsed = parse_c_file(source, filename="flags.h") + + module = c_file_to_semantic_module(parsed) + project_module = c_project_to_semantic_module(parse_c_project({"flags.h": source}), name="flags") + + assert module.classes == [] + assert project_module.classes == [] + assert [variable.name for variable in module.variables] == ["FLAG_NONE", "FLAG_READ"] + assert [variable.name for variable in project_module.variables] == ["FLAG_NONE", "FLAG_READ"] + assert [variable.semantic_type.name for variable in module.variables] == ["Int", "Int"] + assert module.variables[0].semantic_type.metadata["enum_name"] == "flag_t" + assert _function(module, "get_flags").return_type.name == "Int" + assert _function(project_module, "get_flags").return_type.name == "Int" + + +def test_c2ir_enum_values_emit_only_python_compatible_expressions(): + parsed = parse_c_file( + "enum flags { FLAG_ONE = 1U, FLAG_OCTAL = 010, FLAG_SHIFT = FLAG_ONE << 1, FLAG_CHAR = 'A' };", + filename="flags.h", + ) + module = c_file_to_semantic_module(parsed) + + code = emit_module(module) + + assert "FLAG_ONE: Final[Int] = 1" in code + assert "FLAG_OCTAL: Final[Int] = 8" in code + assert "FLAG_SHIFT: Final[Int] = FLAG_ONE << 1" in code + assert "FLAG_CHAR: Final[Int]" in code + assert {variable.name: variable.default_value for variable in module.variables} == { + "FLAG_ONE": "1U", + "FLAG_OCTAL": "010", + "FLAG_SHIFT": "FLAG_ONE << 1", + "FLAG_CHAR": "'A'", + } + assert [variable.name for variable in parse_pyi_text(code, module_name="flags").variables] == [ + "FLAG_ONE", + "FLAG_OCTAL", + "FLAG_SHIFT", + "FLAG_CHAR", + ] + + +def test_c2ir_cross_header_enum_references_import_the_owner_enum(): + project = parse_c_project( + { + "types.h": "enum status { STATUS_OK = 0 };", + "api.h": "enum status get_status(void);", + } + ) + + modules = {module.name: module for module in c_project_to_semantic_modules(project)} + + assert modules["api"].classes == [] + assert modules["types"].classes == [] + assert _function(modules["api"], "get_status").return_type.name == "Int" + assert _function(modules["api"], "get_status").return_type.metadata["c_enum"] == "enum status" + + anonymous_project = parse_c_project( + { + "types.h": "typedef enum { FLAG_NONE = 0 } flag_t;", + "api.h": "flag_t get_flags(void);", + } + ) + anonymous_modules = {module.name: module for module in c_project_to_semantic_modules(anonymous_project)} + assert _function(anonymous_modules["api"], "get_flags").return_type.name == "Int" + + +def test_c2ir_uses_enum_specific_underlying_type_facts_when_supplied(): + parsed = parse_c_file( + "enum status { STATUS_OK = 0, STATUS_ERROR = 255 }; enum status get_status(void);", + filename="status.h", + ) + module = CToIRConverter( + standard_type_report={ + "types": { + "enum status": { + "available": True, + "kind": "integer", + "signed": False, + "bits": 8, + "underlying_c_type": "unsigned char", + } + } + } + ).visit(parsed) + + return_type = _function(module, "get_status").return_type + assert module.classes == [] + assert return_type.name == "UInt8" + assert return_type.dtype == "UInt8" + assert return_type.metadata["c_kind"] == "enum" + assert return_type.metadata["c_enum_type_fact_source"] == "compiler_probe" diff --git a/tests/c/fixtures/parser/README.md b/tests/c/fixtures/parser/README.md index d82391464..df84635db 100644 --- a/tests/c/fixtures/parser/README.md +++ b/tests/c/fixtures/parser/README.md @@ -46,7 +46,7 @@ Fatal diagnostic fixtures live in `tests/c/fixtures/native/errors/parser/` and t expected metadata lives in `fixtures/errors/`. Regenerate them with: ```bash -C_PARSER_UPDATE_GOLDENS=1 PYTHONPATH=. pytest -q tests/c/parsing/test_c_error_fixture_suite.py +C_PARSER_UPDATE_GOLDENS=1 PYTHONPATH=. pytest -q tests/c/infrastructure/parsing/test_c_error_fixture_suite.py ``` The standalone error generator remains available for targeted refreshes, and diff --git a/tests/c/functions/codegen/test_positional_only_lowering.py b/tests/c/functions/codegen/test_positional_only_lowering.py new file mode 100644 index 000000000..14e6f3aa9 --- /dev/null +++ b/tests/c/functions/codegen/test_positional_only_lowering.py @@ -0,0 +1,40 @@ +"""A positional-only binding parses its call tuple and installs no keyword table.""" + +from prik.parsers.c import parse_c_file +from prik.pipeline.wrapper import WrapperGenerator +from prik.planning import WrapperPlanner +from prik.policy.completion import complete_semantic_policies +from prik.semantics.c2ir import c_file_to_semantic_module + +# Reserved parameter spellings are exactly what a real system header supplies. +_SOURCE = "double blend(double __x, double __y) { return __x + __y; }\n" + + +def _binding(**options) -> str: + module = c_file_to_semantic_module(parse_c_file(_SOURCE, filename="surface.c")) + complete_semantic_policies(module, **options) + generated = WrapperGenerator().generate(WrapperPlanner().build(module)) + return next(source.text for source in generated.sources if source.path.suffix == ".c") + + +def test_a_positional_only_binding_takes_no_keyword_dictionary(): + binding = _binding(positional_only=True) + + assert "static PyObject * wrap_blend(PyObject * self, PyObject * args) {" in binding + assert 'if (!PyArg_ParseTuple(args, "OO", &bound_arg0_obj, &bound_arg1_obj)) return NULL' in binding + assert "kwlist" not in binding + assert "METH_KEYWORDS" not in binding + + # The native declaration keeps the header's spelling; the Python surface does not. + assert "double blend(double __x, double __y);" in binding + assert "blend(arg0, arg1) -> float64" in binding + assert "for argument arg0." in binding + assert "__x" not in binding.split("static PyObject * wrap_blend")[1] + + +def test_the_default_binding_still_accepts_keywords_under_the_declared_names(): + binding = _binding() + + assert "static PyObject * wrap_blend(PyObject * self, PyObject * args, PyObject * kwargs) {" in binding + assert 'static char * kwlist[] = {"__x", "__y", NULL};' in binding + assert "METH_VARARGS | METH_KEYWORDS" in binding diff --git a/tests/c/functions/end_to_end/test_direct_c_hidden_native_outputs.py b/tests/c/functions/end_to_end/test_direct_c_hidden_native_outputs.py new file mode 100644 index 000000000..db9d1b1f5 --- /dev/null +++ b/tests/c/functions/end_to_end/test_direct_c_hidden_native_outputs.py @@ -0,0 +1,114 @@ +"""Direct C ``Hidden`` storage never becomes part of the Python result. + +A hidden slot is passed to the native call like any other output, but it is not +a Python result, so the return annotation states exactly what the caller gets. +""" + +import shutil +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_pyi_extension +from tests.c._support.runtime import sole_native_module + +SOURCE = """void tally(int n, int *doubled, int *squared) { + *doubled = n * 2; + *squared = n * n; +} + +void split_four(int n, int *doubled, int *tripled, int *quadrupled, int *quintupled) { + *doubled = n * 2; + *tripled = n * 3; + *quadrupled = n * 4; + *quintupled = n * 5; +} +""" + + +def _build(tmp_path: Path, contract: str, name: str): + (tmp_path / f"{name}.pyi").write_text(contract, encoding="utf-8") + (tmp_path / f"{name}.c").write_text(SOURCE, encoding="utf-8") + return build_pyi_extension( + tmp_path / f"{name}.pyi", + native_language="c", + native_c_sources=[tmp_path / f"{name}.c"], + output_dir=tmp_path / f"build_{name}", + output_name=name, + ) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_hidden_outputs_reach_the_native_call_without_becoming_results(tmp_path: Path): + """Every hidden slot is passed by address; none of them is returned.""" + result = _build( + tmp_path, + """from prik.contracts import Arg, Hidden, Int32, bind, native_call + +@bind("tally") +@native_call([Arg(0), Hidden("doubled", Int32), Hidden("squared", Int32)]) +def tally(n: Int32) -> None: ... +""", + "all_hidden", + ) + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + assert "void tally(int32_t n, int32_t * doubled, int32_t * squared);" in binding + assert module.tally(np.int32(5)) is None + assert module.tally.__doc__.splitlines()[0] == "tally(n) -> None" + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_hidden_and_returned_outputs_share_one_native_call(tmp_path: Path): + """``Returns`` comes back and ``Hidden`` does not, from the same call.""" + result = _build( + tmp_path, + """from prik.contracts import Arg, Hidden, Int32, Return, Returns, bind, native_call + +@bind("tally") +@native_call([Arg(0), Return("doubled", 0), Hidden("squared", Int32)]) +def tally(n: Int32) -> Returns["doubled", Int32]: ... +""", + "mixed_hidden", + ) + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + # Both outputs still cross the boundary; only one is a Python result. + assert "void tally(int32_t n, int32_t * doubled, int32_t * squared);" in binding + assert module.tally(np.int32(5)) == np.int32(10) + assert module.tally.__doc__.splitlines()[0] == "tally(n) -> int32" + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_four_returned_outputs_compile_and_use_shared_failure_cleanup(tmp_path: Path): + """A linear cleanup suffix preserves the successful four-result surface.""" + result = _build( + tmp_path, + """from prik.contracts import Arg, Int32, Return, Returns, bind, native_call + +@bind("split_four") +@native_call([ + Arg(0), + Return("doubled", 0), + Return("tripled", 1), + Return("quadrupled", 2), + Return("quintupled", 3), +]) +def split_four(n: Int32) -> tuple[ + Returns["doubled", Int32], + Returns["tripled", Int32], + Returns["quadrupled", Int32], + Returns["quintupled", Int32], +]: ... +""", + "four_returned", + ) + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + assert module.split_four(np.int32(5)) == tuple(np.int32(value) for value in (10, 15, 20, 25)) + assert "goto prik_output_cleanup_4;" in binding + assert binding.count("Py_XDECREF(result_0_obj);") == 1 diff --git a/tests/c/functions/end_to_end/test_export_symbol_workflow.py b/tests/c/functions/end_to_end/test_export_symbol_workflow.py new file mode 100644 index 000000000..809b70f62 --- /dev/null +++ b/tests/c/functions/end_to_end/test_export_symbol_workflow.py @@ -0,0 +1,97 @@ +"""Compiled and CLI evidence for selecting functions from a private C include.""" + +import shutil +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_c_extension +from prik.preprocessing import PreprocessingConfig +from tests.c._support.paths import REPO_ROOT +from tests.c._support.runtime import sole_native_module + + +pytestmark = pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") + + +def _write_private_include_project(tmp_path: Path) -> tuple[Path, Path, Path]: + header = tmp_path / "reviewed_api.h" + header.write_text( + "extern int private_state;\nint increment(int __value);\nint omitted(int __value);\n", + encoding="utf-8", + ) + probe = tmp_path / "probe.c" + probe.write_text('#include "reviewed_api.h"\n', encoding="utf-8") + implementation = tmp_path / "implementation.c" + implementation.write_text( + '#include "reviewed_api.h"\nint increment(int value) { return value + 1; }\n', + encoding="utf-8", + ) + return header, probe, implementation + + +def test_generate_pyi_selects_one_function_from_a_private_include(tmp_path: Path): + _header, probe, _implementation = _write_private_include_project(tmp_path) + exports = tmp_path / "exports.txt" + exports.write_text("# reviewed public surface\nincrement\n", encoding="utf-8") + contract = tmp_path / "api.pyi" + + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + "--language", + "c", + str(probe), + "--compiler", + shutil.which("cc") or "cc", + "--include-exposure", + "roots-only", + "--export-symbols", + str(exports), + "--out", + str(contract), + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=True, + ) + + text = contract.read_text(encoding="utf-8") + assert "def increment(" in text + assert "omitted" not in text + assert "private_state" not in text + + +def test_source_build_reuses_selection_with_positional_and_collision_policies(tmp_path: Path): + _header, probe, implementation = _write_private_include_project(tmp_path) + preprocessing = PreprocessingConfig( + mode="compiler", + compiler=shutil.which("cc") or "cc", + include_exposure="roots-only", + ) + + result = build_c_extension( + probe, + output_dir=tmp_path / "build", + output_name="selected_api", + input_c_compiler=shutil.which("cc") or "cc", + preprocessing=preprocessing, + export_symbols=["increment"], + native_c_sources=[implementation], + positional_only=True, + collision_adapter_all=True, + ) + module = sole_native_module(result.import_module()) + + assert module.increment(np.int32(4)) == np.int32(5) + with pytest.raises(TypeError, match="keyword"): + module.increment(arg0=np.int32(4)) + assert {name for name in dir(module) if not name.startswith("_")} == {"increment"} diff --git a/tests/c/parsing/test_c_functions.py b/tests/c/functions/parsing/test_c_functions.py similarity index 96% rename from tests/c/parsing/test_c_functions.py rename to tests/c/functions/parsing/test_c_functions.py index 558179646..831419878 100644 --- a/tests/c/parsing/test_c_functions.py +++ b/tests/c/functions/parsing/test_c_functions.py @@ -106,6 +106,23 @@ def test_old_style_knr_detection_uses_linemarkers_and_normalized_headers(): assert error.source_line == "__extension__ int exported(a)" +def test_unnamed_builtin_parameter_prototype_is_not_an_old_style_definition(): + from prik.parsers.c import CDouble, parse_c_file + + parsed = parse_c_file( + """# 764 "/Applications/Xcode.app/SDKs/MacOSX.sdk/usr/include/math.h" 1 3 4 +extern long int rinttol(double) +; +""", + filename="math.i", + preprocessing="preprocessed", + ) + + assert [function.name for function in parsed.functions] == ["rinttol"] + assert parsed.functions[0].parameters[0].name is None + assert isinstance(parsed.functions[0].parameters[0].type, CDouble) + + def test_modern_prototype_before_old_style_definition_does_not_stop_knr_detection(): from prik.parsers.c import CParseError, CParser, parse_c_file diff --git a/tests/c/functions/semantics/test_export_symbol_selection.py b/tests/c/functions/semantics/test_export_symbol_selection.py new file mode 100644 index 000000000..62baf3c99 --- /dev/null +++ b/tests/c/functions/semantics/test_export_symbol_selection.py @@ -0,0 +1,71 @@ +"""Semantic-IR ownership for exact C function export selection.""" + +from pathlib import Path + +import pytest + +from prik.cli import _read_c_export_symbols +from prik.parsers.c.models import CFile, CFunction, CInt, CVariable +from prik.semantics.c2ir import CToIRConverter, select_c_export_functions +from prik.semantics.metadata import EXPLICIT_C_EXPORT_METADATA + + +def _module_with_declarations(): + parsed = CFile( + filename="probe.h", + functions=[ + CFunction(name="keep", result_type=CInt()), + CFunction(name="drop", result_type=CInt()), + ], + variables=[CVariable(name="state", type=CInt())], + ) + return CToIRConverter().visit(parsed) + + +def test_export_selection_promotes_only_the_named_function(): + module = _module_with_declarations() + module.functions[0].visibility = "private" + + selected = select_c_export_functions([module], ["keep"]) + + assert selected == [module] + assert [function.name for function in module.functions] == ["keep"] + assert module.functions[0].visibility == "public" + assert module.functions[0].metadata[EXPLICIT_C_EXPORT_METADATA] is True + assert module.variables == [] + assert module.classes == [] + assert module.prototypes == [] + assert module.overload_sets == [] + + +@pytest.mark.parametrize( + ("symbols", "message"), + [ + ([], "requires at least one function name"), + (["bad-name"], "invalid C identifiers: bad-name"), + (["keep", "keep"], "repeated names: keep"), + (["missing"], "unknown names: missing"), + (["state"], "non-function names: state"), + ], +) +def test_export_selection_fails_closed_for_invalid_requests(symbols, message): + with pytest.raises(ValueError, match=message): + select_c_export_functions([_module_with_declarations()], symbols) + + +def test_export_selection_rejects_an_ambiguous_function_name(): + first = _module_with_declarations() + second = _module_with_declarations() + + with pytest.raises(ValueError, match="ambiguous function names: keep"): + select_c_export_functions([first, second], ["keep"]) + + +def test_export_symbol_file_accepts_comments_and_rejects_duplicates(tmp_path: Path): + export_file = tmp_path / "exports.txt" + export_file.write_text("# reviewed\nkeep # public\n\ndrop\n", encoding="utf-8") + assert _read_c_export_symbols(export_file) == ("keep", "drop") + + export_file.write_text("keep\nkeep\n", encoding="utf-8") + with pytest.raises(ValueError, match="first appeared on line 1"): + _read_c_export_symbols(export_file) diff --git a/tests/c/semantics/conversion/test_functions_and_callbacks.py b/tests/c/functions/semantics/test_functions_and_callbacks.py similarity index 83% rename from tests/c/semantics/conversion/test_functions_and_callbacks.py rename to tests/c/functions/semantics/test_functions_and_callbacks.py index 5aa978931..ab8ffd86d 100644 --- a/tests/c/semantics/conversion/test_functions_and_callbacks.py +++ b/tests/c/functions/semantics/test_functions_and_callbacks.py @@ -1,7 +1,5 @@ """Tests split by stable ownership concept from `test_functions_and_callbacks.py`.""" -from dataclasses import asdict - from prik.parsers.c import parse_c_file from prik.parsers.c.models import ( CAtomic, @@ -22,7 +20,7 @@ CVoid, ) from prik.semantics.c2ir import CToIRConverter, c_file_to_semantic_modules, c_function_to_semantic_function -from tests.c.semantics.conversion._support import ( +from tests.c._support.semantic_conversion import ( _assert_c_origin, _function, ) @@ -67,26 +65,48 @@ def test_c2ir_converts_scalar_function_signatures_and_preserves_native_order(): "specifiers": [], "prototype_style": "prototype", "is_definition": False, - } - assert [asdict(mapping) for mapping in add.projection] == [ - { - "python_name": "a", - "native_name": "a", - "native_position": 0, - "python_position": 0, - "result_position": None, - "value_kind": "", - "value": None, - }, - { - "python_name": "b", - "native_name": "b", - "native_position": 1, - "python_position": 1, - "result_position": None, - "value_kind": "", - "value": None, + "c_abi": { + "calling_convention": "c", + "variadic": False, + "result": { + "source_spelling": "int", + "pointer_depth": 0, + "qualifiers": (), + "const": False, + "has_array_declarator": False, + "has_function_pointer": False, + }, + "parameters": [ + { + "source_spelling": "int", + "pointer_depth": 0, + "qualifiers": (), + "const": False, + "has_array_declarator": False, + "has_function_pointer": False, + }, + { + "source_spelling": "int", + "pointer_depth": 0, + "qualifiers": (), + "const": False, + "has_array_declarator": False, + "has_function_pointer": False, + }, + ], }, + } + assert [ + ( + mapping.python_name, + mapping.native_name, + mapping.native_position, + mapping.python_position, + ) + for mapping in add.projection + ] == [ + ("a", "a", 0, 0), + ("b", "b", 1, 1), ] _assert_c_origin( add.arguments[0].origin, @@ -196,6 +216,19 @@ def test_c2ir_converts_qualifiers_callbacks_bitfields_and_unspecified_functions( "specifiers": [], "prototype_style": "unspecified", "is_definition": False, + "c_abi": { + "calling_convention": "c", + "variadic": False, + "result": { + "source_spelling": "int", + "pointer_depth": 0, + "qualifiers": (), + "const": False, + "has_array_declarator": False, + "has_function_pointer": False, + }, + "parameters": [], + }, } assert qualified.name == "Int8" assert qualified.metadata["c_char_policy"] == "implementation-defined signed 8-bit code unit" diff --git a/tests/c/infrastructure/building/pipeline/test_c_build_cli.py b/tests/c/infrastructure/building/pipeline/test_c_build_cli.py new file mode 100644 index 000000000..8bbf96b84 --- /dev/null +++ b/tests/c/infrastructure/building/pipeline/test_c_build_cli.py @@ -0,0 +1,227 @@ +"""CLI evidence for explicit C wrapper-build inputs.""" + +import importlib +import json +import shutil +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_c_extension, build_pyi_extension, build_pyi_extension_from_manifest +from tests.c._support.paths import REPO_ROOT +from tests.c._support.runtime import sole_native_module + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_cli_builds_a_c_source_only_when_the_c_language_is_explicit(tmp_path: Path): + source = tmp_path / "answer.c" + source.write_text("int answer(int value) { return value + 1; }\n", encoding="utf-8") + + completed = subprocess.run( + [ + sys.executable, + "-m", + "prik", + "--language", + "c", + str(source), + "--out-dir", + str(tmp_path / "build"), + "--json", + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=True, + ) + payload = json.loads(completed.stdout) + + assert Path(payload["shared_library"]).is_file() + assert payload["native_build_plan"]["compilation_units"][0]["language"] == "c" + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_cli_marks_a_source_free_pyi_contract_as_c_native_explicitly(tmp_path: Path): + contract = tmp_path / "api.pyi" + contract.write_text("from prik.contracts import Int\ndef add(value: Int) -> Int: ...\n", encoding="utf-8") + source = tmp_path / "implementation.c" + source.write_text("int add(int value) { return value + 1; }\n", encoding="utf-8") + + completed = subprocess.run( + [ + sys.executable, + "-m", + "prik", + "--language", + "c", + str(contract), + "--native-c-sources", + str(source), + "--out-dir", + str(tmp_path / "build"), + "--json", + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=True, + ) + payload = json.loads(completed.stdout) + + assert payload["manifest"]["extension"]["native_language"] == "c" + assert payload["native_build_plan"]["compilation_units"][0]["language"] == "c" + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_native_manifest_replay_and_makefile_retain_the_c_language(tmp_path: Path): + contract = tmp_path / "api.pyi" + contract.write_text("from prik.contracts import Int\ndef add(value: Int) -> Int: ...\n", encoding="utf-8") + source = tmp_path / "implementation.c" + source.write_text("int add(int value) { return value + 1; }\n", encoding="utf-8") + + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build", + makefile=True, + ) + + assert result.build_manifest is not None + assert result.build_makefile is not None + assert result.manifest["extension"]["native_language"] == "c" + makefile = result.build_makefile.read_text(encoding="utf-8") + assert "CC :=" in makefile + assert "implementation.c" in makefile + assert all(path.suffix != ".f90" for path in result.generated_sources) + + replay = build_pyi_extension_from_manifest(result.build_manifest) + module = sole_native_module(replay.import_module()) + assert module.add(np.int32(4)) == np.int32(5) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_verbose_c_build_reports_c_compilation_and_link_commands(tmp_path: Path, capsys): + source = tmp_path / "answer.c" + source.write_text("int answer(int value) { return value + 1; }\n", encoding="utf-8") + + build_c_extension(source, output_dir=tmp_path / "build", verbose=True) + + output = capsys.readouterr().out + assert "cc" in output + assert "-shared" in output + + +@pytest.mark.skipif( + shutil.which("cc") is None or shutil.which("gfortran") is None, + reason="requires C and Fortran compilers", +) +def test_c_direct_symbol_survives_a_mixed_language_link_with_the_fortran_driver(tmp_path: Path, capsys): + source = tmp_path / "answer.c" + fortran_dependency = tmp_path / "dependency.f90" + source.write_text("int answer(int value) { return value + 1; }\n", encoding="utf-8") + fortran_dependency.write_text( + "subroutine linked_dependency() bind(C)\nend subroutine linked_dependency\n", + encoding="utf-8", + ) + + result = build_c_extension( + source, + native_fortran_sources=[fortran_dependency], + output_dir=tmp_path / "build", + verbose=True, + ) + module = sole_native_module(result.import_module()) + + assert module.answer(np.int32(4)) == np.int32(5) + assert {unit.language for unit in result.native_build_plan.compilation_units} == {"c", "fortran"} + assert "gfortran" in capsys.readouterr().out + + +@pytest.mark.skipif( + sys.platform == "win32" or shutil.which("make") is None or shutil.which("cc") is None, + reason="requires GNU Make, a POSIX shell, and a C compiler", +) +def test_generated_c_makefile_builds_an_importable_extension_from_relative_paths(tmp_path: Path): + """A generated Makefile must run on a clean tree, not only after a build.""" + source = tmp_path / "src" / "answer.c" + source.parent.mkdir() + source.write_text("int answer(int value) { return value + 1; }\n", encoding="utf-8") + + generated = subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--makefile", + "--language", + "c", + "src/answer.c", + "--out-dir", + "build", + "--compiler", + "cc", + "--json", + ], + cwd=tmp_path, + capture_output=True, + text=True, + check=True, + ) + makefile = Path(json.loads(generated.stdout)["build_makefile"]) + subprocess.run( + ["make", "-j4", "-f", str(makefile), "all"], cwd=tmp_path, capture_output=True, text=True, check=True + ) + + sys.modules.pop("answer", None) + sys.path.insert(0, str(tmp_path / "build")) + try: + module = sole_native_module(importlib.import_module("answer")) + assert module.answer(np.int32(4)) == np.int32(5) + finally: + sys.path.remove(str(tmp_path / "build")) + sys.modules.pop("answer", None) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_saved_c_contract_describes_only_the_wrapped_translation_unit(tmp_path: Path): + """Preprocessed headers stay inspection facts, not part of the built API.""" + source = tmp_path / "mathlib.c" + source.write_text( + """#include +#include + +#define DEFAULT_GAIN 2.0 + +double amplify(double value) { return value * DEFAULT_GAIN; } +double hypotenuse(double a, double b) { return sqrt(a * a + b * b); } +""", + encoding="utf-8", + ) + + result = build_c_extension(source, output_dir=tmp_path / "build", output_name="mathlib") + module = sole_native_module(result.import_module()) + contract = (tmp_path / "build" / "contracts" / "mathlib.pyi").read_text(encoding="utf-8") + + assert module.amplify(np.float64(3.0)) == np.float64(6.0) + assert module.hypotenuse(np.float64(3.0), np.float64(4.0)) == np.float64(5.0) + assert "def amplify(" in contract + assert "def hypotenuse(" in contract + assert "private" not in contract + assert "__fpclassify" not in contract + assert "signgam" not in contract + + # The saved contract is the input of the next build without editing. + replay = build_pyi_extension( + tmp_path / "build" / "contracts" / "mathlib.pyi", + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "replay", + output_name="mathlib_replay", + ) + replayed = sole_native_module(replay.import_module()) + assert replayed.amplify(np.float64(3.0)) == np.float64(6.0) diff --git a/tests/c/infrastructure/building/pipeline/test_c_direct_rejections.py b/tests/c/infrastructure/building/pipeline/test_c_direct_rejections.py new file mode 100644 index 000000000..e54cb41ce --- /dev/null +++ b/tests/c/infrastructure/building/pipeline/test_c_direct_rejections.py @@ -0,0 +1,136 @@ +"""Pipeline boundary tests for fail-closed C direct adoption.""" + +import shutil +from pathlib import Path + +import pytest + +from prik import build_c_extension, build_pyi_extension +from prik.preprocessing import PreprocessingConfig + + +def test_unsupported_c_callback_fails_before_build_output_or_native_compilation(tmp_path: Path): + source = tmp_path / "callback.c" + output_dir = tmp_path / "build" + source.write_text("void callback(void (*action)(int));\n", encoding="utf-8") + + with pytest.raises(ValueError, match="C_DIRECT_CALLBACK:action"): + build_c_extension(source, output_dir=output_dir) + + assert not output_dir.exists() + + +def test_volatile_c_access_fails_before_build_output_or_native_compilation(tmp_path: Path): + source = tmp_path / "volatile.c" + output_dir = tmp_path / "build" + source.write_text("void update(volatile int *value);\n", encoding="utf-8") + + with pytest.raises(ValueError, match="C_DIRECT_UNSUPPORTED_QUALIFIER:value"): + build_c_extension(source, output_dir=output_dir) + + assert not output_dir.exists() + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_aggregate_fails_before_target_probe_or_build_output(tmp_path: Path): + source = tmp_path / "aggregate.c" + output_dir = tmp_path / "build" + source.write_text( + "struct pair { int left; int right; };\nint accept_pair(struct pair value);\n", + encoding="utf-8", + ) + + # Source preparation uses a working preprocessor; the target ABI probe uses + # the executable that must never run, so reaching it would fail differently. + with pytest.raises(ValueError, match="C_DIRECT_UNRESOLVED_PRIMITIVE_ABI:value"): + build_c_extension( + source, + preprocessing=PreprocessingConfig(mode="compiler", compiler="cc"), + input_c_compiler="compiler-that-must-not-run", + output_dir=output_dir, + ) + + assert not output_dir.exists() + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_native_global_state_fails_before_any_generated_adapter_source(tmp_path: Path): + source = tmp_path / "globals.c" + output_dir = tmp_path / "build" + source.write_text("double gain = 2.0;\ndouble scale(double value) { return value * gain; }\n", encoding="utf-8") + + with pytest.raises(ValueError, match="C_DIRECT_NATIVE_GLOBAL_STATE:gain"): + build_c_extension(source, output_dir=output_dir) + + assert not output_dir.exists() + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_enum_constants_fail_before_wrapper_planning(tmp_path: Path): + source = tmp_path / "enums.c" + output_dir = tmp_path / "build" + source.write_text("enum color { RED, GREEN };\nint pick(int value) { return value; }\n", encoding="utf-8") + + with pytest.raises(ValueError, match="C_DIRECT_ENUM_CONSTANT:RED"): + build_c_extension(source, output_dir=output_dir) + + assert not output_dir.exists() + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_source_free_contract_module_variable_generates_no_fortran_adapter(tmp_path: Path): + contract = tmp_path / "api.pyi" + implementation = tmp_path / "implementation.c" + output_dir = tmp_path / "build" + contract.write_text( + "from prik.contracts import Float64\n\ngain: Float64\n\ndef scale(value: Float64) -> Float64: ...\n", + encoding="utf-8", + ) + implementation.write_text("double gain = 2.0;\ndouble scale(double value) { return value; }\n", encoding="utf-8") + + with pytest.raises(ValueError, match="C_DIRECT_NATIVE_GLOBAL_STATE:gain"): + build_pyi_extension( + contract, + native_language="c", + native_c_sources=[implementation], + output_dir=output_dir, + ) + + assert not output_dir.exists() + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_unmodeled_c_declaration_is_not_silently_dropped_from_the_public_api(tmp_path: Path): + source = tmp_path / "attributes.c" + output_dir = tmp_path / "build" + source.write_text( + "__attribute__((stdcall)) int convention(int value);\nint ordinary(int value) { return value; }\n", + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="C_DIRECT_UNMODELED_DECLARATION"): + build_c_extension(source, output_dir=output_dir) + + assert not output_dir.exists() + + +def test_raw_c_contract_address_fails_before_target_probe_or_build_output(tmp_path: Path): + contract = tmp_path / "raw_address.pyi" + implementation = tmp_path / "implementation.c" + output_dir = tmp_path / "build" + contract.write_text( + "from prik.contracts import Addr, Int\n\ndef consume(value: Addr(Int)) -> Int: ...\n", + encoding="utf-8", + ) + implementation.write_text("int consume(int value) { return value; }\n", encoding="utf-8") + + with pytest.raises(ValueError, match="C_DIRECT_RAW_ADDRESS:value"): + build_pyi_extension( + contract, + native_language="c", + native_c_sources=[implementation], + input_c_compiler="compiler-that-must-not-run", + output_dir=output_dir, + ) + + assert not output_dir.exists() diff --git a/tests/c/cli/test_c_cli_argument_contract.py b/tests/c/infrastructure/cli/pipeline/test_c_cli_argument_contract.py similarity index 88% rename from tests/c/cli/test_c_cli_argument_contract.py rename to tests/c/infrastructure/cli/pipeline/test_c_cli_argument_contract.py index 195ae4a7b..228f7d631 100644 --- a/tests/c/cli/test_c_cli_argument_contract.py +++ b/tests/c/infrastructure/cli/pipeline/test_c_cli_argument_contract.py @@ -78,20 +78,9 @@ def build(**kwargs): ] -@pytest.mark.parametrize( - ("overrides", "expected"), - [ - ( - {"language": "c"}, - "Compiled wrappers and generate --sources/--makefile currently require --language fortran", - ), - ( - {"language": "c", "parse": True, "show_vars": True}, - "--show-vars is Fortran-only and is not supported for --language c", - ), - ], -) -def test_prik_main_preserves_c_validation_diagnostics(monkeypatch, overrides, expected): +def test_prik_main_rejects_fortran_only_c_parse_options(monkeypatch): + overrides = {"language": "c", "parse": True, "show_vars": True} + expected = "--show-vars is Fortran-only and is not supported for --language c" args = _main_args(**overrides) _install_main_parser(monkeypatch, args) monkeypatch.setattr(prik_cli, "_resolve_language", lambda paths, language, parser: language) diff --git a/tests/c/cli/test_c_cli_output_contract.py b/tests/c/infrastructure/cli/pipeline/test_c_cli_output_contract.py similarity index 100% rename from tests/c/cli/test_c_cli_output_contract.py rename to tests/c/infrastructure/cli/pipeline/test_c_cli_output_contract.py diff --git a/tests/c/parsing/test_c_cli_skeleton.py b/tests/c/infrastructure/cli/pipeline/test_c_cli_skeleton.py similarity index 96% rename from tests/c/parsing/test_c_cli_skeleton.py rename to tests/c/infrastructure/cli/pipeline/test_c_cli_skeleton.py index ca91da197..c6184a264 100644 --- a/tests/c/parsing/test_c_cli_skeleton.py +++ b/tests/c/infrastructure/cli/pipeline/test_c_cli_skeleton.py @@ -209,7 +209,7 @@ def test_cli_c_parse_json_out_writes_file_and_suppresses_stdout(tmp_path: Path): assert "parser_status" not in payload[str(header)] -def test_cli_c_parse_out_without_json_writes_json_and_suppresses_stdout(tmp_path: Path): +def test_cli_c_parse_out_with_json_writes_json_and_suppresses_stdout(tmp_path: Path): header = tmp_path / "api.h" output = tmp_path / "report.json" header.write_text("int run(void);\n", encoding="utf-8") @@ -221,6 +221,7 @@ def test_cli_c_parse_out_without_json_writes_json_and_suppresses_stdout(tmp_path str(header), "--language", "c", + "--json", "--out", str(output), ] @@ -237,7 +238,11 @@ def test_cli_c_semantics_stdout_for_header(tmp_path: Path): header.write_text("int add(int a, int b);\n", encoding="utf-8") cmd = [sys.executable, "-m", "prik", "semantics", str(header), "--language", "c"] - res = subprocess.run(cmd, capture_output=True, text=True, check=True) + summary = subprocess.run(cmd, capture_output=True, text=True, check=True) + assert summary.stdout.startswith(f"File: {header}") + assert "- add(a: Int32 in, b: Int32 in) -> Int32" in summary.stdout + + res = subprocess.run([*cmd, "--json"], capture_output=True, text=True, check=True) payload = json.loads(res.stdout) semantic_modules = payload[str(header)]["semantic_modules"] @@ -559,18 +564,3 @@ def preprocess(path, *, language, config): semantics = prik_cli._semantic_report([str(header)], config, language="c") assert semantics[str(header)]["semantic_modules"][0]["functions"][0]["name"] == "add" assert calls == [header] - - -def test_cli_c_default_build_rejects_the_unsupported_build_language(tmp_path: Path): - header = tmp_path / "api.h" - header.write_text("int add(int a, int b);\n", encoding="utf-8") - - no_stage = subprocess.run( - [sys.executable, "-m", "prik", str(header), "--language", "c"], - capture_output=True, - text=True, - ) - assert no_stage.returncode == 2 - assert "argument --language: invalid choice: 'c'" in no_stage.stderr - assert "choose from" in no_stage.stderr - assert "fortran" in no_stage.stderr diff --git a/tests/c/cli/test_c_cli_stage_dispatch.py b/tests/c/infrastructure/cli/pipeline/test_c_cli_stage_dispatch.py similarity index 100% rename from tests/c/cli/test_c_cli_stage_dispatch.py rename to tests/c/infrastructure/cli/pipeline/test_c_cli_stage_dispatch.py diff --git a/tests/c/parsing/test_c_parser_developer_tutorial.py b/tests/c/infrastructure/execution_examples/test_c_parser_developer_tutorial.py similarity index 100% rename from tests/c/parsing/test_c_parser_developer_tutorial.py rename to tests/c/infrastructure/execution_examples/test_c_parser_developer_tutorial.py diff --git a/tests/c/parsing/test_c_compiler_extensions.py b/tests/c/infrastructure/parsing/test_c_compiler_extensions.py similarity index 92% rename from tests/c/parsing/test_c_compiler_extensions.py rename to tests/c/infrastructure/parsing/test_c_compiler_extensions.py index 57b7d832f..e25a63f9b 100644 --- a/tests/c/parsing/test_c_compiler_extensions.py +++ b/tests/c/infrastructure/parsing/test_c_compiler_extensions.py @@ -241,6 +241,32 @@ def test_typeof_bitint_and_extended_scalars_remain_parseable_as_opaque_types(): ] +def test_system_header_fallback_extended_scalar_typedefs_remain_parseable(): + from prik.parsers.c import CDouble, CFloat, CLongDouble, parse_c_file + + parsed = parse_c_file( + """ +# 214 "/usr/include/bits/floatn-common.h" 1 3 4 +typedef float _Float32; +typedef double _Float64; +typedef double _Float32x; +typedef long double _Float64x; +# 1 "math_api.h" 2 +double exported_sin(double value); +""", + filename="math_api.i", + preprocessing="compiler", + ) + + typedefs = {typedef.name: typedef.type for typedef in parsed.typedefs} + assert isinstance(typedefs["_Float32"], CFloat) + assert isinstance(typedefs["_Float64"], CDouble) + assert isinstance(typedefs["_Float32x"], CDouble) + assert isinstance(typedefs["_Float64x"], CLongDouble) + assert [function.name for function in parsed.functions] == ["exported_sin"] + assert parsed.diagnostics == [] + + def test_preprocessed_extension_diagnostics_and_declarations_use_linemarkers(): from prik.parsers.c import parse_c_file diff --git a/tests/c/parsing/test_c_corpus.py b/tests/c/infrastructure/parsing/test_c_corpus.py similarity index 96% rename from tests/c/parsing/test_c_corpus.py rename to tests/c/infrastructure/parsing/test_c_corpus.py index f6e77edc8..1ebe04adf 100644 --- a/tests/c/parsing/test_c_corpus.py +++ b/tests/c/infrastructure/parsing/test_c_corpus.py @@ -5,12 +5,12 @@ constants, and callback hook fields without requiring a large build system. """ -from pathlib import Path import shutil import pytest +from tests.c._support.paths import C_DATA_DIR -_CJSON_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "native" / "json" +_CJSON_DIR = C_DATA_DIR / "json" def _preprocessed_cjson_source(filename: str) -> str: diff --git a/tests/c/parsing/test_c_declarations_and_declarators.py b/tests/c/infrastructure/parsing/test_c_declarations_and_declarators.py similarity index 100% rename from tests/c/parsing/test_c_declarations_and_declarators.py rename to tests/c/infrastructure/parsing/test_c_declarations_and_declarators.py diff --git a/tests/c/parsing/test_c_error_fixture_suite.py b/tests/c/infrastructure/parsing/test_c_error_fixture_suite.py similarity index 98% rename from tests/c/parsing/test_c_error_fixture_suite.py rename to tests/c/infrastructure/parsing/test_c_error_fixture_suite.py index 7059fe6b2..24c4207a9 100644 --- a/tests/c/parsing/test_c_error_fixture_suite.py +++ b/tests/c/infrastructure/parsing/test_c_error_fixture_suite.py @@ -5,9 +5,10 @@ from pathlib import Path import pytest +from tests.c._support.paths import C_ROOT -_C_ROOT = Path(__file__).resolve().parents[1] +_C_ROOT = C_ROOT _ERRORS_DIR = _C_ROOT / "fixtures" / "native" / "errors" / "parser" _EXPECTED_ERRORS_DIR = _C_ROOT / "fixtures" / "parser" / "fixtures" / "errors" _SOURCE_SUFFIXES = {".c", ".h", ".i"} diff --git a/tests/c/parsing/test_c_fixture_suite.py b/tests/c/infrastructure/parsing/test_c_fixture_suite.py similarity index 99% rename from tests/c/parsing/test_c_fixture_suite.py rename to tests/c/infrastructure/parsing/test_c_fixture_suite.py index 0268d7da1..c232a0b49 100644 --- a/tests/c/parsing/test_c_fixture_suite.py +++ b/tests/c/infrastructure/parsing/test_c_fixture_suite.py @@ -7,8 +7,9 @@ from pathlib import Path import pytest +from tests.c._support.paths import C_ROOT -_C_ROOT = Path(__file__).resolve().parents[1] +_C_ROOT = C_ROOT _DATA_DIR = _C_ROOT / "fixtures" / "native" _SOURCE_SUFFIXES = {".c", ".h", ".i"} _SOURCE_ORDER = {".c": 0, ".h": 1, ".i": 2} diff --git a/tests/c/parsing/test_c_json_sanity.py b/tests/c/infrastructure/parsing/test_c_json_sanity.py similarity index 96% rename from tests/c/parsing/test_c_json_sanity.py rename to tests/c/infrastructure/parsing/test_c_json_sanity.py index ef5a3b85b..57c322e13 100644 --- a/tests/c/parsing/test_c_json_sanity.py +++ b/tests/c/infrastructure/parsing/test_c_json_sanity.py @@ -1,9 +1,9 @@ """JSON schema sanity tests for legacy C parser project snapshots.""" import json -from pathlib import Path +from tests.c._support.paths import PARSER_FIXTURE_ROOT -_FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "parser" / "fixtures" +_FIXTURES_DIR = PARSER_FIXTURE_ROOT / "fixtures" _PARSER_FIXTURE_GROUPS = ("general", "json", "tinyexpr", "linmath", "nanosvg", "stb") diff --git a/tests/c/parsing/test_c_lexer_preprocessor.py b/tests/c/infrastructure/parsing/test_c_lexer_preprocessor.py similarity index 100% rename from tests/c/parsing/test_c_lexer_preprocessor.py rename to tests/c/infrastructure/parsing/test_c_lexer_preprocessor.py diff --git a/tests/c/parsing/test_c_model_serialization.py b/tests/c/infrastructure/parsing/test_c_model_serialization.py similarity index 100% rename from tests/c/parsing/test_c_model_serialization.py rename to tests/c/infrastructure/parsing/test_c_model_serialization.py diff --git a/tests/c/parsing/test_c_parser_benchmark.py b/tests/c/infrastructure/parsing/test_c_parser_benchmark.py similarity index 100% rename from tests/c/parsing/test_c_parser_benchmark.py rename to tests/c/infrastructure/parsing/test_c_parser_benchmark.py diff --git a/tests/c/parsing/test_c_parser_properties.py b/tests/c/infrastructure/parsing/test_c_parser_properties.py similarity index 100% rename from tests/c/parsing/test_c_parser_properties.py rename to tests/c/infrastructure/parsing/test_c_parser_properties.py diff --git a/tests/c/parsing/test_c_project_resolution.py b/tests/c/infrastructure/parsing/test_c_project_resolution.py similarity index 100% rename from tests/c/parsing/test_c_project_resolution.py rename to tests/c/infrastructure/parsing/test_c_project_resolution.py diff --git a/tests/c/parsing/test_c_public_api_skeleton.py b/tests/c/infrastructure/parsing/test_c_public_api_skeleton.py similarity index 100% rename from tests/c/parsing/test_c_public_api_skeleton.py rename to tests/c/infrastructure/parsing/test_c_public_api_skeleton.py diff --git a/tests/c/preprocessing/test_c_preprocessing_cli.py b/tests/c/infrastructure/preprocessing/test_c_preprocessing_cli.py similarity index 100% rename from tests/c/preprocessing/test_c_preprocessing_cli.py rename to tests/c/infrastructure/preprocessing/test_c_preprocessing_cli.py diff --git a/tests/c/preprocessing/test_c_preprocessing_configuration.py b/tests/c/infrastructure/preprocessing/test_c_preprocessing_configuration.py similarity index 100% rename from tests/c/preprocessing/test_c_preprocessing_configuration.py rename to tests/c/infrastructure/preprocessing/test_c_preprocessing_configuration.py diff --git a/tests/c/preprocessing/test_c_preprocessing_dependencies.py b/tests/c/infrastructure/preprocessing/test_c_preprocessing_dependencies.py similarity index 100% rename from tests/c/preprocessing/test_c_preprocessing_dependencies.py rename to tests/c/infrastructure/preprocessing/test_c_preprocessing_dependencies.py diff --git a/tests/c/preprocessing/test_c_preprocessing_execution.py b/tests/c/infrastructure/preprocessing/test_c_preprocessing_execution.py similarity index 100% rename from tests/c/preprocessing/test_c_preprocessing_execution.py rename to tests/c/infrastructure/preprocessing/test_c_preprocessing_execution.py diff --git a/tests/c/preprocessing/test_c_preprocessing_properties.py b/tests/c/infrastructure/preprocessing/test_c_preprocessing_properties.py similarity index 100% rename from tests/c/preprocessing/test_c_preprocessing_properties.py rename to tests/c/infrastructure/preprocessing/test_c_preprocessing_properties.py diff --git a/tests/c/preprocessing/test_error_paths.py b/tests/c/infrastructure/preprocessing/test_error_paths.py similarity index 100% rename from tests/c/preprocessing/test_error_paths.py rename to tests/c/infrastructure/preprocessing/test_error_paths.py diff --git a/tests/c/preprocessing/test_source_mappings.py b/tests/c/infrastructure/preprocessing/test_source_mappings.py similarity index 100% rename from tests/c/preprocessing/test_source_mappings.py rename to tests/c/infrastructure/preprocessing/test_source_mappings.py diff --git a/tests/c/semantics/conversion/test_c_conversion_properties.py b/tests/c/infrastructure/semantic_ir/semantics/test_c_conversion_properties.py similarity index 100% rename from tests/c/semantics/conversion/test_c_conversion_properties.py rename to tests/c/infrastructure/semantic_ir/semantics/test_c_conversion_properties.py diff --git a/tests/c/semantics/conversion/test_projects_and_diagnostics.py b/tests/c/infrastructure/semantic_ir/semantics/test_projects_and_diagnostics.py similarity index 99% rename from tests/c/semantics/conversion/test_projects_and_diagnostics.py rename to tests/c/infrastructure/semantic_ir/semantics/test_projects_and_diagnostics.py index cbfeb52bf..6c95afa23 100644 --- a/tests/c/semantics/conversion/test_projects_and_diagnostics.py +++ b/tests/c/infrastructure/semantic_ir/semantics/test_projects_and_diagnostics.py @@ -27,7 +27,7 @@ c_type_to_semantic_type, ) from prik.semantics.models import SemanticArgument, SemanticModule, SemanticOrigin, SemanticType -from tests.c.semantics.conversion._support import ( +from tests.c._support.semantic_conversion import ( _assert_c_origin, _function, ) diff --git a/tests/c/pipeline/test_c_pyi_contract_fixtures.py b/tests/c/infrastructure/semantic_pyi/pipeline/test_c_pyi_contract_fixtures.py similarity index 100% rename from tests/c/pipeline/test_c_pyi_contract_fixtures.py rename to tests/c/infrastructure/semantic_pyi/pipeline/test_c_pyi_contract_fixtures.py diff --git a/tests/c/semantics/conversion/test_c_pyi_conversion.py b/tests/c/infrastructure/semantic_pyi/semantics/test_c_pyi_conversion.py similarity index 100% rename from tests/c/semantics/conversion/test_c_pyi_conversion.py rename to tests/c/infrastructure/semantic_pyi/semantics/test_c_pyi_conversion.py diff --git a/tests/c/primitive_pointers/end_to_end/test_direct_c_pointer_contracts.py b/tests/c/primitive_pointers/end_to_end/test_direct_c_pointer_contracts.py new file mode 100644 index 000000000..39a03ce01 --- /dev/null +++ b/tests/c/primitive_pointers/end_to_end/test_direct_c_pointer_contracts.py @@ -0,0 +1,160 @@ +"""Compiled scalar-reference and NumPy-array contracts for one-level C pointers.""" + +import shutil +import warnings +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_pyi_extension +from tests.c._support.runtime import sole_native_module + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_pointer_supports_default_scalar_reference_and_edited_c_array_contracts(tmp_path: Path): + contract = tmp_path / "pointers.pyi" + contract.write_text( + """from prik.contracts import Addr, Arg, Float64, Int32, Returns, native_call + +@native_call([Addr(Arg(0))]) +def scale_scalar(value: Float64) -> Returns["value", Float64]: ... + +def scale_zero(value: Float64[()]) -> None: ... + +def scale_vector(values: Float64[n], n: Int32) -> None: ... + +def scale_matrix(values: Float64[2, 2]) -> None: ... +""", + encoding="utf-8", + ) + source = tmp_path / "pointers.c" + source.write_text( + """void scale_scalar(double *value) { *value *= 2.0; } +void scale_zero(double *value) { *value += 1.0; } +void scale_vector(double *values, int n) { for (int i = 0; i < n; ++i) values[i] *= 3.0; } +void scale_matrix(double *values) { for (int i = 0; i < 4; ++i) values[i] += 1.0; } +""", + encoding="utf-8", + ) + + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build", + ) + module = sole_native_module(result.import_module()) + + assert module.scale_scalar(np.float64(2.5)) == np.float64(5.0) + zero = np.array(4.0, dtype=np.float64) + assert module.scale_zero(zero) is None + assert zero[()] == np.float64(5.0) + values = np.array([1.0, 2.0, 3.0], dtype=np.float64) + assert module.scale_vector(values, np.int32(3)) is None + np.testing.assert_allclose(values, np.array([3.0, 6.0, 9.0])) + empty = np.empty(0, dtype=np.float64) + assert module.scale_vector(empty, np.int32(0)) is None + matrix = np.array([[1.0, 2.0], [3.0, 4.0]], dtype=np.float64, order="C") + assert module.scale_matrix(matrix) is None + np.testing.assert_allclose(matrix, np.array([[2.0, 3.0], [4.0, 5.0]])) + with pytest.raises(TypeError, match=r"expected ordering \(C\)"): + module.scale_matrix(np.asfortranarray(matrix)) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_edited_c_array_contract_can_derive_the_native_extent_from_its_shape(tmp_path: Path): + """The documented promotion hides the count behind ``Arg(0).shape[0]``. + + The derived extent is a binding-owned producer, so it keeps its own + ``size_t`` identity while the promoted buffer crosses by address. + """ + contract = tmp_path / "promotion.pyi" + contract.write_text( + """from prik.contracts import Arg, Float64, native_call + +@native_call([Arg(0).shape[0], Arg(0)]) +def scale(values: Float64[:]) -> None: ... +""", + encoding="utf-8", + ) + source = tmp_path / "promotion.c" + source.write_text( + """#include +void scale(size_t n, double *values) { for (size_t i = 0; i < n; ++i) values[i] *= 2.0; } +""", + encoding="utf-8", + ) + + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build", + ) + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + assert "void scale(size_t shape_0, double * values);" in binding + assert all(path.suffix != ".f90" for path in result.generated_sources) + values = np.array([1.0, 2.0, 3.0], dtype=np.float64) + assert module.scale(values) is None + np.testing.assert_allclose(values, np.array([2.0, 4.0, 6.0])) + assert module.scale(np.empty(0, dtype=np.float64)) is None + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_exact_long_long_scalar_address_converts_while_arrays_require_native_storage(tmp_path: Path): + contract = tmp_path / "exact_long_long.pyi" + contract.write_text( + """from prik.contracts import Addr, Arg, CLongLong, Int32, Int64, Returns, native_call + +@native_call([Addr(CLongLong(Arg(0)))]) +def increment_scalar(value: Int64) -> Returns["value", Int64]: ... + +@native_call([CLongLong(Arg(0)), Arg(1)]) +def increment(values: Int64[:], count: Int32) -> None: ... + +@native_call([CLongLong(Arg(0))]) +def increment_zero(value: Int64[()]) -> None: ... +""", + encoding="utf-8", + ) + source = tmp_path / "exact_long_long.c" + source.write_text( + """void increment_scalar(long long *value) { *value += 1; } +void increment(long long *values, int count) { + for (int i = 0; i < count; ++i) values[i] += 1; +} +void increment_zero(long long *value) { *value += 1; } +""", + encoding="utf-8", + ) + + with warnings.catch_warnings(): + warnings.simplefilter("error") + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build", + ) + module = sole_native_module(result.import_module()) + + scalar = module.increment_scalar(np.int64(4)) + assert scalar == np.int64(5) + assert scalar.dtype == np.dtype(np.int64) + + values = np.array([1, 2, 3], dtype=np.longlong) + assert module.increment(values, np.int32(values.size)) is None + np.testing.assert_array_equal(values, np.array([2, 3, 4], dtype=np.longlong)) + + zero = np.array(4, dtype=np.longlong) + assert module.increment_zero(zero) is None + assert zero[()] == np.longlong(5) + + if np.dtype(np.int64).num != np.dtype(np.longlong).num: + with pytest.raises(TypeError, match=r"numpy\.longlong"): + module.increment(np.array([1, 2, 3], dtype=np.int64), np.int32(3)) + with pytest.raises(TypeError, match=r"numpy\.longlong"): + module.increment_zero(np.array(4, dtype=np.int64)) diff --git a/tests/c/primitive_pointers/end_to_end/test_direct_c_pointer_matrix.py b/tests/c/primitive_pointers/end_to_end/test_direct_c_pointer_matrix.py new file mode 100644 index 000000000..5a572c87b --- /dev/null +++ b/tests/c/primitive_pointers/end_to_end/test_direct_c_pointer_matrix.py @@ -0,0 +1,175 @@ +"""Compiled all-primitive evidence for the conservative C pointer contracts.""" + +import shutil +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_c_extension, build_pyi_extension +from prik.parsers.c import parse_c_file +from prik.preprocessing import PreprocessingConfig +from prik.preprocessing.probes.c_types import probe_c_standard_types +from prik.semantics.c2ir import c_file_to_semantic_module +from tests.c._support.runtime import sole_native_module + + +_C_PRIMITIVES = ( + ("bool", "_Bool"), + ("char", "char"), + ("signed_char", "signed char"), + ("unsigned_char", "unsigned char"), + ("short", "short"), + ("unsigned_short", "unsigned short"), + ("int", "int"), + ("unsigned_int", "unsigned int"), + ("long", "long"), + ("unsigned_long", "unsigned long"), + ("long_long", "long long"), + ("unsigned_long_long", "unsigned long long"), + ("float", "float"), + ("double", "double"), + ("long_double", "long double"), + ("float_complex", "float _Complex"), + ("double_complex", "double _Complex"), + ("long_double_complex", "long double _Complex"), + ("size", "size_t"), +) +_VALUES = { + "Bool": True, + "Bool8": True, + "Int8": np.int8(-7), + "UInt8": np.uint8(7), + "Int16": np.int16(-300), + "UInt16": np.uint16(300), + "Int32": np.int32(-70000), + "UInt32": np.uint32(70000), + "Int64": np.int64(-7000000000), + "UInt64": np.uint64(7000000000), + "Float32": np.float32(1.25), + "Float64": np.float64(1.25), + "Float128": np.longdouble("1.25"), + "Complex64": np.complex64(1.25 + 2.5j), + "Complex128": np.complex128(1.25 + 2.5j), + "Complex256": np.clongdouble(1.25 + 2.5j), +} + + +def _pointer_source() -> str: + declarations = ["#include ", "#include "] + for name, c_type in _C_PRIMITIVES: + declarations.extend( + ( + f"{c_type} pointer_read_{name}({c_type} *value) {{ return *value; }}", + f"{c_type} const_pointer_read_{name}(const {c_type} *value) {{ return *value; }}", + ) + ) + return "\n".join(declarations) + "\n" + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_every_c_primitive_pointer_defaults_to_one_call_local_scalar(tmp_path: Path): + """Source C retains ``T *``/``const T *`` as scalar ``Addr(Arg(i))`` calls.""" + source = tmp_path / "pointer_defaults.c" + source.write_text(_pointer_source(), encoding="utf-8") + report = probe_c_standard_types(PreprocessingConfig(mode="compiler", compiler="cc")) + semantic = c_file_to_semantic_module(parse_c_file(source), standard_type_report=report) + result_types = {function.name: function.return_type.dtype for function in semantic.functions} + # A typedef spelling such as ``size_t`` is declared through the builtin the + # probe resolved it to, because the binding writes the prototype itself. + declared_types = { + c_type: str(report.types.get(c_type, {}).get("underlying_c_type") or c_type) for _name, c_type in _C_PRIMITIVES + } + + result = build_c_extension(source, output_dir=tmp_path / "build", output_name="pointer_defaults") + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + for name, c_type in _C_PRIMITIVES: + value = _VALUES[result_types[f"pointer_read_{name}"]] + for prefix in ("pointer_read", "const_pointer_read"): + output = getattr(module, f"{prefix}_{name}")(value) + if type(value) is bool: + assert type(output) is bool + assert output is value + else: + assert output.dtype == np.asarray(value).dtype + assert output == value + declared = declared_types[c_type] + assert f"{declared} pointer_read_{name}({declared} * value);" in binding + assert f"{declared} const_pointer_read_{name}(const {declared} * value);" in binding + + +def _source_free_pointer_contract(type_names: tuple[str, ...]) -> str: + imports = ", ".join((*type_names, "Arg", "Return", "native_call")) + declarations = [f"from prik.contracts import {imports}"] + for type_name in type_names: + declarations.extend( + ( + "", + '@native_call([Arg(0), Return("output", 0)])', + f"def hidden_{type_name.lower()}(value: {type_name}) -> {type_name}: ...", + "", + f"def rank_zero_{type_name.lower()}(value: {type_name}[()]) -> None: ...", + ) + ) + return "\n".join(declarations) + "\n" + + +def _source_free_pointer_implementation(type_to_c_type: dict[str, str]) -> str: + declarations = ["#include ", "#include "] + for type_name, c_type in type_to_c_type.items(): + suffix = type_name.lower() + declarations.append(f"void hidden_{suffix}({c_type} value, {c_type} *output) {{ *output = value; }}") + if type_name in {"Bool", "Bool8"}: + declarations.append(f"void rank_zero_{suffix}({c_type} *value) {{ *value = !*value; }}") + else: + declarations.append(f"void rank_zero_{suffix}({c_type} *value) {{ *value += ({c_type})1; }}") + return "\n".join(declarations) + "\n" + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_every_c_primitive_supports_rank_zero_storage_and_hidden_output(tmp_path: Path): + """Authoritative C contracts select the rank-zero and ``Return`` mechanisms.""" + source = tmp_path / "pointer_contracts.c" + source.write_text(_pointer_source(), encoding="utf-8") + report = probe_c_standard_types(PreprocessingConfig(mode="compiler", compiler="cc")) + semantic = c_file_to_semantic_module(parse_c_file(source), standard_type_report=report) + type_to_c_type: dict[str, str] = {} + for function in semantic.functions: + if function.name.startswith("const_pointer_read_"): + continue + name = function.name.removeprefix("pointer_read_") + type_to_c_type.setdefault(function.return_type.dtype, dict(_C_PRIMITIVES)[name]) + + contract = tmp_path / "pointer_contracts.pyi" + type_names = tuple(sorted(type_to_c_type)) + contract.write_text(_source_free_pointer_contract(type_names), encoding="utf-8") + source.write_text(_source_free_pointer_implementation(type_to_c_type), encoding="utf-8") + + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build", + ) + module = sole_native_module(result.import_module()) + + for type_name in type_names: + value = _VALUES[type_name] + hidden = getattr(module, f"hidden_{type_name.lower()}") + rank_zero = getattr(module, f"rank_zero_{type_name.lower()}") + output = hidden(value) + if type_name in {"Bool", "Bool8"}: + assert type(output) is bool + assert output is bool(value) + else: + assert output.dtype == np.asarray(value).dtype + assert output == value + + storage = np.array(value) + assert rank_zero(storage) is None + if type_name in {"Bool", "Bool8"}: + assert storage[()] == (not bool(value)) + else: + assert storage[()] == np.asarray(value + 1, dtype=storage.dtype)[()] diff --git a/tests/c/primitive_pointers/fixtures/native/starter_contracts.c b/tests/c/primitive_pointers/fixtures/native/starter_contracts.c new file mode 100644 index 000000000..340f86c38 --- /dev/null +++ b/tests/c/primitive_pointers/fixtures/native/starter_contracts.c @@ -0,0 +1,6 @@ +double by_value(double value); +void scalar_reference(double *value); +void const_scalar_reference(const double *value); +void unsupported_multiple_reference(double **value); +double primitive_result(void); +double *unsupported_pointer_result(void); diff --git a/tests/c/primitive_pointers/semantics/test_starter_contracts.py b/tests/c/primitive_pointers/semantics/test_starter_contracts.py new file mode 100644 index 000000000..bb9e74e21 --- /dev/null +++ b/tests/c/primitive_pointers/semantics/test_starter_contracts.py @@ -0,0 +1,40 @@ +"""Public C starter contracts preserve pointer ambiguity for author edits.""" + +import subprocess +import sys +from pathlib import Path + +from tests.c._support.paths import REPO_ROOT + + +def test_c_starter_contract_preserves_every_documented_pointer_row(tmp_path: Path): + source = REPO_ROOT / "tests/c/primitive_pointers/fixtures/native/starter_contracts.c" + output = tmp_path / "starter_contracts.pyi" + + subprocess.run( + [ + sys.executable, + "-m", + "prik", + "generate", + "--pyi", + str(source), + "--language", + "c", + "--out", + str(output), + ], + cwd=REPO_ROOT, + check=True, + capture_output=True, + text=True, + ) + + contract = output.read_text(encoding="utf-8") + assert "def by_value(\n value: Float64\n) -> Float64" in contract + assert contract.count("@native_call([Addr(Arg(0))])") == 2 + assert "def scalar_reference(\n value: Float64\n) -> None" in contract + assert "def const_scalar_reference(\n value: Float64\n) -> None" in contract + assert "def unsupported_multiple_reference(\n value: Addr[2](Float64)\n) -> None" in contract + assert "def primitive_result() -> Float64" in contract + assert "def unsupported_pointer_result() -> Addr(Float64)" in contract diff --git a/tests/c/primitive_scalars/codegen/test_direct_c_codegen.py b/tests/c/primitive_scalars/codegen/test_direct_c_codegen.py new file mode 100644 index 000000000..34ca13b27 --- /dev/null +++ b/tests/c/primitive_scalars/codegen/test_direct_c_codegen.py @@ -0,0 +1,23 @@ +"""C declaration provenance is consumed by direct binding generation.""" + +from prik.parsers.c import parse_c_file +from prik.pipeline.wrapper import WrapperGenerator +from prik.planning import WrapperPlanner +from prik.policy.completion import complete_semantic_policies +from prik.semantics.c2ir import c_file_to_semantic_module + + +def test_direct_c_binding_keeps_qualifiers_pointer_depth_and_user_symbol(): + module = c_file_to_semantic_module( + parse_c_file("double native_read(const double *input) { return *input; }", filename="read.c") + ) + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + generated = WrapperGenerator().generate(plan) + binding = next(source.text for source in generated.sources if source.path.suffix == ".c") + + assert plan.bridge is None + assert plan.entrypoint.native_languages == ("c",) + assert "double native_read(const double * input);" in binding + assert "native_read(&bound_input)" in binding + assert "bind_c_read_wrapper" not in binding diff --git a/tests/c/primitive_scalars/codegen/test_exact_native_scalar_lowering.py b/tests/c/primitive_scalars/codegen/test_exact_native_scalar_lowering.py new file mode 100644 index 000000000..44f0cd282 --- /dev/null +++ b/tests/c/primitive_scalars/codegen/test_exact_native_scalar_lowering.py @@ -0,0 +1,125 @@ +"""Binding lowering consumes exact scalar types completed before planning.""" + +import pytest + +from prik.pipeline.pyi import pyi_text_to_semantic_module +from prik.pipeline.wrapper import WrapperGenerator +from prik.planning import WrapperPlanner +from prik.policy.completion import complete_semantic_policies +from prik.semantics.native_contract import validate_pyi_native_contract + + +def _binding(text: str) -> str: + module = pyi_text_to_semantic_module(text, module_name="exact", native_language="c") + validate_pyi_native_contract([module]) + complete_semantic_policies(module) + generated = WrapperGenerator().generate(WrapperPlanner().build(module)) + return next(source.text for source in generated.sources if source.path.suffix == ".c") + + +def _plan_and_binding(text: str): + module = pyi_text_to_semantic_module(text, module_name="exact", native_language="c") + validate_pyi_native_contract([module]) + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + generated = WrapperGenerator().generate(plan) + binding = next(source.text for source in generated.sources if source.path.suffix == ".c") + return plan, binding + + +def test_exact_value_argument_and_result_use_native_prototype_and_directional_casts(): + binding = _binding( + """from prik.contracts import Arg, CLongLong, Int64, Return, native_call +@native_call([CLongLong(Arg(0))], result=CLongLong(Return(0))) +def convert(value: Int64) -> Int64: ... +""" + ) + + assert "long long convert(long long value);" in binding + assert "result = (int64_t)convert((long long)bound_value);" in binding + + +def test_exact_address_argument_materializes_native_storage_before_taking_its_address(): + binding = _binding( + """from prik.contracts import Addr, Arg, CLongLong, Int64, Returns, native_call +@native_call([Addr(CLongLong(Arg(0)))]) +def update(value: Int64) -> Returns["value", Int64]: ... +""" + ) + + assert "void update(long long * value);" in binding + assert "long long bound_value;" in binding + assert "bound_value = (long long)bound_value_converted;" in binding + assert "update(&bound_value);" in binding + assert "int64_t bound_value_contract = (int64_t)bound_value;" in binding + assert "prik_int64_to_numpy(&bound_value_contract)" in binding + + +def test_exact_output_parameter_uses_native_storage_then_converts_the_python_result(): + binding = _binding( + """from prik.contracts import CLongLong, Int64, Return, native_call +@native_call([CLongLong(Return("out", 0))]) +def read() -> Int64: ... +""" + ) + + assert "void read(long long * out);" in binding + assert "long long out;" in binding + assert "read(&out);" in binding + assert "int64_t out_contract = (int64_t)out;" in binding + + +@pytest.mark.parametrize( + ("native_type", "annotation", "c_type", "numpy_macro", "numpy_name"), + [ + ("CChar", "Int8", "char", "NPY_BYTE", "numpy.byte"), + ("CSignedChar", "Int8", "signed char", "NPY_BYTE", "numpy.byte"), + ("CUnsignedChar", "UInt8", "unsigned char", "NPY_UBYTE", "numpy.ubyte"), + ("CShort", "Int16", "short", "NPY_SHORT", "numpy.short"), + ("CUnsignedShort", "UInt16", "unsigned short", "NPY_USHORT", "numpy.ushort"), + ("CInt", "Int32", "int", "NPY_INT", "numpy.intc"), + ("CUnsignedInt", "UInt32", "unsigned int", "NPY_UINT", "numpy.uintc"), + ("CLong", "Int64", "long", "NPY_LONG", "numpy.long"), + ("CUnsignedLong", "UInt64", "unsigned long", "NPY_ULONG", "numpy.ulong"), + ("CLongLong", "Int64", "long long", "NPY_LONGLONG", "numpy.longlong"), + ( + "CUnsignedLongLong", + "UInt64", + "unsigned long long", + "NPY_ULONGLONG", + "numpy.ulonglong", + ), + ("CFloat", "Float32", "float", "NPY_FLOAT", "numpy.single"), + ("CDouble", "Float64", "double", "NPY_DOUBLE", "numpy.double"), + ("CLongDouble", "Float128", "long double", "NPY_LONGDOUBLE", "numpy.longdouble"), + ("CFloatComplex", "Complex64", "float _Complex", "NPY_CFLOAT", "numpy.csingle"), + ("CDoubleComplex", "Complex128", "double _Complex", "NPY_CDOUBLE", "numpy.cdouble"), + ( + "CLongDoubleComplex", + "Complex256", + "long double _Complex", + "NPY_CLONGDOUBLE", + "numpy.clongdouble", + ), + ], +) +def test_exact_native_array_types_require_the_corresponding_numpy_c_storage( + native_type, + annotation, + c_type, + numpy_macro, + numpy_name, +): + plan, binding = _plan_and_binding( + f"""from prik.contracts import Arg, {native_type}, {annotation}, native_call +@native_call([{native_type}(Arg(0))]) +def update(values: {annotation}[:]) -> None: ... +""" + ) + function = plan.namespaces[0].functions[0] + + assert function.binding.docstring is not None + assert f"Accepts exact {numpy_name} element storage" in function.binding.docstring + assert f"void update({c_type} * values);" in binding + assert f"prik_array_validate_ndarray((PyArrayObject *)bound_values_obj, {numpy_macro}," in binding + assert f'"{numpy_name}", "values")' in binding diff --git a/tests/c/primitive_scalars/end_to_end/test_direct_c_runtime.py b/tests/c/primitive_scalars/end_to_end/test_direct_c_runtime.py new file mode 100644 index 000000000..cae950125 --- /dev/null +++ b/tests/c/primitive_scalars/end_to_end/test_direct_c_runtime.py @@ -0,0 +1,310 @@ +"""Compiled direct-C primitive scalar evidence.""" + +import shutil +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_c_extension, build_pyi_extension +from tests.c._support.runtime import sole_native_module + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_source_build_calls_renamed_user_symbol_without_a_fortran_adapter(tmp_path: Path): + source = tmp_path / "scalar_api.c" + source.write_text( + """double native_add(double left, double right) { return left + right; } +double native_scale(double *value) { *value *= 2.0; return *value; } +""", + encoding="utf-8", + ) + + result = build_c_extension(source, output_dir=tmp_path / "build", output_name="c_scalar_api") + module = sole_native_module(result.import_module()) + + assert module.native_add(np.float64(1.5), np.float64(2.0)) == np.float64(3.5) + assert module.native_scale(np.float64(3.0)) == np.float64(6.0) + assert all(path.suffix != ".f90" for path in result.generated_sources) + binding = next(path for path in result.generated_sources if path.suffix == ".c") + text = binding.read_text(encoding="utf-8") + assert "double native_add(double left, double right);" in text + assert "native_add(" in text + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_native_language_is_explicit_for_a_source_free_pyi_contract(tmp_path: Path): + contract = tmp_path / "contract.pyi" + contract.write_text( + """from prik.contracts import Float64, Int, bind + +@bind("native_add") +def add(left: Float64, right: Float64) -> Float64: ... + +@bind("native_increment") +def increment(value: Int) -> Int: ... +""", + encoding="utf-8", + ) + source = tmp_path / "implementation.c" + source.write_text( + """double native_add(double left, double right) { return left + right; } +int native_increment(int value) { return value + 1; } +""", + encoding="utf-8", + ) + + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build", + ) + module = sole_native_module(result.import_module()) + + assert module.add(np.float64(4.0), np.float64(2.5)) == np.float64(6.5) + assert module.increment(np.int32(4)) == np.int32(5) + assert result.native_build_plan.compilation_units[0].language == "c" + assert result.manifest["extension"]["native_language"] == "c" + assert result.manifest["compiler"]["c_flags"] == [] + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_contract_defaults_matching_python_name_to_native_symbol(tmp_path: Path): + """A C contract needs ``@bind`` only when the names differ.""" + contract = tmp_path / "matching_name.pyi" + contract.write_text( + """from prik.contracts import Int32 + +def increment(value: Int32) -> Int32: ... +""", + encoding="utf-8", + ) + source = tmp_path / "matching_name.c" + source.write_text("int increment(int value) { return value + 1; }\n", encoding="utf-8") + + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build", + ) + module = sole_native_module(result.import_module()) + + assert module.increment(np.int32(4)) == np.int32(5) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + assert "int32_t increment(int32_t value);" in binding + assert "result = increment(bound_value);" in binding + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_contract_reuses_direct_projection_value_address_literal_and_hidden_output_paths(tmp_path: Path): + contract = tmp_path / "projection.pyi" + contract.write_text( + """from prik.contracts import Addr, Arg, Int32, Return, Value, bind, native_call + +@bind("projected_native") +@native_call([Value(Arg(1)), Addr(Arg(0)), Int32(5)]) +def projected(left: Int32, right: Int32) -> Int32: ... + +@bind("projected_output_native") +@native_call([Value(Arg(1)), Addr(Arg(0)), Int32(5), Return("output", 0)]) +def projected_output(left: Int32, right: Int32) -> Int32: ... +""", + encoding="utf-8", + ) + source = tmp_path / "projection.c" + source.write_text( + """int projected_native(int right, int *left, int bias) { return 100 * right + 10 * *left + bias; } +void projected_output_native(int right, int *left, int bias, int *output) { + *output = 100 * right + 10 * *left + bias; +} +""", + encoding="utf-8", + ) + + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build", + ) + module = sole_native_module(result.import_module()) + + assert module.projected(np.int32(2), np.int32(3)) == np.int32(325) + assert module.projected_output(np.int32(2), np.int32(3)) == np.int32(325) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + assert "int32_t projected_native(int32_t right, int32_t * left, int32_t literal_2);" in binding + assert ( + "void projected_output_native(int32_t right, int32_t * left, int32_t literal_2, int32_t * output);" in binding + ) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_reordered_c_projection_keeps_each_argument_its_own_declared_type(tmp_path: Path): + """A route-neutral reorder must not resolve one argument against another.""" + contract = tmp_path / "reordered.pyi" + contract.write_text( + """from prik.contracts import Arg, Float64, Int32, native_call + +@native_call([Arg(1), Arg(0)]) +def combine(scale: Float64, count: Int32) -> Float64: ... +""", + encoding="utf-8", + ) + source = tmp_path / "reordered.c" + source.write_text("double combine(int count, double scale) { return count * scale; }\n", encoding="utf-8") + + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build", + ) + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + assert module.combine(np.float64(2.5), np.int32(4)) == np.float64(10.0) + assert "double combine(int32_t count, double scale);" in binding + with pytest.raises(TypeError, match=r"numpy\.float64 for argument scale"): + module.combine(np.int32(4), np.int32(4)) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_source_directives_are_expanded_before_the_wrapper_reads_declarations(tmp_path: Path): + """A C wrapper build preprocesses its sources like the inspection routes.""" + source = tmp_path / "directives.c" + source.write_text( + """#include +#define PRIK_TEST_GAIN 3.0 + +double scaled(double value) { return value * PRIK_TEST_GAIN; } +size_t total(size_t value) { return value + 1; } +""", + encoding="utf-8", + ) + + result = build_c_extension(source, output_dir=tmp_path / "build", output_name="c_directives") + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + assert module.scaled(np.float64(2.0)) == np.float64(6.0) + assert module.total(np.uint64(4)) == np.uint64(5) + # A typedef-written parameter declares the exact underlying builtin, which + # the binding can always spell; the typedef itself is source provenance. + assert "unsigned long total(unsigned long value);" in binding + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_default_c_pointer_scalar_documents_that_native_mutation_is_discarded(tmp_path: Path): + """The conservative ``T *`` default passes a call-local scalar address.""" + source = tmp_path / "discarded.c" + source.write_text("void twice(double *value) { *value *= 2.0; }\n", encoding="utf-8") + + result = build_c_extension(source, output_dir=tmp_path / "build", output_name="c_discarded") + module = sole_native_module(result.import_module()) + + value = np.float64(3.0) + assert module.twice(value) is None + assert value == np.float64(3.0) + assert "The update is not visible in Python." in module.twice.__doc__ + assert "update the supplied storage in place" not in module.twice.__doc__ + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_typedef_declarations_resolve_to_their_exact_underlying_builtin(tmp_path: Path): + """A user typedef only the source's headers define cannot enter the binding.""" + source = tmp_path / "aliases.c" + source.write_text( + """#include +typedef long my_int; +my_int alias_step(my_int value) { return value + 1; } +ptrdiff_t alias_offset(const ptrdiff_t *value) { return *value + 1; } +""", + encoding="utf-8", + ) + + result = build_c_extension(source, output_dir=tmp_path / "build", output_name="c_aliases") + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + assert "long alias_step(long value);" in binding + assert "long alias_offset(const long * value);" in binding + assert "my_int" not in binding + assert module.alias_step(np.int64(4)) == np.int64(5) + assert module.alias_offset(np.int64(4)) == np.int64(5) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_source_free_c_contract_keeps_the_standard_typedef_it_names(tmp_path: Path): + """``SizeT`` is a contract spelling, so the binding declares ``size_t``.""" + contract = tmp_path / "sizes.pyi" + contract.write_text( + """from prik.contracts import SizeT + +def total(value: SizeT) -> SizeT: ... +""", + encoding="utf-8", + ) + source = tmp_path / "sizes.c" + source.write_text( + """#include +size_t total(size_t value) { return value + 1; } +""", + encoding="utf-8", + ) + + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build", + ) + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + assert "size_t total(size_t value);" in binding + assert "#include " in binding + assert module.total(np.uint64(4)) == np.uint64(5) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_c_contract_supports_private_candidates_behind_one_overloaded_name(tmp_path: Path): + """An unexported concrete procedure is a shared contract feature, not a C limit.""" + contract = tmp_path / "overloads.pyi" + contract.write_text( + """from prik.contracts import Float64, Int32, overload, private + +@private +def scale_integer(value: Int32) -> Int32: ... + +@private +def scale_real(value: Float64) -> Float64: ... + +@overload("scale_integer") +def scale(value: Int32) -> Int32: ... + +@overload("scale_real") +def scale(value: Float64) -> Float64: ... +""", + encoding="utf-8", + ) + source = tmp_path / "overloads.c" + source.write_text( + """int scale_integer(int value) { return value * 2; } +double scale_real(double value) { return value * 2.0; } +""", + encoding="utf-8", + ) + + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build", + ) + module = sole_native_module(result.import_module()) + + assert module.scale(np.int32(21)) == np.int32(42) + assert module.scale(np.float64(1.5)) == np.float64(3.0) + assert [name for name in dir(module) if not name.startswith("_")] == ["scale"] diff --git a/tests/c/primitive_scalars/end_to_end/test_direct_c_scalar_matrix.py b/tests/c/primitive_scalars/end_to_end/test_direct_c_scalar_matrix.py new file mode 100644 index 000000000..7bbdacef7 --- /dev/null +++ b/tests/c/primitive_scalars/end_to_end/test_direct_c_scalar_matrix.py @@ -0,0 +1,87 @@ +"""Compiled Stage 3 arithmetic matrix for direct C entrypoints.""" + +import shutil +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_c_extension +from prik.parsers.c import parse_c_file +from prik.preprocessing import PreprocessingConfig +from prik.preprocessing.probes.c_types import probe_c_standard_types +from prik.semantics.c2ir import c_file_to_semantic_module +from tests.c._support.runtime import sole_native_module + + +_VALUES = { + "Bool": True, + "Bool8": True, + "Int8": np.int8(-7), + "UInt8": np.uint8(7), + "Int16": np.int16(-300), + "UInt16": np.uint16(300), + "Int32": np.int32(-70000), + "UInt32": np.uint32(70000), + "Int64": np.int64(-7000000000), + "UInt64": np.uint64(7000000000), + "Float32": np.float32(1.25), + "Float64": np.float64(1.25), + "Float128": np.longdouble("1.25"), + "Complex64": np.complex64(1.25 + 2.5j), + "Complex128": np.complex128(1.25 + 2.5j), + "Complex256": np.clongdouble(1.25 + 2.5j), +} + +_DTYPES = {name: np.asarray(value).dtype for name, value in _VALUES.items() if name not in {"Bool", "Bool8"}} + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_all_documented_c_arithmetic_spellings_return_exact_numpy_scalar_dtypes(tmp_path: Path): + source = tmp_path / "arithmetic.c" + source_text = """#include +#include +_Bool bool_identity(_Bool value) { return value; } +char char_identity(char value) { return value; } +signed char signed_char_identity(signed char value) { return value; } +unsigned char unsigned_char_identity(unsigned char value) { return value; } +short short_identity(short value) { return value; } +unsigned short unsigned_short_identity(unsigned short value) { return value; } +int int_identity(int value) { return value; } +unsigned int unsigned_int_identity(unsigned int value) { return value; } +long long_identity(long value) { return value; } +unsigned long unsigned_long_identity(unsigned long value) { return value; } +long long long_long_identity(long long value) { return value; } +unsigned long long unsigned_long_long_identity(unsigned long long value) { return value; } +float float_identity(float value) { return value; } +double double_identity(double value) { return value; } +long double long_double_identity(long double value) { return value; } +float _Complex float_complex_identity(float _Complex value) { return value; } +double _Complex double_complex_identity(double _Complex value) { return value; } +long double _Complex long_double_complex_identity(long double _Complex value) { return value; } +size_t size_identity(size_t value) { return value; } +void no_result(void) {} +""" + source.write_text(source_text, encoding="utf-8") + report = probe_c_standard_types(PreprocessingConfig(mode="compiler", compiler="cc")) + semantic = c_file_to_semantic_module(parse_c_file(source), standard_type_report=report) + expected = {function.name: function.return_type.dtype for function in semantic.functions if function.return_type} + + result = build_c_extension(source, output_dir=tmp_path / "build", output_name="c_arithmetic") + module = sole_native_module(result.import_module()) + + for function_name, dtype_name in expected.items(): + value = _VALUES[dtype_name] + output = getattr(module, function_name)(value) + if dtype_name in {"Bool", "Bool8"}: + assert type(output) is bool + else: + assert isinstance(output, np.generic) + assert output.dtype == _DTYPES[dtype_name] + assert output == value + assert module.no_result() is None + with pytest.raises(TypeError, match=r"numpy\.uint8"): + module.unsigned_char_identity(np.uint16(256)) + if np.dtype(np.int64).num != np.dtype(np.longlong).num: + with pytest.raises(TypeError, match=r"numpy\.int64"): + module.long_long_identity(np.longlong(1)) diff --git a/tests/c/primitive_scalars/policy/test_direct_c_policy.py b/tests/c/primitive_scalars/policy/test_direct_c_policy.py new file mode 100644 index 000000000..22370647a --- /dev/null +++ b/tests/c/primitive_scalars/policy/test_direct_c_policy.py @@ -0,0 +1,123 @@ +"""Completed-policy evidence for the direct-only C primitive lane.""" + +import pytest + +from prik.parsers.c import parse_c_file +from prik.policy.completion import complete_semantic_policies +from prik.semantics.c2ir import c_file_to_semantic_module +from prik.pipeline.pyi import pyi_text_to_semantic_module +from prik.semantics.native_contract import validate_pyi_native_contract + + +def _complete(source: str): + module = c_file_to_semantic_module(parse_c_file(source, filename="api.c")) + complete_semantic_policies(module) + return module.functions[0].metadata["resolved_function_wrapper_policy"] + + +def test_supported_c_scalar_policy_selects_direct_c_abi_without_a_bridge_facet(): + policy = _complete("double add(double left, double right) { return left + right; }\n") + + assert policy.supported is True + assert policy.entrypoint_action.value == "direct_c_abi" + assert policy.direct_c_abi.result.source_spelling == "double" + assert tuple(item.source_spelling for item in policy.direct_c_abi.parameters) == ("double", "double") + + +def test_source_free_exact_scalar_contract_completes_native_and_contract_storage_types(): + module = pyi_text_to_semantic_module( + """from prik.contracts import Arg, CLongLong, Int64, Return, native_call +@native_call([CLongLong(Arg(0))], result=CLongLong(Return(0))) +def convert(value: Int64) -> Int64: ... +""", + module_name="exact", + native_language="c", + ) + validate_pyi_native_contract([module]) + complete_semantic_policies(module) + + policy = module.functions[0].metadata["resolved_function_wrapper_policy"] + + assert policy.native_call_slots[0].native_scalar_c_type == "long long" + assert policy.direct_c_abi.parameters[0].source_spelling == "long long" + assert policy.direct_c_abi.result.source_spelling == "long long" + assert policy.direct_c_abi.result.converts_to_contract_storage is True + + +def test_source_free_exact_array_contract_requires_native_numpy_element_storage(): + module = pyi_text_to_semantic_module( + """from prik.contracts import Arg, CLongLong, Int64, native_call +@native_call([CLongLong(Arg(0))]) +def update(values: Int64[:]) -> None: ... +""", + module_name="exact_array", + native_language="c", + ) + validate_pyi_native_contract([module]) + complete_semantic_policies(module) + + policy = module.functions[0].metadata["resolved_function_wrapper_policy"] + + assert policy.arguments[0].native_array_element_c_type == "long long" + assert policy.native_call_slots[0].native_scalar_c_type == "long long" + assert policy.direct_c_abi.parameters[0].source_spelling == "long long *" + assert policy.direct_c_abi.parameters[0].converts_to_contract_storage is False + + +def test_exact_c_bool_rank_zero_storage_fails_before_planning(): + module = pyi_text_to_semantic_module( + """from prik.contracts import Arg, Bool, CBool, native_call +@native_call([CBool(Arg(0))]) +def update(value: Bool[()]) -> None: ... +""", + module_name="exact_bool_array", + native_language="c", + ) + validate_pyi_native_contract([module]) + + with pytest.raises(ValueError, match="C_DIRECT_BOOL_ARRAY:value"): + complete_semantic_policies(module) + + +@pytest.mark.parametrize( + ("source", "diagnostic"), + [ + ("double *result(void);", "C_DIRECT_POINTER_RESULT"), + ("void values(double input[3]);", "C_DIRECT_ARRAY_DECLARATOR:input"), + ("void indirect(double **input);", "C_DIRECT_POINTER_DEPTH:input"), + ("void callback(void (*action)(int));", "C_DIRECT_CALLBACK:action"), + ("struct state { int value; }; void consume(struct state value);", "C_DIRECT_UNRESOLVED_PRIMITIVE_ABI:value"), + ("int total(int first, ...);", "C_DIRECT_VARIADIC_FUNCTION"), + ("static double hidden(double value);", "C_DIRECT_TRANSLATION_UNIT_LOCAL_SYMBOL"), + ("void volatile_value(volatile double value);", "C_DIRECT_UNSUPPORTED_QUALIFIER:value"), + ("void atomic_value(_Atomic(int) value);", "C_DIRECT_UNSUPPORTED_QUALIFIER:value"), + ], +) +def test_ineligible_c_operations_fail_with_a_stable_preplanning_diagnostic(source: str, diagnostic: str): + module = c_file_to_semantic_module(parse_c_file(source, filename="unsupported.c")) + + with pytest.raises(ValueError, match=diagnostic): + complete_semantic_policies(module) + + +@pytest.mark.parametrize( + ("annotation", "diagnostic"), + [ + ("Addr(Float64)", "C_DIRECT_RAW_ADDRESS:value"), + ("Float64 | None", "C_DIRECT_NULLABLE_POINTER:value"), + ("Float64[:] | None", "C_DIRECT_NULLABLE_POINTER:value"), + ("Float64[()] | None", "C_DIRECT_NULLABLE_POINTER:value"), + ("Bool[:]", "C_DIRECT_BOOL_ARRAY:value"), + ], +) +def test_out_of_scope_c_pointer_contracts_fail_before_planning(annotation: str, diagnostic: str): + imports = "Addr, Bool, Float64" if "Addr" in annotation else "Bool, Float64" + module = pyi_text_to_semantic_module( + f"from prik.contracts import {imports}\ndef f(value: {annotation}) -> None: ...\n", + module_name="unsupported_contract", + native_language="c", + ) + validate_pyi_native_contract([module]) + + with pytest.raises(ValueError, match=diagnostic): + complete_semantic_policies(module) diff --git a/tests/c/primitive_scalars/semantics/test_exact_native_scalar_contract.py b/tests/c/primitive_scalars/semantics/test_exact_native_scalar_contract.py new file mode 100644 index 000000000..0798bd84a --- /dev/null +++ b/tests/c/primitive_scalars/semantics/test_exact_native_scalar_contract.py @@ -0,0 +1,107 @@ +"""Semantic C contracts preserve exact native scalar identities at call sites.""" + +import pytest + +from prik.contracts import NATIVE_C_SCALAR_CASTS +from prik.parsers.c import parse_c_file +from prik.pipeline.pyi import pyi_text_to_semantic_module +from prik.printers.pyi import emit_module +from prik.semantics.c2ir import c_file_to_semantic_module + + +_LP64_FACTS = { + "types": { + "long": {"kind": "integer", "signed": True, "bits": 64, "underlying_c_type": "long"}, + "long long": { + "kind": "integer", + "signed": True, + "bits": 64, + "underlying_c_type": "long long", + }, + "int64_t": {"kind": "integer", "signed": True, "bits": 64, "underlying_c_type": "long"}, + } +} + +_LLP64_FACTS = { + "types": { + "long": {"kind": "integer", "signed": True, "bits": 32, "underlying_c_type": "long"}, + "int32_t": {"kind": "integer", "signed": True, "bits": 32, "underlying_c_type": "int"}, + } +} + + +def test_target_generation_emits_only_the_native_identity_lost_by_width_normalization(): + module = c_file_to_semantic_module( + parse_c_file("long keep_long(long value); long long keep_ll(long long value);", filename="exact.h"), + standard_type_report=_LP64_FACTS, + ) + + text = emit_module(module) + + assert "def keep_long(" in text + assert "CLong(Arg(0))" not in text + assert "@native_call([CLongLong(Arg(0))], result=CLongLong(Return(0)))" in text + + +def test_same_width_long_and_int32_t_still_keep_their_distinct_c_identities(): + module = c_file_to_semantic_module( + parse_c_file("long convert(long value);", filename="exact.h"), + standard_type_report=_LLP64_FACTS, + ) + + text = emit_module(module) + + assert "@native_call([CLong(Arg(0))], result=CLong(Return(0)))" in text + assert "def convert(" in text + assert "value: Int32" in text + assert ") -> Int32" in text + + +def test_exact_native_argument_and_result_contract_round_trip(): + text = """from prik.contracts import Arg, CLongLong, Int64, Return, native_call +@native_call([CLongLong(Arg(0))], result=CLongLong(Return(0))) +def convert(value: Int64) -> Int64: ... +""" + + module = pyi_text_to_semantic_module(text, module_name="exact", native_language="c") + + assert module.functions[0].projection[0].native_cast == "CLongLong" + assert module.functions[0].return_type.metadata["native_c_scalar_cast"] == "CLongLong" + rendered = emit_module(module) + assert "@native_call([CLongLong(Arg(0))], result=CLongLong(Return(0)))" in rendered + + +def test_exact_native_array_element_contract_round_trips_without_a_public_c_type(): + text = """from prik.contracts import Arg, CLongLong, Int64, native_call +@native_call([CLongLong(Arg(0))]) +def update(values: Int64[:]) -> None: ... +""" + + module = pyi_text_to_semantic_module(text, module_name="exact_array", native_language="c") + + assert module.functions[0].projection[0].native_cast == "CLongLong" + rendered = emit_module(module) + assert "@native_call([CLongLong(Arg(0))])" in rendered + assert "values: Int64[:]" in rendered + + +def test_native_scalar_cast_requires_exactly_one_positional_reference(): + with pytest.raises(ValueError, match="CLongLong expects positional arguments only"): + pyi_text_to_semantic_module( + """from prik.contracts import Arg, CLongLong, Int64, native_call +@native_call([CLongLong(Arg(0), unexpected=True)]) +def invalid(value: Int64) -> None: ... +""", + module_name="invalid", + native_language="c", + ) + + +@pytest.mark.parametrize("native_name", sorted(NATIVE_C_SCALAR_CASTS)) +def test_native_scalar_names_are_not_public_signature_types(native_name): + with pytest.raises(ValueError, match="valid only inside @native_call"): + pyi_text_to_semantic_module( + f"from prik.contracts import {native_name}\ndef invalid(value: {native_name}) -> None: ...\n", + module_name="invalid", + native_language="c", + ) diff --git a/tests/c/primitive_strings/end_to_end/test_direct_c_strings.py b/tests/c/primitive_strings/end_to_end/test_direct_c_strings.py new file mode 100644 index 000000000..2df72e9d3 --- /dev/null +++ b/tests/c/primitive_strings/end_to_end/test_direct_c_strings.py @@ -0,0 +1,349 @@ +"""Compiled evidence for the adopted rank-zero C character contracts.""" + +import shutil +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_pyi_extension +from tests.c._support.runtime import sole_native_module + +SOURCE = """#include +#include + +int name_length(const char *text) { return (int)strlen(text); } + +void shout(const char *text, char *out) { + size_t index = 0; + for (; text[index]; ++index) { + char value = text[index]; + out[index] = (value >= 'a' && value <= 'z') ? (char)(value - 32) : value; + } + out[index] = '\\0'; +} +""" + + +def _build(tmp_path: Path, contract_text: str, name: str): + contract = tmp_path / f"{name}.pyi" + contract.write_text(contract_text, encoding="utf-8") + source = tmp_path / f"{name}.c" + source.write_text(SOURCE, encoding="utf-8") + return build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / f"build_{name}", + output_name=name, + ) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_string_input_borrows_the_python_payload_as_a_const_char_pointer(tmp_path: Path): + """``String`` states a read-only input, so the prototype keeps ``const``.""" + result = _build( + tmp_path, + "from prik.contracts import Int32, String\n\ndef name_length(text: String) -> Int32: ...\n", + "text_in", + ) + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + assert "int32_t name_length(const char * text);" in binding + assert module.name_length("hello") == np.int32(5) + assert module.name_length("") == np.int32(0) + with pytest.raises(TypeError, match="type str"): + module.name_length(b"bytes") + assert module.name_length("a\0b") == np.int32(1) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_rank_zero_string_storage_is_written_in_place_at_any_declared_capacity(tmp_path: Path): + """``String[...][()]`` passes the caller's bytes through untouched.""" + result = _build( + tmp_path, + "from prik.contracts import String\n\ndef shout(text: String, out: String[...][()]) -> None: ...\n", + "text_assumed", + ) + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + assert "void shout(const char * text, char * out);" in binding + for width in ("S8", "S32"): + buffer = np.array(b"", dtype=width) + assert module.shout("hello", buffer) is None + assert buffer[()] == b"HELLO" + with pytest.raises(TypeError, match=r"rank-zero numpy\.ndarray"): + module.shout("hi", np.array([1.0])) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_declared_string_capacity_validates_the_caller_itemsize(tmp_path: Path): + """``String[n][()]`` is the form that asks PRIK to check the width.""" + result = _build( + tmp_path, + "from prik.contracts import String\n\ndef shout(text: String, out: String[32][()]) -> None: ...\n", + "text_fixed", + ) + module = sole_native_module(result.import_module()) + + buffer = np.array(b"", dtype="S32") + assert module.shout("hello", buffer) is None + assert buffer[()] == b"HELLO" + with pytest.raises(TypeError, match="itemsize 32"): + module.shout("hello", np.array(b"", dtype="S8")) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_string_arrays_stay_outside_the_direct_c_lane(tmp_path: Path): + """Only rank-zero character contracts have a completed C lowering.""" + with pytest.raises(ValueError, match="C_DIRECT_UNSUPPORTED_STRING_CONTRACT:text"): + _build( + tmp_path, + "from prik.contracts import Int32, String\n\ndef name_length(text: String[8][:]) -> Int32: ...\n", + "text_array", + ) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_raises_message_uses_a_binding_owned_buffer_without_an_adapter(tmp_path: Path): + """Direct C owns the message buffer; only a bridged route allocates one.""" + contract = tmp_path / "checked.pyi" + contract.write_text( + """from prik.contracts import Arg, Float64, Hidden, Int32, Return, Returns, String, bind, native_call, raises + +@bind("checked_sqrt") +@raises(status="status", message="message", success=0) +@native_call([Arg(0), Return("root", 0), Hidden("status", Int32), Hidden("message", String[64])]) +def checked_sqrt(value: Float64) -> Returns["root", Float64]: ... +""", + encoding="utf-8", + ) + source = tmp_path / "checked.c" + source.write_text( + """#include + +void checked_sqrt(double value, double *root, int *status, char *message) { + if (value < 0.0) { + *status = -1; + *root = 0.0; + strcpy(message, "value must not be negative"); + return; + } + *status = 0; + message[0] = '\\0'; + *root = value == 4.0 ? 2.0 : value; +} +""", + encoding="utf-8", + ) + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build_message", + output_name="checked", + ) + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + # The callee receives the buffer itself, never the adapter's ``char **``. + assert "void checked_sqrt(double value, double * root, int32_t * status, char * message);" in binding + assert "char message[65]" in binding + assert "free(message)" not in binding + + assert module.checked_sqrt(np.float64(4.0)) == np.float64(2.0) + with pytest.raises(RuntimeError, match="value must not be negative"): + module.checked_sqrt(np.float64(-1.0)) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +@pytest.mark.parametrize("declaration", ["String", "String[...]", "String[:]"]) +def test_raises_message_without_a_declared_capacity_stays_fail_closed(tmp_path: Path, declaration: str): + """An assumed or deferred width leaves the binding no buffer size to emit. + + C has no adapter to allocate one, so every form that omits a fixed capacity + is refused by the language-neutral status-error rule before planning. + """ + contract = f"""from prik.contracts import Arg, Float64, Hidden, Int32, String, bind, native_call, raises + +@bind("checked") +@raises(status="status", message="message", success=0) +@native_call([Arg(0), Hidden("status", Int32), Hidden("message", {declaration})]) +def checked(value: Float64) -> None: ... +""" + with pytest.raises(ValueError, match="native status error message requires a fixed positive character length"): + _build(tmp_path, contract, "message") + + +CHECKED_SOURCE = """#include + +void checked(double value, char *message, int *status) { + if (value < 0.0) { + *status = -1; + snprintf(message, 64, "bad value %g", value); + return; + } + *status = 0; + message[0] = '\\0'; +} +""" + + +def _build_checked(tmp_path: Path, declaration: str, name: str): + contract = tmp_path / f"{name}.pyi" + contract.write_text( + f"""from prik.contracts import Arg, Float64, Hidden, Int32, String, bind, native_call, raises + +@bind("checked") +@raises(status="status", message="message", success=0) +@native_call([Arg(0), Arg(1), Hidden("status", Int32)]) +def checked(value: Float64, message: {declaration}) -> None: ... +""", + encoding="utf-8", + ) + source = tmp_path / f"{name}.c" + source.write_text(CHECKED_SOURCE, encoding="utf-8") + return build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / f"build_{name}", + output_name=name, + ) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_raises_message_reads_a_caller_supplied_buffer(tmp_path: Path): + """A visible ``String[n][()]`` message carries its own capacity.""" + result = _build_checked(tmp_path, "String[64][()]", "visible") + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + # The caller owns the buffer, so the binding neither NULL-checks nor frees it. + assert "free(bound_message)" not in binding + assert "void checked(double value, char * message, int32_t * status);" in binding + + buffer = np.array(b"", dtype="S64") + assert module.checked(np.float64(9.0), buffer) is None + assert buffer[()] == b"" + with pytest.raises(RuntimeError, match="bad value -1"): + module.checked(np.float64(-1.0), buffer) + # Raising does not consume the buffer; the caller can still inspect it. + assert buffer[()] == b"bad value -1" + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_raises_message_accepts_a_borrowed_string_payload(tmp_path: Path): + """``String`` states ``const char *``; PRIK does not police what C writes.""" + result = _build_checked(tmp_path, "String", "borrowed") + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + assert "void checked(double value, const char * message, int32_t * status);" in binding + + scratch = "\0" * 64 + assert module.checked(np.float64(9.0), scratch) is None + with pytest.raises(RuntimeError, match="bad value -1"): + module.checked(np.float64(-1.0), scratch) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_visible_message_needs_no_declared_capacity(tmp_path: Path): + """The caller's storage supplies the width a hidden message must declare.""" + result = _build_checked(tmp_path, "String[...][()]", "assumed") + module = sole_native_module(result.import_module()) + + buffer = np.array(b"", dtype="S64") + with pytest.raises(RuntimeError, match="bad value -2"): + module.checked(np.float64(-2.0), buffer) + + +PADDED_SOURCE = """void checked(double value, char *message, int *status) { + int index = 0; + const char *text = "padded failure"; + if (value >= 0.0) { *status = 0; message[0] = '\\0'; return; } + *status = -1; + /* Fill the whole buffer with blanks, exactly as fixed-length native + character storage does, and leave no terminator. */ + for (; index < 64; ++index) { message[index] = ' '; } + for (index = 0; text[index]; ++index) { message[index] = text[index]; } +} +""" + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_visible_message_never_reads_past_the_caller_capacity(tmp_path: Path): + """An unterminated buffer is read as padded storage, not scanned for a NUL.""" + contract = tmp_path / "padded.pyi" + contract.write_text( + """from prik.contracts import Arg, Float64, Hidden, Int32, String, bind, native_call, raises + +@bind("checked") +@raises(status="status", message="message", success=0) +@native_call([Arg(0), Arg(1), Hidden("status", Int32)]) +def checked(value: Float64, message: String[64][()]) -> None: ... +""", + encoding="utf-8", + ) + source = tmp_path / "padded.c" + source.write_text(PADDED_SOURCE, encoding="utf-8") + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build_padded", + output_name="padded", + ) + module = sole_native_module(result.import_module()) + + buffer = np.array(b"", dtype="S64") + with pytest.raises(RuntimeError, match=r"^padded failure$"): + module.checked(np.float64(-1.0), buffer) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_hidden_message_read_is_bounded_by_the_declared_capacity(tmp_path: Path): + """The binding reads at most the width the contract declared.""" + contract = tmp_path / "wide.pyi" + contract.write_text( + """from prik.contracts import Arg, Float64, Hidden, Int32, String, bind, native_call, raises + +@bind("wide") +@raises(status="status", message="message", success=0) +@native_call([Arg(0), Hidden("status", Int32), Hidden("message", String[8])]) +def wide(value: Float64) -> None: ... +""", + encoding="utf-8", + ) + source = tmp_path / "wide.c" + source.write_text( + """#include + +void wide(double value, int *status, char *message) { + if (value < 0.0) { + *status = -1; + /* Fill the declared width with no terminator inside it. */ + memset(message, 'x', 8); + return; + } + *status = 0; + message[0] = '\\0'; +} +""", + encoding="utf-8", + ) + result = build_pyi_extension( + contract, + native_language="c", + native_c_sources=[source], + output_dir=tmp_path / "build_wide", + output_name="wide", + ) + module = sole_native_module(result.import_module()) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + + assert "prik_status_message_text" in binding + with pytest.raises(RuntimeError, match=r"^x{8}$"): + module.wide(np.float64(-1.0)) diff --git a/tests/c/parsing/test_c_structs_unions_enums_typedefs.py b/tests/c/records/parsing/test_c_structs_unions_typedefs.py similarity index 92% rename from tests/c/parsing/test_c_structs_unions_enums_typedefs.py rename to tests/c/records/parsing/test_c_structs_unions_typedefs.py index b7d1eb17d..b508b84ca 100644 --- a/tests/c/parsing/test_c_structs_unions_enums_typedefs.py +++ b/tests/c/records/parsing/test_c_structs_unions_typedefs.py @@ -1,4 +1,4 @@ -"""C aggregate type, enum, and typedef parser tests.""" +"""C aggregate type and typedef parser tests.""" import pytest @@ -173,45 +173,6 @@ def test_repeated_union_and_enum_tags_normalize_with_duplicate_diagnostics(): ] -def test_enum_constants_preserve_explicit_implicit_and_symbolic_values(): - from prik.parsers.c import parse_c_file - - parsed = parse_c_file( - """ -enum status { - STATUS_OK = 0, - STATUS_WARN, - STATUS_ERROR = 10, - STATUS_NEXT = STATUS_ERROR + 1 -}; -""", - filename="enum.h", - ) - - assert [(item.name, item.value) for item in parsed.enums[0].constants] == [ - ("STATUS_OK", "0"), - ("STATUS_WARN", None), - ("STATUS_ERROR", "10"), - ("STATUS_NEXT", "STATUS_ERROR + 1"), - ] - - -def test_typedef_enum_and_trailing_tag_variable_are_separate_objects(): - from prik.parsers.c import CEnum, CStruct, parse_c_file - - parsed = parse_c_file( - "typedef enum { FLAG_NONE = 0, FLAG_READ = 1 } flag_t;\nstruct point { int x; } origin;\n", - filename="tag_declarators.h", - ) - - assert parsed.enums[0].anonymous_id - assert isinstance(parsed.typedefs[0].type, CEnum) - assert parsed.typedefs[0].type is parsed.enums[0] - assert parsed.variables[0].name == "origin" - assert isinstance(parsed.variables[0].type, CStruct) - assert parsed.variables[0].type is parsed.structs[0] - - def test_recursive_struct_pointer_uses_an_incomplete_struct_component_without_cycles(): from prik.parsers.c import CComposedType, CPointer, CStruct, parse_c_file diff --git a/tests/c/semantics/conversion/test_records_and_enums.py b/tests/c/records/semantics/test_c_record_semantics.py similarity index 73% rename from tests/c/semantics/conversion/test_records_and_enums.py rename to tests/c/records/semantics/test_c_record_semantics.py index 1b514d924..c10e26a03 100644 --- a/tests/c/semantics/conversion/test_records_and_enums.py +++ b/tests/c/records/semantics/test_c_record_semantics.py @@ -1,10 +1,8 @@ -"""Tests split by stable ownership concept from `test_functions_and_callbacks.py`.""" - -from dataclasses import asdict +"""C struct, union, and opaque-handle conversion into the semantic IR.""" from prik.pipeline.pyi import emit_module_stubs from prik.printers import emit_module -from prik.parsers.c import parse_c_file, parse_c_project +from prik.parsers.c import parse_c_file from prik.parsers.c.models import ( CArray, CComposedType, @@ -13,7 +11,6 @@ CFunction, CInitializer, CInt, - CMacro, CParameter, CPointer, CSourceLocation, @@ -28,8 +25,6 @@ CToIRConverter, c_file_to_semantic_module, c_file_to_semantic_modules, - c_project_to_semantic_module, - c_project_to_semantic_modules, ) from prik.semantics.models import ( SemanticArgument, @@ -38,9 +33,8 @@ SemanticModule, SemanticOrigin, SemanticType, - SemanticVariable, ) -from tests.c.semantics.conversion._support import ( +from tests.c._support.semantic_conversion import ( _assert_c_origin, _function, ) @@ -227,152 +221,6 @@ def test_c2ir_externalizes_only_private_opaque_classes_with_external_origins(): } -def test_c2ir_converts_enum_constants_and_simple_macro_constants(): - parsed = parse_c_file( - """ -enum status { STATUS_OK = 0, STATUS_WARN, STATUS_ERROR = 10 }; -""", - filename="constants.h", - ) - parsed.macros = [CMacro(name="API_VERSION", value="3")] - module = c_file_to_semantic_modules(parsed)[0] - - constants = {var.name: var for var in module.variables} - assert constants["API_VERSION"].default_value == "3" - assert constants["API_VERSION"].semantic_type.constraints[0].name == "Constant" - assert constants["STATUS_WARN"].default_value == "1" - assert constants["STATUS_ERROR"].default_value == "10" - api_version = constants["API_VERSION"] - assert isinstance(api_version, SemanticVariable) - assert api_version.semantic_type.name == "Int32" - assert api_version.semantic_type.dtype == "Int32" - assert [asdict(constraint) for constraint in api_version.semantic_type.constraints] == [ - {"name": "Constant", "arguments": []} - ] - _assert_c_origin( - api_version.origin, - native_name="API_VERSION", - source_kind="macro", - ) - status_ok = constants["STATUS_OK"] - assert module.classes == [] - assert status_ok.semantic_type.name == "Int" - assert status_ok.semantic_type.dtype == "Int32" - assert status_ok.semantic_type.metadata["enum_name"] == "status" - assert status_ok.semantic_type.metadata["c_kind"] == "enum" - assert status_ok.semantic_type.metadata["c_enum"] == "enum status" - assert status_ok.semantic_type.metadata["c_underlying_type"] == "Int" - assert status_ok.semantic_type.coercions == [] - _assert_c_origin( - status_ok.origin, - native_name="STATUS_OK", - native_scope="enum status", - source_kind="enum_constant", - source_location={ - "filename": "constants.h", - "line": 2, - "column": 1, - "source_line": "enum status { STATUS_OK = 0, STATUS_WARN, STATUS_ERROR = 10 };", - }, - ) - - -def test_c2ir_names_anonymous_typedef_enums_and_keeps_enumerators_unscoped(): - source = "typedef enum { FLAG_NONE = 0, FLAG_READ = 1 } flag_t; flag_t get_flags(void);" - parsed = parse_c_file(source, filename="flags.h") - - module = c_file_to_semantic_module(parsed) - project_module = c_project_to_semantic_module(parse_c_project({"flags.h": source}), name="flags") - - assert module.classes == [] - assert project_module.classes == [] - assert [variable.name for variable in module.variables] == ["FLAG_NONE", "FLAG_READ"] - assert [variable.name for variable in project_module.variables] == ["FLAG_NONE", "FLAG_READ"] - assert [variable.semantic_type.name for variable in module.variables] == ["Int", "Int"] - assert module.variables[0].semantic_type.metadata["enum_name"] == "flag_t" - assert _function(module, "get_flags").return_type.name == "Int" - assert _function(project_module, "get_flags").return_type.name == "Int" - - -def test_c2ir_enum_values_emit_only_python_compatible_expressions(): - parsed = parse_c_file( - "enum flags { FLAG_ONE = 1U, FLAG_OCTAL = 010, FLAG_SHIFT = FLAG_ONE << 1, FLAG_CHAR = 'A' };", - filename="flags.h", - ) - module = c_file_to_semantic_module(parsed) - - code = emit_module(module) - - assert "FLAG_ONE: Final[Int] = 1" in code - assert "FLAG_OCTAL: Final[Int] = 8" in code - assert "FLAG_SHIFT: Final[Int] = FLAG_ONE << 1" in code - assert "FLAG_CHAR: Final[Int]" in code - assert {variable.name: variable.default_value for variable in module.variables} == { - "FLAG_ONE": "1U", - "FLAG_OCTAL": "010", - "FLAG_SHIFT": "FLAG_ONE << 1", - "FLAG_CHAR": "'A'", - } - assert [variable.name for variable in parse_pyi_text(code, module_name="flags").variables] == [ - "FLAG_ONE", - "FLAG_OCTAL", - "FLAG_SHIFT", - "FLAG_CHAR", - ] - - -def test_c2ir_cross_header_enum_references_import_the_owner_enum(): - project = parse_c_project( - { - "types.h": "enum status { STATUS_OK = 0 };", - "api.h": "enum status get_status(void);", - } - ) - - modules = {module.name: module for module in c_project_to_semantic_modules(project)} - - assert modules["api"].classes == [] - assert modules["types"].classes == [] - assert _function(modules["api"], "get_status").return_type.name == "Int" - assert _function(modules["api"], "get_status").return_type.metadata["c_enum"] == "enum status" - - anonymous_project = parse_c_project( - { - "types.h": "typedef enum { FLAG_NONE = 0 } flag_t;", - "api.h": "flag_t get_flags(void);", - } - ) - anonymous_modules = {module.name: module for module in c_project_to_semantic_modules(anonymous_project)} - assert _function(anonymous_modules["api"], "get_flags").return_type.name == "Int" - - -def test_c2ir_uses_enum_specific_underlying_type_facts_when_supplied(): - parsed = parse_c_file( - "enum status { STATUS_OK = 0, STATUS_ERROR = 255 }; enum status get_status(void);", - filename="status.h", - ) - module = CToIRConverter( - standard_type_report={ - "types": { - "enum status": { - "available": True, - "kind": "integer", - "signed": False, - "bits": 8, - "underlying_c_type": "unsigned char", - } - } - } - ).visit(parsed) - - return_type = _function(module, "get_status").return_type - assert module.classes == [] - assert return_type.name == "UInt8" - assert return_type.dtype == "UInt8" - assert return_type.metadata["c_kind"] == "enum" - assert return_type.metadata["c_enum_type_fact_source"] == "compiler_probe" - - def test_c2ir_uses_standard_type_probe_opaque_handle_facts(): parsed = parse_c_file("void close_file(FILE *stream);\n", filename="stdio_api.h") converter = CToIRConverter( diff --git a/tests/c/symbol_collisions/codegen/test_collision_adapter_lowering.py b/tests/c/symbol_collisions/codegen/test_collision_adapter_lowering.py new file mode 100644 index 000000000..654dc83c2 --- /dev/null +++ b/tests/c/symbol_collisions/codegen/test_collision_adapter_lowering.py @@ -0,0 +1,105 @@ +"""A collision-adapted symbol is reached from a unit that excludes Python.h.""" + +from prik.parsers.c import parse_c_file +from prik.parsers.fortran import parse_fortran_file as parse_fortran_source +from prik.pipeline.pyi import pyi_text_to_semantic_module +from prik.pipeline.wrapper import WrapperGenerator +from prik.planning import WrapperPlanner +from prik.policy.completion import complete_semantic_policies +from prik.semantics.c2ir import c_file_to_semantic_module +from prik.semantics.fortran2ir import fortran_file_to_semantic_modules +from prik.semantics.native_contract import validate_pyi_native_contract + +_SOURCE = """long long native_round(double value) { return (long long)value; } +double native_add(double left, double right) { return left + right; } +""" + + +def _generated(**planner_options): + module = c_file_to_semantic_module(parse_c_file(_SOURCE, filename="collide.c")) + complete_semantic_policies(module) + return WrapperGenerator().generate(WrapperPlanner(**planner_options).build(module)) + + +def _sources_by_name(generated): + return {source.path.name: source.text for source in generated.sources if source.path.suffix == ".c"} + + +def test_unselected_symbols_keep_the_direct_declaration_and_emit_no_adapter_unit(): + sources = _sources_by_name(_generated()) + + assert "collide_adapters.c" not in sources + assert "long long native_round(double value);" in sources["collide_wrapper.c"] + + +def test_a_selected_symbol_moves_its_native_declaration_into_the_adapter_unit(): + sources = _sources_by_name(_generated(collision_adapters=("native_round",))) + binding = sources["collide_wrapper.c"] + adapters = sources["collide_adapters.c"] + + # The binding never declares the colliding identifier itself. + assert "long long native_round(double value);" not in binding + assert "long long prik_collision_adapter_native_round(double value);" in binding + assert "prik_collision_adapter_native_round(" in binding + + # The adapter unit declares it, forwards to it, and includes no Python header. + assert "long long native_round(double value);" in adapters + assert "return (native_round)(value);" in adapters + assert "Python.h" not in adapters + + # An unselected symbol in the same module keeps its direct declaration. + assert "double native_add(double left, double right);" in binding + + +def test_collision_adapter_all_selects_every_direct_c_symbol(): + sources = _sources_by_name(_generated(collision_adapter_all=True)) + binding = sources["collide_wrapper.c"] + adapters = sources["collide_adapters.c"] + + assert "prik_collision_adapter_native_round(" in binding + assert "prik_collision_adapter_native_add(" in binding + assert "return (native_add)(left, right);" in adapters + + +def test_two_callables_naming_one_symbol_define_the_forwarder_once(): + """Several Python names may bind one native symbol; the forwarder is one definition.""" + module = pyi_text_to_semantic_module( + """from prik.contracts import Float64, bind + +def native_add(left: Float64, right: Float64) -> Float64: ... + +@bind("native_add") +def add_alias(left: Float64, right: Float64) -> Float64: ... +""", + module_name="collide", + native_language="c", + ) + validate_pyi_native_contract([module]) + complete_semantic_policies(module) + generated = WrapperGenerator().generate(WrapperPlanner(collision_adapter_all=True).build(module)) + adapters = _sources_by_name(generated)["collide_adapters.c"] + + assert adapters.count("prik_collision_adapter_native_add(double left, double right) {") == 1 + assert adapters.count("double native_add(double left, double right);") == 1 + + +def test_collision_adapter_all_leaves_a_fortran_bind_c_entrypoint_alone(): + """A bind(C) procedure reaches a direct entrypoint but carries no exact C declaration.""" + module = fortran_file_to_semantic_modules( + parse_fortran_source( + """module m + use iso_c_binding + implicit none +contains + real(c_double) function scaled(x) bind(c, name="scaled") + real(c_double), value :: x + scaled = 2.0_c_double * x + end function scaled +end module m +""" + ) + )[0] + complete_semantic_policies(module) + generated = WrapperGenerator().generate(WrapperPlanner(collision_adapter_all=True).build(module)) + + assert "m_adapters.c" not in _sources_by_name(generated) diff --git a/tests/c/symbol_collisions/end_to_end/test_collision_adapter_runtime.py b/tests/c/symbol_collisions/end_to_end/test_collision_adapter_runtime.py new file mode 100644 index 000000000..7efc8ac08 --- /dev/null +++ b/tests/c/symbol_collisions/end_to_end/test_collision_adapter_runtime.py @@ -0,0 +1,216 @@ +"""A native symbol the binding's own headers declare is callable through an adapter.""" + +import importlib +import json +import shutil +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_fortran_extension, build_pyi_extension, build_pyi_extension_from_manifest +from tests.c._support.paths import REPO_ROOT +from tests.c._support.runtime import sole_native_module + +# This user API deliberately reuses the `Py_Initialize` identifier with a +# different signature from the declaration brought in directly by Python.h. +_CONTRACT = """from prik.contracts import Arg, CLongLong, Int64, Return, native_call + +@native_call([CLongLong(Arg(0))], result=CLongLong(Return(0))) +def Py_Initialize(value: Int64) -> Int64: ... +""" + +_NATIVE_SOURCE = """__attribute__((visibility("hidden"))) +long long Py_Initialize(long long value) { return value + 7; } +""" + +_ALIASED_CONTRACT = """from prik.contracts import Arg, CLongLong, Int64, Return, bind, native_call + +@native_call([CLongLong(Arg(0))], result=CLongLong(Return(0))) +def Py_Initialize(value: Int64) -> Int64: ... + +@bind("Py_Initialize") +@native_call([CLongLong(Arg(0))], result=CLongLong(Return(0))) +def initialize_alias(value: Int64) -> Int64: ... +""" + +_BIND_C_SOURCE = """module m + use iso_c_binding + implicit none +contains + real(c_double) function scaled(x) bind(c, name="scaled") + real(c_double), intent(in), value :: x + scaled = 2.0_c_double * x + end function scaled +end module m +""" + + +def _contract(tmp_path: Path) -> Path: + path = tmp_path / "libm_contract.pyi" + path.write_text(_CONTRACT, encoding="utf-8") + return path + + +def _native_source(tmp_path: Path) -> Path: + path = tmp_path / "collision_native.c" + path.write_text(_NATIVE_SOURCE, encoding="utf-8") + return path + + +def _aliased_contract(tmp_path: Path) -> Path: + path = tmp_path / "aliased_contract.pyi" + path.write_text(_ALIASED_CONTRACT, encoding="utf-8") + return path + + +def _bind_c_source(tmp_path: Path) -> Path: + path = tmp_path / "bind_c_collision.f90" + path.write_text(_BIND_C_SOURCE, encoding="utf-8") + return path + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_a_symbol_declared_by_the_binding_headers_fails_to_compile_unadapted(tmp_path: Path): + with pytest.raises(RuntimeError, match="conflicting types for"): + build_pyi_extension( + _contract(tmp_path), + native_language="c", + native_c_sources=[_native_source(tmp_path)], + output_dir=tmp_path / "unadapted", + output_name="libm_unadapted", + ) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_a_collision_adapted_symbol_compiles_and_calls_the_native_implementation(tmp_path: Path): + result = build_pyi_extension( + _contract(tmp_path), + native_language="c", + native_c_sources=[_native_source(tmp_path)], + collision_adapters=["Py_Initialize"], + output_dir=tmp_path / "adapted", + output_name="libm_adapted", + ) + module = sole_native_module(result.import_module()) + + assert module.Py_Initialize(np.int64(5)) == np.int64(12) + assert module.Py_Initialize(np.int64(-9)) == np.int64(-2) + + binding = next(path for path in result.generated_sources if path.name.endswith("_wrapper.c")) + adapters = next(path for path in result.generated_sources if path.name.endswith("_adapters.c")) + assert "long long Py_Initialize(long long value);" not in binding.read_text(encoding="utf-8") + adapter_text = adapters.read_text(encoding="utf-8") + assert "long long Py_Initialize(long long value);" in adapter_text + assert "return (Py_Initialize)(value);" in adapter_text + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_aliased_callables_compile_and_share_one_collision_adapter(tmp_path: Path): + result = build_pyi_extension( + _aliased_contract(tmp_path), + native_language="c", + native_c_sources=[_native_source(tmp_path)], + collision_adapter_all=True, + output_dir=tmp_path / "aliased", + output_name="aliased_collision", + ) + module = sole_native_module(result.import_module()) + + assert module.Py_Initialize(np.int64(5)) == np.int64(12) + assert module.initialize_alias(np.int64(-9)) == np.int64(-2) + + +@pytest.mark.skipif( + shutil.which("cc") is None or shutil.which("gfortran") is None, + reason="requires C and Fortran compilers", +) +def test_collision_adapter_all_builds_a_fortran_bind_c_module_without_an_adapter(tmp_path: Path): + result = build_fortran_extension( + _bind_c_source(tmp_path), + collision_adapter_all=True, + output_dir=tmp_path / "bind_c", + output_name="bind_c_collision", + ) + module = sole_native_module(result.import_module()) + + assert module.scaled(np.float64(3.0)) == np.float64(6.0) + assert not any(path.name.endswith("_adapters.c") for path in result.generated_sources) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_an_unknown_collision_adapter_name_fails_before_wrapper_planning(tmp_path: Path): + with pytest.raises(ValueError, match="unknown or ineligible names: missing"): + build_pyi_extension( + _contract(tmp_path), + native_language="c", + native_c_sources=[_native_source(tmp_path)], + collision_adapters=["missing"], + generate_sources=True, + output_dir=tmp_path / "unknown", + output_name="unknown_collision", + ) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_build_manifest_replay_retains_the_selected_collision_adapter(tmp_path: Path): + generated = build_pyi_extension( + _contract(tmp_path), + native_language="c", + native_c_sources=[_native_source(tmp_path)], + collision_adapters=["Py_Initialize"], + makefile=True, + output_dir=tmp_path / "replay", + output_name="collision_replay", + ) + + assert generated.build_manifest is not None + assert generated.manifest["extension"]["collision_adapters"] == ["Py_Initialize"] + replay = build_pyi_extension_from_manifest(generated.build_manifest) + module = sole_native_module(replay.import_module()) + + assert module.Py_Initialize(np.int64(5)) == np.int64(12) + assert any(path.name.endswith("_adapters.c") for path in replay.generated_sources) + + +@pytest.mark.skipif(shutil.which("cc") is None, reason="requires a C compiler") +def test_cli_selected_collision_adapter_builds_an_importable_extension(tmp_path: Path): + output_dir = tmp_path / "cli" + completed = subprocess.run( + [ + sys.executable, + "-m", + "prik", + "--language", + "c", + str(_contract(tmp_path)), + "--native-c-sources", + str(_native_source(tmp_path)), + "--collision-adapter", + "Py_Initialize", + "--lto", + "--out", + "collision_cli", + "--out-dir", + str(output_dir), + "--json", + ], + cwd=REPO_ROOT, + capture_output=True, + text=True, + check=True, + ) + payload = json.loads(completed.stdout) + + assert any(path.endswith("collision_cli_adapters.c") for path in payload["generated_sources"]) + assert payload["manifest"]["compiler"]["c_flags"][-1] == "-flto" + assert payload["manifest"]["compiler"]["wrapper_c_flags"][-1] == "-flto" + sys.path.insert(0, str(output_dir)) + try: + module = sole_native_module(importlib.import_module("collision_cli")) + assert module.Py_Initialize(np.int64(5)) == np.int64(12) + finally: + sys.path.remove(str(output_dir)) + sys.modules.pop("collision_cli", None) diff --git a/tests/docs/_structure_support.py b/tests/docs/_structure_support.py index b2cc8c144..02c3126b4 100644 --- a/tests/docs/_structure_support.py +++ b/tests/docs/_structure_support.py @@ -81,7 +81,6 @@ "--native-library", "--native-link-item", "--native-library-dir", - "--format", "--expr", "--runner", "--cache-dir", diff --git a/tests/docs/test_examples.py b/tests/docs/test_examples.py index c4a4001d7..ca368741b 100644 --- a/tests/docs/test_examples.py +++ b/tests/docs/test_examples.py @@ -22,8 +22,10 @@ DOC_PATHS = [ ROOT / "README.md", ROOT / "examples/blas/README.md", + ROOT / "examples/bspline/README.md", ROOT / "examples/fftpack/README.md", ROOT / "examples/lapack/README.md", + ROOT / "examples/libm/README.md", ROOT / "examples/minpack/README.md", *sorted(path for path in (ROOT / "docs").rglob("*.md") if "old_docs" not in path.parts), ] diff --git a/tests/fortran/CONTRACT_COVERAGE.md b/tests/fortran/CONTRACT_COVERAGE.md index 8ced24f5f..0318fc9b7 100644 --- a/tests/fortran/CONTRACT_COVERAGE.md +++ b/tests/fortran/CONTRACT_COVERAGE.md @@ -26,7 +26,7 @@ Authoritative sources: - Use `—` only when that evidence kind is not required. - Record every documented unsafe or unsupported behavior in Negative evidence as an exact node followed by its terminal stage, for example - `` `tests/fortran/arrays/policy/test_contracts.py::test_rank_limit` + `` `tests/fortran/allocatables/policy/test_allocatable_result_policy.py::test_direct_allocatable_scalar_function_result_is_blocked_before_codegen` (`policy`) ``. - Record source, generated-`.pyi` replay, edited-`.pyi`, and source-free native artifact routes separately when the documentation claims each route. @@ -36,10 +36,10 @@ Authoritative sources: | Documentation contract | Status | Dimensions | Stage evidence | Runtime evidence | Negative evidence | CI lane | | --- | --- | --- | --- | --- | --- | --- | -| [Inspect a Fortran API: Parse Source Facts](../../docs/user/examples/recipes/inspect-fortran-api.md#parse-source-facts) | Supported | public string, file, path-sequence, and project parser entry points; model traversal; stable source diagnostics | `tests/fortran/source_parsing/parsing/test_public_entrypoints.py::test_parser_public_entrypoint_aliases_and_singular_contracts_use_inline_sources` | — | — | canonical | -| [Compiler Preprocessing: Direct Compiler Settings](../../docs/user/examples/recipes/compiler-preprocessing.md#direct-compiler-settings) | Supported | explicit compiler; include directories; macros; standard; compiler arguments; exact preprocessing recipe | `tests/fortran/source_preprocessing/preprocessing/test_configuration_and_adapters.py::test_direct_fortran_preprocess_invocation_uses_exact_compiler_and_cpp` | — | — | canonical | -| [CLI Commands: Parse And Semantics](../../docs/user/reference/cli-commands.md#parse-and-semantics) | Supported | public parser module and top-level command modes; parse, semantics, `.pyi`, and diagnostic dispatch | `tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py::test_fortran_parser_main_public_api_modes_from_inline_source` | — | — | canonical | -| [Semantic IR: Round Trips And Provenance](../../docs/user/reference/semantic-ir.md#round-trips-and-provenance) | Supported | deterministic source-to-IR conversion; preserved wrapper-relevant facts; checked fixture serialization | `tests/fortran/semantic_ir/semantics/test_fortran_conversion_properties.py::test_generated_fortran_ast_to_semantic_ir_is_deterministic` | — | — | canonical | +| [Inspect a Fortran API: Parse Source Facts](../../docs/user/examples/recipes/inspect-fortran-api.md#parse-source-facts) | Supported | public string, file, path-sequence, and project parser entry points; model traversal; stable source diagnostics | `tests/fortran/infrastructure/parsing/test_public_entrypoints.py::test_parser_public_entrypoint_aliases_and_singular_contracts_use_inline_sources` | — | — | canonical | +| [Compiler Preprocessing: Direct Compiler Settings](../../docs/user/examples/recipes/compiler-preprocessing.md#direct-compiler-settings) | Supported | explicit compiler; include directories; macros; standard; compiler arguments; exact preprocessing recipe | `tests/fortran/infrastructure/preprocessing/test_configuration_and_adapters.py::test_direct_fortran_preprocess_invocation_uses_exact_compiler_and_cpp` | — | — | canonical | +| [CLI Commands: Parse And Semantics](../../docs/user/reference/cli-commands.md#parse-and-semantics) | Supported | public parser module and top-level command modes; parse, semantics, `.pyi`, and diagnostic dispatch | `tests/fortran/infrastructure/cli/pipeline/test_stage_dispatch.py::test_fortran_parser_main_public_api_modes_from_inline_source` | — | — | canonical | +| [Semantic IR: Round Trips And Provenance](../../docs/user/reference/semantic-ir.md#round-trips-and-provenance) | Supported | deterministic source-to-IR conversion; preserved wrapper-relevant facts; checked fixture serialization | `tests/fortran/infrastructure/semantic_ir/semantics/test_fortran_conversion_properties.py::test_generated_fortran_ast_to_semantic_ir_is_deterministic` | — | — | canonical | | [Data Types: Example](../../docs/user/guide/data-types.md#example) | Supported | source generation; reviewed generated `.pyi`; source build; generated-`.pyi` replay | `tests/fortran/data_types/pipeline/test_generated_scalar_contract.py::test_generated_primitive_scalar_contract_matches_reviewed_package` | `tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[source]`
`tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[generated-pyi]` | — | canonical | | [Data Types: Calling from Python](../../docs/user/guide/data-types.md#calling-from-python) | Supported | signed integer; real; complex; Boolean; exact visible values and scalar result types | `tests/fortran/data_types/codegen/test_primitive_scalar_result_lowering.py::test_direct_scalar_results_preserve_numpy_types_with_python_bool_as_the_exception[Complex128-NPY_COMPLEX128-numpy]` | `tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[source]` | — | canonical | | [Data Types: Scalar Type Mapping](../../docs/user/guide/data-types.md#scalar-type-mapping) | Supported | `Bool`/`Bool8/16/32/64`; `Int8/16/32/64`; `Float32/64`; `Complex64/128`; compiler-probed intrinsic, ISO environment, and ISO C kinds | `tests/fortran/data_types/semantics/test_fortran_scalar_semantics.py::test_intrinsic_builtin_kinds_map_to_semantic_types`
`tests/fortran/data_types/probes/test_fortran_type_probes.py::test_fortran_type_probe_evaluates_collected_semantic_requirements`
`tests/fortran/data_types/probes/test_fortran_type_probes.py::test_fortran_type_probe_resolves_supported_logical_storage_widths` | `tests/fortran/data_types/end_to_end/test_primitive_scalar_runtime.py::test_scalar_kind_coverage_uses_compiler_probed_wrapper_types[source]` | — | canonical | @@ -63,7 +63,7 @@ Authoritative sources: | [Strings: String Arrays](../../docs/user/guide/strings.md#string-arrays) | Supported | fixed itemsize; input and in-place mutation; fixed array result; rank/shape/dtype/writeability; zero size | `tests/fortran/strings/codegen/test_character_array_lowering.py::test_fixed_width_character_array_results_reuse_the_ordinary_array_copy_plan` | `tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays`
`tests/fortran/strings/end_to_end/test_character_boundaries.py::test_modern_fortran_character_arguments_and_results[source]` | `tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays` (`runtime`) | canonical | | [Strings: Length And Encoding](../../docs/user/guide/strings.md#length-and-encoding) | Supported | length 1, representative width 8, runtime length, Unicode UTF-8 byte length, blanks, empty values, embedded NUL rejection, conservative no-`intent`, ambiguous mutable deferred scalar rejection | `tests/fortran/strings/parsing/test_character_length_parsing.py::test_character_entity_lengths_and_assumed_bounds_are_preserved`
`tests/fortran/strings/codegen/test_string_input_lowering.py::test_required_string_values_reuse_argument_plan_with_character_handoff_facts` | `tests/fortran/strings/end_to_end/test_character_boundaries.py::test_modern_fortran_character_arguments_and_results[source]` | `tests/fortran/strings/semantics/test_string_pyi_semantics.py::test_bare_string_slice_is_rejected_as_ambiguous` (`semantics`)
`tests/fortran/strings/end_to_end/test_documented_string_journey.py::test_documented_edited_pyi_distinguishes_values_scalar_storage_and_string_arrays` (`runtime`) | canonical | | [Wrapping Functions: Basic Scalar Function](../../docs/user/guide/wrapping-functions.md#basic-scalar-function) | Supported | direct scalar result; exact NumPy inputs; visible value | `tests/fortran/functions/semantics/test_fortran_function_semantics.py::test_function_result` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` (`runtime`) | canonical | -| [Wrapping Functions: Python And Native Names](../../docs/user/guide/wrapping-functions.md#python-and-native-names) | Supported | edited `.pyi`; standalone external; `@bind`; changed Python name; unchanged native ABI; exact signature | — | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` (`runtime`) | canonical | +| [Wrapping Functions: Python And Native Names](../../docs/user/guide/wrapping-functions.md#python-and-native-names) | Supported | edited `.pyi`; standalone external; `@bind`; changed Python name; unchanged native ABI; exact signature | — | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` (`runtime`) | canonical | | [Wrapping Functions: Array Return Values](../../docs/user/guide/wrapping-functions.md#array-return-values) | Supported | automatic shape; new NumPy array; Fortran layout; values | `tests/fortran/arrays/codegen/test_array_result_lowering.py::test_array_results_record_producer_shape_copy_ownership_and_shared_hidden_slot` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | — | canonical | | [Wrapping Functions: Functions with Output Arguments](../../docs/user/guide/wrapping-functions.md#functions-with-output-arguments) | Supported | direct result first; hidden scalar output second; caller array excluded from tuple; stable tuple order | `tests/fortran/functions/policy/test_function_result_policy.py::test_multiple_scalar_result_policy_completes_order_and_hidden_address_before_planning`
`tests/fortran/functions/codegen/test_multiple_function_results.py::test_multiple_scalar_results_lower_to_binding_tuple_and_one_bridge_function_call` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | `tests/fortran/functions/codegen/test_multiple_function_results.py::test_multiple_scalar_result_validation_rejects_position_and_consumer_drift` (`codegen`) | canonical | | [Wrapping Functions: Important Rules](../../docs/user/guide/wrapping-functions.md#important-rules) | Supported | exact dtype; array copy result; projected scalar tuple order; caller array mutation; conservative no-`intent` scalar replacement after direct result | `tests/fortran/functions/semantics/test_fortran_function_semantics.py::test_missing_intent_scalar_uses_conservative_replacement_projection`
`tests/fortran/functions/policy/test_function_result_policy.py::test_scalar_copy_in_out_policy_completes_writeback_before_planning`
`tests/fortran/functions/codegen/test_scalar_function_writeback.py::test_scalar_writeback_is_an_explicit_binding_lifecycle_result` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | — | canonical | @@ -71,12 +71,12 @@ Authoritative sources: | [Wrapping Subroutines: Complete Example](../../docs/user/guide/wrapping-subroutines.md#complete-example) | Supported | source build; hidden bounds tuple; in-place array scaling; scalar replacement; caller output storage | `tests/fortran/subroutines/policy/test_subroutine_output_policy.py::test_source_hidden_scalar_output_completes_call_local_address_before_planning` | `tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py::test_subroutine_outputs_and_caller_storage_follow_documented_projection_rules` | — | canonical | | [Wrapping Subroutines: Python Usage](../../docs/user/guide/wrapping-subroutines.md#python-usage) | Supported | exact NumPy values; scalar object unchanged; arrays mutated in place; visible outputs | — | `tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py::test_subroutine_outputs_and_caller_storage_follow_documented_projection_rules` | `tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py::test_subroutine_outputs_and_caller_storage_follow_documented_projection_rules` (`runtime`) | canonical | | [Wrapping Subroutines: Key Rules](../../docs/user/guide/wrapping-subroutines.md#key-rules) | Supported | hidden scalar ordering; explicit scalar writeback lifecycle; ordinary arrays and derived objects excluded from result; native-created allocatable returned; `.pyi` projection authority | `tests/fortran/subroutines/codegen/test_hidden_scalar_outputs.py::test_hidden_scalar_result_is_one_bridge_output_and_one_python_result`
`tests/fortran/subroutines/codegen/test_scalar_subroutine_writeback_validation.py::test_generator_rejects_writeback_without_python_result_target` | `tests/fortran/subroutines/end_to_end/test_documented_subroutine_journey.py::test_subroutine_outputs_and_caller_storage_follow_documented_projection_rules` | `tests/fortran/subroutines/codegen/test_scalar_subroutine_writeback_validation.py::test_generator_rejects_writeback_from_an_unavailable_handoff` (`codegen`) | canonical | -| [Wrapping Modules: Basic Usage](../../docs/user/guide/wrapping-modules.md#basic-usage) | Supported | generated package entry; child native-module namespace; isolated import | `tests/fortran/modules/pipeline/test_generated_module_contracts.py::test_generated_module_contract_matches_fixture[module_exports]` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | — | canonical | -| [Wrapping Modules: Procedures](../../docs/user/guide/wrapping-modules.md#procedures) | Supported | module functions; standalone external at root; multiple native modules in one source | `tests/fortran/modules/pipeline/test_generated_module_contracts.py::test_generated_module_contract_matches_fixture[module_exports]` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | — | canonical | +| [Wrapping Modules: Basic Usage](../../docs/user/guide/wrapping-modules.md#basic-usage) | Supported | generated package entry; child native-module namespace; isolated import | `tests/fortran/modules/pipeline/test_generated_module_contracts.py::test_generated_module_contract_matches_fixture[module_exports]` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | — | canonical | +| [Wrapping Modules: Procedures](../../docs/user/guide/wrapping-modules.md#procedures) | Supported | module functions; standalone external at root; multiple native modules in one source | `tests/fortran/modules/pipeline/test_generated_module_contracts.py::test_generated_module_contract_matches_fixture[module_exports]` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | — | canonical | | [Wrapping Modules: Public Variables and Constants](../../docs/user/guide/wrapping-modules.md#public-variables-and-constants) | Supported | writable scalar state; true parameter; Python-local constant shadow; native state unchanged | `tests/fortran/modules/policy/test_module_variable_policy.py::test_scalar_module_variable_policy_completes_access_and_storage_before_planning`
`tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py::test_module_variable_plan_contains_only_completed_dispatch_facts` | `tests/fortran/modules/end_to_end/test_module_variables_and_state.py::test_scalar_module_variables_use_attributes_and_parameters_have_no_native_setter[source]` | — | canonical | | [Wrapping Modules: Module Arrays and Saved State](../../docs/user/guide/wrapping-modules.md#module-arrays-saved-state) | Supported | allocatable module array; persistent handle; live NumPy view; mutation; deallocation; procedure-local `save`; shared state across imports | `tests/fortran/modules/policy/test_module_variable_policy.py::test_scalar_module_variable_policy_completes_access_and_storage_before_planning` | `tests/fortran/modules/end_to_end/test_scalar_module_variable_plan.py::test_whole_scalar_module_variable_behavior_uses_canonical_plan`
`tests/fortran/modules/end_to_end/test_module_variables_and_state.py::test_scalar_module_variables_use_attributes_and_parameters_have_no_native_setter[source]` | — | canonical | -| [Wrapping Modules: Shape the Module API With the Contract](../../docs/user/guide/wrapping-modules.md#shape-the-module-api-with-the-contract) | Supported | mutable literal initializer; hidden variable; private procedure; removed declaration; true `Final` constant | `tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_literal_defaults_are_preserved`
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | `tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_expression_defaults_are_rejected[from prik.contracts import Int32\ncounter: Int32 = f(42)\n]` (`semantics`)
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_unsupported_module_variable_initializer_completes_an_unsupported_policy` (`policy`) | canonical | -| [Wrapping Modules: Flatten Module Namespaces](../../docs/user/guide/wrapping-modules.md#flatten-module-namespaces) | Supported | child namespaces; wildcard flattening; selective imports; explicit aliases; unchanged native targets | `tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_rejects_colliding_wildcard_exports` (`pipeline`) | canonical | +| [Wrapping Modules: Shape the Module API With the Contract](../../docs/user/guide/wrapping-modules.md#shape-the-module-api-with-the-contract) | Supported | mutable literal initializer; hidden variable; private procedure; removed declaration; true `Final` constant | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_literal_defaults_are_preserved`
`tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_expression_defaults_are_rejected[from prik.contracts import Int32\ncounter: Int32 = f(42)\n]` (`semantics`)
`tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_unsupported_module_variable_initializer_completes_an_unsupported_policy` (`policy`) | canonical | +| [Wrapping Modules: Flatten Module Namespaces](../../docs/user/guide/wrapping-modules.md#flatten-module-namespaces) | Supported | child namespaces; wildcard flattening; selective imports; explicit aliases; unchanged native targets | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_rejects_colliding_wildcard_exports` (`pipeline`) | canonical | | [Wrapping Modules: Important Rules](../../docs/user/guide/wrapping-modules.md#important-rules) | Supported | private declarations hidden; common-block storage internal; shared native state; source-derived extension identity | `tests/fortran/modules/semantics/test_module_contract_semantics.py::test_module_common_block_storage_stays_internal` | `tests/fortran/modules/end_to_end/test_common_blocks.py::test_common_block_storage_stays_internal_to_wrapped_fortran[source]`
`tests/fortran/modules/end_to_end/test_module_variables_and_state.py::test_scalar_module_variables_use_attributes_and_parameters_have_no_native_setter[source]` | — | canonical | | [Optional Arguments: Complete Example](../../docs/user/guide/optional-arguments.md#complete-example) | Supported | source generation; reviewed generated `.pyi`; optional scalar input; optional ordinary array output; native `present(...)` | `tests/fortran/optional_arguments/pipeline/test_generated_optional_contracts.py::test_generated_optional_contract_matches_fixture[foptional_f90]` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]`
`tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[generated-pyi]` | — | canonical | | [Optional Arguments: Usage in Python](../../docs/user/guide/optional-arguments.md#usage-in-python) | Supported | omission; explicit `None`; positional value; keyword value; skipped earlier positions | `tests/fortran/optional_arguments/codegen/test_optional_lowering.py::test_optional_scalar_lowering_distinguishes_absent_or_none_from_value` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]` | `tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]` (`runtime`) | canonical | @@ -88,21 +88,21 @@ Authoritative sources: | [Generic Interfaces: Generated Contract](../../docs/user/guide/generic-interfaces.md#generated-contract) | Supported | private link targets; one exact overload candidate per declaration; public-generic `@bind`; native target precedence | `tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py::test_convert_pyi_to_ir_resolves_prik_overload_by_explicit_specific_name`
`tests/fortran/generic_interfaces/policy/test_generic_policy.py::test_module_overload_bind_takes_precedence_per_candidate` | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]` | — | canonical | | [Generic Interfaces: Usage in Python](../../docs/user/guide/generic-interfaces.md#usage-in-python) | Supported | exact `Int32`, `Float64`, and `Complex128`; scalar and rank-one dispatch; generated-class dispatch; no implicit coercion | `tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py::test_plan_records_one_exact_numpy_scalar_predicate_per_candidate` | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]` | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]` (`runtime`) | canonical | | [Generic Interfaces: Inspect the Overloads](../../docs/user/guide/generic-interfaces.md#inspect-the-overloads) | Supported | one public callable; all accepted signatures; hidden concrete procedures and internal names | — | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]` | — | canonical | -| [Generic Interfaces: Extend an Overload Set](../../docs/user/guide/generic-interfaces.md#extend-an-overload-set) | Supported | edited `.pyi`; renamed public binding; added overload group; private-specific routing through public generic; absent candidate rejection | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_private_native_specific_without_overload_bind_fails_at_build[private_module_specifics_without_bind-missing_targets0]` (`compiling`) | canonical | +| [Generic Interfaces: Extend an Overload Set](../../docs/user/guide/generic-interfaces.md#extend-an-overload-set) | Supported | edited `.pyi`; renamed public binding; added overload group; private-specific routing through public generic; absent candidate rejection | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_private_native_specific_without_overload_bind_fails_at_build[private_module_specifics_without_bind-missing_targets0]` (`compiling`) | canonical | | [Generic Interfaces: Key Rules](../../docs/user/guide/generic-interfaces.md#key-rules) | Supported | exact dtype/rank/class match; no-match `TypeError`; ambiguous signature rejection; exact-once specific links; `@bind`; private visibility; type-bound generics; defined operators; defined assignment | `tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_module_and_type_bound_generic_overload_sets`
`tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_defined_operators_assignment_and_type_bound_operators`
`tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py::test_plan_records_one_exact_numpy_scalar_predicate_per_candidate` | `tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py::test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extension[source]` | `tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py::test_generator_rejects_ambiguous_edited_overload_plan_before_emission` (`codegen`)
`tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py::test_convert_pyi_to_ir_rejects_invalid_prik_overload_links[@overload("missing")\ndef convert(value: Int32) -> Int32: ...\n-missing specific procedure 'missing']` (`semantics`) | canonical | -| [Generic Interfaces: Limitations](../../docs/user/guide/generic-interfaces.md#limitations) | Blocked | source generic constructor inference; assumed-type `class(*)`; arrays of derived values | — | — | `tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_rejects_generic_constructor_interfaces_during_semantic_conversion` (`semantics`)
`tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py::test_assumed_type_generic_candidate_is_rejected_at_parsing` (`parsing`)
`tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py::test_generic_candidate_with_array_of_derived_values_is_blocked_before_lowering` (`codegen`) | canonical | +| [Generic Interfaces: Limitations](../../docs/user/guide/generic-interfaces.md#limitations) | Partially supported | only a type-named interface is a constructor; assumed-type `class(*)`; arrays of derived values | — | `tests/fortran/derived_types/end_to_end/test_generic_constructor.py::test_constructor_interface_overloads_init_from_its_specifics`
`tests/fortran/derived_types/end_to_end/test_generic_constructor.py::test_type_without_a_constructor_interface_keeps_keyword_fields` | `tests/fortran/generic_interfaces/parsing/test_generic_interface_syntax.py::test_assumed_type_generic_candidate_is_rejected_at_parsing` (`parsing`)
`tests/fortran/generic_interfaces/codegen/test_overload_dispatch_plan.py::test_generic_candidate_with_array_of_derived_values_is_blocked_before_lowering` (`codegen`) | canonical | | [Wrapping Derived Types: Complete Example](../../docs/user/guide/wrapping-derived-types.md#complete-example) | Supported | derived declarations; public and nested fields; source generation; reviewed generated `.pyi`; source build; generated-`.pyi` replay | `tests/fortran/derived_types/parsing/test_derived_type_declarations.py::test_derived_type_fields_and_methods_detection`
`tests/fortran/derived_types/pipeline/test_generated_derived_contracts.py::test_generated_derived_contract_matches_fixture[fderived_boundary_f90]` | `tests/fortran/derived_types/end_to_end/test_derived_boundaries.py::test_scalar_derived_types_cross_procedure_boundaries[source]`
`tests/fortran/derived_types/end_to_end/test_derived_boundaries.py::test_scalar_derived_types_cross_procedure_boundaries[generated-pyi]` | — | canonical | | [Wrapping Derived Types: Usage in Python](../../docs/user/guide/wrapping-derived-types.md#usage-in-python) | Supported | keyword construction; public field get/set; `intent(inout)` identity; owned result; nested borrowed component | `tests/fortran/derived_types/policy/test_derived_policy_defaults.py::test_recursive_module_policy_map_includes_nested_fields_and_functions` | `tests/fortran/derived_types/end_to_end/test_derived_boundaries.py::test_scalar_derived_types_cross_procedure_boundaries[source]` | — | canonical | -| [Wrapping Derived Types: Inspect the Class](../../docs/user/guide/wrapping-derived-types.md#inspect-the-class) | Supported | class, constructor, field, method, parameter, return, and overload docstrings; no native implementation names | `tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_bound_constructor_and_method_reuse_completed_direct_function_plans`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_edited_overloads_complete_exact_dispatch_and_reject_ambiguous_plan` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | — | canonical | +| [Wrapping Derived Types: Inspect the Class](../../docs/user/guide/wrapping-derived-types.md#inspect-the-class) | Supported | class, constructor, field, method, parameter, return, and overload docstrings; no native implementation names | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.py::test_bound_constructor_and_method_reuse_completed_direct_function_plans`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.py::test_edited_overloads_complete_exact_dispatch_and_reject_ambiguous_plan` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | — | canonical | | [Wrapping Derived Types: Key Concepts](../../docs/user/guide/wrapping-derived-types.md#key-concepts) | Supported | Python-owned construction/result; parent-retained component; in-place output/inout/no-`intent`; primitive writable fields; nested types; keyword defaults; destruction | `tests/fortran/derived_types/policy/test_derived_accessor_policy.py::test_derived_field_setter_policy_uses_value_copy_write_through`
`tests/fortran/derived_types/codegen/test_derived_lowering.py::test_projected_derived_argument_returns_the_exact_caller_wrapper_without_release` | `tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py::test_fortran_default_constructor_keywords_and_finalization[source]`
`tests/fortran/derived_types/end_to_end/test_borrowed_components.py::test_borrowed_child_wrapper_never_finalizes_native_component[source]` | — | canonical | -| [Wrapping Derived Types: Custom Constructor](../../docs/user/guide/wrapping-derived-types.md#custom-constructor) | Supported | edited `.pyi`; `@bind`; exactly one `Pass()`; reordered `Addr(Arg)` values; replacement of generated keyword initializer; constructor docs | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bound_constructor_uses_explicit_pass_position_and_native_target`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_bound_constructor_pass_disambiguates_same_type_arguments_and_keeps_module_export` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected[\nclass state:\n @bind("init_state")\n @native_call([Addr(Arg(0))])\n def __init__(self, seed: Int32) -> None: ...\n-Bound constructor native_call requires exactly one Pass() entry]` (`semantics`) | canonical | +| [Wrapping Derived Types: Custom Constructor](../../docs/user/guide/wrapping-derived-types.md#custom-constructor) | Supported | edited `.pyi`; `@bind`; exactly one `Pass()`; reordered `Addr(Arg)` values; replacement of generated keyword initializer; constructor docs | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bound_constructor_uses_explicit_pass_position_and_native_target`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.py::test_bound_constructor_pass_disambiguates_same_type_arguments_and_keeps_module_export` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected[\nclass state:\n @bind("init_state")\n @native_call([Addr(Arg(0))])\n def __init__(self, seed: Int32) -> None: ...\n-Bound constructor native_call requires exactly one Pass() entry]` (`semantics`) | canonical | | [Wrapping Derived Types: Type-Bound Methods](../../docs/user/guide/wrapping-derived-types.md#type-bound-methods) | Supported | passed object becomes `self`; mutation preserves Python identity; direct and generated-`.pyi` replay | `tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py::test_converter_covers_derived_dispatch_methods_and_kind_edges` | `tests/fortran/derived_types/end_to_end/test_type_bound_methods.py::test_modern_fortran_derived_type_exposes_class_and_type_bound_methods[source]`
`tests/fortran/derived_types/end_to_end/test_type_bound_methods.py::test_modern_fortran_derived_type_exposes_class_and_type_bound_methods[generated-pyi]` | — | canonical | -| [Wrapping Derived Types: Expose a Module Procedure as a Method](../../docs/user/guide/wrapping-derived-types.md#expose-a-module-procedure-as-a-method) | Supported | edited class method; `Pass()` receiver; independent module declaration; same or bound native name; optional private module surface; method docs | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_method_and_module_declarations_keep_native_targets_independent`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_module_procedure_method_visibility_is_completed_independently` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | — | canonical | -| [Wrapping Derived Types: Type-Bound Generics](../../docs/user/guide/wrapping-derived-types.md#type-bound-generics) | Supported | private specifics; public generic bind; exact `Int32`/`Float64` dispatch; wrapped receiver fixed by class; no trial calls | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_edited_overloads_complete_exact_dispatch_and_reject_ambiguous_plan` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` (`runtime`) | canonical | +| [Wrapping Derived Types: Expose a Module Procedure as a Method](../../docs/user/guide/wrapping-derived-types.md#expose-a-module-procedure-as-a-method) | Supported | edited class method; `Pass()` receiver; independent module declaration; same or bound native name; optional private module surface; method docs | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_method_and_module_declarations_keep_native_targets_independent`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.py::test_module_procedure_method_visibility_is_completed_independently` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | — | canonical | +| [Wrapping Derived Types: Type-Bound Generics](../../docs/user/guide/wrapping-derived-types.md#type-bound-generics) | Supported | private specifics; public generic bind; exact `Int32`/`Float64` dispatch; wrapped receiver fixed by class; no trial calls | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.py::test_edited_overloads_complete_exact_dispatch_and_reject_ambiguous_plan` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` (`runtime`) | canonical | | [Wrapping Derived Types: Defined Operators](../../docs/user/guide/wrapping-derived-types.md#defined-operators) | Supported | direct/reflected binary; unary; comparison; logical; named operators; defined assignment; exact wrapped/scalar dispatch; operator docstrings | `tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_defined_operators_assignment_and_type_bound_operators` | `tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py::test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extension[source]` | `tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py::test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extension[source]` (`runtime`) | canonical | | [Fortran Wrapper: Derived Types Across Procedure Boundaries](../../docs/user/reference/fortran-wrapper.md#derived-types-across-procedure-boundaries) | Supported | complete scalar actual/dummy matrix; module and nonmodule storage; ordinary, target, allocatable, allocatable-target, pointer; six dummy forms; identity, writeback, empty states, rollback, lifetime, and deliberate blockers | `tests/fortran/derived_types/codegen/test_scalar_actual_dummy_plan.py::test_every_dummy_form_has_one_exhaustive_completed_matrix[object_dummy-object]` | `tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_all_sixty_actual_dummy_cells[A-module_object]`
`tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_one_call_uses_all_six_dummy_forms_and_optional_arguments_stay_linear`
`tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_later_acquisition_failure_rolls_back_earlier_origins` | `tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_reassociable_pointer_dummy_requires_pointer_storage[module_object]` (`runtime`)
`tests/fortran/derived_types/codegen/test_derived_lowering.py::test_unsupported_derived_shapes_fail_on_exact_completed_policy_blockers[\nfrom prik.contracts import Float64\n\nclass point:\n x: Float64\n\ndef consume(value: point[:]) -> None: ...\n-unsupported array of derived values]` (`codegen`) | canonical | -| [Fortran Wrapper: Inheritance And Polymorphism](../../docs/user/reference/fortran-wrapper.md#inheritance-and-polymorphism) | Partially supported | scalar extension inheritance; closed `class(base), intent(in)` dispatch; exact extension classes; unsupported polymorphic results, mutation, arrays, descriptor scalars, and assumed type | `tests/fortran/derived_types/codegen/test_class_surfaces.py::test_inheritance_and_polymorphism_are_completed_before_planning` | `tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py::test_fortran_extension_types_generate_python_inheritance[source]`
`tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py::test_fortran_extension_types_generate_python_inheritance[generated-pyi]` | `tests/fortran/derived_types/codegen/test_class_surfaces.py::test_invalid_class_graph_fails_before_emission` (`codegen`)
`tests/fortran/derived_types/policy/test_derived_accessor_policy.py::test_abstract_type_and_deferred_binding_fail_in_completed_derived_policy` (`policy`) | canonical | -| [Fortran Wrapper: Constructors, Initialization, And Finalizers](../../docs/user/reference/fortran-wrapper.md#constructors-initialization-and-finalizers) | Supported | generated keyword constructor; default field values; custom direct constructor; overloaded constructors; commit-on-success; exact finalization; borrowed non-finalization | `tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py::test_derived_type_initializers_and_finalizers_reach_semantic_ir`
`tests/fortran/derived_types/codegen/test_derived_lowering.py::test_owned_derived_result_has_explicit_failure_and_release_lifecycle` | `tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py::test_fortran_default_constructor_keywords_and_finalization[source]`
`tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract`
`tests/fortran/derived_types/end_to_end/test_borrowed_components.py::test_borrowed_child_wrapper_never_finalizes_native_component[source]` | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected[\nclass state:\n def __init__(self, seed: Int32) -> None: ...\n-Non-generated __init__ declarations must use @bind("specific_name")]` (`semantics`) | canonical | +| [Fortran Wrapper: Inheritance And Polymorphism](../../docs/user/reference/fortran-wrapper.md#inheritance-and-polymorphism) | Partially supported | scalar extension inheritance; closed `class(base), intent(in)` dispatch; exact extension classes; unsupported polymorphic results, mutation, arrays, descriptor scalars, and assumed type | `tests/fortran/derived_types/codegen/test_class_surfaces.py::test_inheritance_and_polymorphism_are_completed_before_planning` | `tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py::test_fortran_extension_types_generate_python_inheritance[source]`
`tests/fortran/derived_types/end_to_end/test_inheritance_and_polymorphism.py::test_fortran_extension_types_generate_python_inheritance[generated-pyi]` | `tests/fortran/derived_types/codegen/test_class_surfaces.py::test_invalid_class_graph_fails_before_emission` (`codegen`)
`tests/fortran/derived_types/policy/test_derived_accessor_policy.py::test_deferred_binding_without_an_abstract_type_is_refused` (`policy`) | canonical | +| [Fortran Wrapper: Constructors, Initialization, And Finalizers](../../docs/user/reference/fortran-wrapper.md#constructors-initialization-and-finalizers) | Supported | generated keyword constructor; default field values; custom direct constructor; overloaded constructors; commit-on-success; exact finalization; borrowed non-finalization | `tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py::test_derived_type_initializers_and_finalizers_reach_semantic_ir`
`tests/fortran/derived_types/codegen/test_derived_lowering.py::test_owned_derived_result_has_explicit_failure_and_release_lifecycle` | `tests/fortran/derived_types/end_to_end/test_default_constructors_and_finalizers.py::test_fortran_default_constructor_keywords_and_finalization[source]`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract`
`tests/fortran/derived_types/end_to_end/test_borrowed_components.py::test_borrowed_child_wrapper_never_finalizes_native_component[source]` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected[\nclass state:\n def __init__(self, seed: Int32) -> None: ...\n-Non-generated __init__ declarations must use @bind("specific_name")]` (`semantics`) | canonical | | [Fortran Wrapper: Derived-Type Layout And Interoperability](../../docs/user/reference/fortran-wrapper.md#derived-type-layout-and-interoperability) | Supported | opaque accessor storage for ordinary, `bind(C)`, and `sequence`; field get/set; by-value copy; no direct C aggregate access | `tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py::test_bind_c_and_sequence_types_preserve_accessor_layout_metadata`
`tests/fortran/derived_types/codegen/test_derived_lowering.py::test_exact_typed_value_lowering_uses_fortran_value_semantics_and_opaque_binding` | `tests/fortran/derived_types/end_to_end/test_opaque_layout.py::test_bind_c_derived_types_use_accessors_and_fortran_value_copy[source]`
`tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py::test_sequence_derived_value_uses_the_same_typed_opaque_call_path` | — | canonical | | [Allocatables: Key Concepts](../../docs/user/guide/allocatables.md#key-concepts) | Supported | scalar value versus array handle; allocated, unallocated, and zero-sized states; live views; module, field, result, and caller-created descriptor origins | `tests/fortran/allocatables/semantics/test_pyi_allocatable_semantics.py::test_persistent_allocatable_descriptors_preserve_scalar_and_array_kinds`
`tests/fortran/allocatables/policy/test_allocatable_handle_policy.py::test_allocatable_array_field_is_wrapper_owned_borrowed_view` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[source]` | — | canonical | | [Allocatables: When To Use An Allocatable Handle](../../docs/user/guide/allocatables.md#when-to-use-an-allocatable-handle) | Supported | descriptor arguments versus ordinary arrays; present-empty caller handle; dtype/rank compatibility; plain NumPy rejection | `tests/fortran/allocatables/runtime/test_allocatable_descriptor_abi.py::test_allocatable_descriptor_hook_accepts_unallocated_descriptor_without_numpy_conversion`
`tests/fortran/allocatables/runtime/test_allocatable_array_actual_abi.py::test_array_actual_argument_abi_packer_uses_allocatable_native_array_actual_without_numpy_conversion` | `tests/fortran/allocatables/end_to_end/test_external_allocatable.py::test_standalone_allocatable_argument_accepts_a_caller_created_handle` | `tests/fortran/allocatables/runtime/test_allocatable_contract_handles.py::test_generated_storage_rejects_incompatible_allocatable_contract_handles[-float64-1-TypeError-fresh contract handle]` (`runtime`) | canonical | @@ -179,57 +179,57 @@ Authoritative sources: | [Error Handling: Best Practices](../../docs/user/guide/error-handling.md#best-practices) | Supported | full diagnostic first; verbose command replay; debug traceback only on demand; edited-contract inspection; risky callback isolation | `tests/fortran/error_handling/parsing/test_fortran_diagnostics.py::test_parse_error_message_includes_filename_and_lineno`
`tests/fortran/error_handling/compiling/test_verbose_commands.py::test_run_command_verbose_prints_replayable_command` | `tests/fortran/error_handling/pipeline/test_debug_cli_tracebacks.py::test_cli_debug_flag_reraises_parse_errors`
`tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]` | — | canonical | | [Fortran Wrapper: Wrapper Errors And Fortran Errors](../../docs/user/reference/fortran-wrapper.md#wrapper-errors-and-fortran-errors) | Supported | ordinary wrapper exceptions; no inferred application convention; opt-in status/message projection; cleanup after failure; native termination remains unrecoverable | `tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_direct_binding_lowering_places_only_opted_in_native_call_outside_the_gil`
`tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_fixed_message_bridge_copy_requires_its_completed_reason` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | `tests/fortran/callbacks/end_to_end/test_scalar_callbacks.py::test_immediate_scalar_dummy_procedure_calls_python_callback[source]` (`runtime`) | canonical | | [Feature Matrix: Runtime Error Projection, GIL Policy, Recursion, OpenMP Path, And GNU ABI Checks](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | status error and message; completed GIL envelope; recursion/OpenMP/ABI remain separately owned; no caller synchronization inference | `tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_planner_records_editable_native_runtime_and_status_error_facts` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | — | canonical | -| [Building The Shared Library: Build](../../docs/user/guide/building-shared-library.md#build) | Supported | source input; default and explicit module names; build directory; generated sources; importable shared library | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_source_build_result_records_structured_native_plan`
`tests/fortran/building_shared_library/pipeline/test_source_generated_contracts.py::test_source_build_generated_pyi_contract_matches_fixture[fruntime_abi_f90]` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_documented_readme_points_example_builds_and_imports` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_wrapper_build_rejects_empty_source_list` (`pipeline`)
`tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_wrapper_build_rejects_missing_source` (`pipeline`) | canonical | -| [Building The Shared Library: Import](../../docs/user/guide/building-shared-library.md#import) | Supported | ABI-suffixed artifact; stable module import name; explicit output name; root-function name collision avoidance | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_dir_separates_abi_artifact_from_cli_alias` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_names_importable_shared_library`
`tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_default_module_name_does_not_collide_with_root_function` | — | canonical | -| [Building The Shared Library: Multiple Source Files](../../docs/user/guide/building-shared-library.md#multiple-source-files) | Supported | caller order; contained-module namespaces; standalone externals; one merged extension; generated and edited contract parity | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_multi_source_pyi_out_writes_one_flat_combined_package` | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_multi_file_modules_build_one_merged_extension`
`tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_multi_file_standalone_procedures_build_one_merged_extension` | `tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_missing_module_directory_reports_compile_error` (`compiling`) | canonical | -| [Building The Shared Library: Use A Makefile](../../docs/user/guide/building-shared-library.md#use-a-makefile) | Supported | generation without compilation; editable compiler and flags; ordered source dependencies; GNU Make build; manifest regeneration and replay | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_pyi_makefile_manifest_and_replay_workflows` | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_makefile_mode_reproduces_multi_source_build` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_wrapper_build_rejects_generation_verbose_combination[makefile]` (`pipeline`) | canonical | -| [Building The Shared Library: Compatibility](../../docs/user/guide/building-shared-library.md#compatibility) | Supported | target ABI; debug and optimized wrappers; top-level kind flags; platform-specific extension; rebuildable native artifacts | `tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py::test_top_level_native_kind_flags_drive_internal_type_measurement` | `tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py::test_debug_and_optimized_wrapper_builds_preserve_runtime_abi` | `tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_incompatible_native_artifact_reports_linker_error` (`compiling`) | canonical | -| [Fortran Wrapper: Building And Importing A Wrapper](../../docs/user/reference/fortran-wrapper.md#building-and-importing-a-wrapper) | Supported | fixed and free source forms; direct source and source-free `.pyi` entry routes; explicit native artifacts; output placement; verbose commands | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_generated_pyi_fixture_builds_from_native_object_without_source_reparse`
`tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_verbose_mode_prints_full_direct_build_commands` | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_scale_runtime_contract[source]`
`tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_scale_runtime_contract[generated-pyi]` | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_pyi_cli_requires_a_native_link_input` (`pipeline`) | canonical | -| [Fortran Wrapper: Wrapper Build Mechanism](../../docs/user/reference/fortran-wrapper.md#wrapper-build-mechanism) | Supported | ordered source preprocessing through parsing, semantics, completed policy, wrapper plan, direct lowering, compilation, and one extension link | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_generate_sources_cli_writes_wrapper_sources_without_native_outputs`
`tests/fortran/building_shared_library/pipeline/test_source_generated_contracts.py::test_source_build_generated_pyi_contract_matches_fixture[verbose_api]` | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_internal_preprocessing_mode_still_builds_importable_runtime_wrapper` | — | canonical | -| [Fortran Wrapper: Native Build Plan In Build Results](../../docs/user/reference/fortran-wrapper.md#native-build-plan-in-build-results) | Supported | semantic sources separate from compilation units; produced and prebuilt artifacts; module/include/library directories; ordered object, archive, shared, named-library, and linker-argument items | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_native_link_plan_serializes_interleaved_item_kinds`
`tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_pyi_cli_preserves_explicit_ordered_link_items` | `tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_mixed_module_external_bundle_resolves_all_native_input_kinds` | — | canonical | -| [Fortran Wrapper: Multiple Sources And Build Modes](../../docs/user/reference/fortran-wrapper.md#multiple-sources-and-build-modes) | Supported | compiler-valid caller order; module and external merging; source/generated contract runtime parity; modified entry exports | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_multi_source_pyi_out_writes_one_flat_combined_package` | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_multi_source_generated_contract_build_matches_source_runtime_and_link_order`
`tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_multi_source_modified_entry_preserves_modules_and_adds_documented_alias` | — | canonical | -| [Fortran Wrapper: Semantic Stub Output](../../docs/user/reference/fortran-wrapper.md#semantic-stub-output) | Supported | one flat combined package; one entry; native module leaves; no per-source or synthetic directory; entry-only semantic input | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_multi_source_pyi_out_writes_one_flat_combined_package`
`tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_generated_pyi_matches_checked_in_fixture` | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_multi_source_generated_contract_build_matches_source_runtime_and_link_order` | — | canonical | -| [Fortran Wrapper: Editable Makefile](../../docs/user/reference/fortran-wrapper.md#editable-makefile) | Supported | resolved compiler; Fortran and C wrapper flags; ordered source prerequisites; manifest-backed `.pyi` generation and replay | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_pyi_makefile_manifest_and_replay_workflows` | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_makefile_mode_reproduces_multi_source_build` | — | canonical | -| [Fortran Wrapper: Advanced Multi-Source Integration](../../docs/user/reference/fortran-wrapper.md#advanced-multi-source-integration) | Partially supported | explicit caller-ordered sources, module directories, libraries, and runtime paths; no automatic dependency, prebuilt-module, or external-library discovery | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_source_build_reuses_native_plan_for_additional_compile_and_link_inputs` | `tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_required_transitive_named_library_resolves_runtime_symbol` | `tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_missing_module_directory_reports_compile_error` (`compiling`) | canonical | -| [Semantic `.pyi`: Native Artifacts And Link Resolution](../../docs/user/reference/semantic-pyi-format.md#native-artifacts-and-link-resolution) | Supported | no filename inference; objects, archives, direct and named shared libraries; transitive providers; archive groups; missing/duplicate/incompatible artifact diagnostics | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_pyi_cli_preserves_explicit_ordered_link_items`
`tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_static_archive_groups_resolve_cyclic_archive_dependencies` | `tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_mixed_module_external_bundle_resolves_all_native_input_kinds` | `tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_missing_symbol_reports_native_link_or_loader_error` (`import`)
`tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_duplicate_native_definitions_report_linker_error` (`compiling`) | canonical | -| [Semantic `.pyi`: Contract Imports](../../docs/user/reference/semantic-pyi-format.md#contract-imports) | Supported | explicit `prik.contracts` imports; arbitrary aliases; missing imports rejected; ordinary and relative imports preserved | `tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_convert_pyi_to_ir_requires_imported_contract_types`
`tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py::test_convert_pyi_to_ir_follows_arbitrary_contract_aliases` | — | `tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_convert_pyi_to_ir_requires_imported_contract_types` (`semantics`) | canonical | -| [Semantic `.pyi`: Misuse, Diagnostics And Risk](../../docs/user/reference/semantic-pyi-format.md#misuse-diagnostics-and-risk) | Supported | syntax, semantic shape, native contract, policy, and unsafe-boundary diagnostics; filename-aware failures; no silent fallback | `tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_pyi_file_to_semantic_module_and_modules_forward_module_name_encoding_and_filename` | — | `tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py::test_pyi_parser_reports_unsupported_lines_and_invalid_helpers` (`parsing`)
`tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_pyi_python_api_rejects_invalid_projection_before_codegen` (`pipeline`) | canonical | -| [Semantic `.pyi`: File Shape](../../docs/user/reference/semantic-pyi-format.md#file-shape) | Supported | Python AST boundary; imports, annotated declarations, classes, ellipsis-only functions, and supported decorators | `tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py::test_pyi_parser_returns_python_ast_only`
`tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py::test_convert_pyi_to_ir_accepts_parsed_pyi_ast_only` | — | `tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py::test_pyi_parser_reports_unsupported_lines_and_invalid_helpers` (`parsing`) | canonical | -| [Semantic `.pyi`: Imported Derived-Type Identity](../../docs/user/reference/semantic-pyi-format.md#imported-derived-type-identity) | Supported | direct, aliased, relative, qualified, opaque, and edited wrapped external type identity | `tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_pyi_paths_to_semantic_modules_reconciles_opaque_and_edited_external_types`
`tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_pyi_paths_to_semantic_modules_reconciles_relative_namespace_type_refs` | — | `tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_pyi_paths_to_semantic_modules_handles_duplicate_roots_and_ambiguous_module_names` (`semantics`) | canonical | -| [Semantic `.pyi`: Contract Files And Native Procedure Placement](../../docs/user/reference/semantic-pyi-format.md#contract-files-and-native-procedure-placement) | Supported | entry contract; native module leaves; standalone root declarations; multiple modules; same-name module collision | `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_multi_module_generation_keeps_each_native_namespace`
`tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_same_named_module_uses_init_entry_and_keeps_externals_at_root` | `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback` | — | canonical | -| [Semantic `.pyi`: Contained Module Procedures](../../docs/user/reference/semantic-pyi-format.md#contained-module-procedures) | Supported | filename-selected native module scope; child Python namespace; exact native procedure name | `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_module_generation_writes_explicit_package_entry_and_native_leaf`
`tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_generated_native_scope_comes_from_contract_filename` | `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback` | — | canonical | -| [Semantic `.pyi`: Standalone Procedures](../../docs/user/reference/semantic-pyi-format.md#standalone-procedures) | Supported | `@standalone`; entry placement; multiple root procedures; no invented module scope | `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_standalone_generation_writes_explicit_package_entry`
`tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_generated_standalone_contract_retains_standalone_native_placement` | — | — | canonical | -| [Semantic `.pyi`: Source-To-Contract Layout](../../docs/user/reference/semantic-pyi-format.md#source-to-contract-layout) | Supported | module-only, standalone-only, mixed, multi-module, same-name, and transitive-import source layouts | `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_import_graph_generation_writes_entry_and_native_leaves`
`tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_multi_module_generation_keeps_each_native_namespace` | `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback` | — | canonical | -| [Semantic `.pyi`: Root Export Contract](../../docs/user/reference/semantic-pyi-format.md#root-export-contract) | Supported | module import, selective symbol export, alias, support-import exclusion, and collision rejection | `tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_checked_entry_discovers_its_complete_contract_package[contract_import_graph]`
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_rejects_colliding_wildcard_exports` (`pipeline`) | canonical | -| [Semantic `.pyi`: Entry Contract And Extension Identity](../../docs/user/reference/semantic-pyi-format.md#entry-contract-and-extension-identity) | Supported | `__init__.pyi` parent identity; explicit output identity; leaf identity; ABI-suffixed shared object | `tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py::test_same_named_module_uses_init_entry_and_keeps_externals_at_root` | `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback`
`tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_dir_separates_abi_artifact_from_cli_alias` | — | canonical | -| [Semantic `.pyi`: Contract Import Graph](../../docs/user/reference/semantic-pyi-format.md#contract-import-graph) | Supported | recursive relative imports; deterministic discovery order; parse cache; missing file and cycle diagnostics | `tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_pyi_contract_bundle_reuses_import_discovery_conversion_cache`
`tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_checked_entry_discovers_its_complete_contract_package[contract_import_graph]` | — | `tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_recursive_graph_reports_missing_relative_contract_before_native_validation` (`pipeline`)
`tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_recursive_graph_reports_cycles_before_codegen` (`pipeline`) | canonical | -| [Semantic `.pyi`: Semantic Type Names](../../docs/user/reference/semantic-pyi-format.md#semantic-type-names) | Supported | canonical primitive, wrapper, nested, qualified, aliased, callback, and storage type spellings | `tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py::test_convert_pyi_to_ir_dispatches_nested_and_qualified_semantic_types`
`tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py::test_convert_pyi_to_ir_accepts_aliased_contract_wrapper_names` | — | — | canonical | -| [Semantic `.pyi`: Metadata With `Annotated`](../../docs/user/reference/semantic-pyi-format.md#metadata-with-annotated) | Supported | constraints; source names; layout/copy; immutability; native descriptor and provenance metadata; stable round trip | `tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py::test_pyi_parser_preserves_generic_constraints_as_annotation_metadata`
`tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py::test_convert_pyi_to_ir_preserves_extended_array_metadata_and_nested_selector` | — | `tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py::test_convert_pyi_to_ir_rejects_additional_invalid_storage_forms[value: Annotated[Int32, 'bad']\n-Unsupported Annotated metadata: "'bad'"]` (`semantics`) | canonical | -| [Semantic `.pyi`: Classes And Native Type Markers](../../docs/user/reference/semantic-pyi-format.md#classes-and-native-type-markers) | Supported | ordinary wrapped classes; opaque external classes; field declarations; irreducible native markers | `tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py::test_pyi_paths_to_semantic_modules_reconciles_opaque_and_edited_external_types`
`tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py::test_value_projection_round_trips_as_argument_specific_native_transport` | `tests/fortran/derived_types/end_to_end/test_derived_boundaries.py::test_scalar_derived_types_cross_procedure_boundaries[generated-pyi]` | — | canonical | -| [Semantic `.pyi`: Functions, Methods And Returns](../../docs/user/reference/semantic-pyi-format.md#functions-methods-and-returns) | Supported | direct and tuple returns; named replacement outputs; native-order identity; method receiver; explicit projection | `tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py::test_plain_tuple_return_types_parse_component_returns`
`tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py::test_native_order_outputs_do_not_get_projected_without_native_call` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | — | canonical | +| [Building The Shared Library: Build](../../docs/user/guide/building-shared-library.md#build) | Supported | source input; default and explicit module names; build directory; generated sources; importable shared library | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_source_build_result_records_structured_native_plan`
`tests/fortran/infrastructure/building/pipeline/test_source_generated_contracts.py::test_source_build_generated_pyi_contract_matches_fixture[fruntime_abi_f90]` | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_documented_readme_points_example_builds_and_imports` | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_wrapper_build_rejects_empty_source_list` (`pipeline`)
`tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_wrapper_build_rejects_missing_source` (`pipeline`) | canonical | +| [Building The Shared Library: Import](../../docs/user/guide/building-shared-library.md#import) | Supported | ABI-suffixed artifact; stable module import name; explicit output name; root-function name collision avoidance | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_dir_separates_abi_artifact_from_cli_alias` | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_names_importable_shared_library`
`tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_fortran_wrapper_default_module_name_does_not_collide_with_root_function` | — | canonical | +| [Building The Shared Library: Multiple Source Files](../../docs/user/guide/building-shared-library.md#multiple-source-files) | Supported | caller order; contained-module namespaces; standalone externals; one merged extension; generated and edited contract parity | `tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py::test_multi_source_pyi_out_writes_one_flat_combined_package` | `tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py::test_multi_file_modules_build_one_merged_extension`
`tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py::test_multi_file_standalone_procedures_build_one_merged_extension` | `tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py::test_missing_module_directory_reports_compile_error` (`compiling`) | canonical | +| [Building The Shared Library: Use A Makefile](../../docs/user/guide/building-shared-library.md#use-a-makefile) | Supported | generation without compilation; editable compiler and flags; ordered source dependencies; GNU Make build; manifest regeneration and replay | `tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_pyi_makefile_manifest_and_replay_workflows` | `tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py::test_makefile_mode_reproduces_multi_source_build` | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_wrapper_build_rejects_generation_verbose_combination[makefile]` (`pipeline`) | canonical | +| [Building The Shared Library: Compatibility](../../docs/user/guide/building-shared-library.md#compatibility) | Supported | target ABI; debug and optimized wrappers; top-level kind flags; platform-specific extension; rebuildable native artifacts | `tests/fortran/infrastructure/building/end_to_end/test_runtime_compatibility.py::test_top_level_native_kind_flags_drive_internal_type_measurement` | `tests/fortran/infrastructure/building/end_to_end/test_runtime_compatibility.py::test_debug_and_optimized_wrapper_builds_preserve_runtime_abi` | `tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py::test_incompatible_native_artifact_reports_linker_error` (`compiling`) | canonical | +| [Fortran Wrapper: Building And Importing A Wrapper](../../docs/user/reference/fortran-wrapper.md#building-and-importing-a-wrapper) | Supported | fixed and free source forms; direct source and source-free `.pyi` entry routes; explicit native artifacts; output placement; verbose commands | `tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_generated_pyi_fixture_builds_from_native_object_without_source_reparse`
`tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_verbose_mode_prints_full_direct_build_commands` | `tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_scale_runtime_contract[source]`
`tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_scale_runtime_contract[generated-pyi]` | `tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_pyi_cli_requires_a_native_link_input` (`pipeline`) | canonical | +| [Fortran Wrapper: Wrapper Build Mechanism](../../docs/user/reference/fortran-wrapper.md#wrapper-build-mechanism) | Supported | ordered source preprocessing through parsing, semantics, completed policy, wrapper plan, direct lowering, compilation, and one extension link | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_generate_sources_cli_writes_wrapper_sources_without_native_outputs`
`tests/fortran/infrastructure/building/pipeline/test_source_generated_contracts.py::test_source_build_generated_pyi_contract_matches_fixture[verbose_api]` | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_internal_preprocessing_mode_still_builds_importable_runtime_wrapper` | — | canonical | +| [Fortran Wrapper: Native Build Plan In Build Results](../../docs/user/reference/fortran-wrapper.md#native-build-plan-in-build-results) | Supported | semantic sources separate from compilation units; produced and prebuilt artifacts; module/include/library directories; ordered object, archive, shared, named-library, and linker-argument items | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_native_link_plan_serializes_interleaved_item_kinds`
`tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_pyi_cli_preserves_explicit_ordered_link_items` | `tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py::test_mixed_module_external_bundle_resolves_all_native_input_kinds` | — | canonical | +| [Fortran Wrapper: Multiple Sources And Build Modes](../../docs/user/reference/fortran-wrapper.md#multiple-sources-and-build-modes) | Supported | compiler-valid caller order; module and external merging; source/generated contract runtime parity; modified entry exports | `tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py::test_multi_source_pyi_out_writes_one_flat_combined_package` | `tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py::test_multi_source_generated_contract_build_matches_source_runtime_and_link_order`
`tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py::test_multi_source_modified_entry_preserves_modules_and_adds_documented_alias` | — | canonical | +| [Fortran Wrapper: Semantic Stub Output](../../docs/user/reference/fortran-wrapper.md#semantic-stub-output) | Supported | one flat combined package; one entry; native module leaves; no per-source or synthetic directory; entry-only semantic input | `tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py::test_multi_source_pyi_out_writes_one_flat_combined_package`
`tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_generated_pyi_matches_checked_in_fixture` | `tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py::test_multi_source_generated_contract_build_matches_source_runtime_and_link_order` | — | canonical | +| [Fortran Wrapper: Editable Makefile](../../docs/user/reference/fortran-wrapper.md#editable-makefile) | Supported | resolved compiler; Fortran and C wrapper flags; ordered source prerequisites; manifest-backed `.pyi` generation and replay | `tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_pyi_makefile_manifest_and_replay_workflows` | `tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py::test_makefile_mode_reproduces_multi_source_build` | — | canonical | +| [Fortran Wrapper: Advanced Multi-Source Integration](../../docs/user/reference/fortran-wrapper.md#advanced-multi-source-integration) | Partially supported | explicit caller-ordered sources, module directories, libraries, and runtime paths; no automatic dependency, prebuilt-module, or external-library discovery | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_source_build_reuses_native_plan_for_additional_compile_and_link_inputs` | `tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py::test_required_transitive_named_library_resolves_runtime_symbol` | `tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py::test_missing_module_directory_reports_compile_error` (`compiling`) | canonical | +| [Semantic `.pyi`: Native Artifacts And Link Resolution](../../docs/user/reference/semantic-pyi-format.md#native-artifacts-and-link-resolution) | Supported | no filename inference; objects, archives, direct and named shared libraries; transitive providers; archive groups; missing/duplicate/incompatible artifact diagnostics | `tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_pyi_cli_preserves_explicit_ordered_link_items`
`tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py::test_static_archive_groups_resolve_cyclic_archive_dependencies` | `tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py::test_mixed_module_external_bundle_resolves_all_native_input_kinds` | `tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py::test_missing_symbol_reports_native_link_or_loader_error` (`import`)
`tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py::test_duplicate_native_definitions_report_linker_error` (`compiling`) | canonical | +| [Semantic `.pyi`: Contract Imports](../../docs/user/reference/semantic-pyi-format.md#contract-imports) | Supported | explicit `prik.contracts` imports; arbitrary aliases; missing imports rejected; ordinary and relative imports preserved | `tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py::test_convert_pyi_to_ir_requires_imported_contract_types`
`tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py::test_convert_pyi_to_ir_follows_arbitrary_contract_aliases` | — | `tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py::test_convert_pyi_to_ir_requires_imported_contract_types` (`semantics`) | canonical | +| [Semantic `.pyi`: Misuse, Diagnostics And Risk](../../docs/user/reference/semantic-pyi-format.md#misuse-diagnostics-and-risk) | Supported | syntax, semantic shape, native contract, policy, and unsafe-boundary diagnostics; filename-aware failures; no silent fallback | `tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py::test_pyi_file_to_semantic_module_and_modules_forward_module_name_encoding_and_filename` | — | `tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py::test_pyi_parser_reports_unsupported_lines_and_invalid_helpers` (`parsing`)
`tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_pyi_python_api_rejects_invalid_projection_before_codegen` (`pipeline`) | canonical | +| [Semantic `.pyi`: File Shape](../../docs/user/reference/semantic-pyi-format.md#file-shape) | Supported | Python AST boundary; imports, annotated declarations, classes, ellipsis-only functions, and supported decorators | `tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py::test_pyi_parser_returns_python_ast_only`
`tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py::test_convert_pyi_to_ir_accepts_parsed_pyi_ast_only` | — | `tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py::test_pyi_parser_reports_unsupported_lines_and_invalid_helpers` (`parsing`) | canonical | +| [Semantic `.pyi`: Imported Derived-Type Identity](../../docs/user/reference/semantic-pyi-format.md#imported-derived-type-identity) | Supported | direct, aliased, relative, qualified, opaque, and edited wrapped external type identity | `tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py::test_pyi_paths_to_semantic_modules_reconciles_opaque_and_edited_external_types`
`tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py::test_pyi_paths_to_semantic_modules_reconciles_relative_namespace_type_refs` | — | `tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py::test_pyi_paths_to_semantic_modules_handles_duplicate_roots_and_ambiguous_module_names` (`semantics`) | canonical | +| [Semantic `.pyi`: Contract Files And Native Procedure Placement](../../docs/user/reference/semantic-pyi-format.md#contract-files-and-native-procedure-placement) | Supported | entry contract; native module leaves; standalone root declarations; multiple modules; same-name module collision | `tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py::test_multi_module_generation_keeps_each_native_namespace`
`tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py::test_same_named_module_uses_init_entry_and_keeps_externals_at_root` | `tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback` | — | canonical | +| [Semantic `.pyi`: Contained Module Procedures](../../docs/user/reference/semantic-pyi-format.md#contained-module-procedures) | Supported | filename-selected native module scope; child Python namespace; exact native procedure name | `tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py::test_module_generation_writes_explicit_package_entry_and_native_leaf`
`tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py::test_generated_native_scope_comes_from_contract_filename` | `tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback` | — | canonical | +| [Semantic `.pyi`: Standalone Procedures](../../docs/user/reference/semantic-pyi-format.md#standalone-procedures) | Supported | `@standalone`; entry placement; multiple root procedures; no invented module scope | `tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py::test_standalone_generation_writes_explicit_package_entry`
`tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py::test_generated_standalone_contract_retains_standalone_native_placement` | — | — | canonical | +| [Semantic `.pyi`: Source-To-Contract Layout](../../docs/user/reference/semantic-pyi-format.md#source-to-contract-layout) | Supported | module-only, standalone-only, mixed, multi-module, same-name, and transitive-import source layouts | `tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py::test_import_graph_generation_writes_entry_and_native_leaves`
`tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py::test_multi_module_generation_keeps_each_native_namespace` | `tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback` | — | canonical | +| [Semantic `.pyi`: Root Export Contract](../../docs/user/reference/semantic-pyi-format.md#root-export-contract) | Supported | module import, selective symbol export, alias, support-import exclusion, and collision rejection | `tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_checked_entry_discovers_its_complete_contract_package[contract_import_graph]`
`tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_rejects_colliding_wildcard_exports` (`pipeline`) | canonical | +| [Semantic `.pyi`: Entry Contract And Extension Identity](../../docs/user/reference/semantic-pyi-format.md#entry-contract-and-extension-identity) | Supported | `__init__.pyi` parent identity; explicit output identity; leaf identity; ABI-suffixed shared object | `tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py::test_same_named_module_uses_init_entry_and_keeps_externals_at_root` | `tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback`
`tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_dir_separates_abi_artifact_from_cli_alias` | — | canonical | +| [Semantic `.pyi`: Contract Import Graph](../../docs/user/reference/semantic-pyi-format.md#contract-import-graph) | Supported | recursive relative imports; deterministic discovery order; parse cache; missing file and cycle diagnostics | `tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_pyi_contract_bundle_reuses_import_discovery_conversion_cache`
`tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_checked_entry_discovers_its_complete_contract_package[contract_import_graph]` | — | `tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_recursive_graph_reports_missing_relative_contract_before_native_validation` (`pipeline`)
`tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_recursive_graph_reports_cycles_before_codegen` (`pipeline`) | canonical | +| [Semantic `.pyi`: Semantic Type Names](../../docs/user/reference/semantic-pyi-format.md#semantic-type-names) | Supported | canonical primitive, wrapper, nested, qualified, aliased, callback, and storage type spellings | `tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py::test_convert_pyi_to_ir_dispatches_nested_and_qualified_semantic_types`
`tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py::test_convert_pyi_to_ir_accepts_aliased_contract_wrapper_names` | — | — | canonical | +| [Semantic `.pyi`: Metadata With `Annotated`](../../docs/user/reference/semantic-pyi-format.md#metadata-with-annotated) | Supported | constraints; source names; layout/copy; immutability; native descriptor and provenance metadata; stable round trip | `tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py::test_pyi_parser_preserves_generic_constraints_as_annotation_metadata`
`tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py::test_convert_pyi_to_ir_preserves_extended_array_metadata_and_nested_selector` | — | `tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py::test_convert_pyi_to_ir_rejects_additional_invalid_storage_forms[value: Annotated[Int32, 'bad']\n-Unsupported Annotated metadata: "'bad'"]` (`semantics`) | canonical | +| [Semantic `.pyi`: Classes And Native Type Markers](../../docs/user/reference/semantic-pyi-format.md#classes-and-native-type-markers) | Supported | ordinary wrapped classes; opaque external classes; field declarations; irreducible native markers | `tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py::test_pyi_paths_to_semantic_modules_reconciles_opaque_and_edited_external_types`
`tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py::test_value_projection_round_trips_as_argument_specific_native_transport` | `tests/fortran/derived_types/end_to_end/test_derived_boundaries.py::test_scalar_derived_types_cross_procedure_boundaries[generated-pyi]` | — | canonical | +| [Semantic `.pyi`: Functions, Methods And Returns](../../docs/user/reference/semantic-pyi-format.md#functions-methods-and-returns) | Supported | direct and tuple returns; named replacement outputs; native-order identity; method receiver; explicit projection | `tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py::test_plain_tuple_return_types_parse_component_returns`
`tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py::test_native_order_outputs_do_not_get_projected_without_native_call` | `tests/fortran/functions/end_to_end/test_documented_function_journeys.py::test_function_results_outputs_arrays_and_no_intent_replacements_follow_documented_order` | — | canonical | | [Semantic `.pyi`: Generic Procedure Overloads](../../docs/user/reference/semantic-pyi-format.md#generic-procedure-overloads) | Supported | explicit specific links; private link targets; native bind; exact signature resolution; deterministic errors | `tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py::test_convert_pyi_to_ir_resolves_prik_overload_by_explicit_specific_name` | `tests/fortran/generic_interfaces/end_to_end/test_generic_interfaces.py::test_fortran_generic_interfaces_dispatch_in_generated_c_extension[generated-pyi]` | `tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py::test_convert_pyi_to_ir_rejects_invalid_prik_overload_links[@overload("missing")\ndef convert(value: Int32) -> Int32: ...\n-missing specific procedure 'missing']` (`semantics`) | canonical | | [Semantic `.pyi`: Defined Operators And Assignment](../../docs/user/reference/semantic-pyi-format.md#defined-operators-and-assignment) | Supported | direct/reflected/unary/comparison/named operators; explicit mutating assignment method | `tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_defined_operators_assignment_and_type_bound_operators` | `tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py::test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extension[generated-pyi]` | — | canonical | | [Semantic `.pyi`: Allocatable Array Handles](../../docs/user/reference/semantic-pyi-format.md#allocatable-array-handles) | Supported | persistent handle syntax; allocated/unallocated state; live views; field/module/result ownership; explicit copy | `tests/fortran/allocatables/semantics/test_pyi_allocatable_semantics.py::test_persistent_allocatable_descriptors_preserve_scalar_and_array_kinds` | `tests/fortran/allocatables/end_to_end/test_allocatable_handles.py::test_allocatable_module_fields_and_results_expose_lifetime_safe_handles[generated-pyi]` | `tests/fortran/allocatables/runtime/test_allocatable_contract_handles.py::test_generated_storage_rejects_a_closed_contract_handle` (`runtime`) | canonical | -| [Semantic `.pyi`: Visibility And Names](../../docs/user/reference/semantic-pyi-format.md#visibility-and-names) | Supported | decorator and type-wrapper privacy; source-name metadata; invalid Python identifiers; native binding retained | `tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py::test_convert_pyi_to_ir_preserves_user_private_bound_function_contract`
`tests/fortran/semantic_pyi_format/semantics/test_round_trip_properties.py::test_generated_pyi_escaping_round_trips_native_names` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | — | canonical | -| [Semantic `.pyi`: Projection Metadata](../../docs/user/reference/semantic-pyi-format.md#projection-metadata) | Supported | ordered `Arg`, `Addr`, `Value`, `Return`, descriptor, length, shape, presence, literal, pass, and workspace entries | `tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py::test_native_call_accepts_hidden_native_values`
`tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py::test_emit_native_call_hidden_native_values` | — | `tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_pyi_python_api_rejects_invalid_projection_before_codegen` (`pipeline`) | canonical | -| [Semantic `.pyi`: Current Generated Coverage](../../docs/user/reference/semantic-pyi-format.md#current-generated-coverage) | Partially supported | canonical parser/printer round trip; reviewed package layout; authoritative runtime input; documented generated and loaded subsets | `tests/fortran/semantic_pyi_format/semantics/test_round_trip_properties.py::test_generated_semantic_ir_round_trips_through_pyi`
`tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_checked_contract_package_has_reviewed_files` | `tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback` | — | canonical | -| [Semantic `.pyi`: Rejected Or Not Yet Supported](../../docs/user/reference/semantic-pyi-format.md#rejected-or-not-yet-supported) | Blocked | unknown types; invalid subscriptions, depth, callable shapes, decorators, bodies, arguments, and overload/projection combinations | — | — | `tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py::test_convert_pyi_to_ir_rejects_invalid_projection_and_type_forms[value: Unknown\n-Unknown semantic type is not allowed in .pyi annotations]` (`semantics`)
`tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py::test_convert_pyi_to_ir_rejects_additional_invalid_storage_forms[value: Float64[ORDER_F]\n-Non-dimensional type subscriptions are not supported; use Final[...] for constants and Annotated[...] for constraints or array metadata]` (`semantics`) | canonical | -| [Semantic `.pyi`: Remaining Format And Runtime Work](../../docs/user/reference/semantic-pyi-format.md#remaining-format-and-runtime-work) | Partially supported | implemented ordered projection and policy dispatch; broader polymorphism, pointer lifetimes, and IDE-only stub separation remain limited | `tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py::test_fortran_to_pyi_and_back_preserves_mixed_input_output_projection` | — | `tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_rejects_generic_constructor_interfaces_during_semantic_conversion` (`semantics`)
`tests/fortran/allocatables/policy/test_allocatable_result_policy.py::test_direct_allocatable_scalar_function_result_is_blocked_before_codegen` (`policy`) | canonical | -| [`.pyi` Exports And Modules: Choose The Package Shape](../../docs/user/reference/pyi-contracts/exports-and-modules.md#choose-the-package-shape) | Supported | child namespaces; wildcard flattening; selective imports; symbol and module aliases; nested aliases; support-import exclusion; reachable declarations only | `tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering`
`tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_checked_entry_discovers_its_complete_contract_package[contract_import_graph]` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_rejects_colliding_wildcard_exports` (`pipeline`)
`tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_recursive_graph_reports_missing_relative_contract_before_native_validation` (`pipeline`)
`tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_recursive_graph_reports_cycles_before_codegen` (`pipeline`) | canonical | -| [`.pyi` Exports And Modules: Remove Or Hide A Declaration](../../docs/user/reference/pyi-contracts/exports-and-modules.md#remove-or-hide-a-declaration) | Supported | deleted function and variable; `@private`; `private[...]`; class constructor suppression; later class/member/overload runtime owner retained | `tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering`
`tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_removing_constructor_suppresses_generated_keyword_initialization` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | — | canonical | -| [`.pyi` Exports And Modules: Add Or Rename A Native Procedure](../../docs/user/reference/pyi-contracts/exports-and-modules.md#add-or-rename-a-native-procedure) | Supported | added module-leaf declaration; `@bind`; renamed standalone `@standalone`; unchanged native targets; no invented implementation | `tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py::test_convert_pyi_to_ir_preserves_user_private_bound_function_contract` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | — | canonical | -| [`.pyi` Exports And Modules: Set Module Values At Import](../../docs/user/reference/pyi-contracts/exports-and-modules.md#set-module-values-at-import) | Supported | mutable Boolean, integer, real, and complex literals; import-time write-through; `Final` constant distinction; unsupported setter/storage and expression rejection | `tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_literal_defaults_are_preserved`
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_module_variable_initializer_policy_is_complete_before_ir_lowering`
`tests/fortran/pyi_contracts/exports_and_modules/codegen/test_module_initializer_lowering.py::test_module_variable_literal_families_select_their_c_spelling` | `tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | `tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_expression_defaults_are_rejected[from prik.contracts import Int32\ncounter: Int32 = f(42)\n]` (`semantics`)
`tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_unsupported_module_variable_initializer_completes_an_unsupported_policy` (`policy`) | canonical | -| [`.pyi` Functions And Classes: Expose A Module Procedure As A Method](../../docs/user/reference/pyi-contracts/functions-and-classes.md#expose-a-module-procedure-as-a-method) | Supported | retained module declaration; `Pass()` receiver placement; public or private module surface; same or bound method target | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_method_and_module_declarations_keep_native_targets_independent`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_module_procedure_method_visibility_is_completed_independently` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | — | canonical | -| [`.pyi` Functions And Classes: Edit An Overload Set](../../docs/user/reference/pyi-contracts/functions-and-classes.md#edit-an-overload-set) | Supported | deleted and added candidates; exact dtype dispatch; module and class `@bind`; private-specific routing; native-private accessibility retained | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_edited_overloads_complete_exact_dispatch_and_reject_ambiguous_plan` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract`
`tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_editable_contract_removes_class_method_constructor_member_and_overload` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_private_native_specific_without_overload_bind_fails_at_build[private_module_specifics_without_bind-missing_targets0]` (`compiling`)
`tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_private_native_specific_without_overload_bind_fails_at_build[private_type_bound_specifics_without_bind-missing_targets1]` (`compiling`) | canonical | -| [`.pyi` Functions And Classes: Replace The Constructor](../../docs/user/reference/pyi-contracts/functions-and-classes.md#replace-the-constructor) | Supported | direct native initializer; one explicit `Pass()`; reordered native position; generated constructor replacement or removal; overload constructor | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bound_constructor_uses_explicit_pass_position_and_native_target`
`tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py::test_bound_constructor_and_method_reuse_completed_direct_function_plans`
`tests/fortran/pyi_contracts/functions_and_classes/codegen/test_constructor_lowering.py::test_bound_constructor_generates_one_initializer_without_keyword_default` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function`
`tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected[\nclass state:\n @bind("init_state")\n @native_call([Addr(Arg(0))])\n def __init__(self, seed: Int32) -> None: ...\n-Bound constructor native_call requires exactly one Pass() entry]` (`semantics`) | canonical | -| [`.pyi` Functions And Classes: Type-Bound And Magic Methods](../../docs/user/reference/pyi-contracts/functions-and-classes.md#type-bound-and-magic-methods) | Supported | concrete native targets; passed object; bound Python/native names; overloaded type-bound calls; operators and assignment retain exact candidate mapping | `tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets`
`tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_defined_operators_assignment_and_type_bound_operators` | `tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract`
`tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py::test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extension[source]` | `tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py::test_convert_pyi_to_ir_rejects_invalid_prik_overload_links[\ndef compare(left: item, right: item) -> Bool: ...\nclass item:\n @overload("compare", generic="operator(.eqv.)")\n def __add__(self, right: item) -> Bool: ...\n-generic 'operator\\(\\.eqv\\.\\)' is incompatible with method '__add__']` (`semantics`) | canonical | -| [`.pyi` Calls And Results: Expose Native Arguments Directly](../../docs/user/reference/pyi-contracts/calls-and-results.md#expose-native-arguments-directly) | Supported | no `@native_call`; native-order scalar, rank-zero storage, array, fixed string, and derived object arguments; visible caller mutation and discarded string-temporary mutation | `tests/fortran/pyi_contracts/calls_and_results/policy/test_call_and_result_policy.py::test_native_order_and_projected_result_positions_are_completed_before_planning` | `tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_native_order_exposes_writable_slots_without_projection` | — | canonical | -| [`.pyi` Calls And Results: Reorder Arguments And Project Outputs](../../docs/user/reference/pyi-contracts/calls-and-results.md#reorder-arguments-and-project-outputs) | Supported | reordered `Arg`/`Addr(Arg)`; hidden scalar, fixed string, and fixed-array results; caller arrays and derived objects; multiple-result tuple order; typed literals and complete projection grammar | `tests/fortran/pyi_contracts/calls_and_results/policy/test_call_and_result_policy.py::test_native_order_and_projected_result_positions_are_completed_before_planning`
`tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py::test_plan_records_reordered_arguments_gil_behavior_and_hidden_result_slots`
`tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py::test_native_call_accepts_hidden_native_values` | `tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_native_call_reorders_arguments_and_projects_mixed_results`
`tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_hidden_fixed_shape_array_output_is_allocated_and_returned` | `tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py::test_pyi_python_api_rejects_invalid_projection_before_codegen` (`pipeline`) | canonical | -| [`.pyi` Calls And Results: Control Mutation](../../docs/user/reference/pyi-contracts/calls-and-results.md#control-mutation) | Supported | immutable scalar, fixed string, array, and derived replacement results; unchanged Python inputs; copy-in/copy-out and identity writeback paths | `tests/fortran/pyi_contracts/calls_and_results/policy/test_call_and_result_policy.py::test_immutable_replacement_policy_is_complete_before_ir_lowering`
`tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py::test_replacement_writeback_dispatches_selected_scalar_result_behavior[copy_in_out]` | `tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_immutable_values_return_replacements_without_mutating_inputs` | `tests/fortran/memory_management/policy/test_memory_ownership_policy.py::test_contradictory_ownership_contract_fails_before_lowering` (`policy`) | canonical | +| [Semantic `.pyi`: Visibility And Names](../../docs/user/reference/semantic-pyi-format.md#visibility-and-names) | Supported | decorator and type-wrapper privacy; source-name metadata; invalid Python identifiers; native binding retained | `tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py::test_convert_pyi_to_ir_preserves_user_private_bound_function_contract`
`tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py::test_generated_pyi_escaping_round_trips_native_names` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | — | canonical | +| [Semantic `.pyi`: Projection Metadata](../../docs/user/reference/semantic-pyi-format.md#projection-metadata) | Supported | ordered `Arg`, `Addr`, `Value`, `Return`, descriptor, length, shape, presence, literal, pass, and workspace entries | `tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py::test_native_call_accepts_hidden_native_values`
`tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py::test_emit_native_call_hidden_native_values` | — | `tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_pyi_python_api_rejects_invalid_projection_before_codegen` (`pipeline`) | canonical | +| [Semantic `.pyi`: Current Generated Coverage](../../docs/user/reference/semantic-pyi-format.md#current-generated-coverage) | Partially supported | canonical parser/printer round trip; reviewed package layout; authoritative runtime input; documented generated and loaded subsets | `tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py::test_generated_semantic_ir_round_trips_through_pyi`
`tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_checked_contract_package_has_reviewed_files` | `tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py::test_generated_contract_rebuilds_without_native_source_fallback` | — | canonical | +| [Semantic `.pyi`: Rejected Or Not Yet Supported](../../docs/user/reference/semantic-pyi-format.md#rejected-or-not-yet-supported) | Blocked | unknown types; invalid subscriptions, depth, callable shapes, decorators, bodies, arguments, and overload/projection combinations | — | — | `tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py::test_convert_pyi_to_ir_rejects_invalid_projection_and_type_forms[value: Unknown\n-Unknown semantic type is not allowed in .pyi annotations]` (`semantics`)
`tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py::test_convert_pyi_to_ir_rejects_additional_invalid_storage_forms[value: Float64[ORDER_F]\n-Non-dimensional type subscriptions are not supported; use Final[...] for constants and Annotated[...] for constraints or array metadata]` (`semantics`) | canonical | +| [Semantic `.pyi`: Remaining Format And Runtime Work](../../docs/user/reference/semantic-pyi-format.md#remaining-format-and-runtime-work) | Partially supported | implemented ordered projection and policy dispatch; broader polymorphism, pointer lifetimes, and IDE-only stub separation remain limited | `tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py::test_fortran_to_pyi_and_back_preserves_mixed_input_output_projection` | — | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected` (`semantics`)
`tests/fortran/allocatables/policy/test_allocatable_result_policy.py::test_direct_allocatable_scalar_function_result_is_blocked_before_codegen` (`policy`) | canonical | +| [`.pyi` Exports And Modules: Choose The Package Shape](../../docs/user/reference/pyi-contracts/exports-and-modules.md#choose-the-package-shape) | Supported | child namespaces; wildcard flattening; selective imports; symbol and module aliases; nested aliases; support-import exclusion; reachable declarations only | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering`
`tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_checked_entry_discovers_its_complete_contract_package[contract_import_graph]` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_rejects_colliding_wildcard_exports` (`pipeline`)
`tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_recursive_graph_reports_missing_relative_contract_before_native_validation` (`pipeline`)
`tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_recursive_graph_reports_cycles_before_codegen` (`pipeline`) | canonical | +| [`.pyi` Exports And Modules: Remove Or Hide A Declaration](../../docs/user/reference/pyi-contracts/exports-and-modules.md#remove-or-hide-a-declaration) | Supported | deleted function and variable; `@private`; `private[...]`; class constructor suppression; later class/member/overload runtime owner retained | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_policy_completion_prunes_unexported_entry_declarations_before_lowering`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_removing_constructor_suppresses_generated_keyword_initialization` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | — | canonical | +| [`.pyi` Exports And Modules: Add Or Rename A Native Procedure](../../docs/user/reference/pyi-contracts/exports-and-modules.md#add-or-rename-a-native-procedure) | Supported | added module-leaf declaration; `@bind`; renamed standalone `@standalone`; unchanged native targets; no invented implementation | `tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py::test_convert_pyi_to_ir_preserves_user_private_bound_function_contract` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py::test_entry_contract_selects_child_flattened_aliased_and_bound_exports` | — | canonical | +| [`.pyi` Exports And Modules: Set Module Values At Import](../../docs/user/reference/pyi-contracts/exports-and-modules.md#set-module-values-at-import) | Supported | mutable Boolean, integer, real, and complex literals; import-time write-through; `Final` constant distinction; unsupported setter/storage and expression rejection | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_literal_defaults_are_preserved`
`tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_module_variable_initializer_policy_is_complete_before_ir_lowering`
`tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/codegen/test_module_initializer_lowering.py::test_module_variable_literal_families_select_their_c_spelling` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py::test_editable_contract_removes_hides_and_initializes_module_declarations` | `tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/semantics/test_module_initializers.py::test_mutable_module_expression_defaults_are_rejected[from prik.contracts import Int32\ncounter: Int32 = f(42)\n]` (`semantics`)
`tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py::test_unsupported_module_variable_initializer_completes_an_unsupported_policy` (`policy`) | canonical | +| [`.pyi` Functions And Classes: Expose A Module Procedure As A Method](../../docs/user/reference/pyi-contracts/functions-and-classes.md#expose-a-module-procedure-as-a-method) | Supported | retained module declaration; `Pass()` receiver placement; public or private module surface; same or bound method target | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_method_and_module_declarations_keep_native_targets_independent`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.py::test_module_procedure_method_visibility_is_completed_independently` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function` | — | canonical | +| [`.pyi` Functions And Classes: Edit An Overload Set](../../docs/user/reference/pyi-contracts/functions-and-classes.md#edit-an-overload-set) | Supported | deleted and added candidates; exact dtype dispatch; module and class `@bind`; private-specific routing; native-private accessibility retained | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.py::test_edited_overloads_complete_exact_dispatch_and_reject_ambiguous_plan` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_editable_contract_removes_class_method_constructor_member_and_overload` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_private_native_specific_without_overload_bind_fails_at_build[private_module_specifics_without_bind-missing_targets0]` (`compiling`)
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_private_native_specific_without_overload_bind_fails_at_build[private_type_bound_specifics_without_bind-missing_targets1]` (`compiling`) | canonical | +| [`.pyi` Functions And Classes: Replace The Constructor](../../docs/user/reference/pyi-contracts/functions-and-classes.md#replace-the-constructor) | Supported | direct native initializer; one explicit `Pass()`; reordered native position; generated constructor replacement or removal; overload constructor | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bound_constructor_uses_explicit_pass_position_and_native_target`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.py::test_bound_constructor_and_method_reuse_completed_direct_function_plans`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/codegen/test_constructor_lowering.py::test_bound_constructor_generates_one_initializer_without_keyword_default` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_procedure_is_reused_by_bound_constructor_method_and_public_function`
`tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_contradictory_constructor_declarations_are_rejected[\nclass state:\n @bind("init_state")\n @native_call([Addr(Arg(0))])\n def __init__(self, seed: Int32) -> None: ...\n-Bound constructor native_call requires exactly one Pass() entry]` (`semantics`) | canonical | +| [`.pyi` Functions And Classes: Type-Bound And Magic Methods](../../docs/user/reference/pyi-contracts/functions-and-classes.md#type-bound-and-magic-methods) | Supported | concrete native targets; passed object; bound Python/native names; overloaded type-bound calls; operators and assignment retain exact candidate mapping | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py::test_bind_selects_module_method_and_constructor_overload_targets`
`tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py::test_converter_preserves_defined_operators_assignment_and_type_bound_operators` | `tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py::test_module_method_and_constructor_overloads_share_one_edited_contract`
`tests/fortran/generic_interfaces/end_to_end/test_defined_operators.py::test_fortran_defined_operators_and_assignment_dispatch_in_generated_c_extension[source]` | `tests/fortran/generic_interfaces/semantics/test_pyi_overload_semantics.py::test_convert_pyi_to_ir_rejects_invalid_prik_overload_links[\ndef compare(left: item, right: item) -> Bool: ...\nclass item:\n @overload("compare", generic="operator(.eqv.)")\n def __add__(self, right: item) -> Bool: ...\n-generic 'operator\\(\\.eqv\\.\\)' is incompatible with method '__add__']` (`semantics`) | canonical | +| [`.pyi` Calls And Results: Expose Native Arguments Directly](../../docs/user/reference/pyi-contracts/calls-and-results.md#expose-native-arguments-directly) | Supported | no `@native_call`; native-order scalar, rank-zero storage, array, fixed string, and derived object arguments; visible caller mutation and discarded string-temporary mutation | `tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/policy/test_call_and_result_policy.py::test_native_order_and_projected_result_positions_are_completed_before_planning` | `tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_native_order_exposes_writable_slots_without_projection` | — | canonical | +| [`.pyi` Calls And Results: Reorder Arguments And Project Outputs](../../docs/user/reference/pyi-contracts/calls-and-results.md#reorder-arguments-and-project-outputs) | Supported | reordered `Arg`/`Addr(Arg)`; hidden scalar, fixed string, and fixed-array results; caller arrays and derived objects; multiple-result tuple order; typed literals and complete projection grammar | `tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/policy/test_call_and_result_policy.py::test_native_order_and_projected_result_positions_are_completed_before_planning`
`tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/codegen/test_call_and_result_lowering.py::test_plan_records_reordered_arguments_gil_behavior_and_hidden_result_slots`
`tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py::test_native_call_accepts_hidden_native_values` | `tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_native_call_reorders_arguments_and_projects_mixed_results`
`tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_hidden_fixed_shape_array_output_is_allocated_and_returned` | `tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py::test_pyi_python_api_rejects_invalid_projection_before_codegen` (`pipeline`) | canonical | +| [`.pyi` Calls And Results: Control Mutation](../../docs/user/reference/pyi-contracts/calls-and-results.md#control-mutation) | Supported | immutable scalar, fixed string, array, and derived replacement results; unchanged Python inputs; copy-in/copy-out and identity writeback paths | `tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/policy/test_call_and_result_policy.py::test_immutable_replacement_policy_is_complete_before_ir_lowering`
`tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/codegen/test_call_and_result_lowering.py::test_replacement_writeback_dispatches_selected_scalar_result_behavior[copy_in_out]` | `tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py::test_immutable_values_return_replacements_without_mutating_inputs` | `tests/fortran/memory_management/policy/test_memory_ownership_policy.py::test_contradictory_ownership_contract_fails_before_lowering` (`policy`) | canonical | | [`.pyi` Calls And Results: Edit Types Shapes Layout And Optionality](../../docs/user/reference/pyi-contracts/calls-and-results.md#edit-types-shapes-layout-and-optionality) | Supported | fixed/open shapes; exact dtype, rank, layout, writeability, byte order, alignment, and zero-size checks; Fortran-order default; supported nullable/defaulted native optionals | `tests/fortran/arrays/codegen/test_dense_array_shape_lowering.py::test_dense_array_lowering_uses_planned_shape_checks_and_bridge_orientation`
`tests/fortran/optional_arguments/policy/test_optional_policy.py::test_optional_scalar_policy_completes_nullable_value_presence_before_planning` | `tests/fortran/arrays/end_to_end/test_array_contract_validation.py::test_remaining_array_contracts_are_validated_before_fortran_calls[source]`
`tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py::test_optional_arguments_drive_fortran_present_behavior[source]` | `tests/fortran/optional_arguments/policy/test_optional_policy.py::test_optional_passed_procedure_is_blocked_before_codegen` (`policy`) | canonical | | [`.pyi` Calls And Results: Translate Status Results Into Exceptions](../../docs/user/reference/pyi-contracts/calls-and-results.md#translate-status-results-into-exceptions) | Supported | named hidden scalar integer status; optional hidden string message; configurable success value; consumed projected outputs | `tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_status_projection_accepts_an_optional_missing_message_target`
`tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_runtime_plan_edits_dispatch_to_named_lowering_and_validate_roles` | `tests/fortran/error_handling/end_to_end/test_status_projection.py::test_status_projection_consumes_outputs_raises_message_and_recovers` | `tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_runtime_status_policy_rejects_invalid_output_contracts[@raises(status="status", message="message")\ndef solve() -> tuple[Returns["status", Int32], Returns["message", Int32]]: ...-must be a scalar string hidden output]` (`policy`) | canonical | | [`.pyi` Calls And Results: Release The GIL For A Native Call](../../docs/user/reference/pyi-contracts/calls-and-results.md#release-the-gil-for-a-native-call) | Supported | ordinary held call; explicit released call; status conversion after reacquisition; callback trampoline reacquisition | `tests/fortran/error_handling/semantics/test_status_contract_semantics.py::test_runtime_policy_decorators_round_trip_through_pyi`
`tests/fortran/error_handling/codegen/test_status_error_lowering.py::test_direct_binding_lowering_places_only_opted_in_native_call_outside_the_gil` | `tests/fortran/callbacks/end_to_end/test_supported_callback_shapes.py::test_immediate_callbacks_cover_all_supported_argument_shapes[source]` | — | canonical | -| [Feature Matrix: Caller-Ordered Multi-Source Builds, Makefiles, Verbose Mode, And Output Placement](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | caller order; direct and Makefile builds; replayable verbose commands; ABI artifact and stable alias placement | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_verbose_mode_prints_full_direct_build_commands` | `tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py::test_makefile_mode_reproduces_multi_source_build`
`tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_dir_separates_abi_artifact_from_cli_alias` | — | canonical | -| [Feature Matrix: Fortran Source Wrapper Builds](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | ordered Fortran source inputs; generated contracts; structured native plan; ABI-compatible import | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_source_build_result_records_structured_native_plan`
`tests/fortran/building_shared_library/pipeline/test_source_generated_contracts.py::test_source_build_generated_pyi_contract_matches_fixture[fdefault_output]` | `tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py::test_debug_and_optimized_wrapper_builds_preserve_runtime_abi` | — | canonical | -| [Feature Matrix: Semantic `.pyi` Wrapper Builds From Explicit Native Artifacts](../../docs/user/language-support/feature-matrix.md#supported-inspection-features) | Partially supported | exactly one entry contract; explicit native input; source-free object build; ordered link items; current runtime subset | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_pyi_python_api_accepts_exactly_one_entry_contract`
`tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_generated_pyi_fixture_builds_from_native_object_without_source_reparse` | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_scale_runtime_contract[generated-pyi]` | `tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py::test_pyi_python_api_rejects_a_missing_native_artifact` (`pipeline`) | canonical | -| [Feature Matrix: Advanced Multi-Source Dependency Discovery And External-Library Integration](../../docs/user/language-support/feature-matrix.md#unsupported-or-blocked-forms) | Blocked | source dependency graphs, prebuilt module paths, and external-library discovery are caller/build-system responsibilities; explicit paths remain supported | `tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py::test_source_build_reuses_native_plan_for_additional_compile_and_link_inputs` | `tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_imported_contracts_resolve_from_one_archive_or_shared_library[archive]` | `tests/fortran/building_shared_library/end_to_end/test_native_bundles.py::test_missing_module_directory_reports_compile_error` (`compiling`) | canonical | +| [Feature Matrix: Caller-Ordered Multi-Source Builds, Makefiles, Verbose Mode, And Output Placement](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | caller order; direct and Makefile builds; replayable verbose commands; ABI artifact and stable alias placement | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_verbose_mode_prints_full_direct_build_commands` | `tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py::test_makefile_mode_reproduces_multi_source_build`
`tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_fortran_wrapper_out_dir_separates_abi_artifact_from_cli_alias` | — | canonical | +| [Feature Matrix: Fortran Source Wrapper Builds](../../docs/user/language-support/feature-matrix.md#supported-runtime-features) | Supported | ordered Fortran source inputs; generated contracts; structured native plan; ABI-compatible import | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_source_build_result_records_structured_native_plan`
`tests/fortran/infrastructure/building/pipeline/test_source_generated_contracts.py::test_source_build_generated_pyi_contract_matches_fixture[fdefault_output]` | `tests/fortran/infrastructure/building/end_to_end/test_runtime_compatibility.py::test_debug_and_optimized_wrapper_builds_preserve_runtime_abi` | — | canonical | +| [Feature Matrix: Semantic `.pyi` Wrapper Builds From Explicit Native Artifacts](../../docs/user/language-support/feature-matrix.md#supported-inspection-features) | Partially supported | exactly one entry contract; explicit native input; source-free object build; ordered link items; current runtime subset | `tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_pyi_python_api_accepts_exactly_one_entry_contract`
`tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_generated_pyi_fixture_builds_from_native_object_without_source_reparse` | `tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_scale_runtime_contract[generated-pyi]` | `tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py::test_pyi_python_api_rejects_a_missing_native_artifact` (`pipeline`) | canonical | +| [Feature Matrix: Advanced Multi-Source Dependency Discovery And External-Library Integration](../../docs/user/language-support/feature-matrix.md#unsupported-or-blocked-forms) | Blocked | source dependency graphs, prebuilt module paths, and external-library discovery are caller/build-system responsibilities; explicit paths remain supported | `tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py::test_source_build_reuses_native_plan_for_additional_compile_and_link_inputs` | `tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py::test_imported_contracts_resolve_from_one_archive_or_shared_library[archive]` | `tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py::test_missing_module_directory_reports_compile_error` (`compiling`) | canonical | diff --git a/tests/fortran/README.md b/tests/fortran/README.md index 4e775cebe..a82dea9f7 100644 --- a/tests/fortran/README.md +++ b/tests/fortran/README.md @@ -4,18 +4,24 @@ Fortran, including semantic `.pyi` wrapper builds and the generated Fortran/C/CPython implementation of that contract. -The final organization is feature first and stage second: +Language-feature evidence is feature first and stage second: ```text -tests/fortran/// +tests/fortran/// ``` -Documented features are direct children of `tests/fortran/`; the -`infrastructure/` directory remains the single container for internal -cross-feature frameworks. Only create a feature or stage directory when it -owns a real test or fixture. +Cross-feature mechanisms use explicit infrastructure owners: -## Documentation feature map +```text +tests/fortran/infrastructure// +``` + +A documentation page does not by itself make a mechanism a language feature. +Parsing, preprocessing, CLI, semantic representation and `.pyi` conversion, +building, and shared policy are infrastructure. Only create a feature, stage, +or infrastructure owner when it owns a real test or fixture. + +## Fortran language-feature map | Documentation | Final feature directory | Focused pytest command | | --- | --- | --- | @@ -35,15 +41,6 @@ owns a real test or fixture. | [Enumerations](../../docs/user/guide/enumerations.md) | `enumerations/` | `python3 -m pytest -q tests/fortran/enumerations` | | [Raw Addresses](../../docs/user/guide/raw-addresses.md) | `raw_addresses/` | `python3 -m pytest -q tests/fortran/raw_addresses` | | [Error Handling](../../docs/user/guide/error-handling.md) | `error_handling/` | `python3 -m pytest -q tests/fortran/error_handling` | -| [Building the Shared Library](../../docs/user/guide/building-shared-library.md) | `building_shared_library/` | `python3 -m pytest -q tests/fortran/building_shared_library` | -| [Inspect a Fortran API](../../docs/user/examples/recipes/inspect-fortran-api.md) | `source_parsing/` | `python3 -m pytest -q tests/fortran/source_parsing` | -| [Compiler Preprocessing](../../docs/user/examples/recipes/compiler-preprocessing.md) | `source_preprocessing/` | `python3 -m pytest -q tests/fortran/source_preprocessing` | -| [CLI Commands](../../docs/user/reference/cli-commands.md) | `command_line_interface/` | `python3 -m pytest -q tests/fortran/command_line_interface` | -| [Semantic IR](../../docs/user/reference/semantic-ir.md) | `semantic_ir/` | `python3 -m pytest -q tests/fortran/semantic_ir` | -| [Semantic `.pyi` Format](../../docs/user/reference/semantic-pyi-format.md) | `semantic_pyi_format/` | `python3 -m pytest -q tests/fortran/semantic_pyi_format` | -| [Exports and Modules](../../docs/user/reference/pyi-contracts/exports-and-modules.md) | `pyi_contracts/exports_and_modules/` | `python3 -m pytest -q tests/fortran/pyi_contracts/exports_and_modules` | -| [Functions and Classes](../../docs/user/reference/pyi-contracts/functions-and-classes.md) | `pyi_contracts/functions_and_classes/` | `python3 -m pytest -q tests/fortran/pyi_contracts/functions_and_classes` | -| [Calls and Results](../../docs/user/reference/pyi-contracts/calls-and-results.md) | `pyi_contracts/calls_and_results/` | `python3 -m pytest -q tests/fortran/pyi_contracts/calls_and_results` | Each feature uses only the stages it needs: `parsing`, `probes`, `preprocessing`, `semantics`, `policy`, `codegen`, `compiling`, @@ -54,26 +51,43 @@ Array declaration-expression coverage is intentionally split by evidence: `arrays/policy/` proves completed dependency roles and named blockers, and `arrays/end_to_end/` compiles supported dimensions and logical array kinds. Cross-module editable-contract reconciliation remains under -the semantic `.pyi` format stage, not under a code-generation test. +`infrastructure/semantic_pyi/`, not under a language-feature code-generation +test. + +## Cross-feature infrastructure map + +| Documentation or mechanism | Infrastructure owner | Focused pytest command | +| --- | --- | --- | +| [Inspect a Fortran API](../../docs/user/examples/recipes/inspect-fortran-api.md) | `infrastructure/parsing/` | `python3 -m pytest -q tests/fortran/infrastructure/parsing` | +| [Compiler Preprocessing](../../docs/user/examples/recipes/compiler-preprocessing.md) | `infrastructure/preprocessing/` | `python3 -m pytest -q tests/fortran/infrastructure/preprocessing` | +| [CLI Commands](../../docs/user/reference/cli-commands.md) | `infrastructure/cli/` | `python3 -m pytest -q tests/fortran/infrastructure/cli` | +| [Semantic IR](../../docs/user/reference/semantic-ir.md) | `infrastructure/semantic_ir/` | `python3 -m pytest -q tests/fortran/infrastructure/semantic_ir` | +| [Semantic `.pyi` Format](../../docs/user/reference/semantic-pyi-format.md) and [contract guides](../../docs/user/reference/pyi-contracts/index.md) | `infrastructure/semantic_pyi/` | `python3 -m pytest -q tests/fortran/infrastructure/semantic_pyi` | +| [Building the Shared Library](../../docs/user/guide/building-shared-library.md) | `infrastructure/building/` | `python3 -m pytest -q tests/fortran/infrastructure/building` | +| Completed ownership and wrapper-policy decisions | `infrastructure/policy/` | `python3 -m pytest -q tests/fortran/infrastructure/policy` | ## Infrastructure owners -Infrastructure contains only internal cross-feature frameworks with no honest -public-capability or documentation-feature owner. Tests of public parsing, -preprocessing, command-line, semantic-IR, contract-printing, and build behavior -belong to their named feature even when they span several lower-level -mechanisms. Infrastructure tests normally start from completed internal models -or synthetic implementation nodes; the starting representation is supporting -evidence, not the ownership rule. +Infrastructure contains every cross-feature mechanism, whether internal-only or +user-invocable. A language feature stays feature-owned when it crosses parsing, +policy, planning, and lowering. Infrastructure tests normally start from +completed internal models or synthetic implementation nodes; the starting +representation is supporting evidence, not the ownership rule. | Final directory | Owner | | --- | --- | | `infrastructure/runtime/` | Native runtime-support package contracts that have no public feature owner | -| `infrastructure/semantics/` | Internal semantic ownership, policy completion, and completed wrapper-policy mechanics | -| `infrastructure/codegen/` | Internal plan, planner, generator, binding, bridge, printer, docstring, advisory review, and visitor mechanics | +| `infrastructure/parsing/` | Shared parser, source fixture, and parser-model behavior | +| `infrastructure/preprocessing/` | Shared source preparation, compiler invocation, and source mapping behavior | +| `infrastructure/cli/` | Shared command-line parsing and output behavior | +| `infrastructure/semantic_ir/` | Source and parser-model conversion into semantic IR | +| `infrastructure/semantic_pyi/` | Semantic `.pyi` parsing, conversion, contracts, and loading | +| `infrastructure/building/` | Shared native build modes, compiler integration, and runtime ABI behavior | +| `infrastructure/policy/` | Internal ownership, policy completion, and completed wrapper-policy mechanics | +| `infrastructure/codegen/` | Internal plan, planner, generator, binding, bridge, docstring, advisory review, and visitor mechanics | | `infrastructure/naming/` | Internal generated-name and public-name policy owned by `prik/naming/` | | `infrastructure/pipeline/` | Generated-wrapper orchestration and transport owned by `prik/pipeline/` | -| `infrastructure/types/` | Internal NumPy type mapping and target mapping-report mechanics | +| `infrastructure/printers/` | Internal C and Fortran source serialization owned by `prik/printers/` | | `infrastructure/utilities/` | Internal string and class-visitor helpers owned by `prik/utilities/` | Each infrastructure test module has an explicit production owner. New internal @@ -85,8 +99,8 @@ inheritance choices, field inventories, and incidental call structure remain review recommendations. Minimized real-source parser regressions live in -`source_parsing/parsing/test_real_world_interaction_regressions.py`. A -third-party project is a temporary discovery input, not a permanent fixture: +`infrastructure/parsing/test_real_world_interaction_regressions.py`. +A third-party project is a temporary discovery input, not a permanent fixture: extract its named parser facts, prove that the focused suite covers its unique lines and branches, then remove the snapshot. Parser regressions are never end-to-end or smoke evidence. @@ -96,7 +110,7 @@ end-to-end or smoke evidence. Feature-specific fixtures stay beneath their feature. End-to-end projects use: ```text -/end_to_end/fixtures//native/ +/end_to_end/fixtures//native/ ``` Generated build products always use pytest temporary directories. `_support/` @@ -110,7 +124,7 @@ artifact-consumer, and support-consumer inventories live under ## Markers -- Every pytest node below a feature `end_to_end/` carries +- Every pytest node below a Fortran `end_to_end/` directory carries `fortran_end_to_end`, and no other node does. - Only the complete `examples/blas/` and `examples/lapack/` correctness projects and BLAS/LAPACK native-source integration nodes additionally carry diff --git a/tests/fortran/_support/fixture_outputs.py b/tests/fortran/_support/fixture_outputs.py index ed8e53d06..e2d2d45b8 100644 --- a/tests/fortran/_support/fixture_outputs.py +++ b/tests/fortran/_support/fixture_outputs.py @@ -4,11 +4,14 @@ from prik.parsers.fortran import parse_fortran_file from prik.semantics.fortran2ir import fortran_module_to_semantic_module - -FORTRAN_ROOT = Path(__file__).resolve().parents[1] -PARSER_FIXTURE_ROOT = FORTRAN_ROOT / "source_parsing" / "parsing" / "fixtures" -GENERAL_FORTRAN_DIR = PARSER_FIXTURE_ROOT / "general" -SEMANTICS_FIXTURE_DIR = FORTRAN_ROOT / "semantic_ir" / "semantics" / "fixtures" / "general" / "expected" +from tests.fortran._support.paths import ( + FORTRAN_ROOT, + GENERAL_FORTRAN_DIR, +) + +SEMANTICS_FIXTURE_DIR = ( + FORTRAN_ROOT / "infrastructure" / "semantic_ir" / "semantics" / "fixtures" / "general" / "expected" +) FORTRAN_SUFFIXES = {".f", ".f90", ".f95", ".f03", ".f08", ".for", ".f77", ".ftn"} diff --git a/tests/fortran/_support/paths.py b/tests/fortran/_support/paths.py new file mode 100644 index 000000000..7a3fec891 --- /dev/null +++ b/tests/fortran/_support/paths.py @@ -0,0 +1,13 @@ +"""Directory anchors for tests that read a file owned by another directory. + +Computing `Path(__file__).parents[N]` couples a test to its own depth in the +tree, so moving it silently resolves the path to the wrong directory instead of +failing. Import the anchor that names what is wanted. +""" + +from pathlib import Path + +FORTRAN_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = FORTRAN_ROOT.parents[1] +PARSER_FIXTURE_ROOT = FORTRAN_ROOT / "infrastructure" / "parsing" / "fixtures" +GENERAL_FORTRAN_DIR = PARSER_FIXTURE_ROOT / "general" diff --git a/tests/fortran/_support/printer_models.py b/tests/fortran/_support/printer_models.py index 5d8157068..e3fe498f4 100644 --- a/tests/fortran/_support/printer_models.py +++ b/tests/fortran/_support/printer_models.py @@ -1,6 +1,3 @@ -from pathlib import Path - - from prik.contracts import CONTRACT_SYMBOLS from prik.parsers.fortran import parse_fortran_file as parse_fortran_source @@ -23,10 +20,9 @@ ) from prik.policy.completion import complete_semantic_policies +from tests.fortran._support.paths import FORTRAN_ROOT -OPERATOR_F90_SOURCE = ( - Path(__file__).parents[1] / "generic_interfaces" / "end_to_end" / "fixtures" / "foperators_f90.f90" -) +OPERATOR_F90_SOURCE = FORTRAN_ROOT / "generic_interfaces" / "end_to_end" / "fixtures" / "foperators_f90.f90" CONTRACT_IMPORT = f"from prik.contracts import {', '.join(sorted(CONTRACT_SYMBOLS))}\n" diff --git a/tests/fortran/_support/wrapper_build.py b/tests/fortran/_support/wrapper_build.py index ee19155f2..d69118c02 100644 --- a/tests/fortran/_support/wrapper_build.py +++ b/tests/fortran/_support/wrapper_build.py @@ -17,6 +17,7 @@ import numpy as np import pytest +from tests.fortran._support.paths import REPO_ROOT from tests.fortran._support.pyi_fixtures import assert_generated_pyi_package_matches_fixture from tests.fortran._support.fmath_cases import fmath_cases from prik import build_pyi_extension @@ -38,7 +39,6 @@ from prik.pipeline.wrapper import WrapperGenerator from prik.planning import WrapperPlanner -REPO_ROOT = Path(__file__).resolve().parents[3] WRAPPER_TEST_ROOT = Path(__file__).resolve().parent WRAPPER_SOURCE_PATHS = { "c_order_flat_buffer.f90": REPO_ROOT @@ -55,7 +55,7 @@ "fmath_arrays_f90.f90": REPO_ROOT / "tests/fortran/arrays/end_to_end/fixtures/baseline/native/fmath_arrays_f90.f90", "fmath_f90.f90": REPO_ROOT / "tests/fortran/data_types/end_to_end/fixtures/baseline/native/fmath_f90.f90", "fnaming_f90.f90": REPO_ROOT - / "tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/visibility/native/fnaming_f90.f90", + / "tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/native/fnaming_f90.f90", "fopenmp_runtime_f90.f90": REPO_ROOT / "tests/fortran/error_handling/end_to_end/fixtures/runtime/native/fopenmp_runtime_f90.f90", "free_external.f90": REPO_ROOT / "tests/fortran/functions/end_to_end/fixtures/external/native/free_external.f90", @@ -307,12 +307,18 @@ def _build_source_and_import( source_template: Path, workdir: Path, expected_generated_sources: set[str], + **build_options, ): - """Build one source entry through the canonical production generator.""" + """Build one source entry through the canonical production generator. + + ``build_options`` forwards public build arguments so a test can exercise an + optional wrapper behavior without duplicating the build and import steps. + """ result = build_fortran_extension( source_template, output_dir=workdir, preprocessing=PreprocessingConfig(mode="compiler", compiler=_compiler()), + **build_options, ) assert result.shared_library.exists() assert {path.name for path in result.generated_sources} == expected_generated_sources @@ -511,15 +517,19 @@ def _assert_array_rejects_strided_views(module, function_name): def _assert_legacy_string_examples(module): - assert module.char_code_default("A") == ord("A") - assert module.char_code_star1(np.str_("B")) == ord("B") - assert module.string_len_star8("short ") == 5 + # Fixed-form sources predate the `intent` attribute, so every character + # dummy here reaches the conservative `intent(inout)` default and its + # unchanged value follows the result. `--assume-intent-in-scalars` is the + # documented way to drop it; see the assumed scalar-intent tests. + assert module.char_code_default("A") == (ord("A"), "A") + assert module.char_code_star1(np.str_("B")) == (ord("B"), "B") + assert module.string_len_star8("short ") == (5, "short ") with pytest.raises(TypeError, match="exactly 8 bytes"): module.string_len_star8("short") with pytest.raises(TypeError, match="exactly 8 bytes"): module.string_len_star8("too-long-value") - assert module.string_len_assumed("variable length") == 15 - assert module.string_len_entity("python") == 6 + assert module.string_len_assumed("variable length") == (15, "variable length") + assert module.string_len_entity("python") == (6, "python") assert module.char_result_default() == "L" assert module.string_result_star8() == "LEGACY!!" assert module.string_result_padded() == "PAD " diff --git a/tests/fortran/arrays/codegen/test_array_buffer_lowering.py b/tests/fortran/arrays/codegen/test_array_buffer_lowering.py index 03ed1f73e..830538f0d 100644 --- a/tests/fortran/arrays/codegen/test_array_buffer_lowering.py +++ b/tests/fortran/arrays/codegen/test_array_buffer_lowering.py @@ -82,8 +82,9 @@ def test_required_array_buffer_dispatches_through_named_binding_and_bridge_metho assert "bound_values = bound_values_actual.data;" in c_source assert "bound_values_extent_0 = bound_values_actual.extents[0];" in c_source assert "if (PyArray_Check(bound_values_obj)) {" in c_source + assert c_source.count("PyArray_Check(bound_values_obj)") == 1 assert ( - "prik_array_validate(bound_values_obj, NPY_FLOAT64, 1, 1, " + "prik_array_validate_ndarray((PyArrayObject *)bound_values_obj, NPY_FLOAT64, 1, 1, " 'PRIK_ARRAY_LAYOUT_ANY_CONTIGUOUS, 1, 1, "numpy.float64", "values")' ) in c_source assert "bound_values = PyArray_DATA((PyArrayObject *)bound_values_obj);" in c_source diff --git a/tests/fortran/arrays/codegen/test_specialized_array_roles.py b/tests/fortran/arrays/codegen/test_specialized_array_roles.py index 6c61a7a9c..2fcb78f90 100644 --- a/tests/fortran/arrays/codegen/test_specialized_array_roles.py +++ b/tests/fortran/arrays/codegen/test_specialized_array_roles.py @@ -66,6 +66,7 @@ def test_optional_assumed_rank_and_character_lowering_follow_named_plan_fields() assert "PyObject * bound_values_obj = Py_None;" in c_source assert "if (bound_values_obj != Py_None)" in c_source + assert "prik_array_validate(bound_values_obj, NPY_FLOAT64, 1, 15, PRIK_ARRAY_LAYOUT_F_CONTIGUOUS" in c_source assert "NPY_FLOAT64, 1, 15, PRIK_ARRAY_LAYOUT_F_CONTIGUOUS" in c_source assert "bound_values_rank = (int64_t)PyArray_NDIM" in c_source assert "NPY_STRING, 1, 1, PRIK_ARRAY_LAYOUT_ANY_CONTIGUOUS" in c_source diff --git a/tests/fortran/conftest.py b/tests/fortran/conftest.py index 8d9dc9960..622e0f7b2 100644 --- a/tests/fortran/conftest.py +++ b/tests/fortran/conftest.py @@ -10,7 +10,8 @@ import pytest -REPO_ROOT = Path(__file__).resolve().parents[2] +from tests.fortran._support.paths import REPO_ROOT + COMPILER_ENV = "PRIK_TEST_FORTRAN_COMPILER" COMPILER_OPTION = "--prik-fortran-compiler" @@ -53,7 +54,7 @@ class ToolchainSmokeCase: "test_fortran_generic_interfaces_dispatch_in_generated_c_extension[source]" ): ToolchainSmokeCase("generic_overload_dispatch", "compiled_generic_module"), ( - "tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py::" + "tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py::" "test_generated_contract_rebuilds_without_native_source_fallback" ): ToolchainSmokeCase("source_generated_pyi_rebuild", "compiled_contract_rebuild"), } @@ -141,14 +142,9 @@ def _relative_test_path(item: pytest.Item) -> Path: return Path(str(item.path)).resolve().relative_to(REPO_ROOT) -def _is_fortran_feature_end_to_end(item: pytest.Item) -> bool: +def _is_fortran_end_to_end(item: pytest.Item) -> bool: parts = _relative_test_path(item).parts - return ( - len(parts) >= 5 - and parts[:2] == ("tests", "fortran") - and parts[2] not in {"_support", "infrastructure"} - and "end_to_end" in parts[3:-1] - ) + return len(parts) >= 5 and parts[:2] == ("tests", "fortran") and "end_to_end" in parts[3:-1] def _is_platform_mark(name: str) -> bool: @@ -159,8 +155,8 @@ def _validate_smoke_item(item: pytest.Item, errors: list[str]) -> None: marker = item.get_closest_marker("toolchain_smoke") if marker is None: return - if not _is_fortran_feature_end_to_end(item): - errors.append(f"toolchain_smoke is outside a feature end_to_end directory: {item.nodeid}") + if not _is_fortran_end_to_end(item): + errors.append(f"toolchain_smoke is outside a Fortran end_to_end directory: {item.nodeid}") if item.get_closest_marker("fortran_end_to_end") is None: errors.append(f"toolchain_smoke lacks fortran_end_to_end: {item.nodeid}") if marker.args or set(marker.kwargs) != {"mechanism", "build_fixture"}: @@ -197,7 +193,7 @@ def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item errors = [] for item in items: - is_end_to_end = _is_fortran_feature_end_to_end(item) + is_end_to_end = _is_fortran_end_to_end(item) has_end_to_end_mark = item.get_closest_marker("fortran_end_to_end") is not None if is_end_to_end != has_end_to_end_mark: errors.append( diff --git a/tests/fortran/data_types/end_to_end/test_verified_baseline.py b/tests/fortran/data_types/end_to_end/test_verified_baseline.py index f9e098165..d3ad50c12 100644 --- a/tests/fortran/data_types/end_to_end/test_verified_baseline.py +++ b/tests/fortran/data_types/end_to_end/test_verified_baseline.py @@ -19,9 +19,10 @@ ) from prik import build_pyi_extension from prik.runtime.handles import _NativeArrayHandoff, AllocatableArray, PointerArray +from tests.fortran._support.paths import FORTRAN_ROOT DATA_TYPE_CONTRACTS = Path(__file__).parent / "fixtures" / "baseline" / "contracts" -ARRAY_CONTRACTS = Path(__file__).parents[2] / "arrays" / "end_to_end" / "fixtures" / "baseline" / "contracts" +ARRAY_CONTRACTS = FORTRAN_ROOT / "arrays" / "end_to_end" / "fixtures" / "baseline" / "contracts" SCALAR_FIXED_SOURCE = wrapper_source("fmath.f") ARRAY_FIXED_SOURCE = wrapper_source("fmath_arrays.f") SCALAR_F90_SOURCE = wrapper_source("fmath_f90.f90") diff --git a/tests/fortran/data_types/pipeline/test_scalar_generated_pyi_contracts.py b/tests/fortran/data_types/pipeline/test_scalar_generated_pyi_contracts.py index 9d23b1caa..75b5d9f5b 100644 --- a/tests/fortran/data_types/pipeline/test_scalar_generated_pyi_contracts.py +++ b/tests/fortran/data_types/pipeline/test_scalar_generated_pyi_contracts.py @@ -12,9 +12,10 @@ contract_case_id, source_contract_case, ) +from tests.fortran._support.paths import FORTRAN_ROOT DATA_TYPE_CONTRACTS = Path(__file__).parents[1] / "end_to_end" / "fixtures" / "baseline" / "contracts" -ARRAY_CONTRACTS = Path(__file__).parents[2] / "arrays" / "end_to_end" / "fixtures" / "baseline" / "contracts" +ARRAY_CONTRACTS = FORTRAN_ROOT / "arrays" / "end_to_end" / "fixtures" / "baseline" / "contracts" CASES = ( source_contract_case(DATA_TYPE_CONTRACTS, "fbind_value_f90.f90"), source_contract_case(DATA_TYPE_CONTRACTS, "fmath.f"), diff --git a/tests/fortran/data_types/pipeline/test_type_mapping_report.py b/tests/fortran/data_types/pipeline/test_type_mapping_report.py index cc3a82bf0..18384cfda 100644 --- a/tests/fortran/data_types/pipeline/test_type_mapping_report.py +++ b/tests/fortran/data_types/pipeline/test_type_mapping_report.py @@ -1,5 +1,6 @@ """Target-specific datatype mapping report tests.""" +import json import shutil import pytest @@ -7,6 +8,15 @@ import prik.pipeline.type_mapping_report as type_mapping_report +def _mapping_markdown(language, **options): + builder = ( + type_mapping_report.c_type_mapping_report + if language == "c" + else type_mapping_report.fortran_type_mapping_report + ) + return type_mapping_report.type_mapping_markdown(builder(**options)) + + @pytest.mark.parametrize( ("language", "compiler", "native_header", "representative"), [ @@ -28,11 +38,7 @@ def test_type_mapping_markdown_covers_target_native_semantic_and_numpy_types( if shutil.which(compiler) is None: pytest.skip(f"{compiler} is required for the target-specific mapping report") - report = ( - type_mapping_report.c_type_mapping_markdown(compiler=compiler) - if language == "c" - else type_mapping_report.fortran_type_mapping_markdown(compiler=compiler) - ) + report = _mapping_markdown(language, compiler=compiler) assert report.startswith(f"Target profile: `{type_mapping_report.target_profile()}`") assert native_header in report @@ -40,17 +46,56 @@ def test_type_mapping_markdown_covers_target_native_semantic_and_numpy_types( assert "Semantic dtype | NumPy dtype" in report +@pytest.mark.parametrize(("language", "compiler"), [("c", "cc"), ("fortran", "gfortran")]) +def test_type_mapping_markdown_renders_only_from_the_serialized_report(language, compiler): + """Markdown must be a pure conversion of the JSON record, not a second measurement.""" + if shutil.which(compiler) is None: + pytest.skip(f"{compiler} is required for the target-specific mapping report") + + builder = ( + type_mapping_report.c_type_mapping_report + if language == "c" + else type_mapping_report.fortran_type_mapping_report + ) + report = builder(compiler=compiler) + round_tripped = json.loads(json.dumps(report)) + + assert type_mapping_report.type_mapping_markdown(round_tripped) == type_mapping_report.type_mapping_markdown(report) + + +@pytest.mark.parametrize(("language", "compiler"), [("c", "cc"), ("fortran", "gfortran")]) +def test_type_mapping_report_records_structured_measurements(language, compiler): + """JSON consumers read measured numbers instead of parsing the display text.""" + if shutil.which(compiler) is None: + pytest.skip(f"{compiler} is required for the target-specific mapping report") + + builder = ( + type_mapping_report.c_type_mapping_report + if language == "c" + else type_mapping_report.fortran_type_mapping_report + ) + report = builder(compiler=compiler) + + assert report["report"] == "type_mapping" + assert report["language"] == language + assert report["recipe"]["compiler"] == compiler + entry = next(item for item in report["types"] if item["native"] in {"int", "integer"}) + assert entry["target_fact"]["bits"] == 32 + assert str(entry["target_fact"]["bits"]) in entry["native_fact"] + + def test_type_mapping_report_main_selects_language(monkeypatch, capsys): monkeypatch.setattr( type_mapping_report, - "c_type_mapping_markdown", + "c_type_mapping_report", lambda *, compiler, compiler_args, **options: f"C:{compiler}:{','.join(compiler_args)}:{options['refresh']}", ) monkeypatch.setattr( type_mapping_report, - "fortran_type_mapping_markdown", + "fortran_type_mapping_report", lambda *, compiler, compiler_args, **options: f"F:{compiler}:{','.join(compiler_args)}:{options['refresh']}", ) + monkeypatch.setattr(type_mapping_report, "type_mapping_markdown", lambda report: report) assert type_mapping_report.main(["--language", "c", "--compiler", "clang", "--compiler-arg=-m32", "--refresh"]) == 0 assert capsys.readouterr().out == "C:clang:-m32:True\n" @@ -63,9 +108,7 @@ def test_fortran_type_mapping_uses_compiler_dependent_defaults(): if shutil.which("gfortran") is None: pytest.skip("gfortran is required for the target-specific mapping report") - report = type_mapping_report.fortran_type_mapping_markdown( - compiler_args=["-fdefault-integer-8", "-fdefault-real-8"] - ) + report = _mapping_markdown("fortran", compiler_args=["-fdefault-integer-8", "-fdefault-real-8"]) assert "| `integer` | 64-bit storage | `Int64` | `numpy.int64` |" in report assert "| `real` | 64-bit storage | `Float64` | `numpy.float64` |" in report @@ -79,7 +122,7 @@ def test_fortran_type_mapping_includes_legacy_and_modern_spellings(): if shutil.which("gfortran") is None: pytest.skip("gfortran is required for the target-specific mapping report") - report = type_mapping_report.fortran_type_mapping_markdown() + report = _mapping_markdown("fortran") assert "| `complex(kind=8)` | 128-bit storage | `Complex128` | `numpy.complex128` |" in report assert "| `complex*8` | 64-bit storage | `Complex64` | `numpy.complex64` |" in report @@ -98,4 +141,27 @@ def test_target_profile_normalizes_common_machine_names(monkeypatch): def test_character_mapping_fact_is_modeled_without_compiler_probe_metadata(): semantic_type = type("SemanticType", (), {"metadata": {}})() - assert type_mapping_report._fortran_fact_text(semantic_type, ("character", "c_char")) == "8-bit storage" + fact = type_mapping_report._fortran_target_fact(semantic_type, ("character", "c_char")) + + assert fact == {"bits": 8} + assert type_mapping_report._fortran_fact_text(fact) == "8-bit storage" + + +def test_expression_probe_markdown_renders_measured_values(): + if shutil.which("gfortran") is None: + pytest.skip("gfortran is required for the Fortran expression probe") + + from prik.preprocessing import PreprocessingConfig + from prik.preprocessing.probes.fortran_types import probe_fortran_type_expressions_cached + + report = probe_fortran_type_expressions_cached( + PreprocessingConfig(mode="compiler", compiler="gfortran"), + ["kind(1.0d0)", "storage_size(0)"], + ) + + markdown = type_mapping_report.expression_probe_markdown(report) + + assert markdown.startswith("Compiler: `gfortran`") + assert "| Fortran expression | Measured value |" in markdown + assert "| `kind(1.0d0)` | 8 |" in markdown + assert "| `storage_size(0)` | 32 |" in markdown diff --git a/tests/fortran/data_types/probes/test_fortran_type_probes.py b/tests/fortran/data_types/probes/test_fortran_type_probes.py index 9920cea01..83e52030c 100644 --- a/tests/fortran/data_types/probes/test_fortran_type_probes.py +++ b/tests/fortran/data_types/probes/test_fortran_type_probes.py @@ -522,6 +522,7 @@ def test_prik_semantics_cli_evaluates_collected_fortran_type_requirements(tmp_pa "prik", "semantics", str(source), + "--json", "--compiler", compiler, ], @@ -559,6 +560,7 @@ def test_prik_semantics_cli_uses_compiler_dependent_default_fortran_kinds(tmp_pa "prik", "semantics", str(source), + "--json", "--compiler", compiler, "--compiler-arg=-fdefault-integer-8", @@ -580,3 +582,56 @@ def test_prik_semantics_cli_uses_compiler_dependent_default_fortran_kinds(tmp_pa assert semantic_types["legacy_value"]["name"] == "Complex128" assert semantic_types["scale"]["metadata"]["fortran_type_fact_source"] == "compiler_probe" assert semantic_types["legacy_value"]["metadata"]["fortran_type_fact_source"] == "legacy_star_storage" + + +def test_probe_skips_expressions_naming_project_symbols(): + """The probe program cannot `use` a module that has not been compiled yet. + + An expression naming a kind parameter declared elsewhere in the project is + left out of the probe rather than compiled into a program that cannot + resolve it. Expressions built only from intrinsic names are still probed. + """ + assert fortran_type_probe.probe_can_resolve_expression("selected_real_kind(15, 307)") + assert fortran_type_probe.probe_can_resolve_expression("storage_size(1_4, kind=int32)") + assert not fortran_type_probe.probe_can_resolve_expression("storage_size(1_ip, kind=ip)") + assert not fortran_type_probe.probe_can_resolve_expression("wp") + + requirements = [ + {"expression": "real64"}, + {"expression": "storage_size(1_ip, kind=ip)"}, + {"expression": "selected_int_kind(9)"}, + ] + assert fortran_type_probe_expressions(requirements) == ["real64", "selected_int_kind(9)"] + + +def test_probe_source_compiles_for_a_module_using_imported_kind_parameters(tmp_path): + """A parameter defined from an imported kind must not break the whole probe.""" + source = tmp_path / "imported_kinds.f90" + source.write_text( + """ +module imported_kinds_kinds + use,intrinsic :: iso_fortran_env + implicit none + private + integer,parameter,public :: ip = int32 +end module imported_kinds_kinds + +module imported_kinds + use imported_kinds_kinds, only: ip + implicit none + integer(ip),parameter :: int_size = storage_size(1_ip, kind=ip) +contains + integer(ip) function bits() + bits = int_size + end function bits +end module imported_kinds +""", + encoding="utf-8", + ) + + project = parse_fortran_project([str(source)]) + expressions = fortran_type_probe_expressions(collect_semantic_compile_time_requirements(project)) + + assert "storage_size(1_ip, kind=ip)" not in expressions + assert "int32" in expressions + build_fortran_type_probe_source(expressions) diff --git a/tests/fortran/derived_types/end_to_end/fixtures/abstract_hierarchy.f90 b/tests/fortran/derived_types/end_to_end/fixtures/abstract_hierarchy.f90 new file mode 100644 index 000000000..a4fb41901 --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/fixtures/abstract_hierarchy.f90 @@ -0,0 +1,93 @@ +module abstract_hierarchy + use, intrinsic :: iso_c_binding + implicit none + private + + public :: shape_base, circle, square, extent, describe + + !> Abstract base: no instance of this type can exist, but it publishes a + !> deferred contract and one implemented binding its extensions inherit. + type, public, abstract :: shape_base + private + integer(4) :: sides = 0 + contains + private + procedure(area_interface), deferred, public :: area + procedure(name_interface), deferred, public :: label + procedure, public, non_overridable :: side_count => shape_side_count + procedure, public, non_overridable :: bump_sides => shape_bump_sides + end type shape_base + + abstract interface + pure real(8) function area_interface(self) + import :: shape_base + class(shape_base), intent(in) :: self + end function area_interface + + pure subroutine name_interface(self, text) + import :: shape_base + class(shape_base), intent(in) :: self + character(len=8), intent(out) :: text + end subroutine name_interface + end interface + + type, extends(shape_base), public :: circle + real(8) :: radius = 1.0d0 + contains + procedure, public :: area => circle_area + procedure, public :: label => circle_label + end type circle + + type, extends(shape_base), public :: square + real(8) :: side = 1.0d0 + contains + procedure, public :: area => square_area + procedure, public :: label => square_label + end type square + + !> An interoperable type keeps its `bind(c)` layout alongside the hierarchy. + type, bind(c), public :: extent + real(c_double) :: width = 0.0_c_double + real(c_double) :: height = 0.0_c_double + end type extent + +contains + + integer(4) function shape_side_count(self) + class(shape_base), intent(in) :: self + shape_side_count = self%sides + end function shape_side_count + + subroutine shape_bump_sides(self) + class(shape_base), intent(inout) :: self + self%sides = self%sides + 1 + end subroutine shape_bump_sides + + pure real(8) function circle_area(self) + class(circle), intent(in) :: self + circle_area = 3.14159265358979d0 * self%radius * self%radius + end function circle_area + + pure subroutine circle_label(self, text) + class(circle), intent(in) :: self + character(len=8), intent(out) :: text + text = "circle " + end subroutine circle_label + + pure real(8) function square_area(self) + class(square), intent(in) :: self + square_area = self%side * self%side + end function square_area + + pure subroutine square_label(self, text) + class(square), intent(in) :: self + character(len=8), intent(out) :: text + text = "square " + end subroutine square_label + + real(c_double) function describe(box) + type(extent), intent(in) :: box + describe = box%width * box%height + end function describe + +end module abstract_hierarchy diff --git a/tests/fortran/derived_types/end_to_end/fixtures/generic_constructor.f90 b/tests/fortran/derived_types/end_to_end/fixtures/generic_constructor.f90 new file mode 100644 index 000000000..adb10afa8 --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/fixtures/generic_constructor.f90 @@ -0,0 +1,41 @@ +module generic_constructor + implicit none + private + + public :: box, plain + + type, public :: box + integer(4) :: count = 0 + real(8) :: value = 0.0d0 + end type box + + !> An interface named for the type is that type's constructor. + interface box + module procedure box_empty, box_from_count, box_from_value + end interface box + + !> A type with no such interface keeps its keyword-field constructor. + type, public :: plain + integer(4) :: tag = 0 + end type plain + +contains + + pure type(box) function box_empty() result(new_box) + new_box%count = 0 + new_box%value = 0.0d0 + end function box_empty + + pure type(box) function box_from_count(count) result(new_box) + integer(4), intent(in) :: count + new_box%count = count + new_box%value = real(count, 8) + end function box_from_count + + pure type(box) function box_from_value(value) result(new_box) + real(8), intent(in) :: value + new_box%count = int(value, 4) + new_box%value = value + end function box_from_value + +end module generic_constructor diff --git a/tests/fortran/derived_types/end_to_end/fixtures/type_accessibility.f90 b/tests/fortran/derived_types/end_to_end/fixtures/type_accessibility.f90 new file mode 100644 index 000000000..a2f0a8e8c --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/fixtures/type_accessibility.f90 @@ -0,0 +1,30 @@ +module type_accessibility + implicit none + private + + public :: gated + + type,public :: gated + private + integer(4) :: hidden = 7 + integer(4),public :: shown = 3 + contains + private + procedure :: internal_step + procedure,public :: step => internal_step + procedure,public :: peek => gated_peek + end type gated + +contains + + subroutine internal_step(self) + class(gated),intent(inout) :: self + self%hidden = self%hidden + 1 + end subroutine internal_step + + integer(4) function gated_peek(self) + class(gated),intent(in) :: self + gated_peek = self%hidden + end function gated_peek + +end module type_accessibility diff --git a/tests/fortran/derived_types/end_to_end/test_abstract_hierarchy.py b/tests/fortran/derived_types/end_to_end/test_abstract_hierarchy.py new file mode 100644 index 000000000..d5f64f681 --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/test_abstract_hierarchy.py @@ -0,0 +1,116 @@ +"""Generated Python surface for an abstract Fortran type hierarchy.""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_and_import + +pytestmark = pytest.mark.fortran_end_to_end + +SOURCE = Path(__file__).parent / "fixtures" / "abstract_hierarchy.f90" +GENERATED = { + "bind_c_abstract_hierarchy_wrapper.f90", + "abstract_hierarchy_wrapper.c", + "abstract_hierarchy_wrapper.h", +} + + +@pytest.fixture(scope="module") +def module(tmp_path_factory): + return _build_source_and_import(SOURCE, tmp_path_factory.mktemp("abstract_hierarchy"), GENERATED) + + +def test_abstract_type_cannot_be_instantiated(module): + """`type, abstract ::` has no instances, so its Python class has no constructor.""" + with pytest.raises(TypeError, match="abstract native type and cannot be instantiated"): + module.shape_base() + + assert "__init__" not in module.shape_base.__dict__ + + +def test_extensions_are_python_subclasses_of_the_abstract_base(module): + """Fortran `extends` becomes real Python inheritance, not copied members.""" + assert issubclass(module.circle, module.shape_base) + assert issubclass(module.square, module.shape_base) + assert module.circle.__mro__[:2] == (module.circle, module.shape_base) + + assert isinstance(module.circle(radius=np.float64(1.0)), module.shape_base) + + +def test_deferred_bindings_dispatch_to_each_concrete_override(module): + """A deferred binding names a contract; the dynamic type selects the body.""" + circle = module.circle(radius=np.float64(2.0)) + square = module.square(side=np.float64(3.0)) + + assert circle.area() == pytest.approx(12.566370614, rel=1e-9) + assert square.area() == pytest.approx(9.0) + assert circle.label() == "circle " + assert square.label() == "square " + + # The base declares the same bindings, and they resolve through the caller's + # concrete type rather than through anything the abstract type implements. + assert module.shape_base.area(circle) == pytest.approx(circle.area()) + assert module.shape_base.area(square) == pytest.approx(square.area()) + + +def test_inherited_bindings_and_components_reach_every_extension(module): + """An implemented binding on the abstract base serves its extensions.""" + circle = module.circle(radius=np.float64(1.0)) + + assert circle.side_count() == np.int32(0) + circle.bump_sides() + circle.bump_sides() + assert circle.side_count() == np.int32(2) + + +def test_private_components_stay_off_the_generated_classes(module): + """The hierarchy publishes only what its `private` statements allow.""" + assert {name for name in dir(module.shape_base) if not name.startswith("_")} == { + "area", + "label", + "side_count", + "bump_sides", + } + assert {name for name in dir(module.circle) if not name.startswith("_")} == { + "area", + "label", + "side_count", + "bump_sides", + "radius", + } + + +def test_interoperable_type_keeps_its_layout_beside_the_hierarchy(module): + """A `bind(c)` type in the same module still wraps through its own accessors.""" + box = module.extent(width=np.float64(3.0), height=np.float64(4.0)) + + assert box.width == np.float64(3.0) + assert module.describe(box) == pytest.approx(12.0) + + box.width = np.float64(5.0) + assert module.describe(box) == pytest.approx(20.0) + + +def test_build_writes_its_semantic_contract_beside_the_extension(tmp_path: Path): + """Every build leaves the contract describing the API it just generated.""" + from prik.pipeline.build import BUILD_CONTRACT_DIRECTORY_NAME, build_fortran_extension + from prik.preprocessing import PreprocessingConfig + from tests.fortran._support.wrapper_build import _compiler + + result = build_fortran_extension( + SOURCE, + output_dir=tmp_path, + preprocessing=PreprocessingConfig(mode="compiler", compiler=_compiler()), + ) + + contracts = result.output_dir / BUILD_CONTRACT_DIRECTORY_NAME + assert (contracts / "abstract_hierarchy.pyi").is_file() + assert (contracts / "__init__.pyi").read_text(encoding="utf-8").strip() == ("from . import abstract_hierarchy") + + text = (contracts / "abstract_hierarchy.pyi").read_text(encoding="utf-8") + assert "@abstract" in text + assert "@abstractmethod" in text diff --git a/tests/fortran/derived_types/end_to_end/test_derived_runtime_mechanisms.py b/tests/fortran/derived_types/end_to_end/test_derived_runtime_mechanisms.py index 6f71a5715..439d93946 100644 --- a/tests/fortran/derived_types/end_to_end/test_derived_runtime_mechanisms.py +++ b/tests/fortran/derived_types/end_to_end/test_derived_runtime_mechanisms.py @@ -16,6 +16,7 @@ ) from prik import build_pyi_extension from prik.runtime.handles import AllocatableArray +from tests.fortran._support.paths import FORTRAN_ROOT FIXTURES = Path(__file__).parent / "fixtures" EDITED_CONTRACTS = FIXTURES / "edited_contracts" @@ -25,7 +26,7 @@ PLAIN_MODULE_CONTRACT = EDITED_CONTRACTS / "module_live_proxy" / "__init__.pyi" ALIASED_MODULE_SOURCE = FIXTURES / "fmodule_derived_alias_f90.f90" ALIASED_MODULE_CONTRACT = EDITED_CONTRACTS / "module_aliased_proxy" / "__init__.pyi" -DERIVED_CONSTANT_SOURCE = Path(__file__).parents[2] / "modules" / "end_to_end" / "fixtures" / "fmodule_vars_f90.f90" +DERIVED_CONSTANT_SOURCE = FORTRAN_ROOT / "modules" / "end_to_end" / "fixtures" / "fmodule_vars_f90.f90" pytestmark = pytest.mark.fortran_end_to_end DERIVED_CONSTANT_CONTRACT = """\ from prik.contracts import Final, Int32 diff --git a/tests/fortran/derived_types/end_to_end/test_generic_constructor.py b/tests/fortran/derived_types/end_to_end/test_generic_constructor.py new file mode 100644 index 000000000..eed4e6bc4 --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/test_generic_constructor.py @@ -0,0 +1,77 @@ +"""Generated Python constructor for each Fortran constructor source.""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_and_import + +pytestmark = pytest.mark.fortran_end_to_end + +SOURCE = Path(__file__).parent / "fixtures" / "generic_constructor.f90" +GENERATED = { + "bind_c_generic_constructor_wrapper.f90", + "generic_constructor_wrapper.c", + "generic_constructor_wrapper.h", +} + + +@pytest.fixture(scope="module") +def module(tmp_path_factory): + return _build_source_and_import(SOURCE, tmp_path_factory.mktemp("generic_constructor"), GENERATED) + + +def test_type_without_a_constructor_interface_keeps_keyword_fields(module): + """No user constructor: the keyword-field `__init__` is unchanged.""" + value = module.plain(tag=np.int32(5)) + + assert value.tag == np.int32(5) + + +def test_constructor_interface_overloads_init_from_its_specifics(module): + """`interface `: each specific becomes an accepted signature.""" + empty = module.box() + from_count = module.box(np.int32(7)) + from_value = module.box(np.float64(2.5)) + + assert (empty.count, empty.value) == (np.int32(0), np.float64(0.0)) + assert (from_count.count, from_count.value) == (np.int32(7), np.float64(7.0)) + assert (from_value.count, from_value.value) == (np.int32(2), np.float64(2.5)) + + +def test_constructor_overload_rejects_an_unmatched_signature(module): + """A call matching no specific is refused rather than guessed at.""" + with pytest.raises(TypeError, match="no matching overload"): + module.box("not a supported signature") + + +def test_constructed_instances_are_independent_wrapper_objects(module): + """Each accepted signature produces its own wrapper-owned instance.""" + first = module.box(np.int32(1)) + second = module.box(np.int32(2)) + + assert first is not second + first.count = np.int32(9) + assert second.count == np.int32(2) + + +def test_constructor_contract_states_no_redundant_link_name(tmp_path: Path): + """A constructor's native generic is named for its type, so `@bind` is omitted. + + `@overload` names the specific this candidate selects; the class name already + states the generic that reaches it, exactly as an unrenamed method omits + `@bind`. + """ + from prik.pipeline.pyi import emit_module_stubs + from prik.parsers.fortran import parse_fortran_file + from prik.semantics.fortran2ir import fortran_file_to_semantic_modules + + modules = fortran_file_to_semantic_modules(parse_fortran_file(str(SOURCE))) + contract = emit_module_stubs(modules)["generic_constructor"] + + assert '@overload("box_from_count")' in contract + assert '@bind("box")' not in contract + assert "@private\n def __init__" not in contract diff --git a/tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py b/tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py index a68c9a4ca..4f670ac19 100644 --- a/tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py +++ b/tests/fortran/derived_types/end_to_end/test_scalar_actual_dummy_matrix.py @@ -13,6 +13,7 @@ import numpy as np import pytest +from tests.fortran._support.paths import REPO_ROOT from tests.fortran._support.wrapper_build import _import_from_build_dir from prik import build_pyi_extension @@ -595,7 +596,7 @@ def test_injected_restoration_failure_poison_isolated_origin_and_continues_clean argument, poisoned_reader, ], - cwd=Path(__file__).parents[4], + cwd=REPO_ROOT, env=environment, check=False, capture_output=True, diff --git a/tests/fortran/derived_types/end_to_end/test_type_accessibility.py b/tests/fortran/derived_types/end_to_end/test_type_accessibility.py new file mode 100644 index 000000000..7cfab3465 --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/test_type_accessibility.py @@ -0,0 +1,39 @@ +"""Generated class surface for Fortran accessibility statements.""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_and_import + +pytestmark = pytest.mark.fortran_end_to_end + +SOURCE = Path(__file__).parent / "fixtures" / "type_accessibility.f90" +GENERATED = { + "bind_c_type_accessibility_wrapper.f90", + "type_accessibility_wrapper.c", + "type_accessibility_wrapper.h", +} + + +def test_accessibility_statements_shape_the_generated_class(tmp_path: Path): + """Only components and bindings the type publishes reach Python. + + A `type, public ::` declaration is exported even though the module defaults + to `private`, while the type's own `private` statements keep its internal + component and binding off the generated surface. + """ + module = _build_source_and_import(SOURCE, tmp_path, GENERATED) + + assert hasattr(module, "gated") + members = {name for name in dir(module.gated) if not name.startswith("_")} + assert members == {"shown", "step", "peek"} + + instance = module.gated(shown=np.int32(5)) + assert instance.shown == np.int32(5) + assert instance.peek() == np.int32(7) + instance.step() + assert instance.peek() == np.int32(8) diff --git a/tests/fortran/derived_types/parsing/test_derived_procedure_syntax.py b/tests/fortran/derived_types/parsing/test_derived_procedure_syntax.py index 4c421aa18..e593fced1 100644 --- a/tests/fortran/derived_types/parsing/test_derived_procedure_syntax.py +++ b/tests/fortran/derived_types/parsing/test_derived_procedure_syntax.py @@ -39,7 +39,21 @@ def test_derived_type_procedure_and_generic_bindings(): end module m """ dt = parse_fortran_file(code).modules[0].derived_types[0] - assert {"name": "init => t_init", "attrs": ["pass(self)"]} in dt.procedure_bindings - assert {"name": "clear", "attrs": ["nopass"]} in dt.procedure_bindings - assert {"name": "assignment(=)", "targets": ["init"], "attrs": []} in dt.generic_bindings - assert {"name": "setup", "targets": ["init", "clear"], "attrs": ["public"]} in dt.generic_bindings + assert { + "name": "init => t_init", + "attrs": ["pass(self)"], + "visibility": "public", + } in dt.procedure_bindings + assert {"name": "clear", "attrs": ["nopass"], "visibility": "public"} in dt.procedure_bindings + assert { + "name": "assignment(=)", + "targets": ["init"], + "attrs": [], + "visibility": "public", + } in dt.generic_bindings + assert { + "name": "setup", + "targets": ["init", "clear"], + "attrs": ["public"], + "visibility": "public", + } in dt.generic_bindings diff --git a/tests/fortran/derived_types/policy/test_derived_accessor_policy.py b/tests/fortran/derived_types/policy/test_derived_accessor_policy.py index e21a68568..5806708e1 100644 --- a/tests/fortran/derived_types/policy/test_derived_accessor_policy.py +++ b/tests/fortran/derived_types/policy/test_derived_accessor_policy.py @@ -36,7 +36,8 @@ from prik.policy.models import ModuleObjectAccessMechanism -def test_abstract_type_and_deferred_binding_fail_in_completed_derived_policy(): +def test_abstract_type_completes_as_a_non_instantiable_derived_policy(): + """An abstract type is supported and records that it has no instances.""" semantic_class = SemanticClass( "shape", metadata={ @@ -48,12 +49,23 @@ def test_abstract_type_and_deferred_binding_fail_in_completed_derived_policy(): complete_semantic_policies(module) + policy = semantic_class.metadata[RESOLVED_DERIVED_TYPE_POLICY_METADATA] + assert policy.supported is True + assert policy.blockers == () + assert policy.abstract is True + assert policy.deferred_bindings == ("area",) + + +def test_deferred_binding_without_an_abstract_type_is_refused(): + """Only an abstract type may declare a binding it does not implement.""" + semantic_class = SemanticClass("shape", metadata={"fortran_deferred_bindings": ["area"]}) + module = SemanticModule("shapes", classes=[semantic_class]) + + complete_semantic_policies(module) + policy = semantic_class.metadata[RESOLVED_DERIVED_TYPE_POLICY_METADATA] assert policy.supported is False - assert policy.blockers == ( - "abstract derived types need a non-instantiable Python class policy", - "deferred type-bound procedure 'area' needs an override and dispatch policy", - ) + assert policy.blockers == ("deferred type-bound procedure 'area' needs a declaring abstract type",) def test_derived_field_setter_policy_uses_value_copy_write_through(): diff --git a/tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py b/tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py index 43768d33f..de7ddd5d1 100644 --- a/tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py +++ b/tests/fortran/derived_types/semantics/test_fortran_derived_semantics.py @@ -293,3 +293,82 @@ def test_class_declarations_preserve_polymorphic_source_fact(): assert module.functions[0].metadata["fortran_passed_object_name"] == "self" assert accept_value.origin.source_type == "class(base)" assert accept_value.metadata["fortran_polymorphic"] is True + + +def test_declared_type_accessibility_wins_over_the_module_default(): + """`type, public ::` states the type's own accessibility. + + A module-level `private` default sets accessibility for symbols that do not + state one; it must not hide a type whose declaration says `public`. + """ + module = fortran_module_to_semantic_module( + parse_fortran_source( + """ +module exports_mod + implicit none + private + type,public :: exported + integer :: n = 0 + end type exported + type :: defaulted + integer :: n = 0 + end type defaulted +end module exports_mod +""" + ) + ) + + visibility = {semantic_class.name: semantic_class.visibility for semantic_class in module.classes} + assert visibility == {"exported": "public", "defaulted": "private"} + + +def test_private_components_carry_their_hidden_accessibility(): + """The type's `private` statement is the default accessibility of its components.""" + module = fortran_module_to_semantic_module( + parse_fortran_source( + """ +module hidden_mod + implicit none + type,public :: partly + private + integer :: hidden = 0 + integer,public :: shown = 0 + end type partly +end module hidden_mod +""" + ) + ) + + partly = module.classes[0] + assert {field.name: field.visibility for field in partly.fields} == { + "hidden": "private", + "shown": "public", + } + + +def test_private_type_bound_procedures_stay_off_the_generated_class_surface(): + """A binding hidden by the `private` statement after `contains` is not a method.""" + module = fortran_module_to_semantic_module( + parse_fortran_source( + """ +module bindings_mod + implicit none + type,public :: gated + integer :: n = 0 + contains + private + procedure :: internal_step + procedure,public :: step => internal_step + end type gated +contains + subroutine internal_step(self) + class(gated),intent(inout) :: self + self%n = self%n + 1 + end subroutine internal_step +end module bindings_mod +""" + ) + ) + + gated = module.classes[0] + assert [method.name for method in gated.methods if method.visibility == "public"] == ["step"] diff --git a/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py b/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py index fa6eeb2e2..e5224bcbd 100644 --- a/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py +++ b/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py @@ -112,6 +112,7 @@ def test_converter_preserves_imported_derived_contexts_through_dispatch_paths(): "result_position": None, "value_kind": "", "value": None, + "native_cast": None, } ] assert semantic_module.origin.source_language == "fortran" diff --git a/tests/fortran/error_handling/codegen/test_status_error_lowering.py b/tests/fortran/error_handling/codegen/test_status_error_lowering.py index 8bc5df7b0..3ac70b890 100644 --- a/tests/fortran/error_handling/codegen/test_status_error_lowering.py +++ b/tests/fortran/error_handling/codegen/test_status_error_lowering.py @@ -96,8 +96,8 @@ def test_direct_binding_lowering_places_only_opted_in_native_call_outside_the_gi assert "Py_END_ALLOW_THREADS" not in held assert solve.index("Py_BEGIN_ALLOW_THREADS") < solve.index("bind_c_solve(&bound_value, &status, &message)") assert solve.index("bind_c_solve(&bound_value, &status, &message)") < solve.index("Py_END_ALLOW_THREADS") - assert solve.index("Py_END_ALLOW_THREADS") < solve.index("PyUnicode_FromString") - assert solve.index("PyUnicode_FromString") < solve.index("status != 0") + assert solve.index("Py_END_ALLOW_THREADS") < solve.index("prik_status_message_text") + assert solve.index("prik_status_message_text") < solve.index("status != 0") assert "PyErr_SetObject(PyExc_RuntimeError, message_obj)" in solve assert "free(message)" in solve diff --git a/tests/fortran/error_handling/end_to_end/fixtures/edited_contract/fruntime_policy_f90.pyi b/tests/fortran/error_handling/end_to_end/fixtures/edited_contract/fruntime_policy_f90.pyi index 73e53c33e..d4da4b871 100644 --- a/tests/fortran/error_handling/end_to_end/fixtures/edited_contract/fruntime_policy_f90.pyi +++ b/tests/fortran/error_handling/end_to_end/fixtures/edited_contract/fruntime_policy_f90.pyi @@ -1,5 +1,5 @@ # Intentional difference: exercise runtime policy decorators from an edited contract. -from prik.contracts import Addr, Arg, Int32, Return, String, native_call, nogil, raises +from prik.contracts import Addr, Arg, Hidden, Int32, String, native_call, nogil, raises @nogil def pause_for_one_second() -> None: ... @@ -8,7 +8,7 @@ def pause_with_gil() -> None: ... @raises(status="status", message="message", success=0) @nogil -@native_call([Addr(Arg(0)), Return('status', 0), Return('message', 1)]) +@native_call([Addr(Arg(0)), Hidden('status', Int32), Hidden('message', String[32])]) def solve( value: Int32 -) -> tuple[Int32, String[32]]: ... +) -> None: ... diff --git a/tests/fortran/error_handling/end_to_end/fixtures/routing/contracts/error_handling_direct_bind_c_f90.pyi b/tests/fortran/error_handling/end_to_end/fixtures/routing/contracts/error_handling_direct_bind_c_f90.pyi index 666e5ca36..3706557a8 100644 --- a/tests/fortran/error_handling/end_to_end/fixtures/routing/contracts/error_handling_direct_bind_c_f90.pyi +++ b/tests/fortran/error_handling/end_to_end/fixtures/routing/contracts/error_handling_direct_bind_c_f90.pyi @@ -1,9 +1,9 @@ -from prik.contracts import Arg, Int32, Return, Returns, Value, native_abi, native_call, nogil, raises +from prik.contracts import Arg, Hidden, Int32, Return, Value, native_abi, native_call, nogil, raises @native_abi("c") @raises(status="status", success=0) @nogil -@native_call([Value(Arg(0)), Return("output", 0), Return("status", 1)]) +@native_call([Value(Arg(0)), Return("output", 0), Hidden("status", Int32)]) def direct_solve( value: Int32 -) -> tuple[Int32, Returns["status", Int32]]: ... +) -> Int32: ... diff --git a/tests/fortran/error_handling/end_to_end/fixtures/routing/contracts/error_handling_mixed_bind_c_f90.pyi b/tests/fortran/error_handling/end_to_end/fixtures/routing/contracts/error_handling_mixed_bind_c_f90.pyi index c7a4e847d..4df810637 100644 --- a/tests/fortran/error_handling/end_to_end/fixtures/routing/contracts/error_handling_mixed_bind_c_f90.pyi +++ b/tests/fortran/error_handling/end_to_end/fixtures/routing/contracts/error_handling_mixed_bind_c_f90.pyi @@ -1,16 +1,16 @@ -from prik.contracts import Addr, Arg, Int32, Return, Returns, Value, native_abi, native_call, nogil, raises +from prik.contracts import Addr, Arg, Hidden, Int32, Return, Value, native_abi, native_call, nogil, raises @native_abi("c") @raises(status="status", success=0) @nogil -@native_call([Value(Arg(0)), Return("output", 0), Return("status", 1)]) +@native_call([Value(Arg(0)), Return("output", 0), Hidden("status", Int32)]) def direct_solve( value: Int32 -) -> tuple[Int32, Returns["status", Int32]]: ... +) -> Int32: ... @raises(status="status", success=0) @nogil -@native_call([Addr(Arg(0)), Return("output", 0), Return("status", 1)]) +@native_call([Addr(Arg(0)), Return("output", 0), Hidden("status", Int32)]) def adapted_solve( value: Int32 -) -> tuple[Int32, Returns["status", Int32]]: ... +) -> Int32: ... diff --git a/tests/fortran/error_handling/end_to_end/test_status_projection.py b/tests/fortran/error_handling/end_to_end/test_status_projection.py index 189b62b98..625da7837 100644 --- a/tests/fortran/error_handling/end_to_end/test_status_projection.py +++ b/tests/fortran/error_handling/end_to_end/test_status_projection.py @@ -62,10 +62,59 @@ def test_status_projection_consumes_outputs_raises_message_and_recovers(tmp_path assert "Py_END_ALLOW_THREADS" not in held solve = binding[binding.index("static PyObject * wrap_solve") : binding.index("PyMODINIT_FUNC")] assert solve.index("Py_END_ALLOW_THREADS") < solve.index("status != 0") - assert solve.index("PyUnicode_FromString") < solve.index("free(message)") < solve.index("status != 0") + assert solve.index("prik_status_message_text") < solve.index("free(message)") < solve.index("status != 0") error_start = solve.index("if (status != 0)") error_path = solve[error_start : solve.index("Py_RETURN_NONE")] assert error_path.index("PyErr_SetObject(PyExc_RuntimeError, message_obj)") < error_path.index( "Py_DECREF(message_obj)" ) assert error_path.index("Py_DECREF(message_obj)") < error_path.index("return NULL") + + +def test_status_projection_reads_a_visible_fortran_message_buffer(tmp_path: Path): + """A caller-owned NumPy string buffer supplies an assumed-width message.""" + source = tmp_path / "visible_status.f90" + source.write_text( + """module visible_status +contains + subroutine check(value, message, status) + integer, intent(in) :: value + character(len=*), intent(inout) :: message + integer, intent(out) :: status + if (value < 0) then + status = -1 + message = "negative input" + else + status = 0 + message = "" + end if + end subroutine +end module +""", + encoding="utf-8", + ) + contract = tmp_path / "visible_status.pyi" + contract.write_text( + """from prik.contracts import Addr, Arg, Hidden, Int32, String, native_call, raises + +@raises(status="status", message="message", success=0) +@native_call([Addr(Arg(0)), Arg(1), Hidden("status", Int32)]) +def check(value: Int32, message: String[...][()]) -> None: ... +""", + encoding="utf-8", + ) + result = build_pyi_extension( + contract, + native_fortran_sources=[source], + output_dir=tmp_path / "visible_build", + output_name="visible_status", + ) + module = result.import_module() + message = np.array(b"", dtype="S32") + + assert module.check(np.int32(1), message) is None + assert message.tobytes() == b" " * 32 + with pytest.raises(RuntimeError, match=r"^negative input$"): + module.check(np.int32(-1), message) + assert message.tobytes() == b"negative input" + b" " * 18 + assert module.check(np.int32(1), message) is None diff --git a/tests/fortran/functions/codegen/test_multiple_function_results.py b/tests/fortran/functions/codegen/test_multiple_function_results.py index 0e9df2526..10c67a2fd 100644 --- a/tests/fortran/functions/codegen/test_multiple_function_results.py +++ b/tests/fortran/functions/codegen/test_multiple_function_results.py @@ -27,6 +27,20 @@ def with_scalar(n: Int32) -> tuple[Int32, Int32]: ... return WrapperPlanner().build(module) +def _four_result_plan(): + module = parse_pyi_text( + """ +from prik.contracts import Addr, Arg, Int32, Return, native_call + +@native_call([Addr(Arg(0)), Return("one", 1), Return("two", 2), Return("three", 3)]) +def with_four_scalars(n: Int32) -> tuple[Int32, Int32, Int32, Int32]: ... +""", + module_name="four_scalar_results", + ) + complete_semantic_policies(module) + return WrapperPlanner().build(module) + + def test_multiple_scalar_result_plan_has_ordered_binding_consumers_and_shared_hidden_slot(): function = _multiple_result_plan().namespaces[0].functions[0] direct, hidden = function.results @@ -63,6 +77,18 @@ def test_multiple_scalar_results_lower_to_binding_tuple_and_one_bridge_function_ assert "PyTuple" not in bridge_source +def test_four_scalar_results_share_one_linear_failure_cleanup_suffix(): + artifacts = WrapperGenerator().generate(_four_result_plan()) + c_source = next(source.text for source in artifacts.sources if source.path.suffix == ".c") + + assert "if (result_1_obj == NULL) {\n goto prik_output_cleanup_1;\n }" in c_source + assert "if (result_obj == NULL) {\n goto prik_output_cleanup_4;\n }" in c_source + for position in range(4, 0, -1): + assert f"prik_output_cleanup_{position}:" in c_source + assert c_source.count(f"Py_XDECREF(result_{position - 1}_obj);") == 1 + assert "Py_DECREF(result_0_obj);" not in c_source + + def test_multiple_scalar_result_validation_rejects_position_and_consumer_drift(): plan = _multiple_result_plan() function = plan.namespaces[0].functions[0] diff --git a/tests/fortran/functions/end_to_end/test_external_procedures.py b/tests/fortran/functions/end_to_end/test_external_procedures.py index 93d95abac..9ba32fa01 100644 --- a/tests/fortran/functions/end_to_end/test_external_procedures.py +++ b/tests/fortran/functions/end_to_end/test_external_procedures.py @@ -26,7 +26,7 @@ C_ORDER_FLAT_BUFFER = wrapper_source("c_order_flat_buffer.f90") BLAS_LIKE_FILENAMES = ("daxpy_like.f90", "ddot_like.f90") BLAS_LIKE_SOURCES = tuple(wrapper_source(filename) for filename in BLAS_LIKE_FILENAMES) -BASIC_SOURCE = REPO_ROOT / "tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90" +BASIC_SOURCE = REPO_ROOT / "tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.f90" CONTRACT_FIXTURES = Path(__file__).parent / "fixtures" / "external" / "contracts" C_ORDER_FLAT_CONTRACT = ( REPO_ROOT diff --git a/tests/fortran/functions/end_to_end/test_hidden_native_outputs.py b/tests/fortran/functions/end_to_end/test_hidden_native_outputs.py new file mode 100644 index 000000000..13095734e --- /dev/null +++ b/tests/fortran/functions/end_to_end/test_hidden_native_outputs.py @@ -0,0 +1,109 @@ +"""``Hidden`` outputs cross the bridge normally but are never published. + +The bridge plans a hidden output exactly like a returned one, so its native +storage is allocated and released on the ordinary path. Only the binding +differs: it builds no Python result from it. +""" + +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_pyi_extension + +pytestmark = pytest.mark.fortran_end_to_end + +SOURCE = """module {name} +contains + subroutine tally(n, doubled, note) + integer, intent(in) :: n + integer, intent(out) :: doubled + character(len=*), intent(out) :: note + doubled = n * 2 + note = "seen" + end subroutine +end module +""" + + +def _build(tmp_path: Path, name: str, contract: str): + (tmp_path / f"{name}.f90").write_text(SOURCE.format(name=name), encoding="utf-8") + (tmp_path / f"{name}.pyi").write_text(contract, encoding="utf-8") + result = build_pyi_extension( + tmp_path / f"{name}.pyi", + native_fortran_sources=[tmp_path / f"{name}.f90"], + output_dir=tmp_path / f"build_{name}", + output_name=name, + ) + binding = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".c") + bridge = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".f90") + return result, binding, bridge + + +def test_hidden_outputs_are_released_but_never_returned(tmp_path: Path): + """The adapter still allocates the string, so the binding still frees it.""" + result, binding, bridge = _build( + tmp_path, + "hidden_all", + """from prik.contracts import Arg, Hidden, Int32, String, native_call + +@native_call([Arg(0), Hidden("doubled", Int32), Hidden("note", String[16])]) +def tally(n: Int32) -> None: ... +""", + ) + module = result.import_module() + + # The bridge is the ordinary owned-allocation adapter for a character output. + assert "note = c_malloc(17_c_size_t)" in bridge + # ... so the binding must still release it even though nothing is published. + assert "free(note)" in binding + + assert module.tally(np.int32(5)) is None + assert module.tally.__doc__.splitlines()[0] == "tally(n) -> None" + + +def test_hidden_and_returned_outputs_share_one_bridge(tmp_path: Path): + """Only the binding distinguishes them; the native call is the same.""" + result, _, bridge = _build( + tmp_path, + "hidden_mixed", + """from prik.contracts import Arg, Hidden, Int32, Return, Returns, String, native_call + +@native_call([Arg(0), Return("doubled", 0), Hidden("note", String[16])]) +def tally(n: Int32) -> Returns["doubled", Int32]: ... +""", + ) + module = result.import_module() + + assert 'subroutine bind_c_tally(n, doubled, note) bind(c, name="bind_c_tally")' in bridge + assert module.tally(np.int32(5)) == np.int32(10) + assert module.tally.__doc__.splitlines()[0] == "tally(n) -> int32" + + +def test_hidden_outputs_do_not_leak_across_repeated_calls(tmp_path: Path): + """A discarded output must not leak its adapter allocation or a reference.""" + result, _, _ = _build( + tmp_path, + "hidden_leak", + """from prik.contracts import Arg, Hidden, Int32, String, native_call + +@native_call([Arg(0), Hidden("doubled", Int32), Hidden("note", String[16])]) +def tally(n: Int32) -> None: ... +""", + ) + module = result.import_module() + + import sys + + def refcount_growth(calls: int) -> int: + """Return how much ``None``'s refcount moved across ``calls`` calls.""" + value = np.int32(3) + before = sys.getrefcount(None) + for _ in range(calls): + module.tally(value) + return sys.getrefcount(None) - before + + refcount_growth(200) # settle any first-call bookkeeping + # A leaked reference scales with the call count; a fixed offset does not. + assert refcount_growth(20_000) == refcount_growth(200) diff --git a/tests/fortran/functions/policy/test_positional_only_surface.py b/tests/fortran/functions/policy/test_positional_only_surface.py new file mode 100644 index 000000000..9139c0c46 --- /dev/null +++ b/tests/fortran/functions/policy/test_positional_only_surface.py @@ -0,0 +1,83 @@ +"""A positional-only surface drops keyword names policy does not owe the caller.""" + +import pytest + +from prik.parsers.fortran import parse_fortran_file +from prik.policy import complete_semantic_policies +from prik.policy.construction import completed_function_wrapper_policy +from prik.semantics.fortran2ir import fortran_module_to_semantic_module + + +_SOURCE = """ +module surface + implicit none +contains + function required_only(alpha, beta) result(total) + real(8), intent(in) :: alpha, beta + real(8) :: total + total = alpha + beta + end function required_only + + function has_optional(value, scale) result(total) + real(8), intent(in) :: value + real(8), intent(in), optional :: scale + real(8) :: total + total = value + if (present(scale)) total = value * scale + end function has_optional +end module surface +""" + + +def _policies(source: str, **options): + module = fortran_module_to_semantic_module(parse_fortran_file(source).modules[0]) + complete_semantic_policies(module, **options) + return {function.name: completed_function_wrapper_policy(function) for function in module.functions} + + +def test_an_all_required_function_becomes_positional_and_is_renamed_by_position(): + policy = _policies(_SOURCE, positional_only=True)["required_only"] + + assert policy.accepts_keyword_arguments is False + assert [argument.python_name for argument in policy.arguments] == ["arg0", "arg1"] + # The native declaration keeps its own names; only the Python surface changes. + assert [argument.name for argument in policy.arguments] == ["alpha", "beta"] + + +def test_an_optional_argument_keeps_keywords_because_skipping_one_requires_naming_the_rest(): + policy = _policies(_SOURCE, positional_only=True)["has_optional"] + + assert policy.accepts_keyword_arguments is True + assert [argument.python_name for argument in policy.arguments] == ["value", "scale"] + + +def test_the_default_surface_is_unchanged(): + policies = _policies(_SOURCE) + + assert policies["required_only"].accepts_keyword_arguments is True + assert [argument.python_name for argument in policies["required_only"].arguments] == ["alpha", "beta"] + + +def test_an_overload_set_cannot_become_positional_only_because_it_dispatches_on_keywords(): + source = """ +module dispatch + implicit none + interface scale_it + module procedure scale_real, scale_int + end interface scale_it +contains + function scale_real(value) result(total) + real(8), intent(in) :: value + real(8) :: total + total = 2.0d0 * value + end function scale_real + function scale_int(value) result(total) + integer, intent(in) :: value + integer :: total + total = 2 * value + end function scale_int +end module dispatch +""" + + with pytest.raises(ValueError, match="positional-only surface does not support overload sets"): + _policies(source, positional_only=True) diff --git a/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py b/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py index c223fd023..6b08051eb 100644 --- a/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py +++ b/tests/fortran/generic_interfaces/semantics/test_fortran_generic_semantics.py @@ -2,7 +2,6 @@ from pathlib import Path -import pytest from prik.semantics.fortran2ir import ( FortranToIRConverter, fortran_module_to_semantic_module, @@ -86,7 +85,12 @@ def test_public_generic_binds_private_inline_module_function_specifics_to_the_ge assert [candidate.metadata[BIND_TARGET_METADATA] for candidate in candidates] == ["shift", "shift"] -def test_converter_rejects_generic_constructor_interfaces_during_semantic_conversion(): +def test_converter_projects_a_generic_constructor_onto_its_class(): + """An interface named for a derived type is that type's constructor. + + Its specifics become the class's own `__init__` overload set rather than a + module-level generic, so the type name stays the only public spelling. + """ source = """ module constructor_generic_mod type :: item @@ -103,10 +107,13 @@ def test_converter_rejects_generic_constructor_interfaces_during_semantic_conver end module constructor_generic_mod """ - with pytest.raises(ValueError, match="cannot represent generic constructor") as exc_info: - fortran_module_to_semantic_module(parse_fortran_source(source)) + module = fortran_module_to_semantic_module(parse_fortran_source(source)) - assert "constructor_generic_mod.item" in str(exc_info.value) + assert [overload.name for overload in module.overload_sets] == [] + item = module.classes[0] + constructors = [overload for overload in item.overload_sets if overload.name == "__init__"] + assert len(constructors) == 1 + assert [procedure.metadata["overload_target"] for procedure in constructors[0].procedures] == ["make_item"] def test_converter_preserves_defined_operators_assignment_and_type_bound_operators(): diff --git a/tests/fortran/building_shared_library/README.md b/tests/fortran/infrastructure/building/README.md similarity index 94% rename from tests/fortran/building_shared_library/README.md rename to tests/fortran/infrastructure/building/README.md index 8319d9992..6672da888 100644 --- a/tests/fortran/building_shared_library/README.md +++ b/tests/fortran/infrastructure/building/README.md @@ -17,7 +17,7 @@ Evidence is split by the stage that establishes it: Run the complete feature with: ```bash -python3 -m pytest -q tests/fortran/building_shared_library +python3 -m pytest -q tests/fortran/infrastructure/building ``` Full BLAS and LAPACK corpus coverage lives in `examples/blas/` and diff --git a/tests/fortran/building_shared_library/compiling/test_compiler_verbose.py b/tests/fortran/infrastructure/building/compiling/test_compiler_verbose.py similarity index 83% rename from tests/fortran/building_shared_library/compiling/test_compiler_verbose.py rename to tests/fortran/infrastructure/building/compiling/test_compiler_verbose.py index 5b4d9bf91..b7ce4cc63 100644 --- a/tests/fortran/building_shared_library/compiling/test_compiler_verbose.py +++ b/tests/fortran/infrastructure/building/compiling/test_compiler_verbose.py @@ -38,7 +38,7 @@ def test_user_compile_flags_follow_default_profile_flags(monkeypatch, tmp_path: source=tmp_path / "source.c", object_path=tmp_path / "source.o", language="c", - flags=("-O0", "-g0"), + flags=("-O0", "-g0", "-funroll-loops"), ) compiler.compile_object(object_file) @@ -46,6 +46,8 @@ def test_user_compile_flags_follow_default_profile_flags(monkeypatch, tmp_path: command = compiler.command_log[0] assert command.index("-O3") < command.index("-O0") assert command.index("-DNDEBUG") < command.index("-g0") + assert command.index("-O3") < command.index("-funroll-loops") + assert command.count("-funroll-loops") == 1 def test_input_language_executable_override_controls_compilation_and_linking(tmp_path: Path): @@ -65,13 +67,13 @@ def test_input_language_executable_override_controls_compilation_and_linking(tmp @pytest.mark.parametrize( - ("fortran_name", "c_name", "vendor", "fortran_flag", "c_flag"), + ("fortran_name", "c_name", "vendor", "fortran_flag"), ( - ("x86_64-linux-gnu-gfortran-15", "x86_64-linux-gnu-gcc-15", "GNU", "-J", "-funroll-loops"), - ("ifx", "icx", "intel", "-module", "-funroll-loops"), - ("flang-22", "clang-22", "LLVM", "-J", "-funroll-loops"), - ("nvfortran", "nvc", "nvidia", "-module", "-Munroll"), - ("pgfortran", "pgcc", "PGI", "-module", "-Munroll"), + ("x86_64-linux-gnu-gfortran-15", "x86_64-linux-gnu-gcc-15", "GNU", "-J"), + ("ifx", "icx", "intel", "-module"), + ("flang-22", "clang-22", "LLVM", "-J"), + ("nvfortran", "nvc", "nvidia", "-module"), + ("pgfortran", "pgcc", "PGI", "-module"), ), ) def test_fortran_selection_uses_one_coherent_vendor_profile( @@ -80,7 +82,6 @@ def test_fortran_selection_uses_one_coherent_vendor_profile( c_name: str, vendor: str, fortran_flag: str, - c_flag: str, ): fortran = tmp_path / fortran_name c_compiler = tmp_path / c_name @@ -108,7 +109,7 @@ def test_fortran_selection_uses_one_coherent_vendor_profile( assert compiler.command_log[0][0] == str(fortran) assert fortran_flag in compiler.command_log[0] assert compiler.command_log[1][0] == str(c_compiler) - assert c_flag in compiler.command_log[1] + assert "-O3" in compiler.command_log[1] assert compiler.command_log[2][0] == str(fortran) @@ -120,6 +121,34 @@ def test_fortran_selection_rejects_an_unknown_compiler_family(tmp_path: Path): Compiler.from_fortran_executable(str(compiler), execute_commands=False) +@pytest.mark.parametrize( + ("banner", "vendor"), + [ + ("Apple clang version 15.0.0 (clang-1500.3.9.4)\nTarget: arm64-apple-darwin23.4.0", "LLVM"), + ("cc (Ubuntu 13.3.0-6ubuntu2) 13.3.0\nCopyright (C) 2023 Free Software Foundation, Inc.", "GNU"), + ("Intel(R) oneAPI DPC++/C++ Compiler 2024.0.0 (2024.0.0.20231017)", "intel"), + ], +) +def test_generic_c_driver_takes_its_vendor_from_its_own_version_banner(tmp_path: Path, banner: str, vendor: str): + """``cc`` names no vendor, and on some platforms it is not a link to one.""" + compiler = tmp_path / "cc" + compiler.write_text(f'#!/bin/sh\nif [ "$1" = "--version" ]; then\n cat <<\'EOF\'\n{banner}\nEOF\nfi\n') + compiler.chmod(0o755) + + selected = Compiler.from_c_executable("cc", execute_commands=False, search_path=str(tmp_path)) + + assert selected._toolchain is available_compilers[vendor] + + +def test_c_selection_rejects_a_driver_that_names_no_family_and_reports_none(tmp_path: Path): + compiler = tmp_path / "mystery-driver" + compiler.write_text("#!/bin/sh\nexit 1\n") + compiler.chmod(0o755) + + with pytest.raises(ValueError, match="Unknown C compiler family"): + Compiler.from_c_executable("mystery-driver", execute_commands=False, search_path=str(tmp_path)) + + def test_fortran_selection_rejects_a_missing_vendor_c_compiler(tmp_path: Path): compiler = tmp_path / "ifx" compiler.touch(mode=0o755) @@ -278,6 +307,9 @@ def test_builtin_toolchains_keep_c_and_fortran_stage_definitions(): assert config["exec"] assert config["debug_flags"] assert config["release_flags"] + assert "-O3" in config["release_flags"] + assert "-funroll-loops" not in config["release_flags"] + assert "-Munroll" not in config["release_flags"] assert config["general_flags"] assert toolchain["fortran"]["module_output_flag"] assert toolchain["c"]["python"]["shared_suffix"] diff --git a/tests/fortran/building_shared_library/compiling/test_example_native_library.py b/tests/fortran/infrastructure/building/compiling/test_example_native_library.py similarity index 52% rename from tests/fortran/building_shared_library/compiling/test_example_native_library.py rename to tests/fortran/infrastructure/building/compiling/test_example_native_library.py index 585746869..953d2be17 100644 --- a/tests/fortran/building_shared_library/compiling/test_example_native_library.py +++ b/tests/fortran/infrastructure/building/compiling/test_example_native_library.py @@ -4,10 +4,12 @@ import os from pathlib import Path +import subprocess import pytest from examples import native_library +from examples.lapack.routine_inventory import EXPECTED_LAPACK_WRAPPED_SOURCE_FILES @pytest.mark.parametrize("example", ("blas", "lapack")) @@ -21,6 +23,26 @@ def test_aggregate_example_build_restores_the_workspace(example: str) -> None: assert f2py_build < restore_workspace < python_path_export +def test_lapack_build_script_stops_when_the_native_library_build_fails(tmp_path: Path) -> None: + for executable in ("python", "gfortran"): + path = tmp_path / executable + path.write_text("#!/bin/sh\nexit 23\n", encoding="utf-8") + path.chmod(0o755) + environment = os.environ | {"PATH": f"{tmp_path}:{os.environ['PATH']}"} + + result = subprocess.run( # nosec B603 - fixed shell and repository-owned example script + ("bash", "-e", "-c", "source examples/lapack/build_prik.sh"), + cwd=native_library.EXAMPLES_ROOT.parent, + env=environment, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 23 + assert "wrapper_sources" not in result.stderr + + def test_native_cache_preserves_module_files_for_wrapper_compilation(tmp_path: Path, monkeypatch) -> None: sources = ( native_library.LAPACK_SOURCE_ROOT / "la_constants.f90", @@ -67,14 +89,21 @@ def fail_if_recompiled(*_args) -> None: @pytest.mark.parametrize( - ("library", "expected_dependencies"), - (("blas", ()), ("lapack", ("-llapack", "-lblas"))), + ("platform", "library", "expected_dependencies", "suffix"), + ( + ("linux", "blas", (), ".so"), + ("linux", "lapack", ("-llapack", "-lblas"), ".so"), + ("darwin", "blas", (), ".dylib"), + ("darwin", "lapack", ("-llapack", "-lblas"), ".dylib"), + ), ) def test_shared_example_library_links_its_native_dependencies( tmp_path: Path, monkeypatch, + platform: str, library: str, expected_dependencies: tuple[str, ...], + suffix: str, ) -> None: commands = [] @@ -84,21 +113,66 @@ def run(command: tuple[str, ...], *, check: bool) -> None: Path(command[3]).touch() monkeypatch.setattr(native_library.subprocess, "run", run) + monkeypatch.setattr(native_library.sys, "platform", platform) archive = tmp_path / f"libprik_full_{library}.a" archive.touch() shared_library = native_library._cached_shared_library(tmp_path, library, archive, "gfortran") assert shared_library.is_file() + assert shared_library.suffix == suffix + if platform == "darwin": + expected_link_flags = ( + f"-Wl,-install_name,{shared_library}", + "-Wl,-force_load", + str(archive), + ) + shared_mode = "-dynamiclib" + else: + expected_link_flags = ("-Wl,--whole-archive", str(archive), "-Wl,--no-whole-archive") + shared_mode = "-shared" assert commands == [ ( "gfortran", - "-shared", + shared_mode, "-o", str(tmp_path / f"{shared_library.name}.{os.getpid()}.tmp"), - "-Wl,--whole-archive", - str(archive), - "-Wl,--no-whole-archive", + *expected_link_flags, *expected_dependencies, ) ] + + +def test_lapack_wrapper_sources_follow_the_upstream_default_non_xblas_boundary() -> None: + wrapper_sources = native_library.wrapper_sources("lapack") + wrapped_names = {source.name for source in wrapper_sources} + xblas_names = native_library._lapack_xblas_source_names() + + assert len(wrapper_sources) == EXPECTED_LAPACK_WRAPPED_SOURCE_FILES + assert len(xblas_names) == 130 + assert wrapped_names.isdisjoint(xblas_names) + assert {"dgesv.f", "dgesdd.f"} <= wrapped_names + assert {"dgerfsx.f", "dgesvxx.f"} <= xblas_names + + native_names = {source.name for source in native_library.native_sources("lapack")} + assert {"sroundup_lwork.f", "droundup_lwork.f"} <= native_names + + +def test_cached_wrapper_source_root_exposes_only_selected_sources(tmp_path: Path) -> None: + sources = ( + native_library.LAPACK_SOURCE_ROOT / "dgesv.f", + native_library.LAPACK_SOURCE_ROOT / "dgesdd.f", + ) + + source_root = native_library._cached_wrapper_source_root(tmp_path, sources) + + assert {path.name for path in source_root.iterdir()} == {source.name for source in sources} + assert all((source_root / source.name).resolve() == source.resolve() for source in sources) + + +@pytest.mark.parametrize( + ("filename", "expected"), + (("libprik_full_blas.so", "prik_full_blas"), ("libprik_full_lapack.dylib", "prik_full_lapack")), +) +def test_example_linker_name_accepts_linux_and_macos_shared_libraries(filename: str, expected: str) -> None: + assert native_library.linker_name(Path(filename)) == expected diff --git a/tests/fortran/building_shared_library/compiling/test_support_probe_artifacts.py b/tests/fortran/infrastructure/building/compiling/test_support_probe_artifacts.py similarity index 100% rename from tests/fortran/building_shared_library/compiling/test_support_probe_artifacts.py rename to tests/fortran/infrastructure/building/compiling/test_support_probe_artifacts.py diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/contracts/multiple_files/combined_modules/__init__.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/__init__.pyi similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/contracts/multiple_files/combined_modules/__init__.pyi rename to tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/__init__.pyi diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi rename to tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/box_ops.pyi diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/contracts/multiple_files/combined_modules/first_math.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/first_math.pyi similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/contracts/multiple_files/combined_modules/first_math.pyi rename to tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/first_math.pyi diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi rename to tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/second_math.pyi diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/contracts/multiple_files/combined_modules/shared_types.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/shared_types.pyi similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/contracts/multiple_files/combined_modules/shared_types.pyi rename to tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/multiple_files/combined_modules/shared_types.pyi diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/contracts/runtime_abi/__init__.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/__init__.pyi similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/contracts/runtime_abi/__init__.pyi rename to tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/__init__.pyi diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/contracts/runtime_abi/fruntime_abi_f90.pyi b/tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/fruntime_abi_f90.pyi similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/contracts/runtime_abi/fruntime_abi_f90.pyi rename to tests/fortran/infrastructure/building/end_to_end/fixtures/contracts/runtime_abi/fruntime_abi_f90.pyi diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/native/double_value.f b/tests/fortran/infrastructure/building/end_to_end/fixtures/native/double_value.f similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/native/double_value.f rename to tests/fortran/infrastructure/building/end_to_end/fixtures/native/double_value.f diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/native/fdefault_output.f b/tests/fortran/infrastructure/building/end_to_end/fixtures/native/fdefault_output.f similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/native/fdefault_output.f rename to tests/fortran/infrastructure/building/end_to_end/fixtures/native/fdefault_output.f diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/native/first_api.f90 b/tests/fortran/infrastructure/building/end_to_end/fixtures/native/first_api.f90 similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/native/first_api.f90 rename to tests/fortran/infrastructure/building/end_to_end/fixtures/native/first_api.f90 diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/native/fruntime_abi_f90.f90 b/tests/fortran/infrastructure/building/end_to_end/fixtures/native/fruntime_abi_f90.f90 similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/native/fruntime_abi_f90.f90 rename to tests/fortran/infrastructure/building/end_to_end/fixtures/native/fruntime_abi_f90.f90 diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/native/home_points.f90 b/tests/fortran/infrastructure/building/end_to_end/fixtures/native/home_points.f90 similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/native/home_points.f90 rename to tests/fortran/infrastructure/building/end_to_end/fixtures/native/home_points.f90 diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/native/scale.f90 b/tests/fortran/infrastructure/building/end_to_end/fixtures/native/scale.f90 similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/native/scale.f90 rename to tests/fortran/infrastructure/building/end_to_end/fixtures/native/scale.f90 diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/native/second_api.f90 b/tests/fortran/infrastructure/building/end_to_end/fixtures/native/second_api.f90 similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/native/second_api.f90 rename to tests/fortran/infrastructure/building/end_to_end/fixtures/native/second_api.f90 diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/native/standalone_api.f b/tests/fortran/infrastructure/building/end_to_end/fixtures/native/standalone_api.f similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/native/standalone_api.f rename to tests/fortran/infrastructure/building/end_to_end/fixtures/native/standalone_api.f diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/native/verbose_api.f90 b/tests/fortran/infrastructure/building/end_to_end/fixtures/native/verbose_api.f90 similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/native/verbose_api.f90 rename to tests/fortran/infrastructure/building/end_to_end/fixtures/native/verbose_api.f90 diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_direct_bind_c_f90.f90 b/tests/fortran/infrastructure/building/end_to_end/fixtures/routing/native/multi_source_direct_bind_c_f90.f90 similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_direct_bind_c_f90.f90 rename to tests/fortran/infrastructure/building/end_to_end/fixtures/routing/native/multi_source_direct_bind_c_f90.f90 diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_direct_helper_f90.f90 b/tests/fortran/infrastructure/building/end_to_end/fixtures/routing/native/multi_source_direct_helper_f90.f90 similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_direct_helper_f90.f90 rename to tests/fortran/infrastructure/building/end_to_end/fixtures/routing/native/multi_source_direct_helper_f90.f90 diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_mixed_bind_c_f90.f90 b/tests/fortran/infrastructure/building/end_to_end/fixtures/routing/native/multi_source_mixed_bind_c_f90.f90 similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_mixed_bind_c_f90.f90 rename to tests/fortran/infrastructure/building/end_to_end/fixtures/routing/native/multi_source_mixed_bind_c_f90.f90 diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_mixed_helper_f90.f90 b/tests/fortran/infrastructure/building/end_to_end/fixtures/routing/native/multi_source_mixed_helper_f90.f90 similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_mixed_helper_f90.f90 rename to tests/fortran/infrastructure/building/end_to_end/fixtures/routing/native/multi_source_mixed_helper_f90.f90 diff --git a/tests/fortran/building_shared_library/end_to_end/real_libraries/__init__.py b/tests/fortran/infrastructure/building/end_to_end/real_libraries/__init__.py similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/real_libraries/__init__.py rename to tests/fortran/infrastructure/building/end_to_end/real_libraries/__init__.py diff --git a/tests/fortran/building_shared_library/end_to_end/real_libraries/_support.py b/tests/fortran/infrastructure/building/end_to_end/real_libraries/_support.py similarity index 95% rename from tests/fortran/building_shared_library/end_to_end/real_libraries/_support.py rename to tests/fortran/infrastructure/building/end_to_end/real_libraries/_support.py index 480089724..227291b52 100644 --- a/tests/fortran/building_shared_library/end_to_end/real_libraries/_support.py +++ b/tests/fortran/infrastructure/building/end_to_end/real_libraries/_support.py @@ -10,9 +10,10 @@ from prik import build_fortran_extension from tests.fortran._support.wrapper_build import _import_from_build_dir +from tests.fortran._support.paths import REPO_ROOT -REPOSITORY_ROOT = Path(__file__).resolve().parents[5] +REPOSITORY_ROOT = REPO_ROOT def real_library_source_dir(library: str) -> Path: diff --git a/tests/fortran/building_shared_library/end_to_end/real_libraries/test_fftpack_routines.py b/tests/fortran/infrastructure/building/end_to_end/real_libraries/test_fftpack_routines.py similarity index 96% rename from tests/fortran/building_shared_library/end_to_end/real_libraries/test_fftpack_routines.py rename to tests/fortran/infrastructure/building/end_to_end/real_libraries/test_fftpack_routines.py index ad91c46b3..d192ced64 100644 --- a/tests/fortran/building_shared_library/end_to_end/real_libraries/test_fftpack_routines.py +++ b/tests/fortran/infrastructure/building/end_to_end/real_libraries/test_fftpack_routines.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from tests.fortran.building_shared_library.end_to_end.real_libraries._support import ( +from tests.fortran.infrastructure.building.end_to_end.real_libraries._support import ( build_real_fortran_library, real_library_source_dir, ) diff --git a/tests/fortran/building_shared_library/end_to_end/real_libraries/test_minpack_routines.py b/tests/fortran/infrastructure/building/end_to_end/real_libraries/test_minpack_routines.py similarity index 96% rename from tests/fortran/building_shared_library/end_to_end/real_libraries/test_minpack_routines.py rename to tests/fortran/infrastructure/building/end_to_end/real_libraries/test_minpack_routines.py index 89dfc06c4..b775f1cf4 100644 --- a/tests/fortran/building_shared_library/end_to_end/real_libraries/test_minpack_routines.py +++ b/tests/fortran/infrastructure/building/end_to_end/real_libraries/test_minpack_routines.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from tests.fortran.building_shared_library.end_to_end.real_libraries._support import ( +from tests.fortran.infrastructure.building.end_to_end.real_libraries._support import ( build_real_fortran_library, real_library_source_dir, ) diff --git a/tests/fortran/building_shared_library/end_to_end/test_build_direct_entrypoint_routing.py b/tests/fortran/infrastructure/building/end_to_end/test_build_direct_entrypoint_routing.py similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/test_build_direct_entrypoint_routing.py rename to tests/fortran/infrastructure/building/end_to_end/test_build_direct_entrypoint_routing.py diff --git a/tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py b/tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/test_multi_source_builds.py rename to tests/fortran/infrastructure/building/end_to_end/test_multi_source_builds.py diff --git a/tests/fortran/building_shared_library/end_to_end/test_native_bundles.py b/tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py similarity index 99% rename from tests/fortran/building_shared_library/end_to_end/test_native_bundles.py rename to tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py index ea8767ef0..bbac1a43c 100644 --- a/tests/fortran/building_shared_library/end_to_end/test_native_bundles.py +++ b/tests/fortran/infrastructure/building/end_to_end/test_native_bundles.py @@ -12,7 +12,7 @@ import pytest from prik import build_pyi_extension -from tests.fortran.building_shared_library.end_to_end.test_multi_source_builds import ( +from tests.fortran.infrastructure.building.end_to_end.test_multi_source_builds import ( _assert_combined_runtime, _compile_native_objects, _generate_combined_contract, diff --git a/tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py b/tests/fortran/infrastructure/building/end_to_end/test_runtime_compatibility.py similarity index 100% rename from tests/fortran/building_shared_library/end_to_end/test_runtime_compatibility.py rename to tests/fortran/infrastructure/building/end_to_end/test_runtime_compatibility.py diff --git a/tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py b/tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py similarity index 95% rename from tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py rename to tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py index f9c3db031..940be1e3d 100644 --- a/tests/fortran/building_shared_library/end_to_end/test_source_build_modes.py +++ b/tests/fortran/infrastructure/building/end_to_end/test_source_build_modes.py @@ -14,6 +14,7 @@ from tests.fortran._support.wrapper_build import _sole_native_module from prik.preprocessing import PreprocessingConfig from prik.pipeline.build import NativeBuildPlan, NativeLinkItem, build_fortran_extension +from tests.fortran._support.paths import REPO_ROOT NATIVE_FIXTURES = Path(__file__).parent / "fixtures" / "native" VERBOSE_SOURCE = NATIVE_FIXTURES / "verbose_api.f90" @@ -21,7 +22,7 @@ SCALE_SOURCE = NATIVE_FIXTURES / "scale.f90" SCALAR_SOURCE = SCALE_SOURCE HOME_POINTS_SOURCE = NATIVE_FIXTURES / "home_points.f90" -BUILD_MODULE = Path(__file__).resolve().parents[4] / "prik" / "pipeline" / "build.py" +BUILD_MODULE = REPO_ROOT / "prik" / "pipeline" / "build.py" pytestmark = pytest.mark.fortran_end_to_end @@ -94,6 +95,33 @@ def test_verbose_mode_prints_full_direct_build_commands(tmp_path: Path): assert "Built extension:" in result.stdout +def test_verbose_mode_prints_failing_compiler_command_before_execution(tmp_path: Path): + source = tmp_path / "verbose_api.f90" + shutil.copyfile(VERBOSE_SOURCE, source) + + result = subprocess.run( + [ + sys.executable, + "-m", + "prik", + str(source), + "--verbose", + "--out-dir", + str(tmp_path), + "--wrapper-c-flags=-fprik-invalid-option", + ], + capture_output=True, + text=True, + check=False, + cwd=tmp_path, + ) + + assert result.returncode == 1 + command = next(line for line in result.stdout.splitlines() if "verbose_api_wrapper.c" in line and "-c" in line) + assert "-fprik-invalid-option" in shlex.split(command) + assert "Native compiler command failed:" in result.stderr + + def test_verbose_mode_prints_custom_wrapper_flags(tmp_path: Path): source = tmp_path / SCALE_SOURCE.name shutil.copyfile(SCALE_SOURCE, source) diff --git a/tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi similarity index 100% rename from tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi rename to tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fdefault_output/__init__.pyi diff --git a/tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/__init__.pyi b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/__init__.pyi similarity index 100% rename from tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/__init__.pyi rename to tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/__init__.pyi diff --git a/tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/fruntime_abi_f90.pyi b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/fruntime_abi_f90.pyi similarity index 100% rename from tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/fruntime_abi_f90.pyi rename to tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/fruntime_abi_f90/fruntime_abi_f90.pyi diff --git a/tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/verbose_api/__init__.pyi b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/__init__.pyi similarity index 100% rename from tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/verbose_api/__init__.pyi rename to tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/__init__.pyi diff --git a/tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/verbose_api/verbose_api.pyi b/tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/verbose_api.pyi similarity index 100% rename from tests/fortran/building_shared_library/pipeline/fixtures/generated_contracts/source_builds/verbose_api/verbose_api.pyi rename to tests/fortran/infrastructure/building/pipeline/fixtures/generated_contracts/source_builds/verbose_api/verbose_api.pyi diff --git a/tests/fortran/building_shared_library/pipeline/test_generated_wrapper_build.py b/tests/fortran/infrastructure/building/pipeline/test_generated_wrapper_build.py similarity index 100% rename from tests/fortran/building_shared_library/pipeline/test_generated_wrapper_build.py rename to tests/fortran/infrastructure/building/pipeline/test_generated_wrapper_build.py index 4871249d1..b5c4aff4f 100644 --- a/tests/fortran/building_shared_library/pipeline/test_generated_wrapper_build.py +++ b/tests/fortran/infrastructure/building/pipeline/test_generated_wrapper_build.py @@ -189,8 +189,8 @@ def scale(x: Float64) -> Float64: ... f"Write binding source: {binding_source}", f"Write binding header: {header}", f"Write native support: {build_dir / 'binding_support'}", - f"Compile bridge source: {bridge_source} -> {bridge_obj.object_path}", f"Compile binding source: {binding_source} -> {binding_obj.object_path}", + f"Compile bridge source: {bridge_source} -> {bridge_obj.object_path}", f"Create shared library: {result.shared_library}", ] diff --git a/tests/fortran/building_shared_library/pipeline/test_parallel_compilation.py b/tests/fortran/infrastructure/building/pipeline/test_parallel_compilation.py similarity index 100% rename from tests/fortran/building_shared_library/pipeline/test_parallel_compilation.py rename to tests/fortran/infrastructure/building/pipeline/test_parallel_compilation.py diff --git a/tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py b/tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py similarity index 99% rename from tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py rename to tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py index 3efe3f7d8..807a0dd3d 100644 --- a/tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py +++ b/tests/fortran/infrastructure/building/pipeline/test_pyi_build_modes.py @@ -216,7 +216,7 @@ def test_pyi_makefile_manifest_and_replay_workflows(tmp_path: Path): assert manifest_path == build_dir / "prik-build.json" assert makefile_path == build_dir / "Makefile.prik" assert manifest == payload["manifest"] - assert manifest["schema_version"] == 3 + assert manifest["schema_version"] == 4 assert manifest["build_kind"] == "pyi-wrapper" assert manifest["compiler"]["input_executable"] == str(selected_compiler) assert manifest["compiler"]["fortran_flags"] == ["-O2", "-g0"] diff --git a/tests/fortran/building_shared_library/pipeline/test_root_build_api.py b/tests/fortran/infrastructure/building/pipeline/test_root_build_api.py similarity index 81% rename from tests/fortran/building_shared_library/pipeline/test_root_build_api.py rename to tests/fortran/infrastructure/building/pipeline/test_root_build_api.py index 0de50f3f8..38c309190 100644 --- a/tests/fortran/building_shared_library/pipeline/test_root_build_api.py +++ b/tests/fortran/infrastructure/building/pipeline/test_root_build_api.py @@ -1,16 +1,23 @@ """Public root-facade contract for normal wrapper builds.""" import prik -from prik.pipeline.build import build_fortran_extension, build_pyi_extension, build_pyi_extension_from_manifest +from prik.pipeline.build import ( + build_c_extension, + build_fortran_extension, + build_pyi_extension, + build_pyi_extension_from_manifest, +) def test_root_facade_exposes_only_version_and_build_entrypoints(): assert prik.__all__ == ( "__version__", + "build_c_extension", "build_fortran_extension", "build_pyi_extension", "build_pyi_extension_from_manifest", ) + assert prik.build_c_extension is build_c_extension assert prik.build_fortran_extension is build_fortran_extension assert prik.build_pyi_extension is build_pyi_extension assert prik.build_pyi_extension_from_manifest is build_pyi_extension_from_manifest diff --git a/tests/fortran/building_shared_library/pipeline/test_source_generated_contracts.py b/tests/fortran/infrastructure/building/pipeline/test_source_generated_contracts.py similarity index 100% rename from tests/fortran/building_shared_library/pipeline/test_source_generated_contracts.py rename to tests/fortran/infrastructure/building/pipeline/test_source_generated_contracts.py diff --git a/tests/fortran/command_line_interface/pipeline/_support.py b/tests/fortran/infrastructure/cli/pipeline/_support.py similarity index 95% rename from tests/fortran/command_line_interface/pipeline/_support.py rename to tests/fortran/infrastructure/cli/pipeline/_support.py index 42427e5ca..1f91fbd93 100644 --- a/tests/fortran/command_line_interface/pipeline/_support.py +++ b/tests/fortran/infrastructure/cli/pipeline/_support.py @@ -1,9 +1,9 @@ import types -from pathlib import Path import prik.cli as prik_cli +from tests.fortran._support.paths import GENERAL_FORTRAN_DIR -TEST_FILE = Path(__file__).parents[2] / "source_parsing" / "parsing" / "fixtures" / "general" / "basic_subroutine.f90" +TEST_FILE = GENERAL_FORTRAN_DIR / "basic_subroutine.f90" class _MainParserError(Exception): diff --git a/tests/fortran/command_line_interface/pipeline/test_argument_contract.py b/tests/fortran/infrastructure/cli/pipeline/test_argument_contract.py similarity index 91% rename from tests/fortran/command_line_interface/pipeline/test_argument_contract.py rename to tests/fortran/infrastructure/cli/pipeline/test_argument_contract.py index e83b11de2..c72e2c335 100644 --- a/tests/fortran/command_line_interface/pipeline/test_argument_contract.py +++ b/tests/fortran/infrastructure/cli/pipeline/test_argument_contract.py @@ -10,7 +10,7 @@ import prik.cli as prik_cli from prik.preprocessing import PreprocessingError -from tests.fortran.command_line_interface.pipeline._support import ( +from tests.fortran.infrastructure.cli.pipeline._support import ( TEST_FILE, _MainParserError, _install_main_parser, @@ -142,7 +142,7 @@ class extra(Opaque): ({"parse": True, "print_limit": -1}, "--print-limit must be >= 0"), ( {"paths": ["input.pyi"]}, - "A .pyi wrapper build requires --native-fortran-sources, --native-objects, " + "A .pyi wrapper build requires --native-fortran-sources, --native-c-sources, --native-objects, " "--native-library, or --native-link-item", ), ( @@ -152,7 +152,7 @@ class extra(Opaque): ), ( {"no_compile_input_sources": True}, - "--no-compile-input-sources requires --native-fortran-sources, --native-objects, " + "--no-compile-input-sources requires --native-fortran-sources, --native-c-sources, --native-objects, " "--native-library, or --native-link-item", ), ( @@ -553,15 +553,16 @@ def assert_group_order(help_text, *headings): assert "--native-link-item" in build_help assert "--jobs" in build_help assert "--wrapper-c-flags" in build_help - assert "Compiler used throughout the extension build" in build_help + assert "compiler used throughout the extension build" in normalized_build_help assert "Add a compiler include search directory" in build_help assert "default: gfortran" in normalized_build_help assert "default: ./__prik__" in normalized_build_help assert ( - "Fortran source file(s), one source directory, or exactly one semantic .pyi contract" in normalized_build_help + "Fortran or C source file(s), one source directory, or exactly one semantic .pyi contract" + in normalized_build_help ) assert "--no-compile-input-sources" in build_help - assert "Input language (default: fortran)" in normalized_build_help + assert "Input language (default: fortran; use c for direct C wrappers)" in normalized_build_help assert "Rebuild the extension from an existing prik-build.json" in normalized_build_help 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 @@ -571,8 +572,7 @@ def assert_group_order(help_text, *headings): 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, --jobs" in normalized_build_help - assert "--language {fortran}" in build_help - assert "--language {fortran,c}" not in build_help + assert "--language {fortran,c}" in build_help assert parse_help.startswith("usage: python3 -m prik parse INPUT [INPUT ...] [OPTIONS]") for heading in ( "positional arguments:", @@ -603,14 +603,16 @@ def assert_group_order(help_text, *headings): assert "native and bridge compilation" not in normalized_parse_help assert "default: gfortran; cc with --language c" in normalized_parse_help assert semantics_help.startswith("usage: python3 -m prik semantics INPUT [INPUT ...] [OPTIONS]") - assert "--json" not in semantics_help - assert "Write combined JSON to PATH" in semantics_help + assert "--json" in semantics_help + assert "--print-limit" in semantics_help + assert "Write the report to PATH" in semantics_help assert "Define a preprocessing macro" in semantics_help for heading in ( "positional arguments:", "input options:", "preprocessing options:", "C include options:", + "report options:", "output options:", "diagnostic options:", ): @@ -622,6 +624,7 @@ def assert_group_order(help_text, *headings): "input options:", "preprocessing options:", "C include options:", + "report options:", "output options:", "diagnostic options:", ) @@ -675,7 +678,8 @@ def assert_group_order(help_text, *headings): "output options:", "diagnostic options:", ) - assert "--format {json,markdown}" in probe_help + assert "--json" in probe_help + assert "--format" not in probe_help assert "Probe compiler-target datatype sizes, alignment, and ABI facts." in probe_help assert "Probe flags that change default kinds:" in probe_help assert "--compiler-arg=-fdefault-real-8 --compiler-arg=-fdefault-integer-8" in probe_help @@ -737,11 +741,21 @@ def test_cli_help_places_a_clear_purpose_below_usage(parser_factory, purpose): ), ( prik_cli._parse_parser, - ("Basic Fortran inspection:", "Detailed Fortran report:", "C header as JSON:"), + ( + "Basic Fortran inspection:", + "Detailed Fortran report:", + "C header as JSON:", + "--json picks the format, --out picks the destination:", + ), ), ( prik_cli._semantics_parser, - ("Basic Fortran conversion:", "C header:", "Save semantic IR:"), + ( + "Basic Fortran conversion:", + "C header:", + "Complete semantic IR as JSON on standard output:", + "--json picks the format, --out picks the destination:", + ), ), ( prik_cli._generate_parser, @@ -750,8 +764,9 @@ def test_cli_help_places_a_clear_purpose_below_usage(parser_factory, purpose): ( prik_cli._probe_parser, ( - "Basic target probes:", - "Human-readable mapping table:", + "Target datatype mapping table:", + "Complete measured report as JSON:", + "--json picks the format, --out picks the destination:", "Probe flags that change default kinds:", "Cross-target probe:", ), @@ -819,6 +834,7 @@ def test_subcommand_help_exposes_every_supported_option(parser_factory): (["--preprocessor-adapter", "auto"], "replays its saved preprocessing recipe"), (["-D", "USE_FAST=1"], "replays its saved preprocessing recipe"), (["--strict-wrapper-names"], "replays saved wrapper behavior"), + (["--assume-intent-in-scalars"], "replays saved wrapper behavior"), (["--native-library", "openblas"], "replays saved native inputs"), ], ) @@ -958,3 +974,38 @@ def test_prik_main_rejects_invalid_macro_names(macro_flag: str, monkeypatch): monkeypatch.setattr(sys, "argv", ["prik", "parse", str(TEST_FILE), macro_flag, "=invalid"]) with pytest.raises(SystemExit): prik_cli.main() + + +def test_assume_intent_in_scalars_is_discoverable_from_the_first_help_screen(): + """The option changes the default Python surface, so it is not hidden behind --help-build.""" + top_help = prik_cli._top_level_parser(["--help"]).format_help() + build_help = prik_cli._build_parser(["input.f90", "--help"]).format_help() + generate_help = prik_cli._generate_parser(["--help"]).format_help() + + semantics_help = prik_cli._semantics_parser(["--help"]).format_help() + + assert "--assume-intent-in-scalars" in top_help + assert "--assume-intent-in-scalars" in build_help + assert "--assume-intent-in-scalars" in generate_help + assert "--assume-intent-in-scalars" in semantics_help + + +def test_pyi_wrapper_build_rejects_assume_intent_in_scalars(tmp_path: Path, capsys): + """A contract states its own results, so the option has no missing intent to interpret.""" + contract = tmp_path / "api.pyi" + contract.write_text("from prik.contracts import Float64\n", encoding="utf-8") + source = tmp_path / "api.f90" + source.write_text("subroutine noop()\nend subroutine noop\n", encoding="utf-8") + + with pytest.raises(SystemExit) as exc_info: + prik_cli.main( + [ + str(contract), + "--native-fortran-sources", + str(source), + "--assume-intent-in-scalars", + ] + ) + + assert exc_info.value.code == 2 + assert "already states its own results" in capsys.readouterr().err diff --git a/tests/fortran/command_line_interface/pipeline/test_output_contract.py b/tests/fortran/infrastructure/cli/pipeline/test_output_contract.py similarity index 90% rename from tests/fortran/command_line_interface/pipeline/test_output_contract.py rename to tests/fortran/infrastructure/cli/pipeline/test_output_contract.py index 26517d30c..7b99f160e 100644 --- a/tests/fortran/command_line_interface/pipeline/test_output_contract.py +++ b/tests/fortran/infrastructure/cli/pipeline/test_output_contract.py @@ -21,7 +21,8 @@ PreprocessingDiagnostic, PreprocessingError, ) -from tests.fortran.command_line_interface.pipeline._support import ( +from tests.fortran._support.paths import GENERAL_FORTRAN_DIR +from tests.fortran.infrastructure.cli.pipeline._support import ( TEST_FILE, _MainParserError, _install_main_parser, @@ -152,9 +153,10 @@ def test_cli_json_out(tmp_path: Path): def test_cli_out_without_filename_uses_source_basename_json(tmp_path: Path): + """--out with no path writes one sibling file per source in the selected format.""" f90 = tmp_path / "mini.f90" f90.write_text("subroutine work(n)\n integer, intent(in) :: n\nend subroutine work\n", encoding="utf-8") - cmd = [sys.executable, "-m", "prik", "parse", str(f90), "--out"] + cmd = [sys.executable, "-m", "prik", "parse", str(f90), "--json", "--out"] res = subprocess.run(cmd, capture_output=True, text=True, check=True) assert res.stdout == "" out = tmp_path / "mini.json" @@ -163,6 +165,17 @@ def test_cli_out_without_filename_uses_source_basename_json(tmp_path: Path): assert str(f90) in file_payload +def test_cli_out_without_json_writes_the_human_report_beside_each_source(tmp_path: Path): + """--out selects only the destination, so without --json it writes the report text.""" + f90 = tmp_path / "mini.f90" + f90.write_text("subroutine work(n)\n integer, intent(in) :: n\nend subroutine work\n", encoding="utf-8") + cmd = [sys.executable, "-m", "prik", "parse", str(f90), "--out"] + res = subprocess.run(cmd, capture_output=True, text=True, check=True) + assert res.stdout == "" + assert not (tmp_path / "mini.json").exists() + assert f"File: {f90}" in (tmp_path / "mini.txt").read_text(encoding="utf-8") + + def test_cli_json_output_without_out(): cmd = [sys.executable, "-m", "prik", "parse", str(TEST_FILE), "--json"] res = subprocess.run(cmd, capture_output=True, text=True, check=True) @@ -198,7 +211,7 @@ def test_cli_formats_parse_error_with_ansi_by_default(tmp_path: Path): def test_cli_semantics_out_writes_json_without_stdout(tmp_path: Path): out = tmp_path / "prik.semantics.json" - cmd = [sys.executable, "-m", "prik", "semantics", str(TEST_FILE), "--out", str(out)] + cmd = [sys.executable, "-m", "prik", "semantics", str(TEST_FILE), "--json", "--out", str(out)] res = subprocess.run(cmd, capture_output=True, text=True, check=True) assert res.stdout == "" @@ -209,13 +222,46 @@ def test_cli_semantics_out_writes_json_without_stdout(tmp_path: Path): def test_cli_semantics_without_json_output(): + """semantics prints the human summary by default and the record under --json.""" cmd = [sys.executable, "-m", "prik", "semantics", str(TEST_FILE)] res = subprocess.run(cmd, capture_output=True, text=True, check=True) + assert res.stdout.startswith(f"File: {TEST_FILE}") + assert "Semantic modules:" in res.stdout + + res = subprocess.run([*cmd, "--json"], capture_output=True, text=True, check=True) payload = json.loads(res.stdout) assert str(TEST_FILE) in payload assert "semantic_modules" in payload[str(TEST_FILE)] +@pytest.mark.parametrize( + ("command", "description"), + [ + (("semantics",), "semantics"), + (("generate", "--pyi"), "generate --pyi"), + ], +) +def test_cli_source_stage_rejects_pyi_contract_instead_of_printing_empty_output( + tmp_path: Path, + command: tuple[str, ...], + description: str, +): + contract = tmp_path / "contract.pyi" + contract.write_text("def add1(value: int) -> int: ...\n", encoding="utf-8") + + result = subprocess.run( + [sys.executable, "-m", "prik", *command, str(contract)], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 2 + assert result.stdout == "" + assert f"{description} expects recognized fortran source suffixes" in result.stderr + assert str(contract) in result.stderr + + def test_cli_pyi_output(): cmd = [sys.executable, "-m", "prik", "generate", "--pyi", str(TEST_FILE)] res = subprocess.run(cmd, capture_output=True, text=True, check=True) @@ -449,7 +495,7 @@ def test_prik_main_preserves_explicit_and_adjacent_json_write_contracts(monkeypa ) explicit_payload = {"input.f90": {"node": 1}} - explicit_args = _main_args(parse=True, out="/tmp/report.json") + explicit_args = _main_args(parse=True, json=True, out="/tmp/report.json") _install_main_parser(monkeypatch, explicit_args) _patch_main_report_payloads(monkeypatch, parse_payload=explicit_payload) assert prik_cli.main() == 0 @@ -458,7 +504,7 @@ def test_prik_main_preserves_explicit_and_adjacent_json_write_contracts(monkeypa "/tmp/first.f90": {"node": 1}, "/tmp/empty.f90": {}, } - adjacent_args = _main_args(parse=True, out="") + adjacent_args = _main_args(parse=True, json=True, out="") _install_main_parser(monkeypatch, adjacent_args) _patch_main_report_payloads(monkeypatch, parse_payload=adjacent_payload) assert prik_cli.main() == 0 @@ -478,7 +524,7 @@ def test_prik_main_preserves_stdout_mode_matrix(monkeypatch, capsys): parse_payload = {"parse": {"node": 1}} semantic_payload = {"semantic": {"node": 2}} scenarios = [ - ({"semantics": True}, json.dumps(semantic_payload, indent=2) + "\n", []), + ({"semantics": True}, "SEMANTIC\n", [("semantic-format", semantic_payload, {"print_limit": None})]), ({"parse": True, "json": True}, json.dumps(parse_payload, indent=2) + "\n", []), ({"pyi": True}, "", [("pyi-format", semantic_payload), ("pyi-output", "PYI")]), ( @@ -502,6 +548,13 @@ def test_prik_main_preserves_stdout_mode_matrix(monkeypatch, capsys): "_format_report", lambda payload, _formats=formats, **kwargs: _formats.append(("parse-format", payload, kwargs)) or "PARSE", ) + monkeypatch.setattr( + prik_cli, + "_format_semantic_report", + lambda payload, _formats=formats, **kwargs: ( + _formats.append(("semantic-format", payload, kwargs)) or "SEMANTIC" + ), + ) monkeypatch.setattr( prik_cli, "_format_pyi_report", @@ -650,9 +703,7 @@ def test_subcommand_help_tailors_shared_compiler_options(command, expected, excl def test_cli_parse_shows_module_derived_types_and_derived_arg_kinds(): - fixture = ( - Path(__file__).parents[2] / "source_parsing" / "parsing" / "fixtures" / "general" / "modern_pyi_example.f90" - ) + fixture = GENERAL_FORTRAN_DIR / "modern_pyi_example.f90" cmd = [sys.executable, "-m", "prik", "parse", str(fixture)] res = subprocess.run(cmd, capture_output=True, text=True, check=True) @@ -940,3 +991,38 @@ def fail_parse(_paths, _preprocessing): monkeypatch.setattr(sys, "argv", ["prik", "parse", str(source), "--debug"]) with pytest.raises(ValueError, match="invalid generated interface"): prik_cli.main() + + +ASSUMED_INTENT_SOURCE = """module legacy_mod +contains + real(8) function weigh(count, factor) + integer(4) :: count + real(8) :: factor + weigh = real(count, 8) * factor + end function weigh +end module legacy_mod +""" + + +def _generated_legacy_contract(tmp_path: Path, *extra_options: str) -> str: + source = tmp_path / f"legacy{len(extra_options)}.f90" + source.write_text(ASSUMED_INTENT_SOURCE, encoding="utf-8") + out = tmp_path / f"contracts{len(extra_options)}" + + cmd = [sys.executable, "-m", "prik", "generate", "--pyi", str(source), "--out", str(out), *extra_options] + subprocess.run(cmd, capture_output=True, text=True, check=True) + return (out / "legacy_mod.pyi").read_text(encoding="utf-8") + + +def test_generated_contract_projects_undeclared_scalars_by_default(tmp_path: Path): + text = _generated_legacy_contract(tmp_path) + + assert 'Returns["count", Int32]' in text + assert 'Returns["factor", Float64]' in text + + +def test_assume_intent_in_scalars_removes_them_from_the_generated_contract(tmp_path: Path): + text = _generated_legacy_contract(tmp_path, "--assume-intent-in-scalars") + + assert "Returns" not in text + assert "-> Float64: ..." in text diff --git a/tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py b/tests/fortran/infrastructure/cli/pipeline/test_stage_dispatch.py similarity index 88% rename from tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py rename to tests/fortran/infrastructure/cli/pipeline/test_stage_dispatch.py index 8e4049cdc..663946a5a 100644 --- a/tests/fortran/command_line_interface/pipeline/test_stage_dispatch.py +++ b/tests/fortran/infrastructure/cli/pipeline/test_stage_dispatch.py @@ -19,7 +19,8 @@ PreprocessingError, ) from prik.semantics.fortran2ir import collect_semantic_compile_time_requirements -from tests.fortran.command_line_interface.pipeline._support import ( +from tests.fortran._support.paths import GENERAL_FORTRAN_DIR +from tests.fortran.infrastructure.cli.pipeline._support import ( TEST_FILE, _install_main_parser, _main_args, @@ -572,9 +573,7 @@ def fail_parse(_paths, _preprocessing): def test_cli_parse_modern_fixture_prints_derived_block_verbatim(): - fixture = ( - Path(__file__).parents[2] / "source_parsing" / "parsing" / "fixtures" / "general" / "modern_pyi_example.f90" - ) + fixture = GENERAL_FORTRAN_DIR / "modern_pyi_example.f90" cmd = [sys.executable, "-m", "prik", "parse", str(fixture)] res = subprocess.run(cmd, capture_output=True, text=True, check=True) @@ -745,3 +744,68 @@ def test_prik_probe_subcommand_dispatches_one_flag_driven_probe(monkeypatch, cap assert calls[0].language == "fortran" assert calls[0].compiler == "gfortran-13" assert calls[0].expressions == ["storage_size(0)"] + + +def _probe_args(**overrides): + defaults = { + "language": "fortran", + "compiler": "gfortran", + "json": False, + "expressions": [], + "include_dirs": [], + "defines": [], + "undefs": [], + "std": None, + "compiler_args": [], + "runner": [], + "cache_dir": None, + "refresh": False, + } + return types.SimpleNamespace(**{**defaults, **overrides}) + + +@pytest.mark.parametrize("language", ["c", "fortran"]) +def test_probe_without_expressions_reports_the_measured_type_mapping(monkeypatch, language): + """Omitting --expr selects the mapping report rather than an empty measurement.""" + measured = {"report": "type_mapping", "language": language, "target_profile": "t", "types": []} + monkeypatch.setattr(prik_cli, "c_type_mapping_report", lambda **options: measured) + monkeypatch.setattr(prik_cli, "fortran_type_mapping_report", lambda **options: measured) + + assert json.loads(prik_cli._probe_output(_probe_args(language=language, json=True))) == measured + + +@pytest.mark.parametrize("as_json", [False, True]) +def test_probe_renders_each_report_in_both_formats(monkeypatch, as_json): + """--json selects a rendering; it must not select a different report.""" + measured = {"report": "type_mapping", "language": "fortran", "target_profile": "t", "types": []} + monkeypatch.setattr(prik_cli, "fortran_type_mapping_report", lambda **options: measured) + monkeypatch.setattr(prik_cli, "type_mapping_markdown", lambda report: f"MD:{report['language']}") + + output = prik_cli._probe_output(_probe_args(json=as_json)) + + assert output == (json.dumps(measured, indent=2) if as_json else "MD:fortran") + + +def test_probe_expressions_render_as_markdown(monkeypatch): + """--expr is a report selector, so its table is the default human rendering.""" + measured = object() + monkeypatch.setattr(prik_cli, "probe_fortran_type_expressions_cached", lambda *args, **options: measured) + monkeypatch.setattr(prik_cli, "expression_probe_markdown", lambda report: "EXPR-TABLE") + + output = prik_cli._probe_output(_probe_args(expressions=["kind(1.0d0)"])) + + assert output == "EXPR-TABLE" + + +@pytest.mark.parametrize( + "option", [{"include_dirs": ["inc"]}, {"defines": ["A=1"]}, {"undefs": ["A"]}, {"std": "f2018"}] +) +def test_probe_mapping_report_rejects_preprocessing_options(option): + """The mapping inventory is fixed, so preprocessing options cannot affect it.""" + with pytest.raises(ValueError, match="add --expr to probe preprocessed expressions"): + prik_cli._probe_output(_probe_args(**option)) + + +def test_probe_expressions_are_fortran_only(): + with pytest.raises(ValueError, match="--expr is supported only for --language fortran"): + prik_cli._probe_output(_probe_args(language="c", expressions=["kind(1.0)"])) diff --git a/tests/fortran/infrastructure/codegen/test_ordinary_fortran_codegen_baseline.py b/tests/fortran/infrastructure/codegen/test_ordinary_fortran_codegen_baseline.py index c1a00a47d..31e5bcd21 100644 --- a/tests/fortran/infrastructure/codegen/test_ordinary_fortran_codegen_baseline.py +++ b/tests/fortran/infrastructure/codegen/test_ordinary_fortran_codegen_baseline.py @@ -21,12 +21,12 @@ def test_ordinary_fortran_wrapper_preserves_exact_generated_bytes(): expected = { "bind_c_ordinary_entrypoint_baseline_wrapper.f90": ( - 740, - "cdda3f054ab348a128cfc31bb338fe0ec12277d41c607b209c8b401cc2a29004", + 843, + "01c092ac9eaa0d90b58f0289a49ba0c71c967510e60a384602fe2e6e1e9b035f", ), "ordinary_entrypoint_baseline_wrapper.c": ( - 1860, - "0401eb6eae8b2b3682a6f04986b8da1553fe77cf070fe4b981e642c7ed13c6d2", + 1941, + "9b944e6ebb8f5b1eef87407e046117b5d2b350286cc32917bb2f8182ab3bbb30", ), "ordinary_entrypoint_baseline_wrapper.h": ( 248, diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_argument_name.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_argument_name.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_argument_name.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_argument_name.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_argument_name.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_argument_name.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_argument_name.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_argument_name.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_declaration_procedure.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_declaration_procedure.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_declaration_procedure.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_declaration_procedure.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_declaration_procedure.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_declaration_procedure.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_declaration_procedure.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_declaration_procedure.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_field_derived_type.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_field_derived_type.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_field_derived_type.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_field_derived_type.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_field_derived_type.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_field_derived_type.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_field_derived_type.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_field_derived_type.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_parameter.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_parameter.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_parameter.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_parameter.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_parameter.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_parameter.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_parameter.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_parameter.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_procedure_global.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_procedure_global.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_procedure_global.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_procedure_global.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_procedure_global.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_procedure_global.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_procedure_global.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_procedure_global.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_procedure_module.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_procedure_module.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_procedure_module.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_procedure_module.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_procedure_module.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_procedure_module.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_procedure_module.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_procedure_module.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_variable_module.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_variable_module.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_variable_module.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_variable_module.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_variable_module.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_variable_module.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_duplicate_variable_module.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_duplicate_variable_module.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_implicit_none_undeclared_arg.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_implicit_none_undeclared_arg.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_implicit_none_undeclared_arg.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_implicit_none_undeclared_arg.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_implicit_none_undeclared_arg.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_implicit_none_undeclared_arg.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_implicit_none_undeclared_arg.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_implicit_none_undeclared_arg.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_implicit_none_undeclared_result.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_implicit_none_undeclared_result.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_implicit_none_undeclared_result.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_implicit_none_undeclared_result.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_implicit_none_undeclared_result.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_implicit_none_undeclared_result.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_implicit_none_undeclared_result.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_implicit_none_undeclared_result.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_parameter_without_type_implicit_none.f b/tests/fortran/infrastructure/parsing/fixtures/errors/err_parameter_without_type_implicit_none.f similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_parameter_without_type_implicit_none.f rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_parameter_without_type_implicit_none.f diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_parameter_without_type_implicit_none.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_parameter_without_type_implicit_none.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_parameter_without_type_implicit_none.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_parameter_without_type_implicit_none.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_result_shadows_argument.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_result_shadows_argument.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_result_shadows_argument.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_result_shadows_argument.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_result_shadows_argument.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_result_shadows_argument.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_result_shadows_argument.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_result_shadows_argument.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_function_result.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_function_result.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_function_result.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_function_result.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_function_result.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_function_result.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_function_result.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_function_result.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_derived_type.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_derived_type.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_derived_type.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_derived_type.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_derived_type.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_derived_type.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_derived_type.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_derived_type.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_module.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_module.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_module.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_module.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_module.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_module.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_module.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_module.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_procedure.f90 b/tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_procedure.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_procedure.f90 rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_procedure.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_procedure.json b/tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_procedure.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/errors/err_unknown_type_procedure.json rename to tests/fortran/infrastructure/parsing/fixtures/errors/err_unknown_type_procedure.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/assumed_shape_and_derived_args.f90 b/tests/fortran/infrastructure/parsing/fixtures/general/assumed_shape_and_derived_args.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/assumed_shape_and_derived_args.f90 rename to tests/fortran/infrastructure/parsing/fixtures/general/assumed_shape_and_derived_args.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/assumed_shape_and_derived_args.json b/tests/fortran/infrastructure/parsing/fixtures/general/assumed_shape_and_derived_args.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/assumed_shape_and_derived_args.json rename to tests/fortran/infrastructure/parsing/fixtures/general/assumed_shape_and_derived_args.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 b/tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.f90 rename to tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.json b/tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/basic_subroutine.json rename to tests/fortran/infrastructure/parsing/fixtures/general/basic_subroutine.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/compile_time_all_exprs.f90 b/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_all_exprs.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/compile_time_all_exprs.f90 rename to tests/fortran/infrastructure/parsing/fixtures/general/compile_time_all_exprs.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/compile_time_all_exprs.json b/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_all_exprs.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/compile_time_all_exprs.json rename to tests/fortran/infrastructure/parsing/fixtures/general/compile_time_all_exprs.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/compile_time_shape_exprs.f90 b/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_shape_exprs.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/compile_time_shape_exprs.f90 rename to tests/fortran/infrastructure/parsing/fixtures/general/compile_time_shape_exprs.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/compile_time_shape_exprs.json b/tests/fortran/infrastructure/parsing/fixtures/general/compile_time_shape_exprs.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/compile_time_shape_exprs.json rename to tests/fortran/infrastructure/parsing/fixtures/general/compile_time_shape_exprs.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/derived_type.f90 b/tests/fortran/infrastructure/parsing/fixtures/general/derived_type.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/derived_type.f90 rename to tests/fortran/infrastructure/parsing/fixtures/general/derived_type.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/derived_type.json b/tests/fortran/infrastructure/parsing/fixtures/general/derived_type.json similarity index 93% rename from tests/fortran/source_parsing/parsing/fixtures/general/derived_type.json rename to tests/fortran/infrastructure/parsing/fixtures/general/derived_type.json index aa21b048e..eeabfd7f2 100644 --- a/tests/fortran/source_parsing/parsing/fixtures/general/derived_type.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/derived_type.json @@ -110,14 +110,18 @@ "procedure_bindings": [ { "name": "move", - "attrs": [] + "attrs": [], + "visibility": "public" }, { "name": "reset", - "attrs": [] + "attrs": [], + "visibility": "public" } ], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" } ], "interfaces": [], @@ -244,14 +248,18 @@ "procedure_bindings": [ { "name": "move", - "attrs": [] + "attrs": [], + "visibility": "public" }, { "name": "reset", - "attrs": [] + "attrs": [], + "visibility": "public" } ], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" } ], "interfaces": [], diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/derived_types_and_methods.f90 b/tests/fortran/infrastructure/parsing/fixtures/general/derived_types_and_methods.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/derived_types_and_methods.f90 rename to tests/fortran/infrastructure/parsing/fixtures/general/derived_types_and_methods.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/derived_types_and_methods.json b/tests/fortran/infrastructure/parsing/fixtures/general/derived_types_and_methods.json similarity index 90% rename from tests/fortran/source_parsing/parsing/fixtures/general/derived_types_and_methods.json rename to tests/fortran/infrastructure/parsing/fixtures/general/derived_types_and_methods.json index 4ff760d38..d886875fc 100644 --- a/tests/fortran/source_parsing/parsing/fixtures/general/derived_types_and_methods.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/derived_types_and_methods.json @@ -73,10 +73,13 @@ "procedure_bindings": [ { "name": "move", - "attrs": [] + "attrs": [], + "visibility": "public" } ], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" }, { "name": "mesh", @@ -141,14 +144,18 @@ "procedure_bindings": [ { "name": "init", - "attrs": [] + "attrs": [], + "visibility": "public" }, { "name": "clear", - "attrs": [] + "attrs": [], + "visibility": "public" } ], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" } ], "interfaces": [], @@ -238,10 +245,13 @@ "procedure_bindings": [ { "name": "move", - "attrs": [] + "attrs": [], + "visibility": "public" } ], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" }, { "name": "mesh", @@ -306,14 +316,18 @@ "procedure_bindings": [ { "name": "init", - "attrs": [] + "attrs": [], + "visibility": "public" }, { "name": "clear", - "attrs": [] + "attrs": [], + "visibility": "public" } ], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" } ], "interfaces": [], diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/f77_subroutine.f b/tests/fortran/infrastructure/parsing/fixtures/general/f77_subroutine.f similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/f77_subroutine.f rename to tests/fortran/infrastructure/parsing/fixtures/general/f77_subroutine.f diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/f77_subroutine.json b/tests/fortran/infrastructure/parsing/fixtures/general/f77_subroutine.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/f77_subroutine.json rename to tests/fortran/infrastructure/parsing/fixtures/general/f77_subroutine.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/modern_pyi_example.f90 b/tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/modern_pyi_example.f90 rename to tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/modern_pyi_example.json b/tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.json similarity index 98% rename from tests/fortran/source_parsing/parsing/fixtures/general/modern_pyi_example.json rename to tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.json index 4b99cbcd5..72d349fbc 100644 --- a/tests/fortran/source_parsing/parsing/fixtures/general/modern_pyi_example.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/modern_pyi_example.json @@ -656,7 +656,9 @@ "extends": null, "attributes": [], "procedure_bindings": [], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" }, { "name": "vector3", @@ -695,7 +697,9 @@ "extends": null, "attributes": [], "procedure_bindings": [], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" }, { "name": "hidden_state", @@ -728,7 +732,9 @@ "extends": null, "attributes": [], "procedure_bindings": [], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" } ], "interfaces": [], @@ -1411,7 +1417,9 @@ "extends": null, "attributes": [], "procedure_bindings": [], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" }, { "name": "vector3", @@ -1450,7 +1458,9 @@ "extends": null, "attributes": [], "procedure_bindings": [], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" }, { "name": "hidden_state", @@ -1483,7 +1493,9 @@ "extends": null, "attributes": [], "procedure_bindings": [], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" } ], "interfaces": [], diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/module_vars_use.f90 b/tests/fortran/infrastructure/parsing/fixtures/general/module_vars_use.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/module_vars_use.f90 rename to tests/fortran/infrastructure/parsing/fixtures/general/module_vars_use.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/module_vars_use.json b/tests/fortran/infrastructure/parsing/fixtures/general/module_vars_use.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/module_vars_use.json rename to tests/fortran/infrastructure/parsing/fixtures/general/module_vars_use.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/procedures_and_functions.f90 b/tests/fortran/infrastructure/parsing/fixtures/general/procedures_and_functions.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/procedures_and_functions.f90 rename to tests/fortran/infrastructure/parsing/fixtures/general/procedures_and_functions.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/procedures_and_functions.json b/tests/fortran/infrastructure/parsing/fixtures/general/procedures_and_functions.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/procedures_and_functions.json rename to tests/fortran/infrastructure/parsing/fixtures/general/procedures_and_functions.json diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/scope_name_reuse_combinations.f90 b/tests/fortran/infrastructure/parsing/fixtures/general/scope_name_reuse_combinations.f90 similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/general/scope_name_reuse_combinations.f90 rename to tests/fortran/infrastructure/parsing/fixtures/general/scope_name_reuse_combinations.f90 diff --git a/tests/fortran/source_parsing/parsing/fixtures/general/scope_name_reuse_combinations.json b/tests/fortran/infrastructure/parsing/fixtures/general/scope_name_reuse_combinations.json similarity index 99% rename from tests/fortran/source_parsing/parsing/fixtures/general/scope_name_reuse_combinations.json rename to tests/fortran/infrastructure/parsing/fixtures/general/scope_name_reuse_combinations.json index 799728509..4b3cfcfeb 100644 --- a/tests/fortran/source_parsing/parsing/fixtures/general/scope_name_reuse_combinations.json +++ b/tests/fortran/infrastructure/parsing/fixtures/general/scope_name_reuse_combinations.json @@ -489,7 +489,9 @@ "extends": null, "attributes": [], "procedure_bindings": [], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" } ], "interfaces": [ @@ -1007,7 +1009,9 @@ "extends": null, "attributes": [], "procedure_bindings": [], - "generic_bindings": [] + "generic_bindings": [], + "component_visibility": "public", + "binding_visibility": "public" } ], "interfaces": [ diff --git a/tests/fortran/source_parsing/parsing/fixtures/json_sanity_allowlist.json b/tests/fortran/infrastructure/parsing/fixtures/json_sanity_allowlist.json similarity index 100% rename from tests/fortran/source_parsing/parsing/fixtures/json_sanity_allowlist.json rename to tests/fortran/infrastructure/parsing/fixtures/json_sanity_allowlist.json diff --git a/tests/fortran/source_parsing/parsing/generate_error_goldens.py b/tests/fortran/infrastructure/parsing/generate_error_goldens.py similarity index 100% rename from tests/fortran/source_parsing/parsing/generate_error_goldens.py rename to tests/fortran/infrastructure/parsing/generate_error_goldens.py diff --git a/tests/fortran/source_parsing/parsing/generate_parser_goldens.py b/tests/fortran/infrastructure/parsing/generate_parser_goldens.py similarity index 100% rename from tests/fortran/source_parsing/parsing/generate_parser_goldens.py rename to tests/fortran/infrastructure/parsing/generate_parser_goldens.py diff --git a/tests/fortran/source_parsing/parsing/test_declaration_and_interface_edges.py b/tests/fortran/infrastructure/parsing/test_declaration_and_interface_edges.py similarity index 100% rename from tests/fortran/source_parsing/parsing/test_declaration_and_interface_edges.py rename to tests/fortran/infrastructure/parsing/test_declaration_and_interface_edges.py diff --git a/tests/fortran/source_parsing/parsing/test_declaration_and_scope_regressions.py b/tests/fortran/infrastructure/parsing/test_declaration_and_scope_regressions.py similarity index 99% rename from tests/fortran/source_parsing/parsing/test_declaration_and_scope_regressions.py rename to tests/fortran/infrastructure/parsing/test_declaration_and_scope_regressions.py index d5d9bbb66..0c4873584 100644 --- a/tests/fortran/source_parsing/parsing/test_declaration_and_scope_regressions.py +++ b/tests/fortran/infrastructure/parsing/test_declaration_and_scope_regressions.py @@ -646,14 +646,15 @@ def test_scope_include_import_and_derived_type_binding_contracts(): assert dtype.methods == ["update", "reset"] assert dtype.procedure_bindings == [ - {"name": "update", "attrs": ["pass(self)", "public"]}, - {"name": "reset", "attrs": ["pass(self)", "public"]}, + {"name": "update", "attrs": ["pass(self)", "public"], "visibility": "public"}, + {"name": "reset", "attrs": ["pass(self)", "public"], "visibility": "public"}, ] assert dtype.generic_bindings == [ { "name": "assignment(=)", "targets": ["assign_child", "assign_other"], "attrs": ["public"], + "visibility": "public", } ] diff --git a/tests/fortran/source_parsing/parsing/test_derived_types_and_program_units.py b/tests/fortran/infrastructure/parsing/test_derived_types_and_program_units.py similarity index 69% rename from tests/fortran/source_parsing/parsing/test_derived_types_and_program_units.py rename to tests/fortran/infrastructure/parsing/test_derived_types_and_program_units.py index c7844d0de..1c6a5bfe1 100644 --- a/tests/fortran/source_parsing/parsing/test_derived_types_and_program_units.py +++ b/tests/fortran/infrastructure/parsing/test_derived_types_and_program_units.py @@ -258,3 +258,93 @@ def test_singular_parse_entrypoint_rejects_ambiguous_sources(): end subroutine second """) assert len(parsed.procedures) == 2 + + +def test_type_accessibility_statements_set_component_and_binding_defaults(): + """A type's `private` statement is a default, not an unsupported declaration. + + The statement before `contains` sets component accessibility; the statement + after it sets type-bound accessibility. Each declaration that states its own + accessibility keeps it. + """ + module = parse_fortran_module( + """ +module access_mod + implicit none + type,public :: t + private + integer :: hidden = 0 + integer,public :: shown = 0 + contains + private + procedure :: internal_step + procedure,public :: step => internal_step + end type t +contains + subroutine internal_step(self) + class(t),intent(inout) :: self + end subroutine internal_step +end module access_mod +""" + ) + + dtype = module.derived_types[0] + assert dtype.component_visibility == "private" + assert dtype.binding_visibility == "private" + assert {field.name: field.visibility for field in dtype.fields} == { + "hidden": "private", + "shown": "public", + } + assert [(binding["name"], binding["visibility"]) for binding in dtype.procedure_bindings] == [ + ("internal_step", "private"), + ("step => internal_step", "public"), + ] + + +def test_deferred_type_bound_binding_records_its_declaring_interface(): + """A deferred binding parses; whether it can be wrapped belongs to policy.""" + module = parse_fortran_module( + """ +module deferred_mod + implicit none + type,public,abstract :: base + contains + procedure(size_func),deferred,public :: size_of + end type base + abstract interface + pure function size_func(self) result(s) + import :: base + class(base),intent(in) :: self + integer :: s + end function size_func + end interface +end module deferred_mod +""" + ) + + binding = module.derived_types[0].procedure_bindings[0] + assert binding["name"] == "size_of" + assert binding["interface"] == "size_func" + assert "deferred" in binding["attrs"] + + +def test_named_block_construct_starts_the_execution_part(): + """`name: block` is an executable construct, not a declaration.""" + module = parse_fortran_module( + """ +module block_mod + implicit none +contains + subroutine scale_value(x) + real(8),intent(inout) :: x + main: block + real(8) :: factor + factor = 2.0d0 + x = x * factor + end block main + end subroutine scale_value +end module block_mod +""" + ) + + assert [procedure.name for procedure in module.procedures] == ["scale_value"] diff --git a/tests/fortran/source_parsing/parsing/test_developer_tutorial.py b/tests/fortran/infrastructure/parsing/test_developer_tutorial.py similarity index 100% rename from tests/fortran/source_parsing/parsing/test_developer_tutorial.py rename to tests/fortran/infrastructure/parsing/test_developer_tutorial.py diff --git a/tests/fortran/source_parsing/parsing/test_error_fixture_suite.py b/tests/fortran/infrastructure/parsing/test_error_fixture_suite.py similarity index 100% rename from tests/fortran/source_parsing/parsing/test_error_fixture_suite.py rename to tests/fortran/infrastructure/parsing/test_error_fixture_suite.py diff --git a/tests/fortran/source_parsing/parsing/test_error_handling.py b/tests/fortran/infrastructure/parsing/test_error_handling.py similarity index 100% rename from tests/fortran/source_parsing/parsing/test_error_handling.py rename to tests/fortran/infrastructure/parsing/test_error_handling.py diff --git a/tests/fortran/source_parsing/parsing/test_fortran_fixture_suite.py b/tests/fortran/infrastructure/parsing/test_fortran_fixture_suite.py similarity index 100% rename from tests/fortran/source_parsing/parsing/test_fortran_fixture_suite.py rename to tests/fortran/infrastructure/parsing/test_fortran_fixture_suite.py diff --git a/tests/fortran/source_parsing/parsing/test_fortran_parser_procedures_and_interfaces.py b/tests/fortran/infrastructure/parsing/test_fortran_parser_procedures_and_interfaces.py similarity index 100% rename from tests/fortran/source_parsing/parsing/test_fortran_parser_procedures_and_interfaces.py rename to tests/fortran/infrastructure/parsing/test_fortran_parser_procedures_and_interfaces.py diff --git a/tests/fortran/source_parsing/parsing/test_fortran_parser_properties.py b/tests/fortran/infrastructure/parsing/test_fortran_parser_properties.py similarity index 100% rename from tests/fortran/source_parsing/parsing/test_fortran_parser_properties.py rename to tests/fortran/infrastructure/parsing/test_fortran_parser_properties.py diff --git a/tests/fortran/source_parsing/parsing/test_json_sanity.py b/tests/fortran/infrastructure/parsing/test_json_sanity.py similarity index 100% rename from tests/fortran/source_parsing/parsing/test_json_sanity.py rename to tests/fortran/infrastructure/parsing/test_json_sanity.py diff --git a/tests/fortran/source_parsing/parsing/test_parser_benchmarks.py b/tests/fortran/infrastructure/parsing/test_parser_benchmarks.py similarity index 90% rename from tests/fortran/source_parsing/parsing/test_parser_benchmarks.py rename to tests/fortran/infrastructure/parsing/test_parser_benchmarks.py index 734cfd0bd..05f754996 100644 --- a/tests/fortran/source_parsing/parsing/test_parser_benchmarks.py +++ b/tests/fortran/infrastructure/parsing/test_parser_benchmarks.py @@ -2,13 +2,13 @@ from __future__ import annotations -from pathlib import Path import pytest from prik.semantics.fortran2ir import fortran_file_to_semantic_modules from prik.pipeline.pyi import emit_module_stubs from prik.parsers.fortran import parse_fortran_file +from tests.fortran._support.paths import REPO_ROOT pytestmark = pytest.mark.skip(reason="Benchmarks are parked until benchmark adoption resumes.") @@ -37,7 +37,7 @@ def test_parse_convert_emit_representative_fortran_module(benchmark): @pytest.mark.benchmark def test_parse_real_lapack_dgesv(benchmark): - source = (Path(__file__).resolve().parents[4] / "examples" / "lapack" / "native" / "dgesv.f").read_text( + source = (REPO_ROOT / "examples" / "lapack" / "native" / "dgesv.f").read_text( encoding="utf-8", ) parsed = benchmark(parse_fortran_file, source, filename="lapack/dgesv.f") diff --git a/tests/fortran/source_parsing/parsing/test_public_entrypoints.py b/tests/fortran/infrastructure/parsing/test_public_entrypoints.py similarity index 100% rename from tests/fortran/source_parsing/parsing/test_public_entrypoints.py rename to tests/fortran/infrastructure/parsing/test_public_entrypoints.py diff --git a/tests/fortran/source_parsing/parsing/test_real_world_interaction_regressions.py b/tests/fortran/infrastructure/parsing/test_real_world_interaction_regressions.py similarity index 100% rename from tests/fortran/source_parsing/parsing/test_real_world_interaction_regressions.py rename to tests/fortran/infrastructure/parsing/test_real_world_interaction_regressions.py diff --git a/tests/fortran/source_parsing/parsing/test_source_form_and_diagnostics_regressions.py b/tests/fortran/infrastructure/parsing/test_source_form_and_diagnostics_regressions.py similarity index 100% rename from tests/fortran/source_parsing/parsing/test_source_form_and_diagnostics_regressions.py rename to tests/fortran/infrastructure/parsing/test_source_form_and_diagnostics_regressions.py diff --git a/tests/fortran/infrastructure/semantics/test_native_array_handles.py b/tests/fortran/infrastructure/policy/test_native_array_handles.py similarity index 100% rename from tests/fortran/infrastructure/semantics/test_native_array_handles.py rename to tests/fortran/infrastructure/policy/test_native_array_handles.py diff --git a/tests/fortran/infrastructure/semantics/test_ownership.py b/tests/fortran/infrastructure/policy/test_ownership.py similarity index 100% rename from tests/fortran/infrastructure/semantics/test_ownership.py rename to tests/fortran/infrastructure/policy/test_ownership.py diff --git a/tests/fortran/infrastructure/semantics/test_policy_completion.py b/tests/fortran/infrastructure/policy/test_policy_completion.py similarity index 100% rename from tests/fortran/infrastructure/semantics/test_policy_completion.py rename to tests/fortran/infrastructure/policy/test_policy_completion.py diff --git a/tests/fortran/infrastructure/semantics/test_wrapper_policy.py b/tests/fortran/infrastructure/policy/test_wrapper_policy.py similarity index 99% rename from tests/fortran/infrastructure/semantics/test_wrapper_policy.py rename to tests/fortran/infrastructure/policy/test_wrapper_policy.py index b695a8d78..4f925e32e 100644 --- a/tests/fortran/infrastructure/semantics/test_wrapper_policy.py +++ b/tests/fortran/infrastructure/policy/test_wrapper_policy.py @@ -316,8 +316,8 @@ def test_runtime_status_policy_is_completed_before_wrapper_planning(): module = parse_pyi_text( """ @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]]: ... +@native_call([Addr(Arg(0)), Hidden("status", Int32), Hidden("message", String[32])]) +def solve(value: Int32) -> None: ... """, module_name="runtime_status", ) @@ -558,10 +558,10 @@ def optional_fixed(label: String[8] = ...) -> Returns["label", String[8]] | None def optional_identity(label: String = ...) -> None: ... @raises(status="status", success=0) -@native_call([Arg(0), Return("status", 1)]) +@native_call([Arg(0), Hidden("status", Int32)]) def with_status( name: String[8] -) -> tuple[Returns["name", String[8]], Returns["status", Int32]]: ... +) -> Returns["name", String[8]]: ... """, module_name="blocked_string_writeback", ) diff --git a/tests/fortran/source_preprocessing/preprocessing/_support.py b/tests/fortran/infrastructure/preprocessing/_support.py similarity index 100% rename from tests/fortran/source_preprocessing/preprocessing/_support.py rename to tests/fortran/infrastructure/preprocessing/_support.py diff --git a/tests/fortran/source_preprocessing/preprocessing/test_cli.py b/tests/fortran/infrastructure/preprocessing/test_cli.py similarity index 98% rename from tests/fortran/source_preprocessing/preprocessing/test_cli.py rename to tests/fortran/infrastructure/preprocessing/test_cli.py index 2ce3ce4cf..16abb1eef 100644 --- a/tests/fortran/source_preprocessing/preprocessing/test_cli.py +++ b/tests/fortran/infrastructure/preprocessing/test_cli.py @@ -5,7 +5,7 @@ import subprocess import sys -from tests.fortran.source_preprocessing.preprocessing._support import _fake_compiler +from tests.fortran.infrastructure.preprocessing._support import _fake_compiler def test_cli_help_documents_exact_compiler_and_preprocessing_examples(): diff --git a/tests/fortran/source_preprocessing/preprocessing/test_configuration_and_adapters.py b/tests/fortran/infrastructure/preprocessing/test_configuration_and_adapters.py similarity index 99% rename from tests/fortran/source_preprocessing/preprocessing/test_configuration_and_adapters.py rename to tests/fortran/infrastructure/preprocessing/test_configuration_and_adapters.py index 7a26d31c8..99448152f 100644 --- a/tests/fortran/source_preprocessing/preprocessing/test_configuration_and_adapters.py +++ b/tests/fortran/infrastructure/preprocessing/test_configuration_and_adapters.py @@ -15,7 +15,7 @@ run_compiler_preprocessor_with_recipe, validate_macro_name, ) -from tests.fortran.source_preprocessing.preprocessing._support import _assert_preprocessing_error +from tests.fortran.infrastructure.preprocessing._support import _assert_preprocessing_error def test_direct_fortran_preprocess_invocation_uses_exact_compiler_and_cpp(tmp_path: Path): diff --git a/tests/fortran/source_preprocessing/preprocessing/test_dependencies_and_includes.py b/tests/fortran/infrastructure/preprocessing/test_dependencies_and_includes.py similarity index 100% rename from tests/fortran/source_preprocessing/preprocessing/test_dependencies_and_includes.py rename to tests/fortran/infrastructure/preprocessing/test_dependencies_and_includes.py diff --git a/tests/fortran/source_preprocessing/preprocessing/test_execution.py b/tests/fortran/infrastructure/preprocessing/test_execution.py similarity index 100% rename from tests/fortran/source_preprocessing/preprocessing/test_execution.py rename to tests/fortran/infrastructure/preprocessing/test_execution.py diff --git a/tests/fortran/source_preprocessing/preprocessing/test_parser_boundaries.py b/tests/fortran/infrastructure/preprocessing/test_parser_boundaries.py similarity index 100% rename from tests/fortran/source_preprocessing/preprocessing/test_parser_boundaries.py rename to tests/fortran/infrastructure/preprocessing/test_parser_boundaries.py diff --git a/tests/fortran/source_preprocessing/preprocessing/test_preprocessing_properties.py b/tests/fortran/infrastructure/preprocessing/test_preprocessing_properties.py similarity index 100% rename from tests/fortran/source_preprocessing/preprocessing/test_preprocessing_properties.py rename to tests/fortran/infrastructure/preprocessing/test_preprocessing_properties.py diff --git a/tests/fortran/infrastructure/printers/test_source_printers.py b/tests/fortran/infrastructure/printers/test_source_printers.py index 484f8c7aa..ad9c12921 100644 --- a/tests/fortran/infrastructure/printers/test_source_printers.py +++ b/tests/fortran/infrastructure/printers/test_source_printers.py @@ -15,8 +15,10 @@ CExpressionStatement, CFunction, CFunctionPrototype, + CGoto, CHeader, CInclude, + CLabel, CModule, CParameter, CReturn, @@ -104,6 +106,23 @@ def test_source_printers_render_complete_c_header_and_fortran_modules(): assert "real(c_double), value :: x" in fortran_source +def test_c_source_printer_renders_function_local_cleanup_jumps(): + function = CFunction( + name="wrap_outputs", + return_type="PyObject *", + body=( + CGoto("prik_output_cleanup_1"), + CLabel("prik_output_cleanup_1"), + CReturn(CodeExpression("NULL")), + ), + ) + + source = CSourcePrinter().doprint(function) + + assert "goto prik_output_cleanup_1;" in source + assert "prik_output_cleanup_1:" in source + + def test_source_printers_reject_wrapper_plan_models(): plan = ModulePlan( owner_path="demo", diff --git a/tests/fortran/infrastructure/runtime/test_native_support.py b/tests/fortran/infrastructure/runtime/test_native_support.py index 105385826..ce00489fc 100644 --- a/tests/fortran/infrastructure/runtime/test_native_support.py +++ b/tests/fortran/infrastructure/runtime/test_native_support.py @@ -1,11 +1,10 @@ """Public native-binding support surface checks.""" -from pathlib import Path +from tests.fortran._support.paths import REPO_ROOT -ROOT = Path(__file__).resolve().parents[4] -SUPPORT_HEADER = ROOT / "prik" / "runtime" / "native_support" / "prik_binding.h" -SUPPORT_SOURCE = ROOT / "prik" / "runtime" / "native_support" / "prik_binding.c" +SUPPORT_HEADER = REPO_ROOT / "prik" / "runtime" / "native_support" / "prik_binding.h" +SUPPORT_SOURCE = REPO_ROOT / "prik" / "runtime" / "native_support" / "prik_binding.c" def test_native_binding_support_is_header_only_and_exposes_the_small_prik_api(): @@ -29,6 +28,9 @@ def test_native_binding_support_is_header_only_and_exposes_the_small_prik_api(): assert name in header assert "PRIK_NO_INLINE static int prik_array_actual_unpack(" in header assert "static inline int prik_array_validate(" in header + assert "static inline int prik_array_validate_ndarray(" in header + assert "PyArrayObject *array," in header + assert header.count("PyArray_Check(value)") == 1 assert "PRIK_ARRAY_LAYOUT_POSITIVE_STRIDED_F" in header assert "prik_array_actual" in header diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/assumed_shape_and_derived_args.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/assumed_shape_and_derived_args.json similarity index 100% rename from tests/fortran/semantic_ir/semantics/fixtures/general/expected/assumed_shape_and_derived_args.json rename to tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/assumed_shape_and_derived_args.json diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json similarity index 98% rename from tests/fortran/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json rename to tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json index 7ae154940..03c228866 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json @@ -203,7 +203,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "x", @@ -212,7 +213,8 @@ "python_position": 1, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json similarity index 98% rename from tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json rename to tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json index d9af79e23..6211c3531 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json @@ -1020,7 +1020,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "x2", @@ -1029,7 +1030,8 @@ "python_position": 1, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "x3", @@ -1038,7 +1040,8 @@ "python_position": 2, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "x4", @@ -1047,7 +1050,8 @@ "python_position": 3, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "x5", @@ -1056,7 +1060,8 @@ "python_position": 4, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "x6", @@ -1065,7 +1070,8 @@ "python_position": 5, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "x7", @@ -1074,7 +1080,8 @@ "python_position": 6, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "x8", @@ -1083,7 +1090,8 @@ "python_position": 7, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "x9", @@ -1092,7 +1100,8 @@ "python_position": 8, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json similarity index 99% rename from tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json rename to tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json index d97881abc..068228eae 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json @@ -243,7 +243,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "y", @@ -252,7 +253,8 @@ "python_position": 1, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_type.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_type.json similarity index 99% rename from tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_type.json rename to tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_type.json index f565e265d..ee400ed05 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_type.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_type.json @@ -94,7 +94,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json similarity index 100% rename from tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json rename to tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/f77_subroutine.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/f77_subroutine.json similarity index 100% rename from tests/fortran/semantic_ir/semantics/fixtures/general/expected/f77_subroutine.json rename to tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/f77_subroutine.json diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json similarity index 98% rename from tests/fortran/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json rename to tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json index ec9b53f6e..140c7f48a 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json @@ -469,7 +469,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "pid", @@ -478,7 +479,8 @@ "python_position": 1, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "mass", @@ -487,7 +489,8 @@ "python_position": 2, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "x", @@ -496,7 +499,8 @@ "python_position": 3, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "y", @@ -505,7 +509,8 @@ "python_position": 4, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "z", @@ -514,7 +519,8 @@ "python_position": 5, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -883,7 +889,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "vx", @@ -892,7 +899,8 @@ "python_position": 1, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "vy", @@ -901,7 +909,8 @@ "python_position": 2, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "vz", @@ -910,7 +919,8 @@ "python_position": 3, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -1127,7 +1137,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "alpha", @@ -1136,7 +1147,8 @@ "python_position": 1, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -1423,7 +1435,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "b", @@ -1432,7 +1445,8 @@ "python_position": 1, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -1584,7 +1598,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -1692,7 +1707,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -1800,7 +1816,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json similarity index 100% rename from tests/fortran/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json rename to tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json similarity index 98% rename from tests/fortran/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json rename to tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json index 328d8afa3..6676d036f 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json @@ -164,7 +164,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -381,7 +382,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null }, { "python_name": "x", @@ -390,7 +392,8 @@ "python_position": 1, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json similarity index 98% rename from tests/fortran/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json rename to tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json index f02813e59..0d4897289 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json +++ b/tests/fortran/infrastructure/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json @@ -96,7 +96,8 @@ "python_position": 0, "result_position": 0, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -204,7 +205,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -312,7 +314,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -422,7 +425,8 @@ "python_position": 0, "result_position": 0, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -532,7 +536,8 @@ "python_position": 0, "result_position": 0, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -676,7 +681,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -822,7 +828,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -968,7 +975,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": {}, @@ -1084,7 +1092,8 @@ "python_position": 0, "result_position": 0, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": { @@ -1196,7 +1205,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": { @@ -1308,7 +1318,8 @@ "python_position": 0, "result_position": null, "value_kind": "", - "value": null + "value": null, + "native_cast": null } ], "metadata": { diff --git a/tests/fortran/semantic_ir/semantics/generate_semantic_fixtures.py b/tests/fortran/infrastructure/semantic_ir/semantics/generate_semantic_fixtures.py similarity index 100% rename from tests/fortran/semantic_ir/semantics/generate_semantic_fixtures.py rename to tests/fortran/infrastructure/semantic_ir/semantics/generate_semantic_fixtures.py diff --git a/tests/fortran/semantic_ir/semantics/test_compile_time_values.py b/tests/fortran/infrastructure/semantic_ir/semantics/test_compile_time_values.py similarity index 100% rename from tests/fortran/semantic_ir/semantics/test_compile_time_values.py rename to tests/fortran/infrastructure/semantic_ir/semantics/test_compile_time_values.py diff --git a/tests/fortran/semantic_ir/semantics/test_fortran_conversion_properties.py b/tests/fortran/infrastructure/semantic_ir/semantics/test_fortran_conversion_properties.py similarity index 100% rename from tests/fortran/semantic_ir/semantics/test_fortran_conversion_properties.py rename to tests/fortran/infrastructure/semantic_ir/semantics/test_fortran_conversion_properties.py diff --git a/tests/fortran/semantic_ir/semantics/test_semantic_conversion_smoke.py b/tests/fortran/infrastructure/semantic_ir/semantics/test_semantic_conversion_smoke.py similarity index 93% rename from tests/fortran/semantic_ir/semantics/test_semantic_conversion_smoke.py rename to tests/fortran/infrastructure/semantic_ir/semantics/test_semantic_conversion_smoke.py index 97c805ac8..9c2843871 100644 --- a/tests/fortran/semantic_ir/semantics/test_semantic_conversion_smoke.py +++ b/tests/fortran/infrastructure/semantic_ir/semantics/test_semantic_conversion_smoke.py @@ -3,10 +3,8 @@ import pytest -from tests.fortran._support.fixture_outputs import ( - PARSER_FIXTURE_ROOT as TESTS_DIR, - parse_fixture, -) +from tests.fortran._support.fixture_outputs import parse_fixture +from tests.fortran._support.paths import PARSER_FIXTURE_ROOT as TESTS_DIR from tests.fortran._support.fixture_conversion import FORTRAN_FIXTURES from tests.fortran._support.fixture_outputs import ( SEMANTICS_FIXTURE_DIR, diff --git a/tests/fortran/semantic_ir/semantics/test_semantic_specialization_properties.py b/tests/fortran/infrastructure/semantic_ir/semantics/test_semantic_specialization_properties.py similarity index 100% rename from tests/fortran/semantic_ir/semantics/test_semantic_specialization_properties.py rename to tests/fortran/infrastructure/semantic_ir/semantics/test_semantic_specialization_properties.py diff --git a/tests/fortran/semantic_pyi_format/README.md b/tests/fortran/infrastructure/semantic_pyi/README.md similarity index 89% rename from tests/fortran/semantic_pyi_format/README.md rename to tests/fortran/infrastructure/semantic_pyi/README.md index eaabb530c..3837a3b71 100644 --- a/tests/fortran/semantic_pyi_format/README.md +++ b/tests/fortran/infrastructure/semantic_pyi/README.md @@ -24,7 +24,7 @@ and call/result behavior remains owned by the three later Run the feature with: ```bash -python3 -m pytest -q tests/fortran/semantic_pyi_format +python3 -m pytest -q tests/fortran/infrastructure/semantic_pyi ``` Refresh the reviewed contract packages only after reviewing a deliberate @@ -32,5 +32,5 @@ format change: ```bash WRAPPER_UPDATE_PYI_FIXTURES=1 python3 -m pytest -q \ - tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py + tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py ``` diff --git a/tests/fortran/pyi_contracts/calls_and_results/README.md b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/README.md similarity index 93% rename from tests/fortran/pyi_contracts/calls_and_results/README.md rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/README.md index e8963fb2a..78e36078b 100644 --- a/tests/fortran/pyi_contracts/calls_and_results/README.md +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/README.md @@ -26,5 +26,5 @@ owners. Run the focused feature with: ```bash -python3 -m pytest -q tests/fortran/pyi_contracts/calls_and_results +python3 -m pytest -q tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results ``` diff --git a/tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/codegen/test_call_and_result_lowering.py similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/codegen/test_call_and_result_lowering.py diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/hidden_array_output/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/hidden_array_output/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/hidden_array_output/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/hidden_array_output/__init__.pyi diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/hidden_array_output/foutputs_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/hidden_array_output/foutputs_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/hidden_array_output/foutputs_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/hidden_array_output/foutputs_f90.pyi diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/immutable_replacements/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/immutable_replacements/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/immutable_replacements/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/immutable_replacements/__init__.pyi diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/immutable_replacements/fnative_call_examples_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/immutable_replacements/fnative_call_examples_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/immutable_replacements/fnative_call_examples_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/immutable_replacements/fnative_call_examples_f90.pyi diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/native_order/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/native_order/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/native_order/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/native_order/__init__.pyi diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/native_order/fnative_call_examples_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/native_order/fnative_call_examples_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/native_order/fnative_call_examples_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/native_order/fnative_call_examples_f90.pyi diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/projected_results/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/projected_results/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/projected_results/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/projected_results/__init__.pyi diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/projected_results/fnative_call_examples_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/projected_results/fnative_call_examples_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/edited_contracts/projected_results/fnative_call_examples_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/edited_contracts/projected_results/fnative_call_examples_f90.pyi diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/native/fnative_call_examples_f90.f90 b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/native/fnative_call_examples_f90.f90 similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/native/fnative_call_examples_f90.f90 rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/native/fnative_call_examples_f90.f90 diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/native/foutputs_f90.f90 b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/native/foutputs_f90.f90 similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/fixtures/native/foutputs_f90.f90 rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/fixtures/native/foutputs_f90.f90 diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/test_edited_call_surfaces.py diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_projected_entrypoint_routes.py b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/test_projected_entrypoint_routes.py similarity index 83% rename from tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_projected_entrypoint_routes.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/test_projected_entrypoint_routes.py index 50b3fb925..a896bd6f3 100644 --- a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_projected_entrypoint_routes.py +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/end_to_end/test_projected_entrypoint_routes.py @@ -98,3 +98,30 @@ def test_adapted_projection_uses_the_same_binding_owned_actual_sequence(tmp_path assert "native_projected(right, left, literal_2)" in bridge assert "subroutine bind_c_projected_output(right, left, literal_2, output)" in bridge assert "native_projected_output(right, left, literal_2, output)" in bridge + + +def test_matching_fortran_contract_name_uses_the_native_procedure_without_bind(tmp_path: Path): + """A Fortran contract needs ``@bind`` only when the names differ.""" + module, result = _build_inline_pyi_contract_module( + tmp_path, + module_name="matching_fortran_name", + source_text=""" +module matching_fortran_name +contains + subroutine increment(value) + integer, intent(inout) :: value + value = value + 1 + end subroutine increment +end module matching_fortran_name +""", + contract_text=""" +from prik.contracts import Addr, Arg, Int32, Returns, native_call + +@native_call([Addr(Arg(0))]) +def increment(value: Int32) -> Returns[\"value\", Int32]: ... +""", + ) + + assert module.increment(np.int32(4)) == np.int32(5) + bridge = (result.output_dir / "bind_c_matching_fortran_name_wrapper.f90").read_text(encoding="utf-8") + assert "call native_increment(value)" in bridge diff --git a/tests/fortran/pyi_contracts/calls_and_results/policy/test_call_and_result_policy.py b/tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/policy/test_call_and_result_policy.py similarity index 100% rename from tests/fortran/pyi_contracts/calls_and_results/policy/test_call_and_result_policy.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/calls_and_results/policy/test_call_and_result_policy.py diff --git a/tests/fortran/pyi_contracts/exports_and_modules/README.md b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/README.md similarity index 92% rename from tests/fortran/pyi_contracts/exports_and_modules/README.md rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/README.md index 3ef1e7700..91cc1cd3c 100644 --- a/tests/fortran/pyi_contracts/exports_and_modules/README.md +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/README.md @@ -24,5 +24,5 @@ overload edits remain owned by the later `pyi_contracts` features. Run the focused feature with: ```bash -python3 -m pytest -q tests/fortran/pyi_contracts/exports_and_modules +python3 -m pytest -q tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules ``` diff --git a/tests/fortran/pyi_contracts/exports_and_modules/codegen/test_module_initializer_lowering.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/codegen/test_module_initializer_lowering.py similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/codegen/test_module_initializer_lowering.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/codegen/test_module_initializer_lowering.py diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/aliases.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/aliases.pyi similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/aliases.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/aliases.pyi diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/collision.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/collision.pyi similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/collision.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/collision.pyi diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/facade.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/facade.pyi similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/facade.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/facade.pyi diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/flatten.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/flatten.pyi similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/flatten.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/flatten.pyi diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/module1_added_binding.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/module1_added_binding.pyi similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/module1_added_binding.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_exports/module1_added_binding.pyi diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_variables_visibility/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_variables_visibility/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_variables_visibility/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_variables_visibility/__init__.pyi diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_variables_visibility/fmodule_vars_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_variables_visibility/fmodule_vars_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_variables_visibility/fmodule_vars_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/edited_contracts/module_variables_visibility/fmodule_vars_f90.pyi diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/__init__.pyi diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/fnaming_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/fnaming_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/fnaming_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/contracts/fnaming_f90/fnaming_f90.pyi diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/visibility/native/fnaming_f90.f90 b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/native/fnaming_f90.f90 similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/fixtures/visibility/native/fnaming_f90.f90 rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/fixtures/visibility/native/fnaming_f90.f90 diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py similarity index 96% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py index e00f95425..5a3dfa0bd 100644 --- a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_package_exports.py +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_package_exports.py @@ -13,8 +13,9 @@ _import_from_build_dir, ) from prik import build_pyi_extension +from tests.fortran._support.paths import FORTRAN_ROOT -MODULE_FIXTURES = Path(__file__).parents[3] / "modules" / "end_to_end" / "fixtures" +MODULE_FIXTURES = FORTRAN_ROOT / "modules" / "end_to_end" / "fixtures" EDITED_ENTRIES = Path(__file__).parent / "fixtures" / "edited_contracts" / "module_exports" SOURCE = MODULE_FIXTURES / "module_exports.f90" BASE_CONTRACT = MODULE_FIXTURES / "contracts" / "module_exports" diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py similarity index 93% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py index e1f0d92e5..318017e92 100644 --- a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_and_initialization.py @@ -11,8 +11,9 @@ _sole_native_module, ) from prik import build_pyi_extension +from tests.fortran._support.paths import FORTRAN_ROOT -MODULE_FIXTURES = Path(__file__).parents[3] / "modules" / "end_to_end" / "fixtures" +MODULE_FIXTURES = FORTRAN_ROOT / "modules" / "end_to_end" / "fixtures" FEATURE_FIXTURES = Path(__file__).parent / "fixtures" MODULE_VARIABLE_SOURCE = MODULE_FIXTURES / "fmodule_vars_f90.f90" MODIFIED_CONTRACT = FEATURE_FIXTURES / "edited_contracts" / "module_variables_visibility" / "__init__.pyi" diff --git a/tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_naming.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_naming.py similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/end_to_end/test_visibility_naming.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/end_to_end/test_visibility_naming.py diff --git a/tests/fortran/pyi_contracts/exports_and_modules/pipeline/test_naming_generated_contracts.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/pipeline/test_naming_generated_contracts.py similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/pipeline/test_naming_generated_contracts.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/pipeline/test_naming_generated_contracts.py diff --git a/tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/policy/test_export_and_initializer_policy.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/policy/test_export_and_initializer_policy.py diff --git a/tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py b/tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/semantics/test_module_initializers.py similarity index 100% rename from tests/fortran/pyi_contracts/exports_and_modules/semantics/test_module_initializers.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/exports_and_modules/semantics/test_module_initializers.py diff --git a/tests/fortran/pyi_contracts/functions_and_classes/README.md b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/README.md similarity index 93% rename from tests/fortran/pyi_contracts/functions_and_classes/README.md rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/README.md index b41a3d2b2..7e2f3a609 100644 --- a/tests/fortran/pyi_contracts/functions_and_classes/README.md +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/README.md @@ -25,5 +25,5 @@ remain with the later Calls and Results feature. Run the focused feature with: ```bash -python3 -m pytest -q tests/fortran/pyi_contracts/functions_and_classes +python3 -m pytest -q tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes ``` diff --git a/tests/fortran/pyi_contracts/functions_and_classes/codegen/test_constructor_lowering.py b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/codegen/test_constructor_lowering.py similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/codegen/test_constructor_lowering.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/codegen/test_constructor_lowering.py diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/method_and_constructor/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/method_and_constructor/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/method_and_constructor/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/method_and_constructor/__init__.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/method_and_constructor/fclasses_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/method_and_constructor/fclasses_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/method_and_constructor/fclasses_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/method_and_constructor/fclasses_f90.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/overloaded_api/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/overloaded_api/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/overloaded_api/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/overloaded_api/__init__.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/overloaded_api/foverloads_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/overloaded_api/foverloads_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/overloaded_api/foverloads_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/overloaded_api/foverloads_f90.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_module_specifics_without_bind/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_module_specifics_without_bind/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_module_specifics_without_bind/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_module_specifics_without_bind/__init__.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_module_specifics_without_bind/foverloads_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_module_specifics_without_bind/foverloads_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_module_specifics_without_bind/foverloads_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_module_specifics_without_bind/foverloads_f90.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_type_bound_specifics_without_bind/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_type_bound_specifics_without_bind/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_type_bound_specifics_without_bind/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_type_bound_specifics_without_bind/__init__.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_type_bound_specifics_without_bind/foverloads_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_type_bound_specifics_without_bind/foverloads_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_type_bound_specifics_without_bind/foverloads_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/private_type_bound_specifics_without_bind/foverloads_f90.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/pruned_surface/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/pruned_surface/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/pruned_surface/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/pruned_surface/__init__.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/pruned_surface/foverloads_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/pruned_surface/foverloads_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/pruned_surface/foverloads_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/pruned_surface/foverloads_f90.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/without_constructor_member/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/without_constructor_member/__init__.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/without_constructor_member/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/without_constructor_member/__init__.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/without_constructor_member/foverloads_f90.pyi b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/without_constructor_member/foverloads_f90.pyi similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/without_constructor_member/foverloads_f90.pyi rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/fixtures/edited_contracts/without_constructor_member/foverloads_f90.pyi diff --git a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py similarity index 95% rename from tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py index bee0f8865..a8fa3cb10 100644 --- a/tests/fortran/pyi_contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/end_to_end/test_edited_class_surfaces.py @@ -11,10 +11,11 @@ _sole_native_module, ) from prik import build_pyi_extension +from tests.fortran._support.paths import FORTRAN_ROOT FEATURE_ROOT = Path(__file__).parent / "fixtures" / "edited_contracts" -DERIVED_FIXTURES = Path(__file__).parents[3] / "derived_types" / "end_to_end" / "fixtures" -GENERIC_FIXTURES = Path(__file__).parents[3] / "generic_interfaces" / "end_to_end" / "fixtures" +DERIVED_FIXTURES = FORTRAN_ROOT / "derived_types" / "end_to_end" / "fixtures" +GENERIC_FIXTURES = FORTRAN_ROOT / "generic_interfaces" / "end_to_end" / "fixtures" CLASS_SOURCE = DERIVED_FIXTURES / "fclasses_f90.f90" OVERLOAD_SOURCE = GENERIC_FIXTURES / "foverloads_f90.f90" pytestmark = pytest.mark.fortran_end_to_end diff --git a/tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.py similarity index 100% rename from tests/fortran/pyi_contracts/functions_and_classes/policy/test_class_surface_policy.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/policy/test_class_surface_policy.py diff --git a/tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py similarity index 99% rename from tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py rename to tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py index dba670f83..4d2f106b3 100644 --- a/tests/fortran/pyi_contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py +++ b/tests/fortran/infrastructure/semantic_pyi/contracts/functions_and_classes/semantics/test_method_and_constructor_contracts.py @@ -36,6 +36,7 @@ def reset(self) -> Int32: ... "result_position": None, "value_kind": None, "value": None, + "native_cast": None, } emitted = emit_module(module) assert " @private\n def reset(self) -> Int32: ..." in emitted diff --git a/tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py b/tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py similarity index 100% rename from tests/fortran/semantic_pyi_format/end_to_end/test_authoritative_contract_runtime.py rename to tests/fortran/infrastructure/semantic_pyi/end_to_end/test_authoritative_contract_runtime.py diff --git a/tests/fortran/semantic_pyi_format/end_to_end/test_contract_package_runtime.py b/tests/fortran/infrastructure/semantic_pyi/end_to_end/test_contract_package_runtime.py similarity index 96% rename from tests/fortran/semantic_pyi_format/end_to_end/test_contract_package_runtime.py rename to tests/fortran/infrastructure/semantic_pyi/end_to_end/test_contract_package_runtime.py index 704a31412..eb0f7a231 100644 --- a/tests/fortran/semantic_pyi_format/end_to_end/test_contract_package_runtime.py +++ b/tests/fortran/infrastructure/semantic_pyi/end_to_end/test_contract_package_runtime.py @@ -14,7 +14,7 @@ from tests.fortran._support.pyi_fixtures import assert_generated_pyi_package_matches_fixture from tests.fortran._support.wrapper_build import REPO_ROOT -SEMANTIC_PYI_FIXTURES = REPO_ROOT / "tests" / "fortran" / "semantic_pyi_format" / "pipeline" / "fixtures" +SEMANTIC_PYI_FIXTURES = REPO_ROOT / "tests" / "fortran" / "infrastructure" / "semantic_pyi" / "pipeline" / "fixtures" NATIVE_FIXTURES = SEMANTIC_PYI_FIXTURES / "native" CONTRACT_FIXTURES = SEMANTIC_PYI_FIXTURES / "contracts" STANDALONE_ONLY = NATIVE_FIXTURES / "contract_standalone_only.f90" diff --git a/tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py b/tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py similarity index 100% rename from tests/fortran/semantic_pyi_format/parsing/test_python_ast_contracts.py rename to tests/fortran/infrastructure/semantic_pyi/parsing/test_python_ast_contracts.py diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_import_graph/generated/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/__init__.pyi similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_import_graph/generated/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/__init__.pyi diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_import_graph/generated/deep.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/deep.pyi similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_import_graph/generated/deep.pyi rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/deep.pyi diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_import_graph/generated/m1.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/m1.pyi similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_import_graph/generated/m1.pyi rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_import_graph/generated/m1.pyi diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/__init__.pyi diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_mixed_module_external/generated/contract_math_mod.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/contract_math_mod.pyi similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_mixed_module_external/generated/contract_math_mod.pyi rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_mixed_module_external/generated/contract_math_mod.pyi diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/__init__.pyi diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_same_name/generated/contract_same_name.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/contract_same_name.pyi similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_same_name/generated/contract_same_name.pyi rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_same_name/generated/contract_same_name.pyi diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/contracts/contract_standalone_only/generated/__init__.pyi diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/invalid/projection_metadata/incomplete_native_call.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/invalid/projection_metadata/incomplete_native_call.pyi similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/invalid/projection_metadata/incomplete_native_call.pyi rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/invalid/projection_metadata/incomplete_native_call.pyi diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/modern_math_physics.pyi b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/modern_math_physics.pyi similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/modern_math_physics.pyi rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/modern_math_physics.pyi diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/native/contract_import_graph.f90 b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/native/contract_import_graph.f90 similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/native/contract_import_graph.f90 rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/native/contract_import_graph.f90 diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/native/contract_mixed_module_external.f90 b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/native/contract_mixed_module_external.f90 similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/native/contract_mixed_module_external.f90 rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/native/contract_mixed_module_external.f90 diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/native/contract_multi_module.f90 b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/native/contract_multi_module.f90 similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/native/contract_multi_module.f90 rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/native/contract_multi_module.f90 diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/native/contract_same_name.f90 b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/native/contract_same_name.f90 similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/native/contract_same_name.f90 rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/native/contract_same_name.f90 diff --git a/tests/fortran/semantic_pyi_format/pipeline/fixtures/native/contract_standalone_only.f90 b/tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/native/contract_standalone_only.f90 similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/fixtures/native/contract_standalone_only.f90 rename to tests/fortran/infrastructure/semantic_pyi/pipeline/fixtures/native/contract_standalone_only.f90 diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_calls_and_policy_metadata.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_calls_and_policy_metadata.py similarity index 99% rename from tests/fortran/semantic_pyi_format/pipeline/test_calls_and_policy_metadata.py rename to tests/fortran/infrastructure/semantic_pyi/pipeline/test_calls_and_policy_metadata.py index 0478b1d37..429cb76ae 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_calls_and_policy_metadata.py +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_calls_and_policy_metadata.py @@ -389,7 +389,7 @@ def test_printer_emits_extended_storage_and_callable_forms(): assert printer.emit(string_pointer_handle) == "Pointer[String[8][:]]" assert printer.emit(annotated_array) == "Annotated[Float64[:, :], ORDER_ANY, Finite, Range(1, 3)]" assert printer.emit(character) == "String[16]" - assert printer.emit(allocatable_character) == "Allocatable[String]" + assert printer.emit(allocatable_character) == "Allocatable[String[:]]" assert printer.emit(pointer_scalar) == "Pointer[Int32]" diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_classes_and_methods.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_classes_and_methods.py similarity index 90% rename from tests/fortran/semantic_pyi_format/pipeline/test_classes_and_methods.py rename to tests/fortran/infrastructure/semantic_pyi/pipeline/test_classes_and_methods.py index d61695840..923b1cf01 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_classes_and_methods.py +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_classes_and_methods.py @@ -2,7 +2,7 @@ import pytest from prik.parsers.fortran import parse_fortran_file as parse_fortran_source -from prik.pipeline.pyi import emit_module_stubs +from prik.pipeline.pyi import emit_module_stubs, pyi_text_to_semantic_module from prik.printers import PyiPrinter, emit_module from prik.semantics.fortran2ir import fortran_module_to_semantic_module from prik.semantics.models import ( @@ -508,3 +508,46 @@ def reset(self) -> None: ...""" @native_call([Return(0)]) def wrapper() -> None: ...""" ) + + +def test_generic_specifics_with_projected_outputs_round_trip(): + """A generic whose specifics project an `intent(out)` reloads from its contract. + + The declaration states the public signature, so the output the projection + turned into a result is not one of the arguments it accepts. Comparing the + declaration against the specific's native arguments rejected every such + generic, which is the common shape in numerical Fortran. + """ + source = """ +module projected_generic_mod + implicit none + private + public :: ink + interface ink + module procedure ink_default, ink_extended + end interface ink +contains + subroutine ink_default(x, n, iflag) + real(8), intent(in) :: x(:) + integer(4), intent(in) :: n + integer(4), intent(out) :: iflag + iflag = 0 + end subroutine ink_default + subroutine ink_extended(x, n, extra, iflag) + real(8), intent(in) :: x(:) + integer(4), intent(in) :: n + real(8), intent(in) :: extra + integer(4), intent(out) :: iflag + iflag = 0 + end subroutine ink_extended +end module projected_generic_mod +""" + + code = generate_pyi(source) + assert '@overload("ink_default")' in code + assert '@overload("ink_extended")' in code + + module = pyi_text_to_semantic_module(code, module_name="projected_generic_mod") + overloads = [item for item in module.overload_sets if item.name == "ink"] + assert len(overloads) == 1 + assert [procedure.name for procedure in overloads[0].procedures] == ["ink_default", "ink_extended"] diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/test_contract_loading.py rename to tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_loading.py diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/test_contract_package_generation.py rename to tests/fortran/infrastructure/semantic_pyi/pipeline/test_contract_package_generation.py diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_modern_example.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_modern_example.py similarity index 88% rename from tests/fortran/semantic_pyi_format/pipeline/test_modern_example.py rename to tests/fortran/infrastructure/semantic_pyi/pipeline/test_modern_example.py index 1b8219b59..f8e12f44d 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_modern_example.py +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_modern_example.py @@ -3,17 +3,11 @@ from prik.parsers.fortran import parse_fortran_file from prik.semantics.fortran2ir import fortran_module_to_semantic_module from prik.printers import emit_module +from tests.fortran._support.paths import GENERAL_FORTRAN_DIR def test_modern_fortran_example_pyi_snapshot(): - fixture = ( - Path(__file__).resolve().parents[2] - / "source_parsing" - / "parsing" - / "fixtures" - / "general" - / "modern_pyi_example.f90" - ) + fixture = GENERAL_FORTRAN_DIR / "modern_pyi_example.f90" expected_fixture = Path(__file__).parent / "fixtures" / "modern_math_physics.pyi" source = fixture.read_text(encoding="utf-8") diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_native_abi_source_round_trip.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_native_abi_source_round_trip.py similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/test_native_abi_source_round_trip.py rename to tests/fortran/infrastructure/semantic_pyi/pipeline/test_native_abi_source_round_trip.py diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_conversion_smoke.py similarity index 83% rename from tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py rename to tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_conversion_smoke.py index 63df217ea..adc55e585 100644 --- a/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_conversion_smoke.py +++ b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_conversion_smoke.py @@ -5,10 +5,8 @@ from prik.semantics.fortran2ir import fortran_module_to_semantic_module from prik.printers import emit_module -from tests.fortran._support.fixture_outputs import ( - PARSER_FIXTURE_ROOT as TESTS_DIR, - parse_fixture, -) +from tests.fortran._support.fixture_outputs import parse_fixture +from tests.fortran._support.paths import PARSER_FIXTURE_ROOT as TESTS_DIR from tests.fortran._support.fixture_conversion import FORTRAN_FIXTURES diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_imports_and_packages.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_imports_and_packages.py similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/test_pyi_printer_imports_and_packages.py rename to tests/fortran/infrastructure/semantic_pyi/pipeline/test_pyi_printer_imports_and_packages.py diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_types_and_declarations.py b/tests/fortran/infrastructure/semantic_pyi/pipeline/test_types_and_declarations.py similarity index 100% rename from tests/fortran/semantic_pyi_format/pipeline/test_types_and_declarations.py rename to tests/fortran/infrastructure/semantic_pyi/pipeline/test_types_and_declarations.py diff --git a/tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py similarity index 99% rename from tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py rename to tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py index 5e0de068c..fdc6ed707 100644 --- a/tests/fortran/semantic_pyi_format/semantics/test_calls_and_projections.py +++ b/tests/fortran/infrastructure/semantic_pyi/semantics/test_calls_and_projections.py @@ -313,7 +313,10 @@ def wrapper( projection = module.functions[0].projection - assert [asdict(mapping) for mapping in projection] == [ + # Exact C scalar casts are orthogonal to these Fortran hidden-value facts. + assert [ + {name: value for name, value in asdict(mapping).items() if name != "native_cast"} for mapping in projection + ] == [ { "python_name": "x", "native_name": "x", diff --git a/tests/fortran/semantic_pyi_format/semantics/test_classes_and_overloads.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_classes_and_overloads.py similarity index 100% rename from tests/fortran/semantic_pyi_format/semantics/test_classes_and_overloads.py rename to tests/fortran/infrastructure/semantic_pyi/semantics/test_classes_and_overloads.py diff --git a/tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py similarity index 100% rename from tests/fortran/semantic_pyi_format/semantics/test_imports_and_packages.py rename to tests/fortran/infrastructure/semantic_pyi/semantics/test_imports_and_packages.py diff --git a/tests/fortran/semantic_pyi_format/semantics/test_native_abi.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_native_abi.py similarity index 100% rename from tests/fortran/semantic_pyi_format/semantics/test_native_abi.py rename to tests/fortran/infrastructure/semantic_pyi/semantics/test_native_abi.py diff --git a/tests/fortran/semantic_pyi_format/semantics/test_round_trip_properties.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py similarity index 100% rename from tests/fortran/semantic_pyi_format/semantics/test_round_trip_properties.py rename to tests/fortran/infrastructure/semantic_pyi/semantics/test_round_trip_properties.py diff --git a/tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py b/tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py similarity index 100% rename from tests/fortran/semantic_pyi_format/semantics/test_types_and_values.py rename to tests/fortran/infrastructure/semantic_pyi/semantics/test_types_and_values.py diff --git a/tests/fortran/memory_management/end_to_end/test_explicit_borrowed_owner.py b/tests/fortran/memory_management/end_to_end/test_explicit_borrowed_owner.py index 4a2d33591..5fced8f67 100644 --- a/tests/fortran/memory_management/end_to_end/test_explicit_borrowed_owner.py +++ b/tests/fortran/memory_management/end_to_end/test_explicit_borrowed_owner.py @@ -12,10 +12,9 @@ _sole_native_module, ) from prik import build_pyi_extension +from tests.fortran._support.paths import FORTRAN_ROOT -FINALIZER_SOURCE = ( - Path(__file__).parents[2] / "derived_types" / "end_to_end" / "fixtures" / "fborrowed_finalizer_f90.f90" -) +FINALIZER_SOURCE = FORTRAN_ROOT / "derived_types" / "end_to_end" / "fixtures" / "fborrowed_finalizer_f90.f90" FINALIZER_CONTRACT = Path(__file__).parent / "fixtures" / "edited_contracts" / "borrowed_owner" / "__init__.pyi" pytestmark = pytest.mark.fortran_end_to_end diff --git a/tests/fortran/modules/end_to_end/test_module_variables_and_state.py b/tests/fortran/modules/end_to_end/test_module_variables_and_state.py index 6528bc396..3cdeae018 100644 --- a/tests/fortran/modules/end_to_end/test_module_variables_and_state.py +++ b/tests/fortran/modules/end_to_end/test_module_variables_and_state.py @@ -8,6 +8,7 @@ import pytest from tests.fortran._support.wrapper_build import ( _build_source_or_generated_pyi_and_import, + _build_text_and_import, _sole_native_module, ) @@ -123,3 +124,308 @@ def test_scalar_module_variables_use_attributes_and_parameters_have_no_native_se assert module.black.r == np.int32(0) assert module.black_sum() == np.int32(0) assert second_module.black.r == np.int32(0) + + +CHARACTER_MODULE_ARRAY_SOURCE = """ +module fchar_module_arrays_f90 + implicit none + character(len=8), target :: labels(3) = ['alpha ', 'beta ', 'gamma '] + character(len=4), target :: grid(2, 2) = reshape(['aa ', 'bb ', 'cc ', 'dd '], [2, 2]) +contains + subroutine relabel_first() + labels(1) = 'ALPHA!!!' + end subroutine relabel_first + + function read_label(index) result(value) + integer(4), intent(in) :: index + character(len=8) :: value + value = labels(index) + end function read_label + + function read_grid(row, column) result(value) + integer(4), intent(in) :: row, column + character(len=4) :: value + value = grid(row, column) + end function read_grid +end module fchar_module_arrays_f90 +""" + + +def test_fixed_shape_character_module_arrays_expose_one_live_bytes_view(tmp_path: Path): + """A character module array borrows the same fixed-width view a numeric one does. + + The element type only changes the dtype width, so the live-view contract is + what has to hold: native writes appear without re-reading the attribute, and + Python writes are visible to Fortran through the same storage. + """ + module = _build_text_and_import( + CHARACTER_MODULE_ARRAY_SOURCE, + "fchar_module_arrays_f90.f90", + tmp_path, + { + "bind_c_fchar_module_arrays_f90_wrapper.f90", + "fchar_module_arrays_f90_wrapper.c", + "fchar_module_arrays_f90_wrapper.h", + }, + ) + + assert module.labels.dtype == np.dtype("S8") + assert module.grid.dtype == np.dtype("S4") + assert module.grid.shape == (2, 2) + assert module.grid.flags["F_CONTIGUOUS"] is True + np.testing.assert_array_equal(module.labels, np.array([b"alpha ", b"beta ", b"gamma "], dtype="S8")) + + # A native write reaches the view the attribute already handed out. + module.relabel_first() + assert module.labels[0] == b"ALPHA!!!" + + # A Python write reaches the storage Fortran reads. + module.labels[1] = b"PYTHON!!" + assert module.read_label(np.int32(2)) == "PYTHON!!" + module.grid[1, 0] = b"ZZ " + assert module.read_grid(np.int32(2), np.int32(1)) == "ZZ " + + +CHARACTER_MODULE_SCALAR_SOURCE = """ +module fchar_module_scalars_f90 + implicit none + character(len=8) :: label = 'alpha ' + character(len=3) :: code = 'abc' + character(len=*), parameter :: tag = 'fixed' +contains + subroutine relabel() + label = 'ALPHA!!!' + end subroutine relabel + + function read_label() result(value) + character(len=8) :: value + value = label + end function read_label +end module fchar_module_scalars_f90 +""" + + +def test_scalar_character_module_variables_read_and_write_through(tmp_path: Path): + """A character module variable is a `str` property, as a numeric one is a value. + + A character value has no by-value C ABI, so the accessors copy through a + fixed-width buffer; what has to hold is that the copy runs in both + directions and that a wrong width is refused rather than truncated. + """ + module = _build_text_and_import( + CHARACTER_MODULE_SCALAR_SOURCE, + "fchar_module_scalars_f90.f90", + tmp_path, + { + "bind_c_fchar_module_scalars_f90_wrapper.f90", + "fchar_module_scalars_f90_wrapper.c", + "fchar_module_scalars_f90_wrapper.h", + }, + ) + + assert module.label == "alpha " + assert module.code == "abc" + assert module.tag == "fixed" + + # A native write is observed by the next read, not cached from import. + module.relabel() + assert module.label == "ALPHA!!!" + + # A Python write reaches the storage Fortran reads. + module.label = "PYTHON!!" + assert module.read_label() == "PYTHON!!" + + # The declared length is a byte width, so a multi-byte encoding still fits exactly. + module.label = "café!!!" + assert module.label == "café!!!" + assert module.read_label() == "café!!!" + + +@pytest.mark.parametrize("value", ["ab", "abcd"]) +def test_scalar_character_module_variable_rejects_a_wrong_encoded_width(value: str, tmp_path: Path): + """Truncating or padding silently would corrupt native state, so the width is exact.""" + module = _build_text_and_import( + CHARACTER_MODULE_SCALAR_SOURCE, + "fchar_module_scalars_f90.f90", + tmp_path, + { + "bind_c_fchar_module_scalars_f90_wrapper.f90", + "fchar_module_scalars_f90_wrapper.c", + "fchar_module_scalars_f90_wrapper.h", + }, + ) + + with pytest.raises(TypeError, match="exactly 3 bytes"): + module.code = value + assert module.code == "abc" + + +CHARACTER_MODULE_DESCRIPTOR_SOURCE = """ +module fchar_module_descriptors_f90 + implicit none + character(len=:), allocatable :: deferred + character(len=6), allocatable :: fixed + character(len=:), pointer :: link => null() + character(len=6), target :: store = 'STORED' + character(len=2), parameter :: pair(2) = ['ab', 'cd'] + character(len=3), parameter :: grid(2, 2) = reshape(['aaa', 'bbb', 'ccc', 'ddd'], [2, 2]) + character(len=*), parameter :: inferred(3) = ['alpha', 'beta ', 'gamma'] +contains + subroutine setup() + deferred = 'alpha' + fixed = 'FIXEDV' + link => store + end subroutine setup + + subroutine grow() + deferred = deferred // '-more' + end subroutine grow + + subroutine clear() + if (allocated(deferred)) deallocate(deferred) + if (allocated(fixed)) deallocate(fixed) + nullify(link) + end subroutine clear +end module fchar_module_descriptors_f90 +""" + + +def _character_descriptor_module(tmp_path: Path): + return _build_text_and_import( + CHARACTER_MODULE_DESCRIPTOR_SOURCE, + "fchar_module_descriptors_f90.f90", + tmp_path, + { + "bind_c_fchar_module_descriptors_f90_wrapper.f90", + "fchar_module_descriptors_f90_wrapper.c", + "fchar_module_descriptors_f90_wrapper.h", + }, + ) + + +def test_descriptor_character_module_variables_snapshot_their_runtime_value(tmp_path: Path): + """An allocatable or pointer character module variable reads as a detached `str`. + + Its width is established at runtime, so the snapshot has to report the + length the descriptor currently holds rather than a width fixed at build + time, and re-reading after native code changes it must observe the change. + """ + module = _character_descriptor_module(tmp_path) + + assert module.deferred is None + assert module.fixed is None + assert module.link is None + + module.setup() + assert module.deferred == "alpha" + assert module.fixed == "FIXEDV" + assert module.link == "STORED" + + # A reallocation to a different width is observed by the next read. + module.grow() + assert module.deferred == "alpha-more" + + +def test_descriptor_character_module_variables_report_absence_as_none(tmp_path: Path): + """Deallocation and nullification are values Python observes, not stale reads.""" + module = _character_descriptor_module(tmp_path) + + module.setup() + module.clear() + assert module.deferred is None + assert module.fixed is None + assert module.link is None + + +def test_character_parameter_arrays_are_read_only_fixed_width_snapshots(tmp_path: Path): + """A character parameter array is copied once, like a numeric one. + + A Fortran parameter has no addressable storage, so the value is a + Python-owned copy taken at import; it must therefore be read-only and keep + the declared element width as its dtype. + """ + module = _character_descriptor_module(tmp_path) + + assert module.pair.dtype == np.dtype("S2") + assert module.grid.dtype == np.dtype("S3") + assert module.grid.shape == (2, 2) + assert module.pair.flags["WRITEABLE"] is False + assert module.grid.flags["WRITEABLE"] is False + np.testing.assert_array_equal(module.pair, np.array([b"ab", b"cd"], dtype="S2")) + np.testing.assert_array_equal( + module.grid, + np.array([[b"aaa", b"ccc"], [b"bbb", b"ddd"]], dtype="S3"), + ) + + +def test_assumed_length_character_parameter_array_reports_its_inferred_width(tmp_path: Path): + """A `len=*` parameter takes its width from its initializer, which prik never reads. + + The width is still a constant the Fortran side knows, so the accessor + reports it beside the extents rather than the binding restating a length + it would have to evaluate the initializer to learn. + """ + module = _character_descriptor_module(tmp_path) + + assert module.inferred.dtype == np.dtype("S5") + np.testing.assert_array_equal( + module.inferred, + np.array([b"alpha", b"beta ", b"gamma"], dtype="S5"), + ) + + +DECLARED_LENGTH_CHARACTER_ARRAY_SOURCE = """ +module fchar_declared_arrays_f90 + implicit none + character(len=4), allocatable :: fixed_alloc(:) + character(len=:), pointer :: deferred_ptr(:) => null() + character(len=4), pointer :: fixed_ptr(:) => null() + character(len=4), target :: store(2) = ['aaaa', 'bbbb'] +contains + subroutine setup() + allocate(fixed_alloc(2)) + fixed_alloc = ['xxxx', 'yyyy'] + fixed_ptr => store + deferred_ptr => store + end subroutine setup +end module fchar_declared_arrays_f90 +""" + + +def test_declared_length_character_module_arrays_compile_and_expose_their_width(tmp_path: Path): + """A descriptor dummy accepts a deferred-length actual only if it declares one. + + The generated descriptor-consumer interface and descriptor ABI both have to + spell the width the module array actually declares; deferring it + unconditionally is rejected by the Fortran compiler, so building the module + at all is the evidence here. + + A deferred-length *allocatable* character array is left out: GNU Fortran + 11.4 fails with an internal compiler error on that declaration, which is a + compiler defect rather than a wrapper contract. + """ + module = _build_text_and_import( + DECLARED_LENGTH_CHARACTER_ARRAY_SOURCE, + "fchar_declared_arrays_f90.f90", + tmp_path, + { + "bind_c_fchar_declared_arrays_f90_wrapper.f90", + "fchar_declared_arrays_f90_wrapper.c", + "fchar_declared_arrays_f90_wrapper.h", + }, + ) + + assert module.fixed_alloc.allocated is False + module.setup() + + assert module.fixed_alloc.allocated is True + np.testing.assert_array_equal( + module.fixed_alloc.to_numpy(), + np.array([b"xxxx", b"yyyy"], dtype="S4"), + ) + # A pointer array reports its shape and association; extracting its target + # stays gated behind PointerPolicy, as it is for a numeric pointer array. + assert module.fixed_ptr.associated is True + assert module.fixed_ptr.shape == (2,) + assert module.deferred_ptr.associated is True + assert module.deferred_ptr.shape == (2,) diff --git a/tests/fortran/pointers/codegen/test_pointer_lowering.py b/tests/fortran/pointers/codegen/test_pointer_lowering.py index a3019f155..5a4a2c7a8 100644 --- a/tests/fortran/pointers/codegen/test_pointer_lowering.py +++ b/tests/fortran/pointers/codegen/test_pointer_lowering.py @@ -102,7 +102,7 @@ def test_pointer_plans_complete_descriptor_ownership_and_operations_before_lower assert pointer_output.native_array_handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE -def test_pointer_lowering_assigns_descriptors_without_target_deallocation(): +def test_pointer_lowering_assigns_descriptors_and_emits_manual_target_release(): artifacts = WrapperGenerator().generate(_pointer_plan()) c_source = next(source.text for source in artifacts.sources if source.path.suffix == ".c") bridge_source = next(source.text for source in artifacts.sources if source.path.suffix == ".f90") @@ -131,4 +131,6 @@ def test_pointer_lowering_assigns_descriptors_without_target_deallocation(): pointer_operations = bridge_source[operations_start:operations_end] assert "result => source" in pointer_operations assert "nullify(result)" in pointer_operations - assert "deallocate(result)" not in pointer_operations + # Release is manual and caller-driven, matching the ``deallocate`` a Fortran + # caller would write for the same pointer; prik never runs it on its own. + assert "deallocate(result)" in pointer_operations diff --git a/tests/fortran/pointers/end_to_end/test_pointer_handles.py b/tests/fortran/pointers/end_to_end/test_pointer_handles.py index 635d2b49c..768ea6121 100644 --- a/tests/fortran/pointers/end_to_end/test_pointer_handles.py +++ b/tests/fortran/pointers/end_to_end/test_pointer_handles.py @@ -1,6 +1,7 @@ """Pointer argument, result, association, and handle-policy tests.""" import gc +import resource import subprocess import sys from pathlib import Path @@ -9,6 +10,7 @@ import pytest from tests.fortran._support.wrapper_build import ( + _build_and_import, _build_text_and_import, _build_source_or_generated_pyi_and_import, _compile_native_object, @@ -612,3 +614,69 @@ def test_pointer_array_results_use_owned_descriptors_without_owning_targets( assert selected.closed is True assert absent.closed is True np.testing.assert_array_equal(values, np.array([1.0, 2.0, 3.0], dtype=np.float64)) + + +POINTER_RELEASE_SOURCE = """ +module fpointer_release_f90 + implicit none + real(8), allocatable, target :: pool(:) +contains + function mint(n) result(values) + integer(4), intent(in) :: n + real(8), pointer :: values(:) + allocate(values(n)) + values = 1.0d0 + end function mint + + function borrow(n) result(values) + integer(4), intent(in) :: n + real(8), pointer :: values(:) + if (.not. allocated(pool)) allocate(pool(n)) + pool = 2.0d0 + values => pool + end function borrow +end module fpointer_release_f90 +""" + + +@pytest.mark.fortran_end_to_end +def test_pointer_handle_releases_native_storage_when_the_caller_asks(tmp_path: Path): + """A pointer handle offers the release a Fortran caller would write itself. + + prik never frees a native target on its own, so withholding the operation + only removes the caller's ability to free storage the procedure handed + over. Reclaiming it has to be observable in the process, because an + unreleased target still reports the same handle state. + """ + source = tmp_path / "native" / "fpointer_release_f90.f90" + source.parent.mkdir() + source.write_text(POINTER_RELEASE_SOURCE, encoding="utf-8") + module = _build_and_import( + source, + tmp_path, + { + "bind_c_fpointer_release_f90_wrapper.f90", + "fpointer_release_f90_wrapper.c", + "fpointer_release_f90_wrapper.h", + }, + ) + + handle = module.mint(np.int32(4)) + assert handle.associated is True + handle.deallocate() + assert handle.associated is False + + def peak_kib() -> int: + return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + + extent = np.int32(4096) + for _ in range(200): + module.mint(extent).deallocate() + baseline = peak_kib() + for _ in range(4000): + module.mint(extent).deallocate() + assert peak_kib() - baseline == 0 + + # A borrowed target is module storage the library keeps; releasing is the + # caller's decision there too, so only the untouched path is asserted. + assert module.borrow(np.int32(4)).associated is True diff --git a/tests/fortran/pointers/policy/test_pointer_ownership_policy.py b/tests/fortran/pointers/policy/test_pointer_ownership_policy.py index 46121032a..c17eea2f6 100644 --- a/tests/fortran/pointers/policy/test_pointer_ownership_policy.py +++ b/tests/fortran/pointers/policy/test_pointer_ownership_policy.py @@ -131,7 +131,7 @@ def select_values() -> Pointer[Float64[:]]: ... assert policy.target_lifetime == "unknown" assert policy.destroy_behavior == "handle_finalizer" assert policy.to_numpy == "unsupported" - assert set(policy.operations) == {"associate", "associated", "nullify", "to_numpy"} + assert set(policy.operations) == {"associate", "associated", "deallocate", "nullify", "to_numpy"} @pytest.mark.parametrize( @@ -371,9 +371,11 @@ class box: assert module_policy.requires_pointer_c_descriptor_interop is True assert module_policy.target_lifetime == "module" assert module_policy.destroy_behavior == "none" - assert set(module_policy.operations) == {"associate", "associated", "nullify", "to_numpy"} + assert set(module_policy.operations) == {"associate", "associated", "deallocate", "nullify", "to_numpy"} + # Release is available manually, as it is for an allocatable module array. + # Allocation and resize still need PointerPolicy, because they establish a + # new target rather than releasing the one the module already names. assert "allocate" not in module_policy.operations - assert "deallocate" not in module_policy.operations assert "resize" not in module_policy.operations assert not field_policy.is_blocked @@ -384,7 +386,7 @@ class box: assert field_policy.requires_pointer_c_descriptor_interop is True assert field_policy.target_lifetime == "parent_wrapper" assert field_policy.destroy_behavior == "parent_wrapper_finalizer" - assert set(field_policy.operations) == {"associate", "associated", "nullify", "to_numpy"} + assert set(field_policy.operations) == {"associate", "associated", "deallocate", "nullify", "to_numpy"} def test_complete_pointer_policy_metadata_round_trips_without_overriding_container_ownership(): @@ -618,7 +620,7 @@ def make_target() -> Pointer[Float64[:]]: ... assert field_target.descriptor_interop == "pointer_c_descriptor" assert field_target.requires_pointer_c_descriptor_interop is True assert field_target.is_blocked is False - assert set(field_target.operations) == {"associate", "associated", "nullify", "to_numpy"} + assert set(field_target.operations) == {"associate", "associated", "deallocate", "nullify", "to_numpy"} assert argument_values.handle_kind == "argument_descriptor" assert argument_values.origin == "argument" @@ -703,7 +705,7 @@ def make_target() -> Pointer[Float64[:]]: ... assert pointer_result.descriptor_interop == "pointer_c_descriptor" assert pointer_result.requires_pointer_c_descriptor_interop is True assert pointer_result.requires_c_descriptor_interop is True - assert set(pointer_result.operations) == {"associate", "associated", "nullify", "to_numpy"} + assert set(pointer_result.operations) == {"associate", "associated", "deallocate", "nullify", "to_numpy"} assert pointer_result.default_construction == "none" diff --git a/tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py b/tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py index 19efd7a76..b74be1f8e 100644 --- a/tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py +++ b/tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py @@ -169,7 +169,10 @@ def test_raw_array_plan_edits_fail_before_backend_lowering(edit: str, diagnostic elif edit == "shape-role": argument.array.extent_reference_roles = (("edited.missing:value",),) elif edit == "element-family": - argument.semantic_type_name = "UInt8" + # A semantic scalar name policy knows but backend lowering has no + # spelling for. Unsigned widths became first-lane scalars, so this + # names one that is still unlowered. + argument.semantic_type_name = "Float16" elif edit == "character-length": argument.array.itemsize = None else: diff --git a/tests/fortran/raw_addresses/codegen/test_string_address_lowering.py b/tests/fortran/raw_addresses/codegen/test_string_address_lowering.py index 5439f9f34..436e864a9 100644 --- a/tests/fortran/raw_addresses/codegen/test_string_address_lowering.py +++ b/tests/fortran/raw_addresses/codegen/test_string_address_lowering.py @@ -82,7 +82,10 @@ def test_string_addresses_dispatch_to_named_binding_and_bridge_lowering(): c_source = next(source.text for source in artifacts.sources if source.path.suffix == ".c") bridge_source = next(source.text for source in artifacts.sources if source.path.suffix == ".f90") - assert "void bind_c_storage(void * label);" in c_source + # NumPy-backed storage reports the caller's itemsize beside the address; a + # raw address has no Python object to measure, so it carries only the width + # the contract declared. + assert "void bind_c_storage(void * label, int64_t label_length);" in c_source assert "PyArray_TYPE((PyArrayObject *)bound_label_obj) != NPY_STRING" in c_source assert "PyArray_NDIM((PyArrayObject *)bound_label_obj) != 0" in c_source assert "PyArray_ITEMSIZE((PyArrayObject *)bound_label_obj) != 8" in c_source @@ -95,23 +98,26 @@ def test_string_addresses_dispatch_to_named_binding_and_bridge_lowering(): assert "bound_label = PyLong_AsVoidPtr(bound_label_obj);" in c_source assert "prik_malloc" not in c_source - assert 'subroutine bind_c_storage(bound_label) bind(c, name="bind_c_storage")' in bridge_source + assert 'subroutine bind_c_storage(bound_label, label_length) bind(c, name="bind_c_storage")' in bridge_source assert 'subroutine bind_c_raw(bound_label) bind(c, name="bind_c_raw")' in bridge_source assert bridge_source.count("type(c_ptr), value :: bound_label") == 2 - assert bridge_source.count("character(kind=c_char, len=8) :: label") == 2 - assert bridge_source.count("call c_f_pointer(bound_label, label_bytes, [8])") == 2 + assert "integer(c_int64_t), value :: label_length" in bridge_source + assert "character(kind=c_char, len=label_length) :: label" in bridge_source + assert "call c_f_pointer(bound_label, label_bytes, [label_length])" in bridge_source + assert "label_bytes(1:label_length) = transfer(label, label_bytes(1:label_length))" in bridge_source + assert bridge_source.count("character(kind=c_char, len=8) :: label") == 1 + assert bridge_source.count("call c_f_pointer(bound_label, label_bytes, [8])") == 1 assert bridge_source.count("label = transfer(label_bytes, label)") == 2 assert "call native_storage(label)" in bridge_source assert "call native_raw(label)" in bridge_source - assert bridge_source.count("label_bytes(1:8) = transfer(label, label_bytes(1:8))") == 2 - assert "label_length" not in bridge_source + assert bridge_source.count("label_bytes(1:8) = transfer(label, label_bytes(1:8))") == 1 assert "c_null_char" not in "\n".join(line for line in bridge_source.splitlines() if "label_bytes" in line) @pytest.mark.parametrize( ("edit", "diagnostic"), [ - ("missing-length", "invalid-string-storage-length"), + ("missing-raw-length", "invalid-string-raw-address-length"), ("wrong-owner", "invalid-string-storage-owner"), ("runtime-length-role", "unexpected-string-storage-length-handoff"), ("wrong-copy-reason", "invalid-string-storage-copy-reason"), @@ -125,9 +131,11 @@ def test_string_address_plan_edits_fail_before_backend_lowering(edit: str, diagn functions = _functions(plan) storage = functions["storage"].arguments[0] raw = functions["raw"].arguments[0] - if edit == "missing-length": - storage.character_length = None - storage.projected_call_slot.character_length = None + if edit == "missing-raw-length": + # Only a raw address still needs the declared width: NumPy-backed + # storage may leave it assumed and report the itemsize instead. + raw.character_length = None + raw.projected_call_slot.character_length = None elif edit == "wrong-owner": storage.ownership_owner = OwnershipOwner.NATIVE elif edit == "runtime-length-role": diff --git a/tests/fortran/raw_addresses/end_to_end/test_raw_fixed_string_arrays.py b/tests/fortran/raw_addresses/end_to_end/test_raw_fixed_string_arrays.py index 9a0706844..6be8eda7b 100644 --- a/tests/fortran/raw_addresses/end_to_end/test_raw_fixed_string_arrays.py +++ b/tests/fortran/raw_addresses/end_to_end/test_raw_fixed_string_arrays.py @@ -11,8 +11,9 @@ _sole_native_module, ) from prik import build_pyi_extension +from tests.fortran._support.paths import FORTRAN_ROOT -STRING_FIXTURES = Path(__file__).resolve().parents[2] / "strings" / "end_to_end" / "fixtures" +STRING_FIXTURES = FORTRAN_ROOT / "strings" / "end_to_end" / "fixtures" STRING_F90_SOURCE = STRING_FIXTURES / "fstrings_f90.f90" RAW_CONTRACT = Path(__file__).parent / "fixtures" / "edited_contracts" / "raw_string_array" / "__init__.pyi" pytestmark = pytest.mark.fortran_end_to_end diff --git a/tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py b/tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py index 7b726b902..94bab6849 100644 --- a/tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py +++ b/tests/fortran/raw_addresses/end_to_end/test_raw_native_addresses.py @@ -12,10 +12,13 @@ _sole_native_module, ) from prik import build_pyi_extension +from tests.fortran._support.paths import FORTRAN_ROOT NATIVE_CALL_EXAMPLES_F90_SOURCE = ( - Path(__file__).parents[2] - / "pyi_contracts" + FORTRAN_ROOT + / "infrastructure" + / "semantic_pyi" + / "contracts" / "calls_and_results" / "end_to_end" / "fixtures" diff --git a/tests/fortran/strings/codegen/test_fixed_string_result_lowering.py b/tests/fortran/strings/codegen/test_fixed_string_result_lowering.py index ade75d567..b1d8ede79 100644 --- a/tests/fortran/strings/codegen/test_fixed_string_result_lowering.py +++ b/tests/fortran/strings/codegen/test_fixed_string_result_lowering.py @@ -185,8 +185,8 @@ def test_fixed_string_result_policy_blocks_status_error_until_failure_release_is module = parse_pyi_text( """ @raises(status="status", success=0) -@native_call([Return("label", 0), Return("status", 1)]) -def label() -> tuple[String[8], Int32]: ... +@native_call([Return("label", 0), Hidden("status", Int32)]) +def label() -> String[8]: ... """, module_name="string_result_with_status", ) diff --git a/tests/fortran/strings/codegen/test_string_input_lowering.py b/tests/fortran/strings/codegen/test_string_input_lowering.py index 3e3ffe3ac..8c2a68a5f 100644 --- a/tests/fortran/strings/codegen/test_string_input_lowering.py +++ b/tests/fortran/strings/codegen/test_string_input_lowering.py @@ -7,7 +7,12 @@ from tests.fortran._support.ownership_policy import parse_pyi_text from prik.policy.ownership import CodegenAction, NativeBarrierAction, PythonBarrierAction from prik.policy.completion import complete_semantic_policies -from prik.policy.models import ArgumentHandoffMode, BridgeDataAction +from prik.policy.models import ( + ArgumentHandoffMode, + BridgeDataAction, + NativeArrayDescriptorKind, + OptionalMode, +) from prik.pipeline.wrapper import WrapperGenerator from prik.planning import WrapperPlanner from prik.planning.models import DatatypeFamily @@ -101,3 +106,252 @@ def test_string_handoff_plan_edits_fail_before_backend_lowering(edit: str, diagn with pytest.raises(ValueError, match=diagnostic): WrapperGenerator().generate(plan) + + +DEFERRED_UPDATE_SOURCE = """ +module deferred_update + implicit none +contains + subroutine grow(value) + character(len=:), allocatable, intent(inout) :: value + if (allocated(value)) value = value // '!' + end subroutine grow +end module deferred_update +""" + + +DEFERRED_INPUT_SOURCE = """ +module deferred_input + implicit none +contains + subroutine measure(value, length) + character(len=:), allocatable, intent(in) :: value + integer(4), intent(out) :: length + length = len(value) + end subroutine measure +end module deferred_input +""" + + +def _source_route_plan(tmp_path, text: str, module_name: str): + from prik.parsers.fortran.parser import parse_fortran_project + from prik.pipeline.build import ( + _apply_source_python_exports, + _fortran_source_for_pipeline, + _merge_wrapper_modules, + ) + from prik.preprocessing import PreprocessingConfig + from prik.semantics.fortran2ir import fortran_project_to_semantic_modules + + source = tmp_path / f"{module_name}.f90" + source.write_text(text, encoding="utf-8") + parsed = parse_fortran_project({str(source): _fortran_source_for_pipeline(source, PreprocessingConfig())}) + modules = fortran_project_to_semantic_modules(parsed) + _apply_source_python_exports(modules) + module = _merge_wrapper_modules(modules, name=module_name) + complete_semantic_policies(module) + return WrapperPlanner().build(module) + + +def _deferred_input_plan(tmp_path): + return _source_route_plan(tmp_path, DEFERRED_INPUT_SOURCE, "deferred_input") + + +def _deferred_update_plan(tmp_path): + return _source_route_plan(tmp_path, DEFERRED_UPDATE_SOURCE, "deferred_update") + + +def test_deferred_length_string_input_plans_an_allocatable_adapter_local(tmp_path): + """The bridge facet carries the deferred fact; the shared entrypoint does not. + + A deferred-length dummy cannot appear in a ``bind(C)`` interface, so the + adapter local is adapter-local conversion rather than part of the C ABI. + """ + plan = _deferred_input_plan(tmp_path) + function = next( + function + for namespace in plan.namespaces + for function in namespace.functions + if function.binding.python_name == "measure" + ) + argument = function.arguments[0] + + assert argument.bridge.character_local is not None + assert argument.bridge.character_local.deferred_length is True + assert argument.bridge.character_local.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE + assert argument.entrypoint.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER + assert argument.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION + + +def test_deferred_length_string_input_lowers_to_allocatable_local_without_changing_the_binding(tmp_path): + """The adapter allocates on assignment; the C binding keeps the byte buffer.""" + artifacts = WrapperGenerator().generate(_deferred_input_plan(tmp_path)) + bridge_source = next(source.text for source in artifacts.sources if source.path.suffix == ".f90") + c_source = next(source.text for source in artifacts.sources if source.path.suffix == ".c") + + assert "character(kind=c_char, len=:), allocatable :: value" in bridge_source + assert "transfer(value_bytes, repeat(' ', value_length))" in bridge_source + assert "character(kind=c_char, len=value_length)" not in bridge_source + # The shared C ABI is unchanged: the binding still hands over bytes plus a length. + assert "bind_c_measure" in c_source + + +def test_deferred_length_string_update_plans_one_input_and_one_output_group(tmp_path): + """The update adds an output group beside its input, not a descriptor argument. + + The Python-visible argument keeps the plain character-buffer handoff, so the + C ABI gains only the descriptor output group the reallocated value needs. + """ + plan = _deferred_update_plan(tmp_path) + function = next( + function + for namespace in plan.namespaces + for function in namespace.functions + if function.binding.python_name == "grow" + ) + argument = function.arguments[0] + result = function.results[0] + + assert argument.entrypoint.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER + assert argument.binding.optional_mode is OptionalMode.REQUIRED + assert argument.binding.descriptor_boundary is False + assert argument.entrypoint.descriptor_output_role is None + assert argument.projects_character_descriptor_update is True + + assert result.updates_argument is True + assert result.owner_path == argument.owner_path + assert result.projected_call_slot is argument.projected_call_slot + assert result.scalar_descriptor is not None + assert result.entrypoint.parameter_name == "value_output" + assert tuple( + (parameter.owner_path, parameter.source_kind) + for parameter in sorted(function.entrypoint.parameters, key=lambda item: item.position) + ) == ((argument.owner_path, "argument"), (result.owner_path, "hidden_result")) + + +def test_deferred_length_string_update_copies_the_reallocated_local_into_c_storage(tmp_path): + """The adapter reads back the same local the native procedure may reallocate. + + Reading a separate output local would return the value the caller passed in, + which compiles and imports but silently discards the update. + """ + artifacts = WrapperGenerator().generate(_deferred_update_plan(tmp_path)) + bridge_source = next(source.text for source in artifacts.sources if source.path.suffix == ".f90") + c_source = next(source.text for source in artifacts.sources if source.path.suffix == ".c") + + assert "character(kind=c_char, len=:), allocatable :: value" in bridge_source + assert "call native_grow(value)" in bridge_source + assert "if (allocated(value)) then" in bridge_source + assert "value_output_length = len(value, kind=c_int64_t)" in bridge_source + assert "transfer(value, value_output_copy(1:value_output_length))" in bridge_source + # No separate output local exists to read the pre-call value from. + assert "value_output_value" not in bridge_source + assert "bind_c_grow(bound_value, (int64_t)bound_value_length, &value_output, " in c_source + + +@pytest.mark.parametrize( + ("edit", "diagnostic"), + [ + ("drop-descriptor", "missing-update-result-descriptor"), + ("drop-slot", "missing-update-result-native-slot"), + ("drop-deferred-input", "invalid-update-result-argument"), + ], +) +def test_deferred_length_string_update_plan_edits_fail_before_backend_lowering( + edit: str, + diagnostic: str, + tmp_path, +): + """The update lane's producer facts are validated, not assumed. + + Each edit leaves a plan that still lowers to compilable code while losing + the reason the reallocated value reaches Python, so validation has to reject + it rather than emit a wrapper that returns the caller's own value. + """ + plan = _deferred_update_plan(tmp_path) + function = next( + item for namespace in plan.namespaces for item in namespace.functions if item.binding.python_name == "grow" + ) + result = function.results[0] + if edit == "drop-descriptor": + result.scalar_descriptor = None + elif edit == "drop-slot": + result.projected_call_slot = None + else: + function.arguments[0].bridge.character_local = None + + with pytest.raises(ValueError, match=diagnostic): + WrapperGenerator().generate(plan) + + +DESCRIPTOR_LOCAL_SOURCE = """ +module descriptor_locals + implicit none +contains + subroutine fixed_allocatable(value, length) + character(len=4), allocatable, intent(in) :: value + integer(4), intent(out) :: length + length = len(value) + end subroutine fixed_allocatable + subroutine deferred_pointer(value, length) + character(len=:), pointer, intent(in) :: value + integer(4), intent(out) :: length + length = len(value) + end subroutine deferred_pointer + subroutine fixed_pointer(value, length) + character(len=4), pointer, intent(in) :: value + integer(4), intent(out) :: length + length = len(value) + end subroutine fixed_pointer + subroutine pointer_update(value) + character(len=:), pointer, intent(inout) :: value + if (associated(value)) value = 'z' + end subroutine pointer_update +end module descriptor_locals +""" + + +def _descriptor_local_source(tmp_path) -> str: + artifacts = WrapperGenerator().generate(_source_route_plan(tmp_path, DESCRIPTOR_LOCAL_SOURCE, "descriptor_locals")) + return next(source.text for source in artifacts.sources if source.path.suffix == ".f90") + + +def test_descriptor_character_locals_carry_the_attribute_the_native_dummy_declares(tmp_path): + """An allocatable or pointer dummy rejects a plain local as its actual argument. + + The local is the only thing that changes: each of these arguments still + crosses the C ABI as a byte buffer and a length. + """ + bridge_source = _descriptor_local_source(tmp_path) + + assert "character(kind=c_char, len=4), allocatable :: value" in bridge_source + assert "character(kind=c_char, len=:), pointer :: value" in bridge_source + assert "character(kind=c_char, len=4), pointer :: value" in bridge_source + + +def test_descriptor_character_locals_are_allocated_before_the_copy_that_needs_them(tmp_path): + """Only a deferred-length allocatable is established by assignment alone. + + A pointer has no storage until it is allocated, and a fixed-length + allocatable would otherwise be moulded from storage that does not exist. + """ + bridge_source = _descriptor_local_source(tmp_path) + + assert "allocate(character(kind=c_char, len=value_length) :: value)" in bridge_source + assert "allocate(value)" in bridge_source + + +def test_pointer_character_locals_release_the_storage_the_adapter_allocated(tmp_path): + """A read-only pointer dummy cannot reassociate, so its allocation is always still ours. + + An update dummy may be reassociated or deallocated by the native procedure, + so the adapter compares against the seed it recorded and leaves native-owned + storage alone. + """ + bridge_source = _descriptor_local_source(tmp_path) + + assert "value => null()" in bridge_source + assert "if (associated(value)) then" in bridge_source + assert "deallocate(value)" in bridge_source + assert "value_seed => value" in bridge_source + assert "if (associated(value, value_seed)) then" in bridge_source diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/__init__.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/__init__.pyi new file mode 100644 index 000000000..bf2f33a49 --- /dev/null +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/__init__.pyi @@ -0,0 +1 @@ +from . import fstring_descriptors_f90 diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/fstring_descriptors_f90.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/fstring_descriptors_f90.pyi new file mode 100644 index 000000000..0fab1f839 --- /dev/null +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fstring_descriptors_f90/fstring_descriptors_f90.pyi @@ -0,0 +1,126 @@ +from prik.contracts import Allocatable, Annotated, Arg, Destruction, Int32, Ownership, Pointer, Return, Returns, String, Transfer, native_call + +@native_call([Allocatable(Arg(0))]) +def grow( + value: String[:] | None +) -> Returns["value", String[:]] | None: ... + +@native_call([Allocatable(Arg(0))]) +def shrink( + value: String[:] | None +) -> Returns["value", String[:]] | None: ... + +@native_call([Allocatable(Arg(0))]) +def drop( + value: String[:] | None +) -> Returns["value", String[:]] | None: ... + +@native_call([Allocatable(Arg(0))]) +def optional_grow( + value: String[:] | None = ... +) -> Returns["value", String[:]] | None: ... + +@native_call([Allocatable(Arg(0)), Allocatable(Arg(1))]) +def grow_both( + first: String[:] | None, + second: String[:] | None +) -> tuple[Returns["first", String[:]] | None, Returns["second", String[:]] | None]: ... + +@native_call([Allocatable(Arg(0)), Return('length', 1)]) +def grow_and_measure( + value: String[:] | None +) -> tuple[Returns["value", String[:]] | None, Int32]: ... + +@native_call([Allocatable(Arg(0)), Return('length', 0)]) +def measure( + value: String[:] | None +) -> Int32: ... + +@native_call([Allocatable(Return('value', 0))]) +def make() -> String[:] | None: ... + +@native_call([Allocatable(Arg(0)), Return('length', 0)]) +def measure_fixed_allocatable( + value: String[4] | None +) -> Int32: ... + +@native_call([Allocatable(Return('value', 0))]) +def make_fixed_allocatable() -> String[4] | None: ... + +@native_call([Allocatable(Arg(0))]) +def relabel_fixed_allocatable( + value: String[4] | None +) -> Returns["value", String[4]] | None: ... + +@native_call([Allocatable(Arg(0))]) +def drop_fixed_allocatable( + value: String[4] | None +) -> Returns["value", String[4]] | None: ... + +@native_call([Pointer(Arg(0)), Return('length', 0)]) +def measure_pointer( + value: Annotated[String[:], Ownership("caller"), Transfer("call_local"), Destruction("call_local")] | None +) -> Int32: ... + +@native_call([Pointer(Return('value', 0))]) +def point_at_static() -> String[:] | None: ... + +@native_call([Pointer(Arg(0))]) +def edit_pointer_in_place( + value: String[:] | None +) -> Returns["value", String[:]] | None: ... + +@native_call([Pointer(Arg(0))]) +def reassociate_pointer( + value: String[:] | None +) -> Returns["value", String[:]] | None: ... + +@native_call([Pointer(Arg(0))]) +def deallocate_pointer( + value: String[:] | None +) -> Returns["value", String[:]] | None: ... + +@native_call([Pointer(Arg(0))]) +def nullify_pointer( + value: String[:] | None +) -> Returns["value", String[:]] | None: ... + +@native_call([Pointer(Arg(0)), Return('length', 0)]) +def optional_pointer_measure( + value: Annotated[String[:], Ownership("caller"), Transfer("call_local"), Destruction("call_local")] | None = ... +) -> Int32: ... + +@native_call([Pointer(Arg(0))]) +def optional_pointer_edit( + value: String[:] | None = ... +) -> Returns["value", String[:]] | None: ... + +@native_call([Pointer(Arg(0))]) +def regrow_pointer( + value: String[:] | None +) -> Returns["value", String[:]] | None: ... + +@native_call([Pointer(Arg(0)), Return('length', 0)]) +def measure_fixed_pointer( + value: Annotated[String[4], Ownership("caller"), Transfer("call_local"), Destruction("call_local")] | None +) -> Int32: ... + +@native_call([Pointer(Return('value', 0))]) +def point_at_fixed_static() -> String[4] | None: ... + +@native_call([Pointer(Arg(0))]) +def relabel_fixed_pointer( + value: String[4] | None +) -> Returns["value", String[4]] | None: ... + +@native_call([], result=Allocatable(Return(0))) +def allocatable_result() -> String[:] | None: ... + +@native_call([], result=Allocatable(Return(0))) +def fixed_allocatable_result() -> String[4] | None: ... + +@native_call([], result=Pointer(Return(0))) +def pointer_result() -> Annotated[String[:], Ownership("python"), Transfer("snapshot_copy"), Destruction("python_refcount")] | None: ... + +@native_call([], result=Pointer(Return(0))) +def fixed_pointer_result() -> Annotated[String[4], Ownership("python"), Transfer("snapshot_copy"), Destruction("python_refcount")] | None: ... diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi index 954d1a526..9248c87c6 100644 --- a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings/__init__.pyi @@ -1,34 +1,34 @@ -from prik.contracts import Int32, String, bind, standalone +from prik.contracts import Int32, Returns, String, bind, standalone @bind("CHAR_CODE_DEFAULT") @standalone def char_code_default( C: String[1] -) -> Int32: ... +) -> tuple[Int32, Returns["C", String[1]]]: ... @bind("CHAR_CODE_STAR1") @standalone def char_code_star1( C: String[1] -) -> Int32: ... +) -> tuple[Int32, Returns["C", String[1]]]: ... @bind("STRING_LEN_STAR8") @standalone def string_len_star8( TEXT: String[8] -) -> Int32: ... +) -> tuple[Int32, Returns["TEXT", String[8]]]: ... @bind("STRING_LEN_ASSUMED") @standalone def string_len_assumed( TEXT: String -) -> Int32: ... +) -> tuple[Int32, Returns["TEXT", String]]: ... @bind("STRING_LEN_ENTITY") @standalone def string_len_entity( TEXT: String[6] -) -> Int32: ... +) -> tuple[Int32, Returns["TEXT", String[6]]]: ... @bind("CHAR_RESULT_DEFAULT") @standalone diff --git a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/fstrings_f90.pyi b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/fstrings_f90.pyi index 5335d1be1..b94124e47 100644 --- a/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/fstrings_f90.pyi +++ b/tests/fortran/strings/end_to_end/fixtures/contracts/fstrings_f90/fstrings_f90.pyi @@ -41,7 +41,7 @@ def string_result_c_char() -> String[8]: ... @native_call([Arg(0), Allocatable(Return('value', 0))]) def string_result_deferred( text: String -) -> String | None: ... +) -> String[:] | None: ... def fixed_array_extent( labels: String[8][::] diff --git a/tests/fortran/strings/end_to_end/fixtures/fstring_descriptors_f90.f90 b/tests/fortran/strings/end_to_end/fixtures/fstring_descriptors_f90.f90 new file mode 100644 index 000000000..128449590 --- /dev/null +++ b/tests/fortran/strings/end_to_end/fixtures/fstring_descriptors_f90.f90 @@ -0,0 +1,207 @@ +module fstring_descriptors_f90 + implicit none + character(len=6), target, private :: static_six = 'STATIC' + character(len=4), target, private :: static_four = 'FOUR' +contains + ! --- character(len=:), allocatable --- + subroutine grow(value) + character(len=:), allocatable, intent(inout) :: value + + if (allocated(value)) value = value // '!!!' + end subroutine grow + + subroutine shrink(value) + character(len=:), allocatable, intent(inout) :: value + + if (allocated(value)) then + if (len(value) > 2) value = value(1:2) + end if + end subroutine shrink + + subroutine drop(value) + character(len=:), allocatable, intent(inout) :: value + + if (allocated(value)) deallocate(value) + end subroutine drop + + subroutine optional_grow(value) + character(len=:), allocatable, intent(inout), optional :: value + + if (present(value)) then + if (allocated(value)) value = value // '?' + end if + end subroutine optional_grow + + subroutine grow_both(first, second) + character(len=:), allocatable, intent(inout) :: first + character(len=:), allocatable, intent(inout) :: second + + if (allocated(first)) first = first // '-1' + if (allocated(second)) second = second // '-2' + end subroutine grow_both + + subroutine grow_and_measure(value, length) + character(len=:), allocatable, intent(inout) :: value + integer(4), intent(out) :: length + + if (allocated(value)) value = value // '-tail' + length = 0 + if (allocated(value)) length = len(value) + end subroutine grow_and_measure + + subroutine measure(value, length) + character(len=:), allocatable, intent(in) :: value + integer(4), intent(out) :: length + + length = len(value) + end subroutine measure + + subroutine make(value) + character(len=:), allocatable, intent(out) :: value + + value = 'made' + end subroutine make + + ! --- character(len=n), allocatable --- + subroutine measure_fixed_allocatable(value, length) + character(len=4), allocatable, intent(in) :: value + integer(4), intent(out) :: length + + length = -1 + if (allocated(value)) length = len(value) + end subroutine measure_fixed_allocatable + + subroutine make_fixed_allocatable(value) + character(len=4), allocatable, intent(out) :: value + + value = 'MADE' + end subroutine make_fixed_allocatable + + subroutine relabel_fixed_allocatable(value) + character(len=4), allocatable, intent(inout) :: value + + if (allocated(value)) value = 'X' // value(2:4) + end subroutine relabel_fixed_allocatable + + subroutine drop_fixed_allocatable(value) + character(len=4), allocatable, intent(inout) :: value + + if (allocated(value)) deallocate(value) + end subroutine drop_fixed_allocatable + + ! --- character(len=:), pointer --- + subroutine measure_pointer(value, length) + character(len=:), pointer, intent(in) :: value + integer(4), intent(out) :: length + + length = -1 + if (associated(value)) length = len(value) + end subroutine measure_pointer + + subroutine point_at_static(value) + character(len=:), pointer, intent(out) :: value + + value => static_six + end subroutine point_at_static + + subroutine edit_pointer_in_place(value) + character(len=:), pointer, intent(inout) :: value + + if (associated(value)) value = 'Z' + end subroutine edit_pointer_in_place + + subroutine reassociate_pointer(value) + character(len=:), pointer, intent(inout) :: value + + value => static_six + end subroutine reassociate_pointer + + subroutine deallocate_pointer(value) + character(len=:), pointer, intent(inout) :: value + + if (associated(value)) deallocate(value) + end subroutine deallocate_pointer + + subroutine nullify_pointer(value) + character(len=:), pointer, intent(inout) :: value + + nullify(value) + end subroutine nullify_pointer + + subroutine optional_pointer_measure(value, length) + character(len=:), pointer, intent(in), optional :: value + integer(4), intent(out) :: length + + length = -1 + if (present(value)) then + length = -2 + if (associated(value)) length = len(value) + end if + end subroutine optional_pointer_measure + + subroutine optional_pointer_edit(value) + character(len=:), pointer, intent(inout), optional :: value + + if (present(value)) then + if (associated(value)) value = 'q' + end if + end subroutine optional_pointer_edit + + subroutine regrow_pointer(value) + character(len=:), pointer, intent(inout) :: value + character(len=:), pointer :: fresh + + if (associated(value)) then + allocate(character(len=len(value) + 3) :: fresh) + fresh = value // '>>>' + deallocate(value) + value => fresh + end if + end subroutine regrow_pointer + + ! --- character(len=n), pointer --- + subroutine measure_fixed_pointer(value, length) + character(len=4), pointer, intent(in) :: value + integer(4), intent(out) :: length + + length = -1 + if (associated(value)) length = len(value) + end subroutine measure_fixed_pointer + + subroutine point_at_fixed_static(value) + character(len=4), pointer, intent(out) :: value + + value => static_four + end subroutine point_at_fixed_static + + subroutine relabel_fixed_pointer(value) + character(len=4), pointer, intent(inout) :: value + + if (associated(value)) value = 'P' // value(2:4) + end subroutine relabel_fixed_pointer + + ! --- descriptor function results --- + function allocatable_result() result(value) + character(len=:), allocatable :: value + + value = 'allocatable' + end function allocatable_result + + function fixed_allocatable_result() result(value) + character(len=4), allocatable :: value + + value = 'FIXA' + end function fixed_allocatable_result + + function pointer_result() result(value) + character(len=:), pointer :: value + + value => static_six + end function pointer_result + + function fixed_pointer_result() result(value) + character(len=4), pointer :: value + + value => static_four + end function fixed_pointer_result +end module fstring_descriptors_f90 diff --git a/tests/fortran/strings/end_to_end/test_assumed_width_character_storage.py b/tests/fortran/strings/end_to_end/test_assumed_width_character_storage.py new file mode 100644 index 000000000..f52bb981f --- /dev/null +++ b/tests/fortran/strings/end_to_end/test_assumed_width_character_storage.py @@ -0,0 +1,117 @@ +"""Assumed-width character contracts take their width from the caller's array. + +Every element of a NumPy ``S`` array shares one itemsize, and a Fortran +``character(len=n)`` array is uniform by definition, so a contract may leave the +width unstated and let the runtime value cross beside the buffer. +""" + +from pathlib import Path + +import numpy as np +import pytest + +from prik import build_pyi_extension + +pytestmark = pytest.mark.fortran_end_to_end + +SCALAR_SOURCE = """module {name} +contains + subroutine stamp(text) + character(len=*), intent(inout) :: text + text = "abc" + end subroutine +end module +""" + +ARRAY_SOURCE = """module {name} +contains + integer function stamp_all(text) + character(len=*), intent(inout) :: text(:) + stamp_all = size(text) + text(1)(1:1) = 'Z' + end function +end module +""" + + +def _build(tmp_path: Path, name: str, source: str, contract: str): + (tmp_path / f"{name}.f90").write_text(source.format(name=name), encoding="utf-8") + (tmp_path / f"{name}.pyi").write_text(contract, encoding="utf-8") + result = build_pyi_extension( + tmp_path / f"{name}.pyi", + native_fortran_sources=[tmp_path / f"{name}.f90"], + output_dir=tmp_path / f"build_{name}", + output_name=name, + ) + adapter = next(path.read_text(encoding="utf-8") for path in result.generated_sources if path.suffix == ".f90") + return result, adapter + + +def test_assumed_width_scalar_storage_accepts_any_caller_itemsize(tmp_path: Path): + """``String[...][()]`` declares its adapter local from the runtime width.""" + result, adapter = _build( + tmp_path, + "assumed_scalar_any", + SCALAR_SOURCE, + "from prik.contracts import String\n\ndef stamp(text: String[...][()]) -> None: ...\n", + ) + module = result.import_module() + + assert "character(kind=c_char, len=text_length) :: text" in adapter + for width, expected in (("S8", b"abc "), ("S32", b"abc" + b" " * 29)): + buffer = np.array(b"Z", dtype=width) + assert module.stamp(buffer) is None + assert buffer.tobytes() == expected + + +def test_declared_and_assumed_scalar_storage_share_one_adapter_shape(tmp_path: Path): + """The width always crosses beside the address, declared or not.""" + _, assumed = _build( + tmp_path, + "assumed_scalar_shape", + SCALAR_SOURCE, + "from prik.contracts import String\n\ndef stamp(text: String[...][()]) -> None: ...\n", + ) + (tmp_path / "declared").mkdir() + _, declared = _build( + tmp_path / "declared", + "declared_scalar_shape", + SCALAR_SOURCE, + "from prik.contracts import String\n\ndef stamp(text: String[8][()]) -> None: ...\n", + ) + + signature = 'subroutine bind_c_stamp(bound_text, text_length) bind(c, name="bind_c_stamp")' + assert signature in assumed + assert signature in declared + + +def test_assumed_width_character_array_accepts_any_caller_itemsize(tmp_path: Path): + """``String[...][:]`` names the itemsize the ABI already reports.""" + result, adapter = _build( + tmp_path, + "assumed_array_any", + ARRAY_SOURCE, + "from prik.contracts import Int32, String\n\ndef stamp_all(text: String[...][:]) -> Int32: ...\n", + ) + module = result.import_module() + + assert "character(kind=c_char, len=text_itemsize)" in adapter + for width in ("S8", "S16", "S32"): + values = np.array([b"alpha", b"beta"], dtype=width) + assert module.stamp_all(values) == np.int32(2) + assert values[0] == b"Zlpha" + + +def test_declared_array_width_still_checks_the_caller_itemsize(tmp_path: Path): + """A stated width keeps its validation; only an assumed one accepts any.""" + result, _ = _build( + tmp_path, + "declared_array_width", + ARRAY_SOURCE, + "from prik.contracts import Int32, String\n\ndef stamp_all(text: String[8][:]) -> Int32: ...\n", + ) + module = result.import_module() + + assert module.stamp_all(np.array([b"alpha"], dtype="S8")) == np.int32(1) + with pytest.raises(TypeError, match="itemsize 8"): + module.stamp_all(np.array([b"alpha"], dtype="S16")) diff --git a/tests/fortran/strings/end_to_end/test_scalar_string_descriptors.py b/tests/fortran/strings/end_to_end/test_scalar_string_descriptors.py new file mode 100644 index 000000000..c67c5fce9 --- /dev/null +++ b/tests/fortran/strings/end_to_end/test_scalar_string_descriptors.py @@ -0,0 +1,172 @@ +"""Runtime evidence for allocatable and pointer scalar ``character`` boundaries.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_or_generated_pyi_and_import + +FIXTURES = Path(__file__).parent / "fixtures" +DESCRIPTOR_SOURCE = FIXTURES / "fstring_descriptors_f90.f90" +CONTRACT_FIXTURES = FIXTURES / "contracts" + +pytestmark = pytest.mark.fortran_end_to_end + + +@pytest.fixture +def compiled_descriptor_module(pyi_parity_build_mode: str, tmp_path: Path): + """Build the same module from Fortran source and from its generated contract. + + Every descriptor form here has a contract spelling, so both routes must + reach the same runtime behavior; building only from source would hide a + contract that no longer describes the procedure it was generated from. + """ + return _build_source_or_generated_pyi_and_import( + DESCRIPTOR_SOURCE, + tmp_path, + { + "bind_c_fstring_descriptors_f90_wrapper.f90", + "fstring_descriptors_f90_wrapper.c", + "fstring_descriptors_f90_wrapper.h", + }, + CONTRACT_FIXTURES / "fstring_descriptors_f90", + pyi_parity_build_mode, + ) + + +def test_deferred_length_string_update_returns_the_reallocated_value(compiled_descriptor_module): + """The caller receives the length the native procedure chose, not the one it passed. + + Returning the pre-call value compiles, imports, and runs, so only a real + call proves the adapter reads back the reallocated local. + """ + module = compiled_descriptor_module + + assert module.grow("ab") == "ab!!!" + assert module.grow("") == "!!!" + assert module.grow("café") == "café!!!" + assert module.shrink("abcdef") == "ab" + + +def test_deferred_length_string_update_reports_an_unallocated_dummy_as_none(compiled_descriptor_module): + """Deallocation is a value Python can observe, so it must not read freed storage.""" + module = compiled_descriptor_module + + assert module.drop("abc") is None + assert module.optional_grow() is None + assert module.optional_grow(None) is None + assert module.optional_grow("abc") == "abc?" + + +def test_deferred_length_string_updates_keep_their_public_result_order(compiled_descriptor_module): + """Several updates, and an update beside another output, stay in contract order.""" + module = compiled_descriptor_module + + assert module.grow_both("a", "b") == ("a-1", "b-2") + assert module.grow_and_measure("ab") == ("ab-tail", np.int32(7)) + + +def test_read_only_deferred_length_character_lanes_still_wrap(compiled_descriptor_module): + """The input and output lanes keep working beside the new update lane.""" + module = compiled_descriptor_module + + assert module.measure("abcd") == 4 + assert module.make() == "made" + + +def test_deferred_length_string_update_reports_allocation_failure( + compiled_descriptor_module, + monkeypatch: pytest.MonkeyPatch, +): + """The returned copy is C storage, so a failed allocation must raise, not truncate.""" + module = compiled_descriptor_module + + monkeypatch.setenv("PRIK_WRAPPER_FAIL_ALLOC", "1") + with pytest.raises(MemoryError): + module.grow("ab") + + +def test_fixed_length_allocatable_character_arguments_wrap_in_every_direction(compiled_descriptor_module): + """A declared length does not remove the allocatable attribute from the dummy. + + The actual argument must still be allocatable, so these forms exercise the + same descriptor lanes a deferred length uses while keeping their width. + """ + module = compiled_descriptor_module + + assert module.measure_fixed_allocatable("abcd") == 4 + assert module.make_fixed_allocatable() == "MADE" + assert module.relabel_fixed_allocatable("abcd") == "Xbcd" + assert module.drop_fixed_allocatable("abcd") is None + + +def test_character_pointer_arguments_wrap_in_every_direction(compiled_descriptor_module): + """A pointer dummy needs an associated actual, which the adapter allocates itself.""" + module = compiled_descriptor_module + + assert module.measure_pointer("abcde") == 5 + assert module.point_at_static() == "STATIC" + assert module.measure_fixed_pointer("abcd") == 4 + assert module.point_at_fixed_static() == "FOUR" + assert module.relabel_fixed_pointer("abcd") == "Pbcd" + + +def test_character_pointer_update_reports_whatever_the_dummy_ends_up_holding(compiled_descriptor_module): + """A pointer update publishes the association the native procedure leaves behind. + + In-place writes, reassociation to native storage, deallocation, and + nullification are four different endings for the same dummy, and each is a + distinct Python value rather than the value the caller passed in. + """ + module = compiled_descriptor_module + + assert module.edit_pointer_in_place("abc") == "Z " + assert module.reassociate_pointer("ab") == "STATIC" + assert module.deallocate_pointer("ab") is None + assert module.nullify_pointer("ab") is None + assert module.regrow_pointer("ab") == "ab>>>" + + +def test_absent_optional_character_pointer_is_never_released(compiled_descriptor_module): + """An absent argument skips the allocation, so the local has nothing to free. + + Releasing it anyway reads an association status the adapter never + established, which crashes rather than returning a wrong value. + """ + module = compiled_descriptor_module + + assert module.optional_pointer_measure() == -1 + assert module.optional_pointer_measure(None) == -1 + assert module.optional_pointer_measure("abcde") == 5 + assert module.optional_pointer_edit() is None + assert module.optional_pointer_edit(None) is None + assert module.optional_pointer_edit("abc") == "q " + + +def test_character_pointer_update_release_survives_repeated_calls(compiled_descriptor_module): + """The adapter allocates a target per call, so its release must be exactly once. + + Deallocating storage the native procedure already freed, or freeing the + replacement it associated instead, crashes rather than fails an assertion, + so repetition is what makes the release decision observable. + """ + module = compiled_descriptor_module + + for _ in range(2000): + assert module.deallocate_pointer("ab") is None + assert module.regrow_pointer("ab") == "ab>>>" + assert module.reassociate_pointer("ab") == "STATIC" + assert module.nullify_pointer("ab") is None + assert module.measure_pointer("abcde") == 5 + assert module.optional_pointer_measure() == -1 + + +def test_descriptor_character_function_results_are_copied_before_return(compiled_descriptor_module): + """Allocatable and pointer results both reach Python as ordinary strings.""" + module = compiled_descriptor_module + + assert module.allocatable_result() == "allocatable" + assert module.fixed_allocatable_result() == "FIXA" + assert module.pointer_result() == "STATIC" + assert module.fixed_pointer_result() == "FOUR" diff --git a/tests/fortran/strings/pipeline/test_generated_string_contracts.py b/tests/fortran/strings/pipeline/test_generated_string_contracts.py index 7a3966743..f59653659 100644 --- a/tests/fortran/strings/pipeline/test_generated_string_contracts.py +++ b/tests/fortran/strings/pipeline/test_generated_string_contracts.py @@ -18,6 +18,7 @@ GeneratedContractCase(source.stem, (source,), CONTRACT_ROOT / source.stem) for source in ( FIXTURE_ROOT / "fcharacter_edges_f90.f90", + FIXTURE_ROOT / "fstring_descriptors_f90.f90", FIXTURE_ROOT / "fstrings.f", FIXTURE_ROOT / "fstrings_f90.f90", ) diff --git a/tests/fortran/strings/policy/test_string_wrapper_policy.py b/tests/fortran/strings/policy/test_string_wrapper_policy.py index 135d3b6cc..d7877a16c 100644 --- a/tests/fortran/strings/policy/test_string_wrapper_policy.py +++ b/tests/fortran/strings/policy/test_string_wrapper_policy.py @@ -1,5 +1,6 @@ from pathlib import Path +import pytest from tests.fortran._support.ownership_policy import parse_pyi_text from tests.fortran._support.wrapper_build import wrapper_source @@ -22,8 +23,11 @@ from prik.policy.completion import complete_semantic_policies from prik.policy.models import ( ArgumentConversionPhase, + CharacterLocalRelease, + NativeArrayDescriptorKind, ArgumentHandoffMode, BridgeDataAction, + OptionalMode, WritebackPhase, ) @@ -122,3 +126,229 @@ def discard_name(name: String[8]) -> None: ... assert identity.arguments[0].codegen_action is CodegenAction.CALL_LOCAL_INPUT assert identity.arguments[0].projects_result is False assert identity.writeback_actions == () + + +def _semantic_module_from_text(source_text: str, tmp_path: Path, *, module_name: str): + """Complete policy for one inline Fortran source without a shared fixture.""" + source = tmp_path / f"{module_name}.f90" + source.write_text(source_text, encoding="utf-8") + parsed = parse_fortran_project({str(source): _fortran_source_for_pipeline(source, PreprocessingConfig())}) + modules = fortran_project_to_semantic_modules(parsed) + _apply_source_python_exports(modules) + module = _merge_wrapper_modules(modules, name=module_name) + complete_semantic_policies(module) + return module + + +def test_read_only_deferred_length_string_argument_completes_deferred_policy(tmp_path: Path): + """A ``character(len=:)`` input records the fact the adapter needs. + + No ``bind(C)`` interface can declare a deferred-length dummy, so the + generated Fortran adapter must build the allocatable local itself. Policy + owns that fact; the bridge only implements it. + """ + module = _semantic_module_from_text( + """ +module deferred_input + implicit none +contains + subroutine measure(value, length) + character(len=:), allocatable, intent(in) :: value + integer(4), intent(out) :: length + length = len(value) + end subroutine measure +end module deferred_input +""", + tmp_path, + module_name="deferred_input", + ) + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + + assert policy.supported is True + argument = policy.arguments[0] + assert argument.character_local is not None + assert argument.character_local.deferred_length is True + assert argument.character_local.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE + assert argument.character_local.release is CharacterLocalRelease.NONE + assert argument.character_length is None + assert argument.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER + + +def test_fixed_and_assumed_length_string_arguments_stay_plain_locals(): + """Only a descriptor attribute selects a descriptor adapter local. + + ``character(len=8)`` and ``character(len=*)`` are neither allocatable nor + pointer, so both keep the plain fixed-length local and owe no release. + """ + module = parse_pyi_text( + """ +def fixed(text: String[8]) -> Int32: ... +def assumed(text: String) -> Int32: ... +""", + module_name="non_deferred_strings", + ) + complete_semantic_policies(module) + + for index in (0, 1): + policy = module.functions[index].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + local = policy.arguments[0].character_local + assert local is not None + assert local.descriptor_kind is None + assert local.deferred_length is False + assert local.release is CharacterLocalRelease.NONE + + +@pytest.mark.parametrize( + ("intent", "release"), + [ + ("in", CharacterLocalRelease.DEALLOCATE), + ("inout", CharacterLocalRelease.DEALLOCATE_IF_RETAINED), + ], +) +def test_character_pointer_arguments_complete_their_release_responsibility( + intent: str, + release: CharacterLocalRelease, + tmp_path: Path, +): + """A pointer local is storage the adapter allocated, so policy must say who frees it. + + An ``intent(in)`` dummy cannot change its association, so the allocation is + always still the adapter's to release. A mutable dummy may be reassociated + or deallocated by the native procedure, so the adapter may only release the + allocation while the dummy still identifies it. + """ + module = _semantic_module_from_text( + f""" +module pointer_input + implicit none +contains + subroutine consume(value, length) + character(len=:), pointer, intent({intent}) :: value + integer(4), intent(out) :: length + length = 0 + if (associated(value)) length = len(value) + end subroutine consume +end module pointer_input +""", + tmp_path, + module_name="pointer_input", + ) + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + + assert policy.supported is True + local = policy.arguments[0].character_local + assert local is not None + assert local.descriptor_kind is NativeArrayDescriptorKind.POINTER + assert local.deferred_length is True + assert local.release is release + + +def test_deferred_length_string_update_completes_input_plus_descriptor_result(tmp_path: Path): + """A mutable ``character(len=:)`` dummy keeps its input and gains a result facet. + + The caller's ``str`` cannot carry back a length chosen during the call, so + policy completes two decisions for the one dummy: a call-local character + buffer for the input, and a nullable descriptor result that owns the + reallocated storage. Argument writeback stays absent because the value + travels as that result. + """ + module = _semantic_module_from_text( + """ +module deferred_update + implicit none +contains + subroutine grow(value) + character(len=:), allocatable, intent(inout) :: value + if (allocated(value)) value = value // '!' + end subroutine grow +end module deferred_update +""", + tmp_path, + module_name="deferred_update", + ) + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + + assert policy.supported is True + argument = policy.arguments[0] + assert argument.codegen_action is CodegenAction.CALL_LOCAL_INPUT + assert argument.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER + assert argument.optional_mode is OptionalMode.REQUIRED + assert argument.descriptor_boundary is False + assert argument.nullable is False + assert argument.projects_character_descriptor_update is True + assert policy.writeback_actions == () + + result = policy.results[0] + assert result.updates_argument is True + assert result.owner_path == argument.owner_path + assert result.codegen_action is CodegenAction.COPY_OUT + assert result.ownership.owner is OwnershipOwner.PYTHON + assert result.ownership.python_visible is False + assert result.scalar_descriptor is not None + assert result.scalar_descriptor.runtime_length is True + assert result.scalar_descriptor.nullable is True + assert result.scalar_descriptor.release_owner is OwnershipOwner.PYTHON + + +def test_fixed_length_allocatable_string_update_takes_the_descriptor_result_lane(tmp_path: Path): + """The descriptor attribute, not the length, selects the update lane. + + A copy-in/copy-out replacement writes back through the caller's buffer, + which means passing that buffer as the actual argument. An allocatable + dummy will not accept one, so a fixed-length allocatable takes the same + call-local input and projected descriptor result a deferred length does. + """ + module = _semantic_module_from_text( + """ +module fixed_update + implicit none +contains + subroutine relabel(value) + character(len=8), allocatable, intent(inout) :: value + value = 'fixed' + end subroutine relabel +end module fixed_update +""", + tmp_path, + module_name="fixed_update", + ) + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + + assert policy.supported is True + argument = policy.arguments[0] + assert argument.codegen_action is CodegenAction.CALL_LOCAL_INPUT + assert argument.projects_character_descriptor_update is True + assert argument.character_local is not None + assert argument.character_local.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE + assert argument.character_local.deferred_length is False + assert policy.writeback_actions == () + assert policy.results[0].updates_argument is True + + +def test_plain_fixed_length_string_update_keeps_copy_in_out_replacement(tmp_path: Path): + """A dummy with no descriptor attribute keeps the caller-buffer replacement. + + Nothing about that dummy rejects the caller's buffer as the actual + argument, so it stays on the writeback lane rather than gaining a result. + """ + module = _semantic_module_from_text( + """ +module plain_update + implicit none +contains + subroutine relabel(value) + character(len=8), intent(inout) :: value + value = 'fixed' + end subroutine relabel +end module plain_update +""", + tmp_path, + module_name="plain_update", + ) + policy = module.functions[0].metadata[RESOLVED_FUNCTION_WRAPPER_POLICY_METADATA] + + argument = policy.arguments[0] + assert argument.codegen_action is CodegenAction.COPY_IN_OUT + assert argument.projects_character_descriptor_update is False + assert policy.results == () + assert tuple(action.phase for action in policy.writeback_actions) == tuple(WritebackPhase) diff --git a/tests/fortran/strings/semantics/test_string_pyi_semantics.py b/tests/fortran/strings/semantics/test_string_pyi_semantics.py index d6025e4d5..8de38d5c5 100644 --- a/tests/fortran/strings/semantics/test_string_pyi_semantics.py +++ b/tests/fortran/strings/semantics/test_string_pyi_semantics.py @@ -35,11 +35,23 @@ def scalar_fixed(value: String[8]) -> None: ... def array_unknown(values: String[:][:]) -> None: ... def array_fixed(values: String[8][:]) -> None: ... def scalar_storage(value: String[8][()]) -> None: ... +def scalar_deferred(value: String[:]) -> None: ... +def array_assumed(values: String[...][:]) -> None: ... +def array_assumed_strided(values: String[...][::]) -> None: ... """, module_name="string_axes", ) - scalar_unknown, scalar_fixed, array_unknown, array_fixed, scalar_storage = module.functions + ( + scalar_unknown, + scalar_fixed, + array_unknown, + array_fixed, + scalar_storage, + scalar_deferred, + array_assumed, + array_assumed_strided, + ) = module.functions assert "fortran_character_length" not in scalar_unknown.arguments[0].semantic_type.metadata assert scalar_fixed.arguments[0].semantic_type.metadata["fortran_character_length"] == "8" @@ -59,20 +71,61 @@ def scalar_storage(value: String[8][()]) -> None: ... assert scalar_storage_type.metadata["fortran_character_length"] == "8" assert scalar_storage_type.storage.array.category == "scalar_storage" + deferred_type = scalar_deferred.arguments[0].semantic_type + assert deferred_type.metadata["fortran_character_length"] == ":" + assert deferred_type.rank == 0 + + assumed_type = array_assumed.arguments[0].semantic_type + assert assumed_type.metadata["fortran_character_length"] == "*" + assert assumed_type.rank == 1 + assert assumed_type.shape == [":"] + assert array_assumed_strided.arguments[0].semantic_type.shape == ["::Strided"] + emitted = emit_module(module) assert "value: String" in emitted assert "value: String[8]" in emitted assert "values: String[:][:]" in emitted assert "values: String[8][:]" in emitted assert "value: String[8][()]" in emitted + assert "value: String[:]" in emitted + assert "values: String[...][:]" in emitted + assert "values: String[...][::]" in emitted assert parse_pyi_text(emitted, module_name="string_axes") == module -def test_bare_string_slice_is_rejected_as_ambiguous(): - with pytest.raises(ValueError, match=r"String\[:\] is ambiguous.*String\[:\]\[:\].*String\[n\]"): +def test_scalar_string_length_subscription_spells_every_character_length(): + """One subscription after ``String`` is the character length, never a shape. + + A scalar carries assumed, explicit, or deferred length in that slot, so a + reader never has to infer which axis a single bracket describes. + """ + module = parse_pyi_text( + """ +def assumed(value: String) -> None: ... +def spelled_assumed(value: String[...]) -> None: ... +def explicit(value: String[8]) -> None: ... +def deferred(value: String[:]) -> None: ... +""", + module_name="string_lengths", + ) + lengths = [ + function.arguments[0].semantic_type.metadata.get("fortran_character_length") for function in module.functions + ] + assert lengths == [None, "*", "8", ":"] + assert all(function.arguments[0].semantic_type.rank == 0 for function in module.functions) + + +@pytest.mark.parametrize("spelling", ["String[::]", "String[1:8]"]) +def test_shape_spellings_are_rejected_in_the_character_length_slot(spelling: str): + """A shape belongs to the second subscription, so the first rejects one. + + ``String[:]`` and ``String[::]`` parse to the same Python AST, so the + contract's own text decides which of the two a length slot accepts. + """ + with pytest.raises(ValueError, match=r"is not a character length"): parse_pyi_text( - """ -def invalid(value: String[:]) -> None: ... + f""" +def invalid(value: {spelling}) -> None: ... """, module_name="string_axes", ) diff --git a/tests/fortran/subroutines/end_to_end/fixtures/assumed_scalar_intent.f90 b/tests/fortran/subroutines/end_to_end/fixtures/assumed_scalar_intent.f90 new file mode 100644 index 000000000..4ee1c58de --- /dev/null +++ b/tests/fortran/subroutines/end_to_end/fixtures/assumed_scalar_intent.f90 @@ -0,0 +1,40 @@ +module assumed_scalar_intent + implicit none + + type :: sample + real(8) :: x = 0.0d0 + end type sample + +contains + + real(8) function weighted(count, values, factor) + integer(4) :: count + real(8) :: values(:) + real(8) :: factor + integer(4) :: index + weighted = 0.0d0 + do index = 1, count + weighted = weighted + values(index) * factor + end do + end function weighted + + subroutine touch(count, item, values) + integer(4) :: count + type(sample) :: item + real(8) :: values(:) + count = count + 1 + item%x = item%x + 1.0d0 + values = values * 2.0d0 + end subroutine touch + + integer(4) function label_width(label) + character(len=4) :: label + label_width = len(label) + end function label_width + + subroutine declared(value) + real(8), intent(inout) :: value + value = value + 1.0d0 + end subroutine declared + +end module assumed_scalar_intent diff --git a/tests/fortran/subroutines/end_to_end/test_assumed_scalar_intent.py b/tests/fortran/subroutines/end_to_end/test_assumed_scalar_intent.py new file mode 100644 index 000000000..4aea04529 --- /dev/null +++ b/tests/fortran/subroutines/end_to_end/test_assumed_scalar_intent.py @@ -0,0 +1,75 @@ +"""Built-extension behavior of the assumed scalar-intent build option.""" + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_source_and_import + +pytestmark = pytest.mark.fortran_end_to_end + +SOURCE = Path(__file__).parent / "fixtures" / "assumed_scalar_intent.f90" +GENERATED = { + "bind_c_assumed_scalar_intent_wrapper.f90", + "assumed_scalar_intent_wrapper.c", + "assumed_scalar_intent_wrapper.h", +} + + +def _module(workdir: Path, *, assume_intent_in_scalars: bool): + return _build_source_and_import( + SOURCE, + workdir, + GENERATED, + assume_intent_in_scalars=assume_intent_in_scalars, + ) + + +def test_conservative_default_returns_every_undeclared_scalar(tmp_path: Path): + module = _module(tmp_path, assume_intent_in_scalars=False) + values = np.array([1.0, 2.0, 3.0], dtype=np.float64) + + assert module.weighted(np.int32(3), values, np.float64(2.0)) == ( + np.float64(12.0), + np.int32(3), + np.float64(2.0), + ) + + +def test_assumed_scalar_intent_returns_only_the_function_result(tmp_path: Path): + module = _module(tmp_path, assume_intent_in_scalars=True) + values = np.array([1.0, 2.0, 3.0], dtype=np.float64) + + assert module.weighted(np.int32(3), values, np.float64(2.0)) == np.float64(12.0) + + +def test_assumed_scalar_intent_keeps_array_and_derived_writeback(tmp_path: Path): + module = _module(tmp_path, assume_intent_in_scalars=True) + item = module.sample(x=np.float64(1.0)) + values = np.array([1.0, 2.0, 3.0], dtype=np.float64) + + assert module.touch(np.int32(5), item, values) is None + assert item.x == np.float64(2.0) + np.testing.assert_array_equal(values, np.array([2.0, 4.0, 6.0])) + + +def test_undeclared_character_scalar_follows_the_same_conservative_default(tmp_path: Path): + """A character dummy with no intent is returned exactly like a primitive one.""" + module = _module(tmp_path, assume_intent_in_scalars=False) + + assert module.label_width("abcd") == (np.int32(4), "abcd") + + +def test_assumed_scalar_intent_also_drops_the_character_result(tmp_path: Path): + module = _module(tmp_path, assume_intent_in_scalars=True) + + assert module.label_width("abcd") == np.int32(4) + + +def test_assumed_scalar_intent_does_not_change_a_declared_intent(tmp_path: Path): + module = _module(tmp_path, assume_intent_in_scalars=True) + + assert module.declared(np.float64(4.0)) == np.float64(5.0) diff --git a/tests/fortran/subroutines/policy/test_subroutine_output_policy.py b/tests/fortran/subroutines/policy/test_subroutine_output_policy.py index dbda093eb..3ace8b682 100644 --- a/tests/fortran/subroutines/policy/test_subroutine_output_policy.py +++ b/tests/fortran/subroutines/policy/test_subroutine_output_policy.py @@ -2,6 +2,7 @@ from tests.fortran._support.ownership_policy import parse_pyi_text +from tests.fortran._support.paths import FORTRAN_ROOT from prik.parsers.fortran.parser import parse_fortran_project from prik.pipeline.build import _apply_source_python_exports, _fortran_source_for_pipeline, _merge_wrapper_modules from prik.preprocessing import PreprocessingConfig @@ -17,7 +18,16 @@ FMATH_CONTRACT = Path("tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fmath/__init__.pyi") -CALLS_NATIVE = Path(__file__).parents[2] / "pyi_contracts" / "calls_and_results" / "end_to_end" / "fixtures" / "native" +CALLS_NATIVE = ( + FORTRAN_ROOT + / "infrastructure" + / "semantic_pyi" + / "contracts" + / "calls_and_results" + / "end_to_end" + / "fixtures" + / "native" +) def _source_semantic_module(filename: str, *, module_name: str): diff --git a/tests/fortran/subroutines/semantics/test_subroutine_argument_projection.py b/tests/fortran/subroutines/semantics/test_subroutine_argument_projection.py index da42c8fe0..8fdeea1a7 100644 --- a/tests/fortran/subroutines/semantics/test_subroutine_argument_projection.py +++ b/tests/fortran/subroutines/semantics/test_subroutine_argument_projection.py @@ -89,3 +89,61 @@ def test_scalar_derived_output_stays_visible_without_result_projection(): python_position=0, ) ] + + +ASSUMED_INTENT_SOURCE = """ +module legacy + type :: pt + real(8) :: x = 0.0d0 + end type pt +contains +subroutine touch(count, item, values, label, declared) + integer(4) :: count + type(pt) :: item + real(8) :: values(:) + character(len=4) :: label + integer(4), intent(inout) :: declared + count = count + 1 + item%x = item%x + 1.0d0 + values = values * 2.0d0 + label = "zzzz" + declared = declared + 1 +end subroutine touch +end module legacy +""" + + +def _touch_result_names(*, assume_intent_in_scalars): + smod = fortran_module_to_semantic_module( + parse_fortran_source(ASSUMED_INTENT_SOURCE), + assume_intent_in_scalars=assume_intent_in_scalars, + ) + touch = get_function(smod, "touch") + return [mapping.native_name for mapping in touch.projection if mapping.result_position is not None] + + +def test_undeclared_intent_scalar_projects_a_replacement_result_by_default(): + """Primitive and character scalars share one conservative default.""" + assert _touch_result_names(assume_intent_in_scalars=False) == ["count", "label", "declared"] + + +def test_assumed_scalar_intent_drops_only_the_undeclared_scalar_results(): + """The assumption reaches undeclared scalars, primitive and character alike. + + A declared ``intent(inout)`` scalar keeps its replacement result, and + arrays and derived-type objects were never projected as results, so their + in-place contract is unchanged either way. + """ + assert _touch_result_names(assume_intent_in_scalars=True) == ["declared"] + + +def test_assumed_scalar_intent_leaves_undeclared_non_scalars_writable(): + smod = fortran_module_to_semantic_module( + parse_fortran_source(ASSUMED_INTENT_SOURCE), + assume_intent_in_scalars=True, + ) + arguments = {argument.name: argument for argument in get_function(smod, "touch").arguments} + + assert arguments["count"].semantic_type.ownership.mutable is False + assert arguments["item"].semantic_type.ownership.mutable is True + assert arguments["values"].semantic_type.ownership.mutable is True diff --git a/tools/run_fortran_toolchain_lane.py b/tools/run_fortran_toolchain_lane.py index 77cd8f203..b7b213ee1 100644 --- a/tools/run_fortran_toolchain_lane.py +++ b/tools/run_fortran_toolchain_lane.py @@ -14,11 +14,11 @@ REPO_ROOT = Path(__file__).resolve().parents[1] PROFILE_TEST_PATHS = ( - "tests/fortran/building_shared_library/compiling/test_compiler_verbose.py", - "tests/fortran/source_preprocessing/preprocessing/test_configuration_and_adapters.py", + "tests/fortran/infrastructure/building/compiling/test_compiler_verbose.py", + "tests/fortran/infrastructure/preprocessing/test_configuration_and_adapters.py", ) FOCUSED_FORTRAN_CLI_NODES = ( - "tests/fortran/source_preprocessing/preprocessing/test_cli.py::" + "tests/fortran/infrastructure/preprocessing/test_cli.py::" "test_cli_fortran_compiler_mode_runs_exact_compiler_and_parses_stdout", )