diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 4a707998b..ea029c849 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -85,7 +85,13 @@ jobs: benchmarks/results/f2py.json \ benchmarks/results/prik.json \ benchmarks/results/f2py-build.json \ - benchmarks/results/prik-build.json + benchmarks/results/prik-build.json \ + benchmarks/results/f2py-direct.json \ + benchmarks/results/prik-direct.json \ + benchmarks/results/prik-adapted.json \ + benchmarks/results/f2py-direct-build.json \ + benchmarks/results/prik-direct-build.json \ + benchmarks/results/prik-adapted-build.json - name: Upload Performance snapshot and raw results uses: actions/upload-artifact@v4 @@ -100,11 +106,30 @@ jobs: benchmarks/results/prik-f2py-first.json benchmarks/results/f2py-build.json benchmarks/results/prik-build.json + benchmarks/results/f2py-direct.json + benchmarks/results/prik-direct.json + benchmarks/results/prik-adapted.json + benchmarks/results/f2py-direct-forward.json + benchmarks/results/f2py-direct-reverse.json + benchmarks/results/prik-direct-forward.json + benchmarks/results/prik-direct-reverse.json + benchmarks/results/prik-adapted-forward.json + benchmarks/results/prik-adapted-reverse.json + benchmarks/results/f2py-direct-build.json + benchmarks/results/prik-direct-build.json + benchmarks/results/prik-adapted-build.json docs/user/performance.md docs/user/assets/performance-comparison.svg docs/user/assets/build-time-comparison.svg retention-days: 90 + - name: Upload direct-entrypoint artifact preflight + uses: actions/upload-artifact@v4 + with: + name: direct-entrypoint-preflight + path: benchmarks/build/direct-runtime + retention-days: 90 + build: name: Documentation site build · Ubuntu 24.04 · Python 3.12 needs: benchmark diff --git a/.github/workflows/merge-validation.yml b/.github/workflows/merge-validation.yml index c7f9fa527..854f1236c 100644 --- a/.github/workflows/merge-validation.yml +++ b/.github/workflows/merge-validation.yml @@ -607,7 +607,13 @@ jobs: benchmarks/results/f2py.json \ benchmarks/results/prik.json \ benchmarks/results/f2py-build.json \ - benchmarks/results/prik-build.json + benchmarks/results/prik-build.json \ + benchmarks/results/f2py-direct.json \ + benchmarks/results/prik-direct.json \ + benchmarks/results/prik-adapted.json \ + benchmarks/results/f2py-direct-build.json \ + benchmarks/results/prik-direct-build.json \ + benchmarks/results/prik-adapted-build.json - name: Upload Performance snapshot and raw results uses: actions/upload-artifact@v4 with: @@ -621,11 +627,30 @@ jobs: benchmarks/results/prik-f2py-first.json benchmarks/results/f2py-build.json benchmarks/results/prik-build.json + benchmarks/results/f2py-direct.json + benchmarks/results/prik-direct.json + benchmarks/results/prik-adapted.json + benchmarks/results/f2py-direct-forward.json + benchmarks/results/f2py-direct-reverse.json + benchmarks/results/prik-direct-forward.json + benchmarks/results/prik-direct-reverse.json + benchmarks/results/prik-adapted-forward.json + benchmarks/results/prik-adapted-reverse.json + benchmarks/results/f2py-direct-build.json + benchmarks/results/prik-direct-build.json + benchmarks/results/prik-adapted-build.json docs/user/performance.md docs/user/assets/performance-comparison.svg docs/user/assets/build-time-comparison.svg retention-days: 90 + - name: Upload direct-entrypoint artifact preflight + uses: actions/upload-artifact@v4 + with: + name: direct-entrypoint-preflight + path: benchmarks/build/direct-runtime + retention-days: 90 + documentation-build: name: Documentation site build · Ubuntu 24.04 · Python 3.12 needs: documentation-benchmark diff --git a/CHANGELOG.md b/CHANGELOG.md index c1c7e020a..c8a81b5a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,39 @@ release tags add a leading `v` to the package version. ## Unreleased +### 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 `@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. +- Added selective direct routing for policy-proved Fortran `bind(C)` procedures, + including direct/mixed compiled feature fixtures and support-only generated + Fortran artifacts where independent helpers remain necessary. +- Added a separate direct-entrypoint PRIK/f2py runtime and clean-build benchmark + cohort with untimed correctness, generated-source membership, and linked + direct-symbol preflight, plus an ordinary-Fortran PRIK control. + +### Changed + +- Made nested semantic classes use the same complete planning and backend-symbol + allocation path as top-level classes. +- Published the maintained-run direct-entrypoint runtime, adapter-control, and + clean-build results separately from the normal-interface benchmark cohort. +- Made direct-entrypoint benchmark preflight identify binding and native + objects by their inspected symbol relationships across f2py build backends. +- Reduced exact numeric-scalar call overhead by using typed NumPy scalar + payload access and result allocation while preserving strict dtype checking + and exact NumPy result types. +- Separated wrapper plans into strict binding, shared native-entrypoint, and + Fortran-adapter facets, with planner-owned generated support procedure + entrypoints for accessors, lifecycles, descriptors, and callbacks, without + changing generated wrappers. +- Build manifest schema 3 records physical generated sources and separate + adapter/support membership, including zero-generated-native builds. + ## 0.3.0 — 2026-08-14 ### Added diff --git a/benchmarks/README.md b/benchmarks/README.md index 62bcaf727..0556795c1 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -4,6 +4,28 @@ This suite compares wrappers generated by prik and NumPy's f2py on the same machine. It measures both runtime call and NumPy-array overhead and clean, end-to-end build time. +The existing default-interface cohort remains unchanged. A separate +direct-entrypoint cohort uses `sources/direct_kernels.f90` for a no-op, scalar +function, and scalar subroutine. PRIK must call each `bind(C)` label without a +generated user-procedure adapter. f2py retains its Python C/API binding while +using `--no-wrap-functions --skip-empty-wrappers`; those flags suppress +unneeded generated Fortran procedure wrappers/files, not the Python binding. +An untimed preflight checks correctness, records the physical generated and +compiled sources, and verifies that each direct Python binding object refers to +the three user `bind(C)` labels while the native object and linked extension +define those labels. Object discovery uses those symbol relationships rather +than backend-specific filenames because Meson retains source suffixes in object +names while other f2py backends do not. The selected binding and native object +paths are recorded in the preflight report. These checks finish before either +cohort enters a timer. + +Every scalar case receives the same pre-created `numpy.float64` inputs and +produces the same numerical value. The timed calls retain each tool's natural +public result: PRIK returns the contract's exact `numpy.float64`, while f2py +returns a built-in `float`. The preflight checks those classes explicitly and +the runtime metadata records the result contract; the benchmark does not hide +the difference behind an untimed or Python-level normalization shim. + The default prik wrapper and the f2py wrappers measured here keep the GIL held, so the suite reports one like-for-like comparison of their normal generated interfaces. @@ -59,6 +81,20 @@ per-tool JSON suites, so comparison and publication commands remain unchanged. The script retains f2py's generated sources under `build/f2py` for local inspection. +Direct-entrypoint results are written separately as +`prik-direct.json`/`f2py-direct.json`, while the ordinary-Fortran PRIK control +is `prik-adapted.json`. Their clean-build counterparts end in `-build.json`. +Metadata identifies the `direct_c_abi` or `generated_fortran_adapter` route, +f2py wrapper mode, natural result class, compiler flags, process order, +affinity protocol, and inspected source membership. These files are published +as separate tables and are never merged into the default cohort or its +geometric mean. + +The three-route runtime control uses a forward/reverse sequence: +PRIK-direct, f2py-direct, PRIK-adapted, followed by PRIK-adapted, +f2py-direct, PRIK-direct. Clean-build rounds use the same reversal. This +balances every pair instead of always measuring the adapted control last. + To compare existing results without rebuilding: ```bash @@ -77,6 +113,15 @@ python3 -m pyperf compare_to \ --table ``` +Compare the direct-entrypoint cohort with: + +```bash +python3 -m pyperf compare_to results/f2py-direct.json results/prik-direct.json --table +python3 -m pyperf compare_to results/prik-adapted.json results/prik-direct.json --table +python3 -m pyperf compare_to results/f2py-direct-build.json results/prik-direct-build.json --table +python3 -m pyperf compare_to results/prik-adapted-build.json results/prik-direct-build.json --table +``` + Results are machine-specific. Compare files produced in the same run; CPU, compiler, Python, and NumPy differences can otherwise dominate small timings. The generated build directories, extensions, and result files are local @@ -84,8 +129,8 @@ artifacts rather than repository sources. ## Publish a Documentation Snapshot -After a completed run, refresh the generated sections of the public Performance -page and its chart with: +After a complete paired run on the maintained benchmark runner, refresh the +generated sections of the public Performance page and its chart with: ```bash python3 tools/generate_performance_docs.py @@ -94,9 +139,14 @@ python3 tools/generate_performance_docs.py Run this command from the repository root. It reads the runtime and build-time `pyperf` pairs, checks that each pair contains the same benchmarks and compatible platform metadata, records the host operating-system distribution and compiler, -and updates only the marked result sections in `docs/user/performance.md` plus -the runtime and clean-build comparison SVGs in `docs/user/assets/`. +and updates only the marked normal, direct, adapter-control, and build result +sections in `docs/user/performance.md` plus the normal runtime and clean-build +comparison SVGs in `docs/user/assets/`. Explanatory prose and the reproduction instructions remain hand-maintained. +Do not publish a local sample or revise the default cohort's geometric-mean +population. The generator keeps the direct-entrypoint and adapter-control +results in their own published sections and requires compatible maintained-run +metadata across every cohort. The Documentation workflow performs the same generation after successful correctness checks and rigorous measurements on pushes to `main`. The benchmark @@ -106,6 +156,10 @@ ARM64 with the expected Neoverse N2/Cobalt 100 CPU part before measuring. The documentation build and deployment remain separate x86-64 jobs. The workflow keeps the raw `pyperf` files as an artifact and overlays the generated snapshot only in the website build; it does not create a result commit. +It also uploads the complete untimed direct-entrypoint preflight directory as a +separate artifact so the pinned generated C sources, binding/native objects, +linked extensions, and symbol report remain inspectable alongside the +published timing tables. The publication environment pins Python 3.12, NumPy/f2py 2.5.1, pyperf 2.10.0, Meson 1.11.2, Ninja 1.13.0, GNU Fortran 13, and the ARM64 runner label. Update diff --git a/benchmarks/direct_benchmark.py b/benchmarks/direct_benchmark.py new file mode 100644 index 000000000..68e489119 --- /dev/null +++ b/benchmarks/direct_benchmark.py @@ -0,0 +1,365 @@ +"""Shared inputs and untimed verification for direct-entrypoint benchmarks.""" + +from __future__ import annotations + +from collections.abc import Mapping +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess # nosec B404 - fixed local benchmark build commands +import sys +from typing import Literal + +import numpy as np + + +BENCHMARK_ROOT = Path(__file__).resolve().parent +SOURCE_ROOT = BENCHMARK_ROOT / "sources" +DIRECT_SOURCE = SOURCE_ROOT / "direct_kernels.f90" +DIRECT_SIGNATURE = SOURCE_ROOT / "direct_kernels.pyf" +ADAPTED_SOURCE = SOURCE_ROOT / "adapted_kernels.f90" +OPTIMIZED_FLAGS = "-O3 -march=native -mtune=native" +ROUTES = ("prik-direct", "f2py-direct", "prik-adapted") +DIRECT_SYMBOLS = ("noop", "add_scalars", "add_scalars_out") +Route = Literal["prik-direct", "f2py-direct", "prik-adapted"] + + +def module_name(route: Route) -> str: + """Return the import name kept distinct for each measured route.""" + return { + "prik-direct": "bench_prik_direct", + "f2py-direct": "bench_f2py_direct", + "prik-adapted": "bench_prik_adapted", + }[route] + + +def route_action(route: Route) -> str: + """Return the completed PRIK route or equivalent comparison label.""" + return "generated_fortran_adapter" if route == "prik-adapted" else "direct_c_abi" + + +def wrapper_mode(route: Route) -> str: + """Describe the generated native wrapper mode without implying no C binding.""" + if route == "f2py-direct": + return "python_c_api;no_wrap_functions;skip_empty_wrappers;intent_c_signature" + if route == "prik-direct": + return "python_c_binding;no_user_fortran_adapter" + return "python_c_binding;generated_fortran_adapter" + + +def natural_result_type(route: Route) -> str: + """Return the unnormalized public scalar result class for one route.""" + return "builtins.float" if route == "f2py-direct" else "numpy.float64" + + +def native_source(route: Route) -> Path: + """Return the native source used by one route.""" + return ADAPTED_SOURCE if route == "prik-adapted" else DIRECT_SOURCE + + +def build_command( + route: Route, + workdir: Path, + *, + compiler: str, + jobs: int, +) -> tuple[str, ...]: + """Return a clean source-to-extension command for one benchmark route.""" + name = module_name(route) + generated = workdir / "generated" + source = str(native_source(route).resolve()) + if route.startswith("prik-"): + return ( + sys.executable, + "-m", + "prik", + source, + "--out", + name, + "--out-dir", + str(generated), + "--compiler", + compiler, + "--jobs", + str(jobs), + f"--native-compile-flags={OPTIMIZED_FLAGS}", + f"--wrapper-fortran-flags={OPTIMIZED_FLAGS}", + f"--wrapper-c-flags={OPTIMIZED_FLAGS}", + ) + return ( + sys.executable, + "-m", + "numpy.f2py", + "-c", + "-m", + name, + str(DIRECT_SIGNATURE.resolve()), + source, + "--build-dir", + str(generated), + "--no-wrap-functions", + "--skip-empty-wrappers", + f"--f77flags={OPTIMIZED_FLAGS}", + f"--f90flags={OPTIMIZED_FLAGS}", + f"--opt={OPTIMIZED_FLAGS}", + ) + + +def build_environment(compiler: str) -> dict[str, str]: + """Return the common optimized native compilation environment.""" + environment = dict(os.environ) + environment.update( + { + "CFLAGS": OPTIMIZED_FLAGS, + "FC": compiler, + "F77": compiler, + "F90": compiler, + "FFLAGS": OPTIMIZED_FLAGS, + "F90FLAGS": OPTIMIZED_FLAGS, + } + ) + return environment + + +def failure_message(command: tuple[str, ...], result: subprocess.CompletedProcess[str]) -> str: + """Render one failed fixed benchmark command with captured diagnostics.""" + stdout = result.stdout.rstrip() or "" + stderr = result.stderr.rstrip() or "" + return ( + f"Build command failed with exit code {result.returncode}:\n" + f"{' '.join(command)}\nstdout:\n{stdout}\nstderr:\n{stderr}" + ) + + +def run_build(route: Route, workdir: Path, *, compiler: str, jobs: int) -> None: + """Run one clean benchmark build; correctness and artifacts remain separate.""" + shutil.rmtree(workdir, ignore_errors=True) + workdir.mkdir(parents=True) + command = build_command(route, workdir, compiler=compiler, jobs=jobs) + result = subprocess.run( # nosec B603 - command uses fixed benchmark inputs + command, + cwd=workdir, + env=build_environment(compiler), + capture_output=True, + text=True, + check=False, + ) + if result.returncode: + raise RuntimeError(failure_message(command, result)) + + +def extension_path(route: Route, workdir: Path) -> Path: + """Locate the loadable extension output for the requested route.""" + plain_name = workdir / f"{module_name(route)}.so" + if plain_name.is_file(): + return plain_name + matches = tuple(workdir.rglob(f"{module_name(route)}*.so")) + if len(matches) != 1: + raise RuntimeError(f"Expected one {module_name(route)!r} extension, found {len(matches)}") + return matches[0] + + +def _global_symbols(path: Path) -> dict[str, str]: + """Return the globally visible symbol kinds reported by the platform tool.""" + inspector = shutil.which("nm") + if inspector is None: + raise RuntimeError("Direct benchmark artifact preflight requires nm") + result = subprocess.run( # nosec B603 - fixed tool and inspected build artifact + (inspector, "-g", "-P", str(path)), + capture_output=True, + text=True, + check=False, + ) + if result.returncode: + raise RuntimeError(f"Cannot inspect symbols in {path}: {result.stderr.rstrip() or result.stdout.rstrip()}") + symbols = {} + for line in result.stdout.splitlines(): + fields = line.split() + if len(fields) >= 2: + symbols[fields[0]] = fields[1] + return symbols + + +def _compiled_object_symbols( + workdir: Path, + files: tuple[str, ...], +) -> dict[Path, dict[str, str]]: + """Inspect every compiled object without relying on backend filenames.""" + return { + workdir / relative_path: _global_symbols(workdir / relative_path) + for relative_path in files + if Path(relative_path).suffix == ".o" + } + + +def _one_direct_symbol_object( + objects: Mapping[Path, Mapping[str, str]], + *, + expected_kinds: frozenset[str], + description: str, + relationship: str, +) -> Path: + """Return the unique object with the requested direct-symbol relationship.""" + matches = tuple( + path + for path, symbols in objects.items() + if all(symbols.get(symbol, "").upper() in expected_kinds for symbol in DIRECT_SYMBOLS) + ) + if len(matches) != 1: + observed = { + path.as_posix(): {symbol: symbols.get(symbol) for symbol in DIRECT_SYMBOLS} + for path, symbols in objects.items() + } + raise RuntimeError( + f"Expected one {description} that {relationship} every direct symbol, " + f"found {len(matches)}; observed {observed!r}" + ) + return matches[0] + + +def _require_symbol_kinds( + path: Path, + symbols: Mapping[str, str], + *, + expected_kinds: frozenset[str], + relationship: str, +) -> None: + """Require every direct label to have the planned object-file relationship.""" + mismatches = { + symbol: symbols.get(symbol) + for symbol in DIRECT_SYMBOLS + if symbols.get(symbol, "").upper() not in expected_kinds + } + if mismatches: + raise RuntimeError(f"{path} does not {relationship} every direct symbol: {mismatches!r}") + + +def _direct_symbol_report(route: Route, workdir: Path, files: tuple[str, ...], linked: Path) -> dict[str, object]: + """Prove binding references and native/linked definitions for a direct route.""" + if route == "prik-adapted": + return {} + object_symbols = _compiled_object_symbols(workdir, files) + binding_object = _one_direct_symbol_object( + object_symbols, + expected_kinds=frozenset({"U"}), + description="direct Python binding object", + relationship="refers to", + ) + native_object = _one_direct_symbol_object( + object_symbols, + expected_kinds=frozenset({"T", "W"}), + description="direct native object", + relationship="defines", + ) + _require_symbol_kinds( + binding_object, + object_symbols[binding_object], + expected_kinds=frozenset({"U"}), + relationship="refer to", + ) + _require_symbol_kinds( + native_object, + object_symbols[native_object], + expected_kinds=frozenset({"T", "W"}), + relationship="define", + ) + _require_symbol_kinds( + linked, + _global_symbols(linked), + expected_kinds=frozenset({"T", "W"}), + relationship="export", + ) + return { + "binding_direct_symbol_object": binding_object.relative_to(workdir).as_posix(), + "binding_direct_symbol_references": DIRECT_SYMBOLS, + "native_direct_symbol_object": native_object.relative_to(workdir).as_posix(), + "native_direct_symbol_definitions": DIRECT_SYMBOLS, + "linked_direct_symbol_definitions": DIRECT_SYMBOLS, + } + + +def artifact_report(route: Route, workdir: Path) -> dict[str, object]: + """Validate and report generated/compiled membership outside any timer.""" + files = tuple(sorted(path.relative_to(workdir).as_posix() for path in workdir.rglob("*") if path.is_file())) + names = tuple(Path(path).name for path in files) + adapter_sources = tuple(path for path in files if Path(path).name.startswith("bind_c_") and path.endswith(".f90")) + f2py_wrapper_sources = tuple( + path + for path in files + if "f2pywrappers" in Path(path).name.casefold() and Path(path).suffix.casefold().startswith(".f") + ) + if route == "prik-direct" and adapter_sources: + raise RuntimeError(f"PRIK direct preflight found generated user adapters: {adapter_sources!r}") + if route == "prik-adapted" and not adapter_sources: + raise RuntimeError("PRIK adapted preflight found no generated Fortran adapter source") + if route == "f2py-direct" and f2py_wrapper_sources: + raise RuntimeError(f"f2py direct preflight found generated Fortran wrapper sources: {f2py_wrapper_sources!r}") + if route == "f2py-direct" and not any(name.endswith("module.c") for name in names): + raise RuntimeError("f2py direct preflight found no generated Python C/API module") + if route.startswith("prik-") and not any(name.endswith("_wrapper.c") for name in names): + raise RuntimeError("PRIK preflight found no generated Python C binding") + linked = extension_path(route, workdir) + report = { + "route": route_action(route), + "wrapper_mode": wrapper_mode(route), + "native_source": native_source(route).name, + "generated_fortran_adapter_sources": adapter_sources, + "f2py_fortran_wrapper_sources": f2py_wrapper_sources, + "generated_c_sources": tuple(path for path in files if path.endswith(".c")), + "compiled_objects": tuple(path for path in files if path.endswith(".o")), + "linked_extension": linked.relative_to(workdir).as_posix(), + } + report.update(_direct_symbol_report(route, workdir, files, linked)) + return report + + +def load_extension(route: Route, workdir: Path): + """Load an isolated extension directly from a completed build.""" + path = extension_path(route, workdir) + spec = importlib.util.spec_from_file_location(module_name(route), path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Cannot load extension specification from {path}") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def check_api(api, route: Route) -> None: + """Check common inputs/values and each tool's natural scalar result class.""" + assert api.noop() is None + results = ( + api.add_scalars(np.float64(1.25), np.float64(2.75)), + api.add_scalars_out(np.float64(1.25), np.float64(2.75)), + ) + expected_type = float if route == "f2py-direct" else np.float64 + for result in results: + assert type(result) is expected_type + assert result == np.float64(4.0) + + +def verify_build(route: Route, workdir: Path) -> dict[str, object]: + """Validate artifacts, importability, and runtime results outside timing.""" + report = artifact_report(route, workdir) + check_api(load_extension(route, workdir), route) + return report + + +def compact_artifact_membership(report: Mapping[str, object]) -> str: + """Return stable route facts suitable for scalar pyperf metadata.""" + adapters = len(tuple(report["generated_fortran_adapter_sources"])) + f2py_wrappers = len(tuple(report["f2py_fortran_wrapper_sources"])) + c_sources = len(tuple(report["generated_c_sources"])) + objects = len(tuple(report["compiled_objects"])) + direct_symbols = len(tuple(report.get("linked_direct_symbol_definitions", ()))) + return ( + f"generated_c={c_sources};fortran_adapters={adapters};" + f"f2py_fortran_wrappers={f2py_wrappers};objects={objects};direct_symbols={direct_symbols}" + ) + + +def write_preflight_report(reports: Mapping[str, Mapping[str, object]], path: Path) -> None: + """Write the complete untimed artifact/correctness record.""" + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(reports, indent=2, sort_keys=True) + "\n", encoding="utf-8") diff --git a/benchmarks/direct_build_time.py b/benchmarks/direct_build_time.py new file mode 100644 index 000000000..35176b9c2 --- /dev/null +++ b/benchmarks/direct_build_time.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Measure clean small builds for direct PRIK/f2py and adapted PRIK routes.""" + +from __future__ import annotations + +import argparse +from datetime import datetime +import os +from pathlib import Path +import platform +import shutil +import subprocess # nosec B404 - fixed local benchmark build commands +import sys +from tempfile import TemporaryDirectory +import time + +import numpy as np +import pyperf + +if __package__: + from .direct_benchmark import ( + BENCHMARK_ROOT, + OPTIMIZED_FLAGS, + ROUTES, + Route, + build_command, + build_environment, + compact_artifact_membership, + failure_message, + route_action, + run_build, + verify_build, + wrapper_mode, + ) +else: + from direct_benchmark import ( + BENCHMARK_ROOT, + OPTIMIZED_FLAGS, + ROUTES, + Route, + build_command, + build_environment, + compact_artifact_membership, + failure_message, + route_action, + run_build, + verify_build, + wrapper_mode, + ) + + +RESULTS_ROOT = BENCHMARK_ROOT / "results" + + +def available_jobs() -> int: + """Return the compiler-process budget available to the current job.""" + try: + return max(1, len(os.sched_getaffinity(0))) + except AttributeError: + return max(1, os.cpu_count() or 1) + + +def route_order(first: str, round_index: int) -> tuple[Route, Route, Route]: + """Reverse all three routes so every pair receives both relative orders.""" + direct: tuple[Route, Route] = ("prik-direct", "f2py-direct") if first == "prik" else ("f2py-direct", "prik-direct") + if round_index % 2: + return "prik-adapted", direct[1], direct[0] + return direct[0], direct[1], "prik-adapted" + + +def timed_build(route: Route, workdir: Path, *, compiler: str, jobs: int) -> tuple[float, dict[str, object]]: + """Time generation/compilation/linking, then verify artifacts and behavior.""" + shutil.rmtree(workdir, ignore_errors=True) + workdir.mkdir(parents=True) + command = build_command(route, workdir, compiler=compiler, jobs=jobs) + started = time.perf_counter() + result = subprocess.run( # nosec B603 - command uses fixed benchmark inputs + command, + cwd=workdir, + env=build_environment(compiler), + capture_output=True, + text=True, + check=False, + ) + elapsed = time.perf_counter() - started + if result.returncode: + raise RuntimeError(failure_message(command, result)) + report = verify_build(route, workdir) + return elapsed, report + + +def preflight(*, compiler: str, jobs: int, root: Path) -> dict[Route, dict[str, object]]: + """Prove correctness and physical membership before any measured round.""" + reports: dict[Route, dict[str, object]] = {} + for route_value in ROUTES: + route: Route = route_value + workdir = root / route + run_build(route, workdir, compiler=compiler, jobs=jobs) + reports[route] = verify_build(route, workdir) + return reports + + +def _write_suite( + route: Route, + values: list[float], + report: dict[str, object], + *, + runs: int, + warmups: int, + first: str, + compiler: str, + jobs: int, + results_root: Path, +) -> Path: + """Write one route-specific pyperf build suite with preflight membership.""" + metadata = { + "artifact_membership": compact_artifact_membership(report), + "benchmark_cohort": "direct_entrypoint", + "binding_tool": route, + "build_first_tool": first, + "build_order_protocol": "balanced_three_route_forward_reverse", + "build_runs": runs, + "build_scope": "clean small source-to-extension generation, compilation, and linking", + "build_warmups": warmups, + "compile_flags": OPTIMIZED_FLAGS, + "compiler": compiler, + "date": datetime.now().isoformat(sep=" "), + "name": "direct.build.optimized.small", + "native_source": report["native_source"], + "numpy_version": np.__version__, + "platform_details": platform.platform(), + "prik_build_jobs": jobs, + "route": route_action(route), + "wrapper_mode": wrapper_mode(route), + } + if cpu_model := os.environ.get("PRIK_BENCHMARK_CPU_MODEL"): + metadata["cpu_model_name"] = cpu_model + run = pyperf.Run(values, metadata=metadata, collect_metadata=True) + suite = pyperf.BenchmarkSuite([pyperf.Benchmark([run])]) + results_root.mkdir(parents=True, exist_ok=True) + path = results_root / f"{route}-build.json" + suite.dump(str(path), replace=True) + return path + + +def run_benchmarks( + *, + runs: int, + warmups: int, + first: str, + compiler: str, + jobs: int, + results_root: Path = RESULTS_ROOT, +) -> tuple[Path, Path, Path]: + """Run preflight and measured clean builds without touching default results.""" + values: dict[Route, list[float]] = {route: [] for route in ROUTES} + reports: dict[Route, dict[str, object]] + with TemporaryDirectory(prefix="prik-direct-build-benchmark-") as temporary: + root = Path(temporary) + reports = preflight(compiler=compiler, jobs=jobs, root=root / "preflight") + for round_index in range(warmups + runs): + measured = round_index >= warmups + phase = "run" if measured else "warm-up" + phase_index = round_index - warmups + 1 if measured else round_index + 1 + phase_total = runs if measured else warmups + order_index = phase_index - 1 + for route in route_order(first, order_index): + elapsed, report = timed_build( + route, + root / "timed" / route, + compiler=compiler, + jobs=jobs, + ) + if compact_artifact_membership(report) != compact_artifact_membership(reports[route]): + raise RuntimeError(f"Artifact membership changed after {route!r} preflight") + print(f"{phase} {phase_index}/{phase_total}: {route} took {elapsed:.3f} sec", flush=True) + if measured: + values[route].append(elapsed) + return tuple( + _write_suite( + route, + values[route], + reports[route], + runs=runs, + warmups=warmups, + first=first, + compiler=compiler, + jobs=jobs, + results_root=results_root, + ) + for route in ROUTES + ) + + +def parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--runs", type=int, default=4) + parser.add_argument("--warmups", type=int, default=1) + parser.add_argument("--first", choices=("prik", "f2py"), default="prik") + parser.add_argument("--compiler", default="gfortran") + parser.add_argument("--jobs", type=int, default=None) + parser.add_argument("--results-dir", type=Path, default=RESULTS_ROOT) + args = parser.parse_args(argv) + if args.runs < 2: + parser.error("--runs must be at least 2") + if args.warmups < 0: + parser.error("--warmups must be non-negative") + if args.jobs is not None and args.jobs < 1: + parser.error("--jobs must be a positive integer") + return args + + +def main(argv: list[str] | None = None) -> int: + args = parse_args(list(argv or sys.argv[1:])) + compiler = shutil.which(args.compiler) + if compiler is None: + print(f"cannot run direct build benchmark: compiler not found: {args.compiler}", file=sys.stderr) + return 2 + try: + paths = run_benchmarks( + runs=args.runs, + warmups=args.warmups, + first=args.first, + compiler=compiler, + jobs=args.jobs or available_jobs(), + results_root=args.results_dir, + ) + except (OSError, RuntimeError, ValueError) as exc: + print(f"cannot run direct build benchmark: {exc}", file=sys.stderr) + return 2 + print(f"Wrote direct/adapted clean-build results to {', '.join(str(path) for path in paths)}.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/direct_preflight.py b/benchmarks/direct_preflight.py new file mode 100644 index 000000000..6f7fbfe3f --- /dev/null +++ b/benchmarks/direct_preflight.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Build and verify every direct-entrypoint benchmark route before timing.""" + +from __future__ import annotations + +import os +import shutil +import sys + +if __package__: + from .direct_benchmark import ( + BENCHMARK_ROOT, + ROUTES, + Route, + extension_path, + run_build, + verify_build, + write_preflight_report, + ) +else: + from direct_benchmark import ( + BENCHMARK_ROOT, + ROUTES, + Route, + extension_path, + run_build, + verify_build, + write_preflight_report, + ) + + +def _available_jobs() -> int: + try: + return max(1, len(os.sched_getaffinity(0))) + except AttributeError: + return 1 + + +def main() -> int: + compiler = shutil.which("gfortran") + if compiler is None: + print("cannot run direct benchmark preflight: gfortran not found", file=sys.stderr) + return 2 + root = BENCHMARK_ROOT / "build" / "direct-runtime" + reports: dict[str, dict[str, object]] = {} + try: + for route_value in ROUTES: + route: Route = route_value + workdir = root / route + run_build(route, workdir, compiler=compiler, jobs=_available_jobs()) + reports[route] = verify_build(route, workdir) + extension = extension_path(route, workdir) + for old_extension in BENCHMARK_ROOT.glob(f"{extension.name.split('.')[0]}*.so"): + old_extension.unlink() + shutil.copy2(extension, BENCHMARK_ROOT / extension.name) + except (OSError, RuntimeError, ValueError) as exc: + print(f"direct benchmark preflight failed: {exc}", file=sys.stderr) + return 2 + report_path = root / "preflight.json" + write_preflight_report(reports, report_path) + print(f"Direct benchmark correctness and artifact preflight passed; wrote {report_path}.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/benchmarks/direct_runtime.py b/benchmarks/direct_runtime.py new file mode 100644 index 000000000..bd29d5b09 --- /dev/null +++ b/benchmarks/direct_runtime.py @@ -0,0 +1,88 @@ +"""Measure direct and adapted scalar call overhead after untimed preflight.""" + +from __future__ import annotations + +import importlib +import json +import os +from pathlib import Path +import platform +import sys + +import numpy as np +import pyperf + +if __package__: + from .direct_benchmark import ( + BENCHMARK_ROOT, + OPTIMIZED_FLAGS, + Route, + check_api, + compact_artifact_membership, + module_name, + natural_result_type, + route_action, + wrapper_mode, + ) +else: + from direct_benchmark import ( + BENCHMARK_ROOT, + OPTIMIZED_FLAGS, + Route, + check_api, + compact_artifact_membership, + module_name, + natural_result_type, + route_action, + wrapper_mode, + ) + + +route_value = os.environ.get("PRIK_DIRECT_BENCHMARK_ROUTE") +order_pass = os.environ.get("PRIK_DIRECT_ORDER_PASS") +if route_value not in {"prik-direct", "f2py-direct", "prik-adapted"}: + raise RuntimeError("Set PRIK_DIRECT_BENCHMARK_ROUTE to prik-direct, f2py-direct, or prik-adapted.") +if order_pass not in {"forward", "reverse"}: + raise RuntimeError("Set PRIK_DIRECT_ORDER_PASS to forward or reverse.") +route: Route = route_value +api = importlib.import_module(module_name(route)) +check_api(api, route) +preflight_path = Path( + os.environ.get("PRIK_DIRECT_PREFLIGHT_REPORT", BENCHMARK_ROOT / "build/direct-runtime/preflight.json") +) +if not preflight_path.is_file(): + raise RuntimeError(f"Run direct_preflight.py before timing; missing {preflight_path}") +preflight = json.loads(preflight_path.read_text(encoding="utf-8")) +artifact_membership = compact_artifact_membership(preflight[route]) + +metadata = { + "binding_tool": route, + "benchmark_cohort": "direct_entrypoint", + "artifact_membership": artifact_membership, + "compile_flags": OPTIMIZED_FLAGS, + "gil_policy": "held", + "numpy_version": np.__version__, + "natural_result_type": natural_result_type(route), + "platform_details": platform.platform(), + "python_version": sys.version, + "route": route_action(route), + "runtime_order_pass": order_pass, + "runtime_order_protocol": "balanced_three_route_forward_reverse", + "wrapper_mode": wrapper_mode(route), +} +if cpu_model := os.environ.get("PRIK_BENCHMARK_CPU_MODEL"): + metadata["cpu_model_name"] = cpu_model +runner = pyperf.Runner(processes=16, values=4, metadata=metadata) +runner.timeit("direct.call.noop", stmt="fn()", globals={"fn": api.noop}, duplicate=100) +runner.timeit( + "direct.call.scalar_function", + stmt="fn(a, b)", + globals={"fn": api.add_scalars, "a": np.float64(1.25), "b": np.float64(2.75)}, + duplicate=50, +) +runner.timeit( + "direct.call.scalar_subroutine", + stmt="fn(a, b)", + globals={"fn": api.add_scalars_out, "a": np.float64(1.25), "b": np.float64(2.75)}, + duplicate=50, +) diff --git a/benchmarks/run.sh b/benchmarks/run.sh index 58d95d365..60c29fbe8 100644 --- a/benchmarks/run.sh +++ b/benchmarks/run.sh @@ -39,6 +39,10 @@ echo "========================================" echo echo "Check correctness of all shared libraries..." python3 correctness.py + +echo +echo "Check direct/adapted correctness and generated artifacts..." +python3 direct_preflight.py echo echo "========================================" echo "========================================" @@ -55,6 +59,15 @@ python3 build_time.py \ --warmups "${PRIK_BUILD_BENCHMARK_WARMUPS:-1}" \ --first "$benchmark_first" +echo +echo "========================================" +echo " Benchmarking direct/adapted clean builds" +echo "========================================" +python3 direct_build_time.py \ + --runs "${PRIK_DIRECT_BUILD_BENCHMARK_RUNS:-4}" \ + --warmups "${PRIK_DIRECT_BUILD_BENCHMARK_WARMUPS:-1}" \ + --first "$benchmark_first" + runtime_groups=( calls vector-latency @@ -105,11 +118,63 @@ for binding_tool in prik f2py; do --output "results/$binding_tool.json" done +direct_runtime_passes=(forward reverse) +for runtime_pass in "${direct_runtime_passes[@]}"; do + case "$runtime_pass" in + forward) + direct_routes=(prik-direct f2py-direct prik-adapted) + ;; + reverse) + direct_routes=(prik-adapted f2py-direct prik-direct) + ;; + esac + echo "Direct runtime benchmark order ($runtime_pass): ${direct_routes[*]}" + for direct_route in "${direct_routes[@]}"; do + PRIK_DIRECT_BENCHMARK_ROUTE="$direct_route" \ + PRIK_DIRECT_ORDER_PASS="$runtime_pass" \ + OMP_NUM_THREADS=1 \ + OPENBLAS_NUM_THREADS=1 \ + MKL_NUM_THREADS=1 \ + python3 direct_runtime.py \ + --rigorous \ + --affinity=0 \ + --inherit-environ=PRIK_DIRECT_BENCHMARK_ROUTE,PRIK_DIRECT_ORDER_PASS,PRIK_DIRECT_PREFLIGHT_REPORT,PRIK_BENCHMARK_CPU_MODEL,OMP_NUM_THREADS,OPENBLAS_NUM_THREADS,MKL_NUM_THREADS \ + -o "results/$direct_route-$runtime_pass.json" + done +done + +for direct_route in prik-direct f2py-direct prik-adapted; do + python3 -m pyperf convert \ + "results/$direct_route-forward.json" \ + --add "results/$direct_route-reverse.json" \ + --output "results/$direct_route.json" +done + python3 -m pyperf compare_to \ results/f2py.json \ results/prik.json \ --table +python3 -m pyperf compare_to \ + results/f2py-direct.json \ + results/prik-direct.json \ + --table + +python3 -m pyperf compare_to \ + results/prik-adapted.json \ + results/prik-direct.json \ + --table + +python3 -m pyperf compare_to \ + results/f2py-direct-build.json \ + results/prik-direct-build.json \ + --table + +python3 -m pyperf compare_to \ + results/prik-adapted-build.json \ + results/prik-direct-build.json \ + --table + python3 -m pyperf compare_to \ results/f2py-build.json \ results/prik-build.json \ diff --git a/benchmarks/sources/adapted_kernels.f90 b/benchmarks/sources/adapted_kernels.f90 new file mode 100644 index 000000000..c77185b27 --- /dev/null +++ b/benchmarks/sources/adapted_kernels.f90 @@ -0,0 +1,15 @@ +subroutine noop() +end subroutine noop + +real(8) function add_scalars(a, b) result(c) + real(8), intent(in) :: a, b + + c = a + b +end function add_scalars + +subroutine add_scalars_out(a, b, c) + real(8), intent(in) :: a, b + real(8), intent(out) :: c + + c = a + b +end subroutine add_scalars_out diff --git a/benchmarks/sources/direct_kernels.f90 b/benchmarks/sources/direct_kernels.f90 new file mode 100644 index 000000000..41a1f8247 --- /dev/null +++ b/benchmarks/sources/direct_kernels.f90 @@ -0,0 +1,18 @@ +subroutine noop() bind(C) + use iso_c_binding +end subroutine noop + +real(c_double) function add_scalars(a, b) bind(C) result(c) + use iso_c_binding + real(c_double), value, intent(in) :: a, b + + c = a + b +end function add_scalars + +subroutine add_scalars_out(a, b, c) bind(C) + use iso_c_binding + real(c_double), value, intent(in) :: a, b + real(c_double), intent(out) :: c + + c = a + b +end subroutine add_scalars_out diff --git a/benchmarks/sources/direct_kernels.pyf b/benchmarks/sources/direct_kernels.pyf new file mode 100644 index 000000000..31d9efbe4 --- /dev/null +++ b/benchmarks/sources/direct_kernels.pyf @@ -0,0 +1,21 @@ +python module bench_f2py_direct + interface + subroutine noop() + intent(c) noop + end subroutine noop + function add_scalars(a, b) + use iso_c_binding + intent(c) add_scalars + real(kind=c_double), intent(c) :: add_scalars + real(kind=c_double), intent(c) :: a + real(kind=c_double), intent(c) :: b + end function add_scalars + subroutine add_scalars_out(a, b, c) + use iso_c_binding + intent(c) add_scalars_out + real(kind=c_double), intent(c) :: a + real(kind=c_double), intent(c) :: b + real(kind=c_double), intent(out) :: c + end subroutine add_scalars_out + end interface +end python module bench_f2py_direct diff --git a/docs/developer/architecture.md b/docs/developer/architecture.md index 7d1af4a03..d7f2de317 100644 --- a/docs/developer/architecture.md +++ b/docs/developer/architecture.md @@ -77,8 +77,8 @@ function illustrates the representations produced across the pipeline. | `parsers/` | A `FortranProject` with source-faithful function, argument, type, and `intent` facts. | Record syntax and source-located diagnostics without deciding wrapper behavior. | | `semantics/` | A language-neutral semantic model ([`SemanticModule`](packages/semantics.md)) containing a callable and its stable type, shape, origin, and raw contract metadata. | Give frontend facts a shared meaning. | | `policy/` | The semantic model with complete export, transport, ownership, projection, lifecycle, and support choices. | Decide how the callable may interoperate with Python. | -| `planning/` | A deterministic wrapper plan ([`ModulePlan`](packages/planning.md)) with ordered binding and bridge operations, names, and build requirements. | Project and validate completed choices without making new policy. | -| `codegen/` | Planned CPython-binding and Fortran-bridge nodes plus Python-facade representation. | Implement the plan-selected mechanisms. | +| `planning/` | A deterministic wrapper plan ([`ModulePlan`](packages/planning.md)) with binding, shared native-entrypoint, and bridge facets, ordered C ABI and original-Fortran call records, names, and build requirements. | Project and validate completed choices without making new policy. | +| `codegen/` | CPython-binding nodes from binding plus entrypoint facets, and Fortran-bridge nodes from entrypoint plus bridge facets, with Python-facade representation. | Implement the plan-selected mechanisms. | | `printers/` | Generated C and Fortran source text. | Serialize formed nodes without deciding behavior. | | `compiler/` | Recorded or executed native commands and a linked extension. | Compile and link the explicit native inputs. | | Extension module and [`runtime/`](packages/runtime.md) | An importable `scale` module; its call returns `np.float64(7.5)` for `np.float64(3.0)` and `np.float64(2.5)`. | The generated public Python interface and any imported runtime support it uses. | @@ -123,6 +123,9 @@ The architecture preserves these invariants: - Code generation implements a plan; printers only serialize it. - Pipeline orchestrates stages; it does not become a parser, policy engine, or lowering backend. +- Internal tree and declaration traversals materialize explicit ordered + collections. Callers receive a complete reusable stage input rather than + depending on hidden generator control flow. - Supported behavior has focused owner-stage evidence and, when public, end-to-end evidence. diff --git a/docs/developer/packages/codegen.md b/docs/developer/packages/codegen.md index 57f171af8..aa2ad1c16 100644 --- a/docs/developer/packages/codegen.md +++ b/docs/developer/packages/codegen.md @@ -113,11 +113,16 @@ inspection, not overload matching or dispatch policy. ### Specialized lowerings -The C binding and Fortran bridge lower separate, completed views of the same -plan. Their algorithms, source-printing examples, and failure boundaries are -described in [C Binding Lowering](codegen/c-binding.md) and [Fortran Bridge +The C binding lowers binding plus native-entrypoint views; the Fortran bridge +lowers that same entrypoint plus bridge-local views. The entrypoint is their +shared, bidirectional C ABI contract, while original Fortran invocation facts +remain unavailable to the C binding. Their algorithms, source-printing +examples, and failure boundaries are described in [C Binding +Lowering](codegen/c-binding.md) and [Fortran Bridge Lowering](codegen/fortran-bridge.md). Neither page assigns policy completion to -code generation. +code generation. A direct C-ABI entrypoint is called by the same C-binding +lowering path and has no Fortran adapter facet; adapter-selected operations +continue through the Fortran bridge. ### `checks.py`: maintainability recommendations diff --git a/docs/developer/packages/codegen/c-binding.md b/docs/developer/packages/codegen/c-binding.md index 51af48ee4..13565938d 100644 --- a/docs/developer/packages/codegen/c-binding.md +++ b/docs/developer/packages/codegen/c-binding.md @@ -12,20 +12,41 @@ publication: reviewed ## Role And Boundary [`prik/codegen/c/binding.py`](../../../../prik/codegen/c/binding.py) lowers the -binding view of a completed `ModulePlan` into a `CModule` and `CHeader`. The -result is CPython and NumPy C syntax nodes, not source text and not a compiled -extension. `CSourcePrinter` serializes the nodes later. - -The generator implements the Python boundary selected by the plan: argument -conversion, bridge calls, Python results, errors, lifecycle actions, extension -initialization, and generated Python surfaces. It may select local names and -the necessary C syntax, but never chooses ownership, optionality, storage, or +binding and native-entrypoint views of a completed `ModulePlan` into a +`CModule` and `CHeader`. The result is CPython and NumPy C syntax nodes, not +source text and not a compiled extension. `CSourcePrinter` serializes the +nodes later. + +The binding view owns Python extraction, validation, local storage, returned or +output C storage, Python result construction, errors, lifecycle actions, +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. +Ordinary functions use their function-owned entrypoint. Every other externally +linked generated call is looked up in the generated support procedure registry. +That includes constructors, field/member accessors, derived-origin and holder +operations, descriptor helpers, and callback trampolines. The C lowerer may +create static Python helpers, but it does not invent an external symbol or C +prototype when a generated support procedure entrypoint is missing. A +binding-implemented callback trampoline uses the same record for its function +definition that the Fortran side uses for its interface. + +Binding-local derived capsule destructors, holder presence methods, and private +field methods follow the explicit module binding-support inventories. The +lowerer may join their planned owner paths to namespace-owned derived-type +records for emitted names and fields. It does not reconstruct those inventories +from results, arguments, constructors, release actions, or holder storage. Any +native call made by a local helper still obtains its existence, symbol, and ABI +from the generated support procedure registry. + ## Input And Output ```text -ModulePlan.binding + namespaces + function binding views +ModulePlan.binding + ModulePlan.entrypoint + + namespaces + function binding/entrypoint views -> CBindingGenerator.require_supported() -> CBindingGenerator.visit() -> CModule + CHeader @@ -36,14 +57,21 @@ ModulePlan.binding + namespaces + function binding views `require_supported()` checks that the already selected primitive spellings are available. It is capability preflight, not a second policy pass. +Numeric scalar boundaries retain their exact NumPy contract without using the +generic dtype-conversion path on a successful call. The native support helper +checks the planned NumPy scalar class, reads its typed payload directly, and +allocates the matching typed NumPy scalar for a result. Type mismatches still +follow the generated diagnostic path; this fast path changes neither accepted +inputs nor returned result types. + ## Lowering Algorithm `_visit_ModulePlan()` returns the paired C module and header. `binding_module()` collects namespace functions, determines whether the plan requires runtime helpers, and assembles declarations and functions in emitted dependency order: shared helpers, class and descriptor support, wrappers, overload dispatchers, -then module initialization. `binding_header()` derives bridge prototypes from -the same plan. +then module initialization. `binding_header()` lowers prototypes from the +shared entrypoint records. `_visit_FunctionPlan()` works in three ordered parts: @@ -51,7 +79,8 @@ the same plan. statements. 2. It applies the plan's `argument_conversion_order`; each transfer dispatches on its completed optional, callback, descriptor, or derived facet. -3. It emits the bridge call, selected result construction, and lifecycle work. +3. It invokes the planned entrypoint, receives its direct or output-parameter + C storage, and applies selected result construction and lifecycle work. `PythonSurfaceEmitter` is used only when the plan contains generated classes, holders, or module proxies. `CBindingNames` keeps its private C symbols aligned @@ -75,11 +104,15 @@ Expand the full source to run the complete example. ```python binding = BindingFunctionPlan(...) +entrypoint = NativeEntrypointFunctionPlan(...) bridge = BridgeFunctionPlan(...) -function = FunctionPlan(..., binding=binding, bridge=bridge) +function = FunctionPlan( + ..., binding=binding, entrypoint=entrypoint, bridge=bridge +) namespace = NamespacePlan(..., functions=(function,)) plan = ModulePlan( binding=BindingModulePlan(...), + entrypoint=NativeEntrypointModulePlan(...), bridge=BridgeModulePlan(...), namespaces=(namespace,), ) @@ -97,9 +130,13 @@ print(CSourcePrinter().doprint(...)) from prik.codegen.c.binding import CBindingGenerator from prik.planning.models import ( BindingFunctionPlan, BindingModulePlan, BridgeFunctionPlan, - BridgeModulePlan, FunctionPlan, ModulePlan, NamespacePlan, + BridgeModulePlan, FunctionPlan, ModulePlan, + NativeEntrypointFunctionPlan, NativeEntrypointModulePlan, + NativeGeneratedCodeGroupKind, NativeGeneratedCodeGroupPlan, NamespacePlan, +) +from prik.policy.models import ( + ExternalDeclarationMode, NativeEntrypointAction, NativeInvocationKind, ) -from prik.policy.models import ExternalDeclarationMode, NativeInvocationKind from prik.printers.c import CSourcePrinter binding = BindingFunctionPlan( @@ -118,15 +155,22 @@ bridge = BridgeFunctionPlan( native_module=None, native_is_subroutine=True, ) +entrypoint = NativeEntrypointFunctionPlan( + symbol_name="bind_c_ping", + action=NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER, + parameters=(), + results=(), + projected_slots=(), +) function = FunctionPlan( owner_path="demo.ping", symbol_name="ping", binding=binding, + entrypoint=entrypoint, bridge=bridge, class_call=None, arguments=(), results=(), - native_call_slots=(), declaration_callables=(), available_roles=(), ) @@ -139,8 +183,17 @@ namespace = NamespacePlan( plan = ModulePlan( owner_path="demo", binding=BindingModulePlan(owner_path="demo"), + entrypoint=NativeEntrypointModulePlan(owner_path="demo"), bridge=BridgeModulePlan(owner_path="demo"), namespaces=(namespace,), + native_generated_code_groups=( + NativeGeneratedCodeGroupPlan( + kind=NativeGeneratedCodeGroupKind.FORTRAN_ADAPTERS, + language="fortran", + member_keys=("demo.ping",), + source_paths=("bind_c_demo_wrapper.f90",), + ), + ), ) generator = CBindingGenerator() @@ -161,9 +214,11 @@ static PyObject * wrap_ping(PyObject * self, PyObject * args, PyObject * kwargs) -The plan's public name produces `wrap_ping`; its bridge record produces -`bind_c_ping`. The C generator adds CPython parsing and result mechanics, but -the selected plan remains the reason that call is permitted and named that way. +The binding record's public name produces `wrap_ping`; the entrypoint record +supplies `bind_c_ping`. The C generator adds CPython parsing and result +mechanics, but the selected plan remains the reason that call is permitted and +named that way. The bridge record's `PING` target is deliberately unavailable +to this generator. ## Run The Module Demonstration @@ -202,8 +257,11 @@ static PyObject * wrap_double_value(PyObject * self, PyObject * args, PyObject * } ``` -The header exposes the bridge wrapper prototype. The wrapper's rendered body -shows the Python-to-bridge call and conversion back to a NumPy scalar result. +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 +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 499476a93..a131e058b 100644 --- a/docs/developer/packages/codegen/fortran-bridge.md +++ b/docs/developer/packages/codegen/fortran-bridge.md @@ -12,20 +12,39 @@ publication: reviewed ## Role And Boundary [`prik/codegen/fortran/bridge.py`](../../../../prik/codegen/fortran/bridge.py) -lowers the bridge view of a completed `ModulePlan` into a `FortranModule`. The -result is a typed Fortran syntax tree, not formatted source or a compiled -library. `FortranSourcePrinter` serializes it later. - -The bridge implements the ABI selected by the plan: `bind(C)` procedures, -native imports and interfaces, declarations, representation conversion, -ordered native calls, writeback, cleanup, and derived-value lifecycles. It -does not infer a native interface, argument optionality, ownership, or result -projection from source-language details. +lowers the native-entrypoint and bridge views of a completed `ModulePlan` into +a `FortranModule`. The result is a typed Fortran syntax tree, not formatted +source or a compiled library. `FortranSourcePrinter` serializes it later. + +The entrypoint records define each public `bind(C)` symbol, ordered argument +and result transport, and direct-return ABI. Bridge records define native +imports and interfaces, adapter-local declarations and representation +conversion, the ordered original-Fortran call, writeback, cleanup, and +derived-value lifecycles. The generator does not infer a native interface, +argument optionality, ownership, or result projection from source-language +details. + +Generated support procedures obtain their exported symbol and public +parameter/result contract from the same module support-procedure registry +used by the C binding. Derived-field bodies, origin storage manipulation, +descriptor association, destruction, and callback adaptation remain +Fortran-local implementation. The lowerer does not reconstruct a support ABI +from those local facts. The registry also marks callback trampolines as +binding-implemented, so Fortran consumes their signatures as interfaces rather +than emitting duplicate implementations. + +Fortran-local typed-holder definitions and holder field bodies follow explicit +bridge-module inventories projected beside that registry. The lowerer joins a +planned owner path to its derived declaration for spelling, but does not walk +function results, argument call cases, or storage policy to rediscover which +holder types exist. Per-argument lowering still dispatches the already-planned +derived call case selected for that one invocation. ## Input And Output ```text -ModulePlan.bridge + namespaces + function bridge views +ModulePlan.entrypoint + ModulePlan.bridge + + namespaces + function entrypoint/bridge views -> FortranBridgeGenerator.require_supported() -> FortranBridgeGenerator.visit() -> FortranModule @@ -46,17 +65,17 @@ them. `_visit_FunctionPlan()` preserves the plan's execution order: -1. It determines the bridge result form and orders ABI parameters by their - recorded position. +1. It lowers the entrypoint result form and ordered C ABI parameters into the + public `bind(C)` declaration. 2. It emits declarations and representation initializers, then forms the native invocation from the ordered call slots. 3. It runs the selected writeback and cleanup finalizers, wrapping derived result or carrier lifecycles when the plan requires them. -The bridge result exposes a `bind(C)` name for the C binding. For a standalone -native procedure, the bridge record explicitly selects its external declaration; -for a module procedure, it supplies the native module use. Those are completed -plan facts, not heuristics in the generator. +The entrypoint record exposes a `bind(C)` name shared with the C binding. For a +standalone native procedure, the bridge record explicitly selects its external +declaration; for a module procedure, it supplies the native module use. Those +are completed plan facts, not heuristics in the generator. ## Run A Minimal Manual Plan @@ -76,11 +95,15 @@ Expand the full source to run the complete example. ```python binding = BindingFunctionPlan(...) +entrypoint = NativeEntrypointFunctionPlan(...) bridge = BridgeFunctionPlan(...) -function = FunctionPlan(..., binding=binding, bridge=bridge) +function = FunctionPlan( + ..., binding=binding, entrypoint=entrypoint, bridge=bridge +) namespace = NamespacePlan(..., functions=(function,)) plan = ModulePlan( binding=BindingModulePlan(...), + entrypoint=NativeEntrypointModulePlan(...), bridge=BridgeModulePlan(...), namespaces=(namespace,), ) @@ -98,9 +121,13 @@ print(FortranSourcePrinter().doprint(...)) from prik.codegen.fortran.bridge import FortranBridgeGenerator from prik.planning.models import ( BindingFunctionPlan, BindingModulePlan, BridgeFunctionPlan, - BridgeModulePlan, FunctionPlan, ModulePlan, NamespacePlan, + BridgeModulePlan, FunctionPlan, ModulePlan, + NativeEntrypointFunctionPlan, NativeEntrypointModulePlan, + NativeGeneratedCodeGroupKind, NativeGeneratedCodeGroupPlan, NamespacePlan, +) +from prik.policy.models import ( + ExternalDeclarationMode, NativeEntrypointAction, NativeInvocationKind, ) -from prik.policy.models import ExternalDeclarationMode, NativeInvocationKind from prik.printers.fortran import FortranSourcePrinter binding = BindingFunctionPlan( @@ -119,15 +146,22 @@ bridge = BridgeFunctionPlan( native_module=None, native_is_subroutine=True, ) +entrypoint = NativeEntrypointFunctionPlan( + symbol_name="bind_c_ping", + action=NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER, + parameters=(), + results=(), + projected_slots=(), +) function = FunctionPlan( owner_path="demo.ping", symbol_name="ping", binding=binding, + entrypoint=entrypoint, bridge=bridge, class_call=None, arguments=(), results=(), - native_call_slots=(), declaration_callables=(), available_roles=(), ) @@ -140,8 +174,17 @@ namespace = NamespacePlan( plan = ModulePlan( owner_path="demo", binding=BindingModulePlan(owner_path="demo"), + entrypoint=NativeEntrypointModulePlan(owner_path="demo"), bridge=BridgeModulePlan(owner_path="demo"), namespaces=(namespace,), + native_generated_code_groups=( + NativeGeneratedCodeGroupPlan( + kind=NativeGeneratedCodeGroupKind.FORTRAN_ADAPTERS, + language="fortran", + member_keys=("demo.ping",), + source_paths=("bind_c_demo_wrapper.f90",), + ), + ), ) generator = FortranBridgeGenerator() @@ -159,9 +202,10 @@ end subroutine bind_c_ping -The bridge record fixes the public C-ABI name and marks `PING` as an external -subroutine. The generator contributes the bridge declaration and call syntax; -it does not decide whether `PING` is callable or how values cross the boundary. +The entrypoint record fixes the public C-ABI name; the bridge record marks +`PING` as an external subroutine and original native target. The generator +contributes the adapter declaration and call syntax; it does not decide whether +`PING` is callable or how values cross the boundary. ## Run The Module Demonstration @@ -217,12 +261,13 @@ alias. This gives every imported module procedure a distinct bridge-local name. ## Change Routes And Evidence -- Change bridge ABI declarations, native invocation, conversion, writeback, or - cleanup in `bridge.py`. +- Change entrypoint declaration lowering or bridge-local native invocation, + conversion, writeback, or cleanup in `bridge.py`. - Change a primitive's Fortran spelling in [`primitive_scalar_types.py`](../../../../prik/codegen/primitive_scalar_types.py). -- If an ABI fact is absent from a bridge record, add the completed policy and - plan fact upstream; do not inspect semantic source or invent a default here. +- If a C ABI fact is absent from an entrypoint record, or an adapter-local fact + is absent from a bridge record, add the completed policy and plan fact + upstream; do not inspect semantic source or invent a default here. | Evidence | What it establishes | | --- | --- | diff --git a/docs/developer/packages/contracts.md b/docs/developer/packages/contracts.md index 10bdad5e2..014138088 100644 --- a/docs/developer/packages/contracts.md +++ b/docs/developer/packages/contracts.md @@ -47,8 +47,8 @@ the complete public namespace. Its contents have four roles: - scalar, array, descriptor, and wrapped-type markers describe values; - expression helpers such as `Arg`, `Len`, and `Ownership` describe metadata; -- decorators such as `native_call`, `prototype`, and `standalone` describe - callable structure; and +- decorators such as `native_abi`, `native_call`, `prototype`, and `standalone` + describe callable ABI or structure; and - `CONTRACT_SYMBOLS` and `CONTRACT_TYPE_NAMES` give parsers and printers the canonical public vocabulary. diff --git a/docs/developer/packages/pipeline.md b/docs/developer/packages/pipeline.md index ab372079c..39afa79ba 100644 --- a/docs/developer/packages/pipeline.md +++ b/docs/developer/packages/pipeline.md @@ -28,7 +28,7 @@ Fortran source -> policy completion -> WrapperPlanner -> WrapperGenerator - -> GeneratedWrapper: rendered C, Fortran, and header payloads in memory + -> GeneratedWrapper: rendered C, optional Fortran, and header payloads in memory -> build.py: files, NativeBuildPlan, compiler execution, and linking -> WrapperBuildResult -> import_module(): imported extension @@ -69,7 +69,11 @@ prik/pipeline/ - **`wrapper.py` is the rendered-wrapper boundary.** `GeneratedSource` and `GeneratedWrapper` are the handoff records. `WrapperGenerator.generate()` freezes and validates the completed plan before either backend lowers it, - then assembles printed C, Fortran, and header payloads with stable names. + then assembles printed C, optional Fortran, and header payloads with stable + names. An all-direct Fortran module has no bridge payload; its retained + native-language requirement still selects the Fortran link driver. Generated + native-code groups retain adapter and support membership separately even + 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. @@ -95,6 +99,25 @@ source or .pyi contract plus native inputs -> manifest serialization or replay ``` +Generated wrapper membership is data, not a filename convention. The build +materializes and compiles only the paths listed by `GeneratedWrapper`; an empty +bridge-source tuple is a complete all-direct result. Link-driver selection +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. + +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 +adapter membership, not caller-supplied artifact order or the required Fortran +link runtime. + +`WrapperBuildResult` and saved `.pyi` manifests report each generated native +group's kind, language, member keys, and physical source paths. This makes +zero-source, adapter-only, support-only, and mixed output factual in direct +builds, source-only output, Makefiles, and manifest replay. Progress and +compiler records are emitted only for physical sources that are present. + The source file groups helpers around build configuration, generated-wrapper materialization, native compilation scheduling, `.pyi` contract loading and exports, native planning and link inputs, manifest handling, wrapper-module diff --git a/docs/developer/packages/planning.md b/docs/developer/packages/planning.md index 7662f8b81..0774f1b7d 100644 --- a/docs/developer/packages/planning.md +++ b/docs/developer/packages/planning.md @@ -13,9 +13,10 @@ publication: reviewed `prik/planning/` mechanically projects policy-completed semantic IR into one backend-neutral `ModulePlan`. It joins common transfer facts with explicit -binding and bridge views, namespaces, stable native symbols, lifecycle order, -and build requirements. It may organize and validate completed decisions; it -may not reinterpret source declarations, choose policy, or render text. +binding, native-entrypoint, and bridge views, namespaces, stable native +symbols, lifecycle order, and build requirements. It may organize and validate +completed decisions; it may not reinterpret source declarations, choose +policy, or render text. ## Local Structure @@ -30,10 +31,12 @@ prik/planning/ ```text policy-completed SemanticModule - -> WrapperPlanner validates completed records and projects views + -> WrapperPlanner projects binding + entrypoint views + plus an adapter view only where completed policy selected one -> editable ModulePlan -> freeze at WrapperGenerator boundary - -> binding and bridge node generation + -> binding + entrypoint C generation + -> optional entrypoint + bridge Fortran generation ``` ## Directory Tour @@ -41,8 +44,9 @@ policy-completed SemanticModule | Module | Main entrypoints and contents | Change it when | | --- | --- | --- | | [`prik/planning/__init__.py`](../../../prik/planning/__init__.py) | Re-exports `WrapperPlanner` and the supported plan records. | A supported planning type or import path changes. | -| [`prik/planning/models.py`](../../../prik/planning/models.py) | `ModulePlan` and typed function, argument, result, slot, lifecycle, class, overload, binding, and bridge records form the editable plan tree. | Lowering needs a new *already completed* fact represented explicitly. | -| [`prik/planning/planner.py`](../../../prik/planning/planner.py) | `WrapperPlanner` validates policy, indexes declarations, allocates names, and projects deterministic binding and bridge views; `_ClassPolicyCatalog` is a validated lookup. | A completed policy fact is projected or ordered incorrectly. | +| [`prik/planning/models.py`](../../../prik/planning/models.py) | `ModulePlan` and typed function, argument, result, bridge-call-slot, lifecycle, class, overload, binding, entrypoint, and bridge records form the editable plan tree. | Lowering needs a new *already completed* fact represented explicitly. | +| [`prik/planning/planner.py`](../../../prik/planning/planner.py) | `WrapperPlanner` validates policy, indexes declarations, allocates names, and projects deterministic binding, entrypoint, and bridge views; `_ClassPolicyCatalog` is a validated lookup. | A completed policy fact is projected or ordered incorrectly. | +| [`prik/planning/entrypoints.py`](../../../prik/planning/entrypoints.py) | Projects every generated support procedure entrypoint into the module registry, including its implementation owner and structured C ABI. | An accessor, lifecycle, descriptor, origin, constructor, or callback operation changes its shared boundary. | The private class-policy catalogue is a validated lookup, not another semantic authority. The planner does not generate docstrings or source. @@ -54,20 +58,72 @@ actions, and module variables: ```text ModulePlan ├── BindingModulePlan -├── BridgeModulePlan +├── NativeEntrypointModulePlan +│ └── GeneratedSupportProcedureEntrypointPlan +│ └── NativeEntrypointSignaturePlan +├── NativeGeneratedCodeGroupPlan (zero or more) +├── BridgeModulePlan (optional; Fortran-local holder inventories) └── NamespacePlan (root and child namespaces) ├── FunctionPlan │ ├── ArgumentTransferPlan │ ├── ResultPlan - │ ├── NativeCallSlotPlan + │ ├── NativeEntrypointParameterPlan + │ ├── NativeEntrypointProjectedSlotPlan + │ │ └── BridgeCallSlotPlan (optional adapter facet) │ └── LifecycleActionPlan └── ModuleVariablePlan ``` -Each argument or result owns explicit binding and bridge views. Its native-call -slot is the same record referenced from the transfer and the function-wide ABI -ordering index, not a duplicated policy fact. Function orchestration owns call, -result, lifecycle, GIL, and status order without becoming datatype policy. +Each callable, argument, and result always owns binding and entrypoint views; +an adapter-backed callable additionally owns a bridge view. Binding records own Python extraction and result +construction. Native-entrypoint records own the complete bidirectional C ABI: +the exported symbol, direct return, ordered parameter groups, value/address +projection, presence and length fields, descriptors, and hidden outputs. +Bridge records own adapter-local representation conversion and the invocation +of the original Fortran procedure. + +`NativeEntrypointModulePlan.support_procedures` is the authoritative registry for +externally linked generated helper callables that are not ordinary wrapped +functions. Each operation stores one collision-safe key and symbol plus a +structured signature of ordered ABI values. The registry covers constructors, +accessors, derived-origin transactions, destruction and holder helpers, +native-array operations, and callback trampolines. Each record also identifies +whether the binding or bridge implements the callable; the opposite side uses +the same record as its declaration/call contract. Static CPython helpers and +bridge-internal procedures are deliberately absent. + +`BindingModulePlan` separately records which derived-type owners need +binding-local capsule and holder surfaces. Those static CPython helpers are not +entrypoints, but their membership is still planned rather than rediscovered by +C lowering. `BridgeModulePlan` likewise records the broad typed-holder +definitions required by adapter calls and the narrower holder field-support +inventories. Planning derives both backend-local inventories and the external +support-procedure registry together. Validation requires every planned local +helper that calls native support to resolve an entrypoint with the matching +owner and role. + +`ModulePlan.native_generated_code_groups` records generated native membership +without using the presence of a physical source file as policy. Adapter groups +contain only user operations selected for a generated Fortran adapter; support +groups contain only Fortran-owned generated support procedure keys. Empty +groups are omitted. Both groups may initially name the same Fortran source, +but their membership remains independently inspectable for support-only and +mixed builds. + +`NativeEntrypointFunctionPlan.results` includes public Python results and +binding-private outputs such as native status and message values. A public +`ResultPlan` shares its exact entrypoint-result object; a private output remains +available to C prototype, storage, and call lowering without exposing a bridge +call slot to the binding generator. + +An argument or hidden result shares one authoritative +`NativeEntrypointProjectedSlotPlan` with the function-wide binding projection +sequence. It owns source mapping, ordering, passing, optionality, and the C ABI +actual. Adapter-backed operations attach a narrow `BridgeCallSlotPlan`; direct +operations do not. `NativeEntrypointParameterPlan` independently groups the +resulting C declaration fields. +Function orchestration owns call, result, lifecycle, GIL, and status order +without becoming datatype policy. `OverloadPlan` stores ordered candidates, exact match records, receiver conventions, and one candidate ID per overload set. Generated dispatch chooses @@ -83,12 +139,14 @@ policy-completed `SemanticModule` and dispatches it through the planner's visitor. The completed-policy accessors used during projection reject missing or blocked records, so planning cannot fill in a default. -For each module, the planner resets its derived-type and field indexes, then -assigns backend symbols, qualifying only genuinely colliding native type names. -It projects direct functions and variables, then uses `_ClassPolicyCatalog` to -join each public class to its completed derived-type, surface, method, and -overload policies. The catalogue is read-only: it maps existing owner paths to -their semantic declarations without deciding policy again. +For each module, the planner first collects top-level and nested semantic +classes into one depth-first, source-ordered tuple. That same collection feeds +derived-type name indexing, backend-symbol allocation, and +`_ClassPolicyCatalog`, so a nested class cannot reach projection without its +symbol being registered. It projects direct functions and variables, then uses +the catalogue to join each public class to its completed derived-type, surface, +method, and overload policies. The catalogue is read-only: it maps existing +owner paths to their semantic declarations without deciding policy again. The planner attaches class and overload callables to the function collections that need their native entrypoints. It completes generated symbols, adds every @@ -96,18 +154,28 @@ required parent namespace, and creates namespace plans in root-first path order. Finally it collects headers selected by completed descriptor-handle plans and returns one editable `ModulePlan`. -### `models.py`: shared plans and backend views +### `models.py`: shared plans and three lowering views `models.py` defines editable `StageRecord` plans. `ModulePlan` is the root; each `NamespacePlan` groups the public functions, variables, derived types, classes, and overloads for one Python path. A `FunctionPlan` owns call-wide -ordering, while its transfers, results, native slots, and lifecycle actions -carry the datatype-specific details. - -Binding and bridge records are separate facets of the same planned operation. -For example, an `ArgumentTransferPlan` holds both backend views and shares its -single `NativeCallSlotPlan` with `FunctionPlan.native_call_slots`. This prevents -two backends from carrying independent interpretations of one ABI position. +ordering, while its transfers, results, entrypoint parameters, projected call +slots, optional adapter facets, and lifecycle actions carry the +datatype-specific details. + +Binding, native-entrypoint, and optional bridge records are separate facets of +the same planned operation. For example, an `ArgumentTransferPlan` shares its +projected entrypoint slot with `FunctionPlan.entrypoint.projected_slots`; only +an adapted route also references that slot's adapter facet. +`FunctionPlan.entrypoint.parameters` is the ordered C ABI grouping consumed by +lowering. This keeps the binding projection authoritative while leaving +original-Fortran invocation details out of direct routes and C lowering. + +`WrapperPlanner` constructs the selected facets directly from completed upstream +facts. `WrapperGenerator` validates matching entrypoint roles, parameter +owners, and projected-slot references before freezing the graph. A generator may +not derive an entrypoint from a bridge record or split a two-part plan after +planning. The plan remains editable only until `WrapperGenerator.generate()` validates and freezes it. Add presentation details to code generation, not planning. @@ -125,12 +193,12 @@ python3 prik/planning/models.py Plan owner: demo Python export: ping Native procedure: PING -Native slots: 0 +Projected call slots: 0 ``` The two names show the separate Python and native views carried by one plan. -Zero slots is complete for a no-argument subroutine; it is not an omitted -decision. +Zero entrypoint parameters and zero projected call slots are complete for a +no-argument subroutine; they are not omitted decisions. `planner.py` follows the normal route. It constructs one `Float64` function, completes its semantic policy, then asks `WrapperPlanner` to project the plan. @@ -170,11 +238,25 @@ than reconstructed by either backend. ## Boundaries And Invariants - Missing completed policy is an error, never a reason to infer a default. -- Binding and bridge views may share one ABI contract without hiding their - backend-specific lowering facts. +- The C binding consumes only binding and entrypoint views; the Fortran bridge + consumes entrypoint and bridge views. +- Neither generator may reconstruct the symbol, existence, parameter order, or + result ABI of a generated support procedure from a derived field, storage + kind, array operation, lifecycle action, or callback record. +- C lowering may map a planned binding-local derived owner back to its + `DerivedTypePlan` for names and fields, but it may not walk results, + arguments, module variables, constructors, releases, or storage kinds to + rediscover capsule or holder membership. +- Fortran lowering may perform the same mechanical owner-to-type join for its + planned holder definitions and holder field bodies. It may not walk result + storage or argument call cases to reconstruct either module inventory. +- Direct Fortran operations have no bridge facet. Adapter and Fortran-support + membership are separate generated-code groups even when they share a + physical source. - Planning does not depend on presentation helpers such as docstring builders. -- Native slots may interleave argument, result, literal, and helper positions; - keep their function-wide order explicit. +- Shared projected slots own argument, result, literal, and helper ordering. + An adapted slot may add only its Fortran-local conversion and invocation + facet; it does not own a second projection order. - Lifecycle actions stay explicit because cleanup and writeback order may span several transfers and differ on failure. diff --git a/docs/developer/packages/policy.md b/docs/developer/packages/policy.md index bc16dc8d3..79dd47433 100644 --- a/docs/developer/packages/policy.md +++ b/docs/developer/packages/policy.md @@ -40,7 +40,7 @@ prik/policy/ ```text SemanticModule + normalized raw metadata -> entry-export filtering and Python export completion - -> ownership and accessor decisions + -> ownership, accessor, and per-operation native-entrypoint decisions -> class, callable, result, overload, and descriptor policy construction -> immutable completed policies attached to semantic IR -> WrapperPlanner @@ -100,13 +100,52 @@ substitute. `decide_semantic_variable()`, `decide_semantic_getter()`, and The construction helpers combine completed decisions into immutable records for functions, results, native call slots, module variables, derived types, -classes, overloads, callbacks, transformations, and lifecycles. +classes, overloads, callbacks, transformations, lifecycles, and native +entrypoints. For a function, `build_function_wrapper_policy()` fixes native-slot order, -projects visible arguments, completes result and declaration-callable records, -binds declaration extents, records writeback and cleanup, and aggregates all -support blockers. The resulting `FunctionWrapperPolicy` is the planner's -complete description of the wrapper mechanism; +projects visible arguments, completes each binding-owned projection and C +passing convention, completes result and declaration-callable records, binds +declaration extents, records writeback and cleanup, and then selects exactly +one `NativeEntrypointAction`. A Fortran procedure without the retained C ABI +fact selects `GENERATED_FORTRAN_ADAPTER`. A `bind(C)` procedure selects +`DIRECT_C_ABI` only when every argument, result, optional-presence, +representation, ownership, lifecycle, and invocation fact is directly +interoperable; otherwise it keeps the adapter route. Optional non-`VALUE` +interoperable dummies use a nullable C pointer, while optional `VALUE` dummies +remain adapter-backed. + +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 +ABI. The binding then passes its binding-owned trampoline as the planned +function-pointer actual. Array, string, derived, optional, and non-`bind(C)` +callback interfaces retain the generated Fortran adapter route. + +An allocatable or pointer array argument may use the direct route only when +completed native-array policy supplies a persistent standard C descriptor and +the original `bind(C)` dummy accepts that descriptor. Optional descriptor +arguments use the standard three states: a null descriptor pointer is absent, +a non-null empty descriptor is present but unallocated or unassociated, and a +non-null populated descriptor is present with storage. Fact-packed call-local +descriptors and owned descriptor results remain adapter-backed. + +A `character(c_char)` scalar or explicit/assumed-size array is directly +interoperable only when its completed element length is one. Policy selects a +C value for a scalar `VALUE` dummy and a character pointer for reference or +array storage; longer or runtime-length character storage remains +adapter-backed. The lowerer does not infer this choice from a C spelling. + +`EntrypointPassingConvention` describes value, reference, nullable pointer, +C-descriptor, runtime-handle, C-return, and output-storage transport. +`EntrypointOptionalityAction` remains independent from the Python default and +nullable surface. `EntrypointProjectionAction` records how the binding +materializes every ordered `@native_call` item. Adapter data actions describe +only representation or original-invocation work after the shared C boundary; +they do not choose the call source, ordering, or C passing convention. + +The resulting `FunctionWrapperPolicy` is the planner's complete description of +the wrapper mechanism; `completed_function_wrapper_policy()` rejects absent or blocked records at that boundary. diff --git a/docs/developer/packages/semantics.md b/docs/developer/packages/semantics.md index b3ac15f4a..e4b13667a 100644 --- a/docs/developer/packages/semantics.md +++ b/docs/developer/packages/semantics.md @@ -129,8 +129,11 @@ references against the batch: prototypes become callback references and classes become wrapped or opaque external types. The converter validates the supported contract subset and preserves declared -native facts. It does not execute declarations, load native code, infer a -wrapper mechanism, or complete ownership policy. +native facts. For a Fortran contract, `@native_abi("c")` records an original +procedure's C ABI independently from its `@bind(...)` link label, placement, +and route-neutral `@native_call(...)` projection. It does not execute +declarations, load native code, infer a wrapper route, or complete ownership +policy. ### `models.py` and `scalar_types.py`: the shared vocabulary diff --git a/docs/developer/roadmap/index.md b/docs/developer/roadmap/index.md index 7bd6590ed..c37e6808a 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, semantic-pyi-wrapper-checklist.md, fortran-test-suite-cleanup-checklist.md, documentation-content-checklist.md +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 status: active-roadmap publication: draft --- @@ -16,6 +16,7 @@ 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 new file mode 100644 index 000000000..d5f2dbd70 --- /dev/null +++ b/docs/developer/roadmap/native-entrypoint-adoption-checklist.md @@ -0,0 +1,959 @@ +--- +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/user/assets/build-time-comparison.svg b/docs/user/assets/build-time-comparison.svg index 458ccfeec..8f872a8c2 100644 --- a/docs/user/assets/build-time-comparison.svg +++ b/docs/user/assets/build-time-comparison.svg @@ -17,41 +17,41 @@ 0 s - 8 s - 16 s - 24 s - 32 s - 40 s + 3 s + 6 s + 9 s + 12 s + 15 s Development · small module f2py - - 2.37 sec + + 1.50 sec PRIK - - 867 ms + + 550 ms Development · full reference BLAS f2py - - 10.8 sec + + 7.00 sec PRIK - - 9.29 sec + + 5.05 sec Optimized · small module f2py - - 2.88 sec + + 1.72 sec PRIK - - 1.03 sec + + 644 ms Optimized · full reference BLAS f2py - - 22.3 sec + + 11.0 sec PRIK - - 33.1 sec + + 13.3 sec diff --git a/docs/user/assets/performance-comparison.svg b/docs/user/assets/performance-comparison.svg index 279125379..be2ecc61c 100644 --- a/docs/user/assets/performance-comparison.svg +++ b/docs/user/assets/performance-comparison.svg @@ -1,7 +1,7 @@ PRIK performance relative to f2py - Relative speed across 13 benchmarks. Values above one indicate PRIK is faster. PRIK is faster in 7 benchmarks. + Relative speed across 13 benchmarks. Values above one indicate PRIK is faster. PRIK is faster in 11 benchmarks. @@ -9,75 +9,75 @@ f2py time ÷ PRIK time · farther right means faster PRIK calls - - - - + + + + - + - 0.7× - 0.8× - 1.0× - 1.2× - 1.4× - 1.6× - 1.0× equal + 0.9× + 1.0× + 1.1× + 1.2× + 1.3× + 1.4× + 1.0× equal Empty call - - - 1.08× + + + 1.16× Add scalars - - - 0.97× + + + 1.08× Increment vector · n=1 - - - 1.49× + + + 1.27× Increment vector · n=16 - - - 1.43× + + + 1.28× Increment vector · n=1,024 - - - 0.95× + + + 1.24× Increment vector · n=1,000,000 - - - 0.99× + + + 1.02× Sum matrix · 4×4 - - - 1.13× + + + 1.05× Sum matrix · 32×32 - - - 1.02× + + + 1.01× Sum matrix · 256×256 - - - 0.991× + + + 0.998× Sum matrix · 1,024×1,024 - - - 0.89× + + + 0.998× Update matrix · 4×4 - - - 1.07× + + + 1.16× Update matrix · 256×256 - - - 1.01× + + + 1.02× Update matrix · 1,024×1,024 - - - 0.80× + + + 1.03× @@ -86,7 +86,7 @@ f2py faster no significant difference - Geometric mean: PRIK 1.05× faster + Geometric mean: PRIK 1.10× faster diff --git a/docs/user/performance.md b/docs/user/performance.md index b5c7daf4b..bf409e762 100644 --- a/docs/user/performance.md +++ b/docs/user/performance.md @@ -13,21 +13,21 @@ publication: reviewed **Low-overhead Python calls for real Fortran workloads.** -On the benchmark system, the normal PRIK interface delivered a **1.05× geometric-mean -speedup over NumPy's f2py**. Across 13 workloads, PRIK was faster in 7 and f2py in 5; 1 -workload showed no statistically significant difference. +On the benchmark system, the normal PRIK interface delivered a **1.10× geometric-mean +speedup over NumPy's f2py**. Across 13 workloads, PRIK was faster in 11 and f2py in 2; +all comparisons were statistically significant.
- 1.05× + 1.10× PRIK geometric-mean speedup
- 7 of 13 + 11 of 13 workloads faster with PRIK
- 1.49× + 1.28× best measured PRIK speedup
@@ -48,26 +48,61 @@ the normal generated interface of each tool. | Workload | f2py | PRIK | Relative result | | --- | ---: | ---: | ---: | -| Empty function call | 46.2 ns | **42.9 ns** | PRIK 1.08× faster | -| Add two scalars | **426 ns** | 440 ns | f2py 1.03× faster | -| Increment vector, 1 element | 134 ns | **89.9 ns** | PRIK 1.49× faster | -| Increment vector, 16 elements | 145 ns | **101 ns** | PRIK 1.43× faster | -| Increment vector, 1,024 elements | **283 ns** | 299 ns | f2py 1.06× faster | -| Increment vector, 1,000,000 elements | 1.19 ms | 1.21 ms | No significant difference | -| Sum 4×4 F-order matrix | 166 ns | **148 ns** | PRIK 1.13× faster | -| Sum 32×32 F-order matrix | 1.15 µs | **1.13 µs** | PRIK 1.02× faster | -| Sum 256×256 F-order matrix | **63.5 µs** | 64.1 µs | f2py 1.009× faster | -| Sum 1,024×1,024 F-order matrix | **1.09 ms** | 1.22 ms | f2py 1.12× faster | -| Update 4×4 F-order matrix | 346 ns | **324 ns** | PRIK 1.07× faster | -| Update 256×256 F-order matrix | 26.0 µs | **25.7 µs** | PRIK 1.01× faster | -| Update 1,024×1,024 F-order matrix | **1.13 ms** | 1.42 ms | f2py 1.25× faster | -| **Geometric mean** | reference | — | **PRIK 1.05× faster** | +| Empty function call | 34.6 ns | **29.9 ns** | PRIK 1.16× faster | +| Add two scalars | 341 ns | **316 ns** | PRIK 1.08× faster | +| Increment vector, 1 element | 103 ns | **80.6 ns** | PRIK 1.27× faster | +| Increment vector, 16 elements | 106 ns | **82.8 ns** | PRIK 1.28× faster | +| Increment vector, 1,024 elements | 262 ns | **211 ns** | PRIK 1.24× faster | +| Increment vector, 1,000,000 elements | 186 µs | **183 µs** | PRIK 1.02× faster | +| Sum 4×4 F-order matrix | 127 ns | **121 ns** | PRIK 1.05× faster | +| Sum 32×32 F-order matrix | 718 ns | **711 ns** | PRIK 1.01× faster | +| Sum 256×256 F-order matrix | **38.9 µs** | 39.0 µs | f2py 1.002× faster | +| Sum 1,024×1,024 F-order matrix | **621 µs** | 622 µs | f2py 1.002× faster | +| Update 4×4 F-order matrix | 249 ns | **215 ns** | PRIK 1.16× faster | +| Update 256×256 F-order matrix | 13.0 µs | **12.7 µs** | PRIK 1.02× faster | +| Update 1,024×1,024 F-order matrix | 199 µs | **193 µs** | PRIK 1.03× faster | +| **Geometric mean** | reference | — | **PRIK 1.10× faster** | The smallest workloads expose wrapper overhead most clearly. As more time is spent inside Fortran, both tools approach the cost of the native operation and small differences matter less. +## Direct `bind(C)` Entrypoints + +This separate cohort isolates three scalar call boundaries: an empty +subroutine, a scalar function, and a scalar subroutine with an output. PRIK and +f2py compile the same Fortran source and call the same `bind(C)` labels. The +normal-interface geometric mean above remains unchanged. + +Before timing, artifact inspection verifies that neither direct route contains +a generated Fortran procedure adapter. Each Python binding object refers to +the three user labels, while its native object and linked extension define +them. f2py keeps its Python C/API binding and uses `--no-wrap-functions` plus +`--skip-empty-wrappers`; these options remove unnecessary generated Fortran +wrappers, not the Python binding. The PRIK adapter control measures equivalent +ordinary-Fortran procedures separately. + + +### Direct PRIK and f2py + +| Workload | f2py direct | PRIK direct | Relative result | +| --- | ---: | ---: | ---: | +| Empty call | 36.5 ns | **29.7 ns** | PRIK direct 1.23× faster | +| Scalar function | 121 ns | **103 ns** | PRIK direct 1.17× faster | +| Scalar subroutine | 122 ns | **103 ns** | PRIK direct 1.18× faster | +| **Geometric mean** | reference | — | **PRIK direct 1.19× faster** | + +### PRIK adapter control + +| Workload | PRIK adapted | PRIK direct | Relative result | +| --- | ---: | ---: | ---: | +| Empty call | 29.9 ns | **29.7 ns** | PRIK direct 1.008× faster | +| Scalar function | 104 ns | **103 ns** | PRIK direct 1.01× faster | +| Scalar subroutine | 105 ns | **103 ns** | PRIK direct 1.01× faster | +| **Geometric mean** | reference | — | **PRIK direct 1.01× faster** | + + ## Clean Build Time Build latency is measured separately from runtime-call overhead. Each timing @@ -86,16 +121,40 @@ optimized build with `-O3 -march=native -mtune=native`. { .prik-performance-chart } -Each value is the mean of 6 clean builds after 1 untimed warm-up. +Each value is the mean of 4 clean builds after 1 untimed warm-up. | Clean build workload | f2py | PRIK | Relative result | | --- | ---: | ---: | ---: | -| Development (`-O0`) · small module (1 source, 5 procedures) | 2.37 sec | **867 ms** | PRIK 2.73× faster | -| Development (`-O0`) · full reference BLAS (155 sources) | 10.8 sec | **9.29 sec** | PRIK 1.16× faster | -| Optimized (`-O3 -march=native -mtune=native`) · small module (1 source, 5 procedures) | 2.88 sec | **1.03 sec** | PRIK 2.80× faster | -| Optimized (`-O3 -march=native -mtune=native`) · full reference BLAS (155 sources) | **22.3 sec** | 33.1 sec | f2py 1.48× faster | +| Development (`-O0`) · small module (1 source, 5 procedures) | 1.50 sec | **550 ms** | PRIK 2.73× faster | +| Development (`-O0`) · full reference BLAS (155 sources) | 7.00 sec | **5.05 sec** | PRIK 1.39× faster | +| Optimized (`-O3 -march=native -mtune=native`) · small module (1 source, 5 procedures) | 1.72 sec | **644 ms** | PRIK 2.68× faster | +| Optimized (`-O3 -march=native -mtune=native`) · full reference BLAS (155 sources) | **11.0 sec** | 13.3 sec | f2py 1.22× faster | +## Direct-Entrypoint Clean Build Time + +The direct build workload compiles the same one-source, three-procedure module +used by the direct runtime cohort with the optimized profile. It includes +contract or signature processing, Python binding generation, compilation, and +linking. The separate PRIK adapter control shows whether omitting one generated +Fortran adapter materially changes this small end-to-end build. + + +Each value is the mean of 4 clean builds after 1 untimed warm-up. + +### Direct PRIK and f2py + +| Clean build workload | f2py direct | PRIK direct | Relative result | +| --- | ---: | ---: | ---: | +| Optimized (`-O3 -march=native -mtune=native`) · small direct module (1 source, 3 procedures) | 1.70 sec | **545 ms** | PRIK direct 3.12× faster | + +### PRIK adapter control + +| Clean build workload | PRIK adapted | PRIK direct | Relative result | +| --- | ---: | ---: | ---: | +| Optimized (`-O3 -march=native -mtune=native`) · small direct module (1 source, 3 procedures) | 547 ms | 545 ms | No significant difference | + + ## Should I use PRIK or f2py? These benchmarks answer two narrow questions: runtime-call overhead and clean @@ -128,9 +187,10 @@ so check the linked guides for exact limitations. ## Fair, Like-for-Like Setup -The suite wraps one set of Fortran kernels with the default PRIK and f2py -interfaces. It checks both extensions for the same results before measuring -them. No benchmark-only wrapper mode is used. +The normal-interface suite wraps one set of Fortran kernels with the default +PRIK and f2py interfaces. It checks both extensions for the same results before +measuring them. The direct-entrypoint cohort is kept separate and uses only the +documented direct-call modes described above. Each runtime group uses an A/B/B/A sequence with equal PRIK-first and f2py-first process budgets. The two passes are merged before significance, winner counts @@ -145,20 +205,23 @@ independently. - Both interfaces keep the GIL held. - OpenMP, OpenBLAS, and MKL are limited to one thread. - `pyperf --rigorous` pins each benchmark to logical CPU `0`. -- PRIK build timings use up to 8 concurrent compiler +- Normal runtime samples combine equal PRIK-first and f2py-first process budgets. +- Direct runtime samples use balanced forward and reverse PRIK-direct, + f2py-direct, and PRIK-adapted process order. +- PRIK build timings use up to 4 concurrent compiler processes; f2py uses its normal Meson/Ninja scheduler. -- Build timings alternate tool order, use clean output directories, and exclude - post-build import checks. -- CPU: Intel(R) Core(TM) i7-4712MQ CPU @ 2.30GHz. -- Operating system: Ubuntu 26.04 LTS. -- Kernel/platform: `Linux-7.0.0-28-generic-x86_64-with-glibc2.43`. -- Python: 3.14.4. +- Normal and three-route direct build timings alternate tool order, use clean + output directories, and exclude post-build import checks. +- CPU: Arm Neoverse N2. +- Operating system: Ubuntu 24.04.4 LTS. +- Kernel/platform: `Linux-6.17.0-1022-azure-aarch64-with-glibc2.39`. +- Python: 3.12.13. - NumPy/f2py: 2.5.1. -- Fortran compiler: GNU Fortran 15.2.0. +- Fortran compiler: GNU Fortran 13.3.0. - pyperf: 2.10.0. -- PRIK revision: `0bcaafdf162d`. +- PRIK revision: `8ff253070c44`. -These results were recorded on August 1, 2026. Performance depends on the CPU, +These results were recorded on August 15, 2026. Performance depends on the CPU, compiler, operating system, and background activity, so comparisons should use results produced together on the same machine. @@ -173,5 +236,6 @@ measurements, and comparison with one command: bash benchmarks/run.sh ``` -The command writes the runtime and clean-build `pyperf` result pairs under -`benchmarks/results/` and prints both comparison tables. +The command writes the normal, direct, and adapter-control runtime and +clean-build `pyperf` result pairs under `benchmarks/results/` and prints their +comparison tables. diff --git a/docs/user/reference/configuration-files.md b/docs/user/reference/configuration-files.md index 5198948a9..3327a939a 100644 --- a/docs/user/reference/configuration-files.md +++ b/docs/user/reference/configuration-files.md @@ -37,19 +37,23 @@ Stable top-level fields: | Field | Meaning | | --- | --- | -| `schema_version` | Manifest schema version. The current supported value is `2`. | +| `schema_version` | Manifest schema version. The current supported value is `3`. | | `build_kind` | Manifest kind. The current supported value is `pyi-wrapper`. | | `entry_contract` | Entry semantic `.pyi` path used for the build. | | `contract_paths` | Complete discovered `.pyi` import graph. Replay fails if the current graph differs. | | `extension` | Requested and resolved Python extension names. | | `output` | Output directory, shared-library path, and strict-name setting. | | `compiler` | Input-language compiler executable, compiler profile, and wrapper/native flag values recorded by the build. | +| `generated_wrapper` | Physical generated sources plus separate adapter and generated-support membership groups. | | `native_build_plan` | Native compilation units, produced objects, prebuilt artifacts, module/include directories, library directories, and ordered link items. | Relative paths are resolved relative to the manifest directory during replay. -Schema 2 records the selected input-language compiler executable and the -build-wide include directories needed to reproduce native, bridge, binding, -and link commands; schema 1 manifests are no longer accepted. Use: +Schema 3 records generated-native group membership in addition to the selected +input-language compiler executable and the build-wide include directories +needed to reproduce native, adapter, binding, and link commands. This makes an +all-direct build's empty generated-native set, a support-only source, and mixed +adapter/support membership explicit. Earlier schemas are no longer accepted. +Use: ```bash python3 -m prik --build-manifest build/module/prik-build.json diff --git a/docs/user/reference/fortran-wrapper.md b/docs/user/reference/fortran-wrapper.md index 17ec02da1..38465cb79 100644 --- a/docs/user/reference/fortran-wrapper.md +++ b/docs/user/reference/fortran-wrapper.md @@ -129,8 +129,9 @@ ordered Fortran source files -> semantic modules and completed policy -> post-IR policy completion -> ordered wrapper plan preserving native module namespaces and ABI slots - -> direct native-bridge and Python-binding lowering - -> compile and link one Python extension module + -> Python-binding lowering plus selected Fortran adapters/support procedures + -> compile native inputs and only the generated groups that are present + -> link one Python extension module with the required native-language driver ``` -The generated bridge preserves native calling contracts while the Python -binding validates arguments, manages wrapper-owned temporaries, calls native -code, and projects results onto the documented Python API. Shared runtime -support supplies array, error, allocation, and ownership helpers. +The Python binding validates arguments, manages wrapper-owned temporaries, +calls the planned C ABI entrypoint, and projects results onto the documented +Python API. Safely interoperable `bind(C)` operations use their existing native +symbols directly. Other operations use generated Fortran adapters, and shared +runtime support supplies array, error, allocation, and ownership helpers. There is no separate codegen-AST conversion stage. Post-IR completion freezes object kind, storage, ownership, mutation, output projection, and native-call @@ -172,11 +174,11 @@ Typical generated artifacts are: | Artifact | Purpose | | --- | --- | | `binding_support/` | Header-only native binding support | -| user and generated `.o`/`.mod` files | Native build intermediates | +| user and selected generated `.o`/`.mod` files | Native build intermediates | | `..so` | Importable extension on Linux | @@ -979,6 +981,12 @@ Optional scalar allocatable and pointer descriptors are the three-state exception: omission means absent, explicit `None` means a present unallocated or unassociated descriptor, and a concrete value means present storage. +For a directly routed standard C array descriptor, the native ABI represents +absence with a null descriptor pointer. Omission and explicit `None` therefore +both mean absent; pass an unallocated `Allocatable[T[...]]` or unassociated +`Pointer[T[...]]` handle for the distinct present-empty state, and a populated +handle for the present-value state. + Optional `intent(out)` and `intent(inout)` dummies remain visible so omission or `None` makes native `present(dummy)` false. Optional scalar outputs use mutable rank-zero storage such as `Int32[()]`. An optional allocatable or pointer array @@ -1022,10 +1030,12 @@ rename the Python function. PRIK_C_DOCS_END --> ### Input, Output, And Replacement @@ -1837,15 +1856,18 @@ PRIK_C_DOCS_END --> @@ -2110,6 +2132,13 @@ The generated wrapper keeps a strong reference to the callback only until the native call returns. Nested callback-taking calls on the same entering Python thread are supported. +When both the callback prototype and its containing procedure are `bind(C)`, a +primitive-scalar callback signature can use the direct route. The generated C +binding passes its call-scoped trampoline as the native function pointer; the +same GIL, exception, nesting, and lifetime rules below still apply. A callback +prototype that requires array, string, derived-object, optional, or other +Fortran-local conversion keeps the generated adapter route. + ### Callback Values - primitive scalars use matching owned NumPy scalar values, regardless of diff --git a/docs/user/reference/pyi-contracts/exports-and-modules.md b/docs/user/reference/pyi-contracts/exports-and-modules.md index 6d1500984..ac2882eb7 100644 --- a/docs/user/reference/pyi-contracts/exports-and-modules.md +++ b/docs/user/reference/pyi-contracts/exports-and-modules.md @@ -112,6 +112,23 @@ from prik.contracts import Float64, bind, standalone def norm2(values: Float64[:]) -> Float64: ... ``` +When an original Fortran procedure was declared `bind(C)`, preserve that ABI +fact separately with `@native_abi("c")`. An optional `@bind(...)` then names +its linkable label; neither decorator changes the procedure's module or +standalone placement: + +```python +from prik.contracts import Float64, bind, native_abi + +@native_abi("c") +@bind("vendor_norm2") +def norm2(values: Float64[:]) -> Float64: ... +``` + +This marker does not guarantee direct routing. Policy still validates the +complete operation before choosing the direct C ABI entrypoint or a generated +Fortran adapter. + The declaration must include the correct native arguments, types, ranks, and call shape. Adding Python syntax cannot create a native procedure that is not present in the linked implementation. diff --git a/docs/user/reference/semantic-pyi-format.md b/docs/user/reference/semantic-pyi-format.md index 527cea3e9..84e5c761a 100644 --- a/docs/user/reference/semantic-pyi-format.md +++ b/docs/user/reference/semantic-pyi-format.md @@ -305,11 +305,36 @@ from prik.contracts import Float64, standalone def update(value: Float64[()]) -> None: ... ``` -`@bind("native_name")` remains necessary only when the Python declaration name -differs from the native symbol. `@native_call` remains necessary only when the -Python signature hides, inserts, or reorders native arguments. `Pass()` marks -the implicit class instance. This is a method receiver or a newly allocated -constructor object. +`@native_abi("c")` records that an original Fortran procedure has a standard C +ABI entrypoint, as declared by Fortran `bind(C)`. It is a Fortran contract fact: +it does not reinterpret the contract as C source. The only accepted ABI value +is `"c"`; C-native contracts use their language identity instead and must not +repeat this decorator. + +`@bind("native_name")` remains necessary only when the linkable native label +differs from the declaration name. It composes with `@native_abi("c")`, so a +Fortran declaration such as `bind(C, name="solver_step")` is retained as: + +```python +from prik.contracts import Float64, bind, native_abi + +@native_abi("c") +@bind("solver_step") +def step(value: Float64) -> Float64: ... +``` + +The ABI marker is valid on module and standalone procedures, class methods, +overload declarations, and exact callable prototypes. Each declaration still +retains its Fortran placement and source identity. The marker records an input +fact only: post-IR policy decides independently whether an operation is safe to +call directly or requires a generated Fortran adapter. + +`@native_call` remains necessary only when the Python signature hides, inserts, +or reorders original native-procedure arguments. It is route-neutral: its +ordered arguments, hidden results, typed literals, address/value projections, +lengths, presence values, shapes, strides, and work values do not imply that a +Fortran adapter will execute them. `Pass()` marks the implicit class instance. +This is a method receiver or a newly allocated constructor object. Ordinary semantic types are the native type contract. `Int32`, `Float64`, `Addr`, scalar storage rank `()`, array rank, shape, and focused metadata such @@ -1127,6 +1152,12 @@ Dimension entries have the following meaning: | `Flat` | edge-position flat contiguous storage dimension | | `...` | rank-polymorphic storage | +For a Fortran semantic contract, a concrete dimension list such as +`Float64[n]` or `Float64[rows, columns]` is an explicit-shape dummy contract. +That spelling preserves the pointer-based interoperable mechanism needed by a +`@native_abi("c")` procedure; it is not treated as an unknown source category +when the `.pyi` is loaded without the original Fortran source. + Declaration extents use Python expression syntax, even when a contract was generated from another source language. Generated Fortran contracts translate array inquiries to the corresponding public array properties: @@ -1764,15 +1795,15 @@ default `Arg(i)` representation is already the native storage, handle, or raw address representation. Address projections of `Return(...)` and `Work(...)` are also rejected; native outputs and workspaces already name their storage. -`Value(Arg(i))` is the inverse override for an exact rank-zero monomorphic -wrapped derived object. Plain `Arg(i)` passes that object by reference; -`Value(Arg(i))` asks the typed bridge to pass the exact native object by value. -Primitive scalars already use value passing with plain `Arg(i)`, while arrays, -strings, raw addresses, and descriptor handles keep their normal storage ABI and -do not accept `Value(...)`. The foreign binding boundary still carries an opaque object -address: `Value(...)` records the native Fortran dummy contract, and the Fortran -compiler performs any required copy when the typed bridge makes the call. It -never asks the binding to pass aggregate bytes through the foreign ABI. +`Value(Arg(i))` is the inverse of `Addr(Arg(i))` for a primitive scalar: it +records a C-value entrypoint actual explicitly, which is useful in reordered or +edited source-free signatures. A scalar `String[1]` may also use it to preserve +a `character(kind=c_char), value` native parameter. Plain `Arg(i)` retains the +argument's completed declaration transport. `Value(Arg(i))` also remains valid +for an exact rank-zero monomorphic wrapped derived object; that aggregate case +stays adapter-backed, with an opaque binding boundary and a typed Fortran-local +copy. Arrays, longer strings, raw addresses, and descriptor handles keep their +normal storage ABI and do not accept `Value(...)`. ```python from prik.contracts import Returns, String @@ -2418,7 +2449,7 @@ Loaded projection entries: | --- | --- | | `Arg(i)` | native argument is Python argument `i`'s default native representation | | `Addr(Arg(i))` | native argument is the address of Python argument `i`'s call-local native scalar representation | -| `Value(Arg(i))` | exact rank-zero monomorphic wrapped derived object is passed to the native value dummy by the typed bridge | +| `Value(Arg(i))` | primitive scalar or scalar `String[1]` uses a C-value actual; exact rank-zero monomorphic wrapped derived object uses an adapter-local native value copy | | `Allocatable(Arg(i))`, `Pointer(Arg(i))` | native argument is a nullable call-local scalar descriptor initialized from Python argument `i`; `None` means present but unallocated or unassociated | | `Return(i)` | native argument is supplied by projected return slot `i` as hidden writable storage passed by address | | `Return("name", i)` | named native argument is supplied by projected return slot `i` as hidden writable storage passed by address | @@ -2510,7 +2541,7 @@ ambiguous, unsafe, or stale before wrapper lowering: for the generated derived-type constructor shape. - nested enum declarations. - ordinary function bodies instead of `...`. -- unsupported decorators other than `@private`, `@bind`, `@standalone`, +- unsupported decorators other than `@private`, `@bind`, `@native_abi("c")`, `@standalone`, `@native_call`, `@native_type`, `@overload("specific")`, the class-operator `generic=` form, `@raises`, `@nogil`, and `@staticmethod`. @@ -2518,6 +2549,9 @@ ambiguous, unsafe, or stale before wrapper lowering: procedure name. - `@overload(...)` combined with `@native_call(...)`; the linked concrete procedure owns native projection metadata. +- `@native_abi(...)` with a value other than `"c"`, on a class or value, or in + a C-native contract; the decorator is only the source-free spelling of a + Fortran procedure's `bind(C)` ABI. ## Remaining Format And Runtime Work diff --git a/mkdocs.yml b/mkdocs.yml index ca0c17a6e..ff0060c59 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -152,6 +152,7 @@ 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/codegen/c/binding.py b/prik/codegen/c/binding.py index 61f02b651..16e116e69 100644 --- a/prik/codegen/c/binding.py +++ b/prik/codegen/c/binding.py @@ -9,6 +9,7 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass, replace import math import re @@ -26,13 +27,10 @@ CallbackResultAction, CallbackTransferAction, ClassConstructorKind, - DerivedActualAccess, - DerivedCallAction, DerivedDummyCategory, DerivedFieldAccessMechanism, DerivedObjectStorage, DerivedOwnerRetention, - DerivedRelease, DerivedWriteback, DirectResultABI, ModuleObjectAccessMechanism, @@ -42,6 +40,8 @@ NativeArrayDefaultConstruction, NativeArrayOperation, NativeDescriptorHandoffABI, + EntrypointProjectionAction, + EntrypointPassingConvention, OptionalMode, OverloadMatchKind, PythonExceptionKind, @@ -79,7 +79,6 @@ CSwitch, CodeExpression, ) -from prik.naming.native_symbols import NativeSymbolNames from prik.codegen.overloads import OverloadPlanQueries from prik.planning.models import ( ArrayHandoffPlan, @@ -99,7 +98,13 @@ NamespacePlan, NativeArrayActualPlan, NativeArrayHandlePlan, - NativeCallSlotPlan, + NativeEntrypointABIValueKind, + NativeEntrypointABIValuePlan, + GeneratedSupportProcedureImplementationOwner, + GeneratedSupportProcedureEntrypointPlan, + NativeEntrypointParameterPlan, + NativeEntrypointProjectedSlotPlan, + NativeEntrypointResultPlan, OverloadArgumentMatchPlan, OverloadPlan, ResultPlan, @@ -135,7 +140,7 @@ class _CFunctionContext: """Per-function names and role substitutions shared across C lowering. The record is derived from a completed function plan and is read-only while - declarations, conversion nodes, bridge calls, and result assembly are + declarations, conversion nodes, entrypoint calls, and result assembly are emitted. """ @@ -209,9 +214,6 @@ def _require_function_supported(self, function: FunctionPlan) -> None: for argument in function.arguments: self._require_argument_supported(argument) self._require_function_results_supported(function) - for slot in function.native_call_slots: - if slot.source_kind == "result": - self._require_backend_type_supported(slot.semantic_type_name, slot.datatype_family) for action in function.writeback_actions: self._require_backend_type_supported(action.semantic_type_name, action.datatype_family) @@ -248,14 +250,24 @@ def binding_module(self, plan: ModulePlan) -> CModule: then assembles module support, runtime helpers, wrappers, and module initialization in emitted dependency order. """ - # Stage 1: complete the immutable name index consumed by Python-surface emission. + # Stage 1: index planner-owned cross-language operations before any lowering. + self._generated_support_procedure_entrypoints = { + (procedure.owner_path, procedure.role): procedure for procedure in plan.entrypoint.support_procedures + } + self._derived_owner_paths = { + derived.backend_symbol: derived.owner_path for derived in self._derived_types(plan) + } + self._binding_owned_derived_owner_paths = frozenset(plan.binding.owned_derived_type_owner_paths) + self._binding_allocatable_holder_owner_paths = frozenset(plan.binding.allocatable_holder_type_owner_paths) + self._binding_pointer_holder_owner_paths = frozenset(plan.binding.pointer_holder_type_owner_paths) + # Stage 2: complete the immutable name index consumed by Python-surface emission. class_python_names = { surface.type_identity: surface.python_names[0] for namespace in plan.namespaces for surface in namespace.classes if surface.python_names } - # Stage 2: select support and assemble generated functions in dependency order. + # Stage 3: select support and assemble generated functions in dependency order. functions = tuple(function for namespace in plan.namespaces for function in self.visit(namespace)) needs_native_support = self.requires_native_support(plan) needs_free = self._module_needs_allocator(plan) @@ -281,6 +293,85 @@ def binding_module(self, plan: ModulePlan) -> CModule: ), ) + def _generated_support_procedure_entrypoint( + self, owner_path: str, role: str + ) -> GeneratedSupportProcedureEntrypointPlan: + """Return one required planner-owned operation without a naming fallback.""" + try: + return self._generated_support_procedure_entrypoints[(owner_path, role)] + except (AttributeError, KeyError): + raise ValueError(f"Missing generated support procedure entrypoint {owner_path!r} role {role!r}") from None + + def _generated_support_procedure_entrypoints_for( + self, + owner_path: str, + role_prefix: str, + ) -> tuple[GeneratedSupportProcedureEntrypointPlan, ...]: + """Return planner-ordered operations for one owner and role family.""" + return tuple( + operation + for operation in self._generated_support_procedure_entrypoints.values() + if operation.owner_path == owner_path and operation.role.startswith(role_prefix) + ) + + def _has_generated_support_procedure_entrypoint(self, owner_path: str, role: str) -> bool: + """Return whether planning registered one generated support procedure.""" + return (owner_path, role) in self._generated_support_procedure_entrypoints + + def _generated_support_procedure_entrypoint_prototype( + self, + operation: GeneratedSupportProcedureEntrypointPlan, + ) -> CFunctionPrototype: + """Lower one complete planner-owned C ABI into a declaration.""" + if operation.implementation_owner is not GeneratedSupportProcedureImplementationOwner.FORTRAN: + raise ValueError(f"C cannot declare binding-owned operation {operation.key!r} as an external bridge") + return CFunctionPrototype( + operation.symbol_name, + self._support_procedure_c_type(operation.signature.result), + tuple(self._support_procedure_c_parameter(parameter) for parameter in operation.signature.parameters), + ) + + def _support_procedure_c_parameter(self, value: NativeEntrypointABIValuePlan) -> CParameter: + """Lower one ordered generated-support C-ABI parameter.""" + if value.kind is NativeEntrypointABIValueKind.CALLBACK and value.callback_signature is not None: + if value.c_type_name is not None: + return CParameter(value.c_name, value.c_type_name) + return CParameter( + value.c_name, + self._support_procedure_c_type(value.callback_signature.result), + tuple(self._support_procedure_c_type(item) for item in value.callback_signature.parameters), + ) + return CParameter(value.c_name, self._support_procedure_c_type(value)) + + @staticmethod + def _support_procedure_c_type(value: NativeEntrypointABIValuePlan) -> str: + """Spell one structured generated-support ABI value for C.""" + base_types = { + NativeEntrypointABIValueKind.VOID: "void", + NativeEntrypointABIValueKind.BOOL: "bool", + NativeEntrypointABIValueKind.INT: "int", + NativeEntrypointABIValueKind.INT8: "int8_t", + NativeEntrypointABIValueKind.INT64: "int64_t", + NativeEntrypointABIValueKind.OPAQUE: "void", + NativeEntrypointABIValueKind.CHARACTER: "char", + NativeEntrypointABIValueKind.DESCRIPTOR: "CFI_cdesc_t", + } + if value.kind is NativeEntrypointABIValueKind.SEMANTIC_SCALAR: + if value.semantic_type_name is None: + raise ValueError(f"Generated-support ABI value {value.role!r} has no semantic scalar type") + base = PrimitiveScalarTypeRegistry.type_for(value.semantic_type_name).c_spelling + elif value.kind is NativeEntrypointABIValueKind.CALLBACK: + if value.c_type_name is None: + raise ValueError(f"Generated-support ABI callback {value.role!r} has no C typedef") + base = value.c_type_name + else: + try: + base = base_types[value.kind] + except KeyError: + raise ValueError(f"Unsupported generated-support C ABI kind {value.kind.value!r}") from None + prefix = "const " if value.const else "" + return f"{prefix}{base}{' *' * value.pointer_depth}" + def binding_modules(self, plan: ModulePlan) -> tuple[CModule, ...]: """Build one implementation module or independently compilable wrapper shards. @@ -335,7 +426,7 @@ def _binding_worker_modules( name=f"{module.name}_{index:03d}", defines=worker_defines, includes=module.includes, - declarations=tuple(self._bridge_prototype(function) for function in group), + declarations=tuple(self._entrypoint_prototype(function) for function in group), functions=tuple(wrappers[self._binding_function_name(function)] for function in group), ) for index, group in enumerate(function_groups, start=1) @@ -453,7 +544,7 @@ def _module_uses_extent_power(self, plan: ModulePlan) -> bool: def _function_uses_extent_power(self, function: FunctionPlan) -> bool: """Scan one function's direct and callback transfer arrays for ``**``.""" - direct_owners = (*function.arguments, *function.results, *function.native_call_slots) + direct_owners = (*function.arguments, *function.results) if any(self._array_uses_extent_power(owner.array) for owner in direct_owners): return True callbacks = (argument.callback for argument in function.arguments if argument.callback is not None) @@ -572,23 +663,13 @@ def _module_needs_allocator(self, plan: ModulePlan) -> bool: ) or any(self._function_needs_allocator(function) for function in self._functions(plan)) def _function_needs_allocator(self, function: FunctionPlan) -> bool: - """Return whether one binding/bridge function owns allocated string storage.""" - return ( - any( - result.scalar_descriptor is not None - or result.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY} - for result in function.results - ) - or any( - argument.object_kind is ObjectKind.STRING - and argument.binding.codegen_action is CodegenAction.COPY_IN_OUT - for argument in function.arguments - ) - or any( - slot.scalar_descriptor is not None or slot.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY} - for slot in function.native_call_slots - if slot.source_kind == "result" - ) + """Return whether one wrapper function requires allocated string storage.""" + return any( + result.scalar_descriptor is not None or result.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY} + for result in function.entrypoint.results + ) or any( + argument.object_kind is ObjectKind.STRING and argument.binding.codegen_action is CodegenAction.COPY_IN_OUT + for argument in function.arguments ) def _module_defines(self, plan: ModulePlan, needs_native_support: bool) -> tuple[CMacroDefinition, ...]: @@ -721,21 +802,21 @@ def _callback_runtime_declarations(self, plan: ModulePlan) -> tuple: declarations.extend( ( CStructDefinition( - callback.context_type_symbol, + callback.binding.context_type_symbol, ( CParameter("callable", "PyObject *"), CParameter("module", "PyObject *"), CParameter("thread_id", "unsigned long"), CParameter( "previous", - f"struct {callback.context_type_symbol} *", + f"struct {callback.binding.context_type_symbol} *", ), CParameter("last_result", "PyObject *"), ), ), CDeclaration( - callback.context_current_symbol, - f"static _Thread_local {callback.context_type_symbol} *", + callback.binding.context_current_symbol, + f"static _Thread_local {callback.binding.context_type_symbol} *", CodeExpression("NULL"), ), ) @@ -757,7 +838,7 @@ def _callback_runtime_functions(self, plan: ModulePlan) -> tuple[CFunction, ...] def _callback_abort_function(callback: CallbackHandoffPlan) -> CFunction: """Emit the single non-returning traceback boundary for one site.""" return CFunction( - callback.abort_symbol, + callback.binding.abort_symbol, "void", parameters=(CParameter("message", "const char *"),), storage="static", @@ -778,8 +859,8 @@ def _callback_trampoline_function(self, callback: CallbackHandoffPlan) -> CFunct nodes = [ CDeclaration( context, - f"{callback.context_type_symbol} *", - CodeExpression(callback.context_current_symbol), + f"{callback.binding.context_type_symbol} *", + CodeExpression(callback.binding.context_current_symbol), ), CIf( CodeExpression(f"{context} == NULL || {context}->thread_id != PyThread_get_thread_ident()"), @@ -790,7 +871,9 @@ def _callback_trampoline_function(self, callback: CallbackHandoffPlan) -> CFunct 'PyErr_SetString(PyExc_RuntimeError, "callback invoked outside its entering Python thread")' ) ), - CExpressionStatement(CodeExpression(f'{callback.abort_symbol}("callback thread violation")')), + CExpressionStatement( + CodeExpression(f'{callback.binding.abort_symbol}("callback thread violation")') + ), ), ), CDeclaration(gil, "PyGILState_STATE", CodeExpression("PyGILState_Ensure()")), @@ -823,10 +906,13 @@ def _callback_trampoline_function(self, callback: CallbackHandoffPlan) -> CFunct *self._callback_result_nodes(callback, context, gil), ) ) + operation = callback.entrypoint.support_procedure return CFunction( - callback.trampoline_symbol, - self._callback_c_return_type(callback), - parameters=self._callback_c_parameters(callback.arguments), + operation.symbol_name, + self._support_procedure_c_type(operation.signature.result), + parameters=tuple( + self._support_procedure_c_parameter(parameter) for parameter in operation.signature.parameters + ), body=tuple(nodes), ) @@ -839,7 +925,7 @@ def _callback_abort_if_null( """Build callback abort if null from the supplied local lowering values; emitted nodes only project completed binding actions.""" return CIf( CodeExpression(f"{name} == NULL"), - body=(CExpressionStatement(CodeExpression(f'{callback.abort_symbol}("{message}")')),), + body=(CExpressionStatement(CodeExpression(f'{callback.binding.abort_symbol}("{message}")')),), ) def _callback_python_argument_nodes( @@ -1022,7 +1108,9 @@ def _callback_void_result_nodes(callback: CallbackHandoffPlan, gil: str) -> tupl CExpressionStatement( CodeExpression('PyErr_SetString(PyExc_TypeError, "callback subroutine must return None")') ), - CExpressionStatement(CodeExpression(f'{callback.abort_symbol}("invalid callback return value")')), + CExpressionStatement( + CodeExpression(f'{callback.binding.abort_symbol}("invalid callback return value")') + ), ), ), CExpressionStatement(CodeExpression("Py_DECREF(callback_result)")), @@ -1046,7 +1134,9 @@ def _callback_scalar_result_nodes( CIf( CodeExpression(self._scalar_unpack_expression(scalar, "callback_result", "callback_value") + " < 0"), body=( - CExpressionStatement(CodeExpression(f'{callback.abort_symbol}("invalid callback return value")')), + CExpressionStatement( + CodeExpression(f'{callback.binding.abort_symbol}("invalid callback return value")') + ), ), ), CExpressionStatement(CodeExpression("Py_DECREF(callback_result)")), @@ -1082,7 +1172,9 @@ def _callback_array_result_nodes( CExpressionStatement( CodeExpression('PyErr_SetString(PyExc_TypeError, "invalid callback array result")') ), - CExpressionStatement(CodeExpression(f'{callback.abort_symbol}("invalid callback return value")')), + CExpressionStatement( + CodeExpression(f'{callback.binding.abort_symbol}("invalid callback return value")') + ), ), ), CExpressionStatement(CodeExpression(f"Py_XDECREF({context}->last_result)")), @@ -1142,7 +1234,9 @@ def _callback_derived_result_nodes( f'PyErr_SetString(PyExc_TypeError, "callback must return {transfer.semantic_type_name}")' ) ), - CExpressionStatement(CodeExpression(f'{callback.abort_symbol}("invalid callback return value")')), + CExpressionStatement( + CodeExpression(f'{callback.binding.abort_symbol}("invalid callback return value")') + ), ), ), CExpressionStatement(CodeExpression("Py_DECREF(callback_expected_type)")), @@ -1173,42 +1267,6 @@ def _callback_derived_result_nodes( CReturn(CodeExpression("callback_value")), ) - def _callback_c_parameters( - self, - transfers: tuple[CallbackTransferPlan, ...], - ) -> tuple[CParameter, ...]: - """Build callback c parameters from the supplied local lowering values; emitted nodes only project completed binding actions.""" - return tuple( - parameter for transfer in transfers for parameter in self._callback_c_transfer_parameters(transfer) - ) - - def _callback_c_transfer_parameters( - self, - transfer: CallbackTransferPlan, - ) -> tuple[CParameter, ...]: - """Build callback c transfer parameters from the supplied local lowering values; emitted nodes only project completed binding actions.""" - base = self._callback_parameter_base_name(transfer) - if transfer.abi is CallbackABIKind.VALUE: - scalar = PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name) - return (CParameter(base, scalar.c_spelling),) - if transfer.abi is CallbackABIKind.DATA_AND_SHAPE: - return ( - CParameter(f"{base}_data", "void *"), - *(CParameter(f"{base}_extent_{axis}", "int64_t") for axis in range(transfer.rank)), - ) - if transfer.abi is CallbackABIKind.DATA_AND_LENGTH: - return CParameter(f"{base}_data", "void *"), CParameter(f"{base}_length", "int64_t") - return (CParameter(f"{base}_data", "void *"),) - - def _callback_c_return_type(self, callback: CallbackHandoffPlan) -> str: - """Return the binding-local callback c return type derived from the supplied local lowering values; this helper preserves completed policy.""" - transfer = callback.result.transfer - if callback.result.action is CallbackResultAction.RETURN_VOID: - return "void" - if callback.result.action is CallbackResultAction.RETURN_SCALAR and transfer is not None: - return PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name).c_spelling - return "void *" - @staticmethod def _callback_parameter_base_name(transfer: CallbackTransferPlan) -> str: """Return the binding-local callback parameter base name derived from the supplied local lowering values; this helper preserves completed policy.""" @@ -1702,40 +1760,11 @@ def _derived_origin_declarations(self, plan: ModulePlan) -> tuple: return tuple(declarations) def _derived_origin_bridge_prototypes(self, variable: ModuleVariablePlan) -> tuple[CFunctionPrototype, ...]: - """Return the binding-local derived origin bridge prototypes derived from the supplied completed binding records; this helper preserves completed policy.""" - prototypes = [] - if self._derived_origin_supports(variable, "present"): - prototypes.append(CFunctionPrototype(self._derived_origin_bridge_name(variable, "present"), "bool")) - if self._derived_origin_supports(variable, "address"): - prototypes.append(CFunctionPrototype(self._derived_origin_bridge_name(variable, "address"), "void *")) - if self._derived_origin_supports(variable, "scoped"): - prototypes.append( - CFunctionPrototype( - self._derived_origin_bridge_name(variable, "scoped"), - "int", - ( - CParameter("consumer", "prik_derived_consumer_fn"), - CParameter("context", "void *"), - ), - ) - ) - if self._derived_origin_supports(variable, "checkout"): - prototypes.append( - CFunctionPrototype( - self._derived_origin_bridge_name(variable, "checkout"), - "int", - (CParameter("holder", "void **"),), - ) - ) - if self._derived_origin_supports(variable, "restore"): - prototypes.append( - CFunctionPrototype( - self._derived_origin_bridge_name(variable, "restore"), - "int", - (CParameter("holder", "void *"),), - ) - ) - return tuple(prototypes) + """Declare planner-owned derived-origin bridge operations.""" + return tuple( + self._generated_support_procedure_entrypoint_prototype(operation) + for operation in self._generated_support_procedure_entrypoints_for(variable.owner_path, "derived_origin:") + ) def _derived_origin_wrapper_prototypes(self, variable: ModuleVariablePlan) -> tuple[CFunctionPrototype, ...]: """Return the binding-local derived origin wrapper prototypes derived from the supplied completed binding records; this helper preserves completed policy.""" @@ -1952,23 +1981,9 @@ def _derived_origin_capsule_method(self, variable: ModuleVariablePlan) -> CFunct ), ) - @staticmethod - def _derived_origin_supports(variable: ModuleVariablePlan, operation: str) -> bool: - """Return derived origin supports from the supplied completed binding records; this helper preserves the selected binding behavior.""" - storage = variable.derived.handoff.storage - support = { - DerivedObjectStorage.MODULE_PROXY: {"scoped"}, - DerivedObjectStorage.MODULE_TARGET: {"address"}, - DerivedObjectStorage.MODULE_ALLOCATABLE: {"present", "scoped", "checkout", "restore"}, - DerivedObjectStorage.MODULE_ALLOCATABLE_TARGET: { - "present", - "address", - "checkout", - "restore", - }, - DerivedObjectStorage.MODULE_POINTER: {"present", "scoped", "checkout", "restore"}, - } - return operation in support.get(storage, set()) + def _derived_origin_supports(self, variable: ModuleVariablePlan, operation: str) -> bool: + """Return whether planning registered one derived-origin operation.""" + return (variable.owner_path, f"derived_origin:{operation}") in self._generated_support_procedure_entrypoints def _derived_origin_needs_guard(self, variable: ModuleVariablePlan) -> bool: """Return derived origin needs guard from the supplied completed binding records; this helper preserves the selected binding behavior.""" @@ -1980,8 +1995,10 @@ def _derived_origin_symbol(variable: ModuleVariablePlan) -> str: return CBindingNames.derived_origin_symbol(variable) def _derived_origin_bridge_name(self, variable: ModuleVariablePlan, operation: str) -> str: - """Return the binding-local derived origin bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_prik_origin_{self._derived_origin_symbol(variable)}_{operation}" + """Return one planner-owned derived-origin entrypoint symbol.""" + return self._generated_support_procedure_entrypoint( + variable.owner_path, f"derived_origin:{operation}" + ).symbol_name def _derived_origin_wrapper_name(self, variable: ModuleVariablePlan, operation: str) -> str: """Return the binding-local derived origin wrapper name derived from the supplied completed binding records; this helper preserves completed policy.""" @@ -2015,29 +2032,31 @@ def _module_declarations( *self._callback_runtime_declarations(plan), *self._derived_call_runtime_declarations(plan), *self._derived_origin_declarations(plan), - *(self._bridge_prototype(function) for function in self._functions(plan)), + *(self._entrypoint_prototype(function) for function in self._functions(plan)), *self._class_constructor_prototypes(plan), - *(self._derived_destroy_bridge_prototype(derived) for derived in self._owned_derived_types(plan)), *( - self._allocatable_holder_destroy_bridge_prototype(derived) - for derived in self._allocatable_holder_types(plan) + self._derived_destroy_entrypoint_prototype(derived) + for derived in self._binding_owned_derived_types(plan) ), - *(self._pointer_holder_destroy_bridge_prototype(derived) for derived in self._pointer_holder_types(plan)), *( - CFunctionPrototype( - self._allocatable_holder_presence_bridge_name(derived.backend_symbol), - "bool", - (CParameter("address", "void *"),), + self._allocatable_holder_destroy_entrypoint_prototype(derived) + for derived in self._binding_allocatable_holder_types(plan) + ), + *( + self._pointer_holder_destroy_entrypoint_prototype(derived) + for derived in self._binding_pointer_holder_types(plan) + ), + *( + self._generated_support_procedure_entrypoint_prototype( + self._generated_support_procedure_entrypoint(derived.owner_path, "holder:allocatable:present") ) - for derived in self._allocatable_holder_types(plan) + for derived in self._binding_allocatable_holder_types(plan) ), *( - CFunctionPrototype( - self._pointer_holder_presence_bridge_name(derived.backend_symbol), - "bool", - (CParameter("address", "void *"),), + self._generated_support_procedure_entrypoint_prototype( + self._generated_support_procedure_entrypoint(derived.owner_path, "holder:pointer:present") ) - for derived in self._pointer_holder_types(plan) + for derived in self._binding_pointer_holder_types(plan) ), *self._owned_native_array_bridge_prototypes(plan), *self._default_native_array_bridge_prototypes(plan), @@ -2092,150 +2111,53 @@ def _derived_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: """Return namespace-owned opaque types in stable plan order.""" return tuple(derived for namespace in plan.namespaces for derived in namespace.derived_types) - def _owned_derived_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: - """Return only types whose completed transfers create wrapper-owned storage.""" - identities = self._owned_derived_result_identities(plan) - identities.update(self._owned_derived_module_identities(plan)) - identities.update(self._constructible_class_identities(plan)) - return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) - - @staticmethod - def _constructible_class_identities(plan: ModulePlan) -> set[tuple[str, str]]: - """Return class identities whose completed constructor allocates storage.""" - return { - surface.type_identity - for namespace in plan.namespaces - for surface in namespace.classes - if surface.constructor.kind is not ClassConstructorKind.ABSENT - } - - def _owned_derived_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Return owned derived result identities from the supplied completed binding records; this helper preserves the selected binding behavior.""" - return { - result.derived.type_identity - for function in self._functions(plan) - for result in function.results - if result.derived is not None - and result.derived.release is DerivedRelease.WRAPPER_DESTROY - and result.derived.storage - not in {DerivedObjectStorage.ALLOCATABLE_HOLDER, DerivedObjectStorage.POINTER_HOLDER} - } - - def _owned_derived_module_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Return owned derived module identities from the supplied completed binding records; this helper preserves the selected binding behavior.""" - return { - variable.derived.handoff.type_identity - for variable in self._variables(plan) - if variable.derived is not None and variable.derived.access is ModuleObjectAccessMechanism.VALUE_COPY - } - - def _allocatable_holder_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: - """Return types carried by wrapper-owned typed allocatable holders.""" - identities = self._allocatable_holder_result_identities(plan) - identities.update(self._allocatable_holder_argument_identities(plan)) - return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) - - def _allocatable_holder_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Build allocatable holder result identities from the supplied completed binding records; emitted nodes only project completed binding actions.""" - return { - result.derived.type_identity - for function in self._functions(plan) - for result in function.results - if result.derived is not None and result.derived.storage is DerivedObjectStorage.ALLOCATABLE_HOLDER - } - - def _allocatable_holder_argument_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Build allocatable holder argument identities from the supplied completed binding records; emitted nodes only project completed binding actions.""" - return { - argument.derived.type_identity - for function in self._functions(plan) - for argument in function.arguments - if argument.derived is not None - and argument.derived_call is not None - and argument.bridge.descriptor_output_role is not None - and any( - case.access is DerivedActualAccess.ALLOCATABLE_HOLDER - for case in argument.derived_call.cases - if case.action is not DerivedCallAction.INCOMPATIBLE - ) - } - - def _pointer_holder_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: - """Return the binding-local pointer holder types derived from the supplied completed binding records; this helper preserves completed policy.""" - identities = self._pointer_holder_result_identities(plan) - identities.update(self._pointer_holder_argument_identities(plan)) - return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) + def _binding_derived_types( + self, + plan: ModulePlan, + owner_paths: frozenset[str], + ) -> tuple[DerivedTypePlan, ...]: + """Join planner-owned binding support membership to derived records.""" + return tuple(derived for derived in self._derived_types(plan) if derived.owner_path in owner_paths) - def _pointer_holder_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Return pointer holder result identities from the supplied completed binding records; this helper preserves the selected binding behavior.""" - return { - result.derived.type_identity - for function in self._functions(plan) - for result in function.results - if result.derived is not None and result.derived.storage is DerivedObjectStorage.POINTER_HOLDER - } + def _binding_owned_derived_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: + """Return planned direct-capsule owners in stable derived-type order.""" + return self._binding_derived_types(plan, self._binding_owned_derived_owner_paths) - def _pointer_holder_argument_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Return pointer holder argument identities from the supplied completed binding records; this helper preserves the selected binding behavior.""" - return { - argument.derived.type_identity - for function in self._functions(plan) - for argument in function.arguments - if argument.derived is not None - and argument.derived_call is not None - and argument.bridge.descriptor_output_role is not None - and any( - case.access is DerivedActualAccess.POINTER_HOLDER - for case in argument.derived_call.cases - if case.action is not DerivedCallAction.INCOMPATIBLE - ) - } + def _binding_allocatable_holder_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: + """Return planned allocatable-holder Python support owners.""" + return self._binding_derived_types(plan, self._binding_allocatable_holder_owner_paths) - @staticmethod - def _uses_allocatable_holder(argument: ArgumentTransferPlan) -> bool: - """Return whether allocatable holder is required by the supplied completed binding records; this helper does not choose policy.""" - call = argument.derived_call - return bool( - call is not None - and any( - case.access is DerivedActualAccess.ALLOCATABLE_HOLDER - for case in call.cases - if case.action is not DerivedCallAction.INCOMPATIBLE - ) - ) + def _binding_pointer_holder_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: + """Return planned pointer-holder Python support owners.""" + return self._binding_derived_types(plan, self._binding_pointer_holder_owner_paths) - def _derived_destroy_bridge_prototype(self, derived: DerivedTypePlan) -> CFunctionPrototype: + def _derived_destroy_entrypoint_prototype(self, derived: DerivedTypePlan) -> CFunctionPrototype: """Declare the native-aware destroy helper for one opaque type.""" - return CFunctionPrototype( - self._derived_destroy_bridge_name(derived.backend_symbol), - "void", - (CParameter("address", "void *"),), + return self._generated_support_procedure_entrypoint_prototype( + self._generated_support_procedure_entrypoint(derived.owner_path, "derived:destroy") ) - def _allocatable_holder_destroy_bridge_prototype(self, derived: DerivedTypePlan) -> CFunctionPrototype: + def _allocatable_holder_destroy_entrypoint_prototype(self, derived: DerivedTypePlan) -> CFunctionPrototype: """Declare one typed holder destructor bridge.""" - return CFunctionPrototype( - self._allocatable_holder_destroy_bridge_name(derived.backend_symbol), - "void", - (CParameter("address", "void *"),), + return self._generated_support_procedure_entrypoint_prototype( + self._generated_support_procedure_entrypoint(derived.owner_path, "holder:allocatable:destroy") ) - def _pointer_holder_destroy_bridge_prototype(self, derived: DerivedTypePlan) -> CFunctionPrototype: + def _pointer_holder_destroy_entrypoint_prototype(self, derived: DerivedTypePlan) -> CFunctionPrototype: """Return the binding-local pointer holder destroy bridge prototype derived from the supplied local lowering values; this helper preserves completed policy.""" - return CFunctionPrototype( - self._pointer_holder_destroy_bridge_name(derived.backend_symbol), - "void", - (CParameter("address", "void *"),), + return self._generated_support_procedure_entrypoint_prototype( + self._generated_support_procedure_entrypoint(derived.owner_path, "holder:pointer:destroy") ) def _derived_capsule_destructor_functions(self, plan: ModulePlan) -> tuple[CFunction, ...]: """Emit one capsule destructor that delegates to the native bridge.""" - direct = tuple(self._derived_capsule_destructor(derived) for derived in self._owned_derived_types(plan)) + direct = tuple(self._derived_capsule_destructor(derived) for derived in self._binding_owned_derived_types(plan)) holders = tuple( - self._allocatable_holder_capsule_destructor(derived) for derived in self._allocatable_holder_types(plan) + self._allocatable_holder_capsule_destructor(derived) + for derived in self._binding_allocatable_holder_types(plan) ) pointers = tuple( - self._pointer_holder_capsule_destructor(derived) for derived in self._pointer_holder_types(plan) + self._pointer_holder_capsule_destructor(derived) for derived in self._binding_pointer_holder_types(plan) ) return (*direct, *holders, *pointers) @@ -2246,9 +2168,11 @@ def _class_constructor_prototypes(self, plan: ModulePlan) -> tuple[CFunctionProt prototype for namespace in plan.namespaces for surface in namespace.classes - if surface.constructor.kind is not ClassConstructorKind.ABSENT + if self._has_generated_support_procedure_entrypoint(surface.owner_path, "class:create") for prototype in ( - CFunctionPrototype(CBindingNames.class_create_bridge(surface), "void *"), + self._generated_support_procedure_entrypoint_prototype( + self._generated_support_procedure_entrypoint(surface.owner_path, "class:create") + ), CFunctionPrototype( CBindingNames.class_create_method(surface), "PyObject *", @@ -2265,7 +2189,7 @@ def _class_constructor_functions(self, plan: ModulePlan) -> tuple[CFunction, ... self._class_constructor_function(surface, derived_by_identity[surface.type_identity]) for namespace in plan.namespaces for surface in namespace.classes - if surface.constructor.kind is not ClassConstructorKind.ABSENT + if self._has_generated_support_procedure_entrypoint(surface.owner_path, "class:create") ) def _class_constructor_function( @@ -2278,7 +2202,8 @@ def _class_constructor_function( capsule = "capsule" helper = "wrapper_helper" result = "result" - destroy = self._derived_destroy_bridge_name(derived.backend_symbol) + destroy = self._generated_support_procedure_entrypoint(derived.owner_path, "derived:destroy").symbol_name + create = self._generated_support_procedure_entrypoint(surface.owner_path, "class:create").symbol_name return CFunction( CBindingNames.class_create_method(surface), "PyObject *", @@ -2289,7 +2214,7 @@ def _class_constructor_function( CodeExpression('!PyArg_ParseTuple(args, "")'), body=(CReturn(CodeExpression("NULL")),), ), - CDeclaration(address, "void *", CodeExpression(f"{CBindingNames.class_create_bridge(surface)}()")), + CDeclaration(address, "void *", CodeExpression(f"{create}()")), CIf( CodeExpression(f"{address} == NULL"), body=( @@ -2430,95 +2355,51 @@ def _derived_field_bridge_prototypes(self, plan: ModulePlan) -> tuple[CFunctionP ) def _direct_field_bridge_prototype_entries(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: - """Return the binding-local direct field bridge prototype entries derived from the supplied completed binding records; this helper preserves completed policy.""" + """Declare planner-owned direct-field bridge operations.""" return tuple( - prototype + self._generated_support_procedure_entrypoint_prototype(operation) for derived in self._derived_types(plan) for field in derived.fields - for prototype in self._direct_field_bridge_prototypes(derived, field) + for operation in self._generated_support_procedure_entrypoints_for( + f"{derived.owner_path}.{field.name}", "field:direct:" + ) ) def _module_member_bridge_prototype_entries(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: - """Return the binding-local module member bridge prototype entries derived from the supplied completed binding records; this helper preserves completed policy.""" + """Declare planner-owned module-member bridge operations.""" return tuple( - prototype + self._generated_support_procedure_entrypoint_prototype(operation) for variable in self._derived_member_proxy_variables(plan) for member in variable.derived.member_paths - for prototype in self._module_member_bridge_prototypes(variable, member) + for operation in self._generated_support_procedure_entrypoints_for( + ".".join((variable.owner_path, *member.path)), "field:module:" + ) ) def _allocatable_holder_field_bridge_prototype_entries( self, plan: ModulePlan, ) -> tuple[CFunctionPrototype, ...]: - """Return the binding-local allocatable holder field bridge prototype entries derived from the supplied completed binding records; this helper preserves completed policy.""" + """Declare planner-owned allocatable-holder field bridge operations.""" return tuple( - prototype - for derived in self._allocatable_holder_types(plan) + self._generated_support_procedure_entrypoint_prototype(operation) + for derived in self._binding_allocatable_holder_types(plan) for field in derived.fields - for prototype in self._allocatable_holder_field_bridge_prototypes(derived, field) + for operation in self._generated_support_procedure_entrypoints_for( + f"{derived.owner_path}.{field.name}", "field:allocatable:" + ) ) def _pointer_holder_field_bridge_prototype_entries(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: - """Return the binding-local pointer holder field bridge prototype entries derived from the supplied completed binding records; this helper preserves completed policy.""" + """Declare planner-owned pointer-holder field bridge operations.""" return tuple( - prototype - for derived in self._pointer_holder_types(plan) + self._generated_support_procedure_entrypoint_prototype(operation) + for derived in self._binding_pointer_holder_types(plan) for field in derived.fields - for prototype in self._pointer_holder_field_bridge_prototypes(derived, field) - ) - - def _allocatable_holder_field_bridge_prototypes( - self, - derived: DerivedTypePlan, - field: DerivedFieldPlan, - ) -> tuple[CFunctionPrototype, ...]: - """Declare typed scalar holder member operations.""" - if field.access is not DerivedFieldAccessMechanism.SCALAR_VALUE: - raise ValueError(f"Unsupported allocatable-holder field for {field.owner_path!r}: {field.access.value}") - value_type = self._derived_field_c_type(field) - prototypes = [ - CFunctionPrototype( - self._allocatable_holder_field_bridge_name(derived, field, "get"), - value_type, - (CParameter("owner", "void *"),), - ) - ] - if field.setter_action is SetterAction.WRITE_THROUGH: - prototypes.append( - CFunctionPrototype( - self._allocatable_holder_field_bridge_name(derived, field, "set"), - "void", - (CParameter("owner", "void *"), CParameter("value", value_type)), - ) - ) - return tuple(prototypes) - - def _pointer_holder_field_bridge_prototypes( - self, - derived: DerivedTypePlan, - field: DerivedFieldPlan, - ) -> tuple[CFunctionPrototype, ...]: - """Return the binding-local pointer holder field bridge prototypes derived from the supplied completed binding records; this helper preserves completed policy.""" - if field.access is not DerivedFieldAccessMechanism.SCALAR_VALUE: - raise ValueError(f"Unsupported pointer-holder field for {field.owner_path!r}: {field.access.value}") - value_type = self._derived_field_c_type(field) - prototypes = [ - CFunctionPrototype( - self._pointer_holder_field_bridge_name(derived, field, "get"), - value_type, - (CParameter("owner_address", "void *"),), - ) - ] - if field.setter_action is SetterAction.WRITE_THROUGH: - prototypes.append( - CFunctionPrototype( - self._pointer_holder_field_bridge_name(derived, field, "set"), - "void", - (CParameter("owner_address", "void *"), CParameter("value", value_type)), - ) + for operation in self._generated_support_procedure_entrypoints_for( + f"{derived.owner_path}.{field.name}", "field:pointer:" ) - return tuple(prototypes) + ) def _derived_private_method_prototypes(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: """Declare private property callables before namespace method tables.""" @@ -2532,292 +2413,49 @@ def _derived_private_method_prototypes(self, plan: ModulePlan) -> tuple[CFunctio for function in self._derived_field_functions(plan) ) - def _direct_field_bridge_prototypes( - self, - derived: DerivedTypePlan, - field: DerivedFieldPlan, - ) -> tuple[CFunctionPrototype, ...]: - """Return getter/setter ABI for one address-backed parent field.""" - if field.access is DerivedFieldAccessMechanism.FIXED_STRING_COPY: - return self._string_field_bridge_prototypes( - self._derived_field_bridge_name(derived, field, "get"), - self._derived_field_bridge_name(derived, field, "set"), - owner_parameter=True, - writable=field.setter_action is SetterAction.WRITE_THROUGH, - ) - if field.access is DerivedFieldAccessMechanism.NATIVE_ARRAY_HANDLE: - return self._derived_handle_bridge_prototypes( - field, - lambda operation: self._derived_handle_bridge_name(derived, field, operation), - owner_parameter=True, - ) - if field.access is DerivedFieldAccessMechanism.ORDINARY_ARRAY_DESCRIPTOR: - return self._ordinary_array_field_bridge_prototypes( - self._derived_field_bridge_name(derived, field, "get"), - self._derived_field_bridge_name(derived, field, "set"), - owner_parameter=True, - writable=field.setter_action is SetterAction.WRITE_THROUGH, - ) - value_type = self._derived_field_c_type(field) - prototypes = [ - CFunctionPrototype( - self._derived_field_bridge_name(derived, field, "get"), - value_type, - (CParameter("owner", "void *"),), - ) - ] - if field.setter_action is SetterAction.WRITE_THROUGH: - prototypes.append( - CFunctionPrototype( - self._derived_field_bridge_name(derived, field, "set"), - "void", - (CParameter("owner", "void *"), CParameter("value", value_type)), - ) - ) - return tuple(prototypes) + def _derived_field_functions(self, plan: ModulePlan) -> tuple[CFunction, ...]: + """Lower address-backed and plain-module field methods.""" + return ( + *self._direct_field_functions_for_plan(plan), + *self._module_member_functions_for_plan(plan), + *self._allocatable_holder_functions_for_plan(plan), + *self._pointer_holder_functions_for_plan(plan), + *self._module_proxy_guard_functions_for_plan(plan), + ) - def _module_member_bridge_prototypes( - self, - variable: ModuleVariablePlan, - member: DerivedMemberPathPlan, - ) -> tuple[CFunctionPrototype, ...]: - """Return typed plain-module member operation declarations.""" - field = member.field - prototypes = [] - if field.access is DerivedFieldAccessMechanism.FIXED_STRING_COPY: - return self._string_field_bridge_prototypes( - self._module_member_bridge_name(variable, member, "get"), - self._module_member_bridge_name(variable, member, "set"), - owner_parameter=False, - writable=field.setter_action is SetterAction.WRITE_THROUGH, - ) - if field.access is DerivedFieldAccessMechanism.NATIVE_ARRAY_HANDLE: - return self._derived_handle_bridge_prototypes( - field, - lambda operation: self._module_member_handle_bridge_name(variable, member, operation), - owner_parameter=False, - ) - if field.access is DerivedFieldAccessMechanism.ORDINARY_ARRAY_DESCRIPTOR: - return self._ordinary_array_field_bridge_prototypes( - self._module_member_bridge_name(variable, member, "get"), - self._module_member_bridge_name(variable, member, "set"), - owner_parameter=False, - writable=field.setter_action is SetterAction.WRITE_THROUGH, - ) - if field.object_kind is ObjectKind.SCALAR: - prototypes.append( - CFunctionPrototype( - self._module_member_bridge_name(variable, member, "get"), - self._derived_field_c_type(field), - ) - ) - if field.setter_action is SetterAction.WRITE_THROUGH: - prototypes.append( - CFunctionPrototype( - self._module_member_bridge_name(variable, member, "set"), - "void", - (CParameter("value", self._derived_field_c_type(field)),), - ) - ) - return tuple(prototypes) + def _direct_field_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: + """Build direct field functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" + return tuple( + function + for derived in self._derived_types(plan) + for field in derived.fields + for function in self._direct_field_functions(derived, field) + ) - @staticmethod - def _string_field_bridge_prototypes( - getter_name: str, - setter_name: str, - *, - owner_parameter: bool, - writable: bool, - ) -> tuple[CFunctionPrototype, ...]: - """Return a fixed-width byte-copy ABI for one scalar string field.""" - owner = (CParameter("owner", "void *"),) if owner_parameter else () - prototypes = [CFunctionPrototype(getter_name, "void", (*owner, CParameter("value", "char *")))] - if writable: - prototypes.append( - CFunctionPrototype( - setter_name, - "void", - (*owner, CParameter("value", "const char *")), - ) - ) - return tuple(prototypes) + def _module_member_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: + """Build module member functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" + return tuple( + function + for variable in self._derived_member_proxy_variables(plan) + for member in variable.derived.member_paths + for function in self._module_member_functions(variable, member) + ) - def _derived_handle_bridge_prototypes( - self, - field: DerivedFieldPlan, - bridge_name, - *, - owner_parameter: bool, - ) -> tuple[CFunctionPrototype, ...]: - """Return typed native operation declarations for one borrowed field handle.""" - handle = field.native_array_handle - if handle is None or handle.array.rank is None: - raise ValueError(f"Native handle field {field.owner_path!r} has no operation plan") - owner = (CParameter("owner", "void *"),) if owner_parameter else () - prototypes = ( - self._derived_handle_bridge_prototype( - operation, - bridge_name(operation), - owner, - handle.array.rank, - ) - for operation in handle.operations + def _allocatable_holder_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: + """Build allocatable holder functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" + derived_types = self._binding_allocatable_holder_types(plan) + fields = tuple( + function + for derived in derived_types + for field in derived.fields + for function in self._allocatable_holder_field_functions(derived, field) ) - return tuple(prototype for prototype in prototypes if prototype is not None) - - def _derived_handle_bridge_prototype( - self, - operation: NativeArrayOperation, - name: str, - owner: tuple[CParameter, ...], - rank: int, - ) -> CFunctionPrototype | None: - """Lower one completed field-handle operation into its bridge ABI.""" - ignored = { - NativeArrayOperation.NATIVE_BYTE_ORDER, - NativeArrayOperation.ALIGNED, - NativeArrayOperation.WRITEABLE, - NativeArrayOperation.LAYOUT, - NativeArrayOperation.TO_NUMPY, - NativeArrayOperation.ARRAY_ACTUAL, - } - if operation in ignored: - return None - if operation in { - NativeArrayOperation.ALLOCATED, - NativeArrayOperation.ASSOCIATED, - NativeArrayOperation.CONTIGUOUS, - }: - return CFunctionPrototype(name, "bool", owner) - if operation is NativeArrayOperation.ELEMENT_LENGTH: - return CFunctionPrototype(name, "int64_t", owner) - if operation is NativeArrayOperation.SHAPE: - return self._derived_handle_shape_prototype(name, owner, rank) - if operation is NativeArrayOperation.DESCRIPTOR: - return self._derived_handle_descriptor_prototype(name, owner) - if operation is NativeArrayOperation.ASSOCIATE: - return CFunctionPrototype( - name, - "void", - (*owner, CParameter("source", "CFI_cdesc_t *")), - ) - if operation in {NativeArrayOperation.ALLOCATE, NativeArrayOperation.RESIZE}: - return self._derived_handle_extent_prototype(name, owner, rank) - if operation in {NativeArrayOperation.DEALLOCATE, NativeArrayOperation.NULLIFY}: - return CFunctionPrototype(name, "void", owner) - raise ValueError(f"Unsupported field handle bridge operation {operation!r}") - - @staticmethod - def _derived_handle_shape_prototype( - name: str, - owner: tuple[CParameter, ...], - rank: int, - ) -> CFunctionPrototype: - """Return the binding-local derived handle shape prototype derived from the supplied local lowering values; this helper preserves completed policy.""" - parameters = (*owner, *(CParameter(f"extent_{axis}", "int64_t *") for axis in range(rank))) - return CFunctionPrototype(name, "void", parameters) - - @staticmethod - def _derived_handle_descriptor_prototype( - name: str, - owner: tuple[CParameter, ...], - ) -> CFunctionPrototype: - """Return the binding-local derived handle descriptor prototype derived from the supplied local lowering values; this helper preserves completed policy.""" - parameters = ( - *owner, - CParameter("callback", "void", function_parameters=("CFI_cdesc_t *", "void *")), - CParameter("context", "void *"), - ) - return CFunctionPrototype(name, "void", parameters) - - @staticmethod - def _derived_handle_extent_prototype( - name: str, - owner: tuple[CParameter, ...], - rank: int, - ) -> CFunctionPrototype: - """Return the binding-local derived handle extent prototype derived from the supplied local lowering values; this helper preserves completed policy.""" - parameters = (*owner, *(CParameter(f"extent_{axis}", "int64_t") for axis in range(rank))) - return CFunctionPrototype(name, "void", parameters) - - @staticmethod - def _ordinary_array_field_bridge_prototypes( - getter_name: str, - setter_name: str, - *, - owner_parameter: bool, - writable: bool, - ) -> tuple[CFunctionPrototype, ...]: - """Return a standard-descriptor callback ABI for one fixed array field.""" - owner = (CParameter("owner", "void *"),) if owner_parameter else () - prototypes = [ - CFunctionPrototype( - getter_name, - "void", - ( - *owner, - CParameter( - "callback", - "void", - function_parameters=("CFI_cdesc_t *", "void *"), - ), - CParameter("context", "void *"), - ), - ) - ] - if writable: - prototypes.append( - CFunctionPrototype( - setter_name, - "void", - (*owner, CParameter("value", "void *")), - ) - ) - return tuple(prototypes) - - def _derived_field_functions(self, plan: ModulePlan) -> tuple[CFunction, ...]: - """Lower address-backed and plain-module field methods.""" - return ( - *self._direct_field_functions_for_plan(plan), - *self._module_member_functions_for_plan(plan), - *self._allocatable_holder_functions_for_plan(plan), - *self._pointer_holder_functions_for_plan(plan), - *self._module_proxy_guard_functions_for_plan(plan), - ) - - def _direct_field_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: - """Build direct field functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" - return tuple( - function - for derived in self._derived_types(plan) - for field in derived.fields - for function in self._direct_field_functions(derived, field) - ) - - def _module_member_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: - """Build module member functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" - return tuple( - function - for variable in self._derived_member_proxy_variables(plan) - for member in variable.derived.member_paths - for function in self._module_member_functions(variable, member) - ) - - def _allocatable_holder_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: - """Build allocatable holder functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" - derived_types = self._allocatable_holder_types(plan) - fields = tuple( - function - for derived in derived_types - for field in derived.fields - for function in self._allocatable_holder_field_functions(derived, field) - ) - presence = tuple(self._allocatable_holder_presence_method(derived) for derived in derived_types) - return (*presence, *fields) + presence = tuple(self._allocatable_holder_presence_method(derived) for derived in derived_types) + return (*presence, *fields) def _pointer_holder_functions_for_plan(self, plan: ModulePlan) -> tuple[CFunction, ...]: """Build pointer holder functions for plan from the supplied completed binding records; emitted nodes only project completed binding actions.""" - derived_types = self._pointer_holder_types(plan) + derived_types = self._binding_pointer_holder_types(plan) fields = tuple( function for derived in derived_types @@ -4098,18 +3736,7 @@ def _field_handle_operation_body(self, owner, field: DerivedFieldPlan, operation }: return self._module_native_array_metadata_body(operation) prefix = self._field_handle_owner_nodes(owner) - bridge = self._field_handle_bridge_name(owner, field, operation) owner_args = self._field_handle_owner_arguments(owner) - if operation in { - NativeArrayOperation.ALLOCATED, - NativeArrayOperation.ASSOCIATED, - NativeArrayOperation.CONTIGUOUS, - }: - return (*prefix, CReturn(CodeExpression(f"PyBool_FromLong({bridge}({owner_args}))"))) - if operation is NativeArrayOperation.ELEMENT_LENGTH: - return (*prefix, CReturn(CodeExpression(f"PyLong_FromLongLong((long long){bridge}({owner_args}))"))) - if operation is NativeArrayOperation.SHAPE: - return (*prefix, *self._field_handle_shape_nodes(field, bridge, owner_args)) if operation in {NativeArrayOperation.DESCRIPTOR, NativeArrayOperation.TO_NUMPY}: callback = self._field_handle_descriptor_callback(owner, field) descriptor_bridge = self._field_handle_bridge_name( @@ -4126,6 +3753,17 @@ def _field_handle_operation_body(self, owner, field: DerivedFieldPlan, operation NativeArrayOperation.DESCRIPTOR, ) return (*prefix, *self._field_handle_actual_nodes(descriptor_bridge, owner_args, callback)) + bridge = self._field_handle_bridge_name(owner, field, operation) + if operation in { + NativeArrayOperation.ALLOCATED, + NativeArrayOperation.ASSOCIATED, + NativeArrayOperation.CONTIGUOUS, + }: + return (*prefix, CReturn(CodeExpression(f"PyBool_FromLong({bridge}({owner_args}))"))) + if operation is NativeArrayOperation.ELEMENT_LENGTH: + return (*prefix, CReturn(CodeExpression(f"PyLong_FromLongLong((long long){bridge}({owner_args}))"))) + if operation is NativeArrayOperation.SHAPE: + return (*prefix, *self._field_handle_shape_nodes(field, bridge, owner_args)) if operation is NativeArrayOperation.ASSOCIATE: return self._field_handle_associate_body(field, prefix, bridge, owner_args) if operation in {NativeArrayOperation.ALLOCATE, NativeArrayOperation.RESIZE}: @@ -4908,8 +4546,10 @@ def _module_native_array_bridge_operation_name( variable: ModuleVariablePlan, operation: NativeArrayOperation, ) -> str: - """Return the binding-local module native array bridge operation name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_{variable.symbol_name}_{operation.value}" + """Return one planner-owned module native-array entrypoint symbol.""" + return self._generated_support_procedure_entrypoint( + variable.owner_path, f"module:native_array:{operation.value}" + ).symbol_name def _module_native_array_cache_name(self, variable: ModuleVariablePlan) -> str: """Return the binding-local module native array cache name derived from the supplied completed binding records; this helper preserves completed policy.""" @@ -5693,10 +5333,10 @@ def _owned_native_array_bridge_operation_name( result: ArgumentTransferPlan | ResultPlan, operation: NativeArrayOperation, ) -> str: - """Return the C-visible typed bridge operation symbol.""" - preferred = result.bridge.native_name or "result" - owner = NativeSymbolNames.compact(result.owner_path, preferred, limit=38) - return f"bind_c_owned_{owner}_{operation.value}" + """Return one planner-owned descriptor operation symbol.""" + return self._generated_support_procedure_entrypoint( + result.owner_path, f"native_array:owned:{operation.value}" + ).symbol_name def _owned_native_array_operation_def_name( self, @@ -6209,7 +5849,7 @@ def _visit_FunctionPlan(self, plan: FunctionPlan) -> CFunction: context = self._function_context(plan) argument_declarations, argument_body = self._declarations_first(self._function_argument_nodes(plan, context)) alias_declarations, alias_body = self._declarations_first(self._derived_alias_preflight_nodes(plan, context)) - # Stage 2: assemble bridge call, completed output projection, and cleanup. + # Stage 2: assemble the entrypoint call, completed output projection, and cleanup. output_nodes = self._output_nodes(plan, context) return CFunction( name=self._binding_function_name(plan), @@ -6353,7 +5993,7 @@ def _callback_context_declarations( return tuple( CDeclaration( self._callback_context_name(argument), - argument.callback.context_type_symbol, + argument.callback.binding.context_type_symbol, ) for argument in plan.arguments if argument.callback is not None @@ -6382,7 +6022,8 @@ def _callback_context_push_nodes( ), CExpressionStatement( CodeExpression( - f"{self._callback_context_name(argument)}.previous = {argument.callback.context_current_symbol}" + f"{self._callback_context_name(argument)}.previous = " + f"{argument.callback.binding.context_current_symbol}" ) ), CExpressionStatement(CodeExpression(f"{self._callback_context_name(argument)}.last_result = NULL")), @@ -6392,7 +6033,7 @@ def _callback_context_push_nodes( CExpressionStatement(CodeExpression("Py_INCREF(self)")), CExpressionStatement( CodeExpression( - f"{argument.callback.context_current_symbol} = &{self._callback_context_name(argument)}" + f"{argument.callback.binding.context_current_symbol} = &{self._callback_context_name(argument)}" ) ), ) @@ -6410,7 +6051,8 @@ def _callback_context_pop_nodes( for node in ( CExpressionStatement( CodeExpression( - f"{argument.callback.context_current_symbol} = {self._callback_context_name(argument)}.previous" + f"{argument.callback.binding.context_current_symbol} = " + f"{self._callback_context_name(argument)}.previous" ) ), CExpressionStatement( @@ -6458,7 +6100,7 @@ def _lower_argument_required_descriptor( CodeExpression("0"), ), ) - if plan.bridge.descriptor_output_role is not None + if plan.entrypoint.descriptor_output_role is not None else () ), CIf( @@ -6573,7 +6215,7 @@ def _derived_argument_nodes( CodeExpression("0"), ), ) - if plan.bridge.descriptor_output_role is not None + if plan.entrypoint.descriptor_output_role is not None else () ), CIf(CodeExpression(f"{names.object_name} != Py_None"), body=extraction, else_body=none_body), @@ -8580,7 +8222,7 @@ def _result_extent_expression( expression: str, context: _CFunctionContext, ) -> str: - """Use the bridge result for native axes and local roles for all others.""" + """Use the entrypoint result for native axes and local roles for all others.""" if handoff.extent_evaluation[axis] == "bridge": return self._declaration_extent_result_name(result, axis) return self._array_extent_expression(handoff, axis, expression, context) @@ -8976,7 +8618,7 @@ def _result_native_name(self, plan: ResultPlan, context: _CFunctionContext) -> s raise ValueError(f"Direct result {plan.owner_path!r} has no C storage") return context.result_name try: - return context.native_outputs[plan.bridge.native_result_role] + return context.native_outputs[plan.entrypoint.native_result_role] except KeyError: raise ValueError(f"Hidden result {plan.owner_path!r} has no C output storage") from None @@ -8988,7 +8630,7 @@ def _output_nodes( """Return the native envelope, status projection, and Python result.""" nodes = [ *self._callback_context_push_nodes(plan, context), - *self._lower_native_call(plan, self._bridge_call_statement(plan, context)), + *self._lower_native_call(plan, self._entrypoint_call_statement(plan, context)), *self._callback_context_pop_nodes(plan), *self._derived_call_failure_nodes(plan, context), *self._derived_after_native_failure_nodes(plan, context), @@ -9383,16 +9025,16 @@ def _python_result_aggregation_nodes( CReturn(CodeExpression(aggregate)), ) - def _bridge_call_statement(self, plan: FunctionPlan, context: _CFunctionContext) -> CExpressionStatement: - """Return the mechanical bridge call selected by result storage.""" - call = self._bridge_call(plan, context) + def _entrypoint_call_statement(self, plan: FunctionPlan, context: _CFunctionContext) -> CExpressionStatement: + """Return the mechanical entrypoint call selected by result storage.""" + call = self._entrypoint_call(plan, context) direct_result = self._direct_result(plan) if direct_result is None or self._is_owned_native_array_result(direct_result): expression = call - elif direct_result.direct_result_abi is DirectResultABI.LOGICAL_LOW_BIT_INT8: + elif direct_result.entrypoint.direct_result_abi is DirectResultABI.LOGICAL_LOW_BIT_INT8: expression = f"{context.result_name} = (bool){call}" elif ( - direct_result.direct_result_abi is DirectResultABI.NATIVE_SCALAR + direct_result.entrypoint.direct_result_abi is DirectResultABI.NATIVE_SCALAR or direct_result.object_kind is not ObjectKind.SCALAR or direct_result.scalar_descriptor is not None ): @@ -9412,14 +9054,14 @@ def _lower_native_call( return self._lower_native_call_held(call) def _lower_native_call_held(self, call: CExpressionStatement) -> tuple[CExpressionStatement, ...]: - """Emit one native bridge call while retaining the GIL.""" + """Emit one native entrypoint call while retaining the GIL.""" return (call,) def _lower_native_call_released( self, call: CExpressionStatement, ) -> tuple[CAllowThreadsBegin | CExpressionStatement | CAllowThreadsEnd, ...]: - """Release the GIL only for the native bridge call.""" + """Release the GIL only for the native entrypoint call.""" return (CAllowThreadsBegin(), call, CAllowThreadsEnd()) def _lower_status_error( @@ -9573,7 +9215,7 @@ def _scalar_writeback_value_nodes( CodeExpression(f"{target} = {self._scalar_result_expression(scalar_type, f'&{names.value_name}')}") ) failure = CIf(CodeExpression(f"{target} == NULL"), body=(*cleanup, CReturn(CodeExpression("NULL")))) - if source.bridge.descriptor_output_presence_role is None: + if source.entrypoint.descriptor_output_presence_role is None: return (CDeclaration(target, "PyObject *", CodeExpression("NULL")), conversion, failure) return ( CDeclaration(target, "PyObject *", CodeExpression("NULL")), @@ -9664,7 +9306,7 @@ def _holder_allocation_status_name(names: _CArgumentNames) -> str: def _argument_for_role(self, plan: FunctionPlan, role: str) -> ArgumentTransferPlan: """Return the argument that produced one validated lifecycle role.""" for argument in plan.arguments: - if argument.binding.handoff_role == role: + if argument.entrypoint.handoff_role == role: return argument raise ValueError(f"{plan.owner_path!r} has no argument for lifecycle role {role!r}") @@ -9693,9 +9335,9 @@ def _argument_contexts(self, plan: FunctionPlan) -> dict[str, _CArgumentNames]: def _native_output_names(self, plan: FunctionPlan) -> dict[str, str]: """Name native hidden-output locals by their completed symbolic roles.""" return { - slot.symbolic_role: slot.native_name.lower() - for slot in plan.native_call_slots - if slot.source_kind == "result" + result.native_result_role: result.parameter_name + for result in plan.entrypoint.results + if result.source_kind == "hidden_output" and result.parameter_name is not None } def _output_owners(self, plan: FunctionPlan) -> tuple[tuple[str, int], ...]: @@ -9744,7 +9386,7 @@ def _argument_role_values( ) -> dict[str, str]: """Map completed handoff roles to their binding value locals.""" values = { - argument.binding.handoff_role: self._argument_role_value( + argument.entrypoint.handoff_role: self._argument_role_value( argument, arguments[argument.owner_path].value_name, ) @@ -9880,21 +9522,19 @@ def _native_output_declarations( plan: FunctionPlan, context: _CFunctionContext, ) -> tuple[CDeclaration, ...]: - """Declare bridge output storage from typed native result slots.""" + """Declare C storage for every hidden entrypoint result.""" declarations = [] - for slot in sorted(plan.native_call_slots, key=lambda item: item.native_position): - if slot.source_kind != "result": - continue - name = context.native_outputs[slot.symbolic_role] - if slot.scalar_descriptor is not None: + for result in self._entrypoint_hidden_results(plan): + name = context.native_outputs[result.native_result_role] + if result.scalar_descriptor is not None: declarations.append(CDeclaration(name, "void *", CodeExpression("NULL"))) declarations.append(CDeclaration(f"{name}_present", "int", CodeExpression("0"))) - if slot.scalar_descriptor.runtime_length: + if result.scalar_descriptor.runtime_length: declarations.append(CDeclaration(f"{name}_length", "int64_t", CodeExpression("0"))) continue - if self._is_owned_native_array_slot(slot): - if self._is_owned_deferred_character_slot(slot): - rank = slot.native_array_handle.array.rank + if self._is_owned_native_array_result(result): + if self._is_owned_deferred_character_result(result): + rank = result.native_array_handle.array.rank declarations.extend( ( CDeclaration(name, "void *", CodeExpression("NULL")), @@ -9919,12 +9559,10 @@ def _native_output_declarations( ) ) continue - if slot.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY, ObjectKind.DERIVED_TYPE}: + if result.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY, ObjectKind.DERIVED_TYPE}: declarations.append(CDeclaration(name, "void *", CodeExpression("NULL"))) continue - if slot.semantic_type_name is None: - raise ValueError(f"Missing native result datatype for {slot.owner_path!r}") - scalar_type = PrimitiveScalarTypeRegistry.type_for(slot.semantic_type_name) + scalar_type = PrimitiveScalarTypeRegistry.type_for(result.semantic_type_name) declarations.append(CDeclaration(name, scalar_type.c_spelling)) return tuple(declarations) @@ -10189,27 +9827,16 @@ def _decref_names(names: tuple[str, ...]) -> tuple[CExpressionStatement, ...]: return tuple(CExpressionStatement(CodeExpression(f"Py_DECREF({name})")) for name in names) @staticmethod - def _is_owned_native_array_result(result: ResultPlan) -> bool: + def _is_owned_native_array_result(result: ResultPlan | NativeEntrypointResultPlan) -> bool: """Return whether one result owns persistent standard-descriptor storage.""" handle = result.native_array_handle return handle is not None and handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE - @staticmethod - def _is_owned_native_array_slot(slot: NativeCallSlotPlan) -> bool: - """Return whether one hidden slot shares owned descriptor storage.""" - handle = slot.native_array_handle - return handle is not None and handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE - @classmethod - def _is_owned_deferred_character_result(cls, result: ResultPlan) -> bool: + def _is_owned_deferred_character_result(cls, result: ResultPlan | NativeEntrypointResultPlan) -> bool: """Return whether owner storage needs runtime character-width materialization.""" return cls._is_owned_native_array_result(result) and result.datatype_family is DatatypeFamily.STRING - @classmethod - def _is_owned_deferred_character_slot(cls, slot: NativeCallSlotPlan) -> bool: - """Return whether a hidden owner slot carries runtime-width characters.""" - return cls._is_owned_native_array_slot(slot) and slot.datatype_family is DatatypeFamily.STRING - def _owned_result_descriptor_name(self, result: ResultPlan, context: _CFunctionContext) -> str: """Return persistent owner storage after any deferred-character materialization.""" native_name = self._result_native_name(result, context) @@ -10217,81 +9844,178 @@ def _owned_result_descriptor_name(self, result: ResultPlan, context: _CFunctionC return f"{native_name}_owner_descriptor" return native_name - def _bridge_call(self, plan: FunctionPlan, context: _CFunctionContext) -> str: - """Return bridge call from the supplied completed binding records; this helper preserves the selected binding behavior.""" + def _entrypoint_call(self, plan: FunctionPlan, context: _CFunctionContext) -> str: + """Return the call assembled in planned entrypoint-parameter order.""" arguments = [ - *self._bridge_visible_argument_values(plan, context), - *self._bridge_hidden_result_values(plan, context), - *self._bridge_direct_result_values(plan, context), - *self._declaration_extent_result_values(plan), + value + for parameter in sorted(plan.entrypoint.parameters, key=lambda item: item.position) + for value in self._entrypoint_parameter_values(plan, parameter, context) ] - return f"{self._bridge_function_name(plan)}({', '.join(arguments)})" + return f"{self._entrypoint_function_name(plan)}({', '.join(arguments)})" - def _declaration_extent_result_values(self, plan: FunctionPlan) -> tuple[str, ...]: - """Pass native-dependent result extent output storage to the main bridge.""" - return tuple( - f"&{self._declaration_extent_result_name(result, axis)}" - for result in plan.results - if result.array is not None - for axis, evaluation in enumerate(result.array.extent_evaluation) - if evaluation == "bridge" - ) - - def _bridge_visible_argument_values( + def _entrypoint_parameter_values( self, plan: FunctionPlan, + parameter: NativeEntrypointParameterPlan, context: _CFunctionContext, ) -> tuple[str, ...]: - """Return ordered visible binding-to-bridge ABI values.""" - values = [] - for argument in sorted(plan.arguments, key=lambda item: item.bridge.abi_position): + """Lower one planned C-ABI parameter group to binding call actuals.""" + if parameter.source_kind == "argument": + argument = self._argument_by_owner(plan, parameter.owner_path) names = context.arguments[argument.owner_path] - values.extend(self._bridge_call_arguments(argument, names)) - if argument.bridge.optional_mode is OptionalMode.DESCRIPTOR: + slot = self._projected_slot_for_parameter(plan, parameter) + values = list( + self._entrypoint_argument_values( + argument, + names, + passing=slot.passing, + ) + ) + if argument.entrypoint.pass_descriptor_presence: values.append(names.present_name) - if argument.bridge.descriptor_output_role is not None: + if argument.entrypoint.descriptor_output_role is not None: values.extend((f"&{names.value_name}", f"&{self._descriptor_output_present_name(names)}")) - return tuple(values) + return tuple(values) + if parameter.source_kind == "projected_slot": + return self._projected_slot_values( + plan, + self._projected_slot_for_parameter(plan, parameter), + context, + ) + result = self._entrypoint_result_by_owner(plan, parameter.owner_path) + if parameter.source_kind == "hidden_result": + name = context.native_outputs[result.native_result_role] + return self._entrypoint_hidden_result_values(result, name) + if parameter.source_kind == "direct_result": + return self._entrypoint_direct_result_values(result, context) + if parameter.source_kind == "declaration_extent": + return self._declaration_extent_result_values_for_result(result) + raise ValueError(f"Unsupported entrypoint parameter group {parameter.source_kind!r}") + + @staticmethod + def _argument_by_owner(plan: FunctionPlan, owner_path: str) -> ArgumentTransferPlan: + """Return the argument referenced by one entrypoint parameter group.""" + return next(argument for argument in plan.arguments if argument.owner_path == owner_path) + + @staticmethod + def _entrypoint_result_by_owner(plan: FunctionPlan, owner_path: str) -> NativeEntrypointResultPlan: + """Return the C-ABI result referenced by one parameter group.""" + return next(result for result in plan.entrypoint.results if result.owner_path == owner_path) + + @staticmethod + def _projected_slot_for_parameter( + plan: FunctionPlan, + parameter: NativeEntrypointParameterPlan, + ) -> NativeEntrypointProjectedSlotPlan: + """Return the authoritative projected slot referenced by one C ABI group.""" + return next( + slot for slot in plan.entrypoint.projected_slots if slot.native_position == parameter.native_position + ) - def _bridge_hidden_result_values( + def _projected_slot_values( self, plan: FunctionPlan, + slot: NativeEntrypointProjectedSlotPlan, context: _CFunctionContext, ) -> tuple[str, ...]: - """Return ordered hidden output ABI pointers.""" - values = [] - for slot in sorted(plan.native_call_slots, key=lambda item: item.native_position): - if slot.source_kind != "result": - continue - name = context.native_outputs[slot.symbolic_role] - values.extend(self._bridge_hidden_result_slot_values(slot, name)) - return tuple(values) + """Materialize one non-argument projected actual in the C binding.""" + if slot.projection_action is EntrypointProjectionAction.TYPED_LITERAL: + return (self._projected_literal_expression(slot),) + if slot.python_position is None: + raise ValueError(f"Projected slot {slot.owner_path!r} has no binding source") + argument = next(item for item in plan.arguments if item.python_position == slot.python_position) + names = context.arguments[argument.owner_path] + if slot.projection_action is EntrypointProjectionAction.COMPUTED_LENGTH: + return (f"(size_t){names.length_name}",) + if slot.projection_action is EntrypointProjectionAction.COMPUTED_PRESENCE: + return (f"({names.nullable_name} != NULL)",) + if not isinstance(slot.literal_value, Mapping): + raise ValueError(f"Projected array fact {slot.owner_path!r} has no axis metadata") + axis = slot.literal_value.get("dim") + if not isinstance(axis, int): + raise ValueError(f"Projected array fact {slot.owner_path!r} has no integer axis") + if slot.projection_action is EntrypointProjectionAction.COMPUTED_SHAPE: + return (f"(size_t)PyArray_DIM((PyArrayObject *){names.object_name}, {axis})",) + if slot.projection_action is EntrypointProjectionAction.COMPUTED_STRIDE: + return (f"(size_t)PyArray_STRIDE((PyArrayObject *){names.object_name}, {axis})",) + raise ValueError(f"Unsupported projected C actual {slot.projection_action.value!r}") - def _bridge_hidden_result_slot_values(self, slot: NativeCallSlotPlan, name: str) -> tuple[str, ...]: + @staticmethod + def _projected_slot_parameters( + slot: NativeEntrypointProjectedSlotPlan, + ) -> tuple[CParameter, ...]: + """Declare one binding-materialized projected C ABI value.""" + if slot.semantic_type_name is None: + raise ValueError(f"Projected slot {slot.owner_path!r} has no semantic type") + scalar_type = PrimitiveScalarTypeRegistry.type_for(slot.semantic_type_name).c_spelling + if slot.passing in { + EntrypointPassingConvention.POINTER_REFERENCE, + EntrypointPassingConvention.NULLABLE_POINTER, + }: + scalar_type = f"{scalar_type} *" + elif slot.passing is not EntrypointPassingConvention.C_VALUE: + raise ValueError(f"Unsupported projected C parameter passing {slot.passing.value!r}") + return (CParameter(slot.native_name.casefold(), scalar_type),) + + @staticmethod + def _projected_literal_expression(slot: NativeEntrypointProjectedSlotPlan) -> str: + """Render one typed literal as a C call-site value.""" + value = slot.literal_value + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, complex): + return f"({value.real!r} + {value.imag!r} * I)" + if isinstance(value, (int, float)): + return repr(value) + raise ValueError(f"Unsupported projected C literal {value!r}") + + def _entrypoint_hidden_results(self, plan: FunctionPlan) -> tuple[NativeEntrypointResultPlan, ...]: + """Return hidden results in planned C-ABI parameter-group order.""" + return tuple( + self._entrypoint_result_by_owner(plan, parameter.owner_path) + for parameter in sorted(plan.entrypoint.parameters, key=lambda item: item.position) + if parameter.source_kind == "hidden_result" + ) + + def _declaration_extent_result_values_for_result( + self, + result: NativeEntrypointResultPlan, + ) -> tuple[str, ...]: + """Return extent output actuals for one planned result group.""" + if result.array is None: + return () + return tuple( + f"&{self._declaration_extent_result_name(result, axis)}" + for axis, evaluation in enumerate(result.array.extent_evaluation) + if evaluation == "bridge" + ) + + def _entrypoint_hidden_result_values( + self, + result: NativeEntrypointResultPlan, + name: str, + ) -> tuple[str, ...]: """Return ABI pointers for one hidden output slot.""" - if self._is_owned_deferred_character_slot(slot): - rank = slot.native_array_handle.array.rank + if self._is_owned_deferred_character_result(result): + rank = result.native_array_handle.array.rank return ( f"&{name}", f"&{name}_itemsize", *(f"&{name}_extent_{axis}" for axis in range(rank)), ) - values = [name if self._is_owned_native_array_slot(slot) else f"&{name}"] - if slot.scalar_descriptor is not None: + 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") - if slot.scalar_descriptor.runtime_length: + if result.scalar_descriptor.runtime_length: values.append(f"&{name}_length") return tuple(values) - def _bridge_direct_result_values( + def _entrypoint_direct_result_values( self, - plan: FunctionPlan, + result: NativeEntrypointResultPlan, context: _CFunctionContext, ) -> tuple[str, ...]: - """Return helper ABI fields for one direct result.""" - result = self._direct_result(plan) - if result is None: - return () + """Return helper ABI actuals for one planned direct-result group.""" if self._is_owned_native_array_result(result): if context.result_name is None: raise ValueError(f"Owned direct result {result.owner_path!r} has no C storage") @@ -10310,26 +10034,38 @@ def _bridge_direct_result_values( values.append(f"&{context.result_name}_length") return tuple(values) - def _bridge_call_arguments(self, plan: ArgumentTransferPlan, names: _CArgumentNames) -> tuple[str, ...]: - """Return one binding-to-bridge C handoff, including helper ABI fields.""" + def _entrypoint_argument_values( + self, + plan: ArgumentTransferPlan, + names: _CArgumentNames, + *, + passing: EntrypointPassingConvention, + ) -> tuple[str, ...]: + """Return one binding-to-entrypoint C handoff, including helper ABI fields.""" if plan.callback is not None: - return () - if plan.bridge.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER: - return self._string_bridge_call_arguments(names) - if plan.bridge.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: - return self._array_bridge_call_arguments(plan, names) - if plan.bridge.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: + if not plan.entrypoint.pass_callback_parameter: + return () + return (plan.callback.entrypoint.support_procedure.symbol_name,) + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER: + return self._string_entrypoint_argument_values(plan, names, passing=passing) + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: + return self._array_entrypoint_argument_values(plan, names) + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: return (names.value_name,) - return self._scalar_bridge_call_arguments(plan, names) + return self._scalar_entrypoint_argument_values(plan, names, passing=passing) - # Scalar bridge call arguments. - def _scalar_bridge_call_arguments( + # Scalar entrypoint call arguments. + def _scalar_entrypoint_argument_values( self, plan: ArgumentTransferPlan, names: _CArgumentNames, + *, + passing: EntrypointPassingConvention, ) -> tuple[str, ...]: """Return one scalar value, storage, address, or optional handoff.""" if plan.derived_call is not None: + if not plan.entrypoint.pass_derived_transaction: + return (names.value_name,) ops = self._derived_ops_name(names) return ( names.value_name, @@ -10341,21 +10077,42 @@ def _scalar_bridge_call_arguments( f"{ops} != NULL ? {ops}->restore : NULL", f"&{self._derived_status_name(names)}", ) - if plan.bridge.optional_mode is not OptionalMode.REQUIRED: + if plan.entrypoint.optional_mode is not OptionalMode.REQUIRED: return (names.nullable_name,) - if plan.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: + return (names.value_name,) + if passing is EntrypointPassingConvention.C_VALUE: + return (names.value_name,) + if passing is EntrypointPassingConvention.POINTER_REFERENCE: + return (f"&{names.value_name}",) + if passing is not plan.entrypoint.passing: + raise ValueError(f"Unsupported projected scalar passing convention {passing.value!r}") + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: return (names.value_name,) - if plan.bridge.handoff_mode is ArgumentHandoffMode.TYPED_REFERENCE: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.TYPED_REFERENCE: return (f"&{names.value_name}",) return (names.value_name,) - # String bridge call arguments. - def _string_bridge_call_arguments(self, names: _CArgumentNames) -> tuple[str, ...]: + # String entrypoint call arguments. + def _string_entrypoint_argument_values( + self, + plan: ArgumentTransferPlan, + names: _CArgumentNames, + *, + passing: EntrypointPassingConvention, + ) -> tuple[str, ...]: """Return one scalar string pointer-and-length handoff.""" - return names.value_name, f"(int64_t){names.length_name}" + values = [ + f"*{names.value_name}" + if not plan.entrypoint.pass_character_length and passing is EntrypointPassingConvention.C_VALUE + else names.value_name + ] + if plan.entrypoint.pass_character_length: + values.append(f"(int64_t){names.length_name}") + return tuple(values) - # Ordinary-array bridge call arguments. - def _array_bridge_call_arguments( + # Ordinary-array entrypoint call arguments. + def _array_entrypoint_argument_values( self, plan: ArgumentTransferPlan, names: _CArgumentNames, @@ -10365,6 +10122,8 @@ def _array_bridge_call_arguments( if handoff is None: raise ValueError(f"Array argument {plan.owner_path!r} has no handoff spec") arguments = [names.value_name] + if not plan.entrypoint.pass_array_metadata: + return tuple(arguments) if handoff.runtime_rank_role is not None: arguments.append(names.runtime_rank_name) if handoff.itemsize_role is not None: @@ -10380,110 +10139,81 @@ def _selected_array_axis_names(self, names: tuple[str, ...], roles: tuple[str, . """Return array ABI local names only when the plan carries their roles.""" return names if roles else () - def _bridge_prototype(self, plan: FunctionPlan) -> CFunctionPrototype: - """Return the binding-local bridge prototype derived from the supplied completed binding records; this helper preserves completed policy.""" - argument_parameters = tuple( - parameter - for argument in sorted(plan.arguments, key=lambda item: item.bridge.abi_position) - for parameter in self._bridge_argument_parameters(argument) - ) - result_parameters = tuple( + def _entrypoint_prototype(self, plan: FunctionPlan) -> CFunctionPrototype: + """Return the shared C prototype in planned parameter-group order.""" + parameters = tuple( parameter - for slot in sorted(plan.native_call_slots, key=lambda item: item.native_position) - for parameter in self._bridge_result_parameters(slot) + for group in sorted(plan.entrypoint.parameters, key=lambda item: item.position) + for parameter in self._entrypoint_parameter_declarations(plan, group) ) - direct_result = self._direct_result(plan) - direct_parameters = self._direct_bridge_result_parameters(direct_result) - declaration_extent_parameters = self._declaration_extent_result_parameters(plan) return CFunctionPrototype( - self._bridge_function_name(plan), - self._bridge_return_type(plan), - (*argument_parameters, *result_parameters, *direct_parameters, *declaration_extent_parameters), + self._entrypoint_function_name(plan), + self._entrypoint_return_type(plan), + parameters, ) - def _declaration_extent_result_parameters(self, plan: FunctionPlan) -> tuple[CParameter, ...]: - """Declare native-dependent result extent outputs in the C prototype.""" + def _entrypoint_parameter_declarations( + self, + plan: FunctionPlan, + parameter: NativeEntrypointParameterPlan, + ) -> tuple[CParameter, ...]: + """Lower one planned entrypoint parameter group into a C prototype.""" + if parameter.source_kind == "argument": + slot = self._projected_slot_for_parameter(plan, parameter) + return self._entrypoint_argument_parameters( + self._argument_by_owner(plan, parameter.owner_path), + passing=slot.passing, + ) + if parameter.source_kind == "projected_slot": + return self._projected_slot_parameters(self._projected_slot_for_parameter(plan, parameter)) + result = self._entrypoint_result_by_owner(plan, parameter.owner_path) + if parameter.source_kind == "hidden_result": + return self._entrypoint_result_parameters(result) + if parameter.source_kind == "direct_result": + return self._direct_entrypoint_result_parameters(result) + if parameter.source_kind == "declaration_extent": + return self._declaration_extent_result_parameters_for_result(result) + raise ValueError(f"Unsupported entrypoint parameter group {parameter.source_kind!r}") + + def _declaration_extent_result_parameters_for_result( + self, + result: NativeEntrypointResultPlan, + ) -> tuple[CParameter, ...]: + """Declare native-dependent extent outputs for one result group.""" + if result.array is None: + return () return tuple( CParameter(self._declaration_extent_result_name(result, axis), "int64_t *") - for result in plan.results - if result.array is not None for axis, evaluation in enumerate(result.array.extent_evaluation) if evaluation == "bridge" ) @staticmethod - def _declaration_extent_result_name(result: ResultPlan, axis: int) -> str: - """Return the shared main-bridge ABI name for one evaluated result axis.""" + def _declaration_extent_result_name(result: ResultPlan | NativeEntrypointResultPlan, axis: int) -> str: + """Return the shared entrypoint ABI name for one evaluated result axis.""" return f"prik_decl_extent_{result.result_position}_{axis}" def _owned_native_array_bridge_prototypes(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: """Declare typed Fortran operations over binding-owned result descriptors.""" return tuple( - prototype + self._generated_support_procedure_entrypoint_prototype(operation) for _function, result in self._owned_native_array_results(plan) if not self._is_owned_deferred_character_result(result) - for operation in result.native_array_handle.operations - if (prototype := self._owned_native_array_bridge_prototype(result, operation)) is not None + for operation in self._generated_support_procedure_entrypoints_for(result.owner_path, "native_array:owned:") ) def _default_native_array_bridge_prototypes(self, plan: ModulePlan) -> tuple[CFunctionPrototype, ...]: """Declare typed operations over lazily attached caller descriptors.""" return tuple( - prototype + self._generated_support_procedure_entrypoint_prototype(operation) for _function, argument in self._default_native_array_arguments(plan) - for operation in argument.native_array_handle.default_handle.operations - if (prototype := self._owned_native_array_bridge_prototype(argument, operation)) is not None - ) - - def _owned_native_array_bridge_prototype( - self, - result: ArgumentTransferPlan | ResultPlan, - operation: NativeArrayOperation, - ) -> CFunctionPrototype | None: - """Return one compiler-backed owned-result operation prototype.""" - if operation in { - NativeArrayOperation.ALLOCATED, - NativeArrayOperation.ASSOCIATED, - NativeArrayOperation.CONTIGUOUS, - }: - return CFunctionPrototype( - self._owned_native_array_bridge_operation_name(result, operation), - "bool", - (CParameter("result", "CFI_cdesc_t *"),), - ) - if operation is NativeArrayOperation.SHAPE: - handle = result.native_array_handle - return CFunctionPrototype( - self._owned_native_array_bridge_operation_name(result, operation), - "void", - ( - CParameter("result", "CFI_cdesc_t *"), - *(CParameter(f"extent_{axis}", "int64_t *") for axis in range(handle.array.rank)), - ), + for operation in self._generated_support_procedure_entrypoints_for( + argument.owner_path, "native_array:owned:" ) - if operation is NativeArrayOperation.ASSOCIATE: - return CFunctionPrototype( - self._owned_native_array_bridge_operation_name(result, operation), - "void", - ( - CParameter("result", "CFI_cdesc_t *"), - CParameter("source", "CFI_cdesc_t *"), - ), - ) - if operation in { - NativeArrayOperation.DEALLOCATE, - NativeArrayOperation.NULLIFY, - NativeArrayOperation.DESTROY, - }: - return CFunctionPrototype( - self._owned_native_array_bridge_operation_name(result, operation), - "void", - (CParameter("result", "CFI_cdesc_t *"),), - ) - return None + ) - def _bridge_return_type(self, plan: FunctionPlan) -> str: - """Return the direct bridge result type, or void for subroutines.""" + def _entrypoint_return_type(self, plan: FunctionPlan) -> str: + """Return the direct entrypoint result type, or void for subroutines.""" result = self._direct_result(plan) if result is None: return "void" @@ -10493,16 +10223,14 @@ def _bridge_return_type(self, plan: FunctionPlan) -> str: return "void *" if result.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY, ObjectKind.DERIVED_TYPE}: return "void *" - if result.direct_result_abi is DirectResultABI.LOGICAL_LOW_BIT_INT8: + if result.entrypoint.direct_result_abi is DirectResultABI.LOGICAL_LOW_BIT_INT8: return "int8_t" - if result.direct_result_abi is DirectResultABI.NATIVE_SCALAR: + if result.entrypoint.direct_result_abi is DirectResultABI.NATIVE_SCALAR: return PrimitiveScalarTypeRegistry.type_for(result.semantic_type_name).c_spelling raise ValueError(f"Scalar result {result.owner_path!r} has no completed direct-result ABI") - def _direct_bridge_result_parameters(self, result: ResultPlan | None) -> tuple[CParameter, ...]: + def _direct_entrypoint_result_parameters(self, result: NativeEntrypointResultPlan) -> tuple[CParameter, ...]: """Return helper ABI parameters associated with one direct result.""" - if result is None: - return () if self._is_owned_native_array_result(result): if self._is_owned_deferred_character_result(result): rank = result.native_array_handle.array.rank @@ -10523,21 +10251,37 @@ def _direct_result(self, plan: FunctionPlan) -> ResultPlan | None: """Return the sole direct native function result, when present.""" return next((result for result in plan.results if result.source_kind == "direct_return"), None) - def _bridge_argument_parameters(self, argument: ArgumentTransferPlan) -> tuple[CParameter, ...]: - """Return the bridge ABI parameters for one Python argument.""" - name = argument.bridge.native_name.lower() + def _entrypoint_argument_parameters( + self, + argument: ArgumentTransferPlan, + *, + passing: EntrypointPassingConvention, + ) -> tuple[CParameter, ...]: + """Return the entrypoint ABI parameters for one Python argument.""" + name = argument.entrypoint.parameter_name if argument.callback is not None: - return () + if not argument.entrypoint.pass_callback_parameter: + return () + signature = argument.callback.entrypoint.support_procedure.signature + return ( + CParameter( + name, + self._support_procedure_c_type(signature.result), + tuple(self._support_procedure_c_type(item) for item in signature.parameters), + ), + ) if argument.derived_call is not None: - return self._derived_bridge_argument_parameters(argument, name) - return self._ordinary_bridge_argument_parameters(argument, name) + return self._derived_entrypoint_argument_parameters(argument, name) + return self._ordinary_entrypoint_argument_parameters(argument, name, passing=passing) @staticmethod - def _derived_bridge_argument_parameters( + def _derived_entrypoint_argument_parameters( argument: ArgumentTransferPlan, name: str, ) -> tuple[CParameter, ...]: """Declare the shared scalar-derived origin transaction ABI.""" + if not argument.entrypoint.pass_derived_transaction: + return (CParameter(name, "void *"),) descriptor_output = ( CParameter(f"{name}_output", "void **"), CParameter(f"{name}_output_present", "int *"), @@ -10551,28 +10295,32 @@ def _derived_bridge_argument_parameters( CParameter(f"{name}_checkout", "prik_derived_checkout_fn"), CParameter(f"{name}_restore", "prik_derived_restore_fn"), CParameter(f"{name}_status", "int *"), - *(descriptor_output if argument.bridge.descriptor_output_role is not None else ()), + *(descriptor_output if argument.entrypoint.descriptor_output_role is not None else ()), ) - def _ordinary_bridge_argument_parameters( + def _ordinary_entrypoint_argument_parameters( self, argument: ArgumentTransferPlan, name: str, + *, + passing: EntrypointPassingConvention, ) -> tuple[CParameter, ...]: - """Dispatch non-callback, non-derived bridge parameters by handoff mode.""" - if argument.bridge.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER: - return self._string_bridge_argument_parameters(argument, name) - if argument.bridge.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: - return self._array_bridge_argument_parameters(argument, name) - if argument.bridge.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: + """Dispatch ordinary entrypoint parameters by completed handoff mode.""" + if argument.entrypoint.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER: + return self._string_entrypoint_argument_parameters(argument, name, passing=passing) + if argument.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: + return self._array_entrypoint_argument_parameters(argument, name) + if argument.entrypoint.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: parameters = [CParameter(name, "CFI_cdesc_t *")] - if argument.bridge.optional_mode is OptionalMode.DESCRIPTOR: + if argument.entrypoint.pass_descriptor_presence: parameters.append(CParameter(f"{name}_present", "void *")) return tuple(parameters) - scalar_type = self._scalar_bridge_argument_type(argument) - if argument.bridge.optional_mode is OptionalMode.DESCRIPTOR: + if argument.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: + return (CParameter(name, "void *"),) + scalar_type = self._scalar_entrypoint_argument_type(argument, passing=passing) + if argument.entrypoint.pass_descriptor_presence: return (CParameter(name, scalar_type), CParameter(f"{name}_present", "void *")) - if argument.bridge.descriptor_output_role is not None: + if argument.entrypoint.descriptor_output_role is not None: return ( CParameter(name, scalar_type), CParameter(f"{name}_output", "void *"), @@ -10580,258 +10328,116 @@ def _ordinary_bridge_argument_parameters( ) return (CParameter(name, scalar_type),) - # Scalar bridge ABI parameters. - def _scalar_bridge_argument_type(self, argument: ArgumentTransferPlan) -> str: - """Return the C ABI type for one scalar bridge input.""" - if argument.bridge.optional_mode is not OptionalMode.REQUIRED: - return "void *" - if argument.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: - return "void *" + # Scalar entrypoint ABI parameters. + def _scalar_entrypoint_argument_type( + self, + argument: ArgumentTransferPlan, + *, + passing: EntrypointPassingConvention, + ) -> str: + """Return the C ABI type for one scalar entrypoint input.""" scalar_type = PrimitiveScalarTypeRegistry.type_for(argument.semantic_type_name).c_spelling - if argument.bridge.handoff_mode is ArgumentHandoffMode.TYPED_REFERENCE: + if passing is EntrypointPassingConvention.C_VALUE: + return scalar_type + if passing in { + EntrypointPassingConvention.POINTER_REFERENCE, + EntrypointPassingConvention.NULLABLE_POINTER, + }: return f"{scalar_type} *" - return scalar_type + if argument.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: + return "void *" + raise ValueError(f"Unsupported projected scalar passing convention {passing.value!r}") - # String bridge ABI parameters. - def _string_bridge_argument_parameters( + # String entrypoint ABI parameters. + def _string_entrypoint_argument_parameters( self, argument: ArgumentTransferPlan, name: str, + *, + passing: EntrypointPassingConvention, ) -> tuple[CParameter, ...]: """Return one scalar string pointer-and-length ABI pair.""" - pointer_type = "char *" if argument.bridge.codegen_action is CodegenAction.COPY_IN_OUT else "const char *" - return CParameter(name, pointer_type), CParameter(f"{name}_length", "int64_t") + if not argument.entrypoint.pass_character_length and passing is EntrypointPassingConvention.C_VALUE: + return (CParameter(name, "char"),) + pointer_type = "char *" if argument.binding.codegen_action is CodegenAction.COPY_IN_OUT else "const char *" + parameters = [CParameter(name, pointer_type)] + if argument.entrypoint.pass_character_length: + parameters.append(CParameter(f"{name}_length", "int64_t")) + return tuple(parameters) - # Ordinary-array bridge ABI parameters. - def _array_bridge_argument_parameters( + # Ordinary-array entrypoint ABI parameters. + def _array_entrypoint_argument_parameters( self, argument: ArgumentTransferPlan, name: str, ) -> tuple[CParameter, ...]: - """Return the completed ordinary-array bridge ABI parameters.""" + """Return the completed ordinary-array entrypoint ABI parameters.""" handoff = argument.array if handoff is None: raise ValueError(f"Array argument {argument.owner_path!r} has no handoff spec") - parameters = [CParameter(name, "void *")] + pointer_type = "void *" + if not argument.entrypoint.pass_array_metadata: + scalar_type = ( + "char" + if argument.datatype_family is DatatypeFamily.STRING + else PrimitiveScalarTypeRegistry.type_for(argument.semantic_type_name).c_spelling + ) + pointer_type = f"{scalar_type} *" + parameters = [CParameter(name, pointer_type)] + if not argument.entrypoint.pass_array_metadata: + return tuple(parameters) if handoff.runtime_rank_role is not None: parameters.append(CParameter(f"{name}_rank", "int64_t")) if handoff.itemsize_role is not None: parameters.append(CParameter(f"{name}_itemsize", "int64_t")) if handoff.dense_actual_role is not None: parameters.append(CParameter(f"{name}_dense_actual", "int")) - parameters.extend(self._array_bridge_axis_parameters(name, "extent", len(handoff.extent_roles))) - parameters.extend(self._array_bridge_axis_parameters(name, "upper_bound", len(handoff.upper_bound_roles))) - parameters.extend(self._array_bridge_axis_parameters(name, "stride", len(handoff.stride_roles))) + parameters.extend(self._array_entrypoint_axis_parameters(name, "extent", len(handoff.extent_roles))) + parameters.extend(self._array_entrypoint_axis_parameters(name, "upper_bound", len(handoff.upper_bound_roles))) + parameters.extend(self._array_entrypoint_axis_parameters(name, "stride", len(handoff.stride_roles))) return tuple(parameters) - def _array_bridge_axis_parameters(self, name: str, label: str, count: int) -> tuple[CParameter, ...]: - """Return one named int64 bridge parameter per ordinary-array axis.""" + def _array_entrypoint_axis_parameters(self, name: str, label: str, count: int) -> tuple[CParameter, ...]: + """Return one named int64 entrypoint parameter per ordinary-array axis.""" return tuple(CParameter(f"{name}_{label}_{axis}", "int64_t") for axis in range(count)) - def _bridge_result_parameters(self, slot: NativeCallSlotPlan) -> tuple[CParameter, ...]: + def _entrypoint_result_parameters(self, result: NativeEntrypointResultPlan) -> tuple[CParameter, ...]: """Return the C ABI parameter for one native result slot.""" - if slot.source_kind != "result": + if result.source_kind != "hidden_output": return () - if slot.scalar_descriptor is not None: + name = result.parameter_name + if name is None: + raise ValueError(f"Hidden result {result.owner_path!r} has no entrypoint parameter name") + if result.scalar_descriptor is not None: parameters = [ - CParameter(slot.native_name.lower(), "void **"), - CParameter(f"{slot.native_name.lower()}_present", "int *"), + CParameter(name, "void **"), + CParameter(f"{name}_present", "int *"), ] - if slot.scalar_descriptor.runtime_length: - parameters.append(CParameter(f"{slot.native_name.lower()}_length", "int64_t *")) + if result.scalar_descriptor.runtime_length: + parameters.append(CParameter(f"{name}_length", "int64_t *")) return tuple(parameters) - if self._is_owned_native_array_slot(slot): - if self._is_owned_deferred_character_slot(slot): - rank = slot.native_array_handle.array.rank - name = slot.native_name.lower() + if self._is_owned_native_array_result(result): + if self._is_owned_deferred_character_result(result): + rank = result.native_array_handle.array.rank return ( CParameter(name, "void **"), CParameter(f"{name}_itemsize", "int64_t *"), *(CParameter(f"{name}_extent_{axis}", "int64_t *") for axis in range(rank)), ) - return (CParameter(slot.native_name.lower(), "CFI_cdesc_t *"),) - if slot.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY, ObjectKind.DERIVED_TYPE}: - return (CParameter(slot.native_name.lower(), "void **"),) - if slot.semantic_type_name is None: - raise ValueError(f"Missing bridge result datatype for {slot.owner_path!r}") - scalar_type = PrimitiveScalarTypeRegistry.type_for(slot.semantic_type_name).c_spelling - return (CParameter(slot.native_name.lower(), f"{scalar_type} *"),) + return (CParameter(name, "CFI_cdesc_t *"),) + 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 + return (CParameter(name, f"{scalar_type} *"),) def _module_variable_bridge_prototypes( self, plan: ModuleVariablePlan, ) -> tuple[CFunctionPrototype, ...]: """Return getter/setter ABI declarations selected by the variable plan.""" - handler = { - ModuleGetterAction.NATIVE_ARRAY_HANDLE: self._module_native_array_bridge_prototypes, - ModuleGetterAction.BORROWED_ARRAY_VIEW: self._module_borrowed_array_bridge_prototypes, - ModuleGetterAction.NATIVE_CONSTANT_ARRAY_VALUE: self._module_borrowed_array_bridge_prototypes, - ModuleGetterAction.DERIVED_OBJECT: self._module_derived_bridge_prototypes, - }.get(plan.binding.getter_action) - if handler is not None: - return handler(plan) - return self._module_scalar_bridge_prototypes(plan) - - def _module_borrowed_array_bridge_prototypes( - self, - plan: ModuleVariablePlan, - ) -> tuple[CFunctionPrototype, ...]: - """Declare one borrowed array getter with explicit extent outputs.""" - if plan.array is None or plan.array.rank is None: - return () - return ( - CFunctionPrototype( - self._module_bridge_getter_name(plan), - "void *", - tuple(CParameter(f"extent_{axis}", "int64_t *") for axis in range(plan.array.rank)), - ), - ) - - def _module_derived_bridge_prototypes( - self, - plan: ModuleVariablePlan, - ) -> tuple[CFunctionPrototype, ...]: - """Declare the selected direct or member-proxy derived getter ABI.""" - if plan.derived is None: - return () - if plan.derived.access is not ModuleObjectAccessMechanism.MEMBER_PROXY: - return (CFunctionPrototype(self._module_bridge_getter_name(plan), "void *"),) - if self._nullable_derived_module_proxy(plan): - return (CFunctionPrototype(self._module_derived_presence_bridge_name(plan), "bool"),) - return () - - def _module_scalar_bridge_prototypes( - self, - plan: ModuleVariablePlan, - ) -> tuple[CFunctionPrototype, ...]: - """Declare ordinary scalar getter and setter bridge functions.""" - prototypes = [] - if plan.bridge.getter_role is not None: - return_type = ( - "void *" - if plan.binding.getter_action is ModuleGetterAction.NULLABLE_SNAPSHOT - else PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name).c_spelling - ) - prototypes.append(CFunctionPrototype(self._module_bridge_getter_name(plan), return_type)) - if plan.bridge.setter_role is not None: - scalar_type = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name).c_spelling - prototypes.append( - CFunctionPrototype( - self._module_bridge_setter_name(plan), - "void", - (CParameter("value", scalar_type),), - ) - ) - return tuple(prototypes) - - def _module_native_array_bridge_prototypes( - self, - plan: ModuleVariablePlan, - ) -> tuple[CFunctionPrototype, ...]: - """Declare only bridge operations named by one borrowed handle plan.""" - handle = plan.native_array_handle - if handle is None or handle.array.rank is None: - raise ValueError(f"Module handle {plan.owner_path!r} has no operation plan") - prototypes = [] - for operation in handle.operations: - prototype = self._module_native_array_bridge_prototype(plan, operation) - if prototype is not None: - prototypes.append(prototype) - return tuple(prototypes) - - def _module_native_array_bridge_prototype( - self, - plan: ModuleVariablePlan, - operation: NativeArrayOperation, - ) -> CFunctionPrototype | None: - """Return the native bridge prototype selected by one operation.""" - if operation in { - NativeArrayOperation.NATIVE_BYTE_ORDER, - NativeArrayOperation.ALIGNED, - NativeArrayOperation.WRITEABLE, - NativeArrayOperation.LAYOUT, - }: - return None - if operation is NativeArrayOperation.TO_NUMPY: - return None - return self._module_native_array_required_bridge_prototype(plan, operation) - - def _module_native_array_required_bridge_prototype( - self, - plan: ModuleVariablePlan, - operation: NativeArrayOperation, - ) -> CFunctionPrototype: - """Declare one operation that must cross the native bridge.""" - name = self._module_native_array_bridge_operation_name(plan, operation) - if operation in { - NativeArrayOperation.ALLOCATED, - NativeArrayOperation.ASSOCIATED, - NativeArrayOperation.CONTIGUOUS, - }: - return CFunctionPrototype(name, "bool") - if operation is NativeArrayOperation.ELEMENT_LENGTH: - return CFunctionPrototype(name, "int64_t") - if operation is NativeArrayOperation.ARRAY_ACTUAL: - if self._uses_module_allocatable_descriptor(plan): - return self._module_allocatable_descriptor_bridge_prototype(name) - return CFunctionPrototype(name, "void *") - if operation is NativeArrayOperation.SHAPE: - return self._module_native_array_shape_prototype(plan, name, pointer=True) - if operation is NativeArrayOperation.DESCRIPTOR: - return self._module_native_array_descriptor_prototype(plan, name) - if operation is NativeArrayOperation.ASSOCIATE: - return CFunctionPrototype(name, "void", (CParameter("source", "CFI_cdesc_t *"),)) - if operation in {NativeArrayOperation.ALLOCATE, NativeArrayOperation.RESIZE}: - return self._module_native_array_shape_prototype(plan, name, pointer=False) - if operation in {NativeArrayOperation.DEALLOCATE, NativeArrayOperation.NULLIFY}: - return CFunctionPrototype(name, "void") - raise ValueError(f"Unsupported module native array operation for {plan.owner_path!r}: {operation!r}") - - def _module_native_array_shape_prototype( - self, - plan: ModuleVariablePlan, - name: str, - *, - pointer: bool, - ) -> CFunctionPrototype: - """Return one rank-specific shape query or mutation prototype.""" - handle = plan.native_array_handle - if handle is None or handle.array.rank is None: - raise ValueError(f"Module handle {plan.owner_path!r} has no rank") - suffix = " *" if pointer else "" - return CFunctionPrototype( - name, - "void", - tuple(CParameter(f"extent_{axis}", f"int64_t{suffix}") for axis in range(handle.array.rank)), - ) - - def _module_native_array_descriptor_prototype( - self, - plan: ModuleVariablePlan, - name: str, - ) -> CFunctionPrototype | None: - """Return a standard-descriptor reader prototype selected by policy.""" - handle = plan.native_array_handle - if self._uses_module_allocatable_descriptor(plan): - return self._module_allocatable_descriptor_bridge_prototype(name) - if handle is None or handle.descriptor_kind is not NativeArrayDescriptorKind.POINTER: - return None - return CFunctionPrototype(name, "void", (CParameter("descriptor", "CFI_cdesc_t *"),)) - - @staticmethod - def _module_allocatable_descriptor_bridge_prototype(name: str) -> CFunctionPrototype: - """Declare one callback-based standard-descriptor module operation.""" - return CFunctionPrototype( - name, - "void", - ( - CParameter( - "callback", - "void", - function_parameters=("CFI_cdesc_t *", "void *"), - ), - CParameter("context", "void *"), - ), + return tuple( + self._generated_support_procedure_entrypoint_prototype(operation) + for operation in self._generated_support_procedure_entrypoints_for(plan.owner_path, "module:") ) def _module_variable_helper_prototypes( @@ -10911,10 +10517,11 @@ def _derived_capsule_destructor_name(type_name: str) -> str: """Return one binding-owned capsule cleanup symbol.""" return f"prik_destroy_{type_name.casefold()}_capsule" - @staticmethod - def _derived_destroy_bridge_name(type_name: str) -> str: - """Return the native-aware bridge destroy symbol.""" - return f"bind_c_prik_destroy_{type_name.casefold()}" + def _derived_destroy_bridge_name(self, type_name: str) -> str: + """Return the planner-owned native-aware destroy symbol.""" + return self._generated_support_procedure_entrypoint( + self._derived_owner_paths[type_name], "derived:destroy" + ).symbol_name @staticmethod def _allocatable_holder_capsule_name(type_name: str) -> str: @@ -10931,30 +10538,34 @@ def _pointer_holder_capsule_destructor_name(type_name: str) -> str: """Return the binding-local pointer holder capsule destructor name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"prik_destroy_{type_name.casefold()}_pointer_holder_capsule" - @staticmethod - def _pointer_holder_destroy_bridge_name(type_name: str) -> str: - """Return the binding-local pointer holder destroy bridge name derived from the supplied local lowering values; this helper preserves completed policy.""" - return f"bind_c_prik_destroy_{type_name.casefold()}_pointer_holder" + def _pointer_holder_destroy_bridge_name(self, type_name: str) -> str: + """Return the planner-owned pointer-holder destroy symbol.""" + return self._generated_support_procedure_entrypoint( + self._derived_owner_paths[type_name], "holder:pointer:destroy" + ).symbol_name - @staticmethod - def _pointer_holder_presence_bridge_name(type_name: str) -> str: - """Return the binding-local pointer holder presence bridge name derived from the supplied local lowering values; this helper preserves completed policy.""" - return f"bind_c_prik_{type_name.casefold()}_pointer_holder_present" + def _pointer_holder_presence_bridge_name(self, type_name: str) -> str: + """Return the planner-owned pointer-holder presence symbol.""" + return self._generated_support_procedure_entrypoint( + self._derived_owner_paths[type_name], "holder:pointer:present" + ).symbol_name @staticmethod def _allocatable_holder_capsule_destructor_name(type_name: str) -> str: """Return the binding-local allocatable holder capsule destructor name derived from the supplied local lowering values; this helper preserves completed policy.""" return f"prik_destroy_{type_name.casefold()}_allocatable_holder_capsule" - @staticmethod - def _allocatable_holder_destroy_bridge_name(type_name: str) -> str: - """Return the binding-local allocatable holder destroy bridge name derived from the supplied local lowering values; this helper preserves completed policy.""" - return f"bind_c_prik_destroy_{type_name.casefold()}_allocatable_holder" + def _allocatable_holder_destroy_bridge_name(self, type_name: str) -> str: + """Return the planner-owned allocatable-holder destroy symbol.""" + return self._generated_support_procedure_entrypoint( + self._derived_owner_paths[type_name], "holder:allocatable:destroy" + ).symbol_name - @staticmethod - def _allocatable_holder_presence_bridge_name(type_name: str) -> str: - """Return the binding-local allocatable holder presence bridge name derived from the supplied local lowering values; this helper preserves completed policy.""" - return f"bind_c_prik_{type_name.casefold()}_allocatable_holder_present" + def _allocatable_holder_presence_bridge_name(self, type_name: str) -> str: + """Return the planner-owned allocatable-holder presence symbol.""" + return self._generated_support_procedure_entrypoint( + self._derived_owner_paths[type_name], "holder:allocatable:present" + ).symbol_name @staticmethod def _allocatable_holder_presence_method_name(type_name: str) -> str: @@ -10976,8 +10587,10 @@ def _derived_field_method_name(self, derived: DerivedTypePlan, field: DerivedFie return CBindingNames.derived_field_method(derived, field, action) def _derived_field_bridge_name(self, derived: DerivedTypePlan, field: DerivedFieldPlan, action: str) -> str: - """Return the binding-local derived field bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_prik_field_{self._derived_field_symbol(derived, field)}_{action}" + """Return the planner-owned direct-field entrypoint symbol.""" + return self._generated_support_procedure_entrypoint( + f"{derived.owner_path}.{field.name}", f"field:direct:{action}" + ).symbol_name def _allocatable_holder_field_bridge_name( self, @@ -10985,8 +10598,10 @@ def _allocatable_holder_field_bridge_name( field: DerivedFieldPlan, action: str, ) -> str: - """Return the binding-local allocatable holder field bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_prik_allocatable_holder_field_{self._derived_field_symbol(derived, field)}_{action}" + """Return the planner-owned allocatable-holder field symbol.""" + return self._generated_support_procedure_entrypoint( + f"{derived.owner_path}.{field.name}", f"field:allocatable:{action}" + ).symbol_name def _allocatable_holder_field_method_name( self, @@ -11003,8 +10618,10 @@ def _pointer_holder_field_bridge_name( field: DerivedFieldPlan, action: str, ) -> str: - """Return the binding-local pointer holder field bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_prik_pointer_holder_field_{self._derived_field_symbol(derived, field)}_{action}" + """Return the planner-owned pointer-holder field symbol.""" + return self._generated_support_procedure_entrypoint( + f"{derived.owner_path}.{field.name}", f"field:pointer:{action}" + ).symbol_name def _pointer_holder_field_method_name( self, @@ -11048,8 +10665,11 @@ def _derived_handle_bridge_name( field: DerivedFieldPlan, operation: NativeArrayOperation, ) -> str: - """Return the binding-local derived handle bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_prik_field_handle_{self._derived_field_symbol(derived, field)}_{operation.value}" + """Return the planner-owned direct-field handle symbol.""" + return self._generated_support_procedure_entrypoint( + f"{derived.owner_path}.{field.name}", + f"field:direct:handle:{operation.value}", + ).symbol_name def _derived_handle_descriptor_callback_name( self, @@ -11087,8 +10707,10 @@ def _module_member_bridge_name( member: DerivedMemberPathPlan, action: str, ) -> str: - """Return the binding-local module member bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_prik_module_field_{self._module_member_symbol(variable, member)}_{action}" + """Return the planner-owned module-member entrypoint symbol.""" + return self._generated_support_procedure_entrypoint( + ".".join((variable.owner_path, *member.path)), f"field:module:{action}" + ).symbol_name def _module_member_descriptor_callback_name( self, @@ -11113,8 +10735,11 @@ def _module_member_handle_bridge_name( member: DerivedMemberPathPlan, operation: NativeArrayOperation, ) -> str: - """Return the binding-local module member handle bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_prik_module_field_handle_{self._module_member_symbol(variable, member)}_{operation.value}" + """Return the planner-owned module-member handle symbol.""" + return self._generated_support_procedure_entrypoint( + ".".join((variable.owner_path, *member.path)), + f"field:module:handle:{operation.value}", + ).symbol_name def _module_member_handle_descriptor_callback_name( self, @@ -11657,102 +11282,42 @@ def _module_member_method_names(self, namespace: NamespacePlan) -> tuple[str, .. for action in self._field_method_actions(member.field) ) - def _namespace_allocatable_holder_identities(self, namespace: NamespacePlan) -> frozenset[tuple[str, str]]: - """Return the binding-local namespace allocatable holder identities derived from the supplied completed binding records; this helper preserves completed policy.""" - identities = self._namespace_allocatable_holder_result_identities(namespace) - identities.update(self._namespace_allocatable_holder_argument_identities(namespace)) - return frozenset(identities) - @staticmethod - def _namespace_allocatable_holder_result_identities(namespace: NamespacePlan) -> set[tuple[str, str]]: - """Return the binding-local namespace allocatable holder result identities derived from the supplied completed binding records; this helper preserves completed policy.""" - return { - result.derived.type_identity - for function in namespace.functions - for result in function.results - if result.derived is not None and result.derived.storage is DerivedObjectStorage.ALLOCATABLE_HOLDER - } - - def _namespace_allocatable_holder_argument_identities( - self, + def _namespace_binding_holder_types( namespace: NamespacePlan, - ) -> set[tuple[str, str]]: - """Return the binding-local namespace allocatable holder argument identities derived from the supplied completed binding records; this helper preserves completed policy.""" - return { - argument.derived.type_identity - for function in namespace.functions - for argument in function.arguments - if argument.derived is not None - and argument.derived_call is not None - and argument.bridge.descriptor_output_role is not None - and self._uses_allocatable_holder(argument) - } + owner_paths: frozenset[str], + ) -> tuple[DerivedTypePlan, ...]: + """Join one namespace to its planner-owned binding holder inventory.""" + return tuple(derived for derived in namespace.derived_types if derived.owner_path in owner_paths) def _allocatable_holder_method_names(self, namespace: NamespacePlan) -> tuple[str, ...]: - """Return the binding-local allocatable holder method names derived from the supplied completed binding records; this helper preserves completed policy.""" - holder_identities = self._namespace_allocatable_holder_identities(namespace) + """Return methods for the namespace's planned allocatable holders.""" + holders = self._namespace_binding_holder_types( + namespace, + self._binding_allocatable_holder_owner_paths, + ) fields = tuple( self._allocatable_holder_field_method_name(derived, field, action) - for derived in namespace.derived_types - if derived.type_identity in holder_identities + for derived in holders for field in derived.fields for action in self._field_method_actions(field) ) - guards = tuple( - self._allocatable_holder_presence_method_name(derived.backend_symbol) - for derived in namespace.derived_types - if derived.type_identity in holder_identities - ) + guards = tuple(self._allocatable_holder_presence_method_name(derived.backend_symbol) for derived in holders) return (*fields, *guards) - def _namespace_pointer_holder_identities(self, namespace: NamespacePlan) -> frozenset[tuple[str, str]]: - """Return the binding-local namespace pointer holder identities derived from the supplied completed binding records; this helper preserves completed policy.""" - identities = self._namespace_pointer_holder_result_identities(namespace) - identities.update(self._namespace_pointer_holder_argument_identities(namespace)) - return frozenset(identities) - - @staticmethod - def _namespace_pointer_holder_result_identities(namespace: NamespacePlan) -> set[tuple[str, str]]: - """Return the binding-local namespace pointer holder result identities derived from the supplied completed binding records; this helper preserves completed policy.""" - return { - result.derived.type_identity - for function in namespace.functions - for result in function.results - if result.derived is not None and result.derived.storage is DerivedObjectStorage.POINTER_HOLDER - } - - @staticmethod - def _namespace_pointer_holder_argument_identities(namespace: NamespacePlan) -> set[tuple[str, str]]: - """Return the binding-local namespace pointer holder argument identities derived from the supplied completed binding records; this helper preserves completed policy.""" - return { - argument.derived.type_identity - for function in namespace.functions - for argument in function.arguments - if argument.derived is not None - and argument.derived_call is not None - and argument.bridge.descriptor_output_role is not None - and any( - case.access is DerivedActualAccess.POINTER_HOLDER - for case in argument.derived_call.cases - if case.action is not DerivedCallAction.INCOMPATIBLE - ) - } - def _pointer_holder_method_names(self, namespace: NamespacePlan) -> tuple[str, ...]: - """Return the binding-local pointer holder method names derived from the supplied completed binding records; this helper preserves completed policy.""" - holder_identities = self._namespace_pointer_holder_identities(namespace) + """Return methods for the namespace's planned pointer holders.""" + holders = self._namespace_binding_holder_types( + namespace, + self._binding_pointer_holder_owner_paths, + ) fields = tuple( self._pointer_holder_field_method_name(derived, field, action) - for derived in namespace.derived_types - if derived.type_identity in holder_identities + for derived in holders for field in derived.fields for action in self._field_method_actions(field) ) - guards = tuple( - self._pointer_holder_presence_method_name(derived.backend_symbol) - for derived in namespace.derived_types - if derived.type_identity in holder_identities - ) + guards = tuple(self._pointer_holder_presence_method_name(derived.backend_symbol) for derived in holders) return (*fields, *guards) def _module_proxy_guard_method_names(self, namespace: NamespacePlan) -> tuple[str, ...]: @@ -11913,9 +11478,17 @@ def _namespace_python_initializer_nodes( has_proxy = any(variable.derived is not None for variable in namespace.variables) if not namespace.derived_types and not has_proxy: return () + allocatable_holders = self._namespace_binding_holder_types( + namespace, + self._binding_allocatable_holder_owner_paths, + ) + pointer_holders = self._namespace_binding_holder_types( + namespace, + self._binding_pointer_holder_owner_paths, + ) context = PythonSurfaceContext( - allocatable_holder_identities=self._namespace_allocatable_holder_identities(namespace), - pointer_holder_identities=self._namespace_pointer_holder_identities(namespace), + allocatable_holder_identities=frozenset(derived.type_identity for derived in allocatable_holders), + pointer_holder_identities=frozenset(derived.type_identity for derived in pointer_holders), nullable_module_proxy_owner_paths=frozenset( variable.owner_path for variable in namespace.variables if self._nullable_derived_module_proxy(variable) ), @@ -12157,9 +11730,9 @@ def _binding_function_name(self, plan: FunctionPlan) -> str: """Return the binding-local binding function name derived from the supplied completed binding records; this helper preserves completed policy.""" return f"wrap_{plan.symbol_name}" - def _bridge_function_name(self, plan: FunctionPlan) -> str: - """Return the binding-local bridge function name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_{plan.symbol_name}" + def _entrypoint_function_name(self, plan: FunctionPlan) -> str: + """Return the shared C-ABI function symbol selected by planning.""" + return 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.""" @@ -12170,12 +11743,12 @@ def _module_setter_name(self, plan: ModuleVariablePlan) -> str: return f"module_set_{plan.symbol_name}" def _module_bridge_getter_name(self, plan: ModuleVariablePlan) -> str: - """Return the binding-local module bridge getter name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_get_{plan.symbol_name}" + """Return the shared module-variable getter entrypoint symbol.""" + return self._generated_support_procedure_entrypoint(plan.owner_path, "module:get").symbol_name def _module_bridge_setter_name(self, plan: ModuleVariablePlan) -> str: - """Return the binding-local module bridge setter name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_set_{plan.symbol_name}" + """Return the shared module-variable setter entrypoint symbol.""" + return self._generated_support_procedure_entrypoint(plan.owner_path, "module:set").symbol_name @staticmethod def _nullable_derived_module_proxy(plan: ModuleVariablePlan) -> bool: @@ -12191,10 +11764,9 @@ def _nullable_derived_module_proxy(plan: ModuleVariablePlan) -> bool: } ) - @staticmethod - def _module_derived_presence_bridge_name(plan: ModuleVariablePlan) -> str: - """Return the binding-local module derived presence bridge name derived from the supplied completed binding records; this helper preserves completed policy.""" - return f"bind_c_prik_module_{plan.symbol_name.casefold()}_present" + def _module_derived_presence_bridge_name(self, plan: ModuleVariablePlan) -> str: + """Return the planner-owned nullable module-derived presence symbol.""" + return self._generated_support_procedure_entrypoint(plan.owner_path, "module:derived:present").symbol_name @staticmethod def _module_derived_presence_method_name(plan: ModuleVariablePlan) -> str: diff --git a/prik/codegen/c/naming.py b/prik/codegen/c/naming.py index 26cfec65c..da249ae76 100644 --- a/prik/codegen/c/naming.py +++ b/prik/codegen/c/naming.py @@ -107,11 +107,6 @@ def class_create_method(surface: ClassSurfacePlan) -> str: """Return the private C constructor callable installed in the namespace.""" return f"_prik_create_{surface.type_identity[1].casefold()}" - @staticmethod - def class_create_bridge(surface: ClassSurfacePlan) -> str: - """Return the Fortran bridge symbol allocating one class owner.""" - return f"bind_c_prik_create_{surface.type_identity[1].casefold()}" - @staticmethod def class_wrap_helper( surface: ClassSurfacePlan | None, diff --git a/prik/codegen/docstrings.py b/prik/codegen/docstrings.py index fd60d6427..827545739 100644 --- a/prik/codegen/docstrings.py +++ b/prik/codegen/docstrings.py @@ -12,6 +12,7 @@ from prik.policy.ownership import OwnershipOwner, SetterAction, TransferMode from prik.policy.models import ( ClassConstructorKind, + EntrypointOptionalityAction, ModuleGetterAction, NativeArrayDescriptorKind, OptionalMode, @@ -768,6 +769,11 @@ def _optional_lines(argument: ArgumentTransferPlan) -> tuple[str, ...]: """ mode = argument.binding.optional_mode if mode is OptionalMode.DESCRIPTOR: + if argument.entrypoint.optionality is EntrypointOptionalityAction.NULL_C_DESCRIPTOR_POINTER: + return ( + " May be omitted or passed as None to make the native optional dummy absent.", + " Pass an empty descriptor handle for a present unallocated or unassociated dummy.", + ) return ( " Omit to make the native optional dummy absent.", " Pass None for a present unallocated or unassociated descriptor.", @@ -979,8 +985,8 @@ def _result_name(result: ResultPlan, arguments: tuple[ArgumentTransferPlan, ...] ) if projected is not None: return projected - if result.native_call_slot is not None and result.native_call_slot.python_name: - return result.native_call_slot.python_name + if result.projected_call_slot is not None and result.projected_call_slot.python_name: + return result.projected_call_slot.python_name return "result" if result.result_position == 0 else f"result_{result.result_position}" def _module_variable_summary_lines(self, variable: ModuleVariablePlan) -> tuple[str, ...]: diff --git a/prik/codegen/fortran/bridge.py b/prik/codegen/fortran/bridge.py index b442a86e4..d4626ee25 100644 --- a/prik/codegen/fortran/bridge.py +++ b/prik/codegen/fortran/bridge.py @@ -9,6 +9,7 @@ from __future__ import annotations +from dataclasses import replace import re from prik.utilities.declaration_expressions import render_declaration_extent @@ -17,7 +18,6 @@ CodegenAction, NativeBarrierAction, ObjectKind, - PythonBarrierAction, SetterAction, ) from prik.semantics.metadata import SCALAR_STORAGE_CATEGORY @@ -29,7 +29,6 @@ CallbackABIKind, CallbackResultAction, CallbackTransferAction, - ClassConstructorKind, ClassInvocationKind, DerivedActualAccess, DerivedCallAction, @@ -37,7 +36,6 @@ DerivedNativeHandoff, DerivedDummyCategory, DerivedObjectStorage, - DerivedRelease, DeclarationCallableAction, DirectResultABI, ExternalDeclarationMode, @@ -50,6 +48,8 @@ NativeArrayResultAllocation, NativeDescriptorHandoffABI, NativeInvocationKind, + EntrypointPassingConvention, + EntrypointProjectionAction, OptionalMode, ScalarLogicalABI, ) @@ -74,7 +74,6 @@ FortranTypeDefinition, FortranUse, ) -from prik.naming.native_symbols import NativeSymbolNames from prik.planning.models import ( ArrayHandoffPlan, ArgumentTransferPlan, @@ -91,7 +90,13 @@ ModuleVariablePlan, NamespacePlan, NativeArrayHandlePlan, - NativeCallSlotPlan, + NativeEntrypointABIValueKind, + NativeEntrypointABIValuePlan, + GeneratedSupportProcedureImplementationOwner, + GeneratedSupportProcedureEntrypointPlan, + NativeEntrypointParameterPlan, + NativeEntrypointProjectedSlotPlan, + NativeEntrypointResultPlan, ProcedurePrototypeArgumentPlan, ProcedurePrototypePlan, ProcedurePrototypeResultPlan, @@ -124,7 +129,8 @@ def require_supported(self, plan: ModulePlan) -> None: for derived in self._derived_types(plan): self._require_derived_type_supported(derived) for function in self._functions(plan): - self._require_function_supported(function) + if function.bridge is not None: + self._require_function_supported(function) for variable in self._variables(plan): self._require_variable_supported(variable) @@ -134,7 +140,7 @@ def _require_function_supported(self, function: FunctionPlan) -> None: self._require_argument_supported(argument) for result in function.results: self._require_backend_type_supported(result.semantic_type_name, result.datatype_family) - for slot in function.native_call_slots: + for slot in self._adapter_slots(function): if slot.source_kind == "result": self._require_backend_type_supported(slot.semantic_type_name, slot.datatype_family) @@ -171,13 +177,77 @@ def _require_backend_type_supported( return PrimitiveScalarTypeRegistry.type_for(semantic_type_name) + @staticmethod + def _adapter_slots(function: FunctionPlan) -> tuple[NativeEntrypointProjectedSlotPlan, ...]: + """Return ordered projected slots that carry adapter-local actions.""" + return tuple( + slot + for slot in sorted(function.entrypoint.projected_slots, key=lambda item: item.native_position) + if slot.adapter is not None + ) + def _visit_ModulePlan(self, plan: ModulePlan) -> FortranModule: """Build one complete bridge module from one validated module plan.""" + self._generated_support_procedure_entrypoints = { + (procedure.owner_path, procedure.role): procedure for procedure in plan.entrypoint.support_procedures + } + self._derived_owner_paths = { + derived.backend_symbol: derived.owner_path for derived in self._derived_types(plan) + } + 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) + self._bridge_pointer_holder_owner_paths = frozenset(plan.bridge.pointer_holder_type_owner_paths) + self._bridge_allocatable_holder_field_owner_paths = frozenset( + plan.bridge.allocatable_holder_field_type_owner_paths + ) + self._bridge_pointer_holder_field_owner_paths = frozenset(plan.bridge.pointer_holder_field_type_owner_paths) # Scoped origins are module-wide facts needed by derived-call lowering. scoped_origin_type_identities = self._scoped_origin_type_identities(plan) + procedures = ( + *( + procedure + for namespace in plan.namespaces + for procedure in self.visit(namespace, scoped_origin_type_identities) + ), + # Typed derived-field access remains separate from class orchestration. + *self._derived_field_procedures(plan), + # Native-aware opaque-owner destruction is Phase 8 substrate, not class orchestration. + *self._class_constructor_procedures(plan), + *( + self._derived_destroy_procedure(derived) + for derived in self._derived_types(plan) + if self._has_generated_support_procedure_entrypoint(derived.owner_path, "derived:destroy") + ), + *( + self._allocatable_holder_destroy_procedure(derived) + for derived in self._derived_types(plan) + if self._has_generated_support_procedure_entrypoint(derived.owner_path, "holder:allocatable:destroy") + ), + *( + self._allocatable_holder_presence_procedure(derived) + for derived in self._derived_types(plan) + if self._has_generated_support_procedure_entrypoint(derived.owner_path, "holder:allocatable:present") + ), + *( + self._pointer_holder_destroy_procedure(derived) + for derived in self._derived_types(plan) + if self._has_generated_support_procedure_entrypoint(derived.owner_path, "holder:pointer:destroy") + ), + *( + self._pointer_holder_presence_procedure(derived) + for derived in self._derived_types(plan) + if self._has_generated_support_procedure_entrypoint(derived.owner_path, "holder:pointer:present") + ), + *( + procedure + for variable in self._derived_origin_variables(plan) + for procedure in self._derived_origin_procedures(variable) + ), + ) # Assemble imports, declarations, and procedures from plan projections. return FortranModule( - name=f"bind_c_{plan.bridge.owner_path}_wrapper", + name=f"bind_c_{plan.entrypoint.owner_path}_wrapper", uses=( FortranUse("iso_c_binding", self._iso_c_symbols(plan)), *self._native_module_uses(plan), @@ -192,36 +262,136 @@ def _visit_ModulePlan(self, plan: ModulePlan) -> FortranModule: *self._allocator_interfaces(plan), ), declarations=self._prototype_entity_declarations(plan), - procedures=( - *( - procedure - for namespace in plan.namespaces - for procedure in self.visit(namespace, scoped_origin_type_identities) - ), - # Typed derived-field access remains separate from class orchestration. - *self._derived_field_procedures(plan), - # Native-aware opaque-owner destruction is Phase 8 substrate, not class orchestration. - *self._class_constructor_procedures(plan), - *(self._derived_destroy_procedure(derived) for derived in self._owned_derived_types(plan)), - *( - self._allocatable_holder_destroy_procedure(derived) - for derived in self._allocatable_holder_types(plan) - ), - *( - self._allocatable_holder_presence_procedure(derived) - for derived in self._allocatable_holder_types(plan) - ), - *(self._pointer_holder_destroy_procedure(derived) for derived in self._pointer_holder_types(plan)), - *(self._pointer_holder_presence_procedure(derived) for derived in self._pointer_holder_types(plan)), - *( - procedure - for variable in self._derived_origin_variables(plan) - for procedure in self._derived_origin_procedures(variable) - ), - ), + procedures=self._apply_generated_support_procedure_entrypoints(procedures), standalone_procedures=self._callback_standalone_adapter_procedures(plan), ) + def _generated_support_procedure_entrypoint( + self, owner_path: str, role: str + ) -> GeneratedSupportProcedureEntrypointPlan: + """Return one required planner-owned operation without a naming fallback.""" + try: + return self._generated_support_procedure_entrypoints[(owner_path, role)] + except (AttributeError, KeyError): + raise ValueError(f"Missing generated support procedure entrypoint {owner_path!r} role {role!r}") from None + + def _generated_support_procedure_entrypoints_for( + self, + owner_path: str, + role_prefix: str, + ) -> tuple[GeneratedSupportProcedureEntrypointPlan, ...]: + """Return planner-ordered operations for one owner and role family.""" + return tuple( + operation + for operation in self._generated_support_procedure_entrypoints.values() + if operation.owner_path == owner_path and operation.role.startswith(role_prefix) + ) + + def _has_generated_support_procedure_entrypoint(self, owner_path: str, role: str) -> bool: + """Return whether planning registered one generated support procedure.""" + return (owner_path, role) in self._generated_support_procedure_entrypoints + + def _generated_support_procedure_entrypoint_function( + self, + operation: GeneratedSupportProcedureEntrypointPlan, + function: FortranFunction, + ) -> FortranFunction: + """Apply one planner-owned bridge ABI to an adapter-local procedure body.""" + if operation.implementation_owner is not GeneratedSupportProcedureImplementationOwner.FORTRAN: + raise ValueError(f"Fortran cannot implement binding-owned operation {operation.key!r}") + result_type = self._support_procedure_fortran_type(operation.signature.result) + is_subroutine = operation.signature.result.kind is NativeEntrypointABIValueKind.VOID + return replace( + function, + name=operation.symbol_name, + parameters=tuple( + self._support_procedure_fortran_parameter(parameter) for parameter in operation.signature.parameters + ), + result_name=None if is_subroutine else function.result_name, + result_type=None if is_subroutine else result_type, + bind_name=operation.symbol_name, + is_subroutine=is_subroutine, + ) + + def _apply_generated_support_procedure_entrypoints( + self, + procedures: tuple[FortranFunction, ...], + ) -> tuple[FortranFunction, ...]: + """Join every adapter-local body to exactly one planner-owned C ABI.""" + operations = { + operation.symbol_name: operation + for operation in self._generated_support_procedure_entrypoints.values() + if operation.implementation_owner is GeneratedSupportProcedureImplementationOwner.FORTRAN + } + generated = {procedure.name for procedure in procedures if procedure.name in operations} + missing = tuple(symbol for symbol in operations if symbol not in generated) + if missing: + raise ValueError(f"Fortran lowering did not implement planned support procedures: {missing!r}") + return tuple( + self._generated_support_procedure_entrypoint_function(operations[procedure.name], procedure) + if procedure.name in operations + else procedure + for procedure in procedures + ) + + def _support_procedure_fortran_parameter( + self, + value: NativeEntrypointABIValuePlan, + ) -> FortranParameter: + """Lower one generated-support C-ABI value as a Fortran dummy.""" + type_name = self._support_procedure_fortran_type(value) + if value.kind is NativeEntrypointABIValueKind.DESCRIPTOR: + if value.descriptor_kind is None or value.rank is None: + raise ValueError(f"Generated-support descriptor {value.role!r} is incomplete") + attributes = ( + value.descriptor_kind.value, + self._array_dimension_attribute(value.rank), + *((f"intent({value.intent})",) if value.intent is not None else ()), + ) + elif value.kind is NativeEntrypointABIValueKind.CHARACTER: + if value.character_length is None: + raise ValueError(f"Generated-support character value {value.role!r} has no length") + attributes = ( + f"dimension({value.character_length})", + *((f"intent({value.intent})",) if value.intent is not None else ()), + ) + elif value.intent is not None: + attributes = (f"intent({value.intent})",) + elif value.pointer_depth == 0 or value.kind in { + NativeEntrypointABIValueKind.OPAQUE, + NativeEntrypointABIValueKind.CALLBACK, + }: + attributes = ("value",) + else: + attributes = () + return FortranParameter(value.fortran_name, type_name, attributes) + + @staticmethod + def _support_procedure_fortran_type(value: NativeEntrypointABIValuePlan) -> str | None: + """Spell one structured generated-support ABI value for Fortran.""" + if value.kind is NativeEntrypointABIValueKind.SEMANTIC_SCALAR: + if value.semantic_type_name is None: + raise ValueError(f"Generated-support ABI value {value.role!r} has no semantic scalar type") + return PrimitiveScalarTypeRegistry.type_for(value.semantic_type_name).fortran_spelling + types = { + NativeEntrypointABIValueKind.VOID: None, + NativeEntrypointABIValueKind.BOOL: "logical(c_bool)", + NativeEntrypointABIValueKind.INT: "integer(c_int)", + NativeEntrypointABIValueKind.INT8: "integer(c_int8_t)", + NativeEntrypointABIValueKind.INT64: "integer(c_int64_t)", + NativeEntrypointABIValueKind.OPAQUE: "type(c_ptr)", + NativeEntrypointABIValueKind.CHARACTER: "character(kind=c_char)", + NativeEntrypointABIValueKind.CALLBACK: "type(c_funptr)", + } + 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") + return PrimitiveScalarTypeRegistry.type_for(value.semantic_type_name).fortran_spelling + try: + return types[value.kind] + except KeyError: + raise ValueError(f"Unsupported generated-support Fortran ABI kind {value.kind.value!r}") from None + def _callback_standalone_adapter_procedures(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: """Return separately linked callback adapters in stable site order.""" return tuple( @@ -241,7 +411,7 @@ def _derived_holder_definitions(self, plan: ModulePlan) -> tuple[FortranTypeDefi ), ), ) - for derived in self._allocatable_holder_types(plan) + for derived in self._bridge_support_types(plan, self._bridge_allocatable_holder_owner_paths) ) pointers = tuple( FortranTypeDefinition( @@ -254,7 +424,7 @@ def _derived_holder_definitions(self, plan: ModulePlan) -> tuple[FortranTypeDefi ), ), ) - for derived in self._pointer_holder_types(plan) + for derived in self._bridge_support_types(plan, self._bridge_pointer_holder_owner_paths) ) return (*allocatable, *pointers) @@ -268,12 +438,17 @@ def _visit_NamespacePlan( *( procedure for function in plan.functions + if function.bridge is not None for procedure in ( self.visit(function, scoped_origin_type_identities), *self._owned_native_array_result_operations(function), - *self._default_native_array_argument_operations(function), ) ), + *( + procedure + for function in plan.functions + for procedure in self._default_native_array_argument_operations(function) + ), *(procedure for variable in plan.variables for procedure in self.visit(variable)), ) @@ -287,22 +462,15 @@ def _visit_FunctionPlan( All declarations and nodes come from completed function-plan actions; this orchestration only preserves their required execution order. """ - # Stage 1: determine the bridge ABI and result representation. + # Stage 1: determine the entrypoint ABI and result representation. result_name, result_type = self._lower_result(plan) owned_direct_result = self._owned_direct_result(plan) parameters = tuple( parameter - for argument in sorted(plan.arguments, key=lambda item: item.bridge.abi_position) - for parameter in self.visit(argument) + for group in sorted(plan.entrypoint.parameters, key=lambda item: item.position) + for parameter in self._entrypoint_parameter_declarations(plan, group) ) - parameters = ( - *parameters, - *self._native_output_parameters(plan), - *self._owned_direct_result_parameters(owned_direct_result), - *self._scalar_descriptor_direct_result_parameters(plan), - *self._declaration_extent_result_parameters(plan), - ) - bridge_name = self._bridge_function_name(plan) + entrypoint_name = self._entrypoint_function_name(plan) is_subroutine = plan.bridge.native_is_subroutine or owned_direct_result is not None # Stage 2: assemble the native invocation and its ordered finalizers. function_body, optional_procedures = self._function_body(plan, result_name) @@ -327,11 +495,11 @@ def _visit_FunctionPlan( scoped_origin_type_identities, ) return FortranFunction( - name=bridge_name, + name=entrypoint_name, parameters=parameters, result_name=result_name, result_type=result_type, - bind_name=bridge_name, + bind_name=entrypoint_name, declarations=( *self._callback_external_declarations(plan), *self._native_external_declarations(plan), @@ -369,16 +537,80 @@ def _visit_FunctionPlan( ), ) - def _declaration_extent_result_parameters(self, plan: FunctionPlan) -> tuple[FortranParameter, ...]: - """Expose bridge-evaluated result extents through the main bridge ABI.""" + def _entrypoint_parameter_declarations( + self, + plan: FunctionPlan, + parameter: NativeEntrypointParameterPlan, + ) -> tuple[FortranParameter, ...]: + """Lower one shared C-ABI parameter group into a bind(C) declaration.""" + if parameter.source_kind == "argument": + return self.visit(self._argument_by_owner(plan, parameter.owner_path)) + if parameter.source_kind == "projected_slot": + return self._projected_slot_parameters(self._projected_slot_for_parameter(plan, parameter)) + result = self._result_by_owner(plan, parameter.owner_path) + if parameter.source_kind == "hidden_result": + return self._native_output_parameters_for_result(result) + if parameter.source_kind == "direct_result": + return ( + *self._owned_direct_result_parameters(result), + *self._scalar_descriptor_direct_result_parameters_for_result(result), + ) + if parameter.source_kind == "declaration_extent": + return self._declaration_extent_result_parameters_for_result(result) + raise ValueError(f"Unsupported entrypoint parameter group {parameter.source_kind!r}") + + @staticmethod + def _argument_by_owner(plan: FunctionPlan, owner_path: str) -> ArgumentTransferPlan: + """Return the argument referenced by one entrypoint parameter group.""" + return next(argument for argument in plan.arguments if argument.owner_path == owner_path) + + @staticmethod + def _result_by_owner(plan: FunctionPlan, owner_path: str) -> NativeEntrypointResultPlan: + """Return the C-ABI result referenced by one entrypoint parameter group.""" + return next(result for result in plan.entrypoint.results if result.owner_path == owner_path) + + @staticmethod + def _projected_slot_for_parameter( + plan: FunctionPlan, + parameter: NativeEntrypointParameterPlan, + ) -> NativeEntrypointProjectedSlotPlan: + """Return the shared projected slot referenced by one adapter parameter.""" + return next( + slot for slot in plan.entrypoint.projected_slots if slot.native_position == parameter.native_position + ) + + @staticmethod + def _projected_slot_parameters( + slot: NativeEntrypointProjectedSlotPlan, + ) -> tuple[FortranParameter, ...]: + """Declare one binding-materialized projection at the shared C ABI.""" + if slot.semantic_type_name is None: + raise ValueError(f"Projected slot {slot.owner_path!r} has no semantic type") + type_name = PrimitiveScalarTypeRegistry.type_for(slot.semantic_type_name).fortran_spelling + if slot.passing is EntrypointPassingConvention.C_VALUE: + attributes = ("value",) + elif slot.passing in { + EntrypointPassingConvention.POINTER_REFERENCE, + EntrypointPassingConvention.NULLABLE_POINTER, + }: + attributes = () + else: + raise ValueError(f"Unsupported projected Fortran parameter passing {slot.passing.value!r}") + return (FortranParameter(slot.native_name.casefold(), type_name, attributes),) + + def _declaration_extent_result_parameters_for_result( + self, + result: NativeEntrypointResultPlan, + ) -> tuple[FortranParameter, ...]: + """Expose bridge-evaluated extents for one entrypoint result group.""" + if result.array is None: + return () return tuple( FortranParameter( self._declaration_extent_result_name(result, axis), "integer(c_int64_t)", ("intent(out)",), ) - for result in plan.results - if result.array is not None for axis, evaluation in enumerate(result.array.extent_evaluation) if evaluation == "bridge" ) @@ -401,8 +633,8 @@ def _declaration_extent_result_assignments(self, plan: FunctionPlan) -> tuple[Fo return tuple(assignments) @staticmethod - def _declaration_extent_result_name(result: ResultPlan, axis: int) -> str: - """Return the shared main-bridge ABI name for one evaluated result axis.""" + def _declaration_extent_result_name(result: ResultPlan | NativeEntrypointResultPlan, axis: int) -> str: + """Return the shared entrypoint ABI name for one evaluated result axis.""" return f"prik_decl_extent_{result.result_position}_{axis}" # Immediate callback adapters. @@ -414,9 +646,9 @@ def _callback_standalone_adapter_procedure( """Adapt one native callback through a separately declared external procedure.""" result = callback.result.transfer is_subroutine = callback.result.action is CallbackResultAction.RETURN_VOID - trampoline_name = f"{callback.trampoline_symbol}_call" + trampoline_name = f"{callback.entrypoint.support_procedure.symbol_name}_call" return FortranFunction( - name=callback.adapter_symbol, + name=callback.bridge.adapter_symbol, parameters=tuple(self._callback_native_parameter(transfer) for transfer in callback.arguments), result_name=None if is_subroutine else "callback_result", result_type=None if is_subroutine else self._callback_native_result_type(result), @@ -428,7 +660,7 @@ def _callback_standalone_adapter_procedure( self._callback_c_interface( callback, name=trampoline_name, - bind_name=callback.trampoline_symbol, + bind_name=callback.entrypoint.support_procedure.symbol_name, ), ) ), @@ -506,7 +738,7 @@ def _callback_standalone_adapter_uses( *( ( FortranUse( - f"bind_c_{plan.bridge.owner_path}_wrapper", + f"bind_c_{plan.entrypoint.owner_path}_wrapper", derived_imports, ), ) @@ -519,7 +751,7 @@ def _callback_external_declarations(self, plan: FunctionPlan) -> tuple[FortranDe """Declare every external callback adapter from its shared prototype.""" return tuple( FortranDeclaration( - callback.adapter_symbol, + callback.bridge.adapter_symbol, f"procedure({callback.prototype.interface_symbol})", ) for callback in ( @@ -756,7 +988,7 @@ def _derived_call_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclara return () declarations = [FortranDeclaration("prik_derived_ready", "logical")] for argument in arguments: - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name native_type = f"type({self._derived_native_alias(argument.derived.backend_symbol)})" declarations.extend( ( @@ -874,7 +1106,7 @@ def _derived_call_invocation( def _derived_argument_initializers(self, argument: ArgumentTransferPlan) -> tuple: """Initialize bridge-local state for one derived carrier before its completed action is dispatched.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name nodes = [ FortranAssignment(f"bound_{name}_status", CodeExpression("0_c_int")), FortranAssignment(f"{name}_created", CodeExpression(".false.")), @@ -883,7 +1115,7 @@ def _derived_argument_initializers(self, argument: ArgumentTransferPlan) -> tupl FortranNullify(name), FortranNullify(f"{name}_call_pointer"), ] - if argument.bridge.descriptor_output_role is not None: + if argument.entrypoint.descriptor_output_role is not None: nodes.extend( ( FortranAssignment(f"bound_{name}_output", CodeExpression("c_null_ptr")), @@ -923,7 +1155,7 @@ def _derived_argument_preparation( ) ) return FortranSelectCase( - CodeExpression(f"bound_{argument.bridge.native_name.lower()}_access"), + CodeExpression(f"bound_{argument.entrypoint.parameter_name}_access"), tuple(cases), ) @@ -931,7 +1163,7 @@ def _derived_direct_preparation(self, argument: ArgumentTransferPlan) -> tuple: """Build preparation for the completed direct scalar-derived handoff case.""" if argument.polymorphic is not None: return self._polymorphic_direct_preparation(argument) - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return ( FortranIf( CodeExpression(f"c_associated(bound_{name})"), @@ -942,7 +1174,7 @@ def _derived_direct_preparation(self, argument: ArgumentTransferPlan) -> tuple: def _polymorphic_direct_preparation(self, argument: ArgumentTransferPlan) -> tuple: """Associate one carrier with the concrete type selected by the binding.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name cases = tuple( FortranCase( variant.abi_code, @@ -977,7 +1209,7 @@ def _polymorphic_direct_preparation(self, argument: ArgumentTransferPlan) -> tup def _derived_scoped_preparation(self, argument: ArgumentTransferPlan) -> tuple: """Build preparation for the completed scoped-address scalar-derived handoff case.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return ( FortranIf( CodeExpression(f"c_associated(bound_{name}_scoped)"), @@ -993,7 +1225,7 @@ def _derived_scoped_preparation(self, argument: ArgumentTransferPlan) -> tuple: def _derived_allocatable_holder_preparation(self, argument: ArgumentTransferPlan) -> tuple: """Build preparation for a completed allocatable-holder scalar-derived handoff.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name holder = f"{name}_allocatable_holder" return ( FortranAssignment(f"{name}_holder_status", CodeExpression("0_c_int")), @@ -1019,7 +1251,7 @@ def _derived_allocatable_payload_preparation(self, argument: ArgumentTransferPla DerivedDummyCategory.ALLOCATABLE_TARGET, }: return () - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return ( FortranIf( CodeExpression(f"allocated({name}_allocatable_holder%value)"), @@ -1030,7 +1262,7 @@ def _derived_allocatable_payload_preparation(self, argument: ArgumentTransferPla def _derived_pointer_holder_preparation(self, argument: ArgumentTransferPlan) -> tuple: """Build preparation for a completed pointer-holder scalar-derived handoff.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name holder = f"{name}_pointer_holder" return ( FortranAssignment(f"{name}_holder_status", CodeExpression("0_c_int")), @@ -1054,7 +1286,7 @@ def _derived_pointer_payload_preparation(self, argument: ArgumentTransferPlan) - """Build preparation for a pointer-holder payload after its carrier has been acquired.""" if argument.derived_call.dummy_category is DerivedDummyCategory.POINTER: return () - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return ( FortranIf( CodeExpression(f"associated({name}_pointer_holder%value)"), @@ -1073,7 +1305,7 @@ def _derived_pointer_transaction_preparation(self, argument: ArgumentTransferPla def _derived_transaction_operation_preparation(self, argument: ArgumentTransferPlan) -> tuple: """Dispatch one derived transaction operation only by its completed action.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return ( FortranIf( CodeExpression(f"c_associated(bound_{name}_checkout) .and. c_associated(bound_{name}_restore)"), @@ -1098,7 +1330,7 @@ def _derived_transaction_acquisition( ) -> FortranIf: """Build acquisition nodes for one derived argument at the current native-call position.""" argument = arguments[index] - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name acquisition = FortranSelectCase( CodeExpression(f"bound_{name}_access"), ( @@ -1131,7 +1363,7 @@ def _one_derived_transaction_acquisition( allocatable: bool, ) -> tuple: """Build the guarded acquisition for one derived argument and preserve earlier failure state.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name holder = f"{name}_{'allocatable' if allocatable else 'pointer'}_holder" return ( FortranAssignment( @@ -1152,7 +1384,7 @@ def _one_derived_transaction_acquisition( def _derived_transaction_restoration(self, argument: ArgumentTransferPlan) -> FortranIf: """Build the restoration selected for one derived carrier after the native call.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return FortranIf( CodeExpression(f"{name}_acquired"), body=( @@ -1176,7 +1408,7 @@ def _derived_scoped_internal_procedures( """Build nested procedures that serialize scoped-origin consumers around the native invocation.""" procedures = [] for index, argument in enumerate(scoped): - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name next_step = self._derived_step_name(index + 1) scoped_body = self._derived_scoped_step_body(argument, scoped[:index], index, next_step) procedures.append( @@ -1232,7 +1464,7 @@ def _derived_scoped_step_body( next_step: str, ) -> tuple: """Reuse a prior read-only scoped origin or acquire it exactly once.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name body: tuple = ( FortranAssignment( f"bound_{name}_status", @@ -1247,7 +1479,7 @@ def _derived_scoped_step_body( and candidate.derived.type_identity == argument.derived.type_identity ) for candidate in reversed(same_type): - prior = candidate.bridge.native_name.lower() + prior = candidate.entrypoint.parameter_name body = ( FortranIf( CodeExpression( @@ -1274,7 +1506,7 @@ def _derived_pointer_call_initializers(self, plan: FunctionPlan) -> tuple: def _one_derived_pointer_call_initializer(self, argument: ArgumentTransferPlan) -> tuple: """Build initialization for one derived pointer actual selected by its completed call action.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name holder = f"{name}_pointer_holder%value" associate_holder = FortranIf( CodeExpression(f"associated({holder})"), @@ -1305,7 +1537,7 @@ def _derived_pointer_call_finalizers(self, plan: FunctionPlan) -> tuple: def _one_derived_pointer_call_finalizer(self, argument: ArgumentTransferPlan) -> tuple: """Build finalization for one derived pointer actual selected by its completed call action.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return ( FortranIf( CodeExpression(f"bound_{name}_access == 4_c_int .or. bound_{name}_access == 6_c_int"), @@ -1326,9 +1558,9 @@ def _one_derived_pointer_call_finalizer(self, argument: ArgumentTransferPlan) -> def _derived_argument_output_and_cleanup(self, argument: ArgumentTransferPlan) -> tuple: """Build output projection and cleanup for one derived argument after restoration.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name nodes = [] - if argument.bridge.descriptor_output_role is not None: + if argument.entrypoint.descriptor_output_role is not None: nodes.append(self._derived_argument_output_finalizer(argument)) else: nodes.extend( @@ -1347,7 +1579,7 @@ def _derived_argument_output_and_cleanup(self, argument: ArgumentTransferPlan) - def _derived_argument_output_finalizer(self, argument: ArgumentTransferPlan) -> FortranIf: """Build the completed output action for one derived carrier.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return FortranIf( CodeExpression(f"bound_{name}_access == 3_c_int"), body=self._derived_holder_output_nodes(name, allocatable=True), @@ -1387,7 +1619,7 @@ def _derived_ready_condition(arguments: tuple[ArgumentTransferPlan, ...]) -> str @staticmethod def _derived_status_parameter(argument: ArgumentTransferPlan) -> str: """Return the bridge status parameter shared by derived transaction helpers.""" - return f"bound_{argument.bridge.native_name.lower()}_status" + return f"bound_{argument.entrypoint.parameter_name}_status" @staticmethod def _derived_step_name(index: int) -> str: @@ -1476,17 +1708,19 @@ def _lower_result_direct_value( result: ResultPlan, ) -> tuple[str | None, str | None]: """Return the procedure shape of a direct native function result.""" - return "result", self._bridge_result_type(plan, result) + return "result", self._entrypoint_result_type(plan, result) def _owned_direct_result_parameters( self, - result: ResultPlan | None, + result: NativeEntrypointResultPlan | None, ) -> tuple[FortranParameter, ...]: """Expose persistent binding-owned descriptor storage as one output dummy.""" if result is None: return () handle = result.native_array_handle - if handle is None or handle.array.rank is None: + if handle is None: + return () + if handle.array.rank is None: raise ValueError(f"Owned result {result.owner_path!r} has no descriptor rank") if self._is_owned_deferred_character_result(result): return ( @@ -1503,13 +1737,12 @@ def _owned_direct_result_parameters( ), ) - def _scalar_descriptor_direct_result_parameters( + def _scalar_descriptor_direct_result_parameters_for_result( self, - plan: FunctionPlan, + result: NativeEntrypointResultPlan, ) -> tuple[FortranParameter, ...]: """Expose runtime metadata associated with a direct descriptor result.""" - result = self._direct_result(plan) - if result is None or result.scalar_descriptor is None: + if result.scalar_descriptor is None: return () parameters = [FortranParameter("result_present", "integer(c_int)")] if result.scalar_descriptor.runtime_length: @@ -1523,10 +1756,10 @@ def _owned_native_array_result_operations(self, function: FunctionPlan) -> tuple for result in function.results: if not self._supports_owned_native_array_result_operations(result): continue - handle = result.native_array_handle - if handle is None: - continue - for operation in handle.operations: + for entrypoint in self._generated_support_procedure_entrypoints_for( + result.owner_path, "native_array:owned:" + ): + operation = NativeArrayOperation(entrypoint.role.rsplit(":", 1)[-1]) procedure = self._owned_native_array_result_operation(result, operation) if procedure is not None: procedures.append(procedure) @@ -1545,7 +1778,10 @@ def _default_native_array_argument_operations( or handle.default_handle.construction is not NativeArrayDefaultConstruction.LAZY_OWNED_DESCRIPTOR ): continue - for operation in handle.default_handle.operations: + for entrypoint in self._generated_support_procedure_entrypoints_for( + argument.owner_path, "native_array:owned:" + ): + operation = NativeArrayOperation(entrypoint.role.rsplit(":", 1)[-1]) procedure = self._owned_native_array_result_operation(argument, operation) if procedure is not None: procedures.append(procedure) @@ -1729,14 +1965,14 @@ def _owned_native_array_result_operation_name( result: ArgumentTransferPlan | ResultPlan, operation: NativeArrayOperation, ) -> str: - """Return the C-visible typed operation name for one owned result.""" - preferred = result.bridge.native_name or "result" - owner = NativeSymbolNames.compact(result.owner_path, preferred, limit=38) - return f"bind_c_owned_{owner}_{operation.value}" + """Return one planner-owned descriptor operation symbol.""" + return self._generated_support_procedure_entrypoint( + result.owner_path, f"native_array:owned:{operation.value}" + ).symbol_name def _visit_ModuleVariablePlan(self, plan: ModuleVariablePlan) -> tuple[FortranFunction, ...]: """Lower bridge-owned getter and setter actions into procedures.""" - if plan.binding.getter_action is ModuleGetterAction.NATIVE_ARRAY_HANDLE: + if plan.bridge.native_getter_action is ModuleGetterAction.NATIVE_ARRAY_HANDLE: return self._lower_module_native_array_operations(plan) return ( *self._lower_module_getter(plan), @@ -1745,7 +1981,7 @@ def _visit_ModuleVariablePlan(self, plan: ModuleVariablePlan) -> tuple[FortranFu def _lower_module_getter(self, plan: ModuleVariablePlan) -> tuple[FortranFunction, ...]: """Dispatch one completed bridge getter action explicitly.""" - action = plan.bridge.getter_action + action = plan.bridge.native_getter_action match action: case ModuleGetterAction.CONSTANT_VALUE: return self._lower_module_getter_constant_value(plan) @@ -2093,32 +2329,15 @@ def _derived_origin_pointer_restore(self, variable: ModuleVariablePlan) -> Fortr ), ) - @staticmethod - def _derived_origin_supports(variable: ModuleVariablePlan, operation: str) -> bool: - """Return whether the completed module-origin handoff declares the requested bridge operation.""" - storage = variable.derived.handoff.storage - support = { - DerivedObjectStorage.MODULE_PROXY: {"scoped"}, - DerivedObjectStorage.MODULE_TARGET: {"address"}, - DerivedObjectStorage.MODULE_ALLOCATABLE: {"present", "scoped", "checkout", "restore"}, - DerivedObjectStorage.MODULE_ALLOCATABLE_TARGET: { - "present", - "address", - "checkout", - "restore", - }, - DerivedObjectStorage.MODULE_POINTER: {"present", "scoped", "checkout", "restore"}, - } - return operation in support.get(storage, set()) - - @staticmethod - def _derived_origin_symbol(variable: ModuleVariablePlan) -> str: - """Return the collision-safe symbol fragment for one scalar-derived module origin.""" - return NativeSymbolNames.compact(variable.owner_path, variable.symbol_name) + def _derived_origin_supports(self, variable: ModuleVariablePlan, operation: str) -> bool: + """Return whether planning registered one derived-origin operation.""" + return (variable.owner_path, f"derived_origin:{operation}") in self._generated_support_procedure_entrypoints def _derived_origin_bridge_name(self, variable: ModuleVariablePlan, operation: str) -> str: - """Return the exported bridge symbol for one derived-origin operation.""" - return f"bind_c_prik_origin_{self._derived_origin_symbol(variable)}_{operation}" + """Return one planner-owned derived-origin operation symbol.""" + return self._generated_support_procedure_entrypoint( + variable.owner_path, f"derived_origin:{operation}" + ).symbol_name def _lower_module_getter_derived_value_copy( self, @@ -2164,32 +2383,15 @@ def _lower_module_native_array_operations( plan: ModuleVariablePlan, ) -> tuple[FortranFunction, ...]: """Lower every planned module-handle operation into a named bridge procedure.""" - handle = plan.native_array_handle - if handle is None: + if plan.native_array_handle is None: raise ValueError(f"Module handle {plan.owner_path!r} has no operation plan") - procedures = [] - for operation in handle.operations: - procedure = self._lower_module_native_array_operation(plan, operation) - if procedure is not None: - procedures.append(procedure) - return tuple(procedures) - - def _lower_module_native_array_operation( - self, - plan: ModuleVariablePlan, - operation: NativeArrayOperation, - ) -> FortranFunction | None: - """Dispatch one operation using only the completed typed operation selector.""" - if operation in { - NativeArrayOperation.NATIVE_BYTE_ORDER, - NativeArrayOperation.ALIGNED, - NativeArrayOperation.WRITEABLE, - NativeArrayOperation.LAYOUT, - }: - return None - if operation is NativeArrayOperation.TO_NUMPY: - return None - return self._lower_module_native_array_bridge_operation(plan, operation) + return tuple( + self._lower_module_native_array_bridge_operation( + plan, + NativeArrayOperation(operation.role.rsplit(":", 1)[-1]), + ) + for operation in self._generated_support_procedure_entrypoints_for(plan.owner_path, "module:native_array:") + ) def _lower_module_native_array_bridge_operation( self, @@ -2476,8 +2678,10 @@ def _module_native_array_element_type(self, plan: ModuleVariablePlan) -> str: return PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name).fortran_spelling def _module_native_array_operation_name(self, plan: ModuleVariablePlan, operation) -> str: - """Return the C-visible operation name derived from plan ownership.""" - return f"bind_c_{plan.symbol_name}_{operation.value}" + """Return one planner-owned module native-array operation symbol.""" + return self._generated_support_procedure_entrypoint( + plan.owner_path, f"module:native_array:{operation.value}" + ).symbol_name def _lower_module_getter_direct_value(self, plan: ModuleVariablePlan) -> tuple[FortranFunction, ...]: """Return one direct scalar module-variable getter.""" @@ -2595,7 +2799,7 @@ def _lower_module_getter_nullable_snapshot( plan: ModuleVariablePlan, ) -> tuple[FortranFunction, ...]: """Return a nullable detached snapshot through C-owned storage.""" - presence = "allocated" if plan.bridge.descriptor_kind == "allocatable" else "associated" + presence = "allocated" if plan.entrypoint.descriptor_kind == "allocatable" else "associated" return self._lower_nullable_module_getter(plan, f"{presence}({self._native_variable_name(plan)})") def _lower_nullable_module_getter( @@ -2677,7 +2881,7 @@ def _lower_argument(self, plan: ArgumentTransferPlan) -> tuple[FortranParameter, """Dispatch one completed bridge optional mode explicitly.""" if plan.callback is not None: return () - mode = plan.bridge.optional_mode + mode = plan.entrypoint.optional_mode if plan.object_kind is ObjectKind.DERIVED_TYPE: return self._lower_derived_argument(plan, mode) if plan.object_kind is ObjectKind.NUMPY_ARRAY: @@ -2695,7 +2899,7 @@ def _lower_derived_argument( _mode: OptionalMode, ) -> tuple[FortranParameter, ...]: """Receive the generic carrier and typed module-origin operations.""" - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name return ( FortranParameter(f"bound_{name}", "type(c_ptr)", ("value",)), FortranParameter(f"bound_{name}_access", "integer(c_int)", ("value",)), @@ -2718,7 +2922,7 @@ def _lower_derived_argument( ("intent(out)",), ), ) - if plan.bridge.descriptor_output_role is not None + if plan.entrypoint.descriptor_output_role is not None else () ), ) @@ -2727,7 +2931,8 @@ def _lower_derived_argument( def _is_character_buffer_argument(plan: ArgumentTransferPlan) -> bool: """Return whether an argument uses the completed character-buffer handoff mode.""" return ( - plan.object_kind is ObjectKind.STRING and plan.bridge.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER + plan.object_kind is ObjectKind.STRING + and plan.entrypoint.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER ) def _lower_character_buffer_argument( @@ -2759,9 +2964,9 @@ def _lower_scalar_or_string_argument( def _lower_argument_required_descriptor(self, plan: ArgumentTransferPlan) -> tuple[FortranParameter, ...]: """Receive one required Python argument as a nullable descriptor payload.""" - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name parameters = [FortranParameter(f"bound_{name}", "type(c_ptr)", ("value",))] - if plan.bridge.descriptor_output_role is not None: + if plan.entrypoint.descriptor_output_role is not None: parameters.extend( ( FortranParameter(f"bound_{name}_output", "type(c_ptr)", ("value",)), @@ -2776,17 +2981,17 @@ def _lower_array_argument( mode: OptionalMode, ) -> tuple[FortranParameter, ...]: """Dispatch one array parameter from its completed handoff mode.""" - if plan.bridge.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: if mode not in {OptionalMode.REQUIRED, OptionalMode.DESCRIPTOR}: raise ValueError(f"Unsupported Fortran descriptor presence mode for {plan.owner_path!r}: {mode!r}") return self._lower_argument_native_array_descriptor(plan) if mode not in {OptionalMode.REQUIRED, OptionalMode.NULLABLE_VALUE}: raise ValueError(f"Unsupported Fortran array presence mode for {plan.owner_path!r}: {mode!r}") - if plan.bridge.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: return self._lower_argument_array_buffer(plan) - if plan.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: return self._lower_opaque_array_argument(plan, mode) - raise ValueError(f"Unsupported Fortran array handoff for {plan.owner_path!r}: {plan.bridge.handoff_mode!r}") + raise ValueError(f"Unsupported Fortran array handoff for {plan.owner_path!r}: {plan.entrypoint.handoff_mode!r}") def _lower_opaque_array_argument( self, @@ -2798,22 +3003,23 @@ def _lower_opaque_array_argument( return self._lower_argument_required_opaque_address(plan) if mode is OptionalMode.NULLABLE_VALUE and self._is_scalar_storage_array(plan.array): return self._lower_argument_nullable_value(plan) - raise ValueError(f"Unsupported Fortran array handoff for {plan.owner_path!r}: {plan.bridge.handoff_mode!r}") + raise ValueError(f"Unsupported Fortran array handoff for {plan.owner_path!r}: {plan.entrypoint.handoff_mode!r}") def _is_opaque_array_required_argument(self, plan: ArgumentTransferPlan) -> bool: """Return whether a required array-shaped argument uses an opaque address.""" return bool( - plan.binding.python_action is PythonBarrierAction.RAW_ADDRESS or self._is_scalar_storage_array(plan.array) + plan.bridge.native_action is NativeBarrierAction.PASS_RAW_ADDRESS + or self._is_scalar_storage_array(plan.array) ) - # Native-array-handle bridge parameters. + # Native-array-handle entrypoint parameters. def _lower_argument_native_array_descriptor( self, plan: ArgumentTransferPlan, ) -> tuple[FortranParameter, ...]: """Receive one standard descriptor as a typed allocatable/pointer dummy.""" handle = plan.native_array_handle - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name attribute = "allocatable" if handle.descriptor_kind is NativeArrayDescriptorKind.ALLOCATABLE else "pointer" parameters = [ FortranParameter( @@ -2822,7 +3028,7 @@ def _lower_argument_native_array_descriptor( (attribute, self._array_dimension_attribute(handle.array.rank)), ) ] - if plan.bridge.optional_mode is OptionalMode.DESCRIPTOR: + if plan.entrypoint.optional_mode is OptionalMode.DESCRIPTOR: parameters.append(FortranParameter(f"bound_{name}_present", "type(c_ptr)", ("value",))) return tuple(parameters) @@ -2833,8 +3039,8 @@ def _native_array_argument_element_type(self, plan: ArgumentTransferPlan) -> str return PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name).fortran_spelling def _lower_argument_required(self, plan: ArgumentTransferPlan) -> tuple[FortranParameter, ...]: - """Dispatch one required bridge parameter from its completed ABI shape.""" - mode = plan.bridge.handoff_mode + """Dispatch one required entrypoint parameter from its completed ABI shape.""" + mode = plan.entrypoint.handoff_mode match mode: case ArgumentHandoffMode.VALUE: return self._lower_argument_required_value(plan) @@ -2863,7 +3069,7 @@ def _lower_argument_required_opaque_address( plan: ArgumentTransferPlan, ) -> tuple[FortranParameter, ...]: """Return one C pointer value for caller-owned opaque storage.""" - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name return (FortranParameter(f"bound_{name}", "type(c_ptr)", ("value",)),) # String argument lowering. @@ -2872,7 +3078,7 @@ def _lower_argument_string_value( plan: ArgumentTransferPlan, ) -> tuple[FortranParameter, ...]: """Receive one C UTF-8 payload address and its runtime byte length.""" - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name return ( FortranParameter(f"bound_{name}", "type(c_ptr)", ("value",)), FortranParameter(f"{name}_length", "integer(c_int64_t)", ("value",)), @@ -2887,7 +3093,7 @@ def _lower_argument_array_buffer( array = plan.array if array is None: raise ValueError(f"Array argument {plan.owner_path!r} has no handoff spec") - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name return ( FortranParameter(f"bound_{name}", "type(c_ptr)", ("value",)), *( @@ -2921,21 +3127,21 @@ def _lower_argument_array_buffer( def _lower_argument_nullable_value(self, plan: ArgumentTransferPlan) -> tuple[FortranParameter, ...]: """Return one nullable C pointer parameter.""" - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name return (FortranParameter(f"bound_{name}", "type(c_ptr)", ("value",)),) def _lower_argument_descriptor(self, plan: ArgumentTransferPlan) -> tuple[FortranParameter, ...]: """Return nullable value and explicit presence pointer parameters.""" - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name return ( FortranParameter(f"bound_{name}", "type(c_ptr)", ("value",)), FortranParameter(f"bound_{name}_present", "type(c_ptr)", ("value",)), ) def _parameter(self, plan: ArgumentTransferPlan, attributes: tuple[str, ...]) -> FortranParameter: - """Return one bridge ABI parameter from its completed transfer plan.""" + """Return one entrypoint ABI parameter from its completed transfer plan.""" scalar_type = PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name) - return FortranParameter(plan.bridge.native_name.lower(), scalar_type.fortran_spelling, attributes) + return FortranParameter(plan.entrypoint.parameter_name, scalar_type.fortran_spelling, attributes) def _function_body( self, @@ -2949,9 +3155,9 @@ def _function_body( result_name = self._native_direct_result_name(plan, result_name) derived_optional = tuple( argument - for argument in sorted(plan.arguments, key=lambda item: item.bridge.abi_position) + for argument in sorted(plan.arguments, key=lambda item: item.projected_call_slot.native_position) if argument.derived_call is not None - and argument.bridge.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} + and argument.entrypoint.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} ) if derived_optional: procedures = self._derived_optional_dispatch_procedures(plan, derived_optional, result_name) @@ -2999,19 +3205,19 @@ def _ordinary_function_body( @staticmethod def _polymorphic_arguments(plan: FunctionPlan) -> tuple[ArgumentTransferPlan, ...]: - """Return polymorphic inputs in bridge ABI order.""" + """Return polymorphic inputs in original-Fortran call order.""" return tuple( argument - for argument in sorted(plan.arguments, key=lambda item: item.bridge.abi_position) + for argument in sorted(plan.arguments, key=lambda item: item.projected_call_slot.native_position) if argument.polymorphic is not None ) @staticmethod def _assumed_rank_arguments(plan: FunctionPlan) -> tuple[ArgumentTransferPlan, ...]: - """Return assumed-rank arrays in bridge ABI order.""" + """Return assumed-rank arrays in original-Fortran call order.""" return tuple( argument - for argument in sorted(plan.arguments, key=lambda item: item.bridge.abi_position) + for argument in sorted(plan.arguments, key=lambda item: item.projected_call_slot.native_position) if argument.array is not None and argument.array.rank is None ) @@ -3020,8 +3226,8 @@ def _non_derived_optional_arguments(plan: FunctionPlan) -> tuple[ArgumentTransfe """Return optional arguments handled by the ordinary presence tree.""" return tuple( argument - for argument in sorted(plan.arguments, key=lambda item: item.bridge.abi_position) - if argument.bridge.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} + for argument in sorted(plan.arguments, key=lambda item: item.projected_call_slot.native_position) + if argument.entrypoint.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} and argument.derived_call is None ) @@ -3058,13 +3264,13 @@ def _polymorphic_call_tree( ) replacements.pop(argument.owner_path, None) cases.append(FortranCase(None, ())) - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return FortranSelectCase(CodeExpression(f"bound_{name}_polymorphic"), tuple(cases)) @staticmethod def _polymorphic_variant_name(argument: ArgumentTransferPlan, abi_code: int) -> str: """Name one bridge-local typed pointer from its stable plan code.""" - return f"{argument.bridge.native_name.lower()}_polymorphic_{abi_code}" + return f"{argument.entrypoint.parameter_name}_polymorphic_{abi_code}" def _derived_optional_dispatch_procedures( self, @@ -3152,7 +3358,7 @@ def _derived_native_parameter( @staticmethod def _derived_optional_parameter_name(argument: ArgumentTransferPlan) -> str: """Return the local optional-presence parameter name for one derived argument.""" - return f"prik_optional_{argument.bridge.native_name.lower()}" + return f"prik_optional_{argument.entrypoint.parameter_name}" @staticmethod def _derived_optional_step_name(index: int) -> str: @@ -3299,7 +3505,7 @@ def _native_invocation_target( if receiver is None: raise ValueError(f"Type-bound call {plan.owner_path!r} has no passed-object argument") expression = replacements.get(receiver.owner_path, self._native_argument_expression(receiver)) - return f"{expression}%{class_call.type_bound_name}", receiver.native_call_slot.native_position + return f"{expression}%{class_call.type_bound_name}", receiver.projected_call_slot.native_position @staticmethod def _is_pointer_derived_holder_result(result: ResultPlan | None) -> bool: @@ -3322,14 +3528,23 @@ def _native_arguments( """Return native call expressions in planned ABI order without recomputing argument policy.""" expressions = dict(self._visible_native_argument_entries(plan, present, replacements)) expressions.update( - (slot.native_position, CodeExpression(self._literal_expression(slot.literal_value))) - for slot in plan.native_call_slots - if slot.source_kind == "literal" + (slot.native_position, CodeExpression(slot.native_name.casefold())) + for slot in plan.entrypoint.projected_slots + if slot.adapter is not None + if slot.projection_action + in { + EntrypointProjectionAction.TYPED_LITERAL, + EntrypointProjectionAction.COMPUTED_LENGTH, + EntrypointProjectionAction.COMPUTED_PRESENCE, + EntrypointProjectionAction.COMPUTED_SHAPE, + EntrypointProjectionAction.COMPUTED_STRIDE, + EntrypointProjectionAction.WORK_STORAGE, + } ) expressions.update(self._hidden_native_result_entries(plan)) return tuple( expressions[slot.native_position] - for slot in sorted(plan.native_call_slots, key=lambda item: item.native_position) + for slot in self._adapter_slots(plan) if slot.native_position in expressions and slot.native_position != excluded_position ) @@ -3344,14 +3559,14 @@ def _visible_native_argument_entries( has_optional = self._has_optional_arguments(plan) for argument in plan.arguments: if ( - argument.bridge.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} + argument.entrypoint.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} and argument.owner_path not in present ): continue expression = replacements.get(argument.owner_path, self._native_argument_expression(argument)) if has_optional: expression = f"{argument.bridge.native_name}={expression}" - entries.append((argument.native_call_slot.native_position, CodeExpression(expression))) + entries.append((argument.projected_call_slot.native_position, CodeExpression(expression))) return tuple(entries) def _assumed_rank_call_tree( @@ -3368,15 +3583,15 @@ def _assumed_rank_call_tree( if index == len(arguments): optional = tuple( argument - for argument in sorted(plan.arguments, key=lambda item: item.bridge.abi_position) - if argument.bridge.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} + for argument in sorted(plan.arguments, key=lambda item: item.projected_call_slot.native_position) + if argument.entrypoint.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} and argument.derived_call is None ) if optional: return self._optional_call_tree(plan, optional, 0, present, result_name, replacements) return self._native_invocation(plan, present, result_name, replacements) argument = arguments[index] - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name cases = [] for rank in range(1, 16): rank_name = f"{name}_rank_{rank}" @@ -3408,7 +3623,7 @@ def _hidden_native_result_entries( ) -> tuple[tuple[int, CodeExpression], ...]: """Return all mechanically lowered hidden-result native entries.""" entries = [] - for slot in plan.native_call_slots: + for slot in self._adapter_slots(plan): if slot.source_kind != "result": continue expression = self._native_output_value_name(slot) @@ -3419,9 +3634,9 @@ def _hidden_native_result_entries( def _native_argument_expression(self, plan: ArgumentTransferPlan) -> str: """Return the native actual expression selected by one completed call slot.""" - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name if plan.callback is not None: - return plan.callback.adapter_symbol + return plan.callback.bridge.adapter_symbol if plan.derived_call is not None: if plan.derived_call.dummy_category in { DerivedDummyCategory.ALLOCATABLE, @@ -3431,22 +3646,22 @@ def _native_argument_expression(self, plan: ArgumentTransferPlan) -> str: if plan.derived_call.dummy_category is DerivedDummyCategory.POINTER: return f"{name}_call_pointer" return name - if plan.bridge.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: return self._array_native_argument_expression(plan) - if plan.bridge.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: return name - if plan.bridge.optional_mode in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR}: + if plan.entrypoint.optional_mode in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR}: return f"{name}_descriptor" if plan.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: return f"{name}_native" return name def _presence_condition(self, plan: ArgumentTransferPlan) -> str: - """Return the local C-pointer association condition for one nullable bridge argument.""" - name = plan.bridge.native_name.lower() + """Return the local C-pointer association condition for one nullable entrypoint argument.""" + name = plan.entrypoint.parameter_name if plan.derived_call is not None: return f"bound_{name}_access /= 0_c_int" - suffix = "_present" if plan.bridge.optional_mode is OptionalMode.DESCRIPTOR else "" + suffix = "_present" if plan.entrypoint.optional_mode is OptionalMode.DESCRIPTOR else "" return f"c_associated(bound_{name}{suffix})" def _present_preparation( @@ -3456,7 +3671,7 @@ def _present_preparation( """Dispatch only the bridge data action completed before lowering.""" if plan.derived_call is not None: return () - if plan.bridge.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: return () action = plan.bridge.data_action match action: @@ -3471,7 +3686,7 @@ def _prepare_present_associated_view( plan: ArgumentTransferPlan, ) -> tuple[FortranPointerAssignment | FortranCall | FortranIf, ...]: """Associate a non-owning native view without copying payload data.""" - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name if self._uses_allocatable_holder(plan): return ( FortranAssignment(f"bound_{name}_allocation_status", CodeExpression("0_c_int")), @@ -3499,20 +3714,20 @@ def _prepare_present_associated_view( (CodeExpression(f"bound_{name}"), CodeExpression(name)), ), ) - if plan.bridge.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: return () - if plan.bridge.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER: return self._array_pointer_initializer_nodes(plan) - if plan.bridge.optional_mode is OptionalMode.NULLABLE_VALUE: + if plan.entrypoint.optional_mode is OptionalMode.NULLABLE_VALUE: return ( FortranCall( "c_f_pointer", (CodeExpression(f"bound_{name}"), CodeExpression(name)), ), ) - if plan.bridge.optional_mode not in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR}: + if plan.entrypoint.optional_mode not in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR}: raise ValueError(f"Associated-view preparation requires an optional argument: {plan.owner_path!r}") - if plan.native_call_slot.value_kind != "pointer": + if plan.projected_call_slot.value_kind != "pointer": raise ValueError(f"Associated descriptor view requires pointer policy: {plan.owner_path!r}") return ( self._descriptor_input_pointer_call(name), @@ -3538,12 +3753,12 @@ def _prepare_present_representation_copy( ) return tuple(nodes) if plan.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name return (FortranAssignment(f"{name}_native", CodeExpression(name)),) - if plan.bridge.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER: return self._string_value_initializer_nodes(plan) if self._is_derived_value_copy(plan): - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name return ( FortranCall( "c_f_pointer", @@ -3551,11 +3766,11 @@ def _prepare_present_representation_copy( ), FortranAssignment(f"{name}_value", CodeExpression(name)), ) - if plan.bridge.optional_mode not in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR}: + if plan.entrypoint.optional_mode not in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR}: raise ValueError(f"Representation copy requires descriptor policy: {plan.owner_path!r}") - if plan.native_call_slot.value_kind != "allocatable": + if plan.projected_call_slot.value_kind != "allocatable": raise ValueError(f"Representation copy requires allocatable policy: {plan.owner_path!r}") - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name return ( self._descriptor_input_pointer_call(name), FortranIf( @@ -3584,14 +3799,14 @@ def _optional_argument_declarations( """Return optional helper declarations for one completed handoff.""" if argument.derived_call is not None: return () - mode = argument.bridge.optional_mode - if mode is OptionalMode.REQUIRED or argument.bridge.handoff_mode in { + mode = argument.entrypoint.optional_mode + if mode is OptionalMode.REQUIRED or argument.entrypoint.handoff_mode in { ArgumentHandoffMode.NATIVE_DESCRIPTOR, ArgumentHandoffMode.CHARACTER_BUFFER, ArgumentHandoffMode.ARRAY_BUFFER, }: return () - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name if argument.object_kind is ObjectKind.DERIVED_TYPE: if self._uses_allocatable_holder(argument): return ( @@ -3607,11 +3822,11 @@ def _optional_argument_declarations( if mode is OptionalMode.NULLABLE_VALUE: return (FortranDeclaration(name, scalar_type.fortran_spelling, ("pointer",)),) declarations = [FortranDeclaration(f"{name}_input", scalar_type.fortran_spelling, ("pointer",))] - descriptor_attribute = "pointer" if argument.native_call_slot.value_kind == "pointer" else "allocatable" + descriptor_attribute = "pointer" if argument.projected_call_slot.value_kind == "pointer" else "allocatable" declarations.append( FortranDeclaration(f"{name}_descriptor", scalar_type.fortran_spelling, (descriptor_attribute,)) ) - if mode is OptionalMode.REQUIRED_DESCRIPTOR and argument.bridge.descriptor_output_role is not None: + if mode is OptionalMode.REQUIRED_DESCRIPTOR and argument.entrypoint.descriptor_output_role is not None: declarations.append(FortranDeclaration(f"{name}_output", scalar_type.fortran_spelling, ("pointer",))) return tuple(declarations) @@ -3628,7 +3843,7 @@ def _logical_scalar_argument_declarations( raise ValueError(f"Logical argument {argument.owner_path!r} has no native type spelling") declarations.append( FortranDeclaration( - f"{argument.bridge.native_name.lower()}_native", + f"{argument.entrypoint.parameter_name}_native", argument.scalar_native_type, ) ) @@ -3641,12 +3856,12 @@ def _logical_scalar_argument_initializers( """Copy required C Boolean values into their exact native kinds.""" return tuple( FortranAssignment( - f"{argument.bridge.native_name.lower()}_native", - CodeExpression(argument.bridge.native_name.lower()), + f"{argument.entrypoint.parameter_name}_native", + CodeExpression(argument.entrypoint.parameter_name), ) for argument in plan.arguments if argument.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY - and argument.bridge.optional_mode is OptionalMode.REQUIRED + and argument.entrypoint.optional_mode is OptionalMode.REQUIRED ) def _logical_scalar_argument_finalizers( @@ -3658,9 +3873,9 @@ def _logical_scalar_argument_finalizers( for argument in plan.arguments: if argument.scalar_logical_abi is not ScalarLogicalABI.NATIVE_KIND_COPY or not argument.mutates_native: continue - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name assignment = FortranAssignment(name, CodeExpression(f"{name}_native")) - if argument.bridge.optional_mode is OptionalMode.REQUIRED: + if argument.entrypoint.optional_mode is OptionalMode.REQUIRED: nodes.append(assignment) else: nodes.append(FortranIf(CodeExpression(self._presence_condition(argument)), body=(assignment,))) @@ -3673,8 +3888,8 @@ def _opaque_address_declarations(self, plan: FunctionPlan) -> tuple[FortranDecla for argument in plan.arguments if ( argument.derived_call is None - and argument.bridge.optional_mode is OptionalMode.REQUIRED - and argument.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS + and argument.entrypoint.optional_mode is OptionalMode.REQUIRED + and argument.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS and argument.bridge.data_action in {BridgeDataAction.ASSOCIATE_VIEW, BridgeDataAction.COPY_REPRESENTATION} and ( @@ -3687,7 +3902,7 @@ def _opaque_address_declarations(self, plan: FunctionPlan) -> tuple[FortranDecla if argument.object_kind is ObjectKind.DERIVED_TYPE else ( FortranDeclaration( - argument.bridge.native_name.lower(), + argument.entrypoint.parameter_name, self._opaque_argument_type(argument), ("pointer",), ), @@ -3700,7 +3915,7 @@ def _derived_argument_declarations( argument: ArgumentTransferPlan, ) -> tuple[FortranDeclaration, ...]: """Declare the typed pointee and optional bridge-local value copy.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name derived_type = f"type({self._derived_native_alias(argument.derived.backend_symbol)})" declarations = [FortranDeclaration(name, derived_type, ("pointer",))] if self._is_derived_value_copy(argument): @@ -3723,8 +3938,8 @@ def _opaque_address_initializers( for argument in plan.arguments if ( argument.derived_call is None - and argument.bridge.optional_mode is OptionalMode.REQUIRED - and argument.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS + and argument.entrypoint.optional_mode is OptionalMode.REQUIRED + and argument.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS and argument.bridge.data_action in {BridgeDataAction.ASSOCIATE_VIEW, BridgeDataAction.COPY_REPRESENTATION} and ( @@ -3740,7 +3955,7 @@ def _opaque_address_initializer_nodes( argument: ArgumentTransferPlan, ) -> tuple[FortranCall | FortranAssignment, ...]: """Associate one typed pointer and materialize its planned value copy.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name association = FortranCall( "c_f_pointer", (CodeExpression(f"bound_{name}"), CodeExpression(name)), @@ -3761,7 +3976,7 @@ def _array_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, . """Declare boundary views and policy-selected native logical storage.""" declarations = [] for argument in plan.arguments: - if argument.bridge.handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER: + if argument.entrypoint.handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER: continue array = argument.array if array is None: @@ -3783,7 +3998,7 @@ def _array_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, . if array.dense_actual_role is not None: declarations.append( FortranDeclaration( - argument.bridge.native_name.lower(), + argument.entrypoint.parameter_name, self._array_element_fortran_type(argument), ("pointer", self._array_dimension_attribute(array.rank)), ) @@ -3821,7 +4036,7 @@ def _logical_array_argument_initializers( for argument in plan.arguments if argument.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY and argument.array_copy_in - and argument.bridge.optional_mode is OptionalMode.REQUIRED + and argument.entrypoint.optional_mode is OptionalMode.REQUIRED ) def _logical_array_argument_finalizers( @@ -3844,7 +4059,7 @@ def _logical_array_argument_finalizers( target, CodeExpression(f"merge(.true._c_bool, .false._c_bool, {native})"), ) - if argument.bridge.optional_mode is OptionalMode.REQUIRED: + if argument.entrypoint.optional_mode is OptionalMode.REQUIRED: finalizers.append(assignment) else: finalizers.append(FortranIf(CodeExpression(self._presence_condition(argument)), body=(assignment,))) @@ -3853,14 +4068,14 @@ def _logical_array_argument_finalizers( @staticmethod def _logical_array_native_name(argument: ArgumentTransferPlan) -> str: """Return the bridge-local exact-kind array name for ``argument``.""" - return f"{argument.bridge.native_name.lower()}_native" + return f"{argument.entrypoint.parameter_name}_native" def _logical_array_dimension_attribute(self, argument: ArgumentTransferPlan) -> str: """Render automatic-array extents in the completed native orientation.""" array = argument.array if array is None or array.rank is None: raise ValueError(f"Logical array {argument.owner_path!r} requires a concrete rank") - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name extents = [f"{name}_extent_{axis}" for axis in range(array.rank)] if array.native_order == "ORDER_C": extents.reverse() @@ -3882,7 +4097,7 @@ def _array_writeback_finalizers( raise ValueError( f"Unsupported array writeback ABI for {argument.owner_path!r}: {argument.array_writeback_abi!r}" ) - if argument.bridge.optional_mode is OptionalMode.REQUIRED: + if argument.entrypoint.optional_mode is OptionalMode.REQUIRED: finalizers.extend(nodes) else: finalizers.append(FortranIf(CodeExpression(self._presence_condition(argument)), body=nodes)) @@ -3898,7 +4113,7 @@ def _logical_array_writeback_nodes( raise ValueError(f"Logical array {argument.owner_path!r} has no handoff") if array.rank is not None: return self._logical_array_writeback_for_rank(argument, array.rank) - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name cases = tuple( FortranCase( rank, @@ -3914,7 +4129,7 @@ def _logical_array_writeback_for_rank( rank: int, ) -> tuple[FortranCall | FortranAssignment, ...]: """Return logical-array writeback nodes for one rank using the completed ABI conversion action.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name byte_pointer = self._logical_array_byte_pointer_name(argument) byte_count = " * ".join(f"{name}_extent_{axis}" for axis in range(rank)) return ( @@ -3935,15 +4150,15 @@ def _logical_array_writeback_for_rank( @staticmethod def _logical_array_byte_pointer_name(argument: ArgumentTransferPlan) -> str: """Return the bridge-local byte-pointer name for one logical-array rank conversion.""" - return f"{argument.bridge.native_name.lower()}_logical_bytes" + return f"{argument.entrypoint.parameter_name}_logical_bytes" def _array_initializers(self, plan: FunctionPlan) -> tuple[FortranCall | FortranIf, ...]: """Associate each completed ordinary array data/extent handoff.""" initializers = [] for argument in plan.arguments: - if argument.bridge.handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER: + if argument.entrypoint.handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER: continue - if argument.bridge.optional_mode is not OptionalMode.REQUIRED: + if argument.entrypoint.optional_mode is not OptionalMode.REQUIRED: continue if argument.array is not None and argument.array.rank is None: continue @@ -3980,7 +4195,7 @@ def _raw_array_pointer_initializer( shape = list(self._array_shape_from_roles(array, plan)) if array.native_order == "ORDER_C": shape.reverse() - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return FortranCall( "c_f_pointer", ( @@ -3996,9 +4211,8 @@ def _raw_array_address_arguments(self, plan: FunctionPlan) -> tuple[ArgumentTran argument for argument in plan.arguments if argument.object_kind is ObjectKind.NUMPY_ARRAY - and argument.binding.python_action is PythonBarrierAction.RAW_ADDRESS and argument.bridge.native_action is NativeBarrierAction.PASS_RAW_ADDRESS - and argument.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS + and argument.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS and argument.bridge.data_action is BridgeDataAction.ASSOCIATE_VIEW ) @@ -4007,7 +4221,7 @@ def _array_pointer_initializer(self, argument: ArgumentTransferPlan) -> FortranC array = argument.array if array is None or array.rank is None: raise ValueError(f"Array argument {argument.owner_path!r} requires a concrete rank") - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name extents = [f"{name}_extent_{axis}" for axis in range(array.rank)] if array.native_order == "ORDER_C": extents.reverse() @@ -4029,7 +4243,7 @@ def _array_pointer_initializer_nodes( array = argument.array if array is None or array.dense_actual_role is None: return (association,) - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return ( association, FortranIf( @@ -4049,7 +4263,7 @@ def _assumed_rank_array_declarations( argument: ArgumentTransferPlan, ) -> tuple[FortranDeclaration, ...]: """Declare one readable typed pointer local for every supported runtime rank.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name element_type = self._array_element_fortran_type(argument) attributes = ("pointer", "contiguous") if argument.array.contiguous is True else ("pointer",) return tuple( @@ -4068,7 +4282,7 @@ def _assumed_rank_pointer_initializer( pointer_name: str, ) -> FortranCall: """Associate one runtime-rank branch with its planned extent prefix.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name extents = ", ".join(f"{name}_extent_{axis}" for axis in range(rank)) return FortranCall( "c_f_pointer", @@ -4081,7 +4295,7 @@ def _assumed_rank_pointer_initializer( def _array_pointer_name(self, argument: ArgumentTransferPlan) -> str: """Name the bridge pointer, separating strided base storage visibly.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name return f"{name}_base" if argument.array is not None and argument.array.contiguous is False else name def _array_native_argument_expression(self, argument: ArgumentTransferPlan) -> str: @@ -4095,7 +4309,7 @@ def _array_boundary_argument_expression(self, argument: ArgumentTransferPlan) -> array = argument.array if array is None: raise ValueError(f"Array argument {argument.owner_path!r} has no handoff spec") - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name if array.rank is None: return name pointer_name = self._array_pointer_name(argument) @@ -4110,7 +4324,7 @@ def _strided_array_section_expression(self, argument: ArgumentTransferPlan) -> s array = argument.array if array is None or array.rank is None: raise ValueError(f"Strided array argument {argument.owner_path!r} requires a concrete rank") - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name pointer_name = self._array_pointer_name(argument) slices = (f"1:{name}_upper_bound_{axis} + 1:{name}_stride_{axis}" for axis in range(array.rank)) return f"{pointer_name}({', '.join(slices)})" @@ -4133,7 +4347,7 @@ def _string_address_declarations(self, plan: FunctionPlan) -> tuple[FortranDecla """Declare fixed helper-local character storage for address boundaries.""" declarations = [] for argument in self._string_address_arguments(plan): - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name length = self._string_address_length(argument) declarations.extend( ( @@ -4154,7 +4368,7 @@ def _string_address_initializers( """Associate fixed-width bytes and materialize native character locals.""" nodes = [] for argument in self._string_address_arguments(plan): - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name length = self._string_address_length(argument) nodes.extend( ( @@ -4177,7 +4391,7 @@ def _string_address_finalizers(self, plan: FunctionPlan) -> tuple[FortranAssignm for argument in self._string_address_arguments(plan): if not argument.mutates_native: continue - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name length = self._string_address_length(argument) nodes.append( FortranAssignment( @@ -4193,7 +4407,7 @@ def _string_address_arguments(self, plan: FunctionPlan) -> tuple[ArgumentTransfe argument for argument in plan.arguments if argument.object_kind is ObjectKind.STRING - and argument.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS + and argument.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS and argument.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION ) @@ -4208,9 +4422,9 @@ def _string_value_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclara """Return bridge-local character storage for string-value inputs.""" declarations = [] for argument in plan.arguments: - if argument.bridge.handoff_mode is not ArgumentHandoffMode.CHARACTER_BUFFER: + if argument.entrypoint.handoff_mode is not ArgumentHandoffMode.CHARACTER_BUFFER: continue - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name declarations.extend( ( FortranDeclaration( @@ -4230,9 +4444,9 @@ def _string_value_initializers( """Associate and copy C bytes only for completed representation-copy inputs.""" nodes = [] for argument in plan.arguments: - if argument.bridge.handoff_mode is not ArgumentHandoffMode.CHARACTER_BUFFER: + if argument.entrypoint.handoff_mode is not ArgumentHandoffMode.CHARACTER_BUFFER: continue - if argument.bridge.optional_mode is not OptionalMode.REQUIRED: + if argument.entrypoint.optional_mode is not OptionalMode.REQUIRED: continue if argument.bridge.data_action is not BridgeDataAction.COPY_REPRESENTATION: raise ValueError(f"String input {argument.owner_path!r} is missing representation-copy policy") @@ -4244,7 +4458,7 @@ def _string_value_initializer_nodes( plan: ArgumentTransferPlan, ) -> tuple[FortranCall | FortranAssignment, ...]: """Associate and materialize one present string payload.""" - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name 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)" @@ -4270,15 +4484,15 @@ def _string_value_finalizers( """Dispatch completed post-call string copyback actions.""" nodes = [] for argument in plan.arguments: - if argument.bridge.handoff_mode is not ArgumentHandoffMode.CHARACTER_BUFFER: + if argument.entrypoint.handoff_mode is not ArgumentHandoffMode.CHARACTER_BUFFER: continue action = argument.bridge.codegen_action if action is CodegenAction.CALL_LOCAL_INPUT: continue if action is CodegenAction.COPY_IN_OUT: copyback = self._lower_argument_string_copyback(argument) - if argument.bridge.optional_mode is OptionalMode.NULLABLE_VALUE: - name = argument.bridge.native_name.lower() + if argument.entrypoint.optional_mode is OptionalMode.NULLABLE_VALUE: + name = argument.entrypoint.parameter_name nodes.append(FortranIf(CodeExpression(f"c_associated(bound_{name})"), body=copyback)) else: nodes.extend(copyback) @@ -4291,7 +4505,7 @@ def _lower_argument_string_copyback( plan: ArgumentTransferPlan, ) -> tuple[FortranAssignment, ...]: """Copy one complete native character value back to binding storage.""" - name = plan.bridge.native_name.lower() + name = plan.entrypoint.parameter_name return ( FortranAssignment( f"{name}_bytes(1:{name}_length)", @@ -4304,14 +4518,14 @@ def _descriptor_initializers(self, plan: FunctionPlan) -> tuple[FortranPointerAs """Initialize pointer descriptors required by ordinary nullable descriptor arguments before call preparation.""" return tuple( FortranPointerAssignment( - f"{argument.bridge.native_name.lower()}_descriptor", + f"{argument.entrypoint.parameter_name}_descriptor", CodeExpression("null()"), ) for argument in plan.arguments if ( - argument.bridge.optional_mode in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR} - and argument.bridge.handoff_mode is not ArgumentHandoffMode.NATIVE_DESCRIPTOR - and argument.native_call_slot.value_kind == "pointer" + argument.entrypoint.optional_mode in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR} + and argument.entrypoint.handoff_mode is not ArgumentHandoffMode.NATIVE_DESCRIPTOR + and argument.projected_call_slot.value_kind == "pointer" and argument.object_kind is not ObjectKind.DERIVED_TYPE ) ) @@ -4324,7 +4538,7 @@ def _required_descriptor_initializers( return tuple( node for argument in plan.arguments - if argument.bridge.optional_mode is OptionalMode.REQUIRED_DESCRIPTOR and argument.derived_call is None + if argument.entrypoint.optional_mode is OptionalMode.REQUIRED_DESCRIPTOR and argument.derived_call is None for node in self._present_preparation(argument) ) @@ -4336,14 +4550,14 @@ def _required_descriptor_finalizers( return tuple( self._required_descriptor_finalizer(argument) for argument in plan.arguments - if argument.bridge.optional_mode is OptionalMode.REQUIRED_DESCRIPTOR - and argument.bridge.descriptor_output_role is not None + if argument.entrypoint.optional_mode is OptionalMode.REQUIRED_DESCRIPTOR + and argument.entrypoint.descriptor_output_role is not None and argument.derived_call is None ) def _required_descriptor_finalizer(self, argument: ArgumentTransferPlan) -> FortranIf: """Lower one planned required-descriptor copy-out ABI.""" - name = argument.bridge.native_name.lower() + name = argument.entrypoint.parameter_name if self._uses_allocatable_holder(argument): state = (FortranAssignment(f"bound_{name}_output", CodeExpression(f"c_loc({name}_holder)")),) return FortranIf( @@ -4357,7 +4571,7 @@ def _required_descriptor_finalizer(self, argument: ArgumentTransferPlan) -> Fort *state, ), ) - inquiry = "associated" if argument.native_call_slot.value_kind == "pointer" else "allocated" + inquiry = "associated" if argument.projected_call_slot.value_kind == "pointer" else "allocated" return FortranIf( CodeExpression(f"{inquiry}({name}_descriptor)"), body=( @@ -4371,34 +4585,28 @@ def _required_descriptor_finalizer(self, argument: ArgumentTransferPlan) -> Fort else_body=(FortranAssignment(f"bound_{name}_output_present", CodeExpression("0")),), ) - def _native_output_parameters(self, plan: FunctionPlan) -> tuple[FortranParameter, ...]: - """Return bridge ABI parameters for every typed native result slot.""" - parameters = [] - for slot in sorted(plan.native_call_slots, key=lambda item: item.native_position): - if slot.source_kind != "result": - continue - parameters.extend(self._native_output_parameters_for_slot(slot)) - return tuple(parameters) - - def _native_output_parameters_for_slot(self, slot: NativeCallSlotPlan) -> tuple[FortranParameter, ...]: - """Lower one completed result-slot kind into bridge ABI parameters.""" - if slot.scalar_descriptor is not None: - return self._scalar_descriptor_output_parameters(slot) - if self._is_owned_native_array_slot(slot): - return self._owned_native_array_output_parameters(slot) - if slot.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY, ObjectKind.DERIVED_TYPE}: - return (FortranParameter(slot.native_name.lower(), "type(c_ptr)"),) - if slot.semantic_type_name is None: - raise ValueError(f"Missing native output datatype for {slot.owner_path!r}") - scalar_type = PrimitiveScalarTypeRegistry.type_for(slot.semantic_type_name) - return (FortranParameter(slot.native_name.lower(), scalar_type.fortran_spelling),) + def _native_output_parameters_for_result(self, result: NativeEntrypointResultPlan) -> tuple[FortranParameter, ...]: + """Lower one hidden result's completed entrypoint transport.""" + if result.scalar_descriptor is not None: + return self._scalar_descriptor_output_parameters(result) + if self._is_owned_native_array_result(result): + return self._owned_native_array_output_parameters(result) + name = result.parameter_name + if name is None: + raise ValueError(f"Hidden result {result.owner_path!r} has no entrypoint parameter name") + if result.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY, ObjectKind.DERIVED_TYPE}: + return (FortranParameter(name, "type(c_ptr)"),) + scalar_type = PrimitiveScalarTypeRegistry.type_for(result.semantic_type_name) + return (FortranParameter(name, scalar_type.fortran_spelling),) - def _scalar_descriptor_output_parameters(self, slot: NativeCallSlotPlan) -> tuple[FortranParameter, ...]: + def _scalar_descriptor_output_parameters(self, result: NativeEntrypointResultPlan) -> tuple[FortranParameter, ...]: """Lower one completed rank-zero descriptor result ABI.""" - descriptor = slot.scalar_descriptor + descriptor = result.scalar_descriptor if descriptor is None: - raise ValueError(f"Scalar output {slot.owner_path!r} has no descriptor plan") - name = slot.native_name.lower() + raise ValueError(f"Scalar output {result.owner_path!r} has no descriptor plan") + name = result.parameter_name + if name is None: + raise ValueError(f"Scalar output {result.owner_path!r} has no entrypoint parameter name") parameters = [ FortranParameter(name, "type(c_ptr)"), FortranParameter(f"{name}_present", "integer(c_int)"), @@ -4407,13 +4615,15 @@ def _scalar_descriptor_output_parameters(self, slot: NativeCallSlotPlan) -> tupl parameters.append(FortranParameter(f"{name}_length", "integer(c_int64_t)")) return tuple(parameters) - def _owned_native_array_output_parameters(self, slot: NativeCallSlotPlan) -> tuple[FortranParameter, ...]: + def _owned_native_array_output_parameters(self, result: NativeEntrypointResultPlan) -> tuple[FortranParameter, ...]: """Lower one completed owned rank-positive descriptor result ABI.""" - handle = slot.native_array_handle + handle = result.native_array_handle if handle is None or handle.array.rank is None: - raise ValueError(f"Owned output {slot.owner_path!r} has no descriptor rank") - name = slot.native_name.lower() - if self._is_owned_deferred_character_slot(slot): + raise ValueError(f"Owned output {result.owner_path!r} has no descriptor rank") + name = result.parameter_name + if name is None: + raise ValueError(f"Owned output {result.owner_path!r} has no entrypoint parameter name") + if self._is_owned_deferred_character_result(result): return ( FortranParameter(name, "type(c_ptr)"), FortranParameter(f"{name}_itemsize", "integer(c_int64_t)"), @@ -4422,7 +4632,7 @@ def _owned_native_array_output_parameters(self, slot: NativeCallSlotPlan) -> tup return ( FortranParameter( name, - self._array_result_element_type(slot), + self._array_result_element_type(result), ( self._owned_native_array_descriptor_attribute(handle), self._array_dimension_attribute(handle.array.rank), @@ -4434,7 +4644,7 @@ def _owned_native_array_output_parameters(self, slot: NativeCallSlotPlan) -> tup def _native_output_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclaration, ...]: """Dispatch helper-local output storage from completed bridge data actions.""" declarations = [] - for slot in plan.native_call_slots: + for slot in self._adapter_slots(plan): if slot.source_kind != "result": continue if slot.scalar_descriptor is not None: @@ -4463,13 +4673,14 @@ def _native_output_declarations(self, plan: FunctionPlan) -> tuple[FortranDeclar ) ) continue - if slot.bridge_data_action is BridgeDataAction.DIRECT_TRANSFER: + if slot.adapter.bridge_data_action is BridgeDataAction.DIRECT_TRANSFER: continue - if slot.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION: + if slot.adapter.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION: declarations.extend(self._representation_copy_output_declarations(plan, slot)) continue raise ValueError( - f"Unsupported native-output bridge data action for {slot.owner_path!r}: {slot.bridge_data_action!r}" + f"Unsupported native-output bridge data action for {slot.owner_path!r}: " + f"{slot.adapter.bridge_data_action!r}" ) return tuple(declarations) @@ -4515,7 +4726,7 @@ def _ordinary_direct_result_declarations( @staticmethod def _direct_scalar_result_declarations(result: ResultPlan) -> tuple[FortranDeclaration, ...]: """Declare storage selected by a completed scalar direct-result ABI.""" - match result.direct_result_abi: + match result.entrypoint.direct_result_abi: case DirectResultABI.LOGICAL_LOW_BIT_INT8: return (FortranDeclaration("c_result", "logical(c_bool)"),) case DirectResultABI.NATIVE_SCALAR: @@ -4611,7 +4822,7 @@ def _derived_result_storage_names(self, plan: FunctionPlan) -> tuple[str, ...]: names.append("result_value") names.extend( f"{slot.native_name.lower()}_value" - for slot in plan.native_call_slots + for slot in self._adapter_slots(plan) if slot.source_kind == "result" and slot.object_kind is ObjectKind.DERIVED_TYPE ) return tuple(names) @@ -4628,7 +4839,7 @@ def _derived_result_execution( return success_body null_outputs = [ FortranAssignment(slot.native_name.lower(), CodeExpression("c_null_ptr")) - for slot in plan.native_call_slots + for slot in self._adapter_slots(plan) if slot.source_kind == "result" and slot.object_kind is ObjectKind.DERIVED_TYPE ] direct = self._direct_result(plan) @@ -4659,14 +4870,14 @@ def _derived_allocation_tree( def _scalar_descriptor_output_declarations( self, - slot: NativeCallSlotPlan, + slot: NativeEntrypointProjectedSlotPlan, ) -> tuple[FortranDeclaration, ...]: """Declare native and detached-copy storage for one hidden descriptor scalar.""" return self._scalar_descriptor_copy_declarations(slot, slot.native_name.lower()) def _scalar_descriptor_copy_declarations( self, - result: ResultPlan | NativeCallSlotPlan, + result: ResultPlan | NativeEntrypointProjectedSlotPlan, name: str, ) -> tuple[FortranDeclaration, ...]: """Declare helper-local storage selected by a scalar descriptor plan.""" @@ -4789,7 +5000,7 @@ def _direct_scalar_result_finalizers( result: ResultPlan, ) -> tuple[FortranAssignment | FortranCall | FortranIf, ...]: """Finalize one scalar through its completed direct-result ABI.""" - match result.direct_result_abi: + match result.entrypoint.direct_result_abi: case DirectResultABI.LOGICAL_LOW_BIT_INT8: return ( FortranAssignment( @@ -4914,7 +5125,7 @@ def _derived_direct_result_finalizers( def _representation_copy_output_declarations( self, plan: FunctionPlan, - slot: NativeCallSlotPlan, + slot: NativeEntrypointProjectedSlotPlan, ) -> tuple[FortranDeclaration, ...]: """Declare storage only for one justified representation-copy output.""" if slot.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: @@ -4970,7 +5181,7 @@ def _representation_copy_output_declarations( def _array_copy_output_declarations( self, plan: FunctionPlan, - slot: NativeCallSlotPlan, + slot: NativeEntrypointProjectedSlotPlan, ) -> tuple[FortranDeclaration, ...]: """Declare typed native and contiguous-copy storage for one hidden array.""" shape = self._array_output_shape(plan, slot) @@ -4996,8 +5207,8 @@ def _native_output_finalizers( ) -> tuple[FortranAssignment | FortranIf, ...]: """Dispatch output finalization from completed bridge data actions.""" nodes = [] - for slot in plan.native_call_slots: - if slot.source_kind != "result" or slot.bridge_data_action is BridgeDataAction.DIRECT_TRANSFER: + for slot in self._adapter_slots(plan): + if slot.source_kind != "result" or slot.adapter.bridge_data_action is BridgeDataAction.DIRECT_TRANSFER: continue if slot.scalar_descriptor is not None: nodes.extend(self._scalar_descriptor_copy_nodes(slot, slot.native_name.lower())) @@ -5024,7 +5235,7 @@ def _native_output_finalizers( ) ) continue - if slot.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION: + if slot.adapter.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION: if slot.object_kind is ObjectKind.DERIVED_TYPE: name = slot.native_name.lower() nodes.append(FortranAssignment(name, CodeExpression(f"c_loc({name}_value)"))) @@ -5032,13 +5243,14 @@ def _native_output_finalizers( nodes.extend(self._lower_native_output_representation_copy(plan, slot)) continue raise ValueError( - f"Unsupported native-output bridge data action for {slot.owner_path!r}: {slot.bridge_data_action!r}" + f"Unsupported native-output bridge data action for {slot.owner_path!r}: " + f"{slot.adapter.bridge_data_action!r}" ) return tuple(nodes) def _scalar_descriptor_copy_nodes( self, - result: ResultPlan | NativeCallSlotPlan, + result: ResultPlan | NativeEntrypointProjectedSlotPlan, name: str, ) -> tuple[FortranAssignment | FortranIf, ...]: """Copy one present scalar descriptor payload into C-owned storage.""" @@ -5108,7 +5320,7 @@ def _scalar_descriptor_copy_nodes( # Deferred-character native-array-handle result copying. def _owned_deferred_character_copy_nodes( self, - result: ResultPlan | NativeCallSlotPlan, + result: ResultPlan | NativeEntrypointProjectedSlotPlan, target_name: str, value_name: str, copy_name: str, @@ -5167,7 +5379,7 @@ def _owned_deferred_character_copy_nodes( def _lower_native_output_representation_copy( self, plan: FunctionPlan, - slot: NativeCallSlotPlan, + slot: NativeEntrypointProjectedSlotPlan, ) -> tuple[FortranAssignment | FortranIf, ...]: """Copy one native output only through the explicit policy permission.""" if slot.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: @@ -5322,7 +5534,10 @@ def _fixed_character_array_copy_nodes( ), ) - def _array_result_element_type(self, plan: ResultPlan | NativeCallSlotPlan) -> str: + def _array_result_element_type( + self, + plan: ResultPlan | NativeEntrypointResultPlan | NativeEntrypointProjectedSlotPlan, + ) -> str: """Return the completed numeric, fixed, or deferred character element type.""" if plan.datatype_family is DatatypeFamily.STRING: if plan.native_array_handle is not None and plan.array is not None and plan.array.itemsize is None: @@ -5335,7 +5550,10 @@ def _array_result_element_type(self, plan: ResultPlan | NativeCallSlotPlan) -> s raise ValueError(f"Array result {plan.owner_path!r} has no element type") return PrimitiveScalarTypeRegistry.type_for(plan.semantic_type_name).fortran_spelling - def _array_result_itemsize(self, plan: ResultPlan | NativeCallSlotPlan) -> int | None: + def _array_result_itemsize( + self, + plan: ResultPlan | NativeEntrypointProjectedSlotPlan, + ) -> int | None: """Return a character-array itemsize after object-kind dispatch.""" if plan.datatype_family is not DatatypeFamily.STRING: return None @@ -5376,7 +5594,7 @@ def _fixed_string_copy_nodes( ), ) - def _native_output_value_name(self, slot: NativeCallSlotPlan) -> str: + def _native_output_value_name(self, slot: NativeEntrypointProjectedSlotPlan) -> str: """Return the native-call expression selected for one output slot.""" name = slot.native_name.lower() if slot.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: @@ -5397,7 +5615,7 @@ def _native_output_value_name(self, slot: NativeCallSlotPlan) -> str: else name ) - def _string_output_length(self, slot: NativeCallSlotPlan) -> int: + def _string_output_length(self, slot: NativeEntrypointProjectedSlotPlan) -> int: """Return a validated fixed length for a hidden native string output; reject absent or non-positive lengths.""" if slot.character_length is None or slot.character_length <= 0: raise ValueError(f"String output {slot.owner_path!r} is missing a fixed character length") @@ -5445,7 +5663,7 @@ def _native_derived_direct_result_name(result: ResultPlan) -> str: @staticmethod def _native_scalar_direct_result_name(result: ResultPlan, result_name: str | None) -> str | None: """Select the native-call target required by a completed scalar ABI.""" - match result.direct_result_abi: + match result.entrypoint.direct_result_abi: case DirectResultABI.LOGICAL_LOW_BIT_INT8: return "c_result" case DirectResultABI.NATIVE_SCALAR: @@ -5453,8 +5671,8 @@ def _native_scalar_direct_result_name(result: ResultPlan, result_name: str | Non case _: raise ValueError(f"Scalar result {result.owner_path!r} has no completed direct-result ABI") - def _bridge_result_type(self, plan: FunctionPlan, result: ResultPlan | None = None) -> str: - """Return the C-interoperable bridge result spelling selected by the completed direct-result plan.""" + def _entrypoint_result_type(self, plan: FunctionPlan, result: ResultPlan | None = None) -> str: + """Return the C-interoperable entrypoint result spelling selected by the completed result plan.""" result = result or self._direct_result(plan) if result is None: raise ValueError(f"{plan.owner_path!r} native function has no result plan") @@ -5462,9 +5680,9 @@ def _bridge_result_type(self, plan: FunctionPlan, result: ResultPlan | None = No return "type(c_ptr)" if result.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY, ObjectKind.DERIVED_TYPE}: return "type(c_ptr)" - if result.direct_result_abi is DirectResultABI.LOGICAL_LOW_BIT_INT8: + if result.entrypoint.direct_result_abi is DirectResultABI.LOGICAL_LOW_BIT_INT8: return "integer(c_int8_t)" - if result.direct_result_abi is DirectResultABI.NATIVE_SCALAR: + if result.entrypoint.direct_result_abi is DirectResultABI.NATIVE_SCALAR: return PrimitiveScalarTypeRegistry.type_for(result.semantic_type_name).fortran_spelling raise ValueError(f"Scalar result {result.owner_path!r} has no completed direct-result ABI") @@ -5494,24 +5712,24 @@ def _owned_direct_array_result_collector_name() -> str: return "prik_collect_allocatable_array_result" @staticmethod - def _is_owned_native_array_result(result: ResultPlan) -> bool: + def _is_owned_native_array_result(result: ResultPlan | NativeEntrypointResultPlan) -> bool: """Return whether one result owns persistent standard-descriptor storage.""" handle = result.native_array_handle return handle is not None and handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE @staticmethod - def _is_owned_native_array_slot(slot: NativeCallSlotPlan) -> bool: + def _is_owned_native_array_slot(slot: NativeEntrypointProjectedSlotPlan) -> bool: """Return whether one hidden slot shares persistent descriptor storage.""" handle = slot.native_array_handle return handle is not None and handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE @classmethod - def _is_owned_deferred_character_result(cls, result: ResultPlan) -> bool: + def _is_owned_deferred_character_result(cls, result: ResultPlan | NativeEntrypointResultPlan) -> bool: """Return whether owner storage needs a runtime-width copy ABI.""" return cls._is_owned_native_array_result(result) and result.datatype_family is DatatypeFamily.STRING @classmethod - def _is_owned_deferred_character_slot(cls, slot: NativeCallSlotPlan) -> bool: + def _is_owned_deferred_character_slot(cls, slot: NativeEntrypointProjectedSlotPlan) -> bool: """Return whether a hidden owner slot needs a runtime-width copy ABI.""" return cls._is_owned_native_array_slot(slot) and slot.datatype_family is DatatypeFamily.STRING @@ -5534,6 +5752,8 @@ def _add_derived_module_uses(self, plan: ModulePlan, modules: dict[str, list[str def _add_function_module_uses(self, plan: ModulePlan, modules: dict[str, list[str]]) -> None: """Import module procedures, excluding direct type-bound invocation.""" for function in self._functions(plan): + if function.bridge is None: + continue if ( function.bridge.native_module is not None and function.bridge.native_invocation is not NativeInvocationKind.PROCEDURE @@ -5567,8 +5787,8 @@ def _add_variable_module_uses(self, plan: ModulePlan, modules: dict[str, list[st """Import only module variables with a planned getter, setter, or proxy.""" for variable in self._variables(plan): if ( - variable.bridge.getter_role is not None - or variable.bridge.setter_role is not None + variable.entrypoint.getter_role is not None + or variable.entrypoint.setter_role is not None or variable.derived is not None ): modules.setdefault(variable.bridge.native_module, []).append( @@ -5579,113 +5799,19 @@ def _derived_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: """Return namespace-owned opaque types in stable plan order.""" return tuple(derived for namespace in plan.namespaces for derived in namespace.derived_types) - def _owned_derived_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: - """Return only native types with completed wrapper-owned result storage.""" - identities = self._owned_derived_result_identities(plan) - identities.update(self._owned_derived_module_identities(plan)) - identities.update(self._constructible_class_identities(plan)) - return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) - - @staticmethod - def _constructible_class_identities(plan: ModulePlan) -> set[tuple[str, str]]: - """Return class identities whose completed constructor allocates storage.""" - return { - surface.type_identity - for namespace in plan.namespaces - for surface in namespace.classes - if surface.constructor.kind is not ClassConstructorKind.ABSENT - } - - def _owned_derived_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Return derived identities required by direct-result ownership helpers.""" - return { - result.derived.type_identity - for function in self._functions(plan) - for result in function.results - if result.derived is not None - and result.derived.release is DerivedRelease.WRAPPER_DESTROY - and result.derived.storage - not in {DerivedObjectStorage.ALLOCATABLE_HOLDER, DerivedObjectStorage.POINTER_HOLDER} - } - - def _owned_derived_module_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Return derived identities required by owned module-origin helpers.""" - return { - variable.derived.handoff.type_identity - for variable in self._variables(plan) - if variable.derived is not None and variable.derived.access is ModuleObjectAccessMechanism.VALUE_COPY - } - - def _allocatable_holder_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: - """Return types whose completed storage or call action uses a typed holder.""" - identities = self._allocatable_holder_result_identities(plan) - identities.update(self._allocatable_holder_argument_identities(plan)) - return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) - - def _allocatable_holder_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Return identities whose direct results need allocatable holder definitions.""" - return { - result.derived.type_identity - for function in self._functions(plan) - for result in function.results - if result.derived is not None and result.derived.storage is DerivedObjectStorage.ALLOCATABLE_HOLDER - } - - def _allocatable_holder_argument_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Return identities whose arguments need allocatable holder definitions.""" - return { - argument.derived.type_identity - for function in self._functions(plan) - for argument in function.arguments - if argument.derived is not None - and argument.derived_call is not None - and any( - case.access is DerivedActualAccess.ALLOCATABLE_HOLDER - for case in argument.derived_call.cases - if case.action is not DerivedCallAction.INCOMPATIBLE - ) - } - - def _pointer_holder_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: - """Return types whose results or call rows use pointer holders.""" - identities = self._pointer_holder_result_identities(plan) - identities.update(self._pointer_holder_argument_identities(plan)) - return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) - - def _pointer_holder_result_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Return identities whose direct results need pointer holder definitions.""" - return { - result.derived.type_identity - for function in self._functions(plan) - for result in function.results - if result.derived is not None and result.derived.storage is DerivedObjectStorage.POINTER_HOLDER - } - - def _pointer_holder_argument_identities(self, plan: ModulePlan) -> set[tuple[str, str]]: - """Return identities whose arguments need pointer holder definitions.""" - return { - argument.derived.type_identity - for function in self._functions(plan) - for argument in function.arguments - if argument.derived is not None - and argument.derived_call is not None - and any( - case.access is DerivedActualAccess.POINTER_HOLDER - for case in argument.derived_call.cases - if case.action is not DerivedCallAction.INCOMPATIBLE - ) - } + def _bridge_support_types( + self, + plan: ModulePlan, + owner_paths: frozenset[str], + ) -> tuple[DerivedTypePlan, ...]: + """Join one planned Fortran support inventory to derived declarations.""" + return tuple(derived for derived in self._derived_types(plan) if derived.owner_path in owner_paths) @staticmethod def _uses_allocatable_holder(argument: ArgumentTransferPlan) -> bool: """Return whether the module plan requires the allocatable holder for one native derived identity.""" return FortranBridgeGenerator._uses_holder(argument, DerivedActualAccess.ALLOCATABLE_HOLDER) - @staticmethod - def _uses_pointer_holder(argument: ArgumentTransferPlan) -> bool: - """Return whether the module plan requires the pointer holder for one native derived identity.""" - return FortranBridgeGenerator._uses_holder(argument, DerivedActualAccess.POINTER_HOLDER) - @staticmethod def _uses_holder(argument: ArgumentTransferPlan, access: DerivedActualAccess) -> bool: """Return whether one completed derived matrix includes a holder row.""" @@ -5711,7 +5837,11 @@ def _direct_field_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFunc procedure for derived in self._derived_types(plan) for field in derived.fields - for procedure in self._direct_field_procedures(derived, field) + for procedure in self._planned_support_procedures( + f"{derived.owner_path}.{field.name}", + "field:direct:", + self._direct_field_procedures(derived, field), + ) ) def _module_member_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: @@ -5720,7 +5850,11 @@ def _module_member_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFun procedure for variable in self._derived_member_proxy_variables(plan) for member in variable.derived.member_paths - for procedure in self._module_member_procedures(variable, member) + for procedure in self._planned_support_procedures( + ".".join((variable.owner_path, *member.path)), + "field:module:", + self._module_member_procedures(variable, member), + ) ) def _allocatable_holder_field_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: @@ -5729,7 +5863,11 @@ def _allocatable_holder_field_procedure_entries(self, plan: ModulePlan) -> tuple procedure for derived in self._allocatable_holder_field_types(plan) for field in derived.fields - for procedure in self._allocatable_holder_field_procedures(derived, field) + for procedure in self._planned_support_procedures( + f"{derived.owner_path}.{field.name}", + "field:allocatable:", + self._allocatable_holder_field_procedures(derived, field), + ) ) def _pointer_holder_field_procedure_entries(self, plan: ModulePlan) -> tuple[FortranFunction, ...]: @@ -5738,34 +5876,40 @@ def _pointer_holder_field_procedure_entries(self, plan: ModulePlan) -> tuple[For procedure for derived in self._pointer_holder_field_types(plan) for field in derived.fields - for procedure in self._pointer_holder_field_procedures(derived, field) + for procedure in self._planned_support_procedures( + f"{derived.owner_path}.{field.name}", + "field:pointer:", + self._pointer_holder_field_procedures(derived, field), + ) ) + def _planned_support_procedures( + self, + owner_path: str, + role_prefix: str, + candidates: tuple[FortranFunction, ...], + ) -> tuple[FortranFunction, ...]: + """Select adapter bodies in the operation order fixed by planning.""" + by_symbol = {candidate.name: candidate for candidate in candidates} + operations = self._generated_support_procedure_entrypoints_for(owner_path, role_prefix) + missing = tuple(operation.symbol_name for operation in operations if operation.symbol_name not in by_symbol) + if missing: + raise ValueError(f"No Fortran body for planned generated support procedures: {missing!r}") + return tuple(by_symbol[operation.symbol_name] for operation in operations) + def _allocatable_holder_field_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: - """Return holders that can cross back to Python and need field operations.""" - identities = self._allocatable_holder_result_identities(plan) - identities.update( - argument.derived.type_identity - for function in self._functions(plan) - for argument in function.arguments - if argument.derived is not None - and argument.bridge.descriptor_output_role is not None - and self._uses_allocatable_holder(argument) + """Return the planned allocatable-holder field-support inventory.""" + return self._bridge_support_types( + plan, + self._bridge_allocatable_holder_field_owner_paths, ) - return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) def _pointer_holder_field_types(self, plan: ModulePlan) -> tuple[DerivedTypePlan, ...]: - """Return pointer holders that can cross back to Python and need fields.""" - identities = self._pointer_holder_result_identities(plan) - identities.update( - argument.derived.type_identity - for function in self._functions(plan) - for argument in function.arguments - if argument.derived is not None - and argument.bridge.descriptor_output_role is not None - and self._uses_pointer_holder(argument) + """Return the planned pointer-holder field-support inventory.""" + return self._bridge_support_types( + plan, + self._bridge_pointer_holder_field_owner_paths, ) - return tuple(derived for derived in self._derived_types(plan) if derived.type_identity in identities) def _allocatable_holder_field_procedures( self, @@ -6657,8 +6801,10 @@ def _derived_field_symbol(derived: DerivedTypePlan, field: DerivedFieldPlan) -> return f"{derived.backend_symbol}_{field.name}".casefold() def _derived_field_bridge_name(self, derived: DerivedTypePlan, field: DerivedFieldPlan, action: str) -> str: - """Return the exported bridge symbol for one direct derived-field action.""" - return f"bind_c_prik_field_{self._derived_field_symbol(derived, field)}_{action}" + """Return the planner-owned direct-field symbol.""" + return self._generated_support_procedure_entrypoint( + f"{derived.owner_path}.{field.name}", f"field:direct:{action}" + ).symbol_name def _allocatable_holder_field_bridge_name( self, @@ -6666,8 +6812,10 @@ def _allocatable_holder_field_bridge_name( field: DerivedFieldPlan, action: str, ) -> str: - """Return the exported bridge symbol for one allocatable-holder field action.""" - return f"bind_c_prik_allocatable_holder_field_{self._derived_field_symbol(derived, field)}_{action}" + """Return the planner-owned allocatable-holder field symbol.""" + return self._generated_support_procedure_entrypoint( + f"{derived.owner_path}.{field.name}", f"field:allocatable:{action}" + ).symbol_name def _pointer_holder_field_bridge_name( self, @@ -6675,8 +6823,10 @@ def _pointer_holder_field_bridge_name( field: DerivedFieldPlan, action: str, ) -> str: - """Return the exported bridge symbol for one pointer-holder field action.""" - return f"bind_c_prik_pointer_holder_field_{self._derived_field_symbol(derived, field)}_{action}" + """Return the planner-owned pointer-holder field symbol.""" + return self._generated_support_procedure_entrypoint( + f"{derived.owner_path}.{field.name}", f"field:pointer:{action}" + ).symbol_name def _derived_field_callback_interface_name( self, @@ -6692,8 +6842,11 @@ def _derived_handle_bridge_name( field: DerivedFieldPlan, operation: NativeArrayOperation, ) -> str: - """Return the exported bridge symbol for one direct native-array-handle field operation.""" - return f"bind_c_prik_field_handle_{self._derived_field_symbol(derived, field)}_{operation.value}" + """Return the planner-owned direct-field handle symbol.""" + return self._generated_support_procedure_entrypoint( + f"{derived.owner_path}.{field.name}", + f"field:direct:handle:{operation.value}", + ).symbol_name def _derived_handle_callback_interface_name( self, @@ -6714,8 +6867,10 @@ def _module_member_bridge_name( member: DerivedMemberPathPlan, action: str, ) -> str: - """Return the exported bridge symbol for one module-member action.""" - return f"bind_c_prik_module_field_{self._module_member_symbol(variable, member)}_{action}" + """Return the planner-owned module-member symbol.""" + return self._generated_support_procedure_entrypoint( + ".".join((variable.owner_path, *member.path)), f"field:module:{action}" + ).symbol_name def _module_member_callback_interface_name( self, @@ -6731,8 +6886,11 @@ def _module_member_handle_bridge_name( member: DerivedMemberPathPlan, operation: NativeArrayOperation, ) -> str: - """Return the exported bridge symbol for one module native-array-handle operation.""" - return f"bind_c_prik_module_field_handle_{self._module_member_symbol(variable, member)}_{operation.value}" + """Return the planner-owned module-member handle symbol.""" + return self._generated_support_procedure_entrypoint( + ".".join((variable.owner_path, *member.path)), + f"field:module:handle:{operation.value}", + ).symbol_name def _module_member_handle_callback_interface_name( self, @@ -6779,7 +6937,7 @@ def _class_constructor_procedures(self, plan: ModulePlan) -> tuple[FortranFuncti self._class_constructor_procedure(surface, derived_by_identity[surface.type_identity]) for namespace in plan.namespaces for surface in namespace.classes - if surface.constructor.kind is not ClassConstructorKind.ABSENT + if self._has_generated_support_procedure_entrypoint(surface.owner_path, "class:create") ) def _class_constructor_procedure( @@ -6814,10 +6972,9 @@ def _class_constructor_procedure( ), ) - @staticmethod - def _class_create_bridge_name(surface: ClassSurfacePlan) -> str: - """Return the exported bridge symbol for a class constructor allocation procedure.""" - return f"bind_c_prik_create_{surface.type_identity[1].casefold()}" + def _class_create_bridge_name(self, surface: ClassSurfacePlan) -> str: + """Return the planner-owned class-constructor symbol.""" + return self._generated_support_procedure_entrypoint(surface.owner_path, "class:create").symbol_name def _allocatable_holder_destroy_procedure(self, derived: DerivedTypePlan) -> FortranFunction: """Destroy one wrapper-owned holder and its allocatable component.""" @@ -6893,10 +7050,11 @@ def _derived_native_alias(type_name: str) -> str: """Return the imported native alias used to disambiguate one derived type in bridge source.""" return f"prik_type_{type_name.casefold()}" - @staticmethod - def _derived_destroy_bridge_name(type_name: str) -> str: - """Return the exported bridge symbol for destroying one wrapper-owned native derived object.""" - return f"bind_c_prik_destroy_{type_name.casefold()}" + def _derived_destroy_bridge_name(self, type_name: str) -> str: + """Return the planner-owned derived-destroy symbol.""" + return self._generated_support_procedure_entrypoint( + self._derived_owner_paths[type_name], "derived:destroy" + ).symbol_name @staticmethod def _allocatable_holder_type_name(type_name: str) -> str: @@ -6908,37 +7066,41 @@ def _pointer_holder_type_name(type_name: str) -> str: """Return the internal Fortran type name for a pointer derived holder.""" return f"prik_{type_name.casefold()}_pointer_holder" - @staticmethod - def _allocatable_holder_destroy_bridge_name(type_name: str) -> str: - """Return the exported bridge symbol for destroying an allocatable holder.""" - return f"bind_c_prik_destroy_{type_name.casefold()}_allocatable_holder" - - @staticmethod - def _allocatable_holder_presence_bridge_name(type_name: str) -> str: - """Return the exported bridge symbol for inquiring an allocatable holder payload.""" - return f"bind_c_prik_{type_name.casefold()}_allocatable_holder_present" - - @staticmethod - def _pointer_holder_destroy_bridge_name(type_name: str) -> str: - """Return the exported bridge symbol for destroying a pointer holder.""" - return f"bind_c_prik_destroy_{type_name.casefold()}_pointer_holder" - - @staticmethod - def _pointer_holder_presence_bridge_name(type_name: str) -> str: - """Return the exported bridge symbol for inquiring a pointer holder payload.""" - return f"bind_c_prik_{type_name.casefold()}_pointer_holder_present" - - @staticmethod - def _module_derived_presence_bridge_name(plan: ModuleVariablePlan) -> str: - """Return the exported bridge symbol for one nullable derived module variable's presence inquiry.""" - return f"bind_c_prik_module_{plan.symbol_name.casefold()}_present" + def _allocatable_holder_destroy_bridge_name(self, type_name: str) -> str: + """Return the planner-owned allocatable-holder destroy symbol.""" + return self._generated_support_procedure_entrypoint( + self._derived_owner_paths[type_name], "holder:allocatable:destroy" + ).symbol_name + + def _allocatable_holder_presence_bridge_name(self, type_name: str) -> str: + """Return the planner-owned allocatable-holder presence symbol.""" + return self._generated_support_procedure_entrypoint( + self._derived_owner_paths[type_name], "holder:allocatable:present" + ).symbol_name + + def _pointer_holder_destroy_bridge_name(self, type_name: str) -> str: + """Return the planner-owned pointer-holder destroy symbol.""" + return self._generated_support_procedure_entrypoint( + self._derived_owner_paths[type_name], "holder:pointer:destroy" + ).symbol_name + + def _pointer_holder_presence_bridge_name(self, type_name: str) -> str: + """Return the planner-owned pointer-holder presence symbol.""" + return self._generated_support_procedure_entrypoint( + self._derived_owner_paths[type_name], "holder:pointer:present" + ).symbol_name + + def _module_derived_presence_bridge_name(self, plan: ModuleVariablePlan) -> str: + """Return the planner-owned nullable module-derived presence symbol.""" + return self._generated_support_procedure_entrypoint(plan.owner_path, "module:derived:present").symbol_name def _external_interfaces(self, plan: ModulePlan) -> tuple[FortranInterface, ...]: """Declare ordinary standalone wrapper targets with explicit interfaces.""" native_procedures = tuple( self._external_interface_procedure(function) for function in self._functions(plan) - if function.bridge.external_declaration is ExternalDeclarationMode.EXPLICIT_INTERFACE + if function.bridge is not None + and function.bridge.external_declaration is ExternalDeclarationMode.EXPLICIT_INTERFACE ) return (FortranInterface(native_procedures),) if native_procedures else () @@ -7137,50 +7299,23 @@ def _callback_c_interface( bind_name: str, ) -> FortranInterfaceProcedure: """Declare the flattened C ABI implemented by one Python trampoline.""" - result = callback.result.transfer - is_subroutine = callback.result.action is CallbackResultAction.RETURN_VOID + operation = callback.entrypoint.support_procedure + if operation.implementation_owner is not GeneratedSupportProcedureImplementationOwner.BINDING: + raise ValueError(f"Callback trampoline {operation.key!r} is not binding-owned") + is_subroutine = operation.signature.result.kind is NativeEntrypointABIValueKind.VOID return FortranInterfaceProcedure( name=name, imports=self._callback_c_imports(callback), parameters=tuple( - parameter for transfer in callback.arguments for parameter in self._callback_c_parameters(transfer) + self._support_procedure_fortran_parameter(parameter) for parameter in operation.signature.parameters ), result_name=None if is_subroutine else "callback_result", - result_type=None if is_subroutine else self._callback_c_result_type(result), + result_type=(None if is_subroutine else self._support_procedure_fortran_type(operation.signature.result)), is_subroutine=is_subroutine, bind_name=bind_name, bind_c=True, ) - def _callback_c_parameters(self, transfer: CallbackTransferPlan) -> tuple[FortranParameter, ...]: - """Flatten one callback transfer into interoperable C parameters.""" - base = self._callback_parameter_base_name(transfer) - if transfer.abi is CallbackABIKind.VALUE: - return ( - FortranParameter( - base, - PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name).fortran_spelling, - ("value",), - ), - ) - parameters = [FortranParameter(f"{base}_data", "type(c_ptr)", ("value",))] - if transfer.abi is CallbackABIKind.DATA_AND_SHAPE: - parameters.extend( - FortranParameter(f"{base}_extent_{axis}", "integer(c_int64_t)", ("value",)) - for axis in range(transfer.rank) - ) - elif transfer.abi is CallbackABIKind.DATA_AND_LENGTH: - parameters.append(FortranParameter(f"{base}_length", "integer(c_int64_t)", ("value",))) - return tuple(parameters) - - def _callback_c_result_type(self, transfer: CallbackTransferPlan | None) -> str: - """Return the interoperable C trampoline result type.""" - if transfer is None: - raise ValueError("Callback function result is missing its transfer plan") - if transfer.abi is CallbackABIKind.VALUE: - return PrimitiveScalarTypeRegistry.type_for(transfer.semantic_type_name).fortran_spelling - return "type(c_ptr)" - def _callback_c_imports(self, callback: CallbackHandoffPlan) -> tuple[str, ...]: """Import only ISO C kinds referenced by the flattened interface.""" imports = [] @@ -7434,7 +7569,8 @@ def _needs_allocator_interface(self, plan: ModulePlan) -> bool: def _needs_module_getter_allocator(self, plan: ModulePlan) -> bool: """Return whether a nullable scalar descriptor getter copies one value.""" return any( - variable.bridge.getter_action is ModuleGetterAction.NULLABLE_SNAPSHOT for variable in self._variables(plan) + variable.bridge.native_getter_action is ModuleGetterAction.NULLABLE_SNAPSHOT + for variable in self._variables(plan) ) def _needs_function_copy_allocator(self, plan: ModulePlan) -> bool: @@ -7456,13 +7592,13 @@ def _native_result_slots_need_allocator(self, function: FunctionPlan) -> bool: """Return whether one hidden native result is an array or string copy.""" return any( slot.scalar_descriptor is not None or slot.object_kind in {ObjectKind.STRING, ObjectKind.NUMPY_ARRAY} - for slot in function.native_call_slots + for slot in self._adapter_slots(function) if slot.source_kind == "result" ) def _external_interface_procedure(self, plan: FunctionPlan) -> FortranInterfaceProcedure: """Declare one standalone native target from its completed function plan.""" - slots = tuple(sorted(plan.native_call_slots, key=lambda item: item.native_position)) + slots = self._adapter_slots(plan) arguments = {argument.owner_path: argument for argument in plan.arguments} parameters = tuple(self._external_interface_slot_parameter(plan, slot, arguments) for slot in slots) result_name, result_type, direct_result = self._external_interface_result(plan) @@ -7490,7 +7626,7 @@ def _external_interface_result( def _external_interface_imports( self, plan: FunctionPlan, - slots: tuple[NativeCallSlotPlan, ...], + slots: tuple[NativeEntrypointProjectedSlotPlan, ...], direct_result: ResultPlan | None, ) -> tuple[str, ...]: """Collect type and declaration-callable symbols visible in the interface body.""" @@ -7506,7 +7642,7 @@ def _external_interface_imports( @staticmethod def _external_interface_parameter_declarations( - slots: tuple[NativeCallSlotPlan, ...], + slots: tuple[NativeEntrypointProjectedSlotPlan, ...], parameters: tuple[FortranParameter, ...], ) -> tuple[FortranParameter, ...]: """Declare extent providers first without changing native ABI order.""" @@ -7537,7 +7673,7 @@ def _external_interface_parameter_declarations( def _external_interface_slot_parameter( self, plan: FunctionPlan, - slot: NativeCallSlotPlan, + slot: NativeEntrypointProjectedSlotPlan, arguments: dict[str, ArgumentTransferPlan], ) -> FortranParameter: """Declare one external dummy from its completed ordered ABI slot.""" @@ -7555,7 +7691,7 @@ def _external_interface_slot_parameter( def _external_interface_result_parameter( self, plan: FunctionPlan, - slot: NativeCallSlotPlan, + slot: NativeEntrypointProjectedSlotPlan, ) -> FortranParameter: """Declare one hidden output dummy from completed result-slot policy.""" if slot.scalar_descriptor is not None: @@ -7573,7 +7709,9 @@ def _external_interface_result_parameter( raise ValueError(f"Unsupported external hidden output {slot.owner_path!r}") @staticmethod - def _external_interface_scalar_descriptor_result_parameter(slot: NativeCallSlotPlan) -> FortranParameter: + def _external_interface_scalar_descriptor_result_parameter( + slot: NativeEntrypointProjectedSlotPlan, + ) -> FortranParameter: """Declare one completed allocatable or pointer scalar output.""" descriptor = slot.scalar_descriptor if descriptor is None: @@ -7587,7 +7725,9 @@ def _external_interface_scalar_descriptor_result_parameter(slot: NativeCallSlotP return FortranParameter(slot.native_name.lower(), scalar_type.fortran_spelling, (attribute,)) @staticmethod - def _external_interface_scalar_result_parameter(slot: NativeCallSlotPlan) -> FortranParameter: + def _external_interface_scalar_result_parameter( + slot: NativeEntrypointProjectedSlotPlan, + ) -> FortranParameter: """Declare one completed primitive scalar output.""" if slot.semantic_type_name is None: raise ValueError(f"Scalar output {slot.owner_path!r} has no element type") @@ -7595,7 +7735,9 @@ def _external_interface_scalar_result_parameter(slot: NativeCallSlotPlan) -> For return FortranParameter(slot.native_name.lower(), scalar_type.fortran_spelling) @staticmethod - def _external_interface_string_result_parameter(slot: NativeCallSlotPlan) -> FortranParameter: + def _external_interface_string_result_parameter( + slot: NativeEntrypointProjectedSlotPlan, + ) -> FortranParameter: """Declare one completed fixed or assumed-length string output.""" length = "*" if slot.character_length is None else str(slot.character_length) return FortranParameter(slot.native_name.lower(), f"character(kind=c_char, len={length})") @@ -7603,7 +7745,7 @@ def _external_interface_string_result_parameter(slot: NativeCallSlotPlan) -> For def _external_interface_array_result_parameter( self, plan: FunctionPlan, - slot: NativeCallSlotPlan, + slot: NativeEntrypointProjectedSlotPlan, ) -> FortranParameter: """Declare one completed ordinary or descriptor array output.""" if slot.array is None: @@ -7625,7 +7767,10 @@ def _external_interface_array_result_parameter( tuple(attributes), ) - def _external_interface_derived_result_parameter(self, slot: NativeCallSlotPlan) -> FortranParameter: + def _external_interface_derived_result_parameter( + self, + slot: NativeEntrypointProjectedSlotPlan, + ) -> FortranParameter: """Declare one completed scalar-derived output.""" if slot.derived is None: raise ValueError(f"Derived output {slot.owner_path!r} has no handoff plan") @@ -7640,7 +7785,9 @@ def _external_interface_derived_result_parameter(self, slot: NativeCallSlotPlan) ) @staticmethod - def _external_interface_literal_parameter(slot: NativeCallSlotPlan) -> FortranParameter: + def _external_interface_literal_parameter( + slot: NativeEntrypointProjectedSlotPlan, + ) -> FortranParameter: """Declare one hidden literal dummy from its completed scalar type.""" if slot.semantic_type_name is None: raise ValueError(f"External literal slot {slot.owner_path!r} has no scalar type") @@ -7686,12 +7833,12 @@ def _external_interface_parameter( name: str | None = None, ) -> FortranParameter: """Return the native external dummy declaration for one planned argument.""" - parameter_name = name or argument.bridge.native_name.lower() + parameter_name = name or argument.bridge.native_name.casefold() if argument.callback is not None: return FortranParameter(parameter_name, "external") attributes = ( ("optional",) - if argument.bridge.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} + if argument.entrypoint.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} else () ) if argument.object_kind is ObjectKind.NUMPY_ARRAY: @@ -7702,7 +7849,7 @@ def _external_interface_parameter( attributes, ) if argument.object_kind is ObjectKind.STRING: - length = argument.native_call_slot.character_length + length = argument.projected_call_slot.character_length length_text = "*" if length is None else str(length) return FortranParameter( parameter_name, @@ -7783,7 +7930,11 @@ def _array_result_shape(self, plan: FunctionPlan, result: ResultPlan) -> tuple[s raise ValueError(f"Array result {result.owner_path!r} has no shape plan") return self._array_shape_from_roles(result.array, plan) - def _array_output_shape(self, plan: FunctionPlan, slot: NativeCallSlotPlan) -> tuple[str, ...]: + def _array_output_shape( + self, + plan: FunctionPlan, + slot: NativeEntrypointProjectedSlotPlan, + ) -> tuple[str, ...]: """Lower one hidden-output shape through the plan's native scalar roles.""" if slot.array is None: raise ValueError(f"Array output {slot.owner_path!r} has no shape plan") @@ -7800,10 +7951,12 @@ def _array_shape_from_roles(self, array: ArrayHandoffPlan, plan: FunctionPlan) - @staticmethod def _array_shape_role_names(plan: FunctionPlan) -> dict[str, str]: """Map planned scalar, extent, and callable roles to bridge spellings.""" - role_names = {argument.binding.handoff_role: argument.bridge.native_name.lower() for argument in plan.arguments} + role_names = { + argument.entrypoint.handoff_role: argument.entrypoint.parameter_name for argument in plan.arguments + } role_names.update( { - role: f"{argument.bridge.native_name.lower()}_extent_{axis}" + role: f"{argument.entrypoint.parameter_name}_extent_{axis}" for argument in plan.arguments if argument.array is not None for axis, role in enumerate(argument.array.extent_roles) @@ -7860,7 +8013,7 @@ def _shape_role_substitutions( def _has_optional_arguments(self, plan: FunctionPlan) -> bool: """Return whether a function plan contains a nullable-value or descriptor optional argument.""" return any( - argument.bridge.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} + argument.entrypoint.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} for argument in plan.arguments ) @@ -7872,17 +8025,17 @@ def _literal_expression(self, value: object) -> str: return f"({value.real}, {value.imag})" return str(value) - def _bridge_function_name(self, plan: FunctionPlan) -> str: - """Return the stable exported bridge name for one function plan.""" - return f"bind_c_{plan.symbol_name}" + def _entrypoint_function_name(self, plan: FunctionPlan) -> str: + """Return the shared C-ABI symbol implemented by this adapter.""" + return plan.entrypoint.symbol_name def _module_bridge_getter_name(self, plan: ModuleVariablePlan) -> str: - """Return the stable exported getter bridge name for one module variable.""" - return f"bind_c_get_{plan.symbol_name}" + """Return the shared module-variable getter entrypoint symbol.""" + return self._generated_support_procedure_entrypoint(plan.owner_path, "module:get").symbol_name def _module_bridge_setter_name(self, plan: ModuleVariablePlan) -> str: - """Return the stable exported setter bridge name for one module variable.""" - return f"bind_c_set_{plan.symbol_name}" + """Return the shared module-variable setter entrypoint symbol.""" + return self._generated_support_procedure_entrypoint(plan.owner_path, "module:set").symbol_name def _native_function_name(self, plan: FunctionPlan) -> str: """Return the in-module alias or standalone symbol selected for the native procedure.""" diff --git a/prik/codegen/primitive_scalar_types.py b/prik/codegen/primitive_scalar_types.py index fa3dd3fe8..432994dc0 100644 --- a/prik/codegen/primitive_scalar_types.py +++ b/prik/codegen/primitive_scalar_types.py @@ -128,6 +128,17 @@ class PrimitiveScalarTypeRegistry: python_module_result_kind="numpy", cfi_type_spelling="CFI_type_int64_t", ), + "SizeT": BackendScalarType( + semantic_name="SizeT", + c_spelling="size_t", + fortran_spelling="integer(c_size_t)", + python_parse_unit="O", + numpy_type_macro="NPY_UINTP", + python_result_kind="numpy", + python_type_name=NumpyDtypeRegistry.expression_for("SizeT"), + python_module_result_kind="numpy", + cfi_type_spelling="CFI_type_size_t", + ), "Float32": BackendScalarType( semantic_name="Float32", c_spelling="float", diff --git a/prik/contracts/__init__.py b/prik/contracts/__init__.py index d7142acee..f7f56674a 100644 --- a/prik/contracts/__init__.py +++ b/prik/contracts/__init__.py @@ -235,6 +235,7 @@ def apply(target): bind = _decorator nogil = _decorator +native_abi = _decorator native_call = _decorator native_type = _decorator overload = _decorator @@ -333,6 +334,7 @@ def apply(target): "WrappedType", "bind", "nogil", + "native_abi", "native_call", "native_type", "overload", diff --git a/prik/parsers/c/lexer.py b/prik/parsers/c/lexer.py index baeee7ebe..dc9badc2a 100644 --- a/prik/parsers/c/lexer.py +++ b/prik/parsers/c/lexer.py @@ -240,15 +240,17 @@ def _blank_preprocessor_directives(source: str) -> str: return "".join(out_lines) -def _scan_code_states(text: str): +def _scan_code_states(text: str) -> tuple[tuple[int, str, tuple[str, ...], str], ...]: + """Return lexical state before each source character in input order.""" state = "normal" quote = "" escaped = False stack: list[str] = [] + scanned = [] for index, char in enumerate(text): if state in {"string", "char"}: - yield index, char, tuple(stack), state + scanned.append((index, char, tuple(stack), state)) if escaped: escaped = False continue @@ -260,7 +262,7 @@ def _scan_code_states(text: str): quote = "" continue - yield index, char, tuple(stack), state + scanned.append((index, char, tuple(stack), state)) if char in {'"', "'"}: state = "string" if char == '"' else "char" @@ -270,6 +272,7 @@ def _scan_code_states(text: str): stack.append(char) elif char in _BRACKET_CLOSERS and stack and stack[-1] == _BRACKET_CLOSERS[char]: stack.pop() + return tuple(scanned) def top_level_split_with_offsets(text: str, delimiter: str = ",") -> list[tuple[str, int]]: diff --git a/prik/parsers/fortran/parser.py b/prik/parsers/fortran/parser.py index a2b18bbcf..146bd3465 100644 --- a/prik/parsers/fortran/parser.py +++ b/prik/parsers/fortran/parser.py @@ -1965,13 +1965,14 @@ def _resolve_file_compile_time_facts(self, units: _ParsedFileUnits) -> None: self._resolve_derived_type_compile_time_facts(derived_type, symbols) @staticmethod - def _helper_file_procedures(units: _ParsedFileUnits): - """Yield file procedures in their established resolution order.""" - yield from units.procedures + def _helper_file_procedures(units: _ParsedFileUnits) -> tuple[FortranProcedureSignature, ...]: + """Return file procedures in their established resolution order.""" + procedures = list(units.procedures) for module in units.modules: - yield from module.procedures + procedures.extend(module.procedures) for submodule in units.submodules: - yield from submodule.procedures + procedures.extend(submodule.procedures) + return tuple(procedures) def _helper_build_fortran_file( self, @@ -2160,32 +2161,32 @@ def _build_project_compile_time_symbols(self, parsed_files: list[FortranFile]) - return self._build_compile_time_symbols(modules, submodules) @staticmethod - def _project_file_derived_types(parsed_file: FortranFile): - """Yield every derived type owned by one parsed project file. + def _project_file_derived_types(parsed_file: FortranFile) -> tuple[FortranDerivedType, ...]: + """Return every derived type owned by one parsed project file. For example, a file-level type followed by types inside a module and a - submodule is yielded in that same ownership order. The iterator lets - project resolution cover all type owners without constructing another - registry or duplicating nested loops. + submodule is returned in that same ownership order. """ - yield from parsed_file.derived_types + derived_types = list(parsed_file.derived_types) for module in (*parsed_file.modules, *parsed_file.submodules): - yield from module.derived_types + derived_types.extend(module.derived_types) + return tuple(derived_types) @staticmethod - def _helper_project_file_procedures(parsed_file: FortranFile): - """Yield direct and interface procedures in project resolution order.""" - yield from parsed_file.procedures + def _helper_project_file_procedures(parsed_file: FortranFile) -> tuple[FortranProcedureSignature, ...]: + """Return direct and interface procedures in project resolution order.""" + procedures = list(parsed_file.procedures) for interface in parsed_file.interfaces: - yield from interface.procedures + procedures.extend(interface.procedures) for module in parsed_file.modules: - yield from module.procedures + procedures.extend(module.procedures) for interface in module.interfaces: - yield from interface.procedures + procedures.extend(interface.procedures) for submodule in parsed_file.submodules: - yield from submodule.procedures + procedures.extend(submodule.procedures) for interface in submodule.interfaces: - yield from interface.procedures + procedures.extend(interface.procedures) + return tuple(procedures) def _helper_index_project_file(self, project: FortranProject, parsed_file: FortranFile) -> None: """Add one parsed file's public models to project registries.""" diff --git a/prik/pipeline/build.py b/prik/pipeline/build.py index a9564f517..716f814bb 100644 --- a/prik/pipeline/build.py +++ b/prik/pipeline/build.py @@ -55,7 +55,7 @@ SemanticModule, SemanticPrototype, SemanticVariable, - _iter_module_semantic_types, + _module_semantic_types, ) from prik.semantics.native_contract import NATIVE_CONTRACT_PREPARED_METADATA, validate_pyi_native_contract from prik.policy.native_array_handles import ( @@ -65,13 +65,13 @@ from prik.policy.completion import complete_semantic_policies from prik.pipeline.pyi import _PyiSemanticModuleCache from prik.semantics.pyi_metadata import PYI_LOADED_METADATA -from prik.planning import WrapperPlanner +from prik.planning import NativeGeneratedCodeGroupPlan, WrapperPlanner from prik.semantics.scalar_types import boolean_storage_bits, is_boolean_semantic_type_name _DEFAULT_BUILD_DIR_NAME = "__prik__" _BUILD_MANIFEST_NAME = "prik-build.json" -_BUILD_MANIFEST_SCHEMA_VERSION = 2 +_BUILD_MANIFEST_SCHEMA_VERSION = 3 _FORTRAN_SOURCE_SUFFIXES = {".f", ".f03", ".f08", ".f77", ".f90", ".f95", ".for", ".ftn"} _C_SOURCE_SUFFIXES = {".c"} _NATIVE_PATH_LINK_KINDS = frozenset({"object", "archive", "shared_library"}) @@ -372,6 +372,7 @@ class WrapperBuildResult: native_build_plan: NativeBuildPlan = field(default_factory=NativeBuildPlan) build_manifest: Path | None = None manifest: dict[str, object] | None = None + native_generated_code_groups: tuple[NativeGeneratedCodeGroupPlan, ...] = () def import_module(self) -> ModuleType: """Import and return this result's built extension module. @@ -436,6 +437,15 @@ def to_dict(self) -> dict[str, object]: "native_build_plan": self.native_build_plan.to_dict(), "build_manifest": str(self.build_manifest) if self.build_manifest is not None else None, "manifest": self.manifest, + "native_generated_code_groups": [ + { + "kind": group.kind.value, + "language": group.language, + "member_keys": list(group.member_keys), + "source_paths": list(group.source_paths), + } + for group in self.native_generated_code_groups + ], } @@ -676,9 +686,18 @@ def _generated_wrapper_object_stages( def _generated_wrapper_link_language( bridge_objects: tuple[ObjectFile, ...], binding_objects: tuple[ObjectFile, ...], + *, + native_objects: tuple[ObjectFile, ...] = (), + required_languages: tuple[str, ...] = (), ) -> str: - """Return the linker language for generated wrapper sources.""" - if bridge_objects: + """Return the linker language required by every generated and native input.""" + languages = { + *required_languages, + *(item.language for item in native_objects), + *(item.language for item in bridge_objects), + *(item.language for item in binding_objects), + } + if "fortran" in languages: return "fortran" if not binding_objects: raise ValueError("Generated wrapper must include at least one binding source") @@ -865,7 +884,12 @@ def _build_generated_wrapper_extension( shared_library = compiler.link_extension( module_name=rendered.module_name, output_dir=shared_output_path, - language=_generated_wrapper_link_language(bridge_objects, binding_objects), + language=_generated_wrapper_link_language( + bridge_objects, + binding_objects, + native_objects=tuple(native_dependencies), + required_languages=rendered.required_link_languages, + ), objects=(*tuple(native_dependencies), *bridge_objects, *binding_objects), link_args=tuple(native_link_args), library_dirs=resolved_native_build_plan.library_dirs, @@ -892,6 +916,7 @@ def _build_generated_wrapper_extension( shared_library=shared_library, ), native_build_plan=resolved_native_build_plan, + native_generated_code_groups=rendered.native_generated_code_groups, ) @@ -2055,6 +2080,22 @@ def _manifest_native_array_requirements(requirements: NativeArrayBuildRequiremen } +def _manifest_generated_wrapper(result: WrapperBuildResult, *, base: Path) -> dict[str, object]: + """Serialize physical sources and independently planned native membership.""" + return { + "sources": [_manifest_path(path, base=base) for path in result.generated_sources], + "native_code_groups": [ + { + "kind": group.kind.value, + "language": group.language, + "member_keys": list(group.member_keys), + "source_paths": list(group.source_paths), + } + for group in result.native_generated_code_groups + ], + } + + def _pyi_build_manifest( *, bundle: _PyiContractBundle, @@ -2070,6 +2111,7 @@ def _pyi_build_manifest( wrapper_c_flags: tuple[str, ...], native_build_plan: NativeBuildPlan, native_array_build_requirements: NativeArrayBuildRequirements, + generated_wrapper: dict[str, object], manifest_dir: Path, ) -> dict[str, object]: """Build the complete in-memory manifest for a semantic ``.pyi`` build. @@ -2103,6 +2145,7 @@ def _pyi_build_manifest( "position_independent_code": True, }, "native_array_build_requirements": _manifest_native_array_requirements(native_array_build_requirements), + "generated_wrapper": generated_wrapper, "native_build_plan": _manifest_native_plan(native_build_plan, base=manifest_dir), } @@ -2146,6 +2189,7 @@ def _with_pyi_manifest( wrapper_c_flags=wrapper_c_flags, native_build_plan=result.native_build_plan, native_array_build_requirements=native_array_build_requirements, + generated_wrapper=_manifest_generated_wrapper(result, base=result.output_dir), manifest_dir=result.output_dir, ) return replace(result, manifest=manifest) @@ -2655,7 +2699,7 @@ def _complete_pyi_fortran_boolean_types( boolean_types = [ semantic_type for module in modules - for semantic_type in _iter_module_semantic_types(module) + for semantic_type in _module_semantic_types(module) if is_boolean_semantic_type_name(semantic_type.name) ] if not boolean_types: diff --git a/prik/pipeline/pyi.py b/prik/pipeline/pyi.py index 109385290..49e0315bd 100644 --- a/prik/pipeline/pyi.py +++ b/prik/pipeline/pyi.py @@ -17,7 +17,7 @@ from prik.parsers.pyi import parse_pyi_text from prik.policy.completion import complete_semantic_policies from prik.printers.pyi import emit_module -from prik.semantics.models import EXTERNAL_TYPE_REF_METADATA, SemanticClass, SemanticModule, _iter_module_semantic_types +from prik.semantics.models import EXTERNAL_TYPE_REF_METADATA, SemanticClass, SemanticModule, _module_semantic_types from prik.semantics.pyi_metadata import PYI_LOADED_METADATA from prik.semantics.pyi2ir import convert_pyi_to_ir, reconcile_external_type_refs @@ -76,7 +76,7 @@ def opaque_dependency_modules( } dependencies: dict[str, dict[str, str | None]] = {} for module in source_modules: - for semantic_type in _iter_module_semantic_types(module): + for semantic_type in _module_semantic_types(module): ref = semantic_type.metadata.get(EXTERNAL_TYPE_REF_METADATA) if not isinstance(ref, dict) or ref.get("representation") != "opaque": continue diff --git a/prik/pipeline/wrapper.py b/prik/pipeline/wrapper.py index 188e0a38d..bdd7b4962 100644 --- a/prik/pipeline/wrapper.py +++ b/prik/pipeline/wrapper.py @@ -68,6 +68,7 @@ ModuleGetterAction, ModuleObjectAccessMechanism, NativeInvocationKind, + NativeEntrypointAction, NativeArrayDescriptorInterop, NativeArrayDescriptorKind, NativeArrayDescriptorOwnership, @@ -112,14 +113,19 @@ ModulePlan, ModuleVariablePlan, NativeArrayHandlePlan, - NativeCallSlotPlan, + NativeEntrypointProjectedSlotPlan, + NativeGeneratedCodeGroupKind, + NativeGeneratedCodeGroupPlan, NamespacePlan, + GeneratedSupportProcedureImplementationOwner, + GeneratedSupportProcedureEntrypointPlan, ProcedurePrototypeArgumentPlan, ProcedurePrototypePlan, ProcedurePrototypeResultPlan, ResultPlan, WrapperPlanDiagnostic, ) +from prik.planning.entrypoints import build_generated_support_procedure_projection from prik.printers import CSourcePrinter, FortranSourcePrinter __all__ = ("GeneratedSource", "GeneratedWrapper", "WrapperGenerator") @@ -145,6 +151,8 @@ class GeneratedWrapper(StageRecord): native_support_keys: tuple[str, ...] required_headers: tuple[str, ...] extension_init_name: str + required_link_languages: tuple[str, ...] = () + native_generated_code_groups: tuple[NativeGeneratedCodeGroupPlan, ...] = () @property def source_paths(self) -> tuple[Path, ...]: @@ -224,7 +232,8 @@ def generate( # Each backend preflights only the typed mechanisms it is responsible for. self._c_generator.require_supported(plan) - self._fortran_generator.require_supported(plan) + if plan.bridge is not None: + self._fortran_generator.require_supported(plan) # Lower and print binding translation units in their established progress order. if progress is not None: @@ -235,14 +244,17 @@ def generate( if progress is not None: progress("Generate binding source", time.perf_counter() - started) - # Lower and print the single bridge module after its binding counterpart. - if progress is not None: - progress("Generate bridge source", None) - started = time.perf_counter() - fortran_module = self._fortran_generator.visit(plan) - fortran_source = self._fortran_printer.doprint(fortran_module) - if progress is not None: - progress("Generate bridge source", time.perf_counter() - started) + # Lower a bridge only when planning selected at least one Fortran-owned + # adapter or generated support procedure for this module. + fortran_source = None + if plan.bridge is not None: + if progress is not None: + progress("Generate bridge source", None) + started = time.perf_counter() + fortran_module = self._fortran_generator.visit(plan) + fortran_source = self._fortran_printer.doprint(fortran_module) + if progress is not None: + progress("Generate bridge source", time.perf_counter() - started) # Render the shared binding header after all source nodes are available. if progress is not None: @@ -261,6 +273,8 @@ def generate( fortran_source, native_support_keys=(("binding_support",) if self._c_generator.requires_native_support(plan) else ()), required_headers=plan.required_headers, + required_link_languages=plan.entrypoint.native_languages, + native_generated_code_groups=plan.native_generated_code_groups, ) # Plan-consistency diagnostics: module graph first, then typed member records. @@ -288,8 +302,20 @@ def _plan_diagnostics(self, plan: ModulePlan) -> tuple[WrapperPlanDiagnostic, .. # Validate module ownership and the complete namespace tree before member links. if plan.binding.owner_path != plan.owner_path: diagnostics.append(self._diagnostic(plan.owner_path, "binding-module-owner", plan.binding.owner_path)) - if plan.bridge.owner_path != plan.owner_path: + if plan.entrypoint.owner_path != plan.owner_path: + diagnostics.append(self._diagnostic(plan.owner_path, "entrypoint-module-owner", plan.entrypoint.owner_path)) + if plan.bridge is not None and plan.bridge.owner_path != plan.owner_path: diagnostics.append(self._diagnostic(plan.owner_path, "bridge-module-owner", plan.bridge.owner_path)) + diagnostics.extend(self._native_generated_code_group_diagnostics(plan)) + if (plan.bridge is not None) is not bool(plan.native_generated_code_groups): + diagnostics.append( + self._diagnostic( + plan.owner_path, + "inconsistent-module-bridge-presence", + bool(plan.native_generated_code_groups), + ) + ) + diagnostics.extend(self._generated_support_procedure_entrypoint_diagnostics(plan)) diagnostics.extend(self._namespace_tree_diagnostics(plan)) # Validate every typed member against the shared records in its namespace. @@ -311,6 +337,178 @@ def _plan_diagnostics(self, plan: ModulePlan) -> tuple[WrapperPlanDiagnostic, .. diagnostics.extend(self._required_header_diagnostics(plan)) return tuple(diagnostics) + def _native_generated_code_group_diagnostics( + self, + plan: ModulePlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Require exact independent adapter and Fortran-support membership.""" + adapter_members = tuple( + function.owner_path + for namespace in plan.namespaces + for function in namespace.functions + if function.entrypoint.action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER + ) + support_members = tuple( + procedure.key + for procedure in plan.entrypoint.support_procedures + if procedure.implementation_owner is GeneratedSupportProcedureImplementationOwner.FORTRAN + ) + expected_members = { + NativeGeneratedCodeGroupKind.FORTRAN_ADAPTERS: adapter_members, + NativeGeneratedCodeGroupKind.FORTRAN_SUPPORT: support_members, + } + groups = plan.native_generated_code_groups + diagnostics = [] + if len({group.kind for group in groups}) != len(groups): + diagnostics.append( + self._diagnostic( + plan.owner_path, "duplicate-native-generated-code-group", tuple(g.kind for g in groups) + ) + ) + for kind, members in expected_members.items(): + actual = next((group for group in groups if group.kind is kind), None) + if bool(actual) is not bool(members): + diagnostics.append(self._diagnostic(plan.owner_path, "missing-native-generated-code-group", kind)) + continue + if actual is None: + continue + if actual.language != "fortran" or actual.member_keys != members or not actual.source_paths: + diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-native-generated-code-group", kind)) + if any(group.kind not in expected_members for group in groups): + diagnostics.append( + self._diagnostic(plan.owner_path, "unknown-native-generated-code-group", tuple(g.kind for g in groups)) + ) + return tuple(diagnostics) + + def _generated_support_procedure_entrypoint_diagnostics( + self, + plan: ModulePlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Require one complete planner-owned support-procedure registry.""" + diagnostics = [] + operations = plan.entrypoint.support_procedures + keys = tuple(operation.key for operation in operations) + symbols = tuple(operation.symbol_name for operation in operations) + if len(keys) != len(set(keys)): + diagnostics.append(self._diagnostic(plan.owner_path, "duplicate-auxiliary-entrypoint-key", keys)) + if len(symbols) != len(set(symbols)): + diagnostics.append(self._diagnostic(plan.owner_path, "duplicate-auxiliary-entrypoint-symbol", symbols)) + for operation in operations: + diagnostics.extend(self._generated_support_procedure_diagnostics(operation)) + try: + expected_projection = build_generated_support_procedure_projection(plan.namespaces) + except ValueError as error: + diagnostics.append(self._diagnostic(plan.owner_path, "invalid-auxiliary-entrypoint-inventory", str(error))) + return tuple(diagnostics) + expected = expected_projection.support_procedures + binding_inventories = ( + plan.binding.owned_derived_type_owner_paths, + plan.binding.allocatable_holder_type_owner_paths, + plan.binding.pointer_holder_type_owner_paths, + ) + expected_binding_inventories = ( + expected_projection.binding_owned_derived_type_owner_paths, + expected_projection.binding_allocatable_holder_type_owner_paths, + expected_projection.binding_pointer_holder_type_owner_paths, + ) + if binding_inventories != expected_binding_inventories: + diagnostics.append( + self._diagnostic( + plan.owner_path, + "inconsistent-binding-derived-support-inventory", + (expected_binding_inventories, binding_inventories), + ) + ) + if plan.bridge is not None: + bridge_inventories = ( + plan.bridge.allocatable_holder_type_owner_paths, + plan.bridge.pointer_holder_type_owner_paths, + plan.bridge.allocatable_holder_field_type_owner_paths, + plan.bridge.pointer_holder_field_type_owner_paths, + ) + expected_bridge_inventories = ( + expected_projection.bridge_allocatable_holder_type_owner_paths, + expected_projection.bridge_pointer_holder_type_owner_paths, + expected_projection.bridge_allocatable_holder_field_type_owner_paths, + expected_projection.bridge_pointer_holder_field_type_owner_paths, + ) + if bridge_inventories != expected_bridge_inventories: + diagnostics.append( + self._diagnostic( + plan.owner_path, + "inconsistent-bridge-derived-support-inventory", + (expected_bridge_inventories, bridge_inventories), + ) + ) + expected_by_key = {operation.key: operation for operation in expected} + actual_by_key = {operation.key: operation for operation in operations} + if expected_by_key.keys() != actual_by_key.keys(): + diagnostics.append( + self._diagnostic( + plan.owner_path, + "incomplete-auxiliary-entrypoint-inventory", + (tuple(expected_by_key), tuple(actual_by_key)), + ) + ) + for callback in ( + argument.callback + for namespace in plan.namespaces + for function in namespace.functions + for argument in function.arguments + if argument.callback is not None + ): + operation = actual_by_key.get(callback.entrypoint.support_procedure.key) + if operation is not callback.entrypoint.support_procedure: + diagnostics.append( + self._diagnostic( + callback.owner_path, + "unshared-callback-entrypoint-operation", + callback.entrypoint.support_procedure.key, + ) + ) + return tuple(diagnostics) + + def _generated_support_procedure_diagnostics( + self, + operation: GeneratedSupportProcedureEntrypointPlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate one operation key, implementation owner, and structured signature.""" + diagnostics = [] + if operation.key != f"{operation.owner_path}::{operation.role}": + diagnostics.append( + self._diagnostic(operation.owner_path, "invalid-auxiliary-entrypoint-key", operation.key) + ) + if not operation.symbol_name or not operation.symbol_name.isidentifier(): + diagnostics.append( + self._diagnostic( + operation.owner_path, + "invalid-auxiliary-entrypoint-symbol", + operation.symbol_name, + ) + ) + if operation.implementation_owner not in tuple(GeneratedSupportProcedureImplementationOwner): + diagnostics.append( + self._diagnostic( + operation.owner_path, + "invalid-auxiliary-entrypoint-implementation", + operation.implementation_owner, + ) + ) + values = (*operation.signature.parameters, operation.signature.result) + if any( + not value.role + or not value.c_name + or not value.c_name.isidentifier() + or not value.fortran_name + or not value.fortran_name.isidentifier() + or value.pointer_depth < 0 + for value in values + ): + diagnostics.append( + self._diagnostic(operation.owner_path, "invalid-auxiliary-entrypoint-signature", operation.key) + ) + return tuple(diagnostics) + def _required_header_diagnostics(self, plan: ModulePlan) -> tuple[WrapperPlanDiagnostic, ...]: """Require module headers to equal the completed handle-plan union.""" handles = tuple( @@ -854,12 +1052,9 @@ def _module_variable_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Return getter, setter, and initialization consistency diagnostics.""" diagnostics = [] - if plan.binding.getter_action is not plan.bridge.getter_action: - diagnostics.append( - self._diagnostic(plan.owner_path, "inconsistent-module-getter-action", plan.binding.getter_action) - ) if not plan.binding.python_names: diagnostics.append(self._diagnostic(plan.owner_path, "missing-module-python-name", plan.owner_path)) + diagnostics.extend(self._module_variable_entrypoint_diagnostics(plan)) diagnostics.extend(self._module_getter_diagnostics(plan)) if plan.binding.getter_action is ModuleGetterAction.DERIVED_OBJECT: diagnostics.extend(self._derived_module_object_diagnostics(plan)) @@ -868,6 +1063,20 @@ def _module_variable_diagnostics( diagnostics.append(self._diagnostic(plan.owner_path, "initializer-without-native-setter", plan.owner_path)) return tuple(diagnostics) + def _module_variable_entrypoint_diagnostics( + self, + plan: ModuleVariablePlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate module-variable roles before support-procedure lookup.""" + diagnostics = [] + for label, role in ( + ("getter", plan.entrypoint.getter_role), + ("setter", plan.entrypoint.setter_role), + ): + if role is not None and not role: + diagnostics.append(self._diagnostic(plan.owner_path, f"invalid-module-{label}-role", role)) + return tuple(diagnostics) + def _derived_module_object_diagnostics( self, plan: ModuleVariablePlan, @@ -971,9 +1180,9 @@ def _module_getter_diagnostics(self, plan: ModuleVariablePlan) -> tuple[WrapperP return self._native_constant_array_getter_diagnostics(plan) if action is ModuleGetterAction.DERIVED_OBJECT: return self._derived_module_getter_role_diagnostics(plan) - if plan.bridge.getter_role is None: + if plan.entrypoint.getter_role is None: return (self._diagnostic(plan.owner_path, "missing-module-getter-role", action.value),) - if action is ModuleGetterAction.NULLABLE_SNAPSHOT and plan.bridge.descriptor_kind not in { + if action is ModuleGetterAction.NULLABLE_SNAPSHOT and plan.entrypoint.descriptor_kind not in { "allocatable", "pointer", }: @@ -986,8 +1195,10 @@ def _binding_constant_getter_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Validate one binding-materialized module constant.""" diagnostics = [] - if plan.bridge.getter_role is not None: - diagnostics.append(self._diagnostic(plan.owner_path, "constant-has-bridge-getter", plan.bridge.getter_role)) + if plan.entrypoint.getter_role is not None: + diagnostics.append( + self._diagnostic(plan.owner_path, "constant-has-bridge-getter", plan.entrypoint.getter_role) + ) if plan.binding.constant_value is None: diagnostics.append(self._diagnostic(plan.owner_path, "missing-module-constant-value", plan.owner_path)) return tuple(diagnostics) @@ -998,7 +1209,7 @@ def _native_constant_getter_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Validate one compiler-evaluated module constant.""" diagnostics = [] - if plan.bridge.getter_role is None: + if plan.entrypoint.getter_role is None: diagnostics.append( self._diagnostic( plan.owner_path, @@ -1018,7 +1229,7 @@ def _native_constant_array_getter_diagnostics( diagnostics = [] if plan.array is None or plan.array.rank is None or plan.array.rank <= 0: diagnostics.append(self._diagnostic(plan.owner_path, "missing-module-constant-array", plan.array)) - if plan.bridge.getter_role is None: + if plan.entrypoint.getter_role is None: diagnostics.append( self._diagnostic( plan.owner_path, @@ -1041,7 +1252,7 @@ def _module_borrowed_array_view_diagnostics( diagnostics.append(self._diagnostic(plan.owner_path, "missing-module-array-view", array)) if plan.native_array_handle is not None or plan.derived is not None: diagnostics.append(self._diagnostic(plan.owner_path, "module-array-view-has-unrelated-facet", None)) - if plan.bridge.getter_role is None: + if plan.entrypoint.getter_role is None: diagnostics.append(self._diagnostic(plan.owner_path, "missing-module-array-getter-role", None)) if plan.bridge.native_assignment is not AssignmentMode.NONE: diagnostics.append( @@ -1063,13 +1274,13 @@ def _derived_module_getter_role_diagnostics( ModuleObjectAccessMechanism.DIRECT_ADDRESS, ModuleObjectAccessMechanism.VALUE_COPY, }: - if plan.bridge.getter_role is None: + if plan.entrypoint.getter_role is None: return (self._diagnostic(plan.owner_path, "missing-derived-module-getter-role", None),) return () - if plan.bridge.getter_role is not None: + if plan.entrypoint.getter_role is not None: return ( self._diagnostic( - plan.owner_path, "module-proxy-fabricates-whole-address-role", plan.bridge.getter_role + plan.owner_path, "module-proxy-fabricates-whole-address-role", plan.entrypoint.getter_role ), ) return () @@ -1132,9 +1343,9 @@ def _module_handle_setter_diagnostics(self, plan: ModuleVariablePlan) -> tuple[W plan.bridge.native_assignment, ) ) - if plan.bridge.setter_role is not None: + if plan.entrypoint.setter_role is not None: diagnostics.append( - self._diagnostic(plan.owner_path, "module-handle-has-replacement-role", plan.bridge.setter_role) + self._diagnostic(plan.owner_path, "module-handle-has-replacement-role", plan.entrypoint.setter_role) ) return tuple(diagnostics) @@ -1148,7 +1359,7 @@ def _module_write_through_setter_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, "invalid-module-native-assignment", plan.bridge.native_assignment) ) - if plan.bridge.setter_role is None: + if plan.entrypoint.setter_role is None: diagnostics.append( self._diagnostic(plan.owner_path, "missing-module-setter-role", plan.binding.setter_action.value) ) @@ -1164,9 +1375,9 @@ def _module_nonwriting_setter_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, "invalid-module-native-assignment", plan.bridge.native_assignment) ) - if plan.bridge.setter_role is not None: + if plan.entrypoint.setter_role is not None: diagnostics.append( - self._diagnostic(plan.owner_path, "setter-role-without-write-through", plan.bridge.setter_role) + self._diagnostic(plan.owner_path, "setter-role-without-write-through", plan.entrypoint.setter_role) ) diagnostics.extend(self._module_nonwriting_action_diagnostics(plan)) return tuple(diagnostics) @@ -1181,7 +1392,7 @@ def _module_nonwriting_action_diagnostics( plan.derived is not None or plan.binding.getter_action is ModuleGetterAction.BORROWED_ARRAY_VIEW ): return () - if action is SetterAction.REJECT_REPLACEMENT and plan.bridge.descriptor_kind not in { + if action is SetterAction.REJECT_REPLACEMENT and plan.entrypoint.descriptor_kind not in { "allocatable", "pointer", }: @@ -1203,8 +1414,14 @@ def _function_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnost This preserves diagnostic order and lets each typed helper validate the lowest record that contains its compared binding and bridge facts. """ + if plan.entrypoint.action is NativeEntrypointAction.DIRECT_C_ABI: + return self._direct_function_diagnostics(plan) + + projected_slots = tuple(sorted(plan.entrypoint.projected_slots, key=lambda item: item.native_position)) + adapter_slots = self._adapter_slots(plan) # Check the function-wide producer/consumer graph before its individual records. diagnostics = [ + *self._entrypoint_diagnostics(plan), *self._sequence_diagnostics( plan.owner_path, "python", @@ -1214,8 +1431,8 @@ def _function_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnost *self._sequence_diagnostics( plan.owner_path, "native", - tuple(slot.native_position for slot in plan.native_call_slots), - len(plan.native_call_slots), + tuple(slot.native_position for slot in projected_slots), + len(projected_slots), ), *self._duplicate_role_diagnostics(plan), *self._available_role_diagnostics(plan), @@ -1225,12 +1442,11 @@ def _function_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnost *self._status_error_diagnostics(plan), *self._class_call_diagnostics(plan), *self._native_invocation_diagnostics(plan), - *self._optional_literal_combination_diagnostics(plan), ] # Validate shared slots and their typed consumers in native/result order. - slots = {slot.native_position: slot for slot in plan.native_call_slots} - for slot in plan.native_call_slots: + slots = {slot.native_position: slot for slot in projected_slots} + for slot in adapter_slots: diagnostics.extend(self._native_slot_diagnostics(slot)) for argument in plan.arguments: diagnostics.extend(self._argument_diagnostics(argument, slots, plan.available_roles)) @@ -1246,6 +1462,214 @@ def _function_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnost diagnostics.extend(self._string_writeback_diagnostics(plan)) return tuple(diagnostics) + @staticmethod + def _adapter_slots(plan: FunctionPlan) -> tuple[NativeEntrypointProjectedSlotPlan, ...]: + """Return ordered projected slots that carry an adapter facet.""" + return tuple( + slot + for slot in sorted(plan.entrypoint.projected_slots, key=lambda item: item.native_position) + if slot.adapter is not None + ) + + def _direct_function_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate a binding-to-native C ABI route without adapter assumptions.""" + slots = plan.entrypoint.projected_slots + diagnostics = [ + *self._entrypoint_diagnostics(plan), + *self._sequence_diagnostics( + plan.owner_path, + "python", + tuple(argument.python_position for argument in plan.arguments), + len(plan.arguments), + ), + *self._sequence_diagnostics( + plan.owner_path, + "native", + tuple(slot.native_position for slot in slots), + len(slots), + ), + *self._duplicate_role_diagnostics(plan), + *self._available_role_diagnostics(plan), + *self._binding_conversion_order_diagnostics(plan), + *self._binding_result_diagnostics(plan), + ] + if plan.bridge is not None or self._adapter_slots(plan): + diagnostics.append(self._diagnostic(plan.owner_path, "direct-route-has-fortran-adapter", plan.bridge)) + for slot in slots: + if slot.adapter is not None: + diagnostics.append(self._diagnostic(slot.owner_path, "direct-slot-has-adapter-facet", slot.adapter)) + indexed_slots = {slot.native_position: slot for slot in slots} + for argument in plan.arguments: + slot = argument.projected_call_slot + if argument.bridge is not None or slot.adapter is not None: + diagnostics.append( + self._diagnostic(argument.owner_path, "direct-argument-has-adapter-facet", argument.bridge) + ) + if indexed_slots.get(argument.native_position) is not slot: + diagnostics.append( + self._diagnostic( + argument.owner_path, "unregistered-direct-projected-slot", argument.native_position + ) + ) + expected_callback_parameter = argument.callback is not None + if argument.entrypoint.pass_callback_parameter is not expected_callback_parameter: + diagnostics.append( + self._diagnostic( + argument.owner_path, + "inconsistent-callback-entrypoint-parameter", + argument.entrypoint.pass_callback_parameter, + ) + ) + if argument.entrypoint.optionality is not slot.optionality: + diagnostics.append( + self._diagnostic( + argument.owner_path, + "inconsistent-direct-optionality", + argument.entrypoint.optionality, + ) + ) + for result in plan.results: + if result.bridge is not None or ( + result.projected_call_slot is not None and result.projected_call_slot.adapter is not None + ): + diagnostics.append( + self._diagnostic(result.owner_path, "direct-result-has-adapter-facet", result.bridge) + ) + return tuple(diagnostics) + + def _entrypoint_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate the complete shared C-ABI symbol and parameter-group index.""" + diagnostics = [] + if not plan.entrypoint.symbol_name or not plan.entrypoint.symbol_name.isidentifier(): + diagnostics.append( + self._diagnostic(plan.owner_path, "invalid-native-entrypoint-symbol", plan.entrypoint.symbol_name) + ) + parameters = tuple(sorted(plan.entrypoint.parameters, key=lambda item: item.position)) + diagnostics.extend( + self._sequence_diagnostics( + plan.owner_path, + "entrypoint-parameter", + tuple(parameter.position for parameter in parameters), + len(parameters), + ) + ) + expected = self._expected_entrypoint_parameter_groups(plan) + actual = tuple((parameter.owner_path, parameter.source_kind) for parameter in parameters) + if actual != expected: + diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-entrypoint-parameters", actual)) + diagnostics.extend(self._entrypoint_parameter_name_diagnostics(plan)) + diagnostics.extend(self._entrypoint_result_diagnostics(plan)) + return tuple(diagnostics) + + @staticmethod + 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} + 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")) + else: + groups.append((slot.owner_path, "projected_slot")) + groups.extend( + (result.owner_path, "direct_result") + for result in plan.results + if result.source_kind == "direct_return" + and ( + result.scalar_descriptor is not None + or ( + result.native_array_handle is not None + and result.native_array_handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE + ) + ) + ) + groups.extend( + (result.owner_path, "declaration_extent") + for result in plan.results + if result.array is not None and "bridge" in result.array.extent_evaluation + ) + return tuple(groups) + + def _entrypoint_parameter_name_diagnostics( + self, + plan: FunctionPlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: + """Require every C-visible argument and hidden result to have a valid name.""" + diagnostics = [] + for argument in plan.arguments: + name = argument.entrypoint.parameter_name + if not name or not name.isidentifier(): + diagnostics.append(self._diagnostic(argument.owner_path, "invalid-entrypoint-argument-name", name)) + for result in plan.entrypoint.results: + name = result.parameter_name + if result.source_kind == "hidden_output" and (not name or not name.isidentifier()): + diagnostics.append(self._diagnostic(result.owner_path, "invalid-entrypoint-result-name", name)) + for slot in plan.entrypoint.projected_slots: + if slot.owner_path in {argument.owner_path for argument in plan.arguments} or slot.source_kind == "result": + continue + if not slot.native_name or not slot.native_name.isidentifier(): + diagnostics.append( + self._diagnostic(slot.owner_path, "invalid-projected-entrypoint-name", slot.native_name) + ) + return tuple(diagnostics) + + def _entrypoint_result_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnostic, ...]: + """Validate every direct and binding-private result at the shared C ABI.""" + diagnostics = [] + by_owner = {result.owner_path: result for result in plan.entrypoint.results} + if len(by_owner) != len(plan.entrypoint.results): + diagnostics.append( + self._diagnostic(plan.owner_path, "duplicate-entrypoint-result-owner", plan.entrypoint.results) + ) + for result in plan.results: + if by_owner.get(result.owner_path) is not result.entrypoint: + diagnostics.append( + self._diagnostic(result.owner_path, "unregistered-public-entrypoint-result", result.owner_path) + ) + for slot in plan.entrypoint.projected_slots: + if slot.source_kind != "result": + continue + result = by_owner.get(slot.owner_path) + if result is None: + diagnostics.append( + self._diagnostic(slot.owner_path, "missing-hidden-entrypoint-result", slot.owner_path) + ) + continue + expected = ( + slot.symbolic_role, + slot.semantic_type_name, + slot.datatype_family, + slot.object_kind, + slot.character_length, + slot.array, + slot.native_array_handle, + slot.scalar_descriptor, + ) + actual = ( + result.native_result_role, + result.semantic_type_name, + result.datatype_family, + result.object_kind, + result.character_length, + result.array, + result.native_array_handle, + result.scalar_descriptor, + ) + if actual != expected: + diagnostics.append(self._diagnostic(slot.owner_path, "inconsistent-hidden-entrypoint-result", actual)) + 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"), + } + extra = tuple( + result.owner_path for result in plan.entrypoint.results if result.owner_path not in expected_owners + ) + if extra: + diagnostics.append(self._diagnostic(plan.owner_path, "unexpected-entrypoint-results", extra)) + return tuple(diagnostics) + def _binding_conversion_order_diagnostics( self, plan: FunctionPlan, @@ -1259,7 +1683,7 @@ def _binding_conversion_order_diagnostics( return tuple(diagnostics) positions = {owner: position for position, owner in enumerate(order)} - role_owners = {argument.binding.handoff_role: argument.owner_path for argument in plan.arguments} + role_owners = {argument.entrypoint.handoff_role: argument.owner_path for argument in plan.arguments} role_owners.update( { role: argument.owner_path @@ -1387,19 +1811,6 @@ def _binding_extent_dependency_owners( if role in role_owners ) - def _optional_literal_combination_diagnostics( - self, - plan: FunctionPlan, - ) -> tuple[WrapperPlanDiagnostic, ...]: - """Reject optional calls whose native slots also contain hidden literals.""" - has_optional = any( - argument.bridge.optional_mode in {OptionalMode.NULLABLE_VALUE, OptionalMode.DESCRIPTOR} - for argument in plan.arguments - ) - if has_optional and any(slot.source_kind == "literal" for slot in plan.native_call_slots): - return (self._diagnostic(plan.owner_path, "optional-native-literal-combination", None),) - return () - def _native_invocation_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagnostic, ...]: """Require one internally consistent completed native call syntax.""" invocation = plan.bridge.native_invocation @@ -1462,10 +1873,10 @@ def _class_call_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiagno def _argument_diagnostics( self, plan: ArgumentTransferPlan, - function_slots: dict[int, NativeCallSlotPlan], + function_slots: dict[int, NativeEntrypointProjectedSlotPlan], available_roles: tuple[str, ...], ) -> tuple[WrapperPlanDiagnostic, ...]: - """Return binding-to-bridge handoff and slot diagnostics.""" + """Return binding-to-entrypoint handoff and bridge-call-slot diagnostics.""" diagnostics = [ *self._argument_policy_consistency_diagnostics(plan), *self._argument_slot_consistency_diagnostics(plan, function_slots), @@ -1488,7 +1899,7 @@ def _array_writeback_abi_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Validate completed mutable-array normalization without selecting it.""" expected = ArrayWritebackABI.NOT_APPLICABLE - if plan.bridge.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER and ( + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER and ( plan.mutates_native or self._publishes_array_replacement(plan) ): if plan.array_logical_abi is ArrayLogicalABI.NATIVE_KIND_COPY: @@ -1805,11 +2216,11 @@ def _callback_symbol_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Require five distinct valid generated identifiers for one site.""" symbols = ( - callback.context_type_symbol, - callback.context_current_symbol, - callback.adapter_symbol, - callback.trampoline_symbol, - callback.abort_symbol, + callback.binding.context_type_symbol, + callback.binding.context_current_symbol, + callback.bridge.adapter_symbol, + callback.entrypoint.support_procedure.symbol_name, + callback.binding.abort_symbol, ) if any(not symbol or not symbol.isidentifier() for symbol in symbols) or len(set(symbols)) != len(symbols): return (self._diagnostic(owner_path, "invalid-callback-symbols", symbols),) @@ -1929,7 +2340,7 @@ def _derived_argument_diagnostics( ) if plan.derived is None: diagnostics.append(self._diagnostic(plan.owner_path, "missing-derived-handoff", None)) - elif plan.native_call_slot.derived is not plan.derived: + elif plan.projected_call_slot.derived is not plan.derived: diagnostics.append(self._diagnostic(plan.owner_path, "unshared-derived-handoff", None)) else: diagnostics.extend(self._derived_handoff_identity_diagnostics(plan.owner_path, plan.derived)) @@ -1938,9 +2349,9 @@ def _derived_argument_diagnostics( self._diagnostic(plan.owner_path, "invalid-derived-argument-origin", plan.derived.origin) ) diagnostics.extend(self._derived_call_diagnostics(plan)) - if plan.bridge.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: + if plan.entrypoint.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: diagnostics.append( - self._diagnostic(plan.owner_path, "invalid-derived-handoff-mode", plan.bridge.handoff_mode) + self._diagnostic(plan.owner_path, "invalid-derived-handoff-mode", plan.entrypoint.handoff_mode) ) if plan.array is not None or plan.native_array_handle is not None: diagnostics.append(self._diagnostic(plan.owner_path, "derived-handoff-has-array-policy", None)) @@ -2133,44 +2544,46 @@ def _argument_policy_consistency_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Return cross-view role and completed-action diagnostics.""" diagnostics = [] - role = plan.binding.handoff_role - if plan.bridge.handoff_role != role: - diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-bridge-handoff", role)) - if plan.native_call_slot.symbolic_role != role: - diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-native-handoff", role)) - if plan.bridge.length_handoff_role != plan.binding.length_handoff_role: + role = plan.entrypoint.handoff_role + slot = plan.projected_call_slot + adapter = slot.adapter + if adapter is None: + return (self._diagnostic(plan.owner_path, "missing-argument-adapter-facet", None),) + if plan.entrypoint.pass_callback_parameter: diagnostics.append( self._diagnostic( plan.owner_path, - "inconsistent-length-handoff", - plan.binding.length_handoff_role, + "inconsistent-callback-entrypoint-parameter", + plan.entrypoint.pass_callback_parameter, ) ) - if plan.bridge.native_action is not plan.native_call_slot.native_action: + if slot.symbolic_role != role: + diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-native-handoff", role)) + if plan.bridge.native_action is not adapter.native_action: diagnostics.append( self._diagnostic(plan.owner_path, "inconsistent-native-action", plan.bridge.native_action.value) ) - if plan.bridge.data_action is not plan.native_call_slot.bridge_data_action: + if plan.bridge.data_action is not adapter.bridge_data_action: diagnostics.append( self._diagnostic( plan.owner_path, "inconsistent-bridge-data-action", - plan.native_call_slot.bridge_data_action.value, + adapter.bridge_data_action.value, ) ) - if plan.array is not plan.native_call_slot.array: + if plan.array is not slot.array: diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-array-handoff", plan.array)) - if plan.native_array_handle is not plan.native_call_slot.native_array_handle: + if plan.native_array_handle is not slot.native_array_handle: diagnostics.append( self._diagnostic(plan.owner_path, "inconsistent-native-array-handle", plan.native_array_handle) ) diagnostics.extend(self._argument_completed_fact_diagnostics(plan)) - if plan.bridge.copy_reason != plan.native_call_slot.bridge_copy_reason: + if plan.bridge.copy_reason != adapter.bridge_copy_reason: diagnostics.append( self._diagnostic( plan.owner_path, "inconsistent-bridge-copy-reason", - plan.native_call_slot.bridge_copy_reason, + adapter.bridge_copy_reason, ) ) return tuple(diagnostics) @@ -2181,26 +2594,30 @@ def _argument_completed_fact_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Return projected action, length, mutability, and nullability drift.""" diagnostics = [] + slot = plan.projected_call_slot + adapter = slot.adapter + if adapter is None: + return (self._diagnostic(plan.owner_path, "missing-argument-adapter-facet", None),) if plan.binding.codegen_action is not plan.bridge.codegen_action: diagnostics.append( self._diagnostic( plan.owner_path, "inconsistent-argument-codegen-action", plan.bridge.codegen_action.value ) ) - if plan.binding.codegen_action is not plan.native_call_slot.codegen_action: + if plan.binding.codegen_action is not adapter.codegen_action: diagnostics.append( self._diagnostic( plan.owner_path, "inconsistent-native-slot-codegen-action", - plan.native_call_slot.codegen_action.value, + adapter.codegen_action.value, ) ) - if plan.character_length != plan.native_call_slot.character_length: + if plan.character_length != slot.character_length: diagnostics.append( self._diagnostic( plan.owner_path, "inconsistent-argument-character-length", - plan.native_call_slot.character_length, + slot.character_length, ) ) if plan.binding.writable != plan.mutates_native: @@ -2211,12 +2628,12 @@ def _argument_completed_fact_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, "inconsistent-argument-nullability", plan.binding.nullable) ) - if plan.native_call_slot.object_kind is not plan.object_kind: + if slot.object_kind is not plan.object_kind: diagnostics.append( self._diagnostic( plan.owner_path, "inconsistent-argument-object-kind", - plan.native_call_slot.object_kind, + slot.object_kind, ) ) diagnostics.extend(self._logical_argument_slot_diagnostics(plan)) @@ -2233,48 +2650,47 @@ def _logical_argument_slot_diagnostics( infer an ABI from the semantic datatype. """ diagnostics = [] - if plan.native_call_slot.scalar_logical_abi is not plan.scalar_logical_abi: + slot = plan.projected_call_slot + if slot.scalar_logical_abi is not plan.scalar_logical_abi: diagnostics.append( self._diagnostic( plan.owner_path, "inconsistent-scalar-logical-abi", - plan.native_call_slot.scalar_logical_abi.value, + slot.scalar_logical_abi.value, ) ) - if plan.native_call_slot.scalar_native_type != plan.scalar_native_type: + if slot.scalar_native_type != plan.scalar_native_type: diagnostics.append( self._diagnostic( plan.owner_path, "inconsistent-scalar-native-type", - plan.native_call_slot.scalar_native_type, + slot.scalar_native_type, ) ) - if plan.native_call_slot.array_logical_abi is not plan.array_logical_abi: + if slot.array_logical_abi is not plan.array_logical_abi: diagnostics.append( self._diagnostic( plan.owner_path, "inconsistent-array-logical-abi", - plan.native_call_slot.array_logical_abi.value, + slot.array_logical_abi.value, ) ) - if plan.native_call_slot.array_native_type != plan.array_native_type: + if slot.array_native_type != plan.array_native_type: diagnostics.append( self._diagnostic( plan.owner_path, "inconsistent-array-native-type", - plan.native_call_slot.array_native_type, + slot.array_native_type, ) ) - if plan.native_call_slot.array_copy_in != plan.array_copy_in: - diagnostics.append( - self._diagnostic(plan.owner_path, "inconsistent-array-copy-in", plan.native_call_slot.array_copy_in) - ) - if plan.native_call_slot.array_copy_out != plan.array_copy_out: + if slot.array_copy_in != plan.array_copy_in: + diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-array-copy-in", slot.array_copy_in)) + if slot.array_copy_out != plan.array_copy_out: diagnostics.append( self._diagnostic( plan.owner_path, "inconsistent-array-copy-out", - plan.native_call_slot.array_copy_out, + slot.array_copy_out, ) ) return tuple(diagnostics) @@ -2282,24 +2698,21 @@ def _logical_argument_slot_diagnostics( def _argument_slot_consistency_diagnostics( self, plan: ArgumentTransferPlan, - function_slots: dict[int, NativeCallSlotPlan], + function_slots: dict[int, NativeEntrypointProjectedSlotPlan], ) -> tuple[WrapperPlanDiagnostic, ...]: """Return argument position and native-slot graph diagnostics.""" diagnostics = [] - if plan.bridge.abi_position != plan.native_position: - diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-bridge-position", plan.native_position)) - if plan.native_call_slot.native_position != plan.native_position: + slot = plan.projected_call_slot + if slot.native_position != plan.native_position: diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-native-position", plan.native_position)) - if plan.native_call_slot.python_position != plan.python_position: + if slot.python_position != plan.python_position: diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-python-position", plan.python_position)) - if function_slots.get(plan.native_position) is not plan.native_call_slot: + if function_slots.get(plan.native_position) is not slot: diagnostics.append( self._diagnostic(plan.owner_path, "inconsistent-function-native-slot", plan.native_position) ) - if plan.native_call_slot.source_kind not in {"implicit", "projection"}: - diagnostics.append( - self._diagnostic(plan.owner_path, "invalid-argument-native-slot", plan.native_call_slot.source_kind) - ) + if slot.source_kind not in {"implicit", "projection"}: + diagnostics.append(self._diagnostic(plan.owner_path, "invalid-argument-native-slot", slot.source_kind)) return tuple(diagnostics) def _argument_data_action_diagnostics( @@ -2332,7 +2745,7 @@ def _expected_argument_data_action(self, plan: ArgumentTransferPlan) -> BridgeDa def _expected_handoff_data_action(self, plan: ArgumentTransferPlan) -> BridgeDataAction: """Dispatch descriptor, buffer, and scalar/address handoff actions.""" - mode = plan.bridge.handoff_mode + mode = plan.entrypoint.handoff_mode if mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: return self._expected_native_descriptor_data_action(plan) buffer_actions = { @@ -2345,12 +2758,12 @@ def _expected_handoff_data_action(self, plan: ArgumentTransferPlan) -> BridgeDat def _expected_scalar_or_address_data_action(self, plan: ArgumentTransferPlan) -> BridgeDataAction: """Select remaining scalar, string, optional, and opaque-address actions.""" - mode = plan.bridge.handoff_mode + mode = plan.entrypoint.handoff_mode if plan.object_kind is ObjectKind.STRING and mode is ArgumentHandoffMode.OPAQUE_ADDRESS: return BridgeDataAction.COPY_REPRESENTATION - if plan.bridge.optional_mode in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR}: + if plan.entrypoint.optional_mode in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR}: return self._expected_optional_descriptor_data_action(plan) - if plan.bridge.optional_mode is OptionalMode.NULLABLE_VALUE or mode is ArgumentHandoffMode.OPAQUE_ADDRESS: + if plan.entrypoint.optional_mode is OptionalMode.NULLABLE_VALUE or mode is ArgumentHandoffMode.OPAQUE_ADDRESS: return BridgeDataAction.ASSOCIATE_VIEW return BridgeDataAction.DIRECT_TRANSFER @@ -2370,7 +2783,7 @@ def _expected_optional_descriptor_data_action(self, plan: ArgumentTransferPlan) """Return the scalar optional descriptor view/copy selection.""" if plan.derived_call is not None: return BridgeDataAction.ASSOCIATE_VIEW - if plan.native_call_slot.value_kind == "allocatable": + if plan.projected_call_slot.value_kind == "allocatable": return BridgeDataAction.COPY_REPRESENTATION return BridgeDataAction.ASSOCIATE_VIEW @@ -2392,7 +2805,7 @@ def _scalar_boundary_diagnostics( PythonBarrierAction.RAW_ADDRESS: NativeBarrierAction.PASS_RAW_ADDRESS, }.get(action) if expected is None: - if plan.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: + if plan.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS: return (self._diagnostic(plan.owner_path, "unexpected-opaque-address-handoff", action.value),) return () diagnostics = [] @@ -2400,9 +2813,9 @@ def _scalar_boundary_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, "invalid-scalar-address-action", plan.bridge.native_action.value) ) - if plan.bridge.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: + if plan.entrypoint.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: diagnostics.append( - self._diagnostic(plan.owner_path, "invalid-scalar-address-handoff", plan.bridge.handoff_mode.value) + self._diagnostic(plan.owner_path, "invalid-scalar-address-handoff", plan.entrypoint.handoff_mode.value) ) if plan.bridge.data_action is not BridgeDataAction.ASSOCIATE_VIEW: diagnostics.append( @@ -2463,7 +2876,7 @@ def _native_array_handle_argument_action_diagnostics( expected = ( ("python-action", plan.binding.python_action, PythonBarrierAction.WRAPPER_INSTANCE), ("native-action", plan.bridge.native_action, NativeBarrierAction.PASS_NATIVE_DESCRIPTOR), - ("handoff-mode", plan.bridge.handoff_mode, ArgumentHandoffMode.NATIVE_DESCRIPTOR), + ("handoff-mode", plan.entrypoint.handoff_mode, ArgumentHandoffMode.NATIVE_DESCRIPTOR), ) diagnostics.extend( self._diagnostic(plan.owner_path, f"invalid-handle-{name}", actual.value) @@ -2684,7 +3097,7 @@ def _native_array_actual_diagnostics( expected_order = None if array is None or array.rank == 1 else ("C" if array.order == "ORDER_C" else "F") if actual.order != expected_order: diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-array-actual-order", actual.order)) - if plan.bridge.handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER: + if plan.entrypoint.handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER: diagnostics.append(self._diagnostic(plan.owner_path, "array-actual-not-buffer-handoff", None)) return tuple(diagnostics) @@ -2940,9 +3353,9 @@ def _native_descriptor_presence_diagnostics( return (self._diagnostic(owner_path, "inconsistent-native-descriptor-presence", presence),) if argument is None: return () - if handle.optional_absent and argument.bridge.presence_role != presence: + if handle.optional_absent and argument.entrypoint.presence_role != presence: return (self._diagnostic(owner_path, "inconsistent-native-descriptor-presence-role", presence),) - if not handle.optional_absent and argument.bridge.presence_role is not None: + if not handle.optional_absent and argument.entrypoint.presence_role is not None: return (self._diagnostic(owner_path, "required-native-descriptor-has-presence", None),) return () @@ -3007,9 +3420,11 @@ def _scalar_storage_array_action_diagnostics( plan.owner_path, "invalid-scalar-storage-native-action", plan.bridge.native_action.value ) ) - if plan.bridge.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: + if plan.entrypoint.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: diagnostics.append( - self._diagnostic(plan.owner_path, "invalid-scalar-storage-handoff-mode", plan.bridge.handoff_mode.value) + self._diagnostic( + plan.owner_path, "invalid-scalar-storage-handoff-mode", plan.entrypoint.handoff_mode.value + ) ) if plan.bridge.data_action is not BridgeDataAction.ASSOCIATE_VIEW: diagnostics.append( @@ -3041,9 +3456,9 @@ def _array_buffer_action_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, "invalid-array-native-action", plan.bridge.native_action.value) ) - if plan.bridge.handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER: + if plan.entrypoint.handoff_mode is not ArgumentHandoffMode.ARRAY_BUFFER: diagnostics.append( - self._diagnostic(plan.owner_path, "invalid-array-handoff-mode", plan.bridge.handoff_mode.value) + self._diagnostic(plan.owner_path, "invalid-array-handoff-mode", plan.entrypoint.handoff_mode.value) ) expected_data_action = ( BridgeDataAction.COPY_REPRESENTATION @@ -3075,9 +3490,9 @@ def _raw_array_action_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, "invalid-raw-array-native-action", plan.bridge.native_action.value) ) - if plan.bridge.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: + if plan.entrypoint.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: diagnostics.append( - self._diagnostic(plan.owner_path, "invalid-raw-array-handoff-mode", plan.bridge.handoff_mode.value) + self._diagnostic(plan.owner_path, "invalid-raw-array-handoff-mode", plan.entrypoint.handoff_mode.value) ) if plan.bridge.data_action is not BridgeDataAction.ASSOCIATE_VIEW: diagnostics.append( @@ -3263,7 +3678,7 @@ def _array_data_role_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Validate the primary data role and nonempty extent producers.""" diagnostics = [] - if array.data_role != plan.binding.handoff_role: + if array.data_role != plan.entrypoint.handoff_role: diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-array-data-role", array.data_role)) if any(not role for role in array.extent_roles): diagnostics.append(self._diagnostic(plan.owner_path, "missing-array-extent-role", array.extent_roles)) @@ -3509,9 +3924,9 @@ def _string_value_action_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, "invalid-string-native-action", plan.bridge.native_action.value) ) - if plan.bridge.handoff_mode is not ArgumentHandoffMode.CHARACTER_BUFFER: + if plan.entrypoint.handoff_mode is not ArgumentHandoffMode.CHARACTER_BUFFER: diagnostics.append( - self._diagnostic(plan.owner_path, "invalid-string-handoff", plan.bridge.handoff_mode.value) + self._diagnostic(plan.owner_path, "invalid-string-handoff", plan.entrypoint.handoff_mode.value) ) if plan.bridge.data_action is not BridgeDataAction.COPY_REPRESENTATION: diagnostics.append( @@ -3551,9 +3966,9 @@ def _string_address_diagnostics( plan.owner_path, f"invalid-string-{label}-native-action", plan.bridge.native_action.value ) ) - if plan.bridge.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: + if plan.entrypoint.handoff_mode is not ArgumentHandoffMode.OPAQUE_ADDRESS: diagnostics.append( - self._diagnostic(plan.owner_path, f"invalid-string-{label}-handoff", plan.bridge.handoff_mode.value) + self._diagnostic(plan.owner_path, f"invalid-string-{label}-handoff", plan.entrypoint.handoff_mode.value) ) if plan.bridge.data_action is not BridgeDataAction.COPY_REPRESENTATION: diagnostics.append( @@ -3614,12 +4029,12 @@ def _string_address_length_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, f"invalid-string-{label}-length", plan.character_length) ) - if plan.binding.length_handoff_role is not None: + if plan.entrypoint.length_handoff_role is not None: diagnostics.append( self._diagnostic( plan.owner_path, f"unexpected-string-{label}-length-handoff", - plan.binding.length_handoff_role, + plan.entrypoint.length_handoff_role, ) ) return tuple(diagnostics) @@ -3675,15 +4090,15 @@ def _string_length_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Return payload-length handoff and fixed-length diagnostics.""" diagnostics = [] - length_role = plan.binding.length_handoff_role + length_role = plan.entrypoint.length_handoff_role if length_role is None: diagnostics.append(self._diagnostic(plan.owner_path, "missing-string-length-handoff", None)) - if plan.native_call_slot.character_length is not None and plan.native_call_slot.character_length <= 0: + if plan.projected_call_slot.character_length is not None and plan.projected_call_slot.character_length <= 0: diagnostics.append( self._diagnostic( plan.owner_path, "invalid-string-character-length", - plan.native_call_slot.character_length, + plan.projected_call_slot.character_length, ) ) if plan.character_length is not None and plan.character_length <= 0: @@ -3708,7 +4123,7 @@ def _descriptor_output_role_diagnostics( plan: ArgumentTransferPlan, ) -> tuple[WrapperPlanDiagnostic, ...]: """Require copy-out roles exactly for projected required descriptors.""" - roles = (plan.bridge.descriptor_output_role, plan.bridge.descriptor_output_presence_role) + roles = (plan.entrypoint.descriptor_output_role, plan.entrypoint.descriptor_output_presence_role) expected = plan.binding.optional_mode is OptionalMode.REQUIRED_DESCRIPTOR and plan.projects_result if expected and any(role is None for role in roles): return (self._diagnostic(plan.owner_path, "missing-required-descriptor-output-role", roles),) @@ -3723,13 +4138,13 @@ def _optional_presence_diagnostics( """Return cross-view presence and descriptor diagnostics.""" diagnostics = [] mode = plan.binding.optional_mode - if plan.bridge.optional_mode is not mode: + if plan.entrypoint.optional_mode is not mode: diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-optional-mode", mode.value)) if plan.native_array_handle is not None: if not plan.binding.descriptor_boundary: diagnostics.append(self._diagnostic(plan.owner_path, "missing-native-descriptor-boundary", mode.value)) expected_presence = plan.native_array_handle.handoff.presence_role - if plan.bridge.presence_role != expected_presence: + if plan.entrypoint.presence_role != expected_presence: diagnostics.append( self._diagnostic(plan.owner_path, "inconsistent-native-descriptor-presence-role", expected_presence) ) @@ -3737,9 +4152,9 @@ def _optional_presence_diagnostics( descriptor_mode = mode in {OptionalMode.REQUIRED_DESCRIPTOR, OptionalMode.DESCRIPTOR} if plan.binding.descriptor_boundary != descriptor_mode: diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-descriptor-boundary", mode.value)) - if mode is OptionalMode.DESCRIPTOR and plan.bridge.presence_role is None: + if mode is OptionalMode.DESCRIPTOR and plan.entrypoint.presence_role is None: diagnostics.append(self._diagnostic(plan.owner_path, "missing-descriptor-presence-role", mode.value)) - if mode is not OptionalMode.DESCRIPTOR and plan.bridge.presence_role is not None: + if mode is not OptionalMode.DESCRIPTOR and plan.entrypoint.presence_role is not None: diagnostics.append(self._diagnostic(plan.owner_path, "unexpected-descriptor-presence-role", mode.value)) return tuple(diagnostics) @@ -3762,9 +4177,9 @@ def _optional_native_diagnostics( diagnostics.append( self._diagnostic(plan.owner_path, "invalid-optional-native-action", plan.bridge.native_action.value) ) - if descriptor_mode and plan.native_call_slot.value_kind not in {"allocatable", "pointer"}: + if descriptor_mode and plan.projected_call_slot.value_kind not in {"allocatable", "pointer"}: diagnostics.append( - self._diagnostic(plan.owner_path, "invalid-descriptor-value-kind", plan.native_call_slot.value_kind) + self._diagnostic(plan.owner_path, "invalid-descriptor-value-kind", plan.projected_call_slot.value_kind) ) return tuple(diagnostics) @@ -3772,7 +4187,7 @@ def _optional_native_diagnostics( def _result_diagnostics( self, plan: ResultPlan, - function_slots: dict[int, NativeCallSlotPlan], + function_slots: dict[int, NativeEntrypointProjectedSlotPlan], available_roles: tuple[str, ...], ) -> tuple[WrapperPlanDiagnostic, ...]: """Return direct or hidden result producer/consumer diagnostics.""" @@ -3817,13 +4232,13 @@ def _direct_result_abi_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanD if plan.datatype_family is DatatypeFamily.BOOL else DirectResultABI.NATIVE_SCALAR ) - if plan.direct_result_abi is expected: + if plan.entrypoint.direct_result_abi is expected: return () return ( self._diagnostic( plan.owner_path, "invalid-direct-result-abi", - f"{plan.direct_result_abi.value}; expected {expected.value}", + f"{plan.entrypoint.direct_result_abi.value}; expected {expected.value}", ), ) @@ -3886,7 +4301,7 @@ def _derived_result_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiag ) if plan.array is not None or plan.native_array_handle is not None: diagnostics.append(self._diagnostic(plan.owner_path, "derived-result-has-array-policy", None)) - if plan.native_call_slot is not None and plan.native_call_slot.derived is not plan.derived: + if plan.projected_call_slot is not None and plan.projected_call_slot.derived is not plan.derived: diagnostics.append(self._diagnostic(plan.owner_path, "unshared-derived-result-handoff", None)) return tuple(diagnostics) @@ -3981,9 +4396,9 @@ def _scalar_descriptor_source_diagnostics( if descriptor is None: return () if plan.source_kind == "hidden_output": - if plan.native_call_slot is None or plan.native_call_slot.scalar_descriptor is not descriptor: + 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),) - elif plan.native_call_slot is not None: + elif plan.projected_call_slot is not None: return (self._diagnostic(plan.owner_path, "direct-scalar-descriptor-has-slot", None),) return () @@ -4129,8 +4544,8 @@ def _result_role_diagnostics( by planning. The returned diagnostic identifies a consumer whose stored producer is unavailable; no role is added here. """ - if plan.bridge.native_result_role not in available_roles: - return (self._diagnostic(plan.owner_path, "unavailable-result-role", plan.bridge.native_result_role),) + if plan.entrypoint.native_result_role not in available_roles: + return (self._diagnostic(plan.owner_path, "unavailable-result-role", plan.entrypoint.native_result_role),) return () def _direct_result_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagnostic, ...]: @@ -4141,7 +4556,7 @@ def _direct_result_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagn completed bridge action, which is compared without altering the plan. """ diagnostics = [] - if plan.native_call_slot is not None or plan.bridge.abi_position is not None: + if plan.projected_call_slot is not None: diagnostics.append(self._diagnostic(plan.owner_path, "direct-result-has-native-slot", plan.source_kind)) expected_data_action = ( BridgeDataAction.COPY_REPRESENTATION @@ -4158,7 +4573,7 @@ def _direct_result_diagnostics(self, plan: ResultPlan) -> tuple[WrapperPlanDiagn def _hidden_result_diagnostics( self, plan: ResultPlan, - function_slots: dict[int, NativeCallSlotPlan], + function_slots: dict[int, NativeEntrypointProjectedSlotPlan], ) -> tuple[WrapperPlanDiagnostic, ...]: """Validate a hidden result's shared slot and function-wide registration. @@ -4167,9 +4582,9 @@ def _hidden_result_diagnostics( its shape and completed-action checks. Missing or mismatched records become diagnostics rather than replacement slots. """ - if plan.native_call_slot is None or plan.bridge.abi_position is None: + if plan.projected_call_slot is None: return (self._diagnostic(plan.owner_path, "missing-result-native-slot", plan.bridge.native_name),) - slot = plan.native_call_slot + slot = plan.projected_call_slot diagnostics = [ *self._hidden_result_shape_diagnostics(plan, slot), *self._hidden_result_policy_consistency_diagnostics(plan, slot), @@ -4183,44 +4598,51 @@ def _hidden_result_diagnostics( def _hidden_result_shape_diagnostics( self, plan: ResultPlan, - slot: NativeCallSlotPlan, + slot: NativeEntrypointProjectedSlotPlan, ) -> tuple[WrapperPlanDiagnostic, ...]: """Return hidden-result native-slot shape diagnostics.""" diagnostics = [] if slot.source_kind != "result": diagnostics.append(self._diagnostic(plan.owner_path, "invalid-result-native-slot", slot.source_kind)) - if slot.native_position != plan.bridge.abi_position: - diagnostics.append( - self._diagnostic(plan.owner_path, "inconsistent-result-native-position", slot.native_position) - ) if slot.result_position != plan.result_position: diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-result-position", slot.result_position)) - if slot.symbolic_role != plan.bridge.native_result_role: + if slot.symbolic_role != plan.entrypoint.native_result_role: diagnostics.append(self._diagnostic(plan.owner_path, "inconsistent-result-role", slot.symbolic_role)) return tuple(diagnostics) def _hidden_result_policy_consistency_diagnostics( self, plan: ResultPlan, - slot: NativeCallSlotPlan, + slot: NativeEntrypointProjectedSlotPlan, ) -> tuple[WrapperPlanDiagnostic, ...]: """Return hidden-result completed-action consistency diagnostics.""" diagnostics = [] - if slot.native_action is not plan.bridge.native_action: + adapter = slot.adapter + if adapter is None: + return (self._diagnostic(plan.owner_path, "missing-result-adapter-facet", None),) + if adapter.native_action is not plan.bridge.native_action: diagnostics.append( - self._diagnostic(plan.owner_path, "inconsistent-result-native-action", slot.native_action.value) + self._diagnostic(plan.owner_path, "inconsistent-result-native-action", adapter.native_action.value) ) - if slot.codegen_action is not plan.bridge.codegen_action: + if adapter.codegen_action is not plan.bridge.codegen_action: diagnostics.append( - self._diagnostic(plan.owner_path, "inconsistent-result-slot-codegen-action", slot.codegen_action.value) + self._diagnostic( + plan.owner_path, + "inconsistent-result-slot-codegen-action", + adapter.codegen_action.value, + ) ) - if slot.bridge_data_action is not plan.bridge.data_action: + if adapter.bridge_data_action is not plan.bridge.data_action: diagnostics.append( - self._diagnostic(plan.owner_path, "inconsistent-result-data-action", slot.bridge_data_action.value) + self._diagnostic( + plan.owner_path, + "inconsistent-result-data-action", + adapter.bridge_data_action.value, + ) ) - if slot.bridge_copy_reason != plan.bridge.copy_reason: + if adapter.bridge_copy_reason != plan.bridge.copy_reason: diagnostics.append( - self._diagnostic(plan.owner_path, "inconsistent-result-copy-reason", slot.bridge_copy_reason) + self._diagnostic(plan.owner_path, "inconsistent-result-copy-reason", adapter.bridge_copy_reason) ) if slot.character_length != plan.character_length: diagnostics.append( @@ -4486,19 +4908,28 @@ def _is_scalar_storage_array(array) -> bool: return bool(array is not None and array.rank == 0 and array.category == SCALAR_STORAGE_CATEGORY) # Native-call-slot and generic lifecycle validation. - def _native_slot_diagnostics(self, plan: NativeCallSlotPlan) -> tuple[WrapperPlanDiagnostic, ...]: + def _native_slot_diagnostics( + self, + plan: NativeEntrypointProjectedSlotPlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: """Return hidden literal and hidden result slot diagnostics.""" + adapter = plan.adapter + if adapter is None: + return (self._diagnostic(plan.owner_path, "missing-native-slot-adapter-facet", None),) diagnostics = list( self._bridge_data_diagnostics( plan.owner_path, - plan.bridge_data_action, - plan.bridge_copy_reason, + adapter.bridge_data_action, + adapter.bridge_copy_reason, ) ) - if plan.source_kind not in {"implicit", "projection", "literal", "result"}: + if plan.source_kind not in {"implicit", "projection", "literal", "computed", "work", "result"}: diagnostics.append(self._diagnostic(plan.owner_path, "unknown-native-slot-source", plan.source_kind)) if plan.source_kind == "literal": diagnostics.extend(self._literal_slot_diagnostics(plan)) + elif plan.source_kind in {"computed", "work"}: + if plan.semantic_type_name is None: + diagnostics.append(self._diagnostic(plan.owner_path, "missing-computed-slot-type", None)) elif plan.object_kind is None: diagnostics.append(self._diagnostic(plan.owner_path, "missing-native-slot-object-kind", None)) if plan.source_kind == "result": @@ -4522,9 +4953,15 @@ def _bridge_data_diagnostics( return (self._diagnostic(owner_path, "unexpected-bridge-copy-reason", action.value),) return () - def _literal_slot_diagnostics(self, plan: NativeCallSlotPlan) -> tuple[WrapperPlanDiagnostic, ...]: + def _literal_slot_diagnostics( + self, + plan: NativeEntrypointProjectedSlotPlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: """Return diagnostics for one hidden literal slot.""" diagnostics = [] + adapter = plan.adapter + if adapter is None: + return (self._diagnostic(plan.owner_path, "missing-literal-adapter-facet", None),) if plan.literal_type is None: diagnostics.append(self._diagnostic(plan.owner_path, "missing-literal-type", plan.native_position)) if plan.literal_value is None: @@ -4533,13 +4970,20 @@ def _literal_slot_diagnostics(self, plan: NativeCallSlotPlan) -> tuple[WrapperPl diagnostics.append(self._diagnostic(plan.owner_path, "literal-python-position", plan.python_position)) if plan.object_kind is not None: diagnostics.append(self._diagnostic(plan.owner_path, "literal-object-kind", plan.object_kind.value)) - if plan.bridge_data_action is not BridgeDataAction.DIRECT_TRANSFER: + if adapter.bridge_data_action is not BridgeDataAction.DIRECT_TRANSFER: diagnostics.append( - self._diagnostic(plan.owner_path, "invalid-literal-data-action", plan.bridge_data_action.value) + self._diagnostic( + plan.owner_path, + "invalid-literal-data-action", + adapter.bridge_data_action.value, + ) ) return tuple(diagnostics) - def _result_slot_diagnostics(self, plan: NativeCallSlotPlan) -> tuple[WrapperPlanDiagnostic, ...]: + def _result_slot_diagnostics( + self, + plan: NativeEntrypointProjectedSlotPlan, + ) -> tuple[WrapperPlanDiagnostic, ...]: """Return diagnostics for one native result slot.""" return ( *self._result_slot_identity_diagnostics(plan), @@ -4549,7 +4993,7 @@ def _result_slot_diagnostics(self, plan: NativeCallSlotPlan) -> tuple[WrapperPla def _result_slot_identity_diagnostics( self, - plan: NativeCallSlotPlan, + plan: NativeEntrypointProjectedSlotPlan, ) -> tuple[WrapperPlanDiagnostic, ...]: """Validate result/native positions and datatype identity.""" diagnostics = [] @@ -4563,7 +5007,7 @@ def _result_slot_identity_diagnostics( def _result_slot_string_diagnostics( self, - plan: NativeCallSlotPlan, + plan: NativeEntrypointProjectedSlotPlan, ) -> tuple[WrapperPlanDiagnostic, ...]: """Require fixed string length unless runtime descriptor length is planned.""" if plan.object_kind is ObjectKind.STRING and plan.character_length is None and plan.scalar_descriptor is None: @@ -4572,7 +5016,7 @@ def _result_slot_string_diagnostics( def _result_slot_data_action_diagnostics( self, - plan: NativeCallSlotPlan, + plan: NativeEntrypointProjectedSlotPlan, ) -> tuple[WrapperPlanDiagnostic, ...]: """Validate direct versus representation-copy native output action.""" expected = ( @@ -4582,12 +5026,15 @@ def _result_slot_data_action_diagnostics( or plan.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY else BridgeDataAction.DIRECT_TRANSFER ) - if plan.bridge_data_action is not expected: + adapter = plan.adapter + if adapter is None: + return (self._diagnostic(plan.owner_path, "missing-result-adapter-facet", None),) + if adapter.bridge_data_action is not expected: return ( self._diagnostic( plan.owner_path, "invalid-result-data-action", - f"{plan.bridge_data_action.value}:{expected.value}", + f"{adapter.bridge_data_action.value}:{expected.value}", ), ) return () @@ -4737,7 +5184,7 @@ def _one_string_writeback_diagnostics( ) -> tuple[WrapperPlanDiagnostic, ...]: """Return lifecycle coverage and fact drift for one replacement.""" actions = tuple( - action for action in plan.writeback_actions if action.source_role == argument.binding.handoff_role + action for action in plan.writeback_actions if action.source_role == argument.entrypoint.handoff_role ) diagnostics = [] if {action.phase for action in actions} != set(WritebackPhase): @@ -4817,7 +5264,7 @@ def _one_derived_result_lifecycle_coverage_diagnostics( result: ResultPlan, ) -> tuple[WrapperPlanDiagnostic, ...]: """Require one failure cleanup and one ownership transfer for a result.""" - source_role = result.bridge.native_result_role + source_role = result.entrypoint.native_result_role cleanup_count = self._lifecycle_operation_count( plan.cleanup_actions, source_role, @@ -4903,7 +5350,7 @@ def _unclaimed_result_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlan """Require exactly one binding or status consumer per native output.""" claimed_roles = self._claimed_result_roles(plan) diagnostics = [] - for slot in plan.native_call_slots: + for slot in plan.entrypoint.projected_slots: if slot.source_kind != "result": continue claim_count = claimed_roles[slot.symbolic_role] @@ -4918,7 +5365,7 @@ def _unclaimed_result_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlan def _claimed_result_roles(self, plan: FunctionPlan) -> Counter[str]: """Return public and status-policy consumers of native result slots.""" roles = Counter( - result.bridge.native_result_role for result in plan.results if result.source_kind == "hidden_output" + result.entrypoint.native_result_role for result in plan.results if result.source_kind == "hidden_output" ) if plan.binding.status_error is not None: roles[plan.binding.status_error.status_role] += 1 @@ -4931,7 +5378,9 @@ def _status_error_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiag policy = plan.binding.status_error if policy is None: return () - result_slots = {slot.symbolic_role: slot for slot in plan.native_call_slots if slot.source_kind == "result"} + result_slots = { + slot.symbolic_role: slot for slot in plan.entrypoint.projected_slots if slot.source_kind == "result" + } diagnostics = [*self._status_role_diagnostics(plan, result_slots)] diagnostics.extend(self._message_role_diagnostics(plan, result_slots)) diagnostics.extend(self._status_policy_diagnostics(plan)) @@ -4940,7 +5389,7 @@ def _status_error_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDiag def _status_role_diagnostics( self, plan: FunctionPlan, - result_slots: dict[str, NativeCallSlotPlan], + result_slots: dict[str, NativeEntrypointProjectedSlotPlan], ) -> tuple[WrapperPlanDiagnostic, ...]: """Validate the completed integer status role.""" policy = plan.binding.status_error @@ -4956,7 +5405,7 @@ def _status_role_diagnostics( def _message_role_diagnostics( self, plan: FunctionPlan, - result_slots: dict[str, NativeCallSlotPlan], + result_slots: dict[str, NativeEntrypointProjectedSlotPlan], ) -> tuple[WrapperPlanDiagnostic, ...]: """Validate the optional fixed-length status message role.""" policy = plan.binding.status_error @@ -5008,7 +5457,7 @@ def _duplicate_role_diagnostics(self, plan: FunctionPlan) -> tuple[WrapperPlanDi """Return duplicate symbolic producer/consumer role diagnostics.""" roles = ( *self._expected_available_roles(plan), - *self._native_slot_roles(plan.native_call_slots, "literal"), + *self._native_slot_roles(plan.entrypoint.projected_slots, "literal"), ) return tuple( self._diagnostic(plan.owner_path, "duplicate-symbolic-role", role) @@ -5029,7 +5478,7 @@ def _expected_available_roles(self, plan: FunctionPlan) -> tuple[str, ...]: *self._argument_handoff_roles(plan.arguments), *self._argument_extent_roles(plan.arguments), *self._argument_descriptor_output_roles(plan.arguments), - *self._native_slot_roles(plan.native_call_slots, "result"), + *self._native_slot_roles(plan.entrypoint.projected_slots, "result"), *self._direct_result_roles(plan.results), *self._declaration_callable_roles(plan.declaration_callables), ) @@ -5037,7 +5486,7 @@ def _expected_available_roles(self, plan: FunctionPlan) -> tuple[str, ...]: @staticmethod def _argument_handoff_roles(arguments: tuple[ArgumentTransferPlan, ...]) -> tuple[str, ...]: """Return the primary binding-produced role for every argument.""" - return tuple(argument.binding.handoff_role for argument in arguments) + return tuple(argument.entrypoint.handoff_role for argument in arguments) @staticmethod def _argument_descriptor_output_roles(arguments: tuple[ArgumentTransferPlan, ...]) -> tuple[str, ...]: @@ -5046,15 +5495,15 @@ def _argument_descriptor_output_roles(arguments: tuple[ArgumentTransferPlan, ... role for argument in arguments for role in ( - argument.bridge.descriptor_output_role, - argument.bridge.descriptor_output_presence_role, + argument.entrypoint.descriptor_output_role, + argument.entrypoint.descriptor_output_presence_role, ) if role is not None ) @staticmethod def _native_slot_roles( - slots: tuple[NativeCallSlotPlan, ...], + slots: tuple[NativeEntrypointProjectedSlotPlan, ...], source_kind: str, ) -> tuple[str, ...]: """Return symbolic roles produced by one native-slot category.""" @@ -5063,7 +5512,9 @@ def _native_slot_roles( @staticmethod def _direct_result_roles(results: tuple[ResultPlan, ...]) -> tuple[str, ...]: """Return native result roles produced by direct-return plans.""" - return tuple(result.bridge.native_result_role for result in results if result.source_kind == "direct_return") + return tuple( + result.entrypoint.native_result_role for result in results if result.source_kind == "direct_return" + ) @staticmethod def _declaration_callable_roles( @@ -5106,9 +5557,11 @@ def _generated_wrapper( module_name: str, c_sources: tuple[str, ...], c_header: str, - fortran_source: str, + fortran_source: str | None, native_support_keys: tuple[str, ...], required_headers: tuple[str, ...], + required_link_languages: tuple[str, ...], + native_generated_code_groups: tuple[NativeGeneratedCodeGroupPlan, ...], ) -> GeneratedWrapper: """Package rendered source text with the filenames owned by build integration. @@ -5122,7 +5575,18 @@ def _generated_wrapper( Path(f"{module_name}_wrapper.c"), *(Path(f"{module_name}_wrapper_{index:03d}.c") for index in range(1, len(c_sources))), ) - bridge_sources = (Path(f"bind_c_{module_name}_wrapper.f90"),) + bridge_sources = tuple( + dict.fromkeys( + Path(path) + for group in native_generated_code_groups + if group.language == "fortran" + for path in group.source_paths + ) + ) + if fortran_source is not None and len(bridge_sources) != 1: + raise ValueError("Generated Fortran payload requires exactly one planned physical source") + if fortran_source is None and bridge_sources: + raise ValueError("Planned generated Fortran groups require a rendered source payload") headers = (Path(f"{module_name}_wrapper.h"),) # Preserve build-consumed source ordering: bridge, binding units, then header. @@ -5130,7 +5594,7 @@ def _generated_wrapper( module_name=module_name, extension_init_name=f"PyInit_{module_name}", sources=( - GeneratedSource(bridge_sources[0], fortran_source), + *((GeneratedSource(bridge_sources[0], fortran_source),) if fortran_source is not None else ()), *(GeneratedSource(path, source) for path, source in zip(binding_sources, c_sources, strict=True)), GeneratedSource(headers[0], c_header), ), @@ -5139,6 +5603,8 @@ def _generated_wrapper( headers=headers, native_support_keys=native_support_keys, required_headers=required_headers, + required_link_languages=required_link_languages, + native_generated_code_groups=native_generated_code_groups, ) diff --git a/prik/planning/__init__.py b/prik/planning/__init__.py index 3af51d8bc..445d25155 100644 --- a/prik/planning/__init__.py +++ b/prik/planning/__init__.py @@ -1,23 +1,26 @@ """Public planning API for policy-completed wrapper plans. ``WrapperPlanner`` projects a completed ``SemanticModule`` into an editable -``ModulePlan``. The records re-exported here describe that plan's shared -binding, bridge, namespace, transfer, result, lifecycle, class, and descriptor -views. They represent completed policy for later lowering; they do not provide -another policy-construction API. +``ModulePlan``. The records re-exported here describe that plan's binding, +shared native-entrypoint, bridge, namespace, transfer, result, lifecycle, +class, and descriptor views. They represent completed policy for later +lowering; they do not provide another policy-construction API. """ from .models import ( ArgumentTransferPlan, ArrayHandoffPlan, BindingArgumentPlan, + BindingCallbackPlan, BindingFunctionPlan, BindingLifecyclePlan, BindingModulePlan, BindingModuleVariablePlan, BindingResultPlan, BindingStatusErrorPlan, + BridgeCallSlotPlan, BridgeArgumentPlan, + BridgeCallbackPlan, BridgeFunctionPlan, BridgeLifecyclePlan, BridgeModulePlan, @@ -30,13 +33,26 @@ DerivedModuleObjectPlan, DerivedTypePlan, FunctionPlan, + GeneratedSupportProcedureEntrypointPlan, + GeneratedSupportProcedureImplementationOwner, LifecycleActionPlan, ModulePlan, ModuleVariablePlan, + NativeGeneratedCodeGroupKind, + NativeGeneratedCodeGroupPlan, + NativeEntrypointABIValueKind, + NativeEntrypointABIValuePlan, + NativeEntrypointArgumentPlan, + NativeEntrypointCallbackPlan, + NativeEntrypointFunctionPlan, + NativeEntrypointModulePlan, + NativeEntrypointModuleVariablePlan, + NativeEntrypointParameterPlan, + NativeEntrypointResultPlan, + NativeEntrypointSignaturePlan, NativeArrayActualPlan, NativeArrayDefaultHandlePlan, NativeArrayHandlePlan, - NativeCallSlotPlan, NativeDescriptorHandoffPlan, NamespacePlan, ResultPlan, @@ -50,6 +66,7 @@ "ArgumentTransferPlan", "ArrayHandoffPlan", "BindingArgumentPlan", + "BindingCallbackPlan", "BindingFunctionPlan", "BindingLifecyclePlan", "BindingModulePlan", @@ -57,6 +74,8 @@ "BindingResultPlan", "BindingStatusErrorPlan", "BridgeArgumentPlan", + "BridgeCallSlotPlan", + "BridgeCallbackPlan", "BridgeFunctionPlan", "BridgeLifecyclePlan", "BridgeModulePlan", @@ -69,6 +88,8 @@ "DerivedModuleObjectPlan", "DerivedTypePlan", "FunctionPlan", + "GeneratedSupportProcedureEntrypointPlan", + "GeneratedSupportProcedureImplementationOwner", "LifecycleActionPlan", "ModulePlan", "ModuleVariablePlan", @@ -76,8 +97,19 @@ "NativeArrayActualPlan", "NativeArrayDefaultHandlePlan", "NativeArrayHandlePlan", - "NativeCallSlotPlan", "NativeDescriptorHandoffPlan", + "NativeEntrypointABIValueKind", + "NativeEntrypointABIValuePlan", + "NativeEntrypointArgumentPlan", + "NativeEntrypointCallbackPlan", + "NativeEntrypointFunctionPlan", + "NativeEntrypointModulePlan", + "NativeEntrypointModuleVariablePlan", + "NativeEntrypointParameterPlan", + "NativeEntrypointResultPlan", + "NativeEntrypointSignaturePlan", + "NativeGeneratedCodeGroupKind", + "NativeGeneratedCodeGroupPlan", "ResultPlan", "ScalarDescriptorResultPlan", "TransformationPlan", diff --git a/prik/planning/entrypoints.py b/prik/planning/entrypoints.py new file mode 100644 index 000000000..2f13ab3b5 --- /dev/null +++ b/prik/planning/entrypoints.py @@ -0,0 +1,1099 @@ +"""Project generated support procedures into one shared C-ABI registry. + +Ordinary wrapped functions own specialized entrypoint facets directly. This +module covers every other externally linked callable emitted by the current +Fortran adapter. It runs inside :class:`WrapperPlanner`; generators consume the +resulting operation records and do not decide which helpers exist. +""" + +from __future__ import annotations + +import re +from dataclasses import dataclass + +from prik.naming.native_symbols import NativeSymbolNames +from prik.policy.models import ( + CallbackABIKind, + CallbackResultAction, + ClassConstructorKind, + DerivedActualAccess, + DerivedCallAction, + DerivedFieldAccessMechanism, + DerivedObjectStorage, + DerivedRelease, + ModuleGetterAction, + ModuleObjectAccessMechanism, + NativeArrayDefaultConstruction, + NativeArrayDescriptorInterop, + NativeArrayOperation, + NativeDescriptorHandoffABI, +) +from prik.policy.ownership import ObjectKind, SetterAction + +from .models import ( + ArgumentTransferPlan, + CallbackHandoffPlan, + CallbackTransferPlan, + DerivedFieldPlan, + DerivedMemberPathPlan, + DerivedTypePlan, + ModuleVariablePlan, + NamespacePlan, + NativeArrayHandlePlan, + NativeEntrypointABIValueKind, + NativeEntrypointABIValuePlan, + GeneratedSupportProcedureImplementationOwner, + GeneratedSupportProcedureEntrypointPlan, + NativeEntrypointSignaturePlan, + ResultPlan, +) + + +_FIELD_HANDLE_LOCAL_OPERATIONS = frozenset( + { + NativeArrayOperation.NATIVE_BYTE_ORDER, + NativeArrayOperation.ALIGNED, + NativeArrayOperation.WRITEABLE, + NativeArrayOperation.LAYOUT, + NativeArrayOperation.TO_NUMPY, + NativeArrayOperation.ARRAY_ACTUAL, + } +) +_MODULE_HANDLE_LOCAL_OPERATIONS = frozenset( + { + NativeArrayOperation.NATIVE_BYTE_ORDER, + NativeArrayOperation.ALIGNED, + NativeArrayOperation.WRITEABLE, + NativeArrayOperation.LAYOUT, + NativeArrayOperation.TO_NUMPY, + } +) +_OWNED_HANDLE_ENTRYPOINT_OPERATIONS = frozenset( + { + NativeArrayOperation.ALLOCATED, + NativeArrayOperation.ASSOCIATED, + NativeArrayOperation.CONTIGUOUS, + NativeArrayOperation.SHAPE, + NativeArrayOperation.ASSOCIATE, + NativeArrayOperation.DEALLOCATE, + NativeArrayOperation.NULLIFY, + NativeArrayOperation.DESTROY, + } +) + + +@dataclass(frozen=True) +class GeneratedSupportProcedureProjection: + """Keep external entrypoints and backend-local support in one projection.""" + + support_procedures: tuple[GeneratedSupportProcedureEntrypointPlan, ...] + binding_owned_derived_type_owner_paths: tuple[str, ...] + binding_allocatable_holder_type_owner_paths: tuple[str, ...] + binding_pointer_holder_type_owner_paths: tuple[str, ...] + bridge_allocatable_holder_type_owner_paths: tuple[str, ...] + bridge_pointer_holder_type_owner_paths: tuple[str, ...] + bridge_allocatable_holder_field_type_owner_paths: tuple[str, ...] + bridge_pointer_holder_field_type_owner_paths: tuple[str, ...] + + +def build_generated_support_procedure_projection( + namespaces: tuple[NamespacePlan, ...], +) -> GeneratedSupportProcedureProjection: + """Return external and backend-local support membership in stable order.""" + builder = _GeneratedSupportProcedureEntrypointBuilder(namespaces) + projection = builder.build() + procedures = projection.support_procedures + keys = [procedure.key for procedure in procedures] + symbols = [procedure.symbol_name for procedure in procedures] + if len(keys) != len(set(keys)): + duplicates = tuple(key for key in dict.fromkeys(keys) if keys.count(key) > 1) + raise ValueError(f"Generated support procedure entrypoint keys are not unique: {duplicates!r}") + if len(symbols) != len(set(symbols)): + duplicates = tuple(symbol for symbol in dict.fromkeys(symbols) if symbols.count(symbol) > 1) + raise ValueError(f"Generated support procedure entrypoint symbols are not unique: {duplicates!r}") + return projection + + +def build_callback_support_procedure_entrypoint( + owner_path: str, + symbol_name: str, + arguments: tuple[CallbackTransferPlan, ...], + result, +) -> GeneratedSupportProcedureEntrypointPlan: + """Project the binding trampoline once while its callback site is planned.""" + builder = _GeneratedSupportProcedureEntrypointBuilder(()) + parameters = tuple( + parameter for transfer in arguments for parameter in builder._callback_transfer_parameters(transfer) + ) + return builder._operation( + owner_path, + "callback:trampoline", + symbol_name, + parameters, + builder._callback_result_from_plan(result), + implementation_owner=GeneratedSupportProcedureImplementationOwner.BINDING, + ) + + +class _GeneratedSupportProcedureEntrypointBuilder: + """Project operation existence, symbols, and ABI signatures from completed plans.""" + + def __init__(self, namespaces: tuple[NamespacePlan, ...]) -> None: + self.namespaces = namespaces + self.functions = tuple(function for namespace in namespaces for function in namespace.functions) + self.variables = tuple(variable for namespace in namespaces for variable in namespace.variables) + self.derived_types = tuple(derived for namespace in namespaces for derived in namespace.derived_types) + self.classes = tuple(surface for namespace in namespaces for surface in namespace.classes) + + def build(self) -> GeneratedSupportProcedureProjection: + """Collect external and binding-local support in declaration order.""" + owned_types = self._owned_derived_types() + allocatable_holders = self._allocatable_holder_types() + pointer_holders = self._pointer_holder_types() + binding_allocatable_holders = self._allocatable_holder_field_types() + binding_pointer_holders = self._pointer_holder_field_types() + binding_allocatable_owner_paths = tuple(derived.owner_path for derived in binding_allocatable_holders) + binding_pointer_owner_paths = tuple(derived.owner_path for derived in binding_pointer_holders) + return GeneratedSupportProcedureProjection( + support_procedures=( + *self._callback_operations(), + *self._class_constructor_operations(), + *(self._derived_destroy_operation(derived) for derived in owned_types), + *(self._holder_destroy_operation(derived, "allocatable") for derived in allocatable_holders), + *(self._holder_destroy_operation(derived, "pointer") for derived in pointer_holders), + *(self._holder_presence_operation(derived, "allocatable") for derived in allocatable_holders), + *(self._holder_presence_operation(derived, "pointer") for derived in pointer_holders), + *self._owned_native_array_operations(), + *self._derived_field_operations( + binding_allocatable_holders, + binding_pointer_holders, + ), + *self._module_variable_operations(), + *self._derived_origin_operations(), + ), + binding_owned_derived_type_owner_paths=tuple(derived.owner_path for derived in owned_types), + binding_allocatable_holder_type_owner_paths=binding_allocatable_owner_paths, + binding_pointer_holder_type_owner_paths=binding_pointer_owner_paths, + bridge_allocatable_holder_type_owner_paths=tuple(derived.owner_path for derived in allocatable_holders), + bridge_pointer_holder_type_owner_paths=tuple(derived.owner_path for derived in pointer_holders), + bridge_allocatable_holder_field_type_owner_paths=binding_allocatable_owner_paths, + bridge_pointer_holder_field_type_owner_paths=binding_pointer_owner_paths, + ) + + # ------------------------------------------------------------------ + # Generic ABI records + # ------------------------------------------------------------------ + + @staticmethod + def _value( + role: str, + kind: NativeEntrypointABIValueKind, + *, + c_name: str | None = None, + fortran_name: str | None = None, + pointer_depth: int = 0, + const: bool = False, + semantic_type_name: str | None = None, + rank: int | None = None, + character_length: int | None = None, + descriptor_kind=None, + intent: str | None = None, + c_type_name: str | None = None, + callback_signature: NativeEntrypointSignaturePlan | None = None, + ) -> NativeEntrypointABIValuePlan: + name = c_name or role + return NativeEntrypointABIValuePlan( + role=role, + c_name=name, + fortran_name=fortran_name or name, + kind=kind, + pointer_depth=pointer_depth, + const=const, + semantic_type_name=semantic_type_name, + rank=rank, + character_length=character_length, + descriptor_kind=descriptor_kind, + intent=intent, + c_type_name=c_type_name, + callback_signature=callback_signature, + ) + + @classmethod + def _void_result(cls) -> NativeEntrypointABIValuePlan: + return cls._value("result", NativeEntrypointABIValueKind.VOID) + + @classmethod + def _operation( + cls, + owner_path: str, + role: str, + symbol_name: str, + parameters: tuple[NativeEntrypointABIValuePlan, ...] = (), + result: NativeEntrypointABIValuePlan | None = None, + *, + implementation_owner: GeneratedSupportProcedureImplementationOwner = ( + GeneratedSupportProcedureImplementationOwner.FORTRAN + ), + ) -> GeneratedSupportProcedureEntrypointPlan: + return GeneratedSupportProcedureEntrypointPlan( + key=f"{owner_path}::{role}", + owner_path=owner_path, + role=role, + symbol_name=symbol_name, + signature=NativeEntrypointSignaturePlan( + parameters=parameters, + result=result or cls._void_result(), + ), + implementation_owner=implementation_owner, + ) + + @classmethod + def _opaque_parameter( + cls, + role: str, + *, + c_name: str | None = None, + fortran_name: str | None = None, + output: bool = False, + intent: str | None = None, + ) -> NativeEntrypointABIValuePlan: + return cls._value( + role, + NativeEntrypointABIValueKind.OPAQUE, + c_name=c_name, + fortran_name=fortran_name, + pointer_depth=2 if output else 1, + intent=intent, + ) + + @classmethod + def _opaque_result(cls) -> NativeEntrypointABIValuePlan: + return cls._value("result", NativeEntrypointABIValueKind.OPAQUE, pointer_depth=1) + + @classmethod + def _scalar_parameter( + cls, + semantic_type_name: str, + *, + role: str = "value", + reference: bool = False, + ) -> NativeEntrypointABIValuePlan: + return cls._value( + role, + NativeEntrypointABIValueKind.SEMANTIC_SCALAR, + pointer_depth=int(reference), + semantic_type_name=semantic_type_name, + ) + + @classmethod + def _scalar_result(cls, semantic_type_name: str) -> NativeEntrypointABIValuePlan: + return cls._value( + "result", + NativeEntrypointABIValueKind.SEMANTIC_SCALAR, + semantic_type_name=semantic_type_name, + ) + + @classmethod + def _bool_result(cls) -> NativeEntrypointABIValuePlan: + return cls._value("result", NativeEntrypointABIValueKind.BOOL) + + @classmethod + def _int_result(cls) -> NativeEntrypointABIValuePlan: + return cls._value("result", NativeEntrypointABIValueKind.INT) + + @classmethod + def _int64_result(cls) -> NativeEntrypointABIValuePlan: + return cls._value("result", NativeEntrypointABIValueKind.INT64) + + @classmethod + def _int64_parameter( + cls, + name: str, + *, + reference: bool = False, + intent: str | None = None, + ) -> NativeEntrypointABIValuePlan: + return cls._value( + name, + NativeEntrypointABIValueKind.INT64, + pointer_depth=int(reference), + intent=intent, + ) + + @classmethod + def _descriptor_parameter( + cls, + name: str, + handle: NativeArrayHandlePlan, + semantic_type_name: str, + *, + intent: str, + ) -> NativeEntrypointABIValuePlan: + return cls._value( + name, + NativeEntrypointABIValueKind.DESCRIPTOR, + pointer_depth=1, + semantic_type_name=semantic_type_name, + rank=handle.array.rank, + descriptor_kind=handle.descriptor_kind, + intent=intent, + ) + + @classmethod + def _descriptor_callback_parameter( + cls, + *, + semantic_type_name: str, + rank: int, + descriptor_kind, + c_name: str = "callback", + fortran_name: str = "callback_address", + ) -> NativeEntrypointABIValuePlan: + descriptor = cls._value( + "descriptor", + NativeEntrypointABIValueKind.DESCRIPTOR, + pointer_depth=1, + semantic_type_name=semantic_type_name, + rank=rank, + descriptor_kind=descriptor_kind, + intent="inout", + ) + context = cls._opaque_parameter("context") + return cls._value( + "callback", + NativeEntrypointABIValueKind.CALLBACK, + c_name=c_name, + fortran_name=fortran_name, + callback_signature=NativeEntrypointSignaturePlan((descriptor, context), cls._void_result()), + ) + + # ------------------------------------------------------------------ + # Callback trampoline boundary + # ------------------------------------------------------------------ + + def _callback_operations(self) -> tuple[GeneratedSupportProcedureEntrypointPlan, ...]: + return tuple(callback.entrypoint.support_procedure for callback in self._callback_sites()) + + def _callback_sites(self) -> tuple[CallbackHandoffPlan, ...]: + return tuple( + argument.callback + for function in self.functions + for argument in sorted(function.arguments, key=lambda item: item.native_position) + if argument.callback is not None + ) + + def _callback_transfer_parameters( + self, + transfer: CallbackTransferPlan, + ) -> tuple[NativeEntrypointABIValuePlan, ...]: + base = re.sub(r"\W", "_", transfer.name).casefold() + if transfer.abi is CallbackABIKind.VALUE: + return (self._scalar_parameter(transfer.semantic_type_name, role=base),) + parameters = [self._opaque_parameter(f"{base}_data")] + if transfer.abi is CallbackABIKind.DATA_AND_SHAPE: + parameters.extend(self._int64_parameter(f"{base}_extent_{axis}") for axis in range(transfer.rank)) + elif transfer.abi is CallbackABIKind.DATA_AND_LENGTH: + parameters.append(self._int64_parameter(f"{base}_length")) + return tuple(parameters) + + def _callback_result_from_plan(self, result) -> NativeEntrypointABIValuePlan: + transfer = result.transfer + if result.action is CallbackResultAction.RETURN_VOID: + return self._void_result() + if result.action is CallbackResultAction.RETURN_SCALAR and transfer is not None: + return self._scalar_result(transfer.semantic_type_name) + return self._opaque_result() + + # ------------------------------------------------------------------ + # Constructors and derived lifecycles + # ------------------------------------------------------------------ + + def _class_constructor_operations(self) -> tuple[GeneratedSupportProcedureEntrypointPlan, ...]: + return tuple( + self._operation( + surface.owner_path, + "class:create", + f"bind_c_prik_create_{surface.type_identity[1].casefold()}", + result=self._opaque_result(), + ) + for surface in self.classes + if surface.constructor.kind is not ClassConstructorKind.ABSENT + ) + + def _derived_destroy_operation(self, derived: DerivedTypePlan) -> GeneratedSupportProcedureEntrypointPlan: + return self._operation( + derived.owner_path, + "derived:destroy", + f"bind_c_prik_destroy_{derived.backend_symbol.casefold()}", + (self._opaque_parameter("address"),), + ) + + def _holder_destroy_operation( + self, derived: DerivedTypePlan, holder: str + ) -> GeneratedSupportProcedureEntrypointPlan: + return self._operation( + derived.owner_path, + f"holder:{holder}:destroy", + f"bind_c_prik_destroy_{derived.backend_symbol.casefold()}_{holder}_holder", + (self._opaque_parameter("address"),), + ) + + def _holder_presence_operation( + self, derived: DerivedTypePlan, holder: str + ) -> GeneratedSupportProcedureEntrypointPlan: + return self._operation( + derived.owner_path, + f"holder:{holder}:present", + f"bind_c_prik_{derived.backend_symbol.casefold()}_{holder}_holder_present", + (self._opaque_parameter("address"),), + self._bool_result(), + ) + + def _owned_derived_types(self) -> tuple[DerivedTypePlan, ...]: + identities = { + result.derived.type_identity + for function in self.functions + for result in function.results + if result.derived is not None + and result.derived.release is DerivedRelease.WRAPPER_DESTROY + and result.derived.storage + not in {DerivedObjectStorage.ALLOCATABLE_HOLDER, DerivedObjectStorage.POINTER_HOLDER} + } + identities.update( + variable.derived.handoff.type_identity + for variable in self.variables + if variable.derived is not None and variable.derived.access is ModuleObjectAccessMechanism.VALUE_COPY + ) + identities.update( + surface.type_identity + for surface in self.classes + if surface.constructor.kind is not ClassConstructorKind.ABSENT + ) + return tuple(derived for derived in self.derived_types if derived.type_identity in identities) + + def _allocatable_holder_types(self) -> tuple[DerivedTypePlan, ...]: + identities = { + result.derived.type_identity + for function in self.functions + for result in function.results + if result.derived is not None and result.derived.storage is DerivedObjectStorage.ALLOCATABLE_HOLDER + } + identities.update( + argument.derived.type_identity + for function in self.functions + for argument in function.arguments + if argument.derived is not None + and argument.derived_call is not None + and any( + case.access is DerivedActualAccess.ALLOCATABLE_HOLDER + for case in argument.derived_call.cases + if case.action is not DerivedCallAction.INCOMPATIBLE + ) + ) + return tuple(derived for derived in self.derived_types if derived.type_identity in identities) + + def _pointer_holder_types(self) -> tuple[DerivedTypePlan, ...]: + identities = { + result.derived.type_identity + for function in self.functions + for result in function.results + if result.derived is not None and result.derived.storage is DerivedObjectStorage.POINTER_HOLDER + } + identities.update( + argument.derived.type_identity + for function in self.functions + for argument in function.arguments + if argument.derived is not None + and argument.derived_call is not None + and any( + case.access is DerivedActualAccess.POINTER_HOLDER + for case in argument.derived_call.cases + if case.action is not DerivedCallAction.INCOMPATIBLE + ) + ) + return tuple(derived for derived in self.derived_types if derived.type_identity in identities) + + def _allocatable_holder_field_types(self) -> tuple[DerivedTypePlan, ...]: + identities = { + result.derived.type_identity + for function in self.functions + for result in function.results + if result.derived is not None and result.derived.storage is DerivedObjectStorage.ALLOCATABLE_HOLDER + } + identities.update( + argument.derived.type_identity + for function in self.functions + for argument in function.arguments + if argument.derived is not None + and argument.derived_call is not None + and argument.entrypoint.descriptor_output_role is not None + and any( + case.access is DerivedActualAccess.ALLOCATABLE_HOLDER + for case in argument.derived_call.cases + if case.action is not DerivedCallAction.INCOMPATIBLE + ) + ) + return tuple(derived for derived in self.derived_types if derived.type_identity in identities) + + def _pointer_holder_field_types(self) -> tuple[DerivedTypePlan, ...]: + identities = { + result.derived.type_identity + for function in self.functions + for result in function.results + if result.derived is not None and result.derived.storage is DerivedObjectStorage.POINTER_HOLDER + } + identities.update( + argument.derived.type_identity + for function in self.functions + for argument in function.arguments + if argument.derived is not None + and argument.derived_call is not None + and argument.entrypoint.descriptor_output_role is not None + and any( + case.access is DerivedActualAccess.POINTER_HOLDER + for case in argument.derived_call.cases + if case.action is not DerivedCallAction.INCOMPATIBLE + ) + ) + return tuple(derived for derived in self.derived_types if derived.type_identity in identities) + + # ------------------------------------------------------------------ + # Derived fields and module-derived members + # ------------------------------------------------------------------ + + def _derived_field_operations( + self, + allocatable_holders: tuple[DerivedTypePlan, ...], + pointer_holders: tuple[DerivedTypePlan, ...], + ) -> tuple[GeneratedSupportProcedureEntrypointPlan, ...]: + operations = [] + for derived in self.derived_types: + for field in derived.fields: + operations.extend(self._field_operations(derived, field, "direct")) + for variable in self.variables: + if variable.derived is None or variable.derived.access is not ModuleObjectAccessMechanism.MEMBER_PROXY: + continue + for member in variable.derived.member_paths: + operations.extend(self._field_operations((variable, member), member.field, "module")) + for derived in allocatable_holders: + for field in derived.fields: + operations.extend(self._field_operations(derived, field, "allocatable")) + for derived in pointer_holders: + for field in derived.fields: + operations.extend(self._field_operations(derived, field, "pointer")) + return tuple(operations) + + def _field_operations( + self, + owner: DerivedTypePlan | tuple[ModuleVariablePlan, DerivedMemberPathPlan], + field: DerivedFieldPlan, + route: str, + ) -> tuple[GeneratedSupportProcedureEntrypointPlan, ...]: + owner_path = self._field_owner_path(owner, field) + owner_parameter = route != "module" + if route in {"allocatable", "pointer"}: + if field.access is not DerivedFieldAccessMechanism.SCALAR_VALUE: + raise ValueError(f"Unsupported {route}-holder field entrypoint for {field.owner_path!r}") + return self._scalar_field_operations(owner, field, route, owner_path, owner_parameter=True) + if field.access is DerivedFieldAccessMechanism.NATIVE_ARRAY_HANDLE: + return self._field_handle_operations(owner, field, route, owner_path, owner_parameter) + if field.access is DerivedFieldAccessMechanism.FIXED_STRING_COPY: + return self._string_field_operations(owner, field, route, owner_path, owner_parameter) + if field.access is DerivedFieldAccessMechanism.ORDINARY_ARRAY_DESCRIPTOR: + return self._ordinary_array_field_operations(owner, field, route, owner_path, owner_parameter) + if route == "module" and field.object_kind is ObjectKind.DERIVED_TYPE: + return self._nested_module_field_operations(owner, field, route, owner_path) + return self._scalar_field_operations( + owner, + field, + route, + owner_path, + owner_parameter=owner_parameter, + ) + + def _scalar_field_operations(self, owner, field, route, owner_path, *, owner_parameter): + parameters = (self._opaque_parameter("owner", fortran_name="owner_address"),) if owner_parameter else () + result = ( + self._opaque_result() + if field.object_kind is ObjectKind.DERIVED_TYPE + else self._scalar_result(field.semantic_type_name) + ) + operations = [ + self._operation( + owner_path, + f"field:{route}:get", + self._field_symbol(owner, field, route, "get"), + parameters, + result, + ) + ] + if field.setter_action is SetterAction.WRITE_THROUGH: + value = ( + self._opaque_parameter("value", fortran_name="value_address") + if field.object_kind is ObjectKind.DERIVED_TYPE + else self._scalar_parameter(field.semantic_type_name) + ) + operations.append( + self._operation( + owner_path, + f"field:{route}:set", + self._field_symbol(owner, field, route, "set"), + (*parameters, value), + ) + ) + return tuple(operations) + + def _nested_module_field_operations(self, owner, field, route, owner_path): + if field.setter_action is not SetterAction.WRITE_THROUGH: + return () + return ( + self._operation( + owner_path, + f"field:{route}:set", + self._field_symbol(owner, field, route, "set"), + (self._opaque_parameter("value", fortran_name="value_address"),), + ), + ) + + def _string_field_operations(self, owner, field, route, owner_path, owner_parameter): + owner_values = (self._opaque_parameter("owner", fortran_name="owner_address"),) if owner_parameter else () + output = self._value( + "value", + NativeEntrypointABIValueKind.CHARACTER, + pointer_depth=1, + character_length=field.character_length, + intent="out", + ) + operations = [ + self._operation( + owner_path, + f"field:{route}:get", + self._field_symbol(owner, field, route, "get"), + (*owner_values, output), + ) + ] + if field.setter_action is SetterAction.WRITE_THROUGH: + value = self._value( + "value", + NativeEntrypointABIValueKind.CHARACTER, + pointer_depth=1, + const=True, + character_length=field.character_length, + intent="in", + ) + operations.append( + self._operation( + owner_path, + f"field:{route}:set", + self._field_symbol(owner, field, route, "set"), + (*owner_values, value), + ) + ) + return tuple(operations) + + def _ordinary_array_field_operations(self, owner, field, route, owner_path, owner_parameter): + owner_values = (self._opaque_parameter("owner", fortran_name="owner_address"),) if owner_parameter else () + callback = self._descriptor_callback_parameter( + semantic_type_name=field.semantic_type_name, + rank=field.array.rank, + descriptor_kind=None, + ) + operations = [ + self._operation( + owner_path, + f"field:{route}:get", + self._field_symbol(owner, field, route, "get"), + (*owner_values, callback, self._opaque_parameter("context")), + ) + ] + if field.setter_action is SetterAction.WRITE_THROUGH: + operations.append( + self._operation( + owner_path, + f"field:{route}:set", + self._field_symbol(owner, field, route, "set"), + (*owner_values, self._opaque_parameter("value", fortran_name="value_address")), + ) + ) + return tuple(operations) + + def _field_handle_operations(self, owner, field, route, owner_path, owner_parameter): + handle = field.native_array_handle + if handle is None or handle.array.rank is None: + raise ValueError(f"Native handle field {field.owner_path!r} has no completed rank") + owner_values = (self._opaque_parameter("owner", fortran_name="owner_address"),) if owner_parameter else () + operations = [] + for operation in handle.operations: + if operation in _FIELD_HANDLE_LOCAL_OPERATIONS: + continue + signature = self._field_handle_signature(field, handle, operation, owner_values) + operations.append( + self._operation( + owner_path, + f"field:{route}:handle:{operation.value}", + self._field_handle_symbol(owner, field, route, operation), + signature.parameters, + signature.result, + ) + ) + return tuple(operations) + + def _field_handle_signature(self, field, handle, operation, owner_values): + if operation in { + NativeArrayOperation.ALLOCATED, + NativeArrayOperation.ASSOCIATED, + NativeArrayOperation.CONTIGUOUS, + }: + return NativeEntrypointSignaturePlan(owner_values, self._bool_result()) + if operation is NativeArrayOperation.ELEMENT_LENGTH: + return NativeEntrypointSignaturePlan(owner_values, self._int64_result()) + if operation is NativeArrayOperation.SHAPE: + extents = tuple( + self._int64_parameter(f"extent_{axis}", reference=True) for axis in range(handle.array.rank) + ) + return NativeEntrypointSignaturePlan((*owner_values, *extents), self._void_result()) + if operation is NativeArrayOperation.DESCRIPTOR: + callback = self._descriptor_callback_parameter( + semantic_type_name=field.semantic_type_name, + rank=handle.array.rank, + descriptor_kind=handle.descriptor_kind, + ) + return NativeEntrypointSignaturePlan( + (*owner_values, callback, self._opaque_parameter("context")), self._void_result() + ) + if operation is NativeArrayOperation.ASSOCIATE: + source = self._descriptor_parameter("source", handle, field.semantic_type_name, intent="in") + return NativeEntrypointSignaturePlan((*owner_values, source), self._void_result()) + if operation in {NativeArrayOperation.ALLOCATE, NativeArrayOperation.RESIZE}: + extents = tuple(self._int64_parameter(f"extent_{axis}") for axis in range(handle.array.rank)) + return NativeEntrypointSignaturePlan((*owner_values, *extents), self._void_result()) + if operation in {NativeArrayOperation.DEALLOCATE, NativeArrayOperation.NULLIFY}: + return NativeEntrypointSignaturePlan(owner_values, self._void_result()) + raise ValueError(f"Unsupported native field handle operation {operation.value!r}") + + @staticmethod + def _field_owner_path(owner, field: DerivedFieldPlan) -> str: + if isinstance(owner, DerivedTypePlan): + return f"{owner.owner_path}.{field.name}" + variable, member = owner + return ".".join((variable.owner_path, *member.path)) + + @staticmethod + def _derived_field_stem(derived: DerivedTypePlan, field: DerivedFieldPlan) -> str: + return f"{derived.backend_symbol}_{field.name}".casefold() + + @staticmethod + def _module_member_stem(variable: ModuleVariablePlan, member: DerivedMemberPathPlan) -> str: + return "_".join((variable.symbol_name, *member.path)).casefold() + + def _field_symbol(self, owner, field, route, action): + if isinstance(owner, tuple): + variable, member = owner + return f"bind_c_prik_module_field_{self._module_member_stem(variable, member)}_{action}" + stem = self._derived_field_stem(owner, field) + prefix = { + "direct": "bind_c_prik_field", + "allocatable": "bind_c_prik_allocatable_holder_field", + "pointer": "bind_c_prik_pointer_holder_field", + }[route] + return f"{prefix}_{stem}_{action}" + + def _field_handle_symbol(self, owner, field, route, operation): + if isinstance(owner, tuple): + variable, member = owner + return f"bind_c_prik_module_field_handle_{self._module_member_stem(variable, member)}_{operation.value}" + return f"bind_c_prik_field_handle_{self._derived_field_stem(owner, field)}_{operation.value}" + + # ------------------------------------------------------------------ + # Owned/default descriptor operations + # ------------------------------------------------------------------ + + def _owned_native_array_operations(self) -> tuple[GeneratedSupportProcedureEntrypointPlan, ...]: + operations = [] + transfers: list[ArgumentTransferPlan | ResultPlan] = [ + result + for function in self.functions + for result in function.results + if result.native_array_handle is not None + and result.native_array_handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE + and result.datatype_family.value != "string" + ] + transfers.extend( + argument + for function in self.functions + for argument in function.arguments + if argument.native_array_handle is not None + and argument.native_array_handle.default_handle.construction + is NativeArrayDefaultConstruction.LAZY_OWNED_DESCRIPTOR + ) + for transfer in transfers: + handle = transfer.native_array_handle + selected = handle.operations if isinstance(transfer, ResultPlan) else handle.default_handle.operations + for operation in selected: + if operation not in _OWNED_HANDLE_ENTRYPOINT_OPERATIONS: + continue + signature = self._owned_native_array_signature(transfer, handle, operation) + preferred = transfer.entrypoint.parameter_name or "result" + owner = NativeSymbolNames.compact(transfer.owner_path, preferred, limit=38) + operations.append( + self._operation( + transfer.owner_path, + f"native_array:owned:{operation.value}", + f"bind_c_owned_{owner}_{operation.value}", + signature.parameters, + signature.result, + ) + ) + return tuple(operations) + + def _owned_native_array_signature(self, transfer, handle, operation): + intent = ( + "inout" + if operation + in { + NativeArrayOperation.ASSOCIATE, + NativeArrayOperation.DEALLOCATE, + NativeArrayOperation.NULLIFY, + NativeArrayOperation.DESTROY, + } + else "in" + ) + result = self._descriptor_parameter("result", handle, transfer.semantic_type_name, intent=intent) + if operation in { + NativeArrayOperation.ALLOCATED, + NativeArrayOperation.ASSOCIATED, + NativeArrayOperation.CONTIGUOUS, + }: + return NativeEntrypointSignaturePlan((result,), self._bool_result()) + if operation is NativeArrayOperation.SHAPE: + extents = tuple( + self._int64_parameter(f"extent_{axis}", reference=True) for axis in range(handle.array.rank) + ) + return NativeEntrypointSignaturePlan((result, *extents), self._void_result()) + if operation is NativeArrayOperation.ASSOCIATE: + source = self._descriptor_parameter("source", handle, transfer.semantic_type_name, intent="in") + return NativeEntrypointSignaturePlan((result, source), self._void_result()) + return NativeEntrypointSignaturePlan((result,), self._void_result()) + + # ------------------------------------------------------------------ + # Module variables and native-array module operations + # ------------------------------------------------------------------ + + def _module_variable_operations(self) -> tuple[GeneratedSupportProcedureEntrypointPlan, ...]: + operations = [] + for variable in self.variables: + operations.extend(self._primary_module_variable_operations(variable)) + if variable.bridge.native_getter_action is ModuleGetterAction.NATIVE_ARRAY_HANDLE: + operations.extend(self._module_native_array_operations(variable)) + if self._nullable_derived_module_proxy(variable): + operations.append( + self._operation( + variable.owner_path, + "module:derived:present", + f"bind_c_prik_module_{variable.symbol_name.casefold()}_present", + result=self._bool_result(), + ) + ) + return tuple(operations) + + def _primary_module_variable_operations(self, variable): + operations = [] + if ( + variable.entrypoint.getter_role is not None + and variable.bridge.native_getter_action is not ModuleGetterAction.NATIVE_ARRAY_HANDLE + ): + if variable.bridge.native_getter_action in { + 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) + ) + result = self._opaque_result() + elif variable.bridge.native_getter_action in { + ModuleGetterAction.NULLABLE_SNAPSHOT, + ModuleGetterAction.DERIVED_OBJECT, + }: + parameters = () + result = self._opaque_result() + else: + parameters = () + result = self._scalar_result(variable.semantic_type_name) + operations.append( + self._operation( + variable.owner_path, + "module:get", + f"bind_c_get_{variable.symbol_name}", + parameters, + result, + ) + ) + if variable.entrypoint.setter_role is not None: + operations.append( + self._operation( + variable.owner_path, + "module:set", + f"bind_c_set_{variable.symbol_name}", + (self._scalar_parameter(variable.semantic_type_name),), + ) + ) + return tuple(operations) + + def _module_native_array_operations(self, variable): + handle = variable.native_array_handle + if handle is None or handle.array.rank is None: + raise ValueError(f"Module handle {variable.owner_path!r} has no completed operation plan") + operations = [] + for operation in handle.operations: + if operation in _MODULE_HANDLE_LOCAL_OPERATIONS: + continue + signature = self._module_native_array_signature(variable, handle, operation) + if signature is None: + continue + operations.append( + self._operation( + variable.owner_path, + f"module:native_array:{operation.value}", + f"bind_c_{variable.symbol_name}_{operation.value}", + signature.parameters, + signature.result, + ) + ) + return tuple(operations) + + def _module_native_array_signature(self, variable, handle, operation): + if operation in { + NativeArrayOperation.ALLOCATED, + NativeArrayOperation.ASSOCIATED, + NativeArrayOperation.CONTIGUOUS, + }: + return NativeEntrypointSignaturePlan((), self._bool_result()) + if operation is NativeArrayOperation.ELEMENT_LENGTH: + return NativeEntrypointSignaturePlan((), self._int64_result()) + if operation is NativeArrayOperation.ARRAY_ACTUAL: + if self._uses_module_allocatable_descriptor(variable): + return self._module_descriptor_callback_signature(variable, handle) + return NativeEntrypointSignaturePlan((), self._opaque_result()) + if operation is NativeArrayOperation.SHAPE: + extents = tuple( + self._int64_parameter(f"extent_{axis}", reference=True, intent="out") + for axis in range(handle.array.rank) + ) + return NativeEntrypointSignaturePlan(extents, self._void_result()) + if operation is NativeArrayOperation.DESCRIPTOR: + if self._uses_module_allocatable_descriptor(variable): + return self._module_descriptor_callback_signature(variable, handle) + if handle.descriptor_kind.value != "pointer": + return None + descriptor = self._descriptor_parameter("descriptor", handle, variable.semantic_type_name, intent="out") + return NativeEntrypointSignaturePlan((descriptor,), self._void_result()) + if operation is NativeArrayOperation.ASSOCIATE: + source = self._descriptor_parameter("source", handle, variable.semantic_type_name, intent="in") + return NativeEntrypointSignaturePlan((source,), self._void_result()) + if operation in {NativeArrayOperation.ALLOCATE, NativeArrayOperation.RESIZE}: + extents = tuple(self._int64_parameter(f"extent_{axis}") for axis in range(handle.array.rank)) + return NativeEntrypointSignaturePlan(extents, self._void_result()) + if operation in {NativeArrayOperation.DEALLOCATE, NativeArrayOperation.NULLIFY}: + return NativeEntrypointSignaturePlan((), self._void_result()) + raise ValueError(f"Unsupported module native-array entrypoint {operation.value!r}") + + def _module_descriptor_callback_signature(self, variable, handle): + callback = self._descriptor_callback_parameter( + semantic_type_name=variable.semantic_type_name, + rank=handle.array.rank, + descriptor_kind=handle.descriptor_kind, + ) + return NativeEntrypointSignaturePlan((callback, self._opaque_parameter("context")), self._void_result()) + + @staticmethod + def _uses_module_allocatable_descriptor(variable: ModuleVariablePlan) -> bool: + handle = variable.native_array_handle + return bool( + handle is not None + and handle.descriptor_interop is NativeArrayDescriptorInterop.MODULE_ALLOCATABLE_C_DESCRIPTOR + ) + + @staticmethod + def _nullable_derived_module_proxy(variable: ModuleVariablePlan) -> bool: + return bool( + variable.derived is not None + and variable.derived.access is ModuleObjectAccessMechanism.MEMBER_PROXY + and variable.derived.handoff.storage + in { + DerivedObjectStorage.MODULE_ALLOCATABLE, + DerivedObjectStorage.MODULE_ALLOCATABLE_TARGET, + DerivedObjectStorage.MODULE_POINTER, + } + ) + + # ------------------------------------------------------------------ + # Derived module-origin transactions + # ------------------------------------------------------------------ + + def _derived_origin_operations(self) -> tuple[GeneratedSupportProcedureEntrypointPlan, ...]: + operations = [] + for variable in self.variables: + if variable.derived is None: + continue + stem = NativeSymbolNames.compact(variable.owner_path, variable.symbol_name) + for operation in ("present", "address", "scoped", "checkout", "restore"): + if not self._derived_origin_supports(variable, operation): + continue + signature = self._derived_origin_signature(operation) + operations.append( + self._operation( + variable.owner_path, + f"derived_origin:{operation}", + f"bind_c_prik_origin_{stem}_{operation}", + signature.parameters, + signature.result, + ) + ) + return tuple(operations) + + def _derived_origin_signature(self, operation): + if operation == "present": + return NativeEntrypointSignaturePlan((), self._bool_result()) + if operation == "address": + return NativeEntrypointSignaturePlan((), self._opaque_result()) + if operation == "scoped": + callback_signature = NativeEntrypointSignaturePlan( + (self._opaque_parameter("address"), self._opaque_parameter("context")), + self._int_result(), + ) + consumer = self._value( + "consumer", + NativeEntrypointABIValueKind.CALLBACK, + fortran_name="consumer", + c_type_name="prik_derived_consumer_fn", + callback_signature=callback_signature, + ) + return NativeEntrypointSignaturePlan((consumer, self._opaque_parameter("context")), self._int_result()) + if operation == "checkout": + return NativeEntrypointSignaturePlan( + (self._opaque_parameter("holder", fortran_name="holder_address", output=True, intent="out"),), + self._int_result(), + ) + if operation == "restore": + return NativeEntrypointSignaturePlan( + (self._opaque_parameter("holder", fortran_name="holder_address"),), self._int_result() + ) + raise ValueError(f"Unsupported derived-origin operation {operation!r}") + + @staticmethod + def _derived_origin_supports(variable: ModuleVariablePlan, operation: str) -> bool: + storage = variable.derived.handoff.storage + support = { + DerivedObjectStorage.MODULE_PROXY: {"scoped"}, + DerivedObjectStorage.MODULE_TARGET: {"address"}, + DerivedObjectStorage.MODULE_ALLOCATABLE: {"present", "scoped", "checkout", "restore"}, + DerivedObjectStorage.MODULE_ALLOCATABLE_TARGET: { + "present", + "address", + "checkout", + "restore", + }, + DerivedObjectStorage.MODULE_POINTER: {"present", "scoped", "checkout", "restore"}, + } + return operation in support.get(storage, set()) diff --git a/prik/planning/models.py b/prik/planning/models.py index 737dc39f5..d87c802ea 100644 --- a/prik/planning/models.py +++ b/prik/planning/models.py @@ -89,6 +89,10 @@ TransformationAction, TransformationLayer, WritebackPhase, + NativeEntrypointAction, + EntrypointPassingConvention, + EntrypointOptionalityAction, + EntrypointProjectionAction, ) from prik.utilities.stage_values import StageRecord @@ -115,6 +119,73 @@ class DatatypeFamily(Enum): CALLBACK = "callback" +class NativeEntrypointABIValueKind(Enum): + """Classify one value in a generated support procedure's C ABI.""" + + VOID = "void" + BOOL = "bool" + INT = "int" + INT8 = "int8" + INT64 = "int64" + OPAQUE = "opaque" + CHARACTER = "character" + SEMANTIC_SCALAR = "semantic_scalar" + DESCRIPTOR = "descriptor" + CALLBACK = "callback" + + +class GeneratedSupportProcedureImplementationOwner(Enum): + """Identify which generated side defines one support procedure.""" + + BINDING = "binding" + FORTRAN = "fortran" + + +@dataclass +class NativeEntrypointABIValuePlan(StageRecord): + """Describe one ordered parameter or result in a support-procedure C ABI. + + ``pointer_depth`` describes the C representation. The remaining structured + facts let C and Fortran lowering spell the same ABI without storing rendered + source text in planning. Callback values may carry their own nested C ABI + signature; named runtime callback typedefs use ``c_type_name``. + """ + + role: str + c_name: str + fortran_name: str + kind: NativeEntrypointABIValueKind + pointer_depth: int = 0 + const: bool = False + semantic_type_name: str | None = None + rank: int | None = None + character_length: int | None = None + descriptor_kind: NativeArrayDescriptorKind | None = None + intent: str | None = None + c_type_name: str | None = None + callback_signature: NativeEntrypointSignaturePlan | None = None + + +@dataclass +class NativeEntrypointSignaturePlan(StageRecord): + """Store one complete ordered generated-support entrypoint signature.""" + + parameters: tuple[NativeEntrypointABIValuePlan, ...] + result: NativeEntrypointABIValuePlan + + +@dataclass +class GeneratedSupportProcedureEntrypointPlan(StageRecord): + """Name one externally linked generated support procedure and its C ABI.""" + + key: str + owner_path: str + role: str + symbol_name: str + signature: NativeEntrypointSignaturePlan + implementation_owner: GeneratedSupportProcedureImplementationOwner + + # ============================================================================ # Derived types and generated class surfaces # ============================================================================ @@ -545,16 +616,61 @@ class BindingStatusErrorPlan(StageRecord): @dataclass class BindingModulePlan(StageRecord): - """Store the binding-facing owner identity for one generated module.""" + """Store module-wide binding surfaces selected before C lowering. + + The three owner-path inventories select static CPython capsule and holder + helpers. They are distinct from externally linked generated support + procedures, whose existence and complete ABI live in the entrypoint module + plan. + """ + + owner_path: str + owned_derived_type_owner_paths: tuple[str, ...] = () + allocatable_holder_type_owner_paths: tuple[str, ...] = () + pointer_holder_type_owner_paths: tuple[str, ...] = () + + +@dataclass +class NativeEntrypointModulePlan(StageRecord): + """Store the shared C-ABI owner and generated support procedures.""" owner_path: str + support_procedures: tuple[GeneratedSupportProcedureEntrypointPlan, ...] = () + native_languages: tuple[str, ...] = () + + +class NativeGeneratedCodeGroupKind(Enum): + """Classify independently planned generated-native membership.""" + + FORTRAN_ADAPTERS = "fortran_adapters" + FORTRAN_SUPPORT = "fortran_support" + + +@dataclass +class NativeGeneratedCodeGroupPlan(StageRecord): + """Group generated native members that share one physical source set.""" + + kind: NativeGeneratedCodeGroupKind + language: str + member_keys: tuple[str, ...] + source_paths: tuple[str, ...] @dataclass class BridgeModulePlan(StageRecord): - """Store the bridge-facing owner identity for one generated module.""" + """Store module-wide Fortran support selected before bridge lowering. + + Holder-definition inventories include every holder used by an adapter or + generated support procedure. The narrower field inventories select only + holder types whose values can cross back to Python and expose field + support. + """ owner_path: str + allocatable_holder_type_owner_paths: tuple[str, ...] = () + pointer_holder_type_owner_paths: tuple[str, ...] = () + allocatable_holder_field_type_owner_paths: tuple[str, ...] = () + pointer_holder_field_type_owner_paths: tuple[str, ...] = () @dataclass @@ -572,26 +688,32 @@ class BindingModuleVariablePlan(StageRecord): constant_value: Any +@dataclass +class NativeEntrypointModuleVariablePlan(StageRecord): + """Describe the C-ABI operations shared by module-variable lowerers.""" + + descriptor_kind: str | None + getter_role: str | None + setter_role: str | None + + @dataclass class BridgeModuleVariablePlan(StageRecord): """Describe native module-variable access selected by completed policy. - Binding and bridge views remain separate: this record contains native names, - assignment behavior, descriptor form, and symbolic getter/setter roles. + This record contains only original-Fortran naming and adapter-local access + behavior. The matching entrypoint record owns public C symbols and roles. """ native_name: str native_module: str - getter_action: ModuleGetterAction + native_getter_action: ModuleGetterAction native_assignment: AssignmentMode - descriptor_kind: str | None - getter_role: str | None - setter_role: str | None @dataclass class ModuleVariablePlan(StageRecord): - """Join binding and bridge views of one module-state value. + """Join binding, entrypoint, and bridge views of one module-state value. Optional array, native-handle, and derived-object facets are attached only when policy selected them. Namespace plans own these records for emission. @@ -602,6 +724,7 @@ class ModuleVariablePlan(StageRecord): semantic_type_name: str datatype_family: DatatypeFamily binding: BindingModuleVariablePlan + entrypoint: NativeEntrypointModuleVariablePlan bridge: BridgeModuleVariablePlan array: ArrayHandoffPlan | None native_array_handle: NativeArrayHandlePlan | None @@ -626,6 +749,31 @@ class BindingFunctionPlan(StageRecord): public: bool = True +@dataclass +class NativeEntrypointParameterPlan(StageRecord): + """Order one argument or result parameter group in the shared C ABI. + + The referenced argument or result entrypoint facet owns the group's exact + transport. ``position`` orders groups after any direct function return. + """ + + owner_path: str + position: int + source_kind: str + native_position: int | None = None + + +@dataclass +class NativeEntrypointFunctionPlan(StageRecord): + """Describe one shared C-ABI symbol, return, and ordered parameter groups.""" + + symbol_name: str + action: NativeEntrypointAction + parameters: tuple[NativeEntrypointParameterPlan, ...] + results: tuple[NativeEntrypointResultPlan, ...] + projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...] + + @dataclass class BridgeFunctionPlan(StageRecord): """Store native invocation and declaration facts for one bridge procedure. @@ -655,46 +803,58 @@ class ClassCallPlan(StageRecord): @dataclass class BindingArgumentPlan(StageRecord): - """Describe Python input conversion and the binding-to-bridge handoff. + """Describe Python input conversion and the binding-to-entrypoint handoff. Argument transfers own this binding view. Its barrier action, conversion - phase, optionality, mutability, and symbolic role are complete policy facts. + phase, Python optionality, mutability, and local extraction facts are + complete policy decisions. Shared C-ABI roles live only in the entrypoint + facet. """ python_name: str python_action: PythonBarrierAction codegen_action: CodegenAction conversion_phase: ArgumentConversionPhase - handoff_role: str optional_mode: OptionalMode nullable: bool writable: bool descriptor_boundary: bool + + +@dataclass +class NativeEntrypointArgumentPlan(StageRecord): + """Describe one binding-to-entrypoint C-ABI parameter group.""" + + parameter_name: str + handoff_mode: ArgumentHandoffMode + handoff_role: str + optional_mode: OptionalMode + presence_role: str | None + passing: EntrypointPassingConvention + optionality: EntrypointOptionalityAction + pass_character_length: bool = False + pass_array_metadata: bool = False + pass_descriptor_presence: bool = False + pass_derived_transaction: bool = False + pass_callback_parameter: bool = False length_handoff_role: str | None = None + descriptor_output_role: str | None = None + descriptor_output_presence_role: str | None = None @dataclass class BridgeArgumentPlan(StageRecord): - """Describe bridge ABI transport and native argument conversion. + """Describe adapter-local conversion into one original Fortran argument. - The transfer's matching binding view supplies the same handoff role; this - record adds the native ABI position, data action, copy reason, and optional - descriptor-output roles required by bridge lowering. + The native name identifies the original dummy. C-ABI transport and ordering + live in the matching native-entrypoint facet. """ native_name: str native_action: NativeBarrierAction codegen_action: CodegenAction - handoff_mode: ArgumentHandoffMode data_action: BridgeDataAction copy_reason: str | None - abi_position: int - handoff_role: str - optional_mode: OptionalMode - presence_role: str | None - length_handoff_role: str | None = None - descriptor_output_role: str | None = None - descriptor_output_presence_role: str | None = None @dataclass @@ -706,17 +866,35 @@ class BindingResultPlan(StageRecord): python_result_role: str +@dataclass +class NativeEntrypointResultPlan(StageRecord): + """Describe one native-to-binding result at the shared C-ABI boundary.""" + + owner_path: str + parameter_name: str | None + source_kind: str + result_position: int | None + native_result_role: str + direct_result_abi: DirectResultABI + semantic_type_name: str + datatype_family: DatatypeFamily + object_kind: ObjectKind + character_length: int | None + array: ArrayHandoffPlan | None + native_array_handle: NativeArrayHandlePlan | None + scalar_descriptor: ScalarDescriptorResultPlan | None + passing: EntrypointPassingConvention + + @dataclass class BridgeResultPlan(StageRecord): - """Describe bridge-side production, ABI transport, and data action for one result.""" + """Describe adapter-local production and conversion of one Fortran result.""" codegen_action: CodegenAction native_action: NativeBarrierAction data_action: BridgeDataAction copy_reason: str | None - native_result_role: str native_name: str | None - abi_position: int | None @dataclass @@ -740,12 +918,23 @@ class BridgeLifecyclePlan(StageRecord): @dataclass -class NativeCallSlotPlan(StageRecord): - """Represent one ordered native ABI slot shared with its owning transfer. +class BridgeCallSlotPlan(StageRecord): + """Store only adapter-local actions for one projected call slot.""" - Function plans index slots in native-call order, while argument and hidden - result transfers hold references to the same mutable records. The shared - identity is intentional: validation checks the views agree before freezing. + native_action: NativeBarrierAction + codegen_action: CodegenAction + bridge_data_action: BridgeDataAction + bridge_copy_reason: str | None + + +@dataclass +class NativeEntrypointProjectedSlotPlan(StageRecord): + """Authoritative ordered binding projection with an optional adapter facet. + + This record owns the route-neutral projection source, order, shared C-ABI + transport, and boundary type facts. Adapter-only conversion and original + Fortran invocation facts live in ``adapter`` and are never copied into this + entrypoint facet. """ owner_path: str @@ -756,10 +945,6 @@ class NativeCallSlotPlan(StageRecord): native_name: str value_kind: str symbolic_role: str - native_action: NativeBarrierAction - codegen_action: CodegenAction - bridge_data_action: BridgeDataAction - bridge_copy_reason: str | None object_kind: ObjectKind | None scalar_logical_abi: ScalarLogicalABI = ScalarLogicalABI.NOT_APPLICABLE scalar_native_type: str | None = None @@ -777,6 +962,10 @@ class NativeCallSlotPlan(StageRecord): native_array_handle: NativeArrayHandlePlan | None = None scalar_descriptor: ScalarDescriptorResultPlan | None = None derived: DerivedHandoffPlan | None = None + projection_action: EntrypointProjectionAction = EntrypointProjectionAction.BLOCKED + passing: EntrypointPassingConvention = EntrypointPassingConvention.BLOCKED + optionality: EntrypointOptionalityAction = EntrypointOptionalityAction.BLOCKED + adapter: BridgeCallSlotPlan | None = None @dataclass @@ -882,6 +1071,29 @@ class CallbackResultPlan(StageRecord): action: CallbackResultAction +@dataclass +class BindingCallbackPlan(StageRecord): + """Store binding-owned callback runtime symbols.""" + + context_type_symbol: str + context_current_symbol: str + abort_symbol: str + + +@dataclass +class NativeEntrypointCallbackPlan(StageRecord): + """Store the binding-implemented trampoline contract shared with Fortran.""" + + support_procedure: GeneratedSupportProcedureEntrypointPlan + + +@dataclass +class BridgeCallbackPlan(StageRecord): + """Store the adapter-local callback symbol used to call original Fortran.""" + + adapter_symbol: str + + @dataclass class CallbackHandoffPlan(StageRecord): """Describe one call-scoped callback context, adapter, transfers, and fatal contract. @@ -892,11 +1104,9 @@ class CallbackHandoffPlan(StageRecord): owner_path: str prototype: ProcedurePrototypePlan - context_type_symbol: str - context_current_symbol: str - adapter_symbol: str - trampoline_symbol: str - abort_symbol: str + binding: BindingCallbackPlan + entrypoint: NativeEntrypointCallbackPlan + bridge: BridgeCallbackPlan arguments: tuple[CallbackTransferPlan, ...] result: CallbackResultPlan lifecycle: tuple[CallbackLifecycleAction, ...] @@ -912,12 +1122,14 @@ class CallbackHandoffPlan(StageRecord): @dataclass class ArgumentTransferPlan(StageRecord): - """Represent one complete Python-to-native transfer and its shared ABI slot. + """Represent one complete Python-to-native transfer and bridge call slot. This is the primary datatype-varying plan record. It combines completed ownership, storage, nullability, mutation, projection, ABI, optional - array/derived/callback facets, and binding/bridge views. The planner shares - ``native_call_slot`` with ``FunctionPlan.native_call_slots`` by identity. + array/derived/callback facets, and the selected lowering views. The planner + shares ``projected_call_slot`` with the function entrypoint sequence by + identity; an adapter route may additionally reference its narrow bridge + facet. """ owner_path: str @@ -952,8 +1164,9 @@ class ArgumentTransferPlan(StageRecord): callback: CallbackHandoffPlan | None polymorphic: PolymorphicDispatchPlan | None binding: BindingArgumentPlan - bridge: BridgeArgumentPlan - native_call_slot: NativeCallSlotPlan + entrypoint: NativeEntrypointArgumentPlan + bridge: BridgeArgumentPlan | None + projected_call_slot: NativeEntrypointProjectedSlotPlan transformations: tuple[TransformationPlan, ...] = () @@ -961,9 +1174,10 @@ class ArgumentTransferPlan(StageRecord): class ResultPlan(StageRecord): """Represent one complete native-to-Python transfer and optional hidden ABI slot. - Direct function results omit ``native_call_slot``; hidden output results - share the corresponding function-wide slot. Binding and bridge facets hold - the completed projection and production choices consumed by each backend. + Direct function results omit ``projected_call_slot``; hidden output results + share the corresponding function-wide projected slot. Binding, entrypoint, + and bridge facets hold their completed projection, transport, and + production choices. """ owner_path: str @@ -972,7 +1186,6 @@ class ResultPlan(StageRecord): source_kind: str result_position: int character_length: int | None - direct_result_abi: DirectResultABI object_kind: ObjectKind ownership_owner: OwnershipOwner transfer_mode: TransferMode @@ -983,8 +1196,9 @@ class ResultPlan(StageRecord): array: ArrayHandoffPlan | None native_array_handle: NativeArrayHandlePlan | None binding: BindingResultPlan - bridge: BridgeResultPlan - native_call_slot: NativeCallSlotPlan | None = None + entrypoint: NativeEntrypointResultPlan + bridge: BridgeResultPlan | None + projected_call_slot: NativeEntrypointProjectedSlotPlan | None = None scalar_descriptor: ScalarDescriptorResultPlan | None = None derived: DerivedHandoffPlan | None = None transformations: tuple[TransformationPlan, ...] = () @@ -1017,18 +1231,19 @@ class FunctionPlan(StageRecord): """Orchestrate one generated call with stable ABI and lifecycle indexes. Namespace plans own functions. Arguments/results hold datatype-specific - facts, while this record owns native-call order, callable declarations, - available roles, and function-wide writeback, cleanup, and release order. + facts, while this record owns entrypoint and original-Fortran call order, + callable declarations, available roles, and function-wide writeback, + cleanup, and release order. """ owner_path: str symbol_name: str binding: BindingFunctionPlan - bridge: BridgeFunctionPlan + entrypoint: NativeEntrypointFunctionPlan + bridge: BridgeFunctionPlan | None class_call: ClassCallPlan | None arguments: tuple[ArgumentTransferPlan, ...] results: tuple[ResultPlan, ...] - native_call_slots: tuple[NativeCallSlotPlan, ...] declaration_callables: tuple[DeclarationCallablePlan, ...] available_roles: tuple[str, ...] writeback_actions: tuple[LifecycleActionPlan, ...] = () @@ -1079,16 +1294,18 @@ class NamespacePlan(StageRecord): class ModulePlan(StageRecord): """Serve as the root editable plan for one generated extension module. - Constructed by ``WrapperPlanner.build()``, this root joins binding and - bridge module views with an explicit namespace tree and required headers. - Pass it to ``WrapperGenerator.generate()``; generation validates then - freezes the graph before it renders artifacts. + Constructed by ``WrapperPlanner.build()``, this root joins binding, + native-entrypoint, and bridge module views with an explicit namespace tree + and required headers. Pass it to ``WrapperGenerator.generate()``; + generation validates then freezes the graph before it renders artifacts. """ owner_path: str binding: BindingModulePlan - bridge: BridgeModulePlan + entrypoint: NativeEntrypointModulePlan + bridge: BridgeModulePlan | None namespaces: tuple[NamespacePlan, ...] + native_generated_code_groups: tuple[NativeGeneratedCodeGroupPlan, ...] = () required_headers: tuple[str, ...] = () @@ -1121,21 +1338,29 @@ class WrapperPlanDiagnostic(StageRecord): native_module=None, native_is_subroutine=True, ) + entrypoint_function = NativeEntrypointFunctionPlan( + symbol_name="bind_c_ping", + action=NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER, + parameters=(), + results=(), + projected_slots=(), + ) function = FunctionPlan( owner_path="demo.ping", symbol_name="ping", binding=binding_function, + entrypoint=entrypoint_function, bridge=bridge_function, class_call=None, arguments=(), results=(), - native_call_slots=(), declaration_callables=(), available_roles=(), ) plan = ModulePlan( owner_path="demo", binding=BindingModulePlan(owner_path="demo"), + entrypoint=NativeEntrypointModulePlan(owner_path="demo"), bridge=BridgeModulePlan(owner_path="demo"), namespaces=(NamespacePlan(owner_path="demo", python_path=(), functions=(function,)),), ) @@ -1143,4 +1368,4 @@ class WrapperPlanDiagnostic(StageRecord): print(f"Plan owner: {plan.owner_path}") print(f"Python export: {plan.namespaces[0].functions[0].binding.python_name}") print(f"Native procedure: {plan.namespaces[0].functions[0].bridge.native_name}") - print(f"Native slots: {len(plan.namespaces[0].functions[0].native_call_slots)}") + print(f"Projected call slots: {len(plan.namespaces[0].functions[0].entrypoint.projected_slots)}") diff --git a/prik/planning/planner.py b/prik/planning/planner.py index b2acc46a2..e4f02f21d 100644 --- a/prik/planning/planner.py +++ b/prik/planning/planner.py @@ -2,10 +2,11 @@ ``WrapperPlanner`` is the boundary between post-IR policy completion and code-generation lowering. It consumes a fully completed -``SemanticModule`` and produces one ``ModulePlan`` that binding and bridge -generators can consume without making further semantic decisions. The -planner groups public exports into namespaces, shares native-call records -between their consumers, and names the backend roles needed by later stages. +``SemanticModule`` and produces one ``ModulePlan`` whose binding, shared +native-entrypoint, and bridge facets can be lowered without further semantic +decisions. The planner groups public exports into namespaces, shares original +Fortran call records between their consumers, and names the roles needed by +later stages. """ from __future__ import annotations @@ -32,6 +33,7 @@ DerivedFieldAccessMechanism, DerivedHandoffPolicy, DerivedTypePolicy, + DirectResultABI, ModuleGetterAction, ModuleObjectAccessMechanism, ModuleVariablePolicy, @@ -57,6 +59,7 @@ ScalarDescriptorResultPolicy, TransformationPolicy, WritebackPhase, + NativeEntrypointAction, ) from prik.policy.construction import ( completed_class_surface_policy, @@ -70,13 +73,16 @@ ArrayHandoffPlan, ArgumentTransferPlan, BindingArgumentPlan, + BindingCallbackPlan, BindingFunctionPlan, BindingLifecyclePlan, BindingModulePlan, BindingModuleVariablePlan, BindingResultPlan, BindingStatusErrorPlan, + BridgeCallSlotPlan, BridgeArgumentPlan, + BridgeCallbackPlan, BridgeFunctionPlan, BridgeLifecyclePlan, BridgeModulePlan, @@ -102,11 +108,22 @@ DerivedModuleObjectPlan, DerivedTypePlan, FunctionPlan, + GeneratedSupportProcedureEntrypointPlan, LifecycleActionPlan, ModulePlan, ModuleVariablePlan, + NativeGeneratedCodeGroupKind, + NativeGeneratedCodeGroupPlan, + GeneratedSupportProcedureImplementationOwner, + NativeEntrypointArgumentPlan, + NativeEntrypointCallbackPlan, + NativeEntrypointFunctionPlan, + NativeEntrypointModulePlan, + NativeEntrypointModuleVariablePlan, + NativeEntrypointParameterPlan, + NativeEntrypointProjectedSlotPlan, + NativeEntrypointResultPlan, NamespacePlan, - NativeCallSlotPlan, NativeArrayActualPlan, NativeArrayDefaultHandlePlan, NativeArrayHandlePlan, @@ -124,6 +141,11 @@ from prik.semantics.scalar_types import BOOLEAN_SEMANTIC_TYPE_NAMES from prik.utilities.visitor import ClassVisitor +from prik.planning.entrypoints import ( + build_callback_support_procedure_entrypoint, + build_generated_support_procedure_projection, +) + _DATATYPE_FAMILIES = { **dict.fromkeys(BOOLEAN_SEMANTIC_TYPE_NAMES, DatatypeFamily.BOOL), @@ -131,6 +153,7 @@ "Int16": DatatypeFamily.INTEGER, "Int32": DatatypeFamily.INTEGER, "Int64": DatatypeFamily.INTEGER, + "SizeT": DatatypeFamily.INTEGER, "Float32": DatatypeFamily.REAL, "Float64": DatatypeFamily.REAL, "Complex64": DatatypeFamily.COMPLEX, @@ -214,24 +237,38 @@ class _ClassPolicyCatalog: def from_module(cls, module: models.SemanticModule) -> _ClassPolicyCatalog: """Collect one module's completed class policies in source order. - The method recursively visits nested classes, creates one catalog entry - per public declaration, and gives planning one shared collection. For a - module containing public ``point`` followed by ``circle``, the returned + The method collects nested classes, creates one catalog entry per public + declaration, and gives planning one shared collection. For a module + containing public ``point`` followed by ``circle``, the returned ``entries`` tuple preserves exactly that order. """ + return cls.from_semantic_classes(cls.ordered_semantic_classes(module.classes)) + + @classmethod + def from_semantic_classes( + cls, + semantic_classes: tuple[models.SemanticClass, ...], + ) -> _ClassPolicyCatalog: + """Join one already ordered class collection to completed policies.""" entries = tuple( _ClassPolicyEntry.from_semantic_class(semantic_class) - for semantic_class in cls._semantic_classes(module.classes) + for semantic_class in semantic_classes if semantic_class.visibility == "public" ) return cls(entries=entries) - @classmethod - def _semantic_classes(cls, classes: list[models.SemanticClass]): - """Yield top-level and nested semantic classes in depth-first source order.""" - for semantic_class in classes: - yield semantic_class - yield from cls._semantic_classes(semantic_class.classes) + @staticmethod + def ordered_semantic_classes( + classes: list[models.SemanticClass], + ) -> tuple[models.SemanticClass, ...]: + """Collect top-level and nested classes in depth-first source order.""" + ordered = [] + pending = list(reversed(classes)) + while pending: + semantic_class = pending.pop() + ordered.append(semantic_class) + pending.extend(reversed(semantic_class.classes)) + return tuple(ordered) class WrapperPlanner(ClassVisitor): @@ -277,13 +314,18 @@ def _visit_SemanticModule(self, module: models.SemanticModule) -> ModulePlan: It raises before plan construction when the module has no public exports; it does not mutate semantic policy. """ - # Initialize the per-module indexes used by derived and field projections. - self._derived_type_names = {semantic_class.name for semantic_class in module.classes} + # Initialize every class-backed index from one complete ordered collection. + semantic_classes = _ClassPolicyCatalog.ordered_semantic_classes(module.classes) + class_policies = _ClassPolicyCatalog.from_semantic_classes(semantic_classes) + self._derived_type_names = {semantic_class.name for semantic_class in semantic_classes} self._derived_field_plans: dict[str, DerivedFieldPlan] = {} - self._complete_derived_backend_symbols(module) + self._complete_derived_backend_symbols(semantic_classes) # Project every public surface before linking private callable entries. - functions, variables, derived_types, classes, overloads = self._namespace_member_plans(module) + functions, variables, derived_types, classes, overloads = self._namespace_member_plans( + module, + class_policies, + ) if not any( (*functions.values(), *variables.values(), *derived_types.values(), *classes.values(), *overloads.values()) ): @@ -295,17 +337,92 @@ def _visit_SemanticModule(self, module: models.SemanticModule) -> ModulePlan: # Complete stable namespace paths, generated symbols, and required headers. namespaces = self._namespace_plans(module.name, functions, variables, derived_types, classes, overloads) + support_projection = build_generated_support_procedure_projection(namespaces) + support_procedures = support_projection.support_procedures + generated_code_groups = self._native_generated_code_groups( + module.name, + namespaces, + support_procedures, + ) return ModulePlan( owner_path=module.name, - binding=BindingModulePlan(module.name), - bridge=BridgeModulePlan(module.name), + binding=BindingModulePlan( + module.name, + support_projection.binding_owned_derived_type_owner_paths, + support_projection.binding_allocatable_holder_type_owner_paths, + support_projection.binding_pointer_holder_type_owner_paths, + ), + entrypoint=NativeEntrypointModulePlan( + module.name, + support_procedures, + ((module.origin.source_language,) if module.origin.source_language else ()), + ), + bridge=( + BridgeModulePlan( + module.name, + support_projection.bridge_allocatable_holder_type_owner_paths, + support_projection.bridge_pointer_holder_type_owner_paths, + support_projection.bridge_allocatable_holder_field_type_owner_paths, + support_projection.bridge_pointer_holder_field_type_owner_paths, + ) + if generated_code_groups + else None + ), namespaces=namespaces, + native_generated_code_groups=generated_code_groups, required_headers=self._required_headers(namespaces), ) + @staticmethod + def _native_generated_code_groups( + module_name: str, + namespaces: tuple[NamespacePlan, ...], + support_procedures: tuple[GeneratedSupportProcedureEntrypointPlan, ...], + ) -> tuple[NativeGeneratedCodeGroupPlan, ...]: + """Keep adapted-user and Fortran-support membership independently visible.""" + source_paths = (f"bind_c_{module_name}_wrapper.f90",) + adapter_members = tuple( + function.owner_path + for namespace in namespaces + for function in namespace.functions + if function.bridge is not None + ) + support_members = tuple( + procedure.key + for procedure in support_procedures + if procedure.implementation_owner is GeneratedSupportProcedureImplementationOwner.FORTRAN + ) + return ( + *( + ( + NativeGeneratedCodeGroupPlan( + kind=NativeGeneratedCodeGroupKind.FORTRAN_ADAPTERS, + language="fortran", + member_keys=adapter_members, + source_paths=source_paths, + ), + ) + if adapter_members + else () + ), + *( + ( + NativeGeneratedCodeGroupPlan( + kind=NativeGeneratedCodeGroupKind.FORTRAN_SUPPORT, + language="fortran", + member_keys=support_members, + source_paths=source_paths, + ), + ) + if support_members + else () + ), + ) + def _namespace_member_plans( self, module: models.SemanticModule, + class_policies: _ClassPolicyCatalog, ) -> tuple[dict, dict, dict, dict, dict]: """Build namespace-owned plan maps from one shared class-policy catalog. @@ -318,8 +435,6 @@ def _namespace_member_plans( functions = self._functions_by_namespace(module) variables = self._variables_by_namespace(module) - # Join each class to its completed policies and callables once for both projections. - class_policies = _ClassPolicyCatalog.from_module(module) return ( functions, variables, @@ -387,9 +502,12 @@ def _namespace_plan( overloads=overloads, ) - def _complete_derived_backend_symbols(self, module: models.SemanticModule) -> None: + def _complete_derived_backend_symbols( + self, + semantic_classes: tuple[models.SemanticClass, ...], + ) -> None: """Keep short native type names unless the complete unit needs qualification.""" - policies = tuple(completed_derived_type_policy(item) for item in module.classes) + policies = tuple(completed_derived_type_policy(item) for item in semantic_classes) counts = Counter(policy.native_type_name.casefold() for policy in policies) self._derived_backend_symbols = { policy.type_identity: self._derived_backend_symbol_for_policy(policy, counts) for policy in policies @@ -910,6 +1028,17 @@ def _complete_generated_symbols( if counts[item.symbol_name.casefold()] > 1: item.symbol_name = self._symbol_name(namespace, item.symbol_name) self._qualify_variable_bridge_collisions(functions, variables) + self._complete_entrypoint_symbols(functions) + + @staticmethod + def _complete_entrypoint_symbols( + functions: dict[tuple[str, ...], list[FunctionPlan]], + ) -> None: + """Finalize shared C symbols after all generated-name collisions resolve.""" + for items in functions.values(): + for function in items: + if function.entrypoint.action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER: + function.entrypoint.symbol_name = f"bind_c_{function.symbol_name}" def _qualify_variable_bridge_collisions( self, @@ -970,14 +1099,16 @@ def _module_variable_plan( initializer=policy.initializer, constant_value=policy.constant_value, ), + entrypoint=NativeEntrypointModuleVariablePlan( + descriptor_kind=policy.descriptor_kind, + getter_role=getter_role, + setter_role=setter_role, + ), bridge=BridgeModuleVariablePlan( native_name=policy.native_name, native_module=policy.native_module, - getter_action=policy.getter_action, + native_getter_action=policy.getter_action, native_assignment=policy.native_assignment, - descriptor_kind=policy.descriptor_kind, - getter_role=getter_role, - setter_role=setter_role, ), array=self._array_plan(policy.array, policy.owner_path), native_array_handle=self._native_array_handle_plan(policy.native_array_handle, policy.owner_path), @@ -1021,18 +1152,20 @@ def _function_plan( ) -> FunctionPlan: """Project one completed function policy for a particular Python export. - Native slots are built first so argument and result transfers share - their exact ABI records. The returned function contains binding and - bridge views of already completed policy, ordered lifecycle actions, - and all named roles required by lowering. ``public`` only controls - the generated binding-table visibility for private overload targets. + Bridge call slots are built first so argument and result transfers + share their exact original-Fortran call records. The returned function + contains complete binding, native-entrypoint, and bridge views, ordered + lifecycle actions, and all named roles required by lowering. ``public`` + only controls binding-table visibility for private overload targets. """ - # Share native-call records before projecting their argument and result consumers. - native_call_slots = self._native_slot_plans(policy) - arguments = self._argument_plans(policy, native_call_slots) - results = self._result_plans(policy, native_call_slots) + # Project one authoritative binding/entrypoint sequence, then attach an + # adapter facet only for operations whose completed action selected it. + 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) declaration_callables = tuple(self._declaration_callable_plan(item) for item in policy.declaration_callables) - status_error = self._status_error_plan(policy.status_error, native_call_slots) + status_error = self._status_error_plan(policy.status_error, projected_slots) # Retain the completed action order; later stages only dispatch from it. writeback_actions = tuple(self.visit(action) for action in policy.writeback_actions) @@ -1049,24 +1182,42 @@ def _function_plan( argument_conversion_order=self._binding_argument_conversion_order(arguments), public=public, ), - bridge=BridgeFunctionPlan( - policy.native_name, - policy.native_invocation, - policy.native_operator, - policy.standalone, - policy.external_declaration, - policy.native_module, - policy.native_is_subroutine, + entrypoint=NativeEntrypointFunctionPlan( + symbol_name=( + policy.entrypoint_symbol + if policy.entrypoint_action is NativeEntrypointAction.DIRECT_C_ABI + else f"bind_c_{export.name.casefold()}" + ), + action=policy.entrypoint_action, + parameters=self._entrypoint_parameter_plans( + arguments, + entrypoint_results, + projected_slots, + ), + results=entrypoint_results, + projected_slots=projected_slots, + ), + bridge=( + BridgeFunctionPlan( + policy.native_name, + policy.native_invocation, + policy.native_operator, + policy.standalone, + policy.external_declaration, + policy.native_module, + policy.native_is_subroutine, + ) + if policy.entrypoint_action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER + else None ), class_call=self._class_call_plan(policy), arguments=arguments, results=results, - native_call_slots=native_call_slots, declaration_callables=declaration_callables, available_roles=self._available_roles( arguments, results, - native_call_slots, + projected_slots, declaration_callables, ), writeback_actions=writeback_actions, @@ -1094,12 +1245,94 @@ def _binding_argument_conversion_order( converted.add(argument.owner_path) return tuple(ordered) + @staticmethod + def _entrypoint_parameter_plans( + arguments: tuple[ArgumentTransferPlan, ...], + results: tuple[NativeEntrypointResultPlan, ...], + projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...], + ) -> tuple[NativeEntrypointParameterPlan, ...]: + """Record C-ABI parameter groups in emitted call/prototype order.""" + argument_owners = {argument.owner_path for argument in arguments} + 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)) + else: + groups.append((slot.owner_path, "projected_slot", slot.native_position)) + groups.extend( + (result.owner_path, "direct_result", None) + for result in results + if result.source_kind == "direct_return" + and ( + result.scalar_descriptor is not None + or ( + result.native_array_handle is not None + and result.native_array_handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE + ) + ) + ) + groups.extend( + (result.owner_path, "declaration_extent", None) + for result in results + if result.array is not None and "bridge" in result.array.extent_evaluation + ) + return tuple( + NativeEntrypointParameterPlan( + owner_path=owner, + position=position, + source_kind=source_kind, + native_position=native_position, + ) + for position, (owner, source_kind, native_position) in enumerate(groups) + ) + + def _entrypoint_result_plans( + self, + results: tuple[ResultPlan, ...], + projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...], + ) -> 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" + ) + direct = tuple(result.entrypoint for result in results if result.source_kind == "direct_return") + return (*hidden, *direct) + + @staticmethod + def _entrypoint_result_plan_from_slot( + slot: NativeEntrypointProjectedSlotPlan, + ) -> 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") + return NativeEntrypointResultPlan( + owner_path=slot.owner_path, + parameter_name=slot.native_name.casefold(), + source_kind="hidden_output", + result_position=slot.result_position, + native_result_role=slot.symbolic_role, + direct_result_abi=DirectResultABI.NOT_APPLICABLE, + semantic_type_name=slot.semantic_type_name, + datatype_family=slot.datatype_family, + object_kind=slot.object_kind, + character_length=slot.character_length, + array=slot.array, + native_array_handle=slot.native_array_handle, + scalar_descriptor=slot.scalar_descriptor, + passing=slot.passing, + ) + @staticmethod def _argument_role_owners( arguments: tuple[ArgumentTransferPlan, ...], ) -> dict[str, str]: """Map every binding-produced value or extent role to its argument owner.""" - owners = {argument.binding.handoff_role: argument.owner_path for argument in arguments} + owners = {argument.entrypoint.handoff_role: argument.owner_path for argument in arguments} owners.update( { role: argument.owner_path @@ -1174,12 +1407,75 @@ def _binding_extent_dependency_owners( owners.discard(argument.owner_path) return owners - def _native_slot_plans(self, policy: FunctionWrapperPolicy) -> tuple[NativeCallSlotPlan, ...]: - """Project ordered native call slots with their completed symbolic roles.""" - return tuple( - self._native_slot_plan(slot, self._native_slot_role(slot, policy.results)) - for slot in policy.native_call_slots - ) + def _projected_slot_plans( + self, + policy: FunctionWrapperPolicy, + ) -> tuple[NativeEntrypointProjectedSlotPlan, ...]: + """Project policy-owned entrypoint slots before optional adapter facets.""" + adapted = policy.entrypoint_action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER + projected = [] + for slot_policy in policy.native_call_slots: + role = self._native_slot_role(slot_policy, policy.results) + include_buffer_roles = slot_policy.native_barrier_action is NativeBarrierAction.PASS_ARRAY_BUFFER + array = self._array_plan( + slot_policy.array, + slot_policy.owner_path, + include_buffer_roles=include_buffer_roles, + include_dense_actual_role=include_buffer_roles and slot_policy.python_position is not None, + ) + native_array_handle = self._native_array_handle_plan( + slot_policy.native_array_handle, + slot_policy.owner_path, + array=array, + ) + scalar_descriptor = self._scalar_descriptor_result_plan( + slot_policy.scalar_descriptor, + slot_policy.owner_path, + ) + derived = self._derived_handoff_plan(slot_policy.derived) + datatype_family = ( + self._transfer_datatype_family( + slot_policy.semantic_type_name, + slot_policy.derived, + callback=slot_policy.callback, + ) + if slot_policy.semantic_type_name + else None + ) + projected.append( + NativeEntrypointProjectedSlotPlan( + owner_path=slot_policy.owner_path, + native_position=slot_policy.native_position, + source_kind=slot_policy.source_kind, + python_position=slot_policy.python_position, + python_name=slot_policy.python_name, + native_name=slot_policy.native_name, + value_kind=slot_policy.value_kind, + symbolic_role=role, + object_kind=slot_policy.object_kind, + scalar_logical_abi=slot_policy.scalar_logical_abi, + scalar_native_type=slot_policy.scalar_native_type, + array_logical_abi=slot_policy.array_logical_abi, + array_native_type=slot_policy.array_native_type, + array_copy_in=slot_policy.array_copy_in, + array_copy_out=slot_policy.array_copy_out, + literal_type=slot_policy.literal_type, + literal_value=slot_policy.literal_value, + result_position=slot_policy.result_position, + semantic_type_name=slot_policy.semantic_type_name, + datatype_family=datatype_family, + character_length=slot_policy.character_length, + array=array, + native_array_handle=native_array_handle, + scalar_descriptor=scalar_descriptor, + derived=derived, + projection_action=slot_policy.projection_action, + passing=slot_policy.entrypoint_passing, + optionality=slot_policy.entrypoint_optionality, + adapter=(self._bridge_slot_plan(slot_policy) if adapted else None), + ) + ) + return tuple(projected) @staticmethod def _class_call_plan(policy: FunctionWrapperPolicy) -> ClassCallPlan | None: @@ -1196,13 +1492,13 @@ def _class_call_plan(policy: FunctionWrapperPolicy) -> ClassCallPlan | None: def _argument_plans( self, policy: FunctionWrapperPolicy, - native_call_slots: tuple[NativeCallSlotPlan, ...], + projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...], ) -> tuple[ArgumentTransferPlan, ...]: - """Return declared transfers sharing the function's native-slot records.""" + """Return declared transfers sharing original-Fortran call slots.""" return tuple( self.visit( argument, - native_slot=self._planned_native_slot(native_call_slots, argument.owner_path), + projected_slot=self._planned_bridge_slot(projected_slots, argument.owner_path), ) for argument in policy.arguments ) @@ -1210,13 +1506,14 @@ def _argument_plans( def _result_plans( self, policy: FunctionWrapperPolicy, - native_call_slots: tuple[NativeCallSlotPlan, ...], + projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...], ) -> tuple[ResultPlan, ...]: """Return ordered result consumers sharing completed native slots.""" return tuple( self.visit( result, - native_slot=self._result_native_slot(result, native_call_slots), + projected_slot=self._result_bridge_slot(result, projected_slots), + adapted=policy.entrypoint_action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER, ) for result in sorted(policy.results, key=lambda item: item.result_position) ) @@ -1226,18 +1523,17 @@ def _visit_ArgumentPolicy( self, policy: ArgumentPolicy, *, - native_slot: NativeCallSlotPlan, + projected_slot: NativeEntrypointProjectedSlotPlan, ) -> ArgumentTransferPlan: """Project one completed argument transfer around its shared native slot. - The supplied ``native_slot`` is the already planned ABI source for the - argument. This method names its value and optional character roles, - then forms binding and bridge views from the completed policy without - re-evaluating ownership, conversion, or descriptor choices. + The supplied ``bridge_slot`` is the already planned original-call + source for the argument. This method names its value and optional + character roles, then forms all three views from completed policy. """ # Derive only symbolic role names; all transfer behavior is policy-owned. role = self._value_role(policy.owner_path) - native_array_handle = native_slot.native_array_handle + native_array_handle = projected_slot.native_array_handle length_role = self._argument_length_role(policy) return ArgumentTransferPlan( owner_path=policy.owner_path, @@ -1268,16 +1564,23 @@ def _visit_ArgumentPolicy( projects_result=policy.projects_result, python_visible=policy.python_visible, result_position=policy.result_position, - array=native_slot.array, + array=projected_slot.array, native_array_actual=self._native_array_actual_plan(policy.native_array_actual), native_array_handle=native_array_handle, - derived=native_slot.derived, + derived=projected_slot.derived, derived_call=self._derived_call_plan(policy.derived_call), callback=self._callback_handoff_plan(policy.callback), polymorphic=self._polymorphic_dispatch_plan(policy.polymorphic), - binding=self._binding_argument_plan(policy, role, length_role), - bridge=self._bridge_argument_plan(policy, native_slot, native_array_handle, role, length_role), - native_call_slot=native_slot, + binding=self._binding_argument_plan(policy), + entrypoint=self._entrypoint_argument_plan( + policy, + projected_slot, + native_array_handle, + role, + length_role, + ), + 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), ) @@ -1289,16 +1592,28 @@ def _callback_handoff_plan( if policy is None: return None stem = NativeSymbolNames.compact(policy.owner_path, "callback", limit=24) + arguments = tuple(self._callback_transfer_plan(item) for item in policy.arguments) + result = self._callback_result_plan(policy.result) + trampoline_symbol = f"prik_callback_trampoline_{stem}" return CallbackHandoffPlan( owner_path=policy.owner_path, prototype=self._procedure_prototype_plan(policy.prototype), - context_type_symbol=f"prik_callback_context_{stem}", - context_current_symbol=f"prik_callback_current_{stem}", - adapter_symbol=f"prik_callback_adapter_{stem}", - trampoline_symbol=f"prik_callback_trampoline_{stem}", - abort_symbol=f"prik_callback_abort_{stem}", - arguments=tuple(self._callback_transfer_plan(item) for item in policy.arguments), - result=self._callback_result_plan(policy.result), + binding=BindingCallbackPlan( + context_type_symbol=f"prik_callback_context_{stem}", + context_current_symbol=f"prik_callback_current_{stem}", + abort_symbol=f"prik_callback_abort_{stem}", + ), + entrypoint=NativeEntrypointCallbackPlan( + support_procedure=build_callback_support_procedure_entrypoint( + policy.owner_path, + trampoline_symbol, + arguments, + result, + ) + ), + bridge=BridgeCallbackPlan(adapter_symbol=f"prik_callback_adapter_{stem}"), + arguments=arguments, + result=result, lifecycle=policy.lifecycle, thread_action=policy.thread_action, gil_actions=policy.gil_actions, @@ -1429,8 +1744,6 @@ def _argument_length_role(policy: ArgumentPolicy) -> str | None: @staticmethod def _binding_argument_plan( policy: ArgumentPolicy, - role: str, - length_role: str | None, ) -> BindingArgumentPlan: """Project the binding-facing view without revisiting semantic decisions.""" return BindingArgumentPlan( @@ -1438,34 +1751,34 @@ def _binding_argument_plan( python_action=policy.python_barrier_action, codegen_action=policy.codegen_action, conversion_phase=policy.conversion_phase, - handoff_role=role, optional_mode=policy.optional_mode, nullable=policy.nullable, writable=policy.writable, descriptor_boundary=policy.descriptor_boundary, - length_handoff_role=length_role, ) - def _bridge_argument_plan( + def _entrypoint_argument_plan( self, policy: ArgumentPolicy, - native_slot: NativeCallSlotPlan, + projected_slot: NativeEntrypointProjectedSlotPlan, native_array_handle: NativeArrayHandlePlan | None, role: str, length_role: str | None, - ) -> BridgeArgumentPlan: - """Project the bridge-facing view without revisiting semantic decisions.""" - return BridgeArgumentPlan( - native_name=policy.native_name, - native_action=policy.native_barrier_action, - codegen_action=policy.codegen_action, + ) -> NativeEntrypointArgumentPlan: + """Project the complete shared C-ABI argument transport.""" + return NativeEntrypointArgumentPlan( + parameter_name=policy.native_name.casefold(), handoff_mode=policy.handoff_mode, - data_action=policy.bridge_data_action, - copy_reason=policy.bridge_copy_reason, - abi_position=native_slot.native_position, handoff_role=role, optional_mode=policy.optional_mode, presence_role=self._argument_presence_role(policy, native_array_handle), + passing=policy.entrypoint_passing, + optionality=policy.entrypoint_optionality, + pass_character_length=policy.entrypoint_pass_character_length, + pass_array_metadata=policy.entrypoint_pass_array_metadata, + pass_descriptor_presence=policy.entrypoint_pass_descriptor_presence, + pass_derived_transaction=policy.entrypoint_pass_derived_transaction, + pass_callback_parameter=policy.entrypoint_pass_callback_parameter, length_handoff_role=length_role, descriptor_output_role=self._required_descriptor_output_role(policy, "descriptor-output"), descriptor_output_presence_role=self._required_descriptor_output_role( @@ -1474,6 +1787,17 @@ def _bridge_argument_plan( ), ) + @staticmethod + def _bridge_argument_plan(policy: ArgumentPolicy) -> BridgeArgumentPlan: + """Project adapter-local conversion and original-dummy facts.""" + return BridgeArgumentPlan( + native_name=policy.native_name, + native_action=policy.native_barrier_action, + codegen_action=policy.codegen_action, + data_action=policy.bridge_data_action, + copy_reason=policy.bridge_copy_reason, + ) + @staticmethod def _argument_presence_role( policy: ArgumentPolicy, @@ -1546,7 +1870,8 @@ def _visit_ResultPolicy( self, policy: ResultPolicy, *, - native_slot: NativeCallSlotPlan | None, + projected_slot: NativeEntrypointProjectedSlotPlan | None, + adapted: bool, ) -> ResultPlan: """Project one completed result with shared binding and bridge views. @@ -1556,20 +1881,20 @@ def _visit_ResultPolicy( slot from which to obtain its ABI details. """ native_role = f"{policy.owner_path}:native-result" - if policy.source_kind == "hidden_output" and native_slot is None: + if policy.source_kind == "hidden_output" and projected_slot is None: raise ValueError(f"{policy.owner_path!r} hidden result requires its completed native-call slot") - # Reuse hidden-output records, or project the direct-result facets once. - array = self._result_array_plan(policy, native_slot) - native_array_handle = self._result_native_array_handle_plan(policy, native_slot, array) + array = self._result_array_plan(policy, projected_slot) + native_array_handle = self._result_native_array_handle_plan(policy, projected_slot, array) + scalar_descriptor = self._result_scalar_descriptor_plan(policy, projected_slot) + datatype_family = self._transfer_datatype_family(policy.semantic_type_name, policy.derived) return ResultPlan( owner_path=policy.owner_path, semantic_type_name=policy.semantic_type_name, - datatype_family=self._transfer_datatype_family(policy.semantic_type_name, policy.derived), + datatype_family=datatype_family, source_kind=policy.source_kind, result_position=policy.result_position, character_length=policy.character_length, - direct_result_abi=policy.direct_result_abi, object_kind=policy.ownership.kind, ownership_owner=policy.ownership.owner, transfer_mode=policy.ownership.transfer, @@ -1579,34 +1904,54 @@ def _visit_ResultPolicy( nullable=policy.ownership.nullable, array=array, native_array_handle=native_array_handle, - derived=(native_slot.derived if native_slot is not None else self._derived_handoff_plan(policy.derived)), + derived=( + projected_slot.derived if projected_slot is not None else self._derived_handoff_plan(policy.derived) + ), binding=BindingResultPlan( policy.codegen_action, policy.python_barrier_action, f"{policy.owner_path}:python-result", ), - bridge=BridgeResultPlan( - policy.codegen_action, - policy.native_barrier_action, - policy.bridge_data_action, - policy.bridge_copy_reason, - native_role, - policy.native_name, - policy.native_position, + entrypoint=NativeEntrypointResultPlan( + owner_path=policy.owner_path, + parameter_name=(policy.native_name.casefold() if policy.native_name is not None else None), + source_kind=policy.source_kind, + result_position=policy.result_position, + native_result_role=native_role, + direct_result_abi=policy.direct_result_abi, + semantic_type_name=policy.semantic_type_name, + datatype_family=datatype_family, + object_kind=policy.ownership.kind, + character_length=policy.character_length, + array=array, + native_array_handle=native_array_handle, + scalar_descriptor=scalar_descriptor, + passing=policy.entrypoint_passing, + ), + bridge=( + BridgeResultPlan( + policy.codegen_action, + policy.native_barrier_action, + policy.bridge_data_action, + policy.bridge_copy_reason, + policy.native_name, + ) + if adapted + else None ), - native_call_slot=native_slot, - scalar_descriptor=self._result_scalar_descriptor_plan(policy, native_slot), + projected_call_slot=projected_slot, + scalar_descriptor=scalar_descriptor, transformations=tuple(self.visit(item) for item in policy.transformations), ) def _result_array_plan( self, policy: ResultPolicy, - native_slot: NativeCallSlotPlan | None, + projected_slot: NativeEntrypointProjectedSlotPlan | None, ) -> ArrayHandoffPlan | None: """Reuse a hidden slot array or project one direct result array.""" - if native_slot is not None: - return native_slot.array + if projected_slot is not None: + return projected_slot.array return self._array_plan( policy.array, policy.owner_path, @@ -1616,78 +1961,34 @@ def _result_array_plan( def _result_native_array_handle_plan( self, policy: ResultPolicy, - native_slot: NativeCallSlotPlan | None, + projected_slot: NativeEntrypointProjectedSlotPlan | None, array: ArrayHandoffPlan | None, ) -> NativeArrayHandlePlan | None: """Reuse a hidden slot handle or project one direct result handle.""" - if native_slot is not None: - return native_slot.native_array_handle + if projected_slot is not None: + return projected_slot.native_array_handle return self._native_array_handle_plan(policy.native_array_handle, policy.owner_path, array=array) def _result_scalar_descriptor_plan( self, policy: ResultPolicy, - native_slot: NativeCallSlotPlan | None, + projected_slot: NativeEntrypointProjectedSlotPlan | None, ) -> ScalarDescriptorResultPlan | None: """Reuse exact hidden descriptor state or project one direct result.""" - if native_slot is not None: - return native_slot.scalar_descriptor + if projected_slot is not None: + return projected_slot.scalar_descriptor return self._scalar_descriptor_result_plan(policy.scalar_descriptor, policy.owner_path) - def _native_slot_plan(self, slot: NativeCallSlotPolicy, role: str) -> NativeCallSlotPlan: - """Project one completed ABI slot shared by arguments, results, and calls. - - ``role`` is its externally visible symbolic source. Buffer and dense - array roles are included only when the completed native action requires - them. The method copies completed actions and ABI facts into one plan - record; it never chooses a backend mechanism. - """ - # The completed native action determines only which already-selected roles are needed. - include_buffer_roles = slot.native_barrier_action is NativeBarrierAction.PASS_ARRAY_BUFFER - array = self._array_plan( - slot.array, - slot.owner_path, - include_buffer_roles=include_buffer_roles, - include_dense_actual_role=include_buffer_roles and slot.python_position is not None, - ) - return NativeCallSlotPlan( - owner_path=slot.owner_path, - native_position=slot.native_position, - source_kind=slot.source_kind, - python_position=slot.python_position, - python_name=slot.python_name, - native_name=slot.native_name, - value_kind=slot.value_kind, - symbolic_role=role, + @staticmethod + def _bridge_slot_plan( + slot: NativeCallSlotPolicy, + ) -> BridgeCallSlotPlan: + """Project only adapter-local actions for one shared call slot.""" + return BridgeCallSlotPlan( native_action=slot.native_barrier_action, codegen_action=slot.codegen_action, bridge_data_action=slot.bridge_data_action, bridge_copy_reason=slot.bridge_copy_reason, - object_kind=slot.object_kind, - scalar_logical_abi=slot.scalar_logical_abi, - scalar_native_type=slot.scalar_native_type, - array_logical_abi=slot.array_logical_abi, - array_native_type=slot.array_native_type, - array_copy_in=slot.array_copy_in, - array_copy_out=slot.array_copy_out, - literal_type=slot.literal_type, - literal_value=slot.literal_value, - result_position=slot.result_position, - semantic_type_name=slot.semantic_type_name, - datatype_family=( - self._transfer_datatype_family( - slot.semantic_type_name, - slot.derived, - callback=slot.callback, - ) - if slot.semantic_type_name - else None - ), - character_length=slot.character_length, - array=array, - native_array_handle=self._native_array_handle_plan(slot.native_array_handle, slot.owner_path, array=array), - scalar_descriptor=self._scalar_descriptor_result_plan(slot.scalar_descriptor, slot.owner_path), - derived=self._derived_handoff_plan(slot.derived), ) # Derived-type argument, result, and module handoff planning. @@ -2039,12 +2340,12 @@ def _array_layout_roles( def _status_error_plan( self, policy: NativeStatusErrorPolicy | None, - native_call_slots: tuple[NativeCallSlotPlan, ...], + projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...], ) -> BindingStatusErrorPlan | None: """Project one completed native-status decision into binding roles.""" if policy is None: return None - roles = {slot.owner_path: slot.symbolic_role for slot in native_call_slots} + roles = {slot.owner_path: slot.symbolic_role for slot in projected_slots} try: status_role = roles[policy.status.owner_path] message_role = roles[policy.message.owner_path] if policy.message is not None else None @@ -2057,32 +2358,32 @@ def _status_error_plan( exception_kind=policy.exception_kind, ) - def _planned_native_slot( + def _planned_bridge_slot( self, - native_call_slots: tuple[NativeCallSlotPlan, ...], + projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...], owner_path: str, - ) -> NativeCallSlotPlan: - """Return the one shared editable native-call slot for an owner.""" - for slot in native_call_slots: + ) -> NativeEntrypointProjectedSlotPlan: + """Return the one authoritative projected slot for an owner.""" + for slot in projected_slots: if slot.owner_path == owner_path: return slot raise ValueError(f"{owner_path!r} is missing a completed native-call slot") - def _result_native_slot( + def _result_bridge_slot( self, result_policy: ResultPolicy, - native_call_slots: tuple[NativeCallSlotPlan, ...], - ) -> NativeCallSlotPlan | None: + projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...], + ) -> NativeEntrypointProjectedSlotPlan | None: """Return the completed slot for one hidden result, if any.""" if result_policy.source_kind != "hidden_output": return None - return self._planned_native_slot(native_call_slots, result_policy.owner_path) + return self._planned_bridge_slot(projected_slots, result_policy.owner_path) def _available_roles( self, arguments: tuple[ArgumentTransferPlan, ...], results: tuple[ResultPlan, ...], - native_call_slots: tuple[NativeCallSlotPlan, ...], + projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...], declaration_callables: tuple[DeclarationCallablePlan, ...], ) -> tuple[str, ...]: """Return symbolic roles available after the native call.""" @@ -2090,7 +2391,7 @@ def _available_roles( *self._argument_handoff_roles(arguments), *self._argument_extent_roles(arguments), *self._argument_descriptor_output_roles(arguments), - *self._native_result_roles(native_call_slots), + *self._native_result_roles(projected_slots), *self._direct_result_roles(results), *self._declaration_callable_roles(declaration_callables), ) @@ -2099,7 +2400,7 @@ def _available_roles( @staticmethod def _argument_handoff_roles(arguments: tuple[ArgumentTransferPlan, ...]) -> tuple[str, ...]: """Return the primary binding-produced role for every argument.""" - return tuple(argument.binding.handoff_role for argument in arguments) + return tuple(argument.entrypoint.handoff_role for argument in arguments) @staticmethod def _argument_descriptor_output_roles(arguments: tuple[ArgumentTransferPlan, ...]) -> tuple[str, ...]: @@ -2108,8 +2409,8 @@ def _argument_descriptor_output_roles(arguments: tuple[ArgumentTransferPlan, ... role for argument in arguments for role in ( - argument.bridge.descriptor_output_role, - argument.bridge.descriptor_output_presence_role, + argument.entrypoint.descriptor_output_role, + argument.entrypoint.descriptor_output_presence_role, ) if role is not None ) @@ -2176,13 +2477,18 @@ def _function_native_array_handles( *(result.native_array_handle for result in function.results), ) - def _native_result_roles(self, native_call_slots: tuple[NativeCallSlotPlan, ...]) -> tuple[str, ...]: + def _native_result_roles( + self, + projected_slots: tuple[NativeEntrypointProjectedSlotPlan, ...], + ) -> tuple[str, ...]: """Return every role produced through a native result slot.""" - return tuple(slot.symbolic_role for slot in native_call_slots if slot.source_kind == "result") + return tuple(slot.symbolic_role for slot in projected_slots if slot.source_kind == "result") def _direct_result_roles(self, results: tuple[ResultPlan, ...]) -> tuple[str, ...]: - """Return direct-return roles produced by the bridge function result.""" - return tuple(result.bridge.native_result_role for result in results if result.source_kind == "direct_return") + """Return direct-return roles produced by the shared entrypoint result.""" + return tuple( + result.entrypoint.native_result_role for result in results if result.source_kind == "direct_return" + ) def _datatype_family(self, semantic_type_name: str) -> DatatypeFamily: """Copy the backend-relevant family of one supported semantic type.""" diff --git a/prik/policy/completion.py b/prik/policy/completion.py index c72bc8bee..5023b2f8f 100644 --- a/prik/policy/completion.py +++ b/prik/policy/completion.py @@ -180,7 +180,7 @@ def _complete_ownership_policies( _complete_local_derived_type_identities(module) # Reuse one source-ordered class population while later phases replace its policies. - class_nodes = tuple(_iter_semantic_classes(module.classes)) + class_nodes = _ordered_semantic_classes(module.classes) # Complete persistent module state and its accessors first. for variable in module.variables: @@ -854,11 +854,15 @@ def _accepted_builtin_scalar_family( return overload_builtin_scalar_family(semantic_type_name) -def _iter_semantic_classes(classes: list[models.SemanticClass]): - """Yield one stable depth-first class sequence for policy completion.""" - for semantic_class in classes: - yield semantic_class - yield from _iter_semantic_classes(semantic_class.classes) +def _ordered_semantic_classes(classes: list[models.SemanticClass]) -> tuple[models.SemanticClass, ...]: + """Collect one stable depth-first class sequence for policy completion.""" + ordered = [] + pending = list(reversed(classes)) + while pending: + semantic_class = pending.pop() + ordered.append(semantic_class) + pending.extend(reversed(semantic_class.classes)) + return tuple(ordered) def _polymorphic_variant_map( diff --git a/prik/policy/construction.py b/prik/policy/construction.py index 82ccc3cf7..c8834d52b 100644 --- a/prik/policy/construction.py +++ b/prik/policy/construction.py @@ -56,6 +56,10 @@ DERIVED_VALUE_COPY_REASON, LOGICAL_SCALAR_KIND_COPY_REASON, LOGICAL_ARRAY_KIND_COPY_REASON, + NativeEntrypointAction, + EntrypointPassingConvention, + EntrypointOptionalityAction, + EntrypointProjectionAction, OptionalMode, ArgumentHandoffMode, ArgumentConversionPhase, @@ -1244,6 +1248,23 @@ 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 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: + raise ValueError(f"Semantic function {policy.owner_path!r} has a direct entrypoint without a linkable symbol") + if policy.entrypoint_action is NativeEntrypointAction.DIRECT_C_ABI and policy.entrypoint_diagnostics: + raise ValueError(f"Semantic function {policy.owner_path!r} has an inconsistent direct entrypoint action") + incomplete_slots = [ + slot.native_position + for slot in policy.native_call_slots + if slot.projection_action is EntrypointProjectionAction.BLOCKED + or slot.entrypoint_passing is EntrypointPassingConvention.BLOCKED + or slot.entrypoint_optionality is EntrypointOptionalityAction.BLOCKED + ] + if incomplete_slots: + 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}") @@ -1298,6 +1319,8 @@ def build_callback_handoff_policy( name=local_name, identity=f"{origin_module or owner_path}.{source_name}", pure=_prototype_metadata_is_pure(semantic_type.metadata.get("prototype_metadata")), + source_language=semantic_type.metadata.get("prototype_source_language"), + native_abi=semantic_type.metadata.get("prototype_native_abi"), arguments=tuple(raw_arguments) if isinstance(raw_arguments, list) else (), result=return_type if isinstance(return_type, models.SemanticType) else None, ) @@ -1332,6 +1355,8 @@ def _procedure_prototype_policy( name: str, identity: str, pure: bool, + source_language: str | None, + native_abi: str | None, arguments: tuple[models.SemanticArgument, ...], result: models.SemanticType | None, ) -> ProcedurePrototypePolicy: @@ -1341,6 +1366,8 @@ def _procedure_prototype_policy( name=name, identity=identity, pure=pure, + source_language=source_language, + native_abi=native_abi, arguments=tuple(_semantic_prototype_argument_policy(argument, owner_path=owner_path) for argument in arguments), result=( _semantic_prototype_result_policy(result, owner_path=owner_path) @@ -1599,11 +1626,17 @@ def build_function_wrapper_policy( native_call_slots, declaration_callables, ) + arguments, native_call_slots = _complete_direct_descriptor_handoffs( + function, + 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) status_error = _completed_native_status_error_policy(function) native_module = _native_module(function, owner_path) + native_call_slots = _complete_entrypoint_slot_policies(arguments, results, native_call_slots) # Aggregate all support validation before exposing the immutable plan input. blockers = ( _function_shape_blockers(function, class_call) @@ -1621,6 +1654,16 @@ def build_function_wrapper_policy( native_name = native_dispatch_name or _native_name(function) native_invocation, native_operator = _native_invocation_policy(native_name) standalone = _is_standalone(function) + arguments, native_call_slots, entrypoint_action, entrypoint_symbol, entrypoint_diagnostics = ( + _complete_function_entrypoint_route( + function, + class_call=class_call, + native_invocation=native_invocation, + arguments=arguments, + results=results, + slots=native_call_slots, + ) + ) return FunctionWrapperPolicy( owner_path=owner_path, python_exports=completed_python_exports(function, function.name), @@ -1652,7 +1695,122 @@ def build_function_wrapper_policy( writeback_actions=writeback_actions, cleanup_actions=cleanup_actions, release_actions=release_actions, + entrypoint_action=entrypoint_action, + entrypoint_symbol=entrypoint_symbol, + entrypoint_diagnostics=entrypoint_diagnostics, + ) + + +def _complete_function_entrypoint_route( + function: models.SemanticFunction, + *, + class_call: ClassMethodPolicy | None, + native_invocation: NativeInvocationKind, + arguments: list[ArgumentPolicy], + results: tuple[ResultPolicy, ...], + slots: tuple[NativeCallSlotPolicy, ...], +) -> tuple[ + list[ArgumentPolicy], + tuple[NativeCallSlotPolicy, ...], + NativeEntrypointAction, + str, + tuple[str, ...], +]: + """Complete one function's route and route-dependent boundary metadata.""" + entrypoint_diagnostics = _direct_c_abi_ineligibility( + function, + class_call=class_call, + native_invocation=native_invocation, + arguments=tuple(arguments), + results=results, + slots=slots, + ) + entrypoint_action = ( + NativeEntrypointAction.DIRECT_C_ABI + if not entrypoint_diagnostics + else NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER + ) + arguments = [_complete_entrypoint_argument_route(argument, entrypoint_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]) + if slot.native_position in optionality_by_position + else slot + for slot in slots + ) + entrypoint_symbol = ( + str(function.origin.native_symbol or function.origin.native_name or function.native_name or function.name) + if entrypoint_action is NativeEntrypointAction.DIRECT_C_ABI + else "" + ) + return arguments, slots, entrypoint_action, entrypoint_symbol, entrypoint_diagnostics + + +def _complete_entrypoint_argument_route( + argument: ArgumentPolicy, + action: NativeEntrypointAction, +) -> ArgumentPolicy: + """Project a selected route into one argument's completed ABI metadata.""" + uses_adapter = action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER + return replace( + argument, + entrypoint_pass_character_length=( + uses_adapter and argument.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER + ), + 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), + entrypoint_pass_derived_transaction=(uses_adapter and argument.derived_call is not None), + entrypoint_pass_callback_parameter=( + action is NativeEntrypointAction.DIRECT_C_ABI and argument.callback is not None + ), + entrypoint_optionality=( + EntrypointOptionalityAction.EXPLICIT_NATIVE_PRESENCE + if uses_adapter and argument.optional_mode is OptionalMode.DESCRIPTOR + else argument.entrypoint_optionality + ), + ) + + +def _complete_direct_descriptor_handoffs( + function: models.SemanticFunction, + arguments: list[ArgumentPolicy], + slots: tuple[NativeCallSlotPolicy, ...], +) -> tuple[list[ArgumentPolicy], tuple[NativeCallSlotPolicy, ...]]: + """Select persistent standard descriptors for Fortran C-ABI candidates.""" + if function.origin.source_language != "fortran" or function.origin.native_abi != "c": + return arguments, slots + upgraded_by_position: dict[int, NativeArrayHandleWrapperPolicy] = {} + completed_arguments = [] + for argument in arguments: + handle = argument.native_array_handle + if ( + handle is not None + and handle.handle_kind + in { + NativeArrayHandleKind.ARGUMENT_DESCRIPTOR, + NativeArrayHandleKind.OPTIONAL_ABSENT_HANDLE, + } + and argument.rank > 0 + and argument.semantic_type_name in _PLAN_PRIMITIVE_SCALAR_TYPES + ): + handle = replace( + handle, + handoff=replace(handle.handoff, abi=NativeDescriptorHandoffABI.DIRECT_STANDARD_DESCRIPTOR), + default_handle=replace( + handle.default_handle, + construction=NativeArrayDefaultConstruction.LAZY_OWNED_DESCRIPTOR, + ), + ) + upgraded_by_position[argument.native_position] = handle + argument = replace(argument, native_array_handle=handle) + completed_arguments.append(argument) + completed_slots = tuple( + replace(slot, native_array_handle=upgraded_by_position[slot.native_position]) + if slot.native_position in upgraded_by_position + else slot + for slot in slots ) + return completed_arguments, completed_slots def _native_invocation_policy(native_name: str) -> tuple[NativeInvocationKind, str | None]: @@ -1665,6 +1823,346 @@ def _native_invocation_policy(native_name: str) -> tuple[NativeInvocationKind, s return NativeInvocationKind.PROCEDURE, None +def _argument_passes_by_value( + argument: models.SemanticArgument, + slot: NativeCallSlotPolicy | None, +) -> bool: + """Return the original declared value transport without backend inference.""" + if "value" in argument.origin.metadata: + return bool(argument.origin.metadata["value"]) + if slot is None: + return False + return slot.value_kind == "value" or slot.native_barrier_action is NativeBarrierAction.PASS_VALUE + + +def _argument_entrypoint_passing( + function: models.SemanticFunction, + argument: models.SemanticArgument, + boundary: _ArgumentBoundaryPolicy, + slot: NativeCallSlotPolicy | None, + callback: CallbackHandoffPolicy | None, +) -> EntrypointPassingConvention: + """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 boundary.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: + return EntrypointPassingConvention.C_DESCRIPTOR_POINTER + if argument.optional: + return EntrypointPassingConvention.NULLABLE_POINTER + if direct_c_abi: + if _argument_passes_by_value(argument, slot): + return EntrypointPassingConvention.C_VALUE + return EntrypointPassingConvention.POINTER_REFERENCE + if boundary.handoff_mode is ArgumentHandoffMode.VALUE: + return EntrypointPassingConvention.C_VALUE + if boundary.handoff_mode in { + ArgumentHandoffMode.TYPED_REFERENCE, + ArgumentHandoffMode.OPAQUE_ADDRESS, + ArgumentHandoffMode.CHARACTER_BUFFER, + ArgumentHandoffMode.ARRAY_BUFFER, + }: + return EntrypointPassingConvention.POINTER_REFERENCE + return EntrypointPassingConvention.BLOCKED + + +def _argument_entrypoint_optionality( + function: models.SemanticFunction, + argument: models.SemanticArgument, + boundary: _ArgumentBoundaryPolicy, + slot: NativeCallSlotPolicy | None, +) -> EntrypointOptionalityAction: + """Complete original native presence independently from the Python surface.""" + if not argument.optional: + return EntrypointOptionalityAction.REQUIRED + if boundary.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR: + 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": + return EntrypointOptionalityAction.NULL_POINTER + return EntrypointOptionalityAction.ADAPTER_SIDE_FORTRAN_OMISSION + + +def _complete_entrypoint_slot_policies( + arguments: list[ArgumentPolicy], + results: tuple[ResultPolicy, ...], + slots: tuple[NativeCallSlotPolicy, ...], +) -> tuple[NativeCallSlotPolicy, ...]: + """Attach binding projection, passing, and presence decisions to ordered slots.""" + arguments_by_position = {argument.python_position: argument for argument in arguments} + results_by_native_position = { + result.native_position: result + for result in results + if result.source_kind == "hidden_output" and result.native_position is not None + } + completed = [] + for slot in slots: + argument = arguments_by_position.get(slot.python_position) if slot.python_position is not None else None + result = results_by_native_position.get(slot.native_position) + projection_action = _entrypoint_projection_action(slot) + if result is not None: + passing = result.entrypoint_passing + optionality = EntrypointOptionalityAction.REQUIRED + elif projection_action is EntrypointProjectionAction.HIDDEN_OUTPUT_STORAGE: + passing = ( + EntrypointPassingConvention.C_DESCRIPTOR_POINTER + if slot.native_array_handle is not None or slot.scalar_descriptor is not None + else EntrypointPassingConvention.OUTPUT_STORAGE + ) + optionality = EntrypointOptionalityAction.REQUIRED + elif projection_action in { + EntrypointProjectionAction.TYPED_LITERAL, + EntrypointProjectionAction.COMPUTED_LENGTH, + EntrypointProjectionAction.COMPUTED_PRESENCE, + EntrypointProjectionAction.COMPUTED_SHAPE, + EntrypointProjectionAction.COMPUTED_STRIDE, + }: + passing = EntrypointPassingConvention.C_VALUE + optionality = EntrypointOptionalityAction.REQUIRED + elif argument is not None: + optionality = argument.entrypoint_optionality + if optionality in { + EntrypointOptionalityAction.NULL_POINTER, + EntrypointOptionalityAction.ADAPTER_SIDE_FORTRAN_OMISSION, + }: + passing = EntrypointPassingConvention.NULLABLE_POINTER + elif projection_action is EntrypointProjectionAction.ARGUMENT_DEFAULT: + passing = argument.entrypoint_passing + elif projection_action is EntrypointProjectionAction.ARGUMENT_ADDRESS: + passing = EntrypointPassingConvention.POINTER_REFERENCE + elif projection_action is EntrypointProjectionAction.ARGUMENT_VALUE: + passing = EntrypointPassingConvention.C_VALUE + else: + passing = argument.entrypoint_passing + elif projection_action is EntrypointProjectionAction.WORK_STORAGE: + passing = EntrypointPassingConvention.POINTER_REFERENCE + optionality = EntrypointOptionalityAction.REQUIRED + else: + passing = EntrypointPassingConvention.BLOCKED + optionality = EntrypointOptionalityAction.BLOCKED + completed.append( + replace( + slot, + projection_action=projection_action, + entrypoint_passing=passing, + entrypoint_optionality=optionality, + ) + ) + return tuple(completed) + + +def _entrypoint_projection_action(slot: NativeCallSlotPolicy) -> EntrypointProjectionAction: + """Map one normalized projection kind to its binding-owned materialization.""" + if slot.source_kind == "literal" or slot.value_kind == "literal": + return EntrypointProjectionAction.TYPED_LITERAL + if slot.source_kind == "result": + return EntrypointProjectionAction.HIDDEN_OUTPUT_STORAGE + if slot.native_array_handle is not None: + return EntrypointProjectionAction.DESCRIPTOR + if slot.callback is not None: + return EntrypointProjectionAction.RUNTIME_HANDLE + return { + "addr": EntrypointProjectionAction.ARGUMENT_ADDRESS, + "arg": EntrypointProjectionAction.ARGUMENT_DEFAULT, + "value": EntrypointProjectionAction.ARGUMENT_VALUE, + "is_present": EntrypointProjectionAction.COMPUTED_PRESENCE, + "len": EntrypointProjectionAction.COMPUTED_LENGTH, + "shape": EntrypointProjectionAction.COMPUTED_SHAPE, + "stride": EntrypointProjectionAction.COMPUTED_STRIDE, + "work": EntrypointProjectionAction.WORK_STORAGE, + "allocatable": EntrypointProjectionAction.DESCRIPTOR, + "pointer": EntrypointProjectionAction.DESCRIPTOR, + "pass": EntrypointProjectionAction.ARGUMENT_ADDRESS, + }.get(slot.value_kind, EntrypointProjectionAction.BLOCKED) + + +def _direct_c_abi_ineligibility( + function: models.SemanticFunction, + *, + class_call: ClassMethodPolicy | None, + native_invocation: NativeInvocationKind, + arguments: tuple[ArgumentPolicy, ...], + results: tuple[ResultPolicy, ...], + slots: tuple[NativeCallSlotPolicy, ...], +) -> tuple[str, ...]: + """Return central reasons an operation must keep its generated Fortran adapter.""" + if function.origin.source_language != "fortran" or function.origin.native_abi != "c": + return ("original procedure has no Fortran C ABI fact",) + + reasons = list( + _direct_operation_ineligibility( + function, + class_call=class_call, + native_invocation=native_invocation, + ) + ) + for argument in arguments: + reasons.extend(_direct_argument_ineligibility(argument)) + for result in results: + reasons.extend(_direct_result_ineligibility(result)) + for slot in slots: + reasons.extend(_direct_slot_ineligibility(slot)) + return tuple(dict.fromkeys(reasons)) + + +def _direct_operation_ineligibility( + function: models.SemanticFunction, + *, + class_call: ClassMethodPolicy | None, + native_invocation: NativeInvocationKind, +) -> tuple[str, ...]: + """Return direct-route blockers owned by the original operation.""" + reasons = [] + if not function.origin.native_symbol: + reasons.append("C ABI procedure has no linkable native symbol") + if class_call is not None: + reasons.append("type-bound or constructor invocation requires a Fortran adapter") + if native_invocation is not NativeInvocationKind.PROCEDURE: + reasons.append("defined or generic invocation requires a Fortran adapter") + return tuple(reasons) + + +def _direct_argument_ineligibility(argument: ArgumentPolicy) -> tuple[str, ...]: + """Return direct-route blockers owned by one completed argument policy.""" + callback_supported = _direct_callback_supported(argument.callback) + descriptor_supported = _direct_descriptor_supported(argument) + derived_reference_supported = _direct_derived_reference_supported(argument) + mechanism_supported = any( + ( + _direct_scalar_supported(argument), + _direct_array_supported(argument), + derived_reference_supported, + callback_supported, + descriptor_supported, + ) + ) + + reasons = [] + if not mechanism_supported: + reasons.append(f"argument {argument.name!r} has no adopted direct interoperable mechanism") + if ( + (argument.callback is not None and not callback_supported) + or (argument.derived is not None and not derived_reference_supported) + or (argument.native_array_handle is not None and not descriptor_supported) + ): + reasons.append(f"argument {argument.name!r} requires a specialized native handoff") + if argument.transformations: + reasons.append(f"argument {argument.name!r} requires representation transformation") + if argument.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY: + reasons.append(f"argument {argument.name!r} uses non-C Boolean storage") + if argument.entrypoint_passing is EntrypointPassingConvention.BLOCKED: + reasons.append(f"argument {argument.name!r} has no completed C passing convention") + if argument.entrypoint_optionality is EntrypointOptionalityAction.ADAPTER_SIDE_FORTRAN_OMISSION: + reasons.append(f"argument {argument.name!r} requires adapter-side Fortran omission") + if argument.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION and not _is_scalar_c_character(argument): + reasons.append(f"argument {argument.name!r} requires adapter representation work") + return tuple(reasons) + + +def _direct_scalar_supported(argument: ArgumentPolicy) -> bool: + """Return whether one scalar has an adopted interoperable C mechanism.""" + return argument.rank == 0 and ( + argument.semantic_type_name in _PLAN_PRIMITIVE_SCALAR_TYPES or _is_scalar_c_character(argument) + ) + + +def _direct_array_supported(argument: ArgumentPolicy) -> bool: + """Return whether one explicit or assumed-size array can use its C pointer.""" + return bool( + argument.rank > 0 + and ( + argument.semantic_type_name in _PLAN_PRIMITIVE_SCALAR_TYPES + or (argument.semantic_type_name == "String" and argument.character_length == 1) + ) + and argument.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER + and argument.array is not None + and argument.array.category in {"explicit_shape", "assumed_size"} + ) + + +def _direct_derived_reference_supported(argument: ArgumentPolicy) -> bool: + """Return whether one opaque interoperable object can cross by reference.""" + return bool( + argument.rank == 0 + and argument.derived is not None + and argument.derived.bind_c + and argument.derived.native_handoff is DerivedNativeHandoff.REFERENCE + and argument.derived.storage is DerivedObjectStorage.DIRECT + and not argument.derived.nullable + and argument.optional_mode is OptionalMode.REQUIRED + and argument.entrypoint_passing is EntrypointPassingConvention.POINTER_REFERENCE + ) + + +def _is_scalar_c_character(argument: ArgumentPolicy) -> bool: + """Return whether one argument is an interoperable scalar C character.""" + return argument.rank == 0 and argument.semantic_type_name == "String" and argument.character_length == 1 + + +def _direct_result_ineligibility(result: ResultPolicy) -> tuple[str, ...]: + """Return direct-route blockers owned by one completed result policy.""" + reasons = [] + if result.rank != 0 or result.semantic_type_name not in _PLAN_PRIMITIVE_SCALAR_TYPES: + reasons.append(f"result {result.owner_path!r} is not a directly supported interoperable scalar") + if result.derived is not None or result.native_array_handle is not None or result.scalar_descriptor is not None: + reasons.append(f"result {result.owner_path!r} requires a specialized native handoff") + if result.transformations or result.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION: + reasons.append(f"result {result.owner_path!r} requires adapter representation work") + return tuple(reasons) + + +def _direct_slot_ineligibility(slot: NativeCallSlotPolicy) -> tuple[str, ...]: + """Return direct-route blockers owned by one completed call projection.""" + 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 + ): + reasons.append(f"native-call slot {slot.native_position} requires adapter representation work") + return tuple(reasons) + + +def _direct_callback_supported(callback: CallbackHandoffPolicy | None) -> bool: + """Return whether one immediate callback has an exact scalar C ABI.""" + if ( + callback is None + or not callback.supported + or callback.prototype.source_language != "fortran" + or callback.prototype.native_abi != "c" + ): + return False + transfers = ( + *callback.arguments, + *((callback.result.transfer,) if callback.result.transfer is not None else ()), + ) + return all( + transfer.rank == 0 + and transfer.semantic_type_name in _PLAN_PRIMITIVE_SCALAR_TYPES + and transfer.derived_type_identity is None + and transfer.abi in {CallbackABIKind.VALUE, CallbackABIKind.REFERENCE} + for transfer in transfers + ) + + +def _direct_descriptor_supported(argument: ArgumentPolicy) -> bool: + """Return whether one array handle supplies the standard descriptor ABI.""" + handle = argument.native_array_handle + return bool( + handle is not None + and handle.handoff.abi is NativeDescriptorHandoffABI.DIRECT_STANDARD_DESCRIPTOR + and argument.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR + and argument.entrypoint_passing is EntrypointPassingConvention.C_DESCRIPTOR_POINTER + and argument.semantic_type_name in _PLAN_PRIMITIVE_SCALAR_TYPES + and argument.rank > 0 + and argument.derived is None + and not argument.transformations + ) + + def _external_declaration_mode( *, standalone: bool, @@ -1828,6 +2326,19 @@ def _argument_policy( derived, ) boundary = _argument_boundary_policy(function, argument, decision, python_position, callback) + entrypoint_passing = _argument_entrypoint_passing( + function, + argument, + boundary, + native_slot, + callback, + ) + entrypoint_optionality = _argument_entrypoint_optionality( + function, + argument, + boundary, + native_slot, + ) blockers = _completed_argument_blockers( argument, decision, @@ -1894,6 +2405,8 @@ def _argument_policy( callback=callback, polymorphic=polymorphic, transformations=transformations, + entrypoint_passing=entrypoint_passing, + entrypoint_optionality=entrypoint_optionality, ), blockers, ) @@ -2178,6 +2691,7 @@ def _direct_result_policy(context: _FunctionPolicyContext) -> _ResultPolicyCandi native_array_handle=direct_handle, scalar_descriptor=scalar_descriptor, derived=derived, + entrypoint_passing=EntrypointPassingConvention.C_FUNCTION_RETURN, ), tuple(blockers), ) @@ -2388,6 +2902,11 @@ def _hidden_result_candidate( native_array_handle=native_array_handle, scalar_descriptor=scalar_descriptor, derived=derived, + entrypoint_passing=( + EntrypointPassingConvention.C_DESCRIPTOR_POINTER + if native_array_handle is not None or scalar_descriptor is not None + else EntrypointPassingConvention.OUTPUT_STORAGE + ), ), tuple(blockers), ) @@ -2472,6 +2991,9 @@ def _projected_native_call_slot_policy( if mapping.value_kind == "literal": slot, blockers = _literal_native_call_slot_policy(mapping, owner_path, native_position) return slot, None, blockers + if mapping.value_kind in {"len", "is_present", "shape", "stride", "work"}: + slot, blockers = _computed_native_call_slot_policy(mapping, owner_path, native_position) + return slot, None, blockers python_position = mapping.python_position if mapping.result_position is not None and python_position is None: slot, blockers = _hidden_result_native_call_slot_policy( @@ -2492,6 +3014,53 @@ def _projected_native_call_slot_policy( ) +def _computed_native_call_slot_policy( + mapping: models.ProjectionMapping, + owner_path: str, + native_position: int, +) -> tuple[NativeCallSlotPolicy, tuple[str, ...]]: + """Complete one binding-owned length, presence, shape, stride, or work slot.""" + value_kind = mapping.value_kind + source_position = _projection_value_argument_position(mapping.value) + semantic_type_name = "Bool" if value_kind == "is_present" else "SizeT" + blockers = [] + if value_kind != "work" and source_position is None: + blockers.append(f"native-call {value_kind} slot {native_position} has no argument source") + if value_kind == "work": + blockers.append(f"native-call work slot {native_position} has no completed typed storage policy") + return ( + NativeCallSlotPolicy( + owner_path=f"{owner_path}.native_slot_{native_position}", + native_position=native_position, + source_kind="computed" if value_kind != "work" else "work", + python_position=source_position, + python_name=None, + native_name=mapping.native_name or f"{value_kind}_{native_position}", + value_kind=value_kind, + native_barrier_action=( + NativeBarrierAction.BLOCKED if value_kind == "work" else NativeBarrierAction.PASS_VALUE + ), + codegen_action=(CodegenAction.BLOCKED if value_kind == "work" else CodegenAction.DIRECT_VALUE), + bridge_data_action=(BridgeDataAction.BLOCKED if value_kind == "work" else BridgeDataAction.DIRECT_TRANSFER), + bridge_copy_reason=None, + object_kind=None, + semantic_type_name=semantic_type_name, + literal_value=mapping.value, + ), + tuple(blockers), + ) + + +def _projection_value_argument_position(value: object) -> int | None: + """Return the Arg source nested in one route-neutral computed projection.""" + if not isinstance(value, dict): + return None + if value.get("kind") == "arg": + position = value.get("position") + return position if isinstance(position, int) and not isinstance(position, bool) else None + return _projection_value_argument_position(value.get("value")) + + def _projected_argument_native_call_slot_policy( mapping: models.ProjectionMapping, owner_path: str, @@ -3008,6 +3577,7 @@ def _derived_handoff_policy( type_identity=type_identity, native_type_name=type_identity[1], native_scope=type_identity[0], + bind_c=bool(type_policy is not None and type_policy.bind_c), origin=origin, owner_retention=retention, release=release, @@ -6105,6 +6675,8 @@ def _direct_prototype_policy( name=local_name, identity=f"{prototype.origin.native_scope or owner_path}.{prototype.name}", pure=prototype.pure, + source_language=prototype.origin.source_language, + native_abi=prototype.origin.native_abi, arguments=tuple(prototype.arguments), result=prototype.return_type, ) diff --git a/prik/policy/exports.py b/prik/policy/exports.py index 7088b7657..62c3808d9 100644 --- a/prik/policy/exports.py +++ b/prik/policy/exports.py @@ -57,11 +57,8 @@ def complete_python_export_policy( def _module_export_owners(module: models.SemanticModule): - """Yield public-name owners in the same order as semantic lowering.""" - yield from module.classes - yield from module.functions - yield from module.overload_sets - yield from module.variables + """Return public-name owners in the same order as semantic lowering.""" + return (*module.classes, *module.functions, *module.overload_sets, *module.variables) def _owner_metadata(owner) -> dict[str, object]: diff --git a/prik/policy/models.py b/prik/policy/models.py index eff375842..5676b95f1 100644 --- a/prik/policy/models.py +++ b/prik/policy/models.py @@ -56,6 +56,55 @@ class OptionalMode(str, Enum): DESCRIPTOR = "descriptor" +class NativeEntrypointAction(str, Enum): + """Completed per-operation route from the binding to native code.""" + + DIRECT_C_ABI = "direct_c_abi" + GENERATED_FORTRAN_ADAPTER = "generated_fortran_adapter" + + +class EntrypointPassingConvention(str, Enum): + """Completed C-boundary transport for one parameter or result.""" + + C_VALUE = "c_value" + POINTER_REFERENCE = "pointer_reference" + NULLABLE_POINTER = "nullable_pointer" + C_DESCRIPTOR_POINTER = "c_descriptor_pointer" + RUNTIME_HANDLE = "runtime_handle" + C_FUNCTION_RETURN = "c_function_return" + OUTPUT_STORAGE = "output_storage" + BLOCKED = "blocked" + + +class EntrypointOptionalityAction(str, Enum): + """Native presence representation, independent of Python defaults.""" + + REQUIRED = "required" + NULL_POINTER = "null_pointer" + NULL_C_DESCRIPTOR_POINTER = "null_c_descriptor_pointer" + EXPLICIT_NATIVE_PRESENCE = "explicit_native_presence" + ADAPTER_SIDE_FORTRAN_OMISSION = "adapter_side_fortran_omission" + BLOCKED = "blocked" + + +class EntrypointProjectionAction(str, Enum): + """Binding-owned materialization for one ordered native-call mapping.""" + + ARGUMENT_DEFAULT = "argument_default" + ARGUMENT_VALUE = "argument_value" + ARGUMENT_ADDRESS = "argument_address" + HIDDEN_OUTPUT_STORAGE = "hidden_output_storage" + TYPED_LITERAL = "typed_literal" + COMPUTED_LENGTH = "computed_length" + COMPUTED_PRESENCE = "computed_presence" + COMPUTED_SHAPE = "computed_shape" + COMPUTED_STRIDE = "computed_stride" + WORK_STORAGE = "work_storage" + DESCRIPTOR = "descriptor" + RUNTIME_HANDLE = "runtime_handle" + BLOCKED = "blocked" + + class ArgumentHandoffMode(str, Enum): """Completed binding-to-bridge ABI shape for one argument.""" @@ -453,6 +502,7 @@ class DerivedHandoffPolicy: type_identity: tuple[str, str] native_type_name: str native_scope: str + bind_c: bool origin: DerivedObjectOrigin owner_retention: DerivedOwnerRetention release: DerivedRelease @@ -902,6 +952,8 @@ class ProcedurePrototypePolicy: name: str identity: str pure: bool + source_language: str | None + native_abi: str | None arguments: tuple[ProcedurePrototypeArgumentPolicy, ...] result: ProcedurePrototypeResultPolicy | None @@ -1109,6 +1161,13 @@ class ArgumentPolicy: callback: CallbackHandoffPolicy | None = None polymorphic: PolymorphicDispatchPolicy | None = None transformations: tuple[TransformationPolicy, ...] = () + entrypoint_passing: EntrypointPassingConvention = EntrypointPassingConvention.BLOCKED + entrypoint_optionality: EntrypointOptionalityAction = EntrypointOptionalityAction.BLOCKED + entrypoint_pass_character_length: bool = False + entrypoint_pass_array_metadata: bool = False + entrypoint_pass_descriptor_presence: bool = False + entrypoint_pass_derived_transaction: bool = False + entrypoint_pass_callback_parameter: bool = False @dataclass(frozen=True) @@ -1137,6 +1196,7 @@ class ResultPolicy: scalar_descriptor: ScalarDescriptorResultPolicy | None = None derived: DerivedHandoffPolicy | None = None transformations: tuple[TransformationPolicy, ...] = () + entrypoint_passing: EntrypointPassingConvention = EntrypointPassingConvention.BLOCKED @dataclass(frozen=True) @@ -1175,6 +1235,9 @@ class NativeCallSlotPolicy: scalar_descriptor: ScalarDescriptorResultPolicy | None = None derived: DerivedHandoffPolicy | None = None callback: CallbackHandoffPolicy | None = None + projection_action: EntrypointProjectionAction = EntrypointProjectionAction.BLOCKED + entrypoint_passing: EntrypointPassingConvention = EntrypointPassingConvention.BLOCKED + entrypoint_optionality: EntrypointOptionalityAction = EntrypointOptionalityAction.BLOCKED @dataclass(frozen=True) @@ -1209,6 +1272,9 @@ class FunctionWrapperPolicy: writeback_actions: tuple[LifecyclePolicy, ...] = () cleanup_actions: tuple[LifecyclePolicy, ...] = () release_actions: tuple[LifecyclePolicy, ...] = () + entrypoint_action: NativeEntrypointAction | None = None + entrypoint_symbol: str = "" + entrypoint_diagnostics: tuple[str, ...] = () if __name__ == "__main__": diff --git a/prik/policy/native_array_handles.py b/prik/policy/native_array_handles.py index 11b3af055..8c7a4326c 100644 --- a/prik/policy/native_array_handles.py +++ b/prik/policy/native_array_handles.py @@ -243,7 +243,7 @@ def native_array_handle_build_requirements( modules = [semantic_ir] if isinstance(semantic_ir, SemanticModule) else list(semantic_ir) requirements = tuple( _c_descriptor_requirement(owner, item, policy) - for owner, item, policy in _iter_native_array_handle_policies(modules) + for owner, item, policy in _native_array_handle_policies(modules) if policy.requires_c_descriptor_interop ) headers = (NATIVE_ARRAY_POINTER_C_DESCRIPTOR_HEADER,) if requirements else () @@ -271,37 +271,58 @@ def _c_descriptor_requirement( ) -def _iter_native_array_handle_policies(modules: Iterable[SemanticModule]): +def _native_array_handle_policies( + modules: Iterable[SemanticModule], +) -> tuple[tuple[str, str, NativeArrayHandlePolicy], ...]: + policies = [] for module in modules: for variable in module.variables: - yield from _variable_native_array_policy(variable, owner=f"{module.name}.{variable.name}") + policies.extend(_variable_native_array_policy(variable, owner=f"{module.name}.{variable.name}")) for semantic_class in module.classes: - yield from _iter_class_native_array_policies(semantic_class, owner=f"{module.name}.{semantic_class.name}") + policies.extend(_class_native_array_policies(semantic_class, owner=f"{module.name}.{semantic_class.name}")) for function in module.functions: - yield from _iter_function_native_array_policies(function, owner=f"{module.name}.{function.name}") + policies.extend(_function_native_array_policies(function, owner=f"{module.name}.{function.name}")) for overload_set in module.overload_sets: - yield from _iter_overload_native_array_policies(overload_set, owner=f"{module.name}.{overload_set.name}") + policies.extend(_overload_native_array_policies(overload_set, owner=f"{module.name}.{overload_set.name}")) + return tuple(policies) -def _iter_class_native_array_policies(semantic_class: SemanticClass, *, owner: str): +def _class_native_array_policies( + semantic_class: SemanticClass, + *, + owner: str, +) -> tuple[tuple[str, str, NativeArrayHandlePolicy], ...]: + policies = [] for field in semantic_class.fields: - yield from _variable_native_array_policy(field, owner=f"{owner}.{field.name}") + policies.extend(_variable_native_array_policy(field, owner=f"{owner}.{field.name}")) for nested in semantic_class.classes: - yield from _iter_class_native_array_policies(nested, owner=f"{owner}.{nested.name}") + policies.extend(_class_native_array_policies(nested, owner=f"{owner}.{nested.name}")) for method in semantic_class.methods: - yield from _iter_function_native_array_policies(method, owner=f"{owner}.{method.name}") + policies.extend(_function_native_array_policies(method, owner=f"{owner}.{method.name}")) for overload_set in semantic_class.overload_sets: - yield from _iter_overload_native_array_policies(overload_set, owner=f"{owner}.{overload_set.name}") + policies.extend(_overload_native_array_policies(overload_set, owner=f"{owner}.{overload_set.name}")) + return tuple(policies) -def _iter_overload_native_array_policies(overload_set: ProcedureOverloadSet, *, owner: str): +def _overload_native_array_policies( + overload_set: ProcedureOverloadSet, + *, + owner: str, +) -> tuple[tuple[str, str, NativeArrayHandlePolicy], ...]: + policies = [] for procedure in overload_set.procedures: - yield from _iter_function_native_array_policies(procedure, owner=owner) + policies.extend(_function_native_array_policies(procedure, owner=owner)) + return tuple(policies) -def _iter_function_native_array_policies(function: SemanticFunction, *, owner: str): +def _function_native_array_policies( + function: SemanticFunction, + *, + owner: str, +) -> tuple[tuple[str, str, NativeArrayHandlePolicy], ...]: + policies = [] for argument in function.arguments: - yield from _variable_native_array_policy(argument, owner=f"{owner}.{argument.name}") + policies.extend(_variable_native_array_policy(argument, owner=f"{owner}.{argument.name}")) if native_array_descriptor_kind(function.return_type) is not None: policy = function.metadata.get(RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA) if policy is None: @@ -309,19 +330,24 @@ def _iter_function_native_array_policies(function: SemanticFunction, *, owner: s f"Native array handle {owner}.return is missing completed policy; " "run complete_semantic_policies before collecting build requirements" ) - yield f"{owner}.return", "return", policy + policies.append((f"{owner}.return", "return", policy)) + return tuple(policies) -def _variable_native_array_policy(variable: SemanticVariable, *, owner: str): +def _variable_native_array_policy( + variable: SemanticVariable, + *, + owner: str, +) -> tuple[tuple[str, str, NativeArrayHandlePolicy], ...]: if native_array_descriptor_kind(variable.semantic_type) is None: - return + return () policy = variable.metadata.get(RESOLVED_NATIVE_ARRAY_HANDLE_POLICY_METADATA) if policy is None: raise ValueError( f"Native array handle {owner} is missing completed policy; " "run complete_semantic_policies before collecting build requirements" ) - yield owner, variable.name, policy + return ((owner, variable.name, policy),) __all__ = ( diff --git a/prik/printers/pyi.py b/prik/printers/pyi.py index 5e8aa77f3..33cc0de34 100644 --- a/prik/printers/pyi.py +++ b/prik/printers/pyi.py @@ -66,7 +66,7 @@ SemanticStorageContract, SemanticType, SemanticVariable, - _iter_module_semantic_types, + _module_semantic_types, ) from prik.semantics.native_array_handles import native_array_data_type, native_array_descriptor_kind from prik.utilities.visitor import ClassVisitor @@ -291,6 +291,10 @@ def _visit_SemanticPrototype( text += " = ..." arguments.append(text) decorators = [] + if prototype.origin.source_language == "fortran" and prototype.origin.native_abi == "c": + decorators.append(f'@{context.contract("native_abi")}("c")') + if prototype.origin.native_symbol and prototype.origin.native_symbol != prototype.origin.native_name: + decorators.append(f"@{context.contract('bind')}({json.dumps(prototype.origin.native_symbol)})") if prototype.pure: decorators.append(f"@{context.contract('pure')}") decorators.append(f"@{context.contract('prototype')}") @@ -384,8 +388,21 @@ 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 candidate.origin.native_abi == "c" and candidate.origin.native_symbol: + bind_target = ( + candidate.origin.native_symbol + if candidate.origin.native_symbol != candidate.origin.native_name + else None + ) bind = f"{indent}@{context.contract('bind')}({json.dumps(str(bind_target))})\n" if bind_target else "" - definitions.append(f'{bind}{indent}@{context.contract("overload")}("{target}"{generic})\n{definition}') + native_abi = ( + f'{indent}@{context.contract("native_abi")}("c")\n' + if candidate.origin.source_language == "fortran" and candidate.origin.native_abi == "c" + else "" + ) + definitions.append( + f'{native_abi}{bind}{indent}@{context.contract("overload")}("{target}"{generic})\n{definition}' + ) return "\n\n".join(definitions) def _visit_SemanticClass( @@ -1336,7 +1353,7 @@ def _module_reserved_names(cls, module: SemanticModule) -> set[str]: names.update(cls._import_local_names(imp)) for item in [*module.classes, *module.prototypes, *module.variables, *module.functions, *module.overload_sets]: cls._collect_reserved_item_names(item, names) - for semantic_type in _iter_module_semantic_types(module): + for semantic_type in _module_semantic_types(module): names.update(cls._contract_like_dimension_names(semantic_type)) return names @@ -1433,7 +1450,7 @@ def _missing_expression_callable_imports( } local_names = {function.name.casefold() for function in module.functions} required: dict[str, list[SemanticImportItem]] = {} - for semantic_type in _iter_module_semantic_types(module): + for semantic_type in _module_semantic_types(module): storage = semantic_type.storage array = storage.array if storage is not None else None if array is None: @@ -1475,7 +1492,7 @@ def _synthetic_flat_external_type_imports( for item in imp.items } synthetic: dict[str, list[SemanticImportItem]] = {} - for semantic_type in _iter_module_semantic_types(module): + for semantic_type in _module_semantic_types(module): ref = cls._flat_external_type_import_ref(semantic_type) if ref is None: continue @@ -1542,7 +1559,7 @@ def _is_procedure_local_external_ref(ref: dict[object, object]) -> bool: def _required_procedure_namespace_import_names(cls, module: SemanticModule) -> set[str]: """Return module namespaces required by procedure-local imported types.""" names: set[str] = set() - for semantic_type in _iter_module_semantic_types(module): + for semantic_type in _module_semantic_types(module): ref = semantic_type.metadata.get(EXTERNAL_TYPE_REF_METADATA) if not isinstance(ref, dict) or not cls._is_procedure_local_external_ref(ref): continue @@ -1951,17 +1968,8 @@ def _decorators( emitted_name: str | None = None, ) -> str: """Handle decorators for the current generation context.""" - decorators = [] emitted_name = emitted_name or func.name - if self._is_private(func): - decorators.append(f"{indent}@{context.contract('private')}") - if isinstance(func, SemanticMethod) and func.is_static: - decorators.append(f"{indent}@staticmethod") - bind_target = func.metadata.get(BIND_TARGET_METADATA) - if bind_target is None and func.native_name and func.native_name != emitted_name: - bind_target = func.native_name - if bind_target and not func.metadata.get(OVERLOAD_TARGET_METADATA): - decorators.append(f"{indent}@{context.contract('bind')}({json.dumps(str(bind_target))})") + decorators = self._identity_decorators(func, context, indent=indent, emitted_name=emitted_name) if ( func.origin.source_language == "fortran" and func.origin.native_scope is None @@ -1981,6 +1989,46 @@ def _decorators( return "" return "\n".join(decorators) + "\n" + def _identity_decorators( + self, + func: SemanticFunction, + context: _PyiEmissionContext, + *, + indent: str, + emitted_name: str, + ) -> list[str]: + """Emit visibility, method-kind, native-ABI, and link-name markers.""" + decorators = [] + if self._is_private(func): + decorators.append(f"{indent}@{context.contract('private')}") + if isinstance(func, SemanticMethod) and func.is_static: + decorators.append(f"{indent}@staticmethod") + 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: + decorators.append(f'{indent}@{context.contract("native_abi")}("c")') + bind_target = self._bind_target(func, emitted_name=emitted_name, is_native_c_abi=is_native_c_abi) + if bind_target and not is_overload: + decorators.append(f"{indent}@{context.contract('bind')}({json.dumps(str(bind_target))})") + return decorators + + @staticmethod + def _bind_target( + func: SemanticFunction, + *, + emitted_name: str, + is_native_c_abi: bool, + ) -> object | None: + """Return the explicit link label that must survive semantic printing.""" + if is_native_c_abi: + if func.origin.native_symbol and func.origin.native_symbol != func.origin.native_name: + return func.origin.native_symbol + return None + bind_target = func.metadata.get(BIND_TARGET_METADATA) + if bind_target is None and func.native_name and func.native_name != emitted_name: + return func.native_name + return bind_target + @staticmethod def _pyi_projection(func: SemanticFunction) -> list[ProjectionMapping]: """Return projection metadata adjusted for bound instance methods.""" @@ -2203,6 +2251,8 @@ def _native_projection_value( return f"{context.contract('Len')}({self._native_value_ref(mapping.value, context)})" if mapping.value_kind == "shape": return f"{self._native_value_ref(mapping.value['value'], context)}.shape[{mapping.value['dim']}]" + if mapping.value_kind == "stride": + return f"{self._native_value_ref(mapping.value['value'], context)}.strides[{mapping.value['dim']}]" if mapping.value_kind == "is_present": return f"{context.contract('IsPresent')}({self._native_value_ref(mapping.value, context)})" if mapping.value_kind == "work": diff --git a/prik/runtime/native_support/prik_binding.h b/prik/runtime/native_support/prik_binding.h index bd399f618..7bd91977d 100644 --- a/prik/runtime/native_support/prik_binding.h +++ b/prik/runtime/native_support/prik_binding.h @@ -20,6 +20,7 @@ #define NO_IMPORT_ARRAY #endif #include +#include #define PRIK_NATIVE_ARRAY_HANDLE_ABI_VERSION 1u #define PRIK_NATIVE_ARRAY_HANDLE_CAPSULE_NAME "prik.native_array_handle.v1" @@ -499,8 +500,8 @@ static inline int prik_int8_unpack_exact(PyObject *value, int8_t *destination) if (!PyArray_IsScalar(value, Int8)) { return -1; } - PyArray_ScalarAsCtype(value, destination); - return PyErr_Occurred() == NULL ? 0 : -1; + *destination = (int8_t)PyArrayScalar_VAL(value, Int8); + return 0; } static inline int prik_int16_unpack_exact(PyObject *value, int16_t *destination) @@ -508,8 +509,8 @@ static inline int prik_int16_unpack_exact(PyObject *value, int16_t *destination) if (!PyArray_IsScalar(value, Int16)) { return -1; } - PyArray_ScalarAsCtype(value, destination); - return PyErr_Occurred() == NULL ? 0 : -1; + *destination = (int16_t)PyArrayScalar_VAL(value, Int16); + return 0; } static inline int prik_int32_unpack_exact(PyObject *value, int32_t *destination) @@ -517,8 +518,8 @@ static inline int prik_int32_unpack_exact(PyObject *value, int32_t *destination) if (!PyArray_IsScalar(value, Int)) { return -1; } - PyArray_ScalarAsCtype(value, destination); - return PyErr_Occurred() == NULL ? 0 : -1; + *destination = (int32_t)PyArrayScalar_VAL(value, Int); + return 0; } static inline int prik_int64_unpack_exact(PyObject *value, int64_t *destination) @@ -526,8 +527,8 @@ static inline int prik_int64_unpack_exact(PyObject *value, int64_t *destination) if (!PyArray_IsScalar(value, Int64)) { return -1; } - PyArray_ScalarAsCtype(value, destination); - return PyErr_Occurred() == NULL ? 0 : -1; + *destination = (int64_t)PyArrayScalar_VAL(value, Int64); + return 0; } static inline int prik_float32_unpack_exact(PyObject *value, float *destination) @@ -535,8 +536,8 @@ static inline int prik_float32_unpack_exact(PyObject *value, float *destination) if (!PyArray_IsScalar(value, Float)) { return -1; } - PyArray_ScalarAsCtype(value, destination); - return PyErr_Occurred() == NULL ? 0 : -1; + *destination = (float)PyArrayScalar_VAL(value, Float); + return 0; } static inline int prik_float64_unpack_exact(PyObject *value, double *destination) @@ -544,8 +545,8 @@ static inline int prik_float64_unpack_exact(PyObject *value, double *destination if (!PyArray_IsScalar(value, Double)) { return -1; } - PyArray_ScalarAsCtype(value, destination); - return PyErr_Occurred() == NULL ? 0 : -1; + *destination = (double)PyArrayScalar_VAL(value, Double); + return 0; } static inline int prik_complex64_unpack_exact(PyObject *value, float complex *destination) @@ -553,8 +554,8 @@ static inline int prik_complex64_unpack_exact(PyObject *value, float complex *de if (!PyArray_IsScalar(value, CFloat)) { return -1; } - PyArray_ScalarAsCtype(value, destination); - return PyErr_Occurred() == NULL ? 0 : -1; + *destination = (float complex)PyArrayScalar_VAL(value, CFloat); + return 0; } static inline int prik_complex128_unpack_exact(PyObject *value, double complex *destination) @@ -562,8 +563,8 @@ static inline int prik_complex128_unpack_exact(PyObject *value, double complex * if (!PyArray_IsScalar(value, CDouble)) { return -1; } - PyArray_ScalarAsCtype(value, destination); - return PyErr_Occurred() == NULL ? 0 : -1; + *destination = (double complex)PyArrayScalar_VAL(value, CDouble); + return 0; } /* Type-specific coercive conversion for boundaries that permit Python scalars. */ @@ -710,56 +711,83 @@ static inline PyObject *prik_complex128_to_python(const double complex *value) /* Create typed NumPy scalars without a runtime dtype argument. */ static inline PyObject *prik_bool_to_numpy(const bool *value) { - PyArray_Descr *descriptor = PyArray_DescrFromType(NPY_BOOL); - return descriptor == NULL ? NULL : PyArray_Scalar((void *)value, descriptor, NULL); + PyObject *result = PyArrayScalar_New(Bool); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, Bool, (npy_bool)*value); + } + return result; } static inline PyObject *prik_int8_to_numpy(const int8_t *value) { - PyArray_Descr *descriptor = PyArray_DescrFromType(NPY_INT8); - return descriptor == NULL ? NULL : PyArray_Scalar((void *)value, descriptor, NULL); + PyObject *result = PyArrayScalar_New(Int8); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, Int8, (npy_int8)*value); + } + return result; } static inline PyObject *prik_int16_to_numpy(const int16_t *value) { - PyArray_Descr *descriptor = PyArray_DescrFromType(NPY_INT16); - return descriptor == NULL ? NULL : PyArray_Scalar((void *)value, descriptor, NULL); + PyObject *result = PyArrayScalar_New(Int16); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, Int16, (npy_int16)*value); + } + return result; } static inline PyObject *prik_int32_to_numpy(const int32_t *value) { - PyArray_Descr *descriptor = PyArray_DescrFromType(NPY_INT32); - return descriptor == NULL ? NULL : PyArray_Scalar((void *)value, descriptor, NULL); + PyObject *result = PyArrayScalar_New(Int); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, Int, (npy_int32)*value); + } + return result; } static inline PyObject *prik_int64_to_numpy(const int64_t *value) { - PyArray_Descr *descriptor = PyArray_DescrFromType(NPY_INT64); - return descriptor == NULL ? NULL : PyArray_Scalar((void *)value, descriptor, NULL); + PyObject *result = PyArrayScalar_New(Int64); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, Int64, (npy_int64)*value); + } + return result; } static inline PyObject *prik_float32_to_numpy(const float *value) { - PyArray_Descr *descriptor = PyArray_DescrFromType(NPY_FLOAT32); - return descriptor == NULL ? NULL : PyArray_Scalar((void *)value, descriptor, NULL); + PyObject *result = PyArrayScalar_New(Float); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, Float, (npy_float32)*value); + } + return result; } static inline PyObject *prik_float64_to_numpy(const double *value) { - PyArray_Descr *descriptor = PyArray_DescrFromType(NPY_FLOAT64); - return descriptor == NULL ? NULL : PyArray_Scalar((void *)value, descriptor, NULL); + PyObject *result = PyArrayScalar_New(Double); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, Double, (npy_float64)*value); + } + return result; } static inline PyObject *prik_complex64_to_numpy(const float complex *value) { - PyArray_Descr *descriptor = PyArray_DescrFromType(NPY_COMPLEX64); - return descriptor == NULL ? NULL : PyArray_Scalar((void *)value, descriptor, NULL); + PyObject *result = PyArrayScalar_New(CFloat); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, CFloat, (npy_cfloat)*value); + } + return result; } static inline PyObject *prik_complex128_to_numpy(const double complex *value) { - PyArray_Descr *descriptor = PyArray_DescrFromType(NPY_COMPLEX128); - return descriptor == NULL ? NULL : PyArray_Scalar((void *)value, descriptor, NULL); + PyObject *result = PyArrayScalar_New(CDouble); + if (result != NULL) { + PyArrayScalar_ASSIGN(result, CDouble, (npy_cdouble)*value); + } + return result; } /* Release a bridge-owned allocation transferred through a NumPy base capsule. */ diff --git a/prik/semantics/c2ir.py b/prik/semantics/c2ir.py index 86a4fa9b9..fe72810ea 100644 --- a/prik/semantics/c2ir.py +++ b/prik/semantics/c2ir.py @@ -71,7 +71,7 @@ SemanticStorageContract, SemanticType, SemanticVariable, - _iter_module_semantic_types, + _module_semantic_types, ) @@ -1270,7 +1270,7 @@ def _externalize_private_classes(self, module: SemanticModule) -> None: if not external_classes: return - for semantic_type in _iter_module_semantic_types(module): + for semantic_type in _module_semantic_types(module): origin_module = external_classes.get(semantic_type.name) if origin_module is None: continue @@ -1309,7 +1309,7 @@ def _classify_project_external_types( } if not external_names: continue - for semantic_type in _iter_module_semantic_types(module): + for semantic_type in _module_semantic_types(module): owner = owners.get(semantic_type.name) if owner is None or owner[0] == module.name: continue @@ -1462,7 +1462,7 @@ def _semantic_type_from_standard_fact(fact: dict[str, Any]) -> str | None: def _standard_type_facts(report: Any | None) -> dict[str, dict[str, Any]]: """Normalize a probe report or mapping into copied per-standard-type facts. - Missing or malformed reports yield an empty lookup. Fact dictionaries + Missing or malformed reports produce an empty lookup. Fact dictionaries are copied so caller-owned reports cannot be mutated during conversion. """ if report is None: diff --git a/prik/semantics/fortran2ir.py b/prik/semantics/fortran2ir.py index 615c2ace9..120a94084 100644 --- a/prik/semantics/fortran2ir.py +++ b/prik/semantics/fortran2ir.py @@ -46,7 +46,12 @@ split_top_level_expression, ) from prik.semantics.ownership_metadata import set_ownership_metadata -from prik.semantics.metadata import BIND_TARGET_METADATA, PROJECTED_OUTPUT_METADATA, SCALAR_STORAGE_CATEGORY +from prik.semantics.metadata import ( + BIND_TARGET_METADATA, + OPTIONAL_ABSENT_HANDLE_METADATA, + PROJECTED_OUTPUT_METADATA, + SCALAR_STORAGE_CATEGORY, +) from prik.semantics.scalar_types import ( BOOLEAN_STORAGE_BITS, SEMANTIC_SCALAR_TYPE_NAMES, @@ -517,19 +522,67 @@ def _visit_FortranArgument( source_kind=source_kind, declaration_arrays=declaration_arrays, ) + semantic_type = self._argument_semantic_type( + arg, + callback_interfaces=callback_interfaces, + derived_type_context=derived_type_context, + declaration_arrays=declaration_arrays, + ) + access = self._argument_access(arg, semantic_type) + self._complete_argument_storage(arg, semantic_type, access=access) + self._apply_argument_ownership(semantic_type, writes_argument=access[1]) + + argument = SemanticArgument( + name=arg.name, + semantic_type=semantic_type, + optional=getattr(arg, "optional", False), + visibility=getattr(arg, "visibility", "public"), + metadata=self._argument_metadata(arg, semantic_type), + origin=self._argument_origin(arg), + ) + # Source access is an internal optimization fact, not part of the + # serialized semantic contract. It lets policy omit a useless copy-in + # for intent(out) arrays without changing scalar ownership behavior. + argument._source_reads_argument = access[0] + return argument + + def _argument_semantic_type( + self, + arg: FortranArgument | FortranVariable, + *, + callback_interfaces: dict[str, FortranProcedureSignature] | None, + derived_type_context: _DerivedTypeContext | None, + declaration_arrays: dict[str, ArrayExpressionSource] | None, + ) -> SemanticType: + """Convert one dummy's declared type without completing storage policy.""" if arg.base_type.lower() == "procedure": - semantic_type = self._callback_semantic_type( + return self._callback_semantic_type( arg, callback_interfaces or {}, derived_type_context=derived_type_context, ) - else: - semantic_type = self._convert_variable_type( - arg, - derived_type_context=derived_type_context, - declaration_arrays=declaration_arrays, - ) - access = self._argument_access(arg, semantic_type) + return self._convert_variable_type( + arg, + derived_type_context=derived_type_context, + declaration_arrays=declaration_arrays, + ) + + def _complete_argument_storage( + self, + arg: FortranArgument | FortranVariable, + semantic_type: SemanticType, + *, + access: tuple[bool, bool], + ) -> None: + """Complete source storage facts for one converted Fortran dummy.""" + if ( + getattr(arg, "optional", False) + and semantic_type.rank > 0 + and semantic_type.storage is not None + and semantic_type.storage.array is not None + and (semantic_type.storage.array.allocatable or semantic_type.storage.array.pointer) + ): + semantic_type.metadata[OPTIONAL_ABSENT_HANDLE_METADATA] = True if semantic_type.storage is not None and semantic_type.storage.kind == "callback": pass elif semantic_type.rank > 0: @@ -542,24 +595,20 @@ def _visit_FortranArgument( semantic_type.storage.pointer_depth = 1 if getattr(arg, "pointer", False) and not access[1]: self._apply_pointer_input_policy(semantic_type) - self._apply_argument_ownership(semantic_type, writes_argument=access[1]) + @staticmethod + def _argument_metadata( + arg: FortranArgument | FortranVariable, + semantic_type: SemanticType, + ) -> dict[str, object]: + """Return semantic-call metadata that must survive contract printing.""" metadata = {} - if getattr(arg, "pass_by_value", False) and str(getattr(arg, "base_type", "")).casefold() == "derived": - metadata[NATIVE_BY_VALUE_METADATA] = True - argument = SemanticArgument( - name=arg.name, - semantic_type=semantic_type, - optional=getattr(arg, "optional", False), - visibility=getattr(arg, "visibility", "public"), - metadata=metadata, - origin=self._argument_origin(arg), + preserves_explicit_value = str(getattr(arg, "base_type", "")).casefold() == "derived" or ( + semantic_type.name == "String" and str(semantic_type.metadata.get("fortran_character_length", "")) == "1" ) - # Source access is an internal optimization fact, not part of the - # serialized semantic contract. It lets policy omit a useless copy-in - # for intent(out) arrays without changing scalar ownership behavior. - argument._source_reads_argument = access[0] - return argument + if getattr(arg, "pass_by_value", False) and preserves_explicit_value: + metadata[NATIVE_BY_VALUE_METADATA] = True + return metadata def _convert_data_member( self, @@ -685,6 +734,8 @@ def _callback_semantic_type( "callback_thread": "entering_thread", "callback_exception": "print_traceback_and_abort", "prototype_metadata": self._procedure_metadata(signature), + "prototype_source_language": "fortran", + "prototype_native_abi": self._procedure_native_abi(signature), }, storage=SemanticStorageContract( kind="callback", @@ -781,6 +832,8 @@ def _module_prototypes( origin=SemanticOrigin( source_language="fortran", native_name=name, + native_abi=self._procedure_native_abi(signature), + native_symbol=self._procedure_native_symbol(signature), native_scope=module.name, source_kind="prototype", metadata={"fortran_interface_kind": "abstract" if interface.abstract else "explicit"}, @@ -875,6 +928,7 @@ def _visit_FortranProcedureSignature( for arg in self._projected_procedure_arguments(proc) ] metadata = self._procedure_metadata(proc) + native_abi = self._procedure_native_abi(proc) return_type = ( self.visit( proc.result, @@ -898,6 +952,8 @@ def _visit_FortranProcedureSignature( origin=SemanticOrigin( source_language="fortran", native_name=proc.name, + native_abi=native_abi, + native_symbol=self._procedure_native_symbol(proc), native_scope=proc.module, source_kind=proc.kind, metadata=dict(metadata), @@ -1768,16 +1824,25 @@ def _fortran_variable_metadata(var: FortranVariable) -> dict[str, object]: @staticmethod def _procedure_metadata(proc: FortranProcedureSignature) -> dict[str, object]: - """Return native procedure attributes and optional bind name for semantic IR.""" + """Return non-ABI native procedure attributes for semantic IR.""" metadata: dict[str, object] = {} - if proc.attributes: - metadata["fortran_attributes"] = list(proc.attributes) - if "bind(c)" in proc.attributes: - metadata["fortran_bind_c"] = True - if proc.bind_name: - metadata["fortran_bind_c_name"] = proc.bind_name + attributes = [item for item in proc.attributes if item.casefold().replace(" ", "") != "bind(c)"] + if attributes: + metadata["fortran_attributes"] = attributes return metadata + @staticmethod + def _procedure_native_abi(proc: FortranProcedureSignature) -> str | None: + """Return the standard ABI declared by an original Fortran procedure.""" + return "c" if any(item.casefold().replace(" ", "") == "bind(c)" for item in proc.attributes) else None + + @classmethod + def _procedure_native_symbol(cls, proc: FortranProcedureSignature) -> str | None: + """Return the linkable C label only for a C-interoperable procedure.""" + if cls._procedure_native_abi(proc) is None: + return None + return proc.bind_name or proc.name + # Storage and argument-contract helpers def _array_storage_contract( @@ -2859,21 +2924,21 @@ def _requirement_unit_name( return unit_name or module or "" -def _iter_fortran_variable_contexts( +def _fortran_variable_contexts( node, *, module_name: str | None = None, unit_kind: str = "file", unit_name: str | None = None, ): - """Yield parser variables with enough unit context for diagnostics. + """Return parser variables with enough unit context for diagnostics. Example: - ``list(_iter_fortran_variable_contexts(parsed_file))`` returns module + ``_fortran_variable_contexts(parsed_file)`` returns module parameters, procedure arguments/results/locals, and type fields with the unit that owns each symbol. """ - yield from _FortranVariableContextVisitor()._visit( + return _FortranVariableContextVisitor()._visit( node, module_name=module_name, unit_kind=unit_kind, @@ -2885,15 +2950,19 @@ class _FortranVariableContextVisitor(ClassVisitor): """Traverse parsed Fortran models through the shared class visitor protocol.""" def _visit_FortranProject(self, node: FortranProject, **_context): - """Yield variable contexts from every project file.""" + """Return variable contexts from every project file.""" + contexts = [] for parsed_file in node.files: - yield from self._visit(parsed_file) + contexts.extend(self._visit(parsed_file)) + return tuple(contexts) def _visit_FortranFile(self, node: FortranFile, **_context): - """Yield file, module, and standalone variable contexts.""" + """Return file, module, and standalone variable contexts.""" file_unit = node.filename or "" - for variable in getattr(node, "variables", []): - yield _variable_context(variable, unit_kind="file", unit=file_unit, module=None, role="variable") + contexts = [ + _variable_context(variable, unit_kind="file", unit=file_unit, module=None, role="variable") + for variable in getattr(node, "variables", []) + ] collections = ( node.modules, node.submodules, @@ -2904,30 +2973,36 @@ def _visit_FortranFile(self, node: FortranFile, **_context): ) for collection in collections: for child in collection: - yield from self._visit(child) + contexts.extend(self._visit(child)) + return tuple(contexts) def _visit_FortranModule(self, node: FortranModule, **_context): - """Yield module-owned variable contexts.""" - yield from self._module_variable_contexts(node, unit_kind="module") + """Return module-owned variable contexts.""" + return self._module_variable_contexts(node, unit_kind="module") def _visit_FortranSubmodule(self, node: FortranSubmodule, **_context): - """Yield submodule-owned variable contexts.""" - yield from self._module_variable_contexts(node, unit_kind="submodule") + """Return submodule-owned variable contexts.""" + return self._module_variable_contexts(node, unit_kind="submodule") def _visit_FortranProgram(self, node: FortranProgram, **_context): - """Yield program-owned variable contexts.""" + """Return program-owned variable contexts.""" owner = node.name or "" - for variable in node.variables: - yield _variable_context(variable, unit_kind="program", unit=owner, module=None, role="variable") + contexts = [ + _variable_context(variable, unit_kind="program", unit=owner, module=None, role="variable") + for variable in node.variables + ] for procedure in node.procedures: - yield from self._visit(procedure, unit_kind="program", unit_name=owner) + contexts.extend(self._visit(procedure, unit_kind="program", unit_name=owner)) + return tuple(contexts) @staticmethod def _visit_FortranBlockData(node: FortranBlockData, **_context): - """Yield block-data variable contexts.""" + """Return block-data variable contexts.""" owner = node.name or "" - for variable in node.variables: - yield _variable_context(variable, unit_kind="block_data", unit=owner, module=None, role="variable") + return tuple( + _variable_context(variable, unit_kind="block_data", unit=owner, module=None, role="variable") + for variable in node.variables + ) @staticmethod def _visit_FortranProcedureSignature( @@ -2936,7 +3011,7 @@ def _visit_FortranProcedureSignature( module_name: str | None = None, **_context, ): - """Yield procedure argument, result, and local contexts.""" + """Return procedure argument, result, and local contexts.""" procedure_module = module_name or node.module owner = _requirement_unit_name(module=procedure_module, unit_name=node.name) context = { @@ -2945,12 +3020,12 @@ def _visit_FortranProcedureSignature( "module": procedure_module, "procedure": node.name, } - for argument in node.arguments: - yield _variable_context(argument, **context, role="argument") + contexts = [_variable_context(argument, **context, role="argument") for argument in node.arguments] if node.result is not None: - yield _variable_context(node.result, **context, role="result") + contexts.append(_variable_context(node.result, **context, role="result")) for variable in node.variables.values(): - yield _variable_context(variable, **context, role="variable") + contexts.append(_variable_context(variable, **context, role="variable")) + return tuple(contexts) @staticmethod def _visit_FortranDerivedType( @@ -2959,11 +3034,11 @@ def _visit_FortranDerivedType( module_name: str | None = None, **_context, ): - """Yield derived-type field contexts.""" + """Return derived-type field contexts.""" owner_module = module_name or node.module owner = _requirement_unit_name(module=owner_module, unit_name=node.name) - for field in node.fields: - yield _variable_context( + return tuple( + _variable_context( field, unit_kind="derived_type", unit=owner, @@ -2971,6 +3046,8 @@ def _visit_FortranDerivedType( type_owner=node.name, role="field", ) + for field in node.fields + ) def _module_variable_contexts( self, @@ -2978,14 +3055,17 @@ def _module_variable_contexts( *, unit_kind: str, ): - """Yield variable, procedure, and type contexts owned by a module-like node.""" + """Return variable, procedure, and type contexts owned by a module-like node.""" owner = node.name - for variable in node.variables: - yield _variable_context(variable, unit_kind=unit_kind, unit=owner, module=owner, role="variable") + contexts = [ + _variable_context(variable, unit_kind=unit_kind, unit=owner, module=owner, role="variable") + for variable in node.variables + ] for procedure in node.procedures: - yield from self._visit(procedure, module_name=owner) + contexts.extend(self._visit(procedure, module_name=owner)) for derived_type in node.derived_types: - yield from self._visit(derived_type, module_name=owner) + contexts.extend(self._visit(derived_type, module_name=owner)) + return tuple(contexts) def _variable_context(variable, *, unit_kind, unit, module, role, **extra): @@ -3056,7 +3136,7 @@ def collect_fortran_type_storage_requirements( converter = FortranToIRConverter(compile_time_values=compile_time_values) requirements: list[dict[str, object]] = [] seen: set[tuple[str, str | None]] = set() - for var, context in _iter_fortran_variable_contexts(parsed): + for var, context in _fortran_variable_contexts(parsed): base_type = str(var.base_type or "").lower() if base_type not in _FORTRAN_STORAGE_PROBE_TYPES: continue @@ -3131,7 +3211,7 @@ def add_requirement( seen.add(key) requirements.append(item) - for var, ctx in _iter_fortran_variable_contexts(parsed): + for var, ctx in _fortran_variable_contexts(parsed): expression = var.symbolic_value if var.symbolic_value is not None else var.value parameter_base_type = str(var.base_type or "").lower() if var.is_parameter and parameter_base_type == "integer" and var.value is None and expression: diff --git a/prik/semantics/models.py b/prik/semantics/models.py index 8944c5ad8..31b8b14f0 100644 --- a/prik/semantics/models.py +++ b/prik/semantics/models.py @@ -77,6 +77,8 @@ class OwnershipPolicy: class SemanticOrigin: source_language: str | None = None native_name: str | None = None + native_abi: str | None = None + native_symbol: str | None = None native_scope: str | None = None source_kind: str | None = None source_type: str | None = None @@ -662,51 +664,67 @@ class SemanticModule: origin: SemanticOrigin = field(default_factory=SemanticOrigin, compare=False) -def _iter_semantic_type_tree(semantic_type: SemanticType | None): +def _semantic_type_tree(semantic_type: SemanticType | None) -> tuple[SemanticType, ...]: + """Collect one semantic type and its callback signature in stable order.""" if semantic_type is None: - return - yield semantic_type - if semantic_type.storage is not None and semantic_type.storage.kind == "callback": - arguments = semantic_type.metadata.get("arguments") + return () + collected = [] + pending = [semantic_type] + while pending: + current = pending.pop() + collected.append(current) + if current.storage is None or current.storage.kind != "callback": + continue + nested = [] + arguments = current.metadata.get("arguments") if isinstance(arguments, list): - for argument in arguments: - yield from _iter_semantic_type_tree(argument) - yield from _iter_semantic_type_tree(semantic_type.metadata.get("return")) + nested.extend(arguments) + result = current.metadata.get("return") + if result is not None: + nested.append(result) + pending.extend(reversed(nested)) + return tuple(collected) -def _iter_module_semantic_types(module: SemanticModule): - def iter_class(declaration: SemanticClass): +def _module_semantic_types(module: SemanticModule) -> tuple[SemanticType, ...]: + """Collect every semantic type owned by a module in established order.""" + + def class_types(declaration: SemanticClass) -> tuple[SemanticType, ...]: + collected = [] for nested in declaration.classes: - yield from iter_class(nested) + collected.extend(class_types(nested)) for semantic_field in declaration.fields: - yield from _iter_semantic_type_tree(semantic_field.semantic_type) + collected.extend(_semantic_type_tree(semantic_field.semantic_type)) for method in declaration.methods: for argument in method.arguments: - yield from _iter_semantic_type_tree(argument.semantic_type) - yield from _iter_semantic_type_tree(method.return_type) + collected.extend(_semantic_type_tree(argument.semantic_type)) + collected.extend(_semantic_type_tree(method.return_type)) for overload_set in declaration.overload_sets: for procedure in overload_set.procedures: for argument in procedure.arguments: - yield from _iter_semantic_type_tree(argument.semantic_type) - yield from _iter_semantic_type_tree(procedure.return_type) + collected.extend(_semantic_type_tree(argument.semantic_type)) + collected.extend(_semantic_type_tree(procedure.return_type)) + return tuple(collected) + collected = [] for variable in module.variables: - yield from _iter_semantic_type_tree(variable.semantic_type) + collected.extend(_semantic_type_tree(variable.semantic_type)) for declaration in module.classes: - yield from iter_class(declaration) + collected.extend(class_types(declaration)) for function in module.functions: for argument in function.arguments: - yield from _iter_semantic_type_tree(argument.semantic_type) - yield from _iter_semantic_type_tree(function.return_type) + collected.extend(_semantic_type_tree(argument.semantic_type)) + collected.extend(_semantic_type_tree(function.return_type)) for prototype in module.prototypes: for argument in prototype.arguments: - yield from _iter_semantic_type_tree(argument.semantic_type) - yield from _iter_semantic_type_tree(prototype.return_type) + collected.extend(_semantic_type_tree(argument.semantic_type)) + collected.extend(_semantic_type_tree(prototype.return_type)) for overload_set in module.overload_sets: for procedure in overload_set.procedures: for argument in procedure.arguments: - yield from _iter_semantic_type_tree(argument.semantic_type) - yield from _iter_semantic_type_tree(procedure.return_type) + collected.extend(_semantic_type_tree(argument.semantic_type)) + collected.extend(_semantic_type_tree(procedure.return_type)) + return tuple(collected) if __name__ == "__main__": diff --git a/prik/semantics/native_contract.py b/prik/semantics/native_contract.py index 8c85998b9..8012033fb 100644 --- a/prik/semantics/native_contract.py +++ b/prik/semantics/native_contract.py @@ -24,7 +24,7 @@ SemanticModule, SemanticPrototype, SemanticType, - _iter_module_semantic_types, + _module_semantic_types, ) @@ -64,7 +64,7 @@ def _prepare_module(module: SemanticModule) -> None: _prepare_function(procedure, native_scope) for semantic_class in module.classes: _prepare_class(semantic_class, native_scope) - for semantic_type in _iter_module_semantic_types(module): + for semantic_type in _module_semantic_types(module): semantic_type.origin.source_language = "fortran" diff --git a/prik/semantics/pyi2ir.py b/prik/semantics/pyi2ir.py index c4f53dfad..62b1484d0 100644 --- a/prik/semantics/pyi2ir.py +++ b/prik/semantics/pyi2ir.py @@ -81,7 +81,7 @@ SemanticStorageContract, SemanticType, SemanticVariable, - _iter_module_semantic_types, + _module_semantic_types, ) __all__ = ("convert_pyi_to_ir", "reconcile_external_type_refs") @@ -156,6 +156,7 @@ class _Decorators: overload_target: str | None = None overload_generic: str | None = None bind_target: str | None = None + native_abi: str | None = None native_type: dict[str, object] | None = None standalone: bool = False is_static: bool = False @@ -227,7 +228,7 @@ def _resolve_local_prototype_references(self) -> None: collisions = sorted(runtime_names & prototypes.keys()) if collisions: raise ValueError(f"Prototype name collides with a runtime declaration: {collisions[0]!r}") - for semantic_type in _iter_module_semantic_types(self.module): + for semantic_type in _module_semantic_types(self.module): if semantic_type.storage is not None and semantic_type.storage.kind == "callback": continue prototype = prototypes.get(semantic_type.name) @@ -250,7 +251,7 @@ def _resolve_declaration_expression_callables(self) -> None: local_functions = {function.name.casefold(): function for function in self.module.functions} local_prototypes = {prototype.name.casefold(): prototype for prototype in self.module.prototypes} explicit_imports, namespace_imports = self._declaration_callable_imports() - for semantic_type in _iter_module_semantic_types(self.module): + for semantic_type in _module_semantic_types(self.module): storage = semantic_type.storage array = storage.array if storage is not None else None if array is None: @@ -516,6 +517,7 @@ def function_def( projection: list[ProjectionMapping] | None = None, native_result: ProjectionMapping | None = None, native_name: str | None = None, + native_abi: str | None = None, standalone: bool = False, has_native_call: bool = False, release_gil: bool = False, @@ -542,15 +544,19 @@ def function_def( if error_status_policy is not None: metadata[RUNTIME_STATUS_ERROR_METADATA] = dict(error_status_policy) origin = self._origin( - source_language="fortran" if standalone else None, + source_language="fortran" if standalone or native_abi is not None else None, user_private=visibility == "private", ) - if standalone: + if standalone or native_abi is not None: origin.source_kind = "function" if return_type is not None else "subroutine" - origin.native_name = native_name or node.name + origin.native_name = node.name if native_abi is not None else native_name or node.name + origin.native_scope = None if standalone else self.module.name + if native_abi is not None: + origin.native_abi = native_abi + origin.native_symbol = native_name or node.name return SemanticFunction( name=node.name, - native_name=native_name or node.name, + native_name=node.name if native_abi is not None else native_name or node.name, arguments=semantic_args, return_type=return_type, projection=actual_projection, @@ -565,6 +571,8 @@ def prototype_def( *, visibility: str, pure: bool, + native_name: str | None = None, + native_abi: str | None = None, ) -> SemanticPrototype: """Convert one exact native interface without creating a runtime function.""" self._validate_callable_header(node) @@ -599,7 +607,10 @@ def prototype_def( metadata=metadata, visibility=visibility, origin=SemanticOrigin( + source_language="fortran" if native_abi is not None else None, native_name=node.name, + native_abi=native_abi, + native_symbol=native_name or node.name if native_abi is not None else None, native_scope=self.module.name, source_kind="prototype", ), @@ -615,6 +626,7 @@ def method_def( native_result: ProjectionMapping | None = None, is_static: bool = False, native_name: str | None = None, + native_abi: str | None = None, class_name: str, infer_passed_object: bool = True, has_native_call: bool = False, @@ -638,41 +650,28 @@ def method_def( metadata = {BIND_TARGET_METADATA: native_name} if native_name is not None else {} if has_native_call: metadata[NATIVE_PROJECTION_METADATA] = True - passed_object_name = None - passed_object_position = None - if infer_passed_object and not is_static: - pass_mappings = [mapping for mapping in actual_projection if mapping.value_kind == "pass"] - if node.name == "__init__" and len(pass_mappings) != 1: - raise ValueError("Bound constructor native_call requires exactly one Pass() entry") - if len(pass_mappings) > 1: - raise ValueError("native_call may contain at most one Pass() entry") - passed_object_position = pass_mappings[0].native_position if pass_mappings else 0 - if not isinstance(passed_object_position, int) or not 0 <= passed_object_position <= len(semantic_args): - raise ValueError("native_call Pass() position is out of range") - passed_object_name = "self" - semantic_args.insert( - passed_object_position, - SemanticArgument( - passed_object_name, - SemanticType( - class_name, - dtype=class_name, - storage=SemanticStorageContract(kind="reference", mutable=True, pointer_depth=1), - ), - ), - ) - self._restore_pass_projection(actual_projection, passed_object_position) + passed_object_name, passed_object_position = self._complete_method_passed_object( + node, + projection=actual_projection, + arguments=semantic_args, + class_name=class_name, + infer_passed_object=infer_passed_object, + is_static=is_static, + ) if release_gil: metadata[RUNTIME_RELEASE_GIL_METADATA] = True if error_status_policy is not None: metadata[RUNTIME_STATUS_ERROR_METADATA] = dict(error_status_policy) - origin = self._origin( - source_language=None, - user_private=visibility == "private", + origin = self._method_origin( + node, + visibility=visibility, + native_name=native_name, + native_abi=native_abi, + return_type=return_type, ) return SemanticMethod( name=node.name, - native_name=native_name or node.name, + native_name=node.name if native_abi is not None else native_name or node.name, arguments=semantic_args, return_type=return_type, projection=actual_projection, @@ -684,6 +683,64 @@ def method_def( passed_object_position=passed_object_position, ) + def _complete_method_passed_object( + self, + node: ast.FunctionDef, + *, + projection: list[ProjectionMapping], + arguments: list[SemanticArgument], + class_name: str, + infer_passed_object: bool, + is_static: bool, + ) -> tuple[str | None, int | None]: + """Insert and project the implicit object for one bound method.""" + if not infer_passed_object or is_static: + return None, None + pass_mappings = [mapping for mapping in projection if mapping.value_kind == "pass"] + if node.name == "__init__" and len(pass_mappings) != 1: + raise ValueError("Bound constructor native_call requires exactly one Pass() entry") + if len(pass_mappings) > 1: + raise ValueError("native_call may contain at most one Pass() entry") + passed_object_position = pass_mappings[0].native_position if pass_mappings else 0 + if not isinstance(passed_object_position, int) or not 0 <= passed_object_position <= len(arguments): + raise ValueError("native_call Pass() position is out of range") + arguments.insert( + passed_object_position, + SemanticArgument( + "self", + SemanticType( + class_name, + dtype=class_name, + storage=SemanticStorageContract(kind="reference", mutable=True, pointer_depth=1), + ), + ), + ) + self._restore_pass_projection(projection, passed_object_position) + return "self", passed_object_position + + def _method_origin( + self, + node: ast.FunctionDef, + *, + visibility: str, + native_name: str | None, + native_abi: str | None, + return_type: SemanticType | None, + ) -> SemanticOrigin: + """Retain a method's language identity, ABI, and optional link label.""" + origin = self._origin( + source_language="fortran" if native_abi is not None else None, + user_private=visibility == "private", + ) + if native_abi is None: + return origin + origin.native_name = node.name + origin.native_abi = native_abi + origin.native_symbol = native_name or node.name + origin.native_scope = self.module.name + origin.source_kind = "function" if return_type is not None else "subroutine" + return origin + @staticmethod def _restore_pass_projection(projection: list[ProjectionMapping], passed_position: int) -> None: """Replace ``Pass()`` markers after inserting ``self`` into a method signature. @@ -757,8 +814,8 @@ def decorators(self, nodes: list[ast.expr], *, context: str) -> _Decorators: "prototype cannot be combined with standalone; " "prototype use already determines its native procedure role" ) - if parsed.has_native_call or parsed.overload_target is not None or parsed.bind_target is not None: - raise ValueError("prototype cannot be combined with native_call, overload, or bind") + if parsed.has_native_call or parsed.overload_target is not None: + raise ValueError("prototype cannot be combined with native_call or overload") if parsed.release_gil or parsed.error_status_policy is not None or parsed.native_type is not None: raise ValueError("prototype cannot carry wrapper or native-type decorators") if parsed.visibility != "public" or parsed.is_static: @@ -782,6 +839,7 @@ def _apply_decorator(self, parsed: _Decorators, node: ast.expr, *, context: str) handlers = { "overload": self._apply_overload_decorator, "bind": self._apply_bind_decorator, + "native_abi": self._apply_native_abi_decorator, "standalone": self._apply_standalone_decorator, "nogil": self._apply_nogil_decorator, "native_call": self._apply_native_call_decorator, @@ -853,6 +911,17 @@ def _apply_bind_decorator(self, parsed: _Decorators, node: ast.expr, context: st raise ValueError(f"Duplicate {context} bind decorator") parsed.bind_target = self._required_string_decorator_argument(node, "bind") + def _apply_native_abi_decorator(self, parsed: _Decorators, node: ast.expr, context: str) -> None: + """Retain the C ABI declared by an original Fortran procedure.""" + if parsed.native_abi is not None: + raise ValueError(f"Duplicate {context} native_abi decorator") + if self.native_language != "fortran": + raise ValueError("native_abi is only valid for Fortran semantic .pyi procedures") + value = self._required_string_decorator_argument(node, "native_abi") + if value.casefold() != "c": + raise ValueError('native_abi accepts only "c"') + parsed.native_abi = "c" + @staticmethod def _apply_nogil_decorator(parsed: _Decorators, node: ast.expr, context: str) -> None: """Record a no-argument GIL-release request in decorator state.""" @@ -999,13 +1068,6 @@ def _resolve_overloads(self) -> None: ) overload_set.procedures.append(candidate) - @classmethod - def _iter_classes(cls, classes: list[SemanticClass]): - """Yield classes and nested classes depth first in source-list order.""" - for semantic_class in classes: - yield semantic_class - yield from cls._iter_classes(semantic_class.classes) - @staticmethod def _overload_set_name(owner: SemanticModule | SemanticClass, declaration_name: str) -> str: """Return the semantic overload-set name, normalizing reflected class operators.""" @@ -1055,6 +1117,12 @@ def _validated_overload_candidate( candidate = deepcopy(target) candidate.visibility = declaration.visibility candidate.metadata[OVERLOAD_TARGET_METADATA] = target.name + if declaration.origin.native_abi is not None: + if candidate.origin.native_abi not in {None, declaration.origin.native_abi}: + raise ValueError("Overload declaration native ABI contradicts its specific procedure") + candidate.origin.native_abi = declaration.origin.native_abi + candidate.origin.source_language = declaration.origin.source_language + candidate.origin.native_symbol = declaration.origin.native_symbol or candidate.origin.native_symbol for key in (RUNTIME_RELEASE_GIL_METADATA, RUNTIME_STATUS_ERROR_METADATA): if key in declaration.metadata: candidate.metadata[key] = deepcopy(declaration.metadata[key]) @@ -1064,7 +1132,10 @@ def _validated_overload_candidate( raise ValueError("generic is only valid for class overloads; use bind on a module overload") self._validate_overload_signature(declaration, candidate, list(candidate.arguments)) if bind_target := declaration.metadata.get(BIND_TARGET_METADATA): - candidate.native_name = str(bind_target) + if candidate.origin.native_abi is not None: + candidate.origin.native_symbol = str(bind_target) + else: + candidate.native_name = str(bind_target) candidate.metadata[BIND_TARGET_METADATA] = str(bind_target) candidate.metadata[OVERLOAD_KIND_METADATA] = "generic" return candidate @@ -1085,7 +1156,10 @@ def _validated_overload_candidate( candidate.metadata[OVERLOAD_KIND_METADATA] = kind candidate.metadata[PYTHON_METHOD_NAME_METADATA] = declaration.name if bind_target := declaration.metadata.get(BIND_TARGET_METADATA): - candidate.native_name = str(bind_target) + if candidate.origin.native_abi is not None: + candidate.origin.native_symbol = str(bind_target) + else: + candidate.native_name = str(bind_target) candidate.metadata[BIND_TARGET_METADATA] = str(bind_target) if bound_position is not None: candidate.metadata[PYTHON_BOUND_POSITION_METADATA] = bound_position @@ -1533,15 +1607,15 @@ def native_shape_projection_entry( node: ast.AST, native_position: int, ) -> ProjectionMapping | None: - """Parse a ``value.shape[i]`` native projection, or return ``None`` if absent.""" + """Parse a ``value.shape[i]`` or ``value.strides[i]`` projection.""" if not isinstance(node, ast.Subscript) or not isinstance(node.value, ast.Attribute): return None attribute = node.value.attr - if attribute != "shape": + if attribute not in {"shape", "strides"}: return None return ProjectionMapping( native_position=native_position, - value_kind="shape", + value_kind="shape" if attribute == "shape" else "stride", value={ "value": self.native_value_ref(node.value.value), "dim": int(ast.literal_eval(node.slice)), @@ -1882,6 +1956,8 @@ def _array_type_from_dimensions( if dims == ["..."]: category = "assumed_rank" source_shape = [".."] + if category is None and self.native_language == "fortran" and source_shape: + category = "explicit_shape" rank = 1 if category == "assumed_rank" else len(dims) array = SemanticArrayContract( @@ -2890,14 +2966,14 @@ def _apply_argument_value_projections( raise ValueError(f"native_call argument position is out of range: {mapping.python_position}") argument = arguments[mapping.python_position] semantic_type = argument.semantic_type - if ( - semantic_type.rank != 0 - or semantic_type.name in SEMANTIC_SCALAR_TYPE_NAMES - or semantic_type.name == "String" - ): + is_c_char_value = ( + semantic_type.name == "String" + and str(semantic_type.metadata.get("fortran_character_length", "")) == "1" + ) + if semantic_type.rank != 0 or (semantic_type.name == "String" and not is_c_char_value): raise ValueError( - "Value(Arg(i)) is only valid for exact rank-zero wrapped derived objects; " - "primitive scalars already use Arg(i) value passing" + "Value(Arg(i)) is only valid for primitive scalars, String[1], " + "or exact rank-zero wrapped derived objects" ) argument.metadata[NATIVE_BY_VALUE_METADATA] = True @@ -3187,6 +3263,7 @@ def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: native_result=decorators.native_result, is_static=decorators.is_static, native_name=decorators.bind_target, + native_abi=decorators.native_abi, class_name=self.class_name, infer_passed_object=decorators.overload_target is None, has_native_call=decorators.has_native_call, @@ -3238,6 +3315,7 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: or decorators.release_gil or decorators.error_status_policy is not None or decorators.standalone + or decorators.native_abi is not None ): raise ValueError(f"Unsupported class body decorator: {ast.unparse(node.decorator_list[-1])!r}") if ( @@ -3301,6 +3379,7 @@ def _visit_ClassDef(self, node: ast.ClassDef) -> None: or decorators.release_gil or decorators.error_status_policy is not None or decorators.standalone + or decorators.native_abi is not None ): raise ValueError(f"Unsupported class decorator: {ast.unparse(node.decorator_list[-1])!r}") if ( @@ -3330,6 +3409,8 @@ def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: node, visibility=decorators.visibility, pure=decorators.pure, + native_name=decorators.bind_target, + native_abi=decorators.native_abi, ) ) return @@ -3339,6 +3420,7 @@ def _visit_FunctionDef(self, node: ast.FunctionDef) -> None: projection=decorators.projection, native_result=decorators.native_result, native_name=decorators.bind_target, + native_abi=decorators.native_abi, standalone=decorators.standalone, has_native_call=decorators.has_native_call, release_gil=decorators.release_gil, @@ -3379,7 +3461,7 @@ def _annotate_imported_external_type_refs(module: SemanticModule) -> None: to :func:`reconcile_external_type_refs`. """ imported = _imported_type_refs(module) - for semantic_type in _iter_module_semantic_types(module): + for semantic_type in _module_semantic_types(module): imported_ref = imported.get(semantic_type.name) if imported_ref is None: continue @@ -3414,7 +3496,7 @@ def _imported_type_refs(module: SemanticModule) -> dict[str, tuple[str, str, str imported[visible_name] = (module_name, visible_name, visible_name) imported_namespaces[visible_name] = module_name - for semantic_type in _iter_module_semantic_types(module): + for semantic_type in _module_semantic_types(module): if "." not in semantic_type.name: continue module_name, type_name = semantic_type.name.rsplit(".", 1) @@ -3453,6 +3535,8 @@ def _bind_prototype_reference( "callback_thread": "entering_thread", "callback_exception": "print_traceback_and_abort", "prototype_metadata": deepcopy(prototype.metadata), + "prototype_source_language": prototype.origin.source_language, + "prototype_native_abi": prototype.origin.native_abi, "native_callback_kind": "subroutine" if return_type.name == "None" else "function", PROTOTYPE_REF_METADATA: { "name": source_name, @@ -3485,7 +3569,7 @@ def reconcile_external_type_refs(modules: list[SemanticModule]) -> list[Semantic prototypes = {(module.name, prototype.name): prototype for module in modules for prototype in module.prototypes} functions = {(module.name, function.name): function for module in modules for function in module.functions} for module in modules: - for semantic_type in _iter_module_semantic_types(module): + for semantic_type in _module_semantic_types(module): ref = semantic_type.metadata.get(EXTERNAL_TYPE_REF_METADATA) if not isinstance(ref, dict): continue @@ -3542,14 +3626,17 @@ def _reconcile_declaration_expression_callables( _bind_declaration_expression_callable(reference, function, native_scope=native_scope, placement="module") -def _unresolved_declaration_expression_callables(module: SemanticModule): - """Yield imported array-expression callables that still need batch binding. +def _unresolved_declaration_expression_callables( + module: SemanticModule, +) -> tuple[SemanticExpressionCallable, ...]: + """Return imported array-expression callables that still need batch binding. The semantic module remains unmodified while traversing. References that already have a declaration or lack a native scope are intentionally omitted because their provenance is complete or explicitly unresolved. """ - for semantic_type in _iter_module_semantic_types(module): + unresolved = [] + for semantic_type in _module_semantic_types(module): storage = semantic_type.storage array = storage.array if storage is not None else None if array is None: @@ -3557,7 +3644,8 @@ def _unresolved_declaration_expression_callables(module: SemanticModule): for references in array.expression_callables: for reference in references: if reference.declaration is None and reference.native_scope is not None: - yield reference + unresolved.append(reference) + return tuple(unresolved) def _declaration_callable_scope_candidates(native_scope: str) -> tuple[str, str, str]: diff --git a/tests/c/semantics/conversion/_support.py b/tests/c/semantics/conversion/_support.py index c9faa4436..31662a31b 100644 --- a/tests/c/semantics/conversion/_support.py +++ b/tests/c/semantics/conversion/_support.py @@ -1,13 +1,14 @@ """C parser model to semantic IR conversion tests.""" -from dataclasses import asdict +from prik.semantics.models import SemanticOrigin def _function(module, name): return next(function for function in module.functions if function.name == name) -def _c_origin( +def _assert_c_origin( + origin: SemanticOrigin, *, native_name=None, native_scope=None, @@ -15,16 +16,17 @@ def _c_origin( source_type=None, source_location=None, metadata=None, -): - return { - "source_language": "c", - "native_name": native_name, - "native_scope": native_scope, - "source_kind": source_kind, - "source_type": source_type, - "source_location": source_location or {}, - "metadata": metadata or {}, - } +) -> None: + """Assert the meaningful C identity and provenance carried by an origin.""" + assert origin.source_language == "c" + assert origin.native_name == native_name + assert origin.native_abi is None + assert origin.native_symbol is None + assert origin.native_scope == native_scope + assert origin.source_kind == source_kind + assert origin.source_type == source_type + assert origin.source_location == (source_location or {}) + assert origin.metadata == (metadata or {}) def _blocker(code, message, item): @@ -36,7 +38,8 @@ def _assert_unsupported_type(semantic_type, *, code, message, owner, source_type assert semantic_type.name == "CUnsupported" assert semantic_type.dtype == "CUnsupported" assert semantic_type.metadata == {} - assert asdict(semantic_type.origin) == _c_origin( + _assert_c_origin( + semantic_type.origin, source_kind="unsupported_type", source_type=source_type, ) diff --git a/tests/c/semantics/conversion/test_functions_and_callbacks.py b/tests/c/semantics/conversion/test_functions_and_callbacks.py index 357f1e2cf..5aa978931 100644 --- a/tests/c/semantics/conversion/test_functions_and_callbacks.py +++ b/tests/c/semantics/conversion/test_functions_and_callbacks.py @@ -23,7 +23,7 @@ ) from prik.semantics.c2ir import CToIRConverter, c_file_to_semantic_modules, c_function_to_semantic_function from tests.c.semantics.conversion._support import ( - _c_origin, + _assert_c_origin, _function, ) @@ -88,13 +88,15 @@ def test_c2ir_converts_scalar_function_signatures_and_preserves_native_order(): "value": None, }, ] - assert asdict(add.arguments[0].origin) == _c_origin( + _assert_c_origin( + add.arguments[0].origin, native_name="a", native_scope="add", source_kind="parameter", source_type="int a", ) - assert asdict(add.origin) == _c_origin( + _assert_c_origin( + add.origin, native_name="add", source_kind="function", source_type="CFunctionType", @@ -105,7 +107,8 @@ def test_c2ir_converts_scalar_function_signatures_and_preserves_native_order(): "source_line": "int add(int a, int b);", }, ) - assert asdict(module.origin) == _c_origin( + _assert_c_origin( + module.origin, native_name="api.h", native_scope="api.h", source_kind="translation_unit", @@ -171,7 +174,8 @@ def test_c2ir_converts_qualifiers_callbacks_bitfields_and_unspecified_functions( assert variable.semantic_type.name == "CFunctionPointer" assert variable.semantic_type.dtype == "CFunctionPointer" assert variable.semantic_type.metadata == {"source_type": "void (*)(int)"} - assert asdict(variable.semantic_type.origin) == _c_origin( + _assert_c_origin( + variable.semantic_type.origin, source_kind="function_pointer", source_type="void (*)(int)", ) @@ -179,7 +183,8 @@ def test_c2ir_converts_qualifiers_callbacks_bitfields_and_unspecified_functions( assert field.semantic_type.metadata["c_type_fact"]["bits"] == 32 assert field.visibility == "public" assert unresolved_variable.semantic_type.name == "missing_t" - assert asdict(field.origin) == _c_origin( + _assert_c_origin( + field.origin, native_name="bits", source_kind="variable", source_type="CInt", @@ -209,7 +214,8 @@ def test_c2ir_converts_qualifiers_callbacks_bitfields_and_unspecified_functions( assert unnamed_function.projection[0].native_name == "arg0" assert variadic.metadata["prototype_style"] == "prototype" assert direct_callback.metadata == {"source_type": "void (*)(int)"} - assert asdict(direct_callback.origin) == _c_origin( + _assert_c_origin( + direct_callback.origin, source_kind="function_pointer", source_type="void (*)(int)", ) @@ -217,7 +223,8 @@ def test_c2ir_converts_qualifiers_callbacks_bitfields_and_unspecified_functions( assert void_type.name == "Any" assert void_type.dtype == "Any" assert void_type.metadata == {"c_void_pointer_pointee": True} - assert asdict(void_type.origin) == _c_origin( + _assert_c_origin( + void_type.origin, source_kind="type", source_type="CVoid", metadata={"c_type": "CVoid"}, @@ -226,7 +233,8 @@ def test_c2ir_converts_qualifiers_callbacks_bitfields_and_unspecified_functions( assert loose_struct.name == "loose" assert loose_struct.dtype == "loose" assert loose_struct.metadata == {"c_kind": "struct", "incomplete": False} - assert asdict(loose_struct.origin) == _c_origin( + _assert_c_origin( + loose_struct.origin, native_name="struct loose", source_kind="type", source_type="struct loose", diff --git a/tests/c/semantics/conversion/test_projects_and_diagnostics.py b/tests/c/semantics/conversion/test_projects_and_diagnostics.py index 5c26ffd3c..cbfeb52bf 100644 --- a/tests/c/semantics/conversion/test_projects_and_diagnostics.py +++ b/tests/c/semantics/conversion/test_projects_and_diagnostics.py @@ -1,7 +1,5 @@ """Tests split by stable ownership concept from `test_functions_and_callbacks.py`.""" -from dataclasses import asdict - import pytest from prik.pipeline.pyi import emit_module_stubs @@ -30,7 +28,7 @@ ) from prik.semantics.models import SemanticArgument, SemanticModule, SemanticOrigin, SemanticType from tests.c.semantics.conversion._support import ( - _c_origin, + _assert_c_origin, _function, ) @@ -177,7 +175,8 @@ def test_c2ir_visitor_and_project_compatibility_entrypoints_cover_supported_node "diagnostics": 0, }, } - assert asdict(merged.origin) == _c_origin( + _assert_c_origin( + merged.origin, native_name="42 api/project", native_scope="42 api/project", source_kind="project", diff --git a/tests/c/semantics/conversion/test_records_and_enums.py b/tests/c/semantics/conversion/test_records_and_enums.py index 4cf8e216b..1b514d924 100644 --- a/tests/c/semantics/conversion/test_records_and_enums.py +++ b/tests/c/semantics/conversion/test_records_and_enums.py @@ -41,7 +41,7 @@ SemanticVariable, ) from tests.c.semantics.conversion._support import ( - _c_origin, + _assert_c_origin, _function, ) @@ -249,7 +249,8 @@ def test_c2ir_converts_enum_constants_and_simple_macro_constants(): assert [asdict(constraint) for constraint in api_version.semantic_type.constraints] == [ {"name": "Constant", "arguments": []} ] - assert asdict(api_version.origin) == _c_origin( + _assert_c_origin( + api_version.origin, native_name="API_VERSION", source_kind="macro", ) @@ -262,7 +263,8 @@ def test_c2ir_converts_enum_constants_and_simple_macro_constants(): 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 asdict(status_ok.origin) == _c_origin( + _assert_c_origin( + status_ok.origin, native_name="STATUS_OK", native_scope="enum status", source_kind="enum_constant", @@ -405,7 +407,8 @@ def test_c2ir_uses_standard_type_probe_opaque_handle_facts(): opaque = module.classes[0] assert opaque.native_name == "FILE" assert opaque.metadata == {"c_kind": "opaque_standard_type"} - assert asdict(opaque.origin) == _c_origin( + _assert_c_origin( + opaque.origin, native_name="FILE", source_kind="standard_type", source_type="FILE", @@ -464,7 +467,8 @@ def test_c2ir_models_pointer_to_arrays_unknown_extents_unions_and_anonymous_alia assert union_type.name == "choice" assert union_type.dtype == "choice" assert union_type.metadata == {"c_kind": "union", "incomplete": False} - assert asdict(union_type.origin) == _c_origin( + _assert_c_origin( + union_type.origin, native_name="union choice", source_kind="type", source_type="union choice", @@ -475,7 +479,8 @@ def test_c2ir_models_pointer_to_arrays_unknown_extents_unions_and_anonymous_alia assert semantic_union.native_name == "union choice" assert [field.name for field in semantic_union.fields] == ["integer"] assert semantic_union.metadata == {"c_kind": "union", "incomplete": False} - assert asdict(semantic_union.origin) == _c_origin( + _assert_c_origin( + semantic_union.origin, native_name="union choice", source_kind="union", source_type="union choice", diff --git a/tests/c/semantics/conversion/test_types_and_constants.py b/tests/c/semantics/conversion/test_types_and_constants.py index 5a450c96a..bc13ed17f 100644 --- a/tests/c/semantics/conversion/test_types_and_constants.py +++ b/tests/c/semantics/conversion/test_types_and_constants.py @@ -52,8 +52,8 @@ c_type_to_semantic_type, ) from tests.c.semantics.conversion._support import ( + _assert_c_origin, _assert_unsupported_type, - _c_origin, _function, ) @@ -233,7 +233,8 @@ def test_c2ir_converts_integer_expression_macro_constants_when_resolvable(): assert constants["API_FORWARD_CHAIN"].semantic_type.name == "Int32" assert "API_TEXT" not in constants assert "API_CALL" not in constants - assert asdict(constants["API_LATE"].origin) == _c_origin( + _assert_c_origin( + constants["API_LATE"].origin, native_name="API_LATE", source_kind="macro", source_location={ @@ -498,7 +499,7 @@ def test_c2ir_reports_unsupported_type_and_declarator_compositions(): assert unresolved.name == "missing_t" assert unresolved.dtype == "missing_t" assert unresolved.metadata == {} - assert asdict(unresolved.origin) == _c_origin(source_kind="type", source_type="missing_t") + _assert_c_origin(unresolved.origin, source_kind="type", source_type="missing_t") _assert_unsupported_type( unsupported_integer, code="c_unsupported_type", diff --git a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/__init__.pyi b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..39218ff83 --- /dev/null +++ b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import allocatables_direct_bind_c_f90 diff --git a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/allocatables_direct_bind_c_f90.pyi b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/allocatables_direct_bind_c_f90.pyi new file mode 100644 index 000000000..aab1df615 --- /dev/null +++ b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_direct_bind_c_f90/allocatables_direct_bind_c_f90.pyi @@ -0,0 +1,16 @@ +from prik.contracts import Allocatable, Annotated, Destruction, Float64, Int32, Ownership, Pointer, PointerAssociation, Returns, Transfer, native_abi + +@native_abi("c") +def direct_optional_state( + values: Allocatable[Float64[:]] | None = ... +) -> Int32: ... + +@native_abi("c") +def direct_allocate( + values: Allocatable[Float64[:]] +) -> Returns["values", Allocatable[Float64[:]]]: ... + +@native_abi("c") +def direct_pointer_sum( + values: Annotated[Pointer[Float64[:]], PointerAssociation("runtime"), Ownership("caller"), Transfer("call_local"), Destruction("none")] +) -> Float64: ... diff --git a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/__init__.pyi b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..c8952818f --- /dev/null +++ b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import allocatables_mixed_bind_c_f90 diff --git a/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/allocatables_mixed_bind_c_f90.pyi b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/allocatables_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..115e5edcb --- /dev/null +++ b/tests/fortran/allocatables/end_to_end/fixtures/routing/contracts/allocatables_mixed_bind_c_f90/allocatables_mixed_bind_c_f90.pyi @@ -0,0 +1,10 @@ +from prik.contracts import Allocatable, Float64, Returns, native_abi + +@native_abi("c") +def direct_allocate( + values: Allocatable[Float64[:]] +) -> Returns["values", Allocatable[Float64[:]]]: ... + +def adapted_sum( + values: Allocatable[Float64[:]] +) -> Float64: ... diff --git a/tests/fortran/allocatables/end_to_end/fixtures/routing/native/allocatables_direct_bind_c_f90.f90 b/tests/fortran/allocatables/end_to_end/fixtures/routing/native/allocatables_direct_bind_c_f90.f90 new file mode 100644 index 000000000..d42ab381f --- /dev/null +++ b/tests/fortran/allocatables/end_to_end/fixtures/routing/native/allocatables_direct_bind_c_f90.f90 @@ -0,0 +1,35 @@ +module allocatables_direct_bind_c_f90 + use iso_c_binding + implicit none + +contains + integer(c_int) function direct_optional_state(values) bind(C) result(state) + real(c_double), allocatable, optional, intent(in) :: values(:) + + if (.not. present(values)) then + state = 0_c_int + else if (.not. allocated(values)) then + state = 1_c_int + else + state = 2_c_int + end if + end function direct_optional_state + + subroutine direct_allocate(values) bind(C) + real(c_double), allocatable, intent(inout) :: values(:) + + if (allocated(values)) deallocate(values) + allocate(values(3)) + values = [1.0_c_double, 2.0_c_double, 3.0_c_double] + end subroutine direct_allocate + + real(c_double) function direct_pointer_sum(values) bind(C) result(total) + real(c_double), pointer, intent(in) :: values(:) + + if (associated(values)) then + total = sum(values) + else + total = -1.0_c_double + end if + end function direct_pointer_sum +end module allocatables_direct_bind_c_f90 diff --git a/tests/fortran/allocatables/end_to_end/fixtures/routing/native/allocatables_mixed_bind_c_f90.f90 b/tests/fortran/allocatables/end_to_end/fixtures/routing/native/allocatables_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..fb7fd1b70 --- /dev/null +++ b/tests/fortran/allocatables/end_to_end/fixtures/routing/native/allocatables_mixed_bind_c_f90.f90 @@ -0,0 +1,18 @@ +module allocatables_mixed_bind_c_f90 + use iso_c_binding + implicit none +contains + subroutine direct_allocate(values) bind(C) + real(c_double), allocatable, intent(inout) :: values(:) + + if (allocated(values)) deallocate(values) + allocate(values(3)) + values = [1.0_c_double, 2.0_c_double, 3.0_c_double] + end subroutine direct_allocate + + real(c_double) function adapted_sum(values) result(total) + real(c_double), allocatable, intent(in) :: values(:) + + total = merge(sum(values), -1.0_c_double, allocated(values)) + end function adapted_sum +end module allocatables_mixed_bind_c_f90 diff --git a/tests/fortran/allocatables/end_to_end/test_descriptor_direct_entrypoint_routing.py b/tests/fortran/allocatables/end_to_end/test_descriptor_direct_entrypoint_routing.py new file mode 100644 index 000000000..d8711cdef --- /dev/null +++ b/tests/fortran/allocatables/end_to_end/test_descriptor_direct_entrypoint_routing.py @@ -0,0 +1,116 @@ +"""Compiled standard-descriptor direct and mixed entrypoint evidence.""" + +from pathlib import Path + +import numpy as np +import pytest + +from prik.contracts import Allocatable, Float64, Pointer +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_descriptors_all_direct_route_preserves_three_states_and_handle_mutation( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "allocatables_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "allocatables_direct_bind_c_f90_wrapper.c", + "allocatables_direct_bind_c_f90_wrapper.h", + "bind_c_allocatables_direct_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "allocatables_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + values = Allocatable[Float64[:]]() + assert module.direct_optional_state() == np.int32(0) + assert module.direct_optional_state(None) == np.int32(0) + assert module.direct_optional_state(values) == np.int32(1) + assert module.direct_allocate(values) is values + np.testing.assert_array_equal(values.to_numpy(), np.array([1.0, 2.0, 3.0], dtype=np.float64)) + assert module.direct_optional_state(values) == np.int32(2) + + pointer = Pointer[Float64[:]]() + assert pointer.associated is False + assert module.direct_pointer_sum(pointer) == np.float64(-1.0) + + if pyi_parity_build_mode == "source": + binding = (tmp_path / "source_build" / "allocatables_direct_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + assert "int32_t direct_optional_state(CFI_cdesc_t * values);" in binding + assert "void direct_allocate(CFI_cdesc_t * values);" in binding + support = ( + (tmp_path / "source_build" / "bind_c_allocatables_direct_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_owned" in support + assert "direct_optional_state" not in support + assert "direct_allocate" not in support + assert "direct_pointer_sum" not in support + + pointer.close() + values.close() + + +def test_descriptors_mixed_route_matches_edited_source_free_contract(tmp_path: Path): + stem = "allocatables_mixed_bind_c_f90" + source = (FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8") + contract = (FIXTURES / "contracts" / stem / f"{stem}.pyi").read_text(encoding="utf-8") + contract = contract.replace("from prik.contracts import ", "from prik.contracts import nogil, ") + contract = contract.replace("def direct_allocate(", "@nogil\ndef direct_allocate(").replace( + "def adapted_sum(", "@nogil\ndef adapted_sum(" + ) + module, result = _build_inline_pyi_contract_module( + tmp_path, module_name=stem, source_text=source, contract_text=contract + ) + + values = Allocatable[Float64[:]]() + assert module.direct_allocate(values) is values + assert module.adapted_sum(values) == np.float64(6.0) + bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_adapted_sum" in bridge + assert "subroutine bind_c_direct_allocate" not in bridge + values.close() + + +def test_descriptors_mixed_route_adapts_only_ordinary_descriptor_operation( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "allocatables_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "allocatables_mixed_bind_c_f90_wrapper.c", + "allocatables_mixed_bind_c_f90_wrapper.h", + "bind_c_allocatables_mixed_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "allocatables_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + + values = Allocatable[Float64[:]]() + assert module.direct_allocate(values) is values + assert module.adapted_sum(values) == np.float64(6.0) + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_allocatables_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_adapted_sum" in bridge + assert "direct_allocate" not in bridge + + values.close() diff --git a/tests/fortran/arrays/codegen/test_array_buffer_lowering.py b/tests/fortran/arrays/codegen/test_array_buffer_lowering.py index e7461fd8f..03ed1f73e 100644 --- a/tests/fortran/arrays/codegen/test_array_buffer_lowering.py +++ b/tests/fortran/arrays/codegen/test_array_buffer_lowering.py @@ -49,19 +49,19 @@ def test_required_array_buffer_has_one_printable_editable_handoff_plan(): assert argument.binding.python_action is PythonBarrierAction.ARRAY_STORAGE assert argument.binding.codegen_action is CodegenAction.IN_PLACE_ARGUMENT assert argument.bridge.native_action is NativeBarrierAction.PASS_ARRAY_BUFFER - assert argument.bridge.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER + assert argument.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER assert argument.bridge.data_action is BridgeDataAction.ASSOCIATE_VIEW assert isinstance(argument.array, ArrayHandoffPlan) - assert argument.array is argument.native_call_slot.array - assert argument.native_call_slot.object_kind is ObjectKind.NUMPY_ARRAY + assert argument.array is argument.projected_call_slot.array + assert argument.projected_call_slot.object_kind is ObjectKind.NUMPY_ARRAY assert argument.array.rank == 1 assert argument.array.shape == (":",) assert argument.array.axes == ("dense",) assert argument.array.contiguous is True assert argument.array.flatten_python_storage is False assert argument.array.flat_axis is None - assert argument.array.data_role == argument.binding.handoff_role + assert argument.array.data_role == argument.entrypoint.handoff_role assert argument.array.extent_roles == (f"{argument.owner_path}:extent:0",) assert argument.array.upper_bound_roles == () assert argument.array.stride_roles == () diff --git a/tests/fortran/arrays/codegen/test_array_result_lowering.py b/tests/fortran/arrays/codegen/test_array_result_lowering.py index e94e112ee..ca852a2e3 100644 --- a/tests/fortran/arrays/codegen/test_array_result_lowering.py +++ b/tests/fortran/arrays/codegen/test_array_result_lowering.py @@ -59,12 +59,12 @@ def test_array_results_record_producer_shape_copy_ownership_and_shared_hidden_sl assert direct.source_kind == "direct_return" assert direct.binding.codegen_action is CodegenAction.COPY_OUT assert direct.bridge.native_action is NativeBarrierAction.NONE - assert direct.native_call_slot is None + assert direct.projected_call_slot is None assert hidden.source_kind == "hidden_output" assert hidden.binding.codegen_action is CodegenAction.COPY_OUT assert hidden.bridge.native_action is NativeBarrierAction.PASS_ARRAY_BUFFER - assert hidden.array is hidden.native_call_slot.array - assert hidden.native_call_slot.object_kind is ObjectKind.NUMPY_ARRAY + assert hidden.array is hidden.projected_call_slot.array + assert hidden.projected_call_slot.object_kind is ObjectKind.NUMPY_ARRAY def test_array_result_lowering_transfers_bridge_copy_to_capsule_owned_numpy_storage(): @@ -142,7 +142,7 @@ def test_array_result_plan_edits_fail_before_backend_lowering(edit: str, diagnos elif edit == "copy": direct.bridge.copy_reason = "edited" else: - hidden.native_call_slot.array = direct.array + hidden.projected_call_slot.array = direct.array with pytest.raises(ValueError, match=diagnostic): WrapperGenerator().generate(plan) diff --git a/tests/fortran/arrays/codegen/test_specialized_array_roles.py b/tests/fortran/arrays/codegen/test_specialized_array_roles.py index 9db98db77..6c61a7a9c 100644 --- a/tests/fortran/arrays/codegen/test_specialized_array_roles.py +++ b/tests/fortran/arrays/codegen/test_specialized_array_roles.py @@ -47,7 +47,7 @@ def test_optional_assumed_rank_and_character_arrays_have_explicit_distinct_roles character = functions["labels"].arguments[0].array assert optional.binding.optional_mode is OptionalMode.NULLABLE_VALUE - assert optional.bridge.optional_mode is OptionalMode.NULLABLE_VALUE + assert optional.entrypoint.optional_mode is OptionalMode.NULLABLE_VALUE assert assumed is not None assert assumed.rank is None assert assumed.contiguous is True diff --git a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..d6b33aa84 --- /dev/null +++ b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import arrays_direct_bind_c_f90 diff --git a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/arrays_direct_bind_c_f90.pyi b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/arrays_direct_bind_c_f90.pyi new file mode 100644 index 000000000..bb5fefe55 --- /dev/null +++ b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_direct_bind_c_f90/arrays_direct_bind_c_f90.pyi @@ -0,0 +1,32 @@ +from prik.contracts import Bool8, Float64, Int32, native_abi + +@native_abi("c") +def sum_values( + n: Int32, + values: Float64[n] +) -> Float64: ... + +@native_abi("c") +def scale_values( + n: Int32, + values: Float64[n] +) -> None: ... + +@native_abi("c") +def all_flags( + n: Int32, + values: Bool8[n] +) -> Bool8: ... + +@native_abi("c") +def invert_flags( + n: Int32, + values: Bool8[n] +) -> None: ... + +@native_abi("c") +def scale_matrix( + rows: Int32, + columns: Int32, + values: Float64[rows, columns] +) -> None: ... diff --git a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/__init__.pyi b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..b00ea2ba4 --- /dev/null +++ b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import arrays_mixed_bind_c_f90 diff --git a/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/arrays_mixed_bind_c_f90.pyi b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/arrays_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..08121f3e2 --- /dev/null +++ b/tests/fortran/arrays/end_to_end/fixtures/routing/contracts/arrays_mixed_bind_c_f90/arrays_mixed_bind_c_f90.pyi @@ -0,0 +1,13 @@ +from prik.contracts import Addr, Arg, Float64, Int32, native_abi, native_call + +@native_abi("c") +def direct_sum( + n: Int32, + values: Float64[n] +) -> Float64: ... + +@native_call([Addr(Arg(0)), Arg(1)]) +def adapted_sum( + n: Int32, + values: Float64[n] +) -> Float64: ... diff --git a/tests/fortran/arrays/end_to_end/fixtures/routing/native/arrays_direct_bind_c_f90.f90 b/tests/fortran/arrays/end_to_end/fixtures/routing/native/arrays_direct_bind_c_f90.f90 new file mode 100644 index 000000000..97c6dd46a --- /dev/null +++ b/tests/fortran/arrays/end_to_end/fixtures/routing/native/arrays_direct_bind_c_f90.f90 @@ -0,0 +1,35 @@ +module arrays_direct_bind_c_f90 + use iso_c_binding +contains + real(c_double) function sum_values(n, values) bind(C) result(output) + integer(c_int), value, intent(in) :: n + real(c_double), intent(in) :: values(n) + output = sum(values) + end function sum_values + + subroutine scale_values(n, values) bind(C) + integer(c_int), value, intent(in) :: n + real(c_double), intent(inout) :: values(n) + values = 2.0_c_double * values + end subroutine scale_values + + logical(c_bool) function all_flags(n, values) bind(C) result(output) + integer(c_int), value, intent(in) :: n + logical(c_bool), intent(in) :: values(n) + output = all(values) + end function all_flags + + subroutine invert_flags(n, values) bind(C) + integer(c_int), value, intent(in) :: n + logical(c_bool), intent(inout) :: values(n) + values = .not. values + end subroutine invert_flags + + subroutine scale_matrix(rows, columns, values) bind(C) + integer(c_int), value, intent(in) :: rows + integer(c_int), value, intent(in) :: columns + real(c_double), intent(inout) :: values(rows, columns) + + values = 3.0_c_double * values + end subroutine scale_matrix +end module arrays_direct_bind_c_f90 diff --git a/tests/fortran/arrays/end_to_end/fixtures/routing/native/arrays_mixed_bind_c_f90.f90 b/tests/fortran/arrays/end_to_end/fixtures/routing/native/arrays_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..9b31c358f --- /dev/null +++ b/tests/fortran/arrays/end_to_end/fixtures/routing/native/arrays_mixed_bind_c_f90.f90 @@ -0,0 +1,15 @@ +module arrays_mixed_bind_c_f90 + use iso_c_binding +contains + real(c_double) function direct_sum(n, values) bind(C) result(output) + integer(c_int), value, intent(in) :: n + real(c_double), intent(in) :: values(n) + output = sum(values) + end function direct_sum + + real(8) function adapted_sum(n, values) result(output) + integer, intent(in) :: n + real(8), intent(in) :: values(n) + output = sum(values) + end function adapted_sum +end module arrays_mixed_bind_c_f90 diff --git a/tests/fortran/arrays/end_to_end/test_array_direct_entrypoint_routing.py b/tests/fortran/arrays/end_to_end/test_array_direct_entrypoint_routing.py new file mode 100644 index 000000000..094963b9b --- /dev/null +++ b/tests/fortran/arrays/end_to_end/test_array_direct_entrypoint_routing.py @@ -0,0 +1,131 @@ +"""Compiled numeric and C-Boolean array direct-route evidence.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_arrays_all_direct_route_preserves_dtype_values_and_mutation( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "arrays_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + {"arrays_direct_bind_c_f90_wrapper.c", "arrays_direct_bind_c_f90_wrapper.h"}, + FIXTURES / "contracts" / "arrays_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + values = np.array([1.0, 2.0, 3.0], dtype=np.float64, order="F") + assert module.sum_values(np.int32(values.size), values) == np.float64(6.0) + scaled = module.scale_values(np.int32(values.size), values) + np.testing.assert_array_equal(values, np.array([2.0, 4.0, 6.0], dtype=np.float64)) + if scaled is not None: + np.testing.assert_array_equal(scaled, values) + + flags = np.array([True, True, False], dtype=np.bool_, order="F") + assert bool(module.all_flags(np.int32(flags.size), flags)) is False + inverted = module.invert_flags(np.int32(flags.size), flags) + np.testing.assert_array_equal(flags, np.array([False, False, True], dtype=np.bool_)) + if inverted is not None: + np.testing.assert_array_equal(inverted, flags) + + empty = np.empty(0, dtype=np.float64) + assert module.sum_values(np.int32(0), empty) == np.float64(0.0) + + matrix = np.ones((2, 3), dtype=np.float64, order="F") + matrix_result = module.scale_matrix(np.int32(2), np.int32(3), matrix) + np.testing.assert_array_equal(matrix, np.full((2, 3), 3.0, dtype=np.float64, order="F")) + if matrix_result is not None: + np.testing.assert_array_equal(matrix_result, matrix) + + with pytest.raises(TypeError, match="dtype"): + module.sum_values(np.int32(3), np.ones(3, dtype=np.float32)) + with pytest.raises(TypeError): + module.sum_values(np.int32(3), np.ones((3, 1), dtype=np.float64, order="F")) + with pytest.raises(TypeError, match="incompatible shape"): + module.sum_values(np.int32(4), np.ones(3, dtype=np.float64)) + with pytest.raises(TypeError, match=r"expected ordering \(F\)"): + module.scale_matrix( + np.int32(2), + np.int32(3), + np.ones((2, 3), dtype=np.float64, order="C"), + ) + + read_only = np.ones(3, dtype=np.float64) + read_only.flags.writeable = False + with pytest.raises(TypeError, match="writeable"): + module.scale_values(np.int32(3), read_only) + + backing = np.zeros(3 * np.dtype(np.float64).itemsize + 1, dtype=np.uint8) + unaligned = np.ndarray((3,), dtype=np.float64, buffer=backing, offset=1) + assert not unaligned.flags.aligned + with pytest.raises(TypeError, match="aligned"): + module.sum_values(np.int32(3), unaligned) + + if pyi_parity_build_mode == "source": + binding = (tmp_path / "source_build" / "arrays_direct_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + assert "double sum_values(int32_t n, double * values);" in binding + assert "void invert_flags(int32_t n, bool * values);" in binding + assert "void scale_matrix(int32_t rows, int32_t columns, double * values);" in binding + + +def test_arrays_mixed_route_keeps_only_ordinary_array_adapter( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "arrays_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "arrays_mixed_bind_c_f90_wrapper.c", + "arrays_mixed_bind_c_f90_wrapper.h", + "bind_c_arrays_mixed_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "arrays_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + values = np.array([1.0, 2.0, 3.0], dtype=np.float64, order="F") + assert module.direct_sum(np.int32(3), values) == np.float64(6.0) + assert module.adapted_sum(np.int32(3), values) == np.float64(6.0) + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_arrays_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_adapted_sum" in bridge + assert "direct_sum" not in bridge + + +def test_arrays_mixed_route_matches_edited_source_free_contract(tmp_path: Path): + stem = "arrays_mixed_bind_c_f90" + source = (FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8") + contract = (FIXTURES / "contracts" / stem / f"{stem}.pyi").read_text(encoding="utf-8") + contract = contract.replace("from prik.contracts import ", "from prik.contracts import nogil, ") + contract = contract.replace("def direct_sum(", "@nogil\ndef direct_sum(").replace( + "def adapted_sum(", "@nogil\ndef adapted_sum(" + ) + module, result = _build_inline_pyi_contract_module( + tmp_path, module_name=stem, source_text=source, contract_text=contract + ) + + values = np.array([1.0, 2.0, 3.0], dtype=np.float64, order="F") + assert module.direct_sum(np.int32(3), values) == np.float64(6.0) + assert module.adapted_sum(np.int32(3), values) == np.float64(6.0) + bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_adapted_sum" in bridge + assert "function bind_c_direct_sum" not in bridge diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_direct_bind_c_f90.f90 b/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_direct_bind_c_f90.f90 new file mode 100644 index 000000000..8d6498122 --- /dev/null +++ b/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_direct_bind_c_f90.f90 @@ -0,0 +1,10 @@ +module multi_source_direct_bind_c_f90 + use iso_c_binding + use multi_source_direct_helper_f90, only: helper_double +contains + integer(c_int) function direct_combined(value) bind(C) result(output) + integer(c_int), value, intent(in) :: value + + output = helper_double(value) + 1_c_int + end function direct_combined +end module multi_source_direct_bind_c_f90 diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_direct_helper_f90.f90 b/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_direct_helper_f90.f90 new file mode 100644 index 000000000..756c6d2fe --- /dev/null +++ b/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_direct_helper_f90.f90 @@ -0,0 +1,9 @@ +module multi_source_direct_helper_f90 + use iso_c_binding +contains + integer(c_int) function helper_double(value) bind(C) result(output) + integer(c_int), value, intent(in) :: value + + output = 2_c_int * value + end function helper_double +end module multi_source_direct_helper_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/building_shared_library/end_to_end/fixtures/routing/native/multi_source_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..4f3f04e15 --- /dev/null +++ b/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_mixed_bind_c_f90.f90 @@ -0,0 +1,10 @@ +module multi_source_mixed_bind_c_f90 + use iso_c_binding + use multi_source_mixed_helper_f90, only: helper_double +contains + integer(c_int) function adapted_combined(value) result(output) + integer(c_int), intent(in) :: value + + output = helper_double(value) + 1_c_int + end function adapted_combined +end module multi_source_mixed_bind_c_f90 diff --git a/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_mixed_helper_f90.f90 b/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_mixed_helper_f90.f90 new file mode 100644 index 000000000..4ab050d89 --- /dev/null +++ b/tests/fortran/building_shared_library/end_to_end/fixtures/routing/native/multi_source_mixed_helper_f90.f90 @@ -0,0 +1,9 @@ +module multi_source_mixed_helper_f90 + use iso_c_binding +contains + integer(c_int) function helper_double(value) bind(C) result(output) + integer(c_int), value, intent(in) :: value + + output = 2_c_int * value + end function helper_double +end module multi_source_mixed_helper_f90 diff --git a/tests/fortran/building_shared_library/end_to_end/test_build_direct_entrypoint_routing.py b/tests/fortran/building_shared_library/end_to_end/test_build_direct_entrypoint_routing.py new file mode 100644 index 000000000..ffc1820e8 --- /dev/null +++ b/tests/fortran/building_shared_library/end_to_end/test_build_direct_entrypoint_routing.py @@ -0,0 +1,56 @@ +"""Compiled all-direct and mixed multi-source entrypoint evidence.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_sources_and_import + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" / "native" +pytestmark = pytest.mark.fortran_end_to_end + + +def _source(name: str) -> str: + return (FIXTURES / name).read_text(encoding="utf-8") + + +def test_multi_source_all_direct_route_preserves_dependency_order_and_has_no_adapter(tmp_path: Path): + module, payload = _build_sources_and_import( + [ + ("multi_source_direct_bind_c_f90.f90", _source("multi_source_direct_bind_c_f90.f90")), + ("multi_source_direct_helper_f90.f90", _source("multi_source_direct_helper_f90.f90")), + ], + tmp_path, + ) + + assert module.multi_source_direct_helper_f90.helper_double(np.int32(4)) == np.int32(8) + assert module.multi_source_direct_bind_c_f90.direct_combined(np.int32(4)) == np.int32(9) + assert {Path(path).name for path in payload["generated_sources"]} == { + "multi_source_direct_bind_c_f90_wrapper.c", + "multi_source_direct_bind_c_f90_wrapper.h", + } + assert [Path(item["source"]).name for item in payload["native_build_plan"]["compilation_units"]] == [ + "multi_source_direct_bind_c_f90.f90", + "multi_source_direct_helper_f90.f90", + ] + assert [Path(item["path"]).name for item in payload["native_build_plan"]["link_items"][:2]] == [ + "multi_source_direct_bind_c_f90.o", + "multi_source_direct_helper_f90.o", + ] + + +def test_multi_source_mixed_route_adapts_only_ordinary_dependent_operation(tmp_path: Path): + module, _payload = _build_sources_and_import( + [ + ("multi_source_mixed_bind_c_f90.f90", _source("multi_source_mixed_bind_c_f90.f90")), + ("multi_source_mixed_helper_f90.f90", _source("multi_source_mixed_helper_f90.f90")), + ], + tmp_path, + ) + + assert module.multi_source_mixed_helper_f90.helper_double(np.int32(4)) == np.int32(8) + assert module.multi_source_mixed_bind_c_f90.adapted_combined(np.int32(4)) == np.int32(9) + bridge = (tmp_path / "bind_c_multi_source_mixed_bind_c_f90_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_adapted_combined" in bridge + assert "helper_double" not in bridge diff --git a/tests/fortran/building_shared_library/end_to_end/test_native_bundles.py b/tests/fortran/building_shared_library/end_to_end/test_native_bundles.py index 04c7e11d9..ea8767ef0 100644 --- a/tests/fortran/building_shared_library/end_to_end/test_native_bundles.py +++ b/tests/fortran/building_shared_library/end_to_end/test_native_bundles.py @@ -126,6 +126,16 @@ def _simple_external_source(name: str, expression: str) -> str: """ +def _direct_external_source(name: str, expression: str) -> str: + return f"""\ +integer(c_int) function {name}(value) bind(C) result(out) + use iso_c_binding + integer(c_int), value, intent(in) :: value + out = {expression} +end function {name} +""" + + @pytest.mark.skipif(sys.platform == "win32", reason="shared-library loader behavior differs on Windows") @pytest.mark.parametrize("artifact_kind", ["archive", "shared_library"]) def test_imported_contracts_resolve_from_one_archive_or_shared_library( @@ -158,6 +168,50 @@ def test_imported_contracts_resolve_from_one_archive_or_shared_library( _assert_combined_runtime(module) +@pytest.mark.skipif(sys.platform == "win32", reason="shared-library loader behavior differs on Windows") +@pytest.mark.parametrize("artifact_kind", ["archive", "shared_library"]) +def test_source_free_direct_entrypoint_resolves_from_external_fortran_library( + tmp_path: Path, + artifact_kind: str, +): + source = _write_source( + tmp_path / "sources", + "external_direct.f90", + _direct_external_source("external_direct", "value + 9_c_int"), + ) + native_object = _compile_source(source, tmp_path / "native" / "objects") + artifact = ( + _archive(tmp_path / "native" / "libexternal_direct.a", (native_object,)) + if artifact_kind == "archive" + else _shared_library(tmp_path / "native" / "libexternal_direct.so", (native_object,)) + ) + entry = _write_contract_package( + tmp_path / "contracts" / "external_direct", + entry=( + "from prik.contracts import Int32, native_abi, standalone\n\n" + '@native_abi("c")\n' + "@standalone\n" + "def external_direct(value: Int32) -> Int32: ...\n" + ), + ) + + result = build_pyi_extension( + entry, + native_objects=[artifact], + output_name="external_direct", + output_dir=tmp_path / "build", + ) + module = _import_from_build(result) + + assert module.external_direct(np.int32(4)) == np.int32(13) + assert {path.name for path in result.generated_sources} == { + "external_direct_wrapper.c", + "external_direct_wrapper.h", + } + assert result.native_generated_code_groups == () + assert result.native_build_plan.to_dict()["link_items"] == [{"kind": artifact_kind, "path": str(artifact)}] + + @pytest.mark.skipif(sys.platform == "win32", reason="shared-library loader behavior differs on Windows") def test_mixed_module_external_bundle_resolves_all_native_input_kinds(tmp_path: Path): sources = tmp_path / "sources" diff --git a/tests/fortran/building_shared_library/pipeline/test_generated_wrapper_build.py b/tests/fortran/building_shared_library/pipeline/test_generated_wrapper_build.py index 50c2060f8..4871249d1 100644 --- a/tests/fortran/building_shared_library/pipeline/test_generated_wrapper_build.py +++ b/tests/fortran/building_shared_library/pipeline/test_generated_wrapper_build.py @@ -21,7 +21,7 @@ ) from prik.policy.completion import complete_semantic_policies from prik.utilities.stage_values import FrozenStageRecordError -from prik.planning import ModulePlan, WrapperPlanner +from prik.planning import ModulePlan, NativeGeneratedCodeGroupKind, WrapperPlanner class RecordingCompiler: @@ -215,3 +215,76 @@ def test_build_generated_wrapper_extension_rejects_unknown_native_support_key(tm ) with pytest.raises(FrozenStageRecordError): rendered.extension_init_name = "PyInit_later" + + +def test_all_direct_fortran_build_compiles_no_bridge_and_retains_fortran_link_driver( + tmp_path: Path, + capsys, +): + rendered = _generated_wrapper( + """ +@native_abi("c") +@bind("native_scale") +def scale(value: Float64) -> Float64: ... +""", + module_name="all_direct_build", + ) + native_source = tmp_path / "native" / "scale.f90" + native_source.parent.mkdir() + native_source.write_text("native source placeholder\n", encoding="utf-8") + native_object = ObjectFile( + source=native_source, + object_path=native_source.with_suffix(".o"), + language="fortran", + ) + native_object.object_path.write_text("native object\n", encoding="utf-8") + compiler = RecordingCompiler() + + result = _build_generated_wrapper_extension( + rendered, + output_dir=tmp_path / "build", + native_dependencies=(native_object,), + compiler=compiler, + verbose=True, + ) + + assert rendered.bridge_sources == () + assert rendered.native_generated_code_groups == () + assert result.native_generated_code_groups == () + assert [item.language for item, _verbose in compiler.compiled] == ["c"] + assert compiler.linked[2] == "fortran" + assert [item.language for item in compiler.linked[3]] == ["fortran", "c"] + assert not (result.output_dir / "bind_c_all_direct_build_wrapper.f90").exists() + assert not (result.output_dir / "bind_c_all_direct_build_wrapper.o").exists() + output = capsys.readouterr().out + assert "Write bridge source" not in output + assert "Compile bridge source" not in output + + +def test_support_only_fortran_build_compiles_the_planned_support_group(tmp_path: Path): + rendered = _generated_wrapper( + """ +counter: Int32 + +@native_abi("c") +def direct(value: Int32) -> Int32: ... +""", + module_name="support_only_build", + ) + compiler = RecordingCompiler() + + result = _build_generated_wrapper_extension( + rendered, + output_dir=tmp_path / "build", + compiler=compiler, + ) + + assert [group.kind for group in rendered.native_generated_code_groups] == [ + NativeGeneratedCodeGroupKind.FORTRAN_SUPPORT + ] + assert result.native_generated_code_groups == rendered.native_generated_code_groups + assert {item.language for item, _verbose in compiler.compiled} == {"fortran", "c"} + assert [item.language for item in compiler.linked[3]] == ["fortran", "c"] + support = (result.output_dir / "bind_c_support_only_build_wrapper.f90").read_text(encoding="utf-8") + assert "bind_c_get_counter" in support + assert "bind_c_direct" not in support diff --git a/tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py b/tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py index ab357bec1..3efe3f7d8 100644 --- a/tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py +++ b/tests/fortran/building_shared_library/pipeline/test_pyi_build_modes.py @@ -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"] == 2 + assert manifest["schema_version"] == 3 assert manifest["build_kind"] == "pyi-wrapper" assert manifest["compiler"]["input_executable"] == str(selected_compiler) assert manifest["compiler"]["fortran_flags"] == ["-O2", "-g0"] @@ -229,6 +229,10 @@ def test_pyi_makefile_manifest_and_replay_workflows(tmp_path: Path): "headers": [], "items": [], } + assert manifest["generated_wrapper"]["native_code_groups"][0]["kind"] == "fortran_adapters" + assert any( + path.endswith("bind_c_fruntime_abi_f90_wrapper.f90") for path in manifest["generated_wrapper"]["sources"] + ) assert [item["kind"] for item in manifest["native_build_plan"]["link_items"]] == ["object"] assert manifest["native_build_plan"]["compilation_units"][0]["source"].endswith(native_source.name) manifest_includes = manifest["native_build_plan"]["compilation_units"][0]["include_dirs"] @@ -292,6 +296,37 @@ def test_pyi_makefile_manifest_and_replay_workflows(tmp_path: Path): _assert_scale_runtime_contract(replayed_module) +def test_all_direct_makefile_and_manifest_record_zero_generated_native_groups(tmp_path: Path): + contract = tmp_path / "direct_manifest.pyi" + contract.write_text( + """from prik.contracts import Int32, native_abi + +@native_abi("c") +def direct(value: Int32) -> Int32: ... +""", + encoding="utf-8", + ) + native_source = tmp_path / "direct_manifest.f90" + native_source.write_text("native implementation placeholder\n", encoding="utf-8") + + result = build_pyi_extension( + contract, + native_fortran_sources=[native_source], + output_dir=tmp_path / "build", + makefile=True, + ) + + manifest = json.loads(result.build_manifest.read_text(encoding="utf-8")) + generated = manifest["generated_wrapper"] + makefile = result.build_makefile.read_text(encoding="utf-8") + + assert result.native_generated_code_groups == () + assert generated["native_code_groups"] == [] + assert {Path(path).suffix for path in generated["sources"]} == {".c", ".h"} + assert "bind_c_direct_manifest_wrapper.f90" not in makefile + assert "direct_manifest.f90" in makefile + + def test_pyi_cli_accepts_exactly_one_entry_contract(tmp_path: Path): other = tmp_path / "other.pyi" other.write_text("", encoding="utf-8") diff --git a/tests/fortran/callbacks/codegen/test_callback_planning.py b/tests/fortran/callbacks/codegen/test_callback_planning.py index d22135534..527aa537d 100644 --- a/tests/fortran/callbacks/codegen/test_callback_planning.py +++ b/tests/fortran/callbacks/codegen/test_callback_planning.py @@ -17,7 +17,7 @@ CallbackTransferAction, ) from prik.pipeline.wrapper import WrapperGenerator -from prik.planning import WrapperPlanner +from prik.planning import GeneratedSupportProcedureImplementationOwner, WrapperPlanner from prik.planning.models import DatatypeFamily CONTRACT_ROOT = Path(__file__).parents[1] / "end_to_end" / "fixtures" / "contracts" @@ -117,9 +117,23 @@ def test_callback_plan_projects_one_explicit_site_and_stable_roles_per_argument( "apply_point_callback", ) ) - assert len({callback.context_current_symbol for callback in callbacks}) == len(callbacks) - assert len({callback.adapter_symbol for callback in callbacks}) == len(callbacks) - assert len({callback.trampoline_symbol for callback in callbacks}) == len(callbacks) + assert len({callback.binding.context_current_symbol for callback in callbacks}) == len(callbacks) + assert len({callback.bridge.adapter_symbol for callback in callbacks}) == len(callbacks) + assert len({callback.entrypoint.support_procedure.symbol_name for callback in callbacks}) == len(callbacks) + assert all( + callback.entrypoint.support_procedure.implementation_owner + is GeneratedSupportProcedureImplementationOwner.BINDING + for callback in callbacks + ) + assert all( + next( + procedure + for procedure in plan.entrypoint.support_procedures + if procedure.key == callback.entrypoint.support_procedure.key + ) + is callback.entrypoint.support_procedure + for callback in callbacks + ) @pytest.mark.parametrize( @@ -129,6 +143,7 @@ def test_callback_plan_projects_one_explicit_site_and_stable_roles_per_argument( ("array_roles", "incomplete-callback-array-roles"), ("scalar_projection", "inconsistent-callback-scalar-value-projection"), ("result", "callback-void-has-transfer"), + ("entrypoint_parameter", "inconsistent-callback-entrypoint-parameter"), ("symbols", "invalid-callback-symbols"), ), ) @@ -146,9 +161,12 @@ def test_callback_plan_edits_fail_central_validation_before_backend_emission(edi elif edit == "result": callback = _callback_argument(plan, "apply_value_callback").callback callback.result.action = CallbackResultAction.RETURN_VOID + elif edit == "entrypoint_parameter": + argument = _callback_argument(plan, "apply_value_callback") + argument.entrypoint.pass_callback_parameter = True else: callback = _callback_argument(plan, "apply_value_callback").callback - callback.trampoline_symbol = callback.adapter_symbol + callback.entrypoint.support_procedure.symbol_name = callback.bridge.adapter_symbol with pytest.raises(ValueError, match=diagnostic): WrapperGenerator().generate(plan) @@ -213,8 +231,8 @@ def test_every_callback_uses_the_shared_generated_abstract_prototype(): assert transform.prototype.interface_symbol.startswith("prik_transform_callback_") _, bridge = _sources(plan) - assert f"procedure({reduce.prototype.interface_symbol}) :: {reduce.adapter_symbol}" in bridge - assert f"procedure({transform.prototype.interface_symbol}) :: {transform.adapter_symbol}" in bridge + assert f"procedure({reduce.prototype.interface_symbol}) :: {reduce.bridge.adapter_symbol}" in bridge + assert f"procedure({transform.prototype.interface_symbol}) :: {transform.bridge.adapter_symbol}" in bridge assert "abstract interface" in bridge assert "=> transform_callback" not in bridge diff --git a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/__init__.pyi b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..e0575265b --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import callbacks_direct_bind_c_f90 diff --git a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/callbacks_direct_bind_c_f90.pyi b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/callbacks_direct_bind_c_f90.pyi new file mode 100644 index 000000000..7de4f6d52 --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_direct_bind_c_f90/callbacks_direct_bind_c_f90.pyi @@ -0,0 +1,25 @@ +from prik.contracts import Float64, In, Int32, native_abi, prototype + +@native_abi("c") +@prototype +def direct_callback( + value: In(Float64) +) -> Float64: ... + +@native_abi("c") +@prototype +def direct_notify( + value: In(Int32) +) -> None: ... + +@native_abi("c") +def direct_apply( + callback: direct_callback, + value: Float64 +) -> Float64: ... + +@native_abi("c") +def direct_call_notify( + callback: direct_notify, + value: Int32 +) -> None: ... diff --git a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/__init__.pyi b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..b715fb96e --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import callbacks_mixed_bind_c_f90 diff --git a/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/callbacks_mixed_bind_c_f90.pyi b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/callbacks_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..cf191fe05 --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/fixtures/routing/contracts/callbacks_mixed_bind_c_f90/callbacks_mixed_bind_c_f90.pyi @@ -0,0 +1,24 @@ +from prik.contracts import Addr, Arg, Float64, In, native_abi, native_call, prototype + +@native_abi("c") +@prototype +def direct_callback( + value: In(Float64) +) -> Float64: ... + +@prototype +def adapted_callback( + value: In(Addr(Float64)) +) -> Float64: ... + +@native_abi("c") +def direct_apply( + callback: direct_callback, + value: Float64 +) -> Float64: ... + +@native_call([Arg(0), Addr(Arg(1))]) +def adapted_apply( + callback: adapted_callback, + value: Float64 +) -> Float64: ... diff --git a/tests/fortran/callbacks/end_to_end/fixtures/routing/native/callbacks_direct_bind_c_f90.f90 b/tests/fortran/callbacks/end_to_end/fixtures/routing/native/callbacks_direct_bind_c_f90.f90 new file mode 100644 index 000000000..d177941c9 --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/fixtures/routing/native/callbacks_direct_bind_c_f90.f90 @@ -0,0 +1,30 @@ +module callbacks_direct_bind_c_f90 + use iso_c_binding + implicit none + + abstract interface + real(c_double) function direct_callback(value) bind(C) result(output) + import c_double + real(c_double), value, intent(in) :: value + end function direct_callback + + subroutine direct_notify(value) bind(C) + import c_int + integer(c_int), value, intent(in) :: value + end subroutine direct_notify + end interface +contains + real(c_double) function direct_apply(callback, value) bind(C) result(output) + procedure(direct_callback) :: callback + real(c_double), value, intent(in) :: value + + output = callback(value) + end function direct_apply + + subroutine direct_call_notify(callback, value) bind(C) + procedure(direct_notify) :: callback + integer(c_int), value, intent(in) :: value + + call callback(value) + end subroutine direct_call_notify +end module callbacks_direct_bind_c_f90 diff --git a/tests/fortran/callbacks/end_to_end/fixtures/routing/native/callbacks_mixed_bind_c_f90.f90 b/tests/fortran/callbacks/end_to_end/fixtures/routing/native/callbacks_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..a96933373 --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/fixtures/routing/native/callbacks_mixed_bind_c_f90.f90 @@ -0,0 +1,30 @@ +module callbacks_mixed_bind_c_f90 + use iso_c_binding + implicit none + + abstract interface + real(c_double) function direct_callback(value) bind(C) result(output) + import c_double + real(c_double), value, intent(in) :: value + end function direct_callback + + real(c_double) function adapted_callback(value) result(output) + import c_double + real(c_double), intent(in) :: value + end function adapted_callback + end interface +contains + real(c_double) function direct_apply(callback, value) bind(C) result(output) + procedure(direct_callback) :: callback + real(c_double), value, intent(in) :: value + + output = callback(value) + end function direct_apply + + real(c_double) function adapted_apply(callback, value) result(output) + procedure(adapted_callback) :: callback + real(c_double), intent(in) :: value + + output = callback(value) + end function adapted_apply +end module callbacks_mixed_bind_c_f90 diff --git a/tests/fortran/callbacks/end_to_end/test_callback_direct_entrypoint_routing.py b/tests/fortran/callbacks/end_to_end/test_callback_direct_entrypoint_routing.py new file mode 100644 index 000000000..8e8d69667 --- /dev/null +++ b/tests/fortran/callbacks/end_to_end/test_callback_direct_entrypoint_routing.py @@ -0,0 +1,134 @@ +"""Compiled immediate-callback direct and mixed entrypoint evidence.""" + +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_callbacks_all_direct_route_uses_binding_trampolines_without_adapter( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "callbacks_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + {"callbacks_direct_bind_c_f90_wrapper.c", "callbacks_direct_bind_c_f90_wrapper.h"}, + FIXTURES / "contracts" / "callbacks_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + calls = [] + assert module.direct_apply(lambda value: np.float64(2.0) * value, np.float64(3.0)) == np.float64(6.0) + assert module.direct_call_notify(lambda value: calls.append(value), np.int32(7)) is None + assert calls == [np.int32(7)] + assert module.direct_apply( + lambda value: module.direct_apply(lambda nested: nested + 1.0, np.float64(value)) * 2.0, + np.float64(3.0), + ) == np.float64(8.0) + + class Callback: + def __call__(self, value): + return value + + callback = Callback() + references_before = sys.getrefcount(callback) + assert module.direct_apply(callback, np.float64(3.0)) == np.float64(3.0) + assert sys.getrefcount(callback) == references_before + with pytest.raises(TypeError, match="must be callable"): + module.direct_apply(42, np.float64(1.0)) + + if pyi_parity_build_mode == "source": + binding = (tmp_path / "source_build" / "callbacks_direct_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + assert "double direct_apply(double (*callback)(double), double value);" in binding + assert "void direct_call_notify(void (*callback)(int32_t), int32_t value);" in binding + assert "static _Thread_local" in binding + assert "PyGILState_Ensure()" in binding + assert "direct_apply(prik_callback_trampoline_" in binding + + build_dir = ( + tmp_path / "source_build" + if pyi_parity_build_mode == "source" + else tmp_path / "generated_pyi_build" / "pyi_build" + ) + failure = subprocess.run( + [ + sys.executable, + "-c", + ( + "import numpy as np; import callbacks_direct_bind_c_f90 as root; " + "module = getattr(root, 'callbacks_direct_bind_c_f90', root); " + "module.direct_apply(lambda value: (_ for _ in ()).throw(" + "ValueError(f'callback exploded at {value}')), np.float64(4.0))" + ), + ], + cwd=build_dir, + capture_output=True, + text=True, + check=False, + ) + assert failure.returncode != 0 + assert "ValueError: callback exploded at 4.0" in failure.stderr + + +def test_callbacks_mixed_route_adapts_only_non_c_callback_signature( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "callbacks_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "callbacks_mixed_bind_c_f90_wrapper.c", + "callbacks_mixed_bind_c_f90_wrapper.h", + "bind_c_callbacks_mixed_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "callbacks_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.direct_apply(lambda value: np.float64(value + 1.0), np.float64(3.0)) == np.float64(4.0) + assert module.adapted_apply(lambda value: np.float64(value + 2.0), np.float64(3.0)) == np.float64(5.0) + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_callbacks_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_adapted_apply" in bridge + assert "direct_apply" not in bridge + + binding = (tmp_path / "source_build" / "callbacks_mixed_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + assert "bind_c_adapted_apply(bound_value)" in binding + + +def test_callbacks_mixed_route_matches_edited_source_free_contract(tmp_path: Path): + stem = "callbacks_mixed_bind_c_f90" + source = (FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8") + contract = (FIXTURES / "contracts" / stem / f"{stem}.pyi").read_text(encoding="utf-8") + contract = contract.replace("from prik.contracts import ", "from prik.contracts import nogil, ") + contract = contract.replace("def direct_apply(", "@nogil\ndef direct_apply(").replace( + "def adapted_apply(", "@nogil\ndef adapted_apply(" + ) + module, result = _build_inline_pyi_contract_module( + tmp_path, module_name=stem, source_text=source, contract_text=contract + ) + + assert module.direct_apply(lambda value: np.float64(value + 1.0), np.float64(3.0)) == np.float64(4.0) + assert module.adapted_apply(lambda value: np.float64(value + 2.0), np.float64(3.0)) == np.float64(5.0) + bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_adapted_apply" in bridge + assert "function bind_c_direct_apply" not in bridge diff --git a/tests/fortran/data_types/codegen/test_default_logical_scalar_lowering.py b/tests/fortran/data_types/codegen/test_default_logical_scalar_lowering.py index 6f8c78598..412d380e1 100644 --- a/tests/fortran/data_types/codegen/test_default_logical_scalar_lowering.py +++ b/tests/fortran/data_types/codegen/test_default_logical_scalar_lowering.py @@ -31,14 +31,14 @@ def _logical_function_plan(): def test_policy_completes_default_logical_input_and_output_kind_copies(): _module_plan, function = _logical_function_plan() input_plan = function.arguments[0] - output_slot = function.results[0].native_call_slot + output_slot = function.results[0].projected_call_slot assert input_plan.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY assert input_plan.scalar_native_type == "logical" assert input_plan.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION assert output_slot.scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY assert output_slot.scalar_native_type == "logical" - assert output_slot.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION + assert output_slot.adapter.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION def test_bridge_mechanically_lowers_completed_default_logical_kind_copies(): diff --git a/tests/fortran/data_types/codegen/test_primitive_scalar_result_lowering.py b/tests/fortran/data_types/codegen/test_primitive_scalar_result_lowering.py index 2b5b337f3..a4affb595 100644 --- a/tests/fortran/data_types/codegen/test_primitive_scalar_result_lowering.py +++ b/tests/fortran/data_types/codegen/test_primitive_scalar_result_lowering.py @@ -49,7 +49,7 @@ def test_direct_bool_result_normalizes_the_fortran_truth_bit_before_c_conversion plan = WrapperPlanner().build(module) result = plan.namespaces[0].functions[0].results[0] - assert result.direct_result_abi is DirectResultABI.LOGICAL_LOW_BIT_INT8 + assert result.entrypoint.direct_result_abi is DirectResultABI.LOGICAL_LOW_BIT_INT8 artifacts = WrapperGenerator().generate(plan) c_source = next(source.text for source in artifacts.sources if source.path.suffix == ".c") @@ -70,7 +70,7 @@ def test_generator_rejects_a_non_normalized_direct_bool_result_abi(): ) complete_semantic_policies(module) plan = WrapperPlanner().build(module) - plan.namespaces[0].functions[0].results[0].direct_result_abi = DirectResultABI.NATIVE_SCALAR + plan.namespaces[0].functions[0].results[0].entrypoint.direct_result_abi = DirectResultABI.NATIVE_SCALAR with pytest.raises(ValueError, match="invalid-direct-result-abi"): WrapperGenerator().generate(plan) diff --git a/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fbind_value_f90/fbind_value_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fbind_value_f90/fbind_value_f90.pyi index 9406a243c..8cd319629 100644 --- a/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fbind_value_f90/fbind_value_f90.pyi +++ b/tests/fortran/data_types/end_to_end/fixtures/baseline/contracts/fbind_value_f90/fbind_value_f90.pyi @@ -1,30 +1,42 @@ -from prik.contracts import Addr, Arg, Bool8, Complex128, Float64, Int32, String, native_call +from prik.contracts import Addr, Arg, Bool8, Complex128, Float64, Int32, String, Value, bind, native_abi, native_call +@native_abi("c") +@bind("prik_plus_value") def plus_value( n: Int32 ) -> Int32: ... +@native_abi("c") def double_value( n: Int32 ) -> Int32: ... +@native_abi("c") @native_call([Addr(Arg(0))]) def plus_reference( n: Int32 ) -> Int32: ... +@native_abi("c") +@bind("prik_scale_real") def scale_real( x: Float64 ) -> Float64: ... +@native_abi("c") +@bind("prik_conjugate_value") def conjugate_value( z: Complex128 ) -> Complex128: ... +@native_abi("c") +@bind("prik_invert_flag") def invert_flag( flag: Bool8 ) -> Bool8: ... +@native_abi("c") +@native_call([Value(Arg(0))]) def char_code( ch: String[1] ) -> Int32: ... diff --git a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/__init__.pyi b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..8059f6da2 --- /dev/null +++ b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import scalar_direct_bind_c_f90 diff --git a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/scalar_direct_bind_c_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/scalar_direct_bind_c_f90.pyi new file mode 100644 index 000000000..919b914d1 --- /dev/null +++ b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_direct_bind_c_f90/scalar_direct_bind_c_f90.pyi @@ -0,0 +1,30 @@ +from prik.contracts import Addr, Arg, Bool8, Float64, Int32, Return, bind, native_abi, native_call + +@native_abi("c") +@bind("scalar_direct_add") +def renamed_add( + value: Int32 +) -> Int32: ... + +@native_abi("c") +@native_call([Addr(Arg(0))]) +def reference_add( + value: Int32 +) -> Int32: ... + +@native_abi("c") +@native_call([Arg(0), Return('output', 0)]) +def scale_output( + value: Float64 +) -> Float64: ... + +@native_abi("c") +def invert_flag( + value: Bool8 +) -> Bool8: ... + +@native_abi("c") +@native_call([Addr(Arg(0)), Return('state', 0)]) +def optional_state( + value: Float64 = ... +) -> Int32: ... diff --git a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/__init__.pyi b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..3f14d14ca --- /dev/null +++ b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import scalar_mixed_bind_c_f90 diff --git a/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/scalar_mixed_bind_c_f90.pyi b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/scalar_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..6e07ecec9 --- /dev/null +++ b/tests/fortran/data_types/end_to_end/fixtures/routing/contracts/scalar_mixed_bind_c_f90/scalar_mixed_bind_c_f90.pyi @@ -0,0 +1,12 @@ +from prik.contracts import Addr, Arg, Int32, bind, native_abi, native_call + +@native_abi("c") +@bind("scalar_mixed_direct_add") +def direct_add( + value: Int32 +) -> Int32: ... + +@native_call([Addr(Arg(0))]) +def adapted_add( + value: Int32 +) -> Int32: ... diff --git a/tests/fortran/data_types/end_to_end/fixtures/routing/native/scalar_direct_bind_c_f90.f90 b/tests/fortran/data_types/end_to_end/fixtures/routing/native/scalar_direct_bind_c_f90.f90 new file mode 100644 index 000000000..b1955aed0 --- /dev/null +++ b/tests/fortran/data_types/end_to_end/fixtures/routing/native/scalar_direct_bind_c_f90.f90 @@ -0,0 +1,39 @@ +module scalar_direct_bind_c_f90 + use iso_c_binding +contains + integer(c_int) function renamed_add(value) bind(C, name="scalar_direct_add") result(output) + integer(c_int), value, intent(in) :: value + + output = value + 4_c_int + end function renamed_add + + integer(c_int) function reference_add(value) bind(C) result(output) + integer(c_int), intent(in) :: value + + output = value + 8_c_int + end function reference_add + + subroutine scale_output(value, output) bind(C) + real(c_double), value, intent(in) :: value + real(c_double), intent(out) :: output + + output = 2.5_c_double * value + end subroutine scale_output + + logical(c_bool) function invert_flag(value) bind(C) result(output) + logical(c_bool), value, intent(in) :: value + + output = .not. value + end function invert_flag + + subroutine optional_state(value, state) bind(C) + real(c_double), optional, intent(in) :: value + integer(c_int), intent(out) :: state + + if (present(value)) then + state = 1_c_int + else + state = 0_c_int + end if + end subroutine optional_state +end module scalar_direct_bind_c_f90 diff --git a/tests/fortran/data_types/end_to_end/fixtures/routing/native/scalar_mixed_bind_c_f90.f90 b/tests/fortran/data_types/end_to_end/fixtures/routing/native/scalar_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..476d2a956 --- /dev/null +++ b/tests/fortran/data_types/end_to_end/fixtures/routing/native/scalar_mixed_bind_c_f90.f90 @@ -0,0 +1,15 @@ +module scalar_mixed_bind_c_f90 + use iso_c_binding +contains + integer(c_int) function direct_add(value) bind(C, name="scalar_mixed_direct_add") result(output) + integer(c_int), value, intent(in) :: value + + output = value + 3_c_int + end function direct_add + + integer function adapted_add(value) result(output) + integer, intent(in) :: value + + output = value + 7 + end function adapted_add +end module scalar_mixed_bind_c_f90 diff --git a/tests/fortran/data_types/end_to_end/test_scalar_direct_entrypoint_routing.py b/tests/fortran/data_types/end_to_end/test_scalar_direct_entrypoint_routing.py new file mode 100644 index 000000000..778eb8d36 --- /dev/null +++ b/tests/fortran/data_types/end_to_end/test_scalar_direct_entrypoint_routing.py @@ -0,0 +1,113 @@ +"""Compiled all-direct and selectively adapted scalar route evidence.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +CONTRACTS = FIXTURES / "contracts" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_scalar_all_direct_route_has_no_generated_fortran_artifact( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "scalar_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + {"scalar_direct_bind_c_f90_wrapper.c", "scalar_direct_bind_c_f90_wrapper.h"}, + CONTRACTS / "scalar_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.renamed_add(np.int32(5)) == np.int32(9) + assert module.reference_add(np.int32(5)) == np.int32(13) + assert module.scale_output(np.float64(4.0)) == np.float64(10.0) + assert bool(module.invert_flag(np.bool_(True))) is False + assert module.optional_state() == np.int32(0) + assert module.optional_state(None) == np.int32(0) + assert module.optional_state(np.float64(1.0)) == np.int32(1) + + if pyi_parity_build_mode == "source": + build_dir = tmp_path / "source_build" + binding = (build_dir / "scalar_direct_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + assert "int32_t scalar_direct_add(int32_t value);" in binding + assert "void scale_output(double value, double * output);" in binding + assert not (build_dir / "bind_c_scalar_direct_bind_c_f90_wrapper.f90").exists() + assert not (build_dir / "bind_c_scalar_direct_bind_c_f90_wrapper.o").exists() + + +def test_scalar_mixed_route_generates_only_the_ordinary_adapter( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "scalar_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "bind_c_scalar_mixed_bind_c_f90_wrapper.f90", + "scalar_mixed_bind_c_f90_wrapper.c", + "scalar_mixed_bind_c_f90_wrapper.h", + }, + CONTRACTS / "scalar_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.direct_add(np.int32(5)) == np.int32(8) + assert module.adapted_add(np.int32(5)) == np.int32(12) + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_scalar_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + binding = (tmp_path / "source_build" / "scalar_mixed_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + assert "bind_c_adapted_add" in bridge + assert "native_adapted_add => adapted_add" in bridge + assert "direct_add" not in bridge + assert "int32_t scalar_mixed_direct_add(int32_t value);" in binding + + +def test_source_free_edited_contract_preserves_direct_symbol_scalars_and_boolean_validation( + tmp_path: Path, +): + source = (FIXTURES / "native" / "scalar_direct_bind_c_f90.f90").read_text(encoding="utf-8") + contract = """ +from prik.contracts import Bool8, Int32, bind, native_abi + +@native_abi("c") +@bind("scalar_direct_add") +def edited_add(value: Int32) -> Int32: ... + +@native_abi("c") +def invert_flag(value: Bool8) -> Bool8: ... +""" + module, result = _build_inline_pyi_contract_module( + tmp_path, + module_name="scalar_direct_bind_c_f90", + source_text=source, + contract_text=contract, + ) + + assert module.edited_add(np.int32(5)) == np.int32(9) + assert bool(module.invert_flag(np.bool_(True))) is False + with pytest.raises(TypeError): + module.invert_flag(1) + + assert {path.name for path in result.generated_sources} == { + "scalar_direct_bind_c_f90_wrapper.c", + "scalar_direct_bind_c_f90_wrapper.h", + } + binding = (result.output_dir / "scalar_direct_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + assert "int32_t scalar_direct_add(int32_t value);" in binding + assert "result = scalar_direct_add(bound_value);" in binding diff --git a/tests/fortran/data_types/end_to_end/test_value_and_bind_c.py b/tests/fortran/data_types/end_to_end/test_value_and_bind_c.py index a417cfcbf..867cdfbf3 100644 --- a/tests/fortran/data_types/end_to_end/test_value_and_bind_c.py +++ b/tests/fortran/data_types/end_to_end/test_value_and_bind_c.py @@ -23,7 +23,6 @@ def test_value_and_existing_bind_c_renamed_symbol_use_correct_abi( BIND_VALUE_F90_SOURCE, tmp_path, { - "bind_c_fbind_value_f90_wrapper.f90", "fbind_value_f90_wrapper.c", "fbind_value_f90_wrapper.h", }, @@ -40,17 +39,14 @@ def test_value_and_existing_bind_c_renamed_symbol_use_correct_abi( assert module.char_code("A") == np.int32(65) if pyi_parity_build_mode == "source": - bridge_source = ( - (tmp_path / "source_build" / "bind_c_fbind_value_f90_wrapper.f90").read_text(encoding="utf-8").lower() - ) + binding_source = (tmp_path / "source_build" / "fbind_value_f90_wrapper.c").read_text(encoding="utf-8") for native_name in ( - "plus_value", + "prik_plus_value", "double_value", "plus_reference", - "scale_real", - "conjugate_value", - "invert_flag", + "prik_scale_real", + "prik_conjugate_value", + "prik_invert_flag", "char_code", ): - assert f"native_{native_name} => {native_name}" in bridge_source - assert f"bind_c_{native_name}" in bridge_source + assert f"{native_name}(" in binding_source diff --git a/tests/fortran/derived_types/codegen/test_derived_lowering.py b/tests/fortran/derived_types/codegen/test_derived_lowering.py index 9bc08e940..6107eabc3 100644 --- a/tests/fortran/derived_types/codegen/test_derived_lowering.py +++ b/tests/fortran/derived_types/codegen/test_derived_lowering.py @@ -62,7 +62,7 @@ def test_exact_typed_value_policy_projects_shared_canonical_derived_handoff(attr assert policy.derived.type_identity == ("derived_value", "point") assert policy.derived.native_handoff is DerivedNativeHandoff.TYPED_VALUE assert policy.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION - assert argument.derived is argument.native_call_slot.derived + assert argument.derived is argument.projected_call_slot.derived assert argument.derived.type_identity == ("derived_value", "point") assert argument.derived.native_handoff is DerivedNativeHandoff.TYPED_VALUE assert "type_identity=('derived_value', 'point')" in str(plan) @@ -123,8 +123,8 @@ def make_point() -> point: ... assert [action.operation for action in function.cleanup_actions] == [LifecycleOperation.DESTROY_ON_FAILURE] assert [action.operation for action in function.release_actions] == [LifecycleOperation.TRANSFER_TO_WRAPPER] - assert function.cleanup_actions[0].source_role == function.results[0].bridge.native_result_role - assert function.release_actions[0].source_role == function.results[0].bridge.native_result_role + assert function.cleanup_actions[0].source_role == function.results[0].entrypoint.native_result_role + assert function.release_actions[0].source_role == function.results[0].entrypoint.native_result_role function.release_actions = () with pytest.raises(ValueError, match="derived-wrapper-release-count"): @@ -150,11 +150,11 @@ def make_point() -> Returns["value", point]: ... result = function.results[0] assert result.source_kind == "hidden_output" - assert result.derived is result.native_call_slot.derived + assert result.derived is result.projected_call_slot.derived assert result.derived.origin is DerivedObjectOrigin.WRAPPER_RESULT assert result.derived.release is DerivedRelease.WRAPPER_DESTROY - assert function.cleanup_actions[0].source_role == result.bridge.native_result_role - assert function.release_actions[0].source_role == result.bridge.native_result_role + assert function.cleanup_actions[0].source_role == result.entrypoint.native_result_role + assert function.release_actions[0].source_role == result.entrypoint.native_result_role c_source, bridge_source = _sources(plan) assert "PyCapsule_New(value" in c_source @@ -178,7 +178,7 @@ def update(value: point) -> Returns["value", point]: ... function = plan.namespaces[0].functions[0] argument = function.arguments[0] - assert argument.derived is argument.native_call_slot.derived + assert argument.derived is argument.projected_call_slot.derived assert argument.derived.origin is DerivedObjectOrigin.CALLER_WRAPPER assert argument.derived.owner_retention is DerivedOwnerRetention.CALLER_WRAPPER assert argument.derived.release is DerivedRelease.NONE diff --git a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/fbind_c_derived_layout_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/fbind_c_derived_layout_f90.pyi index cabd1cdcc..076e850c9 100644 --- a/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/fbind_c_derived_layout_f90.pyi +++ b/tests/fortran/derived_types/end_to_end/fixtures/contracts/fbind_c_derived_layout_f90/fbind_c_derived_layout_f90.pyi @@ -1,4 +1,4 @@ -from prik.contracts import Arg, Complex128, Float64, Int32, Value, native_call, native_type +from prik.contracts import Arg, Complex128, Float64, Int32, Value, native_abi, native_call, native_type @native_type(attributes=('bind(c)',)) class point: @@ -23,6 +23,7 @@ class tagged_point: position: point weight: Complex128 +@native_abi("c") def populate( value: tagged_point, x: Float64, @@ -30,6 +31,7 @@ def populate( weight: Complex128 ) -> None: ... +@native_abi("c") @native_call([Value(Arg(0))]) def score_by_value( value: tagged_point diff --git a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/__init__.pyi b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..f370af40b --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import derived_types_direct_bind_c_f90 diff --git a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/derived_types_direct_bind_c_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/derived_types_direct_bind_c_f90.pyi new file mode 100644 index 000000000..c2a3e086d --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_direct_bind_c_f90/derived_types_direct_bind_c_f90.pyi @@ -0,0 +1,24 @@ +from prik.contracts import Float64, native_abi, native_type + +@native_type(attributes=('bind(c)',)) +class point: + def __init__( + self, + *, + x: Float64 = ..., + y: Float64 = ... + ) -> None: ... + + x: Float64 + y: Float64 + +@native_abi("c") +def direct_sum( + value: point +) -> Float64: ... + +@native_abi("c") +def direct_shift( + value: point, + delta: Float64 +) -> None: ... diff --git a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/__init__.pyi b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..bfa13be19 --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import derived_types_mixed_bind_c_f90 diff --git a/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/derived_types_mixed_bind_c_f90.pyi b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/derived_types_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..4669dd1f1 --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/fixtures/routing/contracts/derived_types_mixed_bind_c_f90/derived_types_mixed_bind_c_f90.pyi @@ -0,0 +1,24 @@ +from prik.contracts import Arg, Float64, Value, native_abi, native_call, native_type + +@native_type(attributes=('bind(c)',)) +class point: + def __init__( + self, + *, + x: Float64 = ..., + y: Float64 = ... + ) -> None: ... + + x: Float64 + y: Float64 + +@native_abi("c") +def direct_sum( + value: point +) -> Float64: ... + +@native_abi("c") +@native_call([Value(Arg(0))]) +def adapted_sum_by_value( + value: point +) -> Float64: ... diff --git a/tests/fortran/derived_types/end_to_end/fixtures/routing/native/derived_types_direct_bind_c_f90.f90 b/tests/fortran/derived_types/end_to_end/fixtures/routing/native/derived_types_direct_bind_c_f90.f90 new file mode 100644 index 000000000..4a9f3c964 --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/fixtures/routing/native/derived_types_direct_bind_c_f90.f90 @@ -0,0 +1,23 @@ +module derived_types_direct_bind_c_f90 + use iso_c_binding + implicit none + + type, bind(C) :: point + real(c_double) :: x + real(c_double) :: y + end type point +contains + real(c_double) function direct_sum(value) bind(C) result(total) + type(point), intent(in) :: value + + total = value%x + value%y + end function direct_sum + + subroutine direct_shift(value, delta) bind(C) + type(point), intent(inout) :: value + real(c_double), value, intent(in) :: delta + + value%x = value%x + delta + value%y = value%y + delta + end subroutine direct_shift +end module derived_types_direct_bind_c_f90 diff --git a/tests/fortran/derived_types/end_to_end/fixtures/routing/native/derived_types_mixed_bind_c_f90.f90 b/tests/fortran/derived_types/end_to_end/fixtures/routing/native/derived_types_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..f02373dd3 --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/fixtures/routing/native/derived_types_mixed_bind_c_f90.f90 @@ -0,0 +1,21 @@ +module derived_types_mixed_bind_c_f90 + use iso_c_binding + implicit none + + type, bind(C) :: point + real(c_double) :: x + real(c_double) :: y + end type point +contains + real(c_double) function direct_sum(value) bind(C) result(total) + type(point), intent(in) :: value + + total = value%x + value%y + end function direct_sum + + real(c_double) function adapted_sum_by_value(value) bind(C) result(total) + type(point), value, intent(in) :: value + + total = value%x + value%y + end function adapted_sum_by_value +end module derived_types_mixed_bind_c_f90 diff --git a/tests/fortran/derived_types/end_to_end/test_derived_direct_entrypoint_routing.py b/tests/fortran/derived_types/end_to_end/test_derived_direct_entrypoint_routing.py new file mode 100644 index 000000000..12814e415 --- /dev/null +++ b/tests/fortran/derived_types/end_to_end/test_derived_direct_entrypoint_routing.py @@ -0,0 +1,108 @@ +"""Compiled opaque derived-object direct and mixed entrypoint evidence.""" + +import sys +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_derived_all_direct_route_keeps_generated_type_support_separate( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "derived_types_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "derived_types_direct_bind_c_f90_wrapper.c", + "derived_types_direct_bind_c_f90_wrapper.h", + "bind_c_derived_types_direct_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "derived_types_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + value = module.point(x=np.float64(1.5), y=np.float64(2.5)) + references_before = sys.getrefcount(value) + assert module.direct_sum(value) == np.float64(4.0) + shifted = module.direct_shift(value, np.float64(2.0)) + assert shifted is None + assert (value.x, value.y) == (np.float64(3.5), np.float64(4.5)) + assert sys.getrefcount(value) == references_before + + if pyi_parity_build_mode == "source": + support = ( + (tmp_path / "source_build" / "bind_c_derived_types_direct_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_prik_create_point" in support + assert "bind_c_prik_destroy_point" in support + assert "direct_sum" not in support + assert "direct_shift" not in support + + binding = (tmp_path / "source_build" / "derived_types_direct_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + assert "double direct_sum(void * value);" in binding + assert "void direct_shift(void * value, double delta);" in binding + assert "struct point" not in binding + + +def test_derived_mixed_route_adapts_only_by_value_aggregate( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "derived_types_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "derived_types_mixed_bind_c_f90_wrapper.c", + "derived_types_mixed_bind_c_f90_wrapper.h", + "bind_c_derived_types_mixed_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "derived_types_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + + value = module.point(x=np.float64(2.0), y=np.float64(3.0)) + assert module.direct_sum(value) == np.float64(5.0) + assert module.adapted_sum_by_value(value) == np.float64(5.0) + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_derived_types_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_adapted_sum_by_value" in bridge + assert "direct_sum" not in bridge + + +def test_derived_mixed_route_matches_edited_source_free_contract(tmp_path: Path): + stem = "derived_types_mixed_bind_c_f90" + source = (FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8") + contract = (FIXTURES / "contracts" / stem / f"{stem}.pyi").read_text(encoding="utf-8") + contract = contract.replace("from prik.contracts import ", "from prik.contracts import nogil, ") + contract = contract.replace("def direct_sum(", "@nogil\ndef direct_sum(").replace( + "def adapted_sum_by_value(", "@nogil\ndef adapted_sum_by_value(" + ) + module, result = _build_inline_pyi_contract_module( + tmp_path, module_name=stem, source_text=source, contract_text=contract + ) + + value = module.point(x=np.float64(2.0), y=np.float64(3.0)) + assert module.direct_sum(value) == np.float64(5.0) + assert module.adapted_sum_by_value(value) == np.float64(5.0) + bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_adapted_sum_by_value" in bridge + assert "function bind_c_direct_sum" not in bridge 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 5cfe6ea0d..fa6eeb2e2 100644 --- a/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py +++ b/tests/fortran/derived_types/semantics/test_imported_derived_semantics.py @@ -86,15 +86,10 @@ def test_converter_preserves_imported_derived_contexts_through_dispatch_paths(): assert semantic_class.fields[0].semantic_type.metadata["external_type_ref"] == external_ref assert isinstance(semantic_class.fields[0], SemanticField) assert semantic_class.visibility == "private" - assert asdict(semantic_class.origin) == { - "source_language": "fortran", - "native_name": "container_t", - "native_scope": "consumer", - "source_kind": "derived_type", - "source_type": None, - "source_location": {}, - "metadata": {}, - } + assert semantic_class.origin.source_language == "fortran" + assert semantic_class.origin.native_name == "container_t" + assert semantic_class.origin.native_scope == "consumer" + assert semantic_class.origin.source_kind == "derived_type" semantic_proc = semantic_module.functions[0] assert semantic_proc.native_name == "step" assert semantic_proc.locals == [] @@ -103,16 +98,11 @@ def test_converter_preserves_imported_derived_contexts_through_dispatch_paths(): assert isinstance(semantic_module.variables[0], SemanticVariable) assert [method.name for method in semantic_module.classes[0].methods] == ["step"] assert semantic_module.classes[0].methods[0].projection == semantic_proc.projection - assert asdict(semantic_module.classes[0].methods[0].origin) == asdict(semantic_proc.origin) - assert asdict(semantic_proc.origin) == { - "source_language": "fortran", - "native_name": "step", - "native_scope": "consumer", - "source_kind": "subroutine", - "source_type": None, - "source_location": {}, - "metadata": {}, - } + assert semantic_module.classes[0].methods[0].origin == semantic_proc.origin + assert semantic_proc.origin.source_language == "fortran" + assert semantic_proc.origin.native_name == "step" + assert semantic_proc.origin.native_scope == "consumer" + assert semantic_proc.origin.source_kind == "subroutine" assert [asdict(mapping) for mapping in semantic_proc.projection] == [ { "python_name": "arg", @@ -124,15 +114,10 @@ def test_converter_preserves_imported_derived_contexts_through_dispatch_paths(): "value": None, } ] - assert asdict(semantic_module.origin) == { - "source_language": "fortran", - "native_name": "consumer", - "native_scope": "consumer", - "source_kind": "module", - "source_type": None, - "source_location": {}, - "metadata": {}, - } + assert semantic_module.origin.source_language == "fortran" + assert semantic_module.origin.native_name == "consumer" + assert semantic_module.origin.native_scope == "consumer" + assert semantic_module.origin.source_kind == "module" assert converter.visit(FortranDerivedType(name="default_t")).visibility == "public" assert converter.visit(FortranVariable(name="local", base_type="derived", kind="state_t")).name == "state_t" diff --git a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/__init__.pyi b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..66217b5f6 --- /dev/null +++ b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import enumerations_direct_bind_c_f90 diff --git a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/enumerations_direct_bind_c_f90.pyi b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/enumerations_direct_bind_c_f90.pyi new file mode 100644 index 000000000..9765eb383 --- /dev/null +++ b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_direct_bind_c_f90/enumerations_direct_bind_c_f90.pyi @@ -0,0 +1,20 @@ +from prik.contracts import Arg, Final, Int32, Return, native_abi, native_call + +terminal: Final[Int32] = 5 + +stopped: Final[Int32] = -1 + +ready: Final[Int32] = 0 + +running: Final[Int32] = 4 + +@native_abi("c") +def direct_round_trip( + state: Int32 +) -> Int32: ... + +@native_abi("c") +@native_call([Arg(0), Return('output', 0)]) +def direct_next( + state: Int32 +) -> Int32: ... diff --git a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/__init__.pyi b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..403943be5 --- /dev/null +++ b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import enumerations_mixed_bind_c_f90 diff --git a/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/enumerations_mixed_bind_c_f90.pyi b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/enumerations_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..4e1caf98f --- /dev/null +++ b/tests/fortran/enumerations/end_to_end/fixtures/routing/contracts/enumerations_mixed_bind_c_f90/enumerations_mixed_bind_c_f90.pyi @@ -0,0 +1,17 @@ +from prik.contracts import Addr, Arg, Final, Int32, native_abi, native_call + +stopped: Final[Int32] = -1 + +ready: Final[Int32] = 0 + +running: Final[Int32] = 4 + +@native_abi("c") +def direct_round_trip( + state: Int32 +) -> Int32: ... + +@native_call([Addr(Arg(0))]) +def adapted_next( + state: Int32 +) -> Int32: ... diff --git a/tests/fortran/enumerations/end_to_end/fixtures/routing/native/enumerations_direct_bind_c_f90.f90 b/tests/fortran/enumerations/end_to_end/fixtures/routing/native/enumerations_direct_bind_c_f90.f90 new file mode 100644 index 000000000..ea1840d24 --- /dev/null +++ b/tests/fortran/enumerations/end_to_end/fixtures/routing/native/enumerations_direct_bind_c_f90.f90 @@ -0,0 +1,23 @@ +module enumerations_direct_bind_c_f90 + use iso_c_binding + implicit none + + enum, bind(C) + enumerator :: stopped = -1, ready, running = 4 + end enum + + integer(c_int), parameter :: terminal = 5 +contains + integer(c_int) function direct_round_trip(state) bind(C) result(output) + integer(c_int), value, intent(in) :: state + + output = state + end function direct_round_trip + + subroutine direct_next(state, output) bind(C) + integer(c_int), value, intent(in) :: state + integer(c_int), intent(out) :: output + + output = state + 1_c_int + end subroutine direct_next +end module enumerations_direct_bind_c_f90 diff --git a/tests/fortran/enumerations/end_to_end/fixtures/routing/native/enumerations_mixed_bind_c_f90.f90 b/tests/fortran/enumerations/end_to_end/fixtures/routing/native/enumerations_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..cea3c44f3 --- /dev/null +++ b/tests/fortran/enumerations/end_to_end/fixtures/routing/native/enumerations_mixed_bind_c_f90.f90 @@ -0,0 +1,20 @@ +module enumerations_mixed_bind_c_f90 + use iso_c_binding + implicit none + + enum, bind(C) + enumerator :: stopped = -1, ready, running = 4 + end enum +contains + integer(c_int) function direct_round_trip(state) bind(C) result(output) + integer(c_int), value, intent(in) :: state + + output = state + end function direct_round_trip + + integer(c_int) function adapted_next(state) result(output) + integer(c_int), intent(in) :: state + + output = state + 1_c_int + end function adapted_next +end module enumerations_mixed_bind_c_f90 diff --git a/tests/fortran/enumerations/end_to_end/test_enumeration_direct_entrypoint_routing.py b/tests/fortran/enumerations/end_to_end/test_enumeration_direct_entrypoint_routing.py new file mode 100644 index 000000000..4b4429b10 --- /dev/null +++ b/tests/fortran/enumerations/end_to_end/test_enumeration_direct_entrypoint_routing.py @@ -0,0 +1,84 @@ +"""Compiled direct and mixed enumeration/constant entrypoint evidence.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_enumerations_all_direct_route_keeps_integer_constants_and_abi( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "enumerations_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + {"enumerations_direct_bind_c_f90_wrapper.c", "enumerations_direct_bind_c_f90_wrapper.h"}, + FIXTURES / "contracts" / "enumerations_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + assert (module.stopped, module.ready, module.running, module.terminal) == tuple( + np.int32(value) for value in (-1, 0, 4, 5) + ) + assert module.direct_round_trip(module.running) == np.int32(4) + assert module.direct_next(module.running) == np.int32(5) + assert not hasattr(module, "Enum") + + +def test_enumerations_mixed_route_adapts_only_ordinary_operation( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "enumerations_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "enumerations_mixed_bind_c_f90_wrapper.c", + "enumerations_mixed_bind_c_f90_wrapper.h", + "bind_c_enumerations_mixed_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "enumerations_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.direct_round_trip(module.running) == np.int32(4) + assert module.adapted_next(module.running) == np.int32(5) + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_enumerations_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_adapted_next" in bridge + assert "direct_round_trip" not in bridge + + +def test_enumerations_mixed_route_matches_edited_source_free_contract(tmp_path: Path): + stem = "enumerations_mixed_bind_c_f90" + source = (FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8") + contract = (FIXTURES / "contracts" / stem / f"{stem}.pyi").read_text(encoding="utf-8") + contract = contract.replace("from prik.contracts import ", "from prik.contracts import nogil, ") + contract = contract.replace("def direct_round_trip(", "@nogil\ndef direct_round_trip(").replace( + "def adapted_next(", "@nogil\ndef adapted_next(" + ) + module, result = _build_inline_pyi_contract_module( + tmp_path, module_name=stem, source_text=source, contract_text=contract + ) + + assert module.direct_round_trip(module.running) == np.int32(4) + assert module.adapted_next(module.running) == np.int32(5) + bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_adapted_next" in bridge + assert "function bind_c_direct_round_trip" not in bridge 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 543dadfff..8bc5df7b0 100644 --- a/tests/fortran/error_handling/codegen/test_status_error_lowering.py +++ b/tests/fortran/error_handling/codegen/test_status_error_lowering.py @@ -55,21 +55,30 @@ def test_planner_records_editable_native_runtime_and_status_error_facts(): assert solve.binding.status_error is not None assert solve.binding.status_error.success == 0 assert solve.binding.status_error.exception_kind is PythonExceptionKind.RUNTIME_ERROR - assert solve.binding.status_error.status_role == solve.native_call_slots[1].symbolic_role - assert solve.binding.status_error.message_role == solve.native_call_slots[2].symbolic_role + assert solve.binding.status_error.status_role == solve.entrypoint.projected_slots[1].symbolic_role + assert solve.binding.status_error.message_role == solve.entrypoint.projected_slots[2].symbolic_role + assert [(result.parameter_name, result.native_result_role) for result in solve.entrypoint.results] == [ + ("status", solve.binding.status_error.status_role), + ("message", solve.binding.status_error.message_role), + ] + assert [parameter.source_kind for parameter in solve.entrypoint.parameters] == [ + "argument", + "hidden_result", + "hidden_result", + ] WrapperGenerator().generate(plan) assert "Raises\n------" in solve.binding.docstring assert solve.binding.docstring.count("RuntimeError\n") == 1 assert "If native status differs from the success value 0." in solve.binding.docstring - assert solve.native_call_slots[1].semantic_type_name == "Int32" - assert solve.native_call_slots[1].datatype_family is DatatypeFamily.INTEGER - assert solve.native_call_slots[1].bridge_data_action is BridgeDataAction.DIRECT_TRANSFER - assert solve.native_call_slots[1].bridge_copy_reason is None - assert solve.native_call_slots[2].semantic_type_name == "String" - assert solve.native_call_slots[2].datatype_family is DatatypeFamily.STRING - assert solve.native_call_slots[2].character_length == 32 - assert solve.native_call_slots[2].bridge_data_action is BridgeDataAction.COPY_REPRESENTATION - assert solve.native_call_slots[2].bridge_copy_reason == ( + assert solve.entrypoint.projected_slots[1].semantic_type_name == "Int32" + assert solve.entrypoint.projected_slots[1].datatype_family is DatatypeFamily.INTEGER + assert solve.entrypoint.projected_slots[1].adapter.bridge_data_action is BridgeDataAction.DIRECT_TRANSFER + assert solve.entrypoint.projected_slots[1].adapter.bridge_copy_reason is None + assert solve.entrypoint.projected_slots[2].semantic_type_name == "String" + assert solve.entrypoint.projected_slots[2].datatype_family is DatatypeFamily.STRING + assert solve.entrypoint.projected_slots[2].character_length == 32 + assert solve.entrypoint.projected_slots[2].adapter.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION + assert solve.entrypoint.projected_slots[2].adapter.bridge_copy_reason == ( "copy fixed-length Fortran character output into C-owned null-terminated storage" ) @@ -113,9 +122,17 @@ def test_fixed_message_bridge_copy_requires_its_completed_reason(): "solve", lambda function: replace( function, - native_call_slots=tuple( - replace(slot, bridge_copy_reason=None) if slot.datatype_family is DatatypeFamily.STRING else slot - for slot in function.native_call_slots + entrypoint=replace( + function.entrypoint, + projected_slots=tuple( + replace( + slot, + adapter=replace(slot.adapter, bridge_copy_reason=None), + ) + if slot.datatype_family is DatatypeFamily.STRING + else slot + for slot in function.entrypoint.projected_slots + ), ), ), ) 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 new file mode 100644 index 000000000..666e5ca36 --- /dev/null +++ b/tests/fortran/error_handling/end_to_end/fixtures/routing/contracts/error_handling_direct_bind_c_f90.pyi @@ -0,0 +1,9 @@ +from prik.contracts import Arg, Int32, Return, Returns, 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)]) +def direct_solve( + value: Int32 +) -> tuple[Int32, Returns["status", 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 new file mode 100644 index 000000000..c7a4e847d --- /dev/null +++ b/tests/fortran/error_handling/end_to_end/fixtures/routing/contracts/error_handling_mixed_bind_c_f90.pyi @@ -0,0 +1,16 @@ +from prik.contracts import Addr, Arg, Int32, Return, Returns, 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)]) +def direct_solve( + value: Int32 +) -> tuple[Int32, Returns["status", Int32]]: ... + +@raises(status="status", success=0) +@nogil +@native_call([Addr(Arg(0)), Return("output", 0), Return("status", 1)]) +def adapted_solve( + value: Int32 +) -> tuple[Int32, Returns["status", Int32]]: ... diff --git a/tests/fortran/error_handling/end_to_end/fixtures/routing/native/error_handling_direct_bind_c_f90.f90 b/tests/fortran/error_handling/end_to_end/fixtures/routing/native/error_handling_direct_bind_c_f90.f90 new file mode 100644 index 000000000..982f96f90 --- /dev/null +++ b/tests/fortran/error_handling/end_to_end/fixtures/routing/native/error_handling_direct_bind_c_f90.f90 @@ -0,0 +1,17 @@ +module error_handling_direct_bind_c_f90 + use iso_c_binding +contains + subroutine direct_solve(value, output, status) bind(C) + integer(c_int), value, intent(in) :: value + integer(c_int), intent(out) :: output + integer(c_int), intent(out) :: status + + if (value < 0_c_int) then + output = 0_c_int + status = 5_c_int + else + output = 2_c_int * value + status = 0_c_int + end if + end subroutine direct_solve +end module error_handling_direct_bind_c_f90 diff --git a/tests/fortran/error_handling/end_to_end/fixtures/routing/native/error_handling_mixed_bind_c_f90.f90 b/tests/fortran/error_handling/end_to_end/fixtures/routing/native/error_handling_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..bd4682e60 --- /dev/null +++ b/tests/fortran/error_handling/end_to_end/fixtures/routing/native/error_handling_mixed_bind_c_f90.f90 @@ -0,0 +1,21 @@ +module error_handling_mixed_bind_c_f90 + use iso_c_binding +contains + subroutine direct_solve(value, output, status) bind(C) + integer(c_int), value, intent(in) :: value + integer(c_int), intent(out) :: output + integer(c_int), intent(out) :: status + + output = 2_c_int * max(value, 0_c_int) + status = merge(5_c_int, 0_c_int, value < 0_c_int) + end subroutine direct_solve + + subroutine adapted_solve(value, output, status) + integer(c_int), intent(in) :: value + integer(c_int), intent(out) :: output + integer(c_int), intent(out) :: status + + output = 3_c_int * max(value, 0_c_int) + status = merge(6_c_int, 0_c_int, value < 0_c_int) + end subroutine adapted_solve +end module error_handling_mixed_bind_c_f90 diff --git a/tests/fortran/error_handling/end_to_end/test_error_direct_entrypoint_routing.py b/tests/fortran/error_handling/end_to_end/test_error_direct_entrypoint_routing.py new file mode 100644 index 000000000..7cd2ded45 --- /dev/null +++ b/tests/fortran/error_handling/end_to_end/test_error_direct_entrypoint_routing.py @@ -0,0 +1,56 @@ +"""Compiled status checking and GIL policy across direct and mixed routes.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_inline_pyi_contract_module + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def _build(tmp_path: Path, stem: str): + return _build_inline_pyi_contract_module( + tmp_path, + module_name=stem, + source_text=(FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8"), + contract_text=(FIXTURES / "contracts" / f"{stem}.pyi").read_text(encoding="utf-8"), + ) + + +def test_status_and_gil_all_direct_route_has_no_adapter(tmp_path: Path): + module, result = _build(tmp_path, "error_handling_direct_bind_c_f90") + + assert module.direct_solve(np.int32(4)) == np.int32(8) + with pytest.raises(RuntimeError, match="status 5"): + module.direct_solve(np.int32(-1)) + assert {path.name for path in result.generated_sources} == { + "error_handling_direct_bind_c_f90_wrapper.c", + "error_handling_direct_bind_c_f90_wrapper.h", + } + + binding = (result.output_dir / "error_handling_direct_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + wrapper = binding[binding.index("static PyObject * wrap_direct_solve") : binding.index("PyMODINIT_FUNC")] + assert wrapper.index("Py_BEGIN_ALLOW_THREADS") < wrapper.index("direct_solve(bound_value, &output, &status)") + assert wrapper.index("Py_END_ALLOW_THREADS") < wrapper.index("status != 0") + + +def test_status_and_gil_mixed_route_adapts_only_ordinary_operation(tmp_path: Path): + module, result = _build(tmp_path, "error_handling_mixed_bind_c_f90") + + assert module.direct_solve(np.int32(4)) == np.int32(8) + assert module.adapted_solve(np.int32(4)) == np.int32(12) + with pytest.raises(RuntimeError): + module.direct_solve(np.int32(-1)) + with pytest.raises(RuntimeError): + module.adapted_solve(np.int32(-1)) + + bridge = ( + (result.output_dir / "bind_c_error_handling_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_adapted_solve" in bridge + assert "direct_solve" not in bridge diff --git a/tests/fortran/functions/codegen/test_multiple_function_results.py b/tests/fortran/functions/codegen/test_multiple_function_results.py index 906eb70ec..0e9df2526 100644 --- a/tests/fortran/functions/codegen/test_multiple_function_results.py +++ b/tests/fortran/functions/codegen/test_multiple_function_results.py @@ -35,13 +35,13 @@ def test_multiple_scalar_result_plan_has_ordered_binding_consumers_and_shared_hi ("direct_return", 0), ("hidden_output", 1), ] - assert direct.native_call_slot is None - assert hidden.native_call_slot is function.native_call_slots[hidden.bridge.abi_position] + assert direct.projected_call_slot is None + assert hidden.projected_call_slot is function.entrypoint.projected_slots[hidden.projected_call_slot.native_position] assert direct.binding.codegen_action is CodegenAction.DIRECT_VALUE assert hidden.binding.codegen_action is CodegenAction.DIRECT_VALUE assert hidden.bridge.native_action is NativeBarrierAction.PASS_CALL_LOCAL_ADDRESS - assert direct.bridge.native_result_role in function.available_roles - assert hidden.bridge.native_result_role in function.available_roles + assert direct.entrypoint.native_result_role in function.available_roles + assert hidden.entrypoint.native_result_role in function.available_roles def test_multiple_scalar_results_lower_to_binding_tuple_and_one_bridge_function_call(): @@ -90,6 +90,6 @@ def test_multiple_scalar_result_validation_rejects_position_and_consumer_drift() plan = _multiple_result_plan() function = plan.namespaces[0].functions[0] _direct, hidden = function.results - hidden.native_call_slot = replace(hidden.native_call_slot) + hidden.projected_call_slot = replace(hidden.projected_call_slot) with pytest.raises(ValueError, match="inconsistent-function-result-slot"): WrapperGenerator().generate(plan) diff --git a/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_direct_bind_c_f90/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..8aa3d1f26 --- /dev/null +++ b/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_direct_bind_c_f90/__init__.pyi @@ -0,0 +1,15 @@ +from prik.contracts import Arg, Int32, Return, bind, native_abi, native_call, standalone + +@native_abi("c") +@bind("standalone_direct_symbol") +@standalone +def standalone_direct( + value: Int32 +) -> Int32: ... + +@native_abi("c") +@standalone +@native_call([Arg(0), Return('output', 0)]) +def standalone_output( + value: Int32 +) -> Int32: ... diff --git a/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_mixed_bind_c_f90/__init__.pyi b/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..c0e58fd2f --- /dev/null +++ b/tests/fortran/functions/end_to_end/fixtures/routing/contracts/standalone_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1,14 @@ +from prik.contracts import Addr, Arg, Int32, bind, native_abi, native_call, standalone + +@native_abi("c") +@bind("standalone_mixed_direct") +@standalone +def standalone_direct( + value: Int32 +) -> Int32: ... + +@standalone +@native_call([Addr(Arg(0))]) +def standalone_adapted( + value: Int32 +) -> Int32: ... diff --git a/tests/fortran/functions/end_to_end/fixtures/routing/native/standalone_direct_bind_c_f90.f90 b/tests/fortran/functions/end_to_end/fixtures/routing/native/standalone_direct_bind_c_f90.f90 new file mode 100644 index 000000000..64fbd4fad --- /dev/null +++ b/tests/fortran/functions/end_to_end/fixtures/routing/native/standalone_direct_bind_c_f90.f90 @@ -0,0 +1,14 @@ +integer(c_int) function standalone_direct(value) bind(C, name="standalone_direct_symbol") result(output) + use iso_c_binding + integer(c_int), value, intent(in) :: value + + output = value + 2_c_int +end function standalone_direct + +subroutine standalone_output(value, output) bind(C) + use iso_c_binding + integer(c_int), value, intent(in) :: value + integer(c_int), intent(out) :: output + + output = 3_c_int * value +end subroutine standalone_output diff --git a/tests/fortran/functions/end_to_end/fixtures/routing/native/standalone_mixed_bind_c_f90.f90 b/tests/fortran/functions/end_to_end/fixtures/routing/native/standalone_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..b3f0a024d --- /dev/null +++ b/tests/fortran/functions/end_to_end/fixtures/routing/native/standalone_mixed_bind_c_f90.f90 @@ -0,0 +1,13 @@ +integer(c_int) function standalone_direct(value) bind(C, name="standalone_mixed_direct") result(output) + use iso_c_binding + integer(c_int), value, intent(in) :: value + + output = value + 2_c_int +end function standalone_direct + +integer(c_int) function standalone_adapted(value) result(output) + use iso_c_binding + integer(c_int), intent(in) :: value + + output = value + 3_c_int +end function standalone_adapted diff --git a/tests/fortran/functions/end_to_end/test_function_direct_entrypoint_routing.py b/tests/fortran/functions/end_to_end/test_function_direct_entrypoint_routing.py new file mode 100644 index 000000000..c0ecb4756 --- /dev/null +++ b/tests/fortran/functions/end_to_end/test_function_direct_entrypoint_routing.py @@ -0,0 +1,84 @@ +"""Compiled direct and mixed standalone-entrypoint evidence.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_standalone_all_direct_route_uses_native_symbols_without_adapter( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "standalone_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + {"standalone_direct_bind_c_f90_wrapper.c", "standalone_direct_bind_c_f90_wrapper.h"}, + FIXTURES / "contracts" / "standalone_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.standalone_direct(np.int32(4)) == np.int32(6) + assert module.standalone_output(np.int32(4)) == np.int32(12) + + if pyi_parity_build_mode == "source": + binding = (tmp_path / "source_build" / "standalone_direct_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + assert "standalone_direct_symbol" in binding + + +def test_standalone_mixed_route_adapts_only_ordinary_external( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "standalone_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "standalone_mixed_bind_c_f90_wrapper.c", + "standalone_mixed_bind_c_f90_wrapper.h", + "bind_c_standalone_mixed_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "standalone_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.standalone_direct(np.int32(4)) == np.int32(6) + assert module.standalone_adapted(np.int32(4)) == np.int32(7) + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_standalone_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_standalone_adapted" in bridge + assert "standalone_direct" not in bridge + + +def test_standalone_mixed_route_matches_edited_source_free_contract(tmp_path: Path): + stem = "standalone_mixed_bind_c_f90" + source = (FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8") + contract = (FIXTURES / "contracts" / stem / "__init__.pyi").read_text(encoding="utf-8") + contract = contract.replace("from prik.contracts import ", "from prik.contracts import nogil, ") + contract = contract.replace("def standalone_direct(", "@nogil\ndef standalone_direct(").replace( + "def standalone_adapted(", "@nogil\ndef standalone_adapted(" + ) + module, result = _build_inline_pyi_contract_module( + tmp_path, module_name=stem, source_text=source, contract_text=contract + ) + + assert module.standalone_direct(np.int32(4)) == np.int32(6) + assert module.standalone_adapted(np.int32(4)) == np.int32(7) + bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_standalone_adapted" in bridge + assert "function bind_c_standalone_direct" not in bridge diff --git a/tests/fortran/functions/policy/test_native_entrypoint_policy.py b/tests/fortran/functions/policy/test_native_entrypoint_policy.py new file mode 100644 index 000000000..f65e88914 --- /dev/null +++ b/tests/fortran/functions/policy/test_native_entrypoint_policy.py @@ -0,0 +1,112 @@ +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.policy.models import ( + EntrypointOptionalityAction, + EntrypointPassingConvention, + EntrypointProjectionAction, + NativeEntrypointAction, + ScalarLogicalABI, +) +from prik.semantics.fortran2ir import fortran_module_to_semantic_module + + +def _completed_policies(source: str): + parsed = parse_fortran_file(source) + module = fortran_module_to_semantic_module(parsed.modules[0]) + complete_semantic_policies(module) + return {function.name: completed_function_wrapper_policy(function) for function in module.functions} + + +def test_entrypoint_policy_selects_direct_per_operation_and_keeps_ordinary_adapter(): + policies = _completed_policies( + """ +module entrypoints + use iso_c_binding +contains + real(c_double) function direct_value(value) bind(C, name="direct_value_label") result(output) + real(c_double), value, intent(in) :: value + output = value + end function direct_value + + subroutine direct_reference(value) bind(C) + real(c_double), intent(inout) :: value + end subroutine direct_reference + + subroutine ordinary(value) + real(c_double), intent(inout) :: value + end subroutine ordinary +end module entrypoints +""" + ) + + direct_value = policies["direct_value"] + assert direct_value.entrypoint_action is NativeEntrypointAction.DIRECT_C_ABI + assert direct_value.entrypoint_symbol == "direct_value_label" + assert direct_value.arguments[0].entrypoint_passing is EntrypointPassingConvention.C_VALUE + assert direct_value.native_call_slots[0].projection_action is EntrypointProjectionAction.ARGUMENT_DEFAULT + + direct_reference = policies["direct_reference"] + assert direct_reference.entrypoint_action is NativeEntrypointAction.DIRECT_C_ABI + assert direct_reference.arguments[0].entrypoint_passing is EntrypointPassingConvention.POINTER_REFERENCE + + ordinary = policies["ordinary"] + assert ordinary.entrypoint_action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER + assert ordinary.entrypoint_symbol == "" + assert ordinary.entrypoint_diagnostics == ("original procedure has no Fortran C ABI fact",) + + +def test_entrypoint_policy_directs_nonvalue_optional_and_adapts_optional_value(): + policies = _completed_policies( + """ +module optional_entrypoints + use iso_c_binding +contains + subroutine optional_reference(value) bind(C) + real(c_double), optional, intent(in) :: value + end subroutine optional_reference + + subroutine optional_value(value) bind(C) + real(c_double), value, optional, intent(in) :: value + end subroutine optional_value +end module optional_entrypoints +""" + ) + + reference = policies["optional_reference"] + assert reference.entrypoint_action is NativeEntrypointAction.DIRECT_C_ABI + assert reference.arguments[0].entrypoint_passing is EntrypointPassingConvention.NULLABLE_POINTER + assert reference.arguments[0].entrypoint_optionality is EntrypointOptionalityAction.NULL_POINTER + + value = policies["optional_value"] + assert value.entrypoint_action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER + assert value.arguments[0].entrypoint_optionality is EntrypointOptionalityAction.ADAPTER_SIDE_FORTRAN_OMISSION + assert "adapter-side Fortran omission" in value.entrypoint_diagnostics[0] + + +def test_entrypoint_policy_distinguishes_c_bool_from_ordinary_logical_storage(): + policies = _completed_policies( + """ +module logical_entrypoints + use iso_c_binding +contains + logical(c_bool) function direct_bool(value) bind(C) result(output) + logical(c_bool), value, intent(in) :: value + output = value + end function direct_bool + + logical function ordinary_bool(value) result(output) + logical, intent(in) :: value + output = value + end function ordinary_bool +end module logical_entrypoints +""" + ) + + direct = policies["direct_bool"] + assert direct.entrypoint_action is NativeEntrypointAction.DIRECT_C_ABI + assert direct.arguments[0].scalar_logical_abi is ScalarLogicalABI.C_BOOL + + ordinary = policies["ordinary_bool"] + assert ordinary.entrypoint_action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER + assert ordinary.arguments[0].scalar_logical_abi is ScalarLogicalABI.NATIVE_KIND_COPY diff --git a/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py b/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py index 609d7c97e..2c5a5f326 100644 --- a/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py +++ b/tests/fortran/functions/semantics/test_fortran_conversion_procedures_and_interfaces.py @@ -35,8 +35,12 @@ def test_bind_c_name_and_value_calling_convention_reach_semantic_ir(): module = fortran_module_to_semantic_module(parsed.modules[0]) renamed = get_function(module, "renamed") - assert renamed.metadata["fortran_bind_c"] is True - assert renamed.metadata["fortran_bind_c_name"] == "prik_renamed" + assert renamed.origin.source_language == "fortran" + assert renamed.origin.native_name == "renamed" + assert renamed.origin.native_abi == "c" + assert renamed.origin.native_symbol == "prik_renamed" + assert "fortran_bind_c" not in renamed.metadata + assert "fortran_bind_c_name" not in renamed.metadata assert renamed.arguments[0].origin.metadata["value"] is True assert renamed.arguments[0].semantic_type.storage is None diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/__init__.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..00d25fbe1 --- /dev/null +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import generic_interfaces_direct_bind_c_f90 diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/generic_interfaces_direct_bind_c_f90.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/generic_interfaces_direct_bind_c_f90.pyi new file mode 100644 index 000000000..4452092ff --- /dev/null +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_direct_bind_c_f90/generic_interfaces_direct_bind_c_f90.pyi @@ -0,0 +1,55 @@ +from prik.contracts import Addr, Arg, Float64, Int32, Returns, bind, native_abi, native_call, overload + +@native_abi("c") +@bind("direct_convert_integer") +def convert_integer( + value: Int32 +) -> Int32: ... + +@native_abi("c") +@bind("direct_convert_real") +def convert_real( + value: Float64 +) -> Float64: ... + +@native_abi("c") +@bind("direct_increment_integer") +@native_call([Addr(Arg(0))]) +def increment_integer( + value: Int32 +) -> Returns["value", Int32]: ... + +@native_abi("c") +@bind("direct_increment_real") +@native_call([Addr(Arg(0))]) +def increment_real( + value: Float64 +) -> Returns["value", Float64]: ... + +@native_abi("c") +@bind("direct_convert_integer") +@overload("convert_integer") +def convert( + value: Int32 +) -> Int32: ... + +@native_abi("c") +@bind("direct_convert_real") +@overload("convert_real") +def convert( + value: Float64 +) -> Float64: ... + +@native_abi("c") +@bind("direct_increment_integer") +@overload("increment_integer") +def increment( + value: Int32 +) -> Returns["value", Int32]: ... + +@native_abi("c") +@bind("direct_increment_real") +@overload("increment_real") +def increment( + value: Float64 +) -> Returns["value", Float64]: ... diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/__init__.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..62ef2d75f --- /dev/null +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import generic_interfaces_mixed_bind_c_f90 diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/generic_interfaces_mixed_bind_c_f90.pyi b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/generic_interfaces_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..9cfbeffba --- /dev/null +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/contracts/generic_interfaces_mixed_bind_c_f90/generic_interfaces_mixed_bind_c_f90.pyi @@ -0,0 +1,24 @@ +from prik.contracts import Addr, Arg, Float64, Int32, bind, native_abi, native_call, overload + +@native_abi("c") +@bind("mixed_convert_integer") +def convert_integer( + value: Int32 +) -> Int32: ... + +@native_call([Addr(Arg(0))]) +def convert_real( + value: Float64 +) -> Float64: ... + +@native_abi("c") +@bind("mixed_convert_integer") +@overload("convert_integer") +def convert( + value: Int32 +) -> Int32: ... + +@overload("convert_real") +def convert( + value: Float64 +) -> Float64: ... diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/native/generic_interfaces_direct_bind_c_f90.f90 b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/native/generic_interfaces_direct_bind_c_f90.f90 new file mode 100644 index 000000000..c5f89502c --- /dev/null +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/native/generic_interfaces_direct_bind_c_f90.f90 @@ -0,0 +1,38 @@ +module generic_interfaces_direct_bind_c_f90 + use iso_c_binding + implicit none + + interface convert + module procedure convert_integer + module procedure convert_real + end interface convert + + interface increment + module procedure increment_integer + module procedure increment_real + end interface increment +contains + integer(c_int) function convert_integer(value) bind(C, name="direct_convert_integer") result(output) + integer(c_int), value, intent(in) :: value + + output = value + 1_c_int + end function convert_integer + + real(c_double) function convert_real(value) bind(C, name="direct_convert_real") result(output) + real(c_double), value, intent(in) :: value + + output = value + 0.5_c_double + end function convert_real + + subroutine increment_integer(value) bind(C, name="direct_increment_integer") + integer(c_int), intent(inout) :: value + + value = value + 1_c_int + end subroutine increment_integer + + subroutine increment_real(value) bind(C, name="direct_increment_real") + real(c_double), intent(inout) :: value + + value = value + 0.5_c_double + end subroutine increment_real +end module generic_interfaces_direct_bind_c_f90 diff --git a/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/native/generic_interfaces_mixed_bind_c_f90.f90 b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/native/generic_interfaces_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..a53a79fab --- /dev/null +++ b/tests/fortran/generic_interfaces/end_to_end/fixtures/routing/native/generic_interfaces_mixed_bind_c_f90.f90 @@ -0,0 +1,21 @@ +module generic_interfaces_mixed_bind_c_f90 + use iso_c_binding + implicit none + + interface convert + module procedure convert_integer + module procedure convert_real + end interface convert +contains + integer(c_int) function convert_integer(value) bind(C, name="mixed_convert_integer") result(output) + integer(c_int), value, intent(in) :: value + + output = value + 1_c_int + end function convert_integer + + real(c_double) function convert_real(value) result(output) + real(c_double), intent(in) :: value + + output = value + 0.5_c_double + end function convert_real +end module generic_interfaces_mixed_bind_c_f90 diff --git a/tests/fortran/generic_interfaces/end_to_end/test_generic_direct_entrypoint_routing.py b/tests/fortran/generic_interfaces/end_to_end/test_generic_direct_entrypoint_routing.py new file mode 100644 index 000000000..11f37b255 --- /dev/null +++ b/tests/fortran/generic_interfaces/end_to_end/test_generic_direct_entrypoint_routing.py @@ -0,0 +1,94 @@ +"""Compiled per-candidate generic direct and mixed entrypoint evidence.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_generic_all_direct_route_selects_each_candidate_independently( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "generic_interfaces_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "generic_interfaces_direct_bind_c_f90_wrapper.c", + "generic_interfaces_direct_bind_c_f90_wrapper.h", + }, + FIXTURES / "contracts" / "generic_interfaces_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.convert(np.int32(4)) == np.int32(5) + assert module.convert(np.float64(4.0)) == np.float64(4.5) + assert module.increment(np.int32(4)) == np.int32(5) + assert module.increment(np.float64(4.0)) == np.float64(4.5) + + if pyi_parity_build_mode == "source": + binding = (tmp_path / "source_build" / "generic_interfaces_direct_bind_c_f90_wrapper.c").read_text( + encoding="utf-8" + ) + assert "direct_convert_integer" in binding + assert "direct_convert_real" in binding + assert "direct_increment_integer" in binding + assert "direct_increment_real" in binding + + +def test_generic_mixed_route_adapts_only_ordinary_candidate( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "generic_interfaces_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "generic_interfaces_mixed_bind_c_f90_wrapper.c", + "generic_interfaces_mixed_bind_c_f90_wrapper.h", + "bind_c_generic_interfaces_mixed_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "generic_interfaces_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.convert(np.int32(4)) == np.int32(5) + assert module.convert(np.float64(4.0)) == np.float64(4.5) + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_generic_interfaces_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_convert_real" in bridge + assert "convert_integer" not in bridge + + +def test_generic_mixed_route_matches_edited_source_free_contract(tmp_path: Path): + stem = "generic_interfaces_mixed_bind_c_f90" + source = (FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8") + contract = (FIXTURES / "contracts" / stem / f"{stem}.pyi").read_text(encoding="utf-8") + contract = contract.replace("from prik.contracts import ", "from prik.contracts import nogil, ") + contract = contract.replace("def convert_integer(", "@nogil\ndef convert_integer(") + contract = contract.replace("def convert_real(", "@nogil\ndef convert_real(") + contract = contract.replace("def convert(", "@nogil\ndef convert(") + module, result = _build_inline_pyi_contract_module( + tmp_path, module_name=stem, source_text=source, contract_text=contract + ) + + assert module.convert(np.int32(4)) == np.int32(5) + assert module.convert(np.float64(4.0)) == np.float64(4.5) + bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_convert_real" in bridge + assert "function bind_c_convert_integer" not in bridge diff --git a/tests/fortran/infrastructure/codegen/test_consumer_facet_boundaries.py b/tests/fortran/infrastructure/codegen/test_consumer_facet_boundaries.py new file mode 100644 index 000000000..dc9c806fe --- /dev/null +++ b/tests/fortran/infrastructure/codegen/test_consumer_facet_boundaries.py @@ -0,0 +1,258 @@ +"""Strict lowering ownership for ordinary and generated support procedures.""" + +from __future__ import annotations + +import ast +from inspect import getsource +from textwrap import dedent + +import pytest + +from tests.fortran._support.ownership_policy import parse_pyi_text +from prik.codegen.c.binding import CBindingGenerator +from prik.codegen.fortran.bridge import FortranBridgeGenerator +from prik.pipeline.wrapper import WrapperGenerator +from prik.planning import GeneratedSupportProcedureImplementationOwner, WrapperPlanner +from prik.policy.completion import complete_semantic_policies + + +def _plan(source: str, *, module_name: str): + module = parse_pyi_text(source, module_name=module_name) + complete_semantic_policies(module) + return WrapperPlanner().build(module) + + +def _rendered(plan, suffix: str) -> str: + generated = WrapperGenerator().generate(plan) + return next(source.text for source in generated.sources if source.path.suffix == suffix) + + +def _attribute_reads(generator_type: type) -> set[str]: + tree = ast.parse(dedent(getsource(generator_type))) + return {node.attr for node in ast.walk(tree) if isinstance(node, ast.Attribute)} + + +def test_backend_lowerers_cannot_read_the_opposite_plan_facet(): + binding_reads = _attribute_reads(CBindingGenerator) + assert "bridge" not in binding_reads + assert "adapter" not in binding_reads + assert "binding" not in _attribute_reads(FortranBridgeGenerator) + + +def test_ordinary_function_facets_change_only_their_own_consumers(): + source = "def scale(value: Float64) -> Float64: ...\n" + original = _plan(source, module_name="ordinary_facet_boundary") + binding_edit = _plan(source, module_name="ordinary_facet_boundary") + bridge_edit = _plan(source, module_name="ordinary_facet_boundary") + entrypoint_edit = _plan(source, module_name="ordinary_facet_boundary") + + binding_edit.namespaces[0].functions[0].binding.release_gil = True + bridge_edit.namespaces[0].functions[0].bridge.native_name = "renamed_native_scale" + entrypoint_edit.namespaces[0].functions[0].entrypoint.symbol_name = "shared_scale_entrypoint" + + original_c = _rendered(original, ".c") + original_fortran = _rendered(_plan(source, module_name="ordinary_facet_boundary"), ".f90") + + assert _rendered(binding_edit, ".f90") == original_fortran + assert _rendered(bridge_edit, ".c") == original_c + assert "shared_scale_entrypoint" in _rendered(entrypoint_edit, ".c") + assert "shared_scale_entrypoint" in _rendered( + _plan_with_entrypoint_symbol(source, "ordinary_facet_boundary", "shared_scale_entrypoint"), + ".f90", + ) + + +def _plan_with_entrypoint_symbol(source: str, module_name: str, symbol: str): + plan = _plan(source, module_name=module_name) + plan.namespaces[0].functions[0].entrypoint.symbol_name = symbol + return plan + + +def test_fortran_owned_support_procedure_uses_one_shared_entrypoint(): + source = "counter: Int32\n" + plan = _plan(source, module_name="fortran_support_boundary") + procedure = next(item for item in plan.entrypoint.support_procedures if item.role == "module:get") + + assert procedure.implementation_owner is GeneratedSupportProcedureImplementationOwner.FORTRAN + procedure.symbol_name = "shared_counter_getter" + + assert "shared_counter_getter" in _rendered(plan, ".c") + assert "shared_counter_getter" in _rendered( + _edited_support_symbol(source, "fortran_support_boundary", "module:get", "shared_counter_getter"), + ".f90", + ) + + +def _edited_support_symbol(source: str, module_name: str, role: str, symbol: str): + plan = _plan(source, module_name=module_name) + next(item for item in plan.entrypoint.support_procedures if item.role == role).symbol_name = symbol + return plan + + +def test_binding_owned_callback_support_procedure_uses_one_shared_entrypoint(): + source = """ +@prototype +def callback(value: Int32) -> Int32: ... + +def apply(callback: callback, value: Int32) -> Int32: ... +""" + plan = _plan(source, module_name="binding_support_boundary") + callback = plan.namespaces[0].functions[0].arguments[0].callback + procedure = callback.entrypoint.support_procedure + + assert procedure.implementation_owner is GeneratedSupportProcedureImplementationOwner.BINDING + procedure.symbol_name = "shared_callback_trampoline" + + assert "shared_callback_trampoline" in _rendered(plan, ".c") + assert "shared_callback_trampoline" in _rendered( + _edited_callback_symbol(source, "shared_callback_trampoline"), + ".f90", + ) + + +def _edited_callback_symbol(source: str, symbol: str): + plan = _plan(source, module_name="binding_support_boundary") + plan.namespaces[0].functions[0].arguments[0].callback.entrypoint.support_procedure.symbol_name = symbol + return plan + + +def test_external_holder_support_is_distinct_from_binding_local_holder_surfaces(): + plan = _plan( + """ +class item: + value: Int32 + +def consume(value: item) -> Int32: ... +""", + module_name="input_holder_boundary", + ) + roles = { + procedure.role + for procedure in plan.entrypoint.support_procedures + if procedure.owner_path == "input_holder_boundary.item" + } + + assert { + "holder:allocatable:destroy", + "holder:allocatable:present", + "holder:pointer:destroy", + "holder:pointer:present", + } <= roles + assert plan.binding.allocatable_holder_type_owner_paths == () + assert plan.binding.pointer_holder_type_owner_paths == () + assert plan.bridge is not None + assert plan.bridge.allocatable_holder_type_owner_paths == ("input_holder_boundary.item",) + assert plan.bridge.pointer_holder_type_owner_paths == ("input_holder_boundary.item",) + + +@pytest.mark.parametrize( + ("bridge_inventory", "definition_name", "support_role"), + [ + ( + "allocatable_holder_type_owner_paths", + "prik_item_allocatable_holder", + "holder:allocatable:destroy", + ), + ( + "pointer_holder_type_owner_paths", + "prik_item_pointer_holder", + "holder:pointer:destroy", + ), + ], +) +def test_fortran_holder_definitions_are_consumed_from_the_bridge_inventory( + bridge_inventory: str, + definition_name: str, + support_role: str, +): + plan = _plan( + """ +class item: + value: Int32 + +def consume(value: item) -> Int32: ... +""", + module_name="bridge_derived_support_boundary", + ) + procedure = next(item for item in plan.entrypoint.support_procedures if item.role == support_role) + planned_module = FortranBridgeGenerator().visit(plan) + + assert plan.bridge is not None + setattr(plan.bridge, bridge_inventory, ()) + edited_module = FortranBridgeGenerator().visit(plan) + + assert definition_name in {definition.name for definition in planned_module.type_definitions} + assert definition_name not in {definition.name for definition in edited_module.type_definitions} + assert procedure.symbol_name in {function.name for function in edited_module.procedures} + with pytest.raises(ValueError, match="inconsistent-bridge-derived-support-inventory"): + WrapperGenerator().generate(plan) + + +@pytest.mark.parametrize( + ("source", "binding_inventory", "support_role"), + [ + ( + """ +class item: + value: Int32 + +def make() -> item: ... +""", + "owned_derived_type_owner_paths", + "derived:destroy", + ), + ( + """ +from prik.contracts import Allocatable, Arg, Int32, Pointer, Return, Returns, native_call + +class item: + value: Int32 + +@native_call([Allocatable(Arg(0))]) +def update(value: item | None) -> Returns["value", item] | None: ... + +@native_call([], result=Pointer(Return(0))) +def make_pointer() -> item | None: ... +""", + "allocatable_holder_type_owner_paths", + "holder:allocatable:destroy", + ), + ( + """ +from prik.contracts import Allocatable, Arg, Int32, Pointer, Return, Returns, native_call + +class item: + value: Int32 + +@native_call([Allocatable(Arg(0))]) +def update(value: item | None) -> Returns["value", item] | None: ... + +@native_call([], result=Pointer(Return(0))) +def make_pointer() -> item | None: ... +""", + "pointer_holder_type_owner_paths", + "holder:pointer:destroy", + ), + ], +) +def test_binding_local_support_membership_is_consumed_from_its_planned_inventory( + source: str, + binding_inventory: str, + support_role: str, +): + plan = _plan(source, module_name="binding_derived_support_boundary") + procedure = next(item for item in plan.entrypoint.support_procedures if item.role == support_role) + planned_module = CBindingGenerator().binding_module(plan) + + setattr(plan.binding, binding_inventory, ()) + edited_module = CBindingGenerator().binding_module(plan) + + assert procedure.symbol_name in {getattr(declaration, "name", None) for declaration in planned_module.declarations} + assert procedure.symbol_name not in { + getattr(declaration, "name", None) for declaration in edited_module.declarations + } + assert sum(procedure.symbol_name in repr(function) for function in planned_module.functions) > sum( + procedure.symbol_name in repr(function) for function in edited_module.functions + ) + with pytest.raises(ValueError, match="inconsistent-binding-derived-support-inventory"): + WrapperGenerator().generate(plan) diff --git a/tests/fortran/infrastructure/codegen/test_native_entrypoint_routing.py b/tests/fortran/infrastructure/codegen/test_native_entrypoint_routing.py new file mode 100644 index 000000000..00d75921d --- /dev/null +++ b/tests/fortran/infrastructure/codegen/test_native_entrypoint_routing.py @@ -0,0 +1,155 @@ +"""Completed direct/adapted route handoffs across policy, plan, and lowering.""" + +from prik.parsers.fortran import parse_fortran_file +from prik.pipeline.wrapper import WrapperGenerator +from prik.planning import NativeGeneratedCodeGroupKind, WrapperPlanner +from prik.policy import complete_semantic_policies +from prik.policy.models import ( + EntrypointPassingConvention, + EntrypointProjectionAction, + NativeEntrypointAction, +) +from prik.semantics.fortran2ir import fortran_module_to_semantic_module + + +def _plan(source: str): + parsed = parse_fortran_file(source) + module = fortran_module_to_semantic_module(parsed.modules[0]) + complete_semantic_policies(module) + return WrapperPlanner().build(module) + + +def test_direct_plan_keeps_one_projected_sequence_and_no_adapter_facets(): + plan = _plan( + """ +module direct_projection + use iso_c_binding +contains + real(c_double) function scale(value) bind(C, name="direct_scale") result(output) + real(c_double), value, intent(in) :: value + output = value + end function scale +end module direct_projection +""" + ) + function = plan.namespaces[0].functions[0] + argument = function.arguments[0] + slot = function.entrypoint.projected_slots[0] + + assert plan.bridge is None + assert plan.native_generated_code_groups == () + assert function.entrypoint.action is NativeEntrypointAction.DIRECT_C_ABI + assert function.entrypoint.symbol_name == "direct_scale" + assert function.bridge is None + assert argument.projected_call_slot is slot + assert argument.bridge is None + assert slot.adapter is None + assert slot.projection_action is EntrypointProjectionAction.ARGUMENT_DEFAULT + assert slot.passing is EntrypointPassingConvention.C_VALUE + + +def test_adapted_plan_attaches_one_narrow_adapter_to_the_shared_projection(): + plan = _plan( + """ +module adapted_projection +contains + real(8) function scale(value) result(output) + real(8), intent(in) :: value + output = value + end function scale +end module adapted_projection +""" + ) + function = plan.namespaces[0].functions[0] + argument = function.arguments[0] + slot = function.entrypoint.projected_slots[0] + + assert plan.bridge is not None + assert [group.kind for group in plan.native_generated_code_groups] == [ + NativeGeneratedCodeGroupKind.FORTRAN_ADAPTERS + ] + assert plan.native_generated_code_groups[0].member_keys == (function.owner_path,) + assert function.entrypoint.action is NativeEntrypointAction.GENERATED_FORTRAN_ADAPTER + assert function.bridge is not None + assert argument.projected_call_slot is slot + assert slot.adapter is not None + assert slot.passing is EntrypointPassingConvention.C_VALUE + + +def test_direct_and_mixed_lowering_emit_only_selected_fortran_membership(): + plan = _plan( + """ +module selective_lowering + use iso_c_binding +contains + integer(c_int) function direct_value(value) bind(C, name="native_direct_value") result(output) + integer(c_int), value, intent(in) :: value + output = value + end function direct_value + + integer function adapted_value(value) result(output) + integer, intent(in) :: value + output = value + end function adapted_value +end module selective_lowering +""" + ) + generated = WrapperGenerator().generate(plan) + binding = next(source.text for source in generated.sources if source.path.suffix == ".c") + bridge = next(source.text for source in generated.sources if source.path.suffix == ".f90") + + assert "int32_t native_direct_value(int32_t value);" in binding + assert "result = native_direct_value(bound_value);" in binding + assert "bind_c_adapted_value" in bridge + assert "native_adapted_value => adapted_value" in bridge + assert "direct_value" not in bridge.casefold() + assert [group.kind for group in generated.native_generated_code_groups] == [ + NativeGeneratedCodeGroupKind.FORTRAN_ADAPTERS + ] + assert generated.native_generated_code_groups[0].member_keys == ("selective_lowering.adapted_value",) + + +def test_direct_user_operation_and_fortran_support_own_separate_group_membership(): + plan = _plan( + """ +module support_only_lowering + use iso_c_binding + integer(c_int) :: counter = 1_c_int +contains + integer(c_int) function direct_value(value) bind(C) result(output) + integer(c_int), value, intent(in) :: value + output = counter + value + end function direct_value +end module support_only_lowering +""" + ) + + assert [group.kind for group in plan.native_generated_code_groups] == [NativeGeneratedCodeGroupKind.FORTRAN_SUPPORT] + assert all("direct_value" not in key for key in plan.native_generated_code_groups[0].member_keys) + + generated = WrapperGenerator().generate(plan) + bridge = next(source.text for source in generated.sources if source.path.suffix == ".f90") + + assert "bind_c_get_counter" in bridge + assert "direct_value" not in bridge.casefold() + + +def test_all_direct_lowering_assembles_binding_and_header_without_fortran_source(): + generated = WrapperGenerator().generate( + _plan( + """ +module all_direct_lowering + use iso_c_binding +contains + integer(c_int) function value(input) bind(C) result(output) + integer(c_int), value, intent(in) :: input + output = input + end function value +end module all_direct_lowering +""" + ) + ) + + assert generated.bridge_sources == () + assert generated.required_link_languages == ("fortran",) + assert [path.suffix for path in generated.source_paths] == [".c", ".h"] diff --git a/tests/fortran/infrastructure/codegen/test_ordinary_fortran_codegen_baseline.py b/tests/fortran/infrastructure/codegen/test_ordinary_fortran_codegen_baseline.py new file mode 100644 index 000000000..c1a00a47d --- /dev/null +++ b/tests/fortran/infrastructure/codegen/test_ordinary_fortran_codegen_baseline.py @@ -0,0 +1,42 @@ +"""Exact generated-output baseline for an ordinary Fortran procedure.""" + +from __future__ import annotations + +from hashlib import sha256 + +from tests.fortran._support.ownership_policy import parse_pyi_text +from prik.pipeline.wrapper import WrapperGenerator +from prik.planning import WrapperPlanner +from prik.policy.completion import complete_semantic_policies + + +def test_ordinary_fortran_wrapper_preserves_exact_generated_bytes(): + module = parse_pyi_text( + "def scale(value: Float64) -> Float64: ...\n", + module_name="ordinary_entrypoint_baseline", + ) + complete_semantic_policies(module) + + generated = WrapperGenerator().generate(WrapperPlanner().build(module)) + + expected = { + "bind_c_ordinary_entrypoint_baseline_wrapper.f90": ( + 740, + "cdda3f054ab348a128cfc31bb338fe0ec12277d41c607b209c8b401cc2a29004", + ), + "ordinary_entrypoint_baseline_wrapper.c": ( + 1860, + "0401eb6eae8b2b3682a6f04986b8da1553fe77cf070fe4b981e642c7ed13c6d2", + ), + "ordinary_entrypoint_baseline_wrapper.h": ( + 248, + "6b29d016c71f463b5395d8875c1e1f04b98a625241baaf8f85685eee4fe2ce63", + ), + } + actual = { + source.path.name: (len(payload), sha256(payload).hexdigest()) + for source in generated.sources + for payload in (source.text.encode("utf-8"),) + } + + assert actual == expected diff --git a/tests/fortran/infrastructure/codegen/test_planner.py b/tests/fortran/infrastructure/codegen/test_planner.py index fcae02e93..f923c976f 100644 --- a/tests/fortran/infrastructure/codegen/test_planner.py +++ b/tests/fortran/infrastructure/codegen/test_planner.py @@ -158,6 +158,12 @@ def move(self, dx: Float64) -> None: ... with pytest.raises(TypeError): inner.methods_by_owner_path["nested_catalog.outer.inner.shift"] = outer.semantic_class + plan = WrapperPlanner().build(module) + generated = WrapperGenerator().generate(plan) + + assert tuple(derived.type_name for derived in plan.namespaces[0].derived_types) == ("outer", "inner") + assert {source.path.suffix for source in generated.sources} == {".c", ".h", ".f90"} + def test_planner_projects_required_array_buffer_policy(): module = parse_pyi_text( @@ -171,6 +177,29 @@ def sum_values(values: Float64[:]) -> Float64: ... assert WrapperPlanner().build(module).namespaces[0].functions[0].arguments[0].array is not None +def test_planner_directly_projects_three_facets_and_distinct_call_orders(): + function = _hidden_result_plan().namespaces[0].functions[0] + argument = function.arguments[0] + result = function.results[0] + + assert function.entrypoint.symbol_name == "bind_c_scale" + assert [(item.source_kind, item.owner_path) for item in function.entrypoint.parameters] == [ + ("projected_slot", function.entrypoint.projected_slots[0].owner_path), + ("argument", argument.owner_path), + ("projected_slot", function.entrypoint.projected_slots[2].owner_path), + ("hidden_result", result.owner_path), + ] + assert function.entrypoint.results == (result.entrypoint,) + assert argument.entrypoint.handoff_role == "hidden_values.scale.x:value" + assert argument.projected_call_slot is function.entrypoint.projected_slots[1] + assert [slot.source_kind for slot in function.entrypoint.projected_slots] == [ + "literal", + "projection", + "literal", + "result", + ] + + def test_planner_fails_when_post_ir_policy_has_not_completed(): module = parse_pyi_text( """ diff --git a/tests/fortran/infrastructure/pipeline/test_wrapper_generator.py b/tests/fortran/infrastructure/pipeline/test_wrapper_generator.py index 3a4ac8949..52581bf1d 100644 --- a/tests/fortran/infrastructure/pipeline/test_wrapper_generator.py +++ b/tests/fortran/infrastructure/pipeline/test_wrapper_generator.py @@ -218,6 +218,7 @@ def calculate(x: Float64, y: Float64) -> Float64: ... function.binding.python_name = "subtract" function.owner_path = "editable_plan.subtract" function.bridge.native_name = "SUB_R8" + plan.native_generated_code_groups[0].member_keys = (function.owner_path,) generated_wrapper = WrapperGenerator().generate(plan) @@ -227,6 +228,40 @@ def calculate(x: Float64, y: Float64) -> Float64: ... function.bridge.native_name = "ADD_R8" +def test_bridge_only_native_target_edit_cannot_change_c_binding(): + source = """ +@bind("ADD_R8") +@standalone +@native_call([Addr(Arg(0)), Addr(Arg(1))]) +def calculate(x: Float64, y: Float64) -> Float64: ... +""" + original = _plan(source, module_name="bridge_only_edit") + edited = _plan(source, module_name="bridge_only_edit") + edited.namespaces[0].functions[0].bridge.native_name = "SUB_R8" + + original_wrapper = WrapperGenerator().generate(original) + edited_wrapper = WrapperGenerator().generate(edited) + + assert _rendered_source(edited_wrapper, ".c") == _rendered_source(original_wrapper, ".c") + assert "result = ADD_R8(x, y)" in _rendered_source(original_wrapper, ".f90") + assert "result = SUB_R8(x, y)" in _rendered_source(edited_wrapper, ".f90") + + +def test_entrypoint_symbol_edit_changes_both_sides_of_shared_c_abi(): + plan = _plan("def scale(x: Float64) -> Float64: ...", module_name="entrypoint_edit") + function = plan.namespaces[0].functions[0] + function.entrypoint.symbol_name = "custom_scale_entrypoint" + + generated = WrapperGenerator().generate(plan) + c_source = _rendered_source(generated, ".c") + fortran_source = _rendered_source(generated, ".f90") + + assert "custom_scale_entrypoint(bound_x)" in c_source + assert "bind_c_scale(bound_x)" not in c_source + assert "function custom_scale_entrypoint(x) result(result)" in fortran_source + assert 'bind(c, name="custom_scale_entrypoint")' in fortran_source + + def test_backend_visitors_return_complete_nodes_and_printers_freeze_them(): plan = _plan( """ @@ -248,7 +283,7 @@ def scale(x: Float64) -> Float64: ... assert [function.name for function in c_module.functions] == ["wrap_scale", "PyInit_backend_nodes"] assert [prototype.name for prototype in c_header.prototypes] == ["wrap_scale"] assert [procedure.name for procedure in fortran_module.procedures] == ["bind_c_scale"] - assert "result = native_scale(1, x, .false.)" in FortranSourcePrinter().doprint(fortran_module) + assert "result = native_scale(literal_0, x, literal_2)" in FortranSourcePrinter().doprint(fortran_module) CSourcePrinter().doprint(c_module) with pytest.raises(FrozenStageRecordError): c_module.name = "later" @@ -267,7 +302,7 @@ def scale(x: Float64) -> Float64: ... invalid_argument = replace( function.arguments[0], binding=replace(function.arguments[0].binding, optional_mode="x"), - bridge=replace(function.arguments[0].bridge, optional_mode="x"), + entrypoint=replace(function.arguments[0].entrypoint, optional_mode="x"), ) root = plan.namespaces[0] invalid = replace( @@ -304,16 +339,18 @@ def test_generator_rejects_hidden_result_slot_codegen_action_disagreement(): plan = _hidden_result_plan() function = plan.namespaces[0].functions[0] result = function.results[0] - edited_slot = replace(result.native_call_slot, codegen_action=CodegenAction.COPY_OUT) + original_slot = result.projected_call_slot + edited_adapter = replace(original_slot.adapter, codegen_action=CodegenAction.COPY_OUT) + edited_slot = replace(original_slot, adapter=edited_adapter) + projected_slots = tuple( + edited_slot if slot is original_slot else slot for slot in function.entrypoint.projected_slots + ) invalid = _edit_first_function( plan, lambda item: replace( item, - results=(replace(result, native_call_slot=edited_slot),), - native_call_slots=tuple( - edited_slot if slot.native_position == edited_slot.native_position else slot - for slot in item.native_call_slots - ), + results=(replace(result, projected_call_slot=edited_slot),), + entrypoint=replace(item.entrypoint, projected_slots=projected_slots), ), ) @@ -324,7 +361,7 @@ def test_generator_rejects_hidden_result_slot_codegen_action_disagreement(): def test_generator_rejects_argument_native_slot_object_kind_disagreement(): plan = _scalar_plan() argument = plan.namespaces[0].functions[0].arguments[0] - argument.native_call_slot.object_kind = ObjectKind.STRING + argument.projected_call_slot.object_kind = ObjectKind.STRING with pytest.raises(ValueError, match="inconsistent-argument-object-kind"): WrapperGenerator().generate(plan) @@ -333,7 +370,7 @@ def test_generator_rejects_argument_native_slot_object_kind_disagreement(): def test_generator_rejects_result_native_slot_object_kind_disagreement(): plan = _hidden_result_plan() result = plan.namespaces[0].functions[0].results[0] - result.native_call_slot.object_kind = ObjectKind.STRING + result.projected_call_slot.object_kind = ObjectKind.STRING with pytest.raises(ValueError, match="inconsistent-result-object-kind"): WrapperGenerator().generate(plan) @@ -420,8 +457,8 @@ def test_generator_rejects_colliding_generated_namespace_symbols(): arguments=( replace( function.arguments[0], - bridge=replace( - function.arguments[0].bridge, + entrypoint=replace( + function.arguments[0].entrypoint, handoff_role="other:role", ), ), @@ -429,7 +466,7 @@ def test_generator_rejects_colliding_generated_namespace_symbols(): ), ), ), - "inconsistent-bridge-handoff", + "inconsistent-native-handoff", ), ], ) @@ -456,12 +493,12 @@ def test_scalar_address_handoff_plan_edits_fail_before_lowering(edit, diagnostic if edit == "native_action": storage.bridge.native_action = NativeBarrierAction.PASS_VALUE elif edit == "handoff": - storage.bridge.handoff_mode = ArgumentHandoffMode.VALUE + storage.entrypoint.handoff_mode = ArgumentHandoffMode.VALUE elif edit == "data_action": storage.bridge.data_action = BridgeDataAction.COPY_REPRESENTATION storage.bridge.copy_reason = "edited scalar-storage copy" - storage.native_call_slot.bridge_data_action = BridgeDataAction.COPY_REPRESENTATION - storage.native_call_slot.bridge_copy_reason = "edited scalar-storage copy" + storage.projected_call_slot.adapter.bridge_data_action = BridgeDataAction.COPY_REPRESENTATION + storage.projected_call_slot.adapter.bridge_copy_reason = "edited scalar-storage copy" elif edit == "codegen": storage.binding.codegen_action = CodegenAction.SNAPSHOT_COPY else: @@ -485,9 +522,9 @@ def test_bridge_data_action_invariant_rejects_unjustified_or_blocked_plans(actio storage = function.arguments[0] storage.bridge.data_action = action storage.bridge.copy_reason = reason - storage.native_call_slot.bridge_data_action = action - storage.native_call_slot.bridge_copy_reason = reason - assert function.native_call_slots[storage.native_position] is storage.native_call_slot + storage.projected_call_slot.adapter.bridge_data_action = action + storage.projected_call_slot.adapter.bridge_copy_reason = reason + assert function.entrypoint.projected_slots[storage.native_position] is storage.projected_call_slot with pytest.raises(ValueError, match=diagnostic): WrapperGenerator().generate(plan) diff --git a/tests/fortran/infrastructure/printers/test_source_printers.py b/tests/fortran/infrastructure/printers/test_source_printers.py index 36cc563b7..484f8c7aa 100644 --- a/tests/fortran/infrastructure/printers/test_source_printers.py +++ b/tests/fortran/infrastructure/printers/test_source_printers.py @@ -31,7 +31,13 @@ FortranUse, UnsupportedWrapperCodegenNodeError, ) -from prik.planning import BindingModulePlan, BridgeModulePlan, ModulePlan, NamespacePlan +from prik.planning import ( + BindingModulePlan, + BridgeModulePlan, + ModulePlan, + NamespacePlan, + NativeEntrypointModulePlan, +) from prik.printers import CSourcePrinter, FortranSourcePrinter @@ -102,6 +108,7 @@ def test_source_printers_reject_wrapper_plan_models(): plan = ModulePlan( owner_path="demo", binding=BindingModulePlan("demo"), + entrypoint=NativeEntrypointModulePlan("demo"), bridge=BridgeModulePlan("demo"), namespaces=(NamespacePlan(owner_path="demo", python_path=()),), ) diff --git a/tests/fortran/memory_management/codegen/test_native_handle_planning.py b/tests/fortran/memory_management/codegen/test_native_handle_planning.py index 8fab159d6..e09ca175e 100644 --- a/tests/fortran/memory_management/codegen/test_native_handle_planning.py +++ b/tests/fortran/memory_management/codegen/test_native_handle_planning.py @@ -184,8 +184,8 @@ def test_native_handle_plans_keep_datatype_specific_state(): NativeArraySourceKind.POINTER_HANDLE, ) assert normal.native_array_actual.require_contiguous is True - assert normal.bridge.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER - assert normal.array is normal.native_call_slot.array + assert normal.entrypoint.handoff_mode is ArgumentHandoffMode.ARRAY_BUFFER + assert normal.array is normal.projected_call_slot.array alloc = functions["alloc"].arguments[0] pointer = functions["pointer"].arguments[0] @@ -195,7 +195,7 @@ def test_native_handle_plans_keep_datatype_specific_state(): ): handle = argument.native_array_handle assert handle is not None - assert handle is argument.native_call_slot.native_array_handle + assert handle is argument.projected_call_slot.native_array_handle assert handle.descriptor_kind is descriptor_kind assert handle.handoff.abi is NativeDescriptorHandoffABI.FACT_PACKED_CALL_LOCAL assert handle.default_handle.construction is NativeArrayDefaultConstruction.FACT_PACKED_EMPTY @@ -204,12 +204,12 @@ def test_native_handle_plans_keep_datatype_specific_state(): assert NativeArrayOperation.DESTROY in handle.default_handle.operations assert len(handle.handoff.extent_roles) == handle.array.rank == 1 assert argument.binding.python_action is PythonBarrierAction.WRAPPER_INSTANCE - assert argument.bridge.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR + assert argument.entrypoint.handoff_mode is ArgumentHandoffMode.NATIVE_DESCRIPTOR optional = functions["optional"].arguments[0] assert optional.native_array_handle is not None assert optional.native_array_handle.optional_absent is True - assert optional.native_array_handle.handoff.presence_role == optional.bridge.presence_role + assert optional.native_array_handle.handoff.presence_role == optional.entrypoint.presence_role assert alloc.native_array_handle is not None assert alloc.native_array_handle.handoff.presence_role is None @@ -282,8 +282,8 @@ def test_native_handle_plans_keep_datatype_specific_state(): assert pointer_output.native_array_handle is not None assert pointer_output.native_array_handle.descriptor_kind is NativeArrayDescriptorKind.POINTER assert pointer_output.native_array_handle.handoff.abi is NativeDescriptorHandoffABI.OWNED_RESULT_STORAGE - assert pointer_output.native_call_slot is not None - assert pointer_output.native_call_slot.source_kind == "result" + assert pointer_output.projected_call_slot is not None + assert pointer_output.projected_call_slot.source_kind == "result" managed_pointer = functions["make_managed_pointer"].results[0] assert managed_pointer.native_array_handle is not None diff --git a/tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py b/tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py index c5f17d25d..30743a01d 100644 --- a/tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py +++ b/tests/fortran/modules/codegen/test_scalar_module_variable_lowering.py @@ -99,10 +99,10 @@ def test_module_variable_plan_contains_only_completed_dispatch_facts(): assert variables["counter"].binding.initializer == 3 assert variables["target_scale"].bridge.native_assignment is AssignmentMode.VALUE_COPY assert variables["optional_scale"].binding.getter_action is ModuleGetterAction.NULLABLE_SNAPSHOT - assert variables["optional_scale"].bridge.descriptor_kind == "allocatable" + assert variables["optional_scale"].entrypoint.descriptor_kind == "allocatable" assert variables["optional_scale"].binding.setter_action is SetterAction.REJECT_REPLACEMENT assert variables["optional_scale"].bridge.native_assignment is AssignmentMode.NONE - assert variables["selected_scale"].bridge.descriptor_kind == "pointer" + assert variables["selected_scale"].entrypoint.descriptor_kind == "pointer" assert variables["selected_scale"].bridge.native_assignment is AssignmentMode.NONE @@ -112,7 +112,7 @@ def test_symbolic_source_parameter_reuses_scalar_bridge_getter_for_module_initia computed = variables["computed"] assert computed.binding.getter_action is ModuleGetterAction.NATIVE_CONSTANT_VALUE assert computed.binding.constant_value is None - assert computed.bridge.getter_role == "computed_constants.computed:getter" + assert computed.entrypoint.getter_role == "computed_constants.computed:getter" assert computed.binding.setter_action is SetterAction.OMIT artifacts = WrapperGenerator().generate(plan) @@ -169,13 +169,15 @@ def test_module_variable_visitors_consume_their_backend_owned_actions(): ), bridge=replace( counter.bridge, - getter_action=ModuleGetterAction.DIRECT_VALUE, + native_getter_action=ModuleGetterAction.DIRECT_VALUE, native_assignment=AssignmentMode.VALUE_COPY, ), ) assert CBindingGenerator().visit(split_actions) == () - assert [procedure.name for procedure in FortranBridgeGenerator().visit(split_actions)] == [ + bridge = FortranBridgeGenerator() + bridge.visit(plan) + assert [procedure.name for procedure in bridge.visit(split_actions)] == [ "bind_c_get_counter", "bind_c_set_counter", ] @@ -188,8 +190,10 @@ def test_fortran_module_setter_rejects_unsupported_bridge_assignment(): ) invalid = replace(counter, bridge=replace(counter.bridge, native_assignment=AssignmentMode.ALIAS)) + bridge = FortranBridgeGenerator() + bridge.visit(plan) with pytest.raises(ValueError, match="Unsupported Fortran module setter assignment"): - FortranBridgeGenerator().visit(invalid) + bridge.visit(invalid) @pytest.mark.parametrize( @@ -261,37 +265,82 @@ def test_module_variable_generators_dispatch_get_set_and_rejection_from_plan(): assert "selected_scale = value" not in fortran_source -def test_generator_rejects_python_module_setter_without_bridge_handoff(): +def test_generated_support_procedure_symbol_is_shared_by_both_boundary_lowerers(): plan = _plan() - counter = next( - variable for variable in plan.namespaces[0].variables if variable.binding.python_names == ("counter",) + procedure = next( + item + for item in plan.entrypoint.support_procedures + if item.owner_path == "scalar_state.counter" and item.role == "module:set" ) - invalid_counter = replace(counter, bridge=replace(counter.bridge, setter_role=None)) - invalid = replace( + renamed = replace(procedure, symbol_name="planned_counter_assignment") + edited = replace( plan, - namespaces=( - replace( - plan.namespaces[0], - variables=tuple( - invalid_counter if variable is counter else variable for variable in plan.namespaces[0].variables - ), + entrypoint=replace( + plan.entrypoint, + support_procedures=tuple( + renamed if item is procedure else item for item in plan.entrypoint.support_procedures ), ), ) - with pytest.raises(ValueError, match="missing-module-setter-role"): - WrapperGenerator().generate(invalid) + artifacts = WrapperGenerator().generate(edited) + c_source = _source(artifacts, ".c") + fortran_source = _source(artifacts, ".f90") + assert "void planned_counter_assignment(int32_t value);" in c_source + assert "planned_counter_assignment(value);" in c_source + assert "subroutine planned_counter_assignment(value)" in fortran_source + assert 'bind(c, name="planned_counter_assignment")' in fortran_source -def test_generator_rejects_binding_bridge_module_getter_disagreement(): + +def test_missing_generated_support_procedure_fails_before_lowering(): plan = _plan() + edited = replace( + plan, + entrypoint=replace( + plan.entrypoint, + support_procedures=tuple( + item + for item in plan.entrypoint.support_procedures + if not (item.owner_path == "scalar_state.counter" and item.role == "module:set") + ), + ), + ) + + with pytest.raises(ValueError, match="incomplete-auxiliary-entrypoint-inventory"): + WrapperGenerator().generate(edited) + + +def test_bridge_local_module_target_edit_does_not_change_the_c_boundary(): + plan = _plan() + baseline = _source(WrapperGenerator().generate(plan), ".c") counter = next( variable for variable in plan.namespaces[0].variables if variable.binding.python_names == ("counter",) ) - invalid_counter = replace( + edited_counter = replace( counter, - bridge=replace(counter.bridge, getter_action=ModuleGetterAction.NULLABLE_SNAPSHOT), + bridge=replace(counter.bridge, native_name="counter_alternate"), ) + root = replace( + plan.namespaces[0], + variables=tuple( + edited_counter if variable is counter else variable for variable in plan.namespaces[0].variables + ), + ) + edited = replace(plan, namespaces=(root, *plan.namespaces[1:])) + + artifacts = WrapperGenerator().generate(edited) + + assert _source(artifacts, ".c") == baseline + assert "native_counter => counter_alternate" in _source(artifacts, ".f90") + + +def test_generator_rejects_python_module_setter_without_bridge_handoff(): + plan = _plan() + counter = next( + variable for variable in plan.namespaces[0].variables if variable.binding.python_names == ("counter",) + ) + invalid_counter = replace(counter, entrypoint=replace(counter.entrypoint, setter_role=None)) invalid = replace( plan, namespaces=( @@ -304,5 +353,5 @@ def test_generator_rejects_binding_bridge_module_getter_disagreement(): ), ) - with pytest.raises(ValueError, match="inconsistent-module-getter-action"): + with pytest.raises(ValueError, match="missing-module-setter-role"): WrapperGenerator().generate(invalid) diff --git a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/__init__.pyi b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..2ccea4308 --- /dev/null +++ b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import modules_direct_bind_c_f90 diff --git a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/modules_direct_bind_c_f90.pyi b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/modules_direct_bind_c_f90.pyi new file mode 100644 index 000000000..fea5b1f7f --- /dev/null +++ b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_direct_bind_c_f90/modules_direct_bind_c_f90.pyi @@ -0,0 +1,15 @@ +from prik.contracts import Final, Int32, native_abi + +limit: Final[Int32] = 12 + +counter: Int32 + +@native_abi("c") +def direct_total( + value: Int32 +) -> Int32: ... + +@native_abi("c") +def direct_set_counter( + value: Int32 +) -> None: ... diff --git a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/__init__.pyi b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..f2ea0d701 --- /dev/null +++ b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import modules_mixed_bind_c_f90 diff --git a/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/modules_mixed_bind_c_f90.pyi b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/modules_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..9a85fa681 --- /dev/null +++ b/tests/fortran/modules/end_to_end/fixtures/routing/contracts/modules_mixed_bind_c_f90/modules_mixed_bind_c_f90.pyi @@ -0,0 +1,13 @@ +from prik.contracts import Addr, Arg, Int32, native_abi, native_call + +counter: Int32 + +@native_abi("c") +def direct_total( + value: Int32 +) -> Int32: ... + +@native_call([Addr(Arg(0))]) +def adapted_total( + value: Int32 +) -> Int32: ... diff --git a/tests/fortran/modules/end_to_end/fixtures/routing/native/modules_direct_bind_c_f90.f90 b/tests/fortran/modules/end_to_end/fixtures/routing/native/modules_direct_bind_c_f90.f90 new file mode 100644 index 000000000..07d24e0f7 --- /dev/null +++ b/tests/fortran/modules/end_to_end/fixtures/routing/native/modules_direct_bind_c_f90.f90 @@ -0,0 +1,19 @@ +module modules_direct_bind_c_f90 + use iso_c_binding + implicit none + + integer(c_int), parameter :: limit = 12_c_int + integer(c_int) :: counter = 3_c_int +contains + integer(c_int) function direct_total(value) bind(C) result(total) + integer(c_int), value, intent(in) :: value + + total = counter + value + end function direct_total + + subroutine direct_set_counter(value) bind(C) + integer(c_int), value, intent(in) :: value + + counter = value + end subroutine direct_set_counter +end module modules_direct_bind_c_f90 diff --git a/tests/fortran/modules/end_to_end/fixtures/routing/native/modules_mixed_bind_c_f90.f90 b/tests/fortran/modules/end_to_end/fixtures/routing/native/modules_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..7ca0c81a9 --- /dev/null +++ b/tests/fortran/modules/end_to_end/fixtures/routing/native/modules_mixed_bind_c_f90.f90 @@ -0,0 +1,18 @@ +module modules_mixed_bind_c_f90 + use iso_c_binding + implicit none + + integer(c_int) :: counter = 3_c_int +contains + integer(c_int) function direct_total(value) bind(C) result(total) + integer(c_int), value, intent(in) :: value + + total = counter + value + end function direct_total + + integer(c_int) function adapted_total(value) result(total) + integer(c_int), intent(in) :: value + + total = counter + value + end function adapted_total +end module modules_mixed_bind_c_f90 diff --git a/tests/fortran/modules/end_to_end/test_module_direct_entrypoint_routing.py b/tests/fortran/modules/end_to_end/test_module_direct_entrypoint_routing.py new file mode 100644 index 000000000..fe62b40e3 --- /dev/null +++ b/tests/fortran/modules/end_to_end/test_module_direct_entrypoint_routing.py @@ -0,0 +1,106 @@ +"""Compiled direct procedures with independently generated module-state support.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_module_state_direct_route_emits_support_only_fortran( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "modules_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "modules_direct_bind_c_f90_wrapper.c", + "modules_direct_bind_c_f90_wrapper.h", + "bind_c_modules_direct_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "modules_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.limit == np.int32(12) + assert module.counter == np.int32(3) + assert module.direct_total(np.int32(4)) == np.int32(7) + module.direct_set_counter(np.int32(9)) + assert module.counter == np.int32(9) + assert module.direct_total(np.int32(4)) == np.int32(13) + + if pyi_parity_build_mode == "source": + support = ( + (tmp_path / "source_build" / "bind_c_modules_direct_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_get_counter" in support + assert "bind_c_set_counter" in support + assert "direct_total" not in support + assert "direct_set_counter" not in support + + +def test_module_state_mixed_route_separates_adapter_and_support_membership( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "modules_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "modules_mixed_bind_c_f90_wrapper.c", + "modules_mixed_bind_c_f90_wrapper.h", + "bind_c_modules_mixed_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "modules_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.direct_total(np.int32(4)) == np.int32(7) + assert module.adapted_total(np.int32(4)) == np.int32(7) + module.counter = np.int32(8) + assert module.direct_total(np.int32(4)) == np.int32(12) + assert module.adapted_total(np.int32(4)) == np.int32(12) + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_modules_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_get_counter" in bridge + assert "bind_c_adapted_total" in bridge + assert "direct_total" not in bridge + + +def test_module_state_mixed_route_matches_edited_source_free_contract(tmp_path: Path): + stem = "modules_mixed_bind_c_f90" + source = (FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8") + contract = (FIXTURES / "contracts" / stem / f"{stem}.pyi").read_text(encoding="utf-8") + contract = contract.replace("from prik.contracts import ", "from prik.contracts import nogil, ") + contract = contract.replace("def direct_total(", "@nogil\ndef direct_total(").replace( + "def adapted_total(", "@nogil\ndef adapted_total(" + ) + module, result = _build_inline_pyi_contract_module( + tmp_path, module_name=stem, source_text=source, contract_text=contract + ) + + assert module.direct_total(np.int32(4)) == np.int32(7) + assert module.adapted_total(np.int32(4)) == np.int32(7) + module.counter = np.int32(8) + assert module.direct_total(np.int32(4)) == np.int32(12) + bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_get_counter" in bridge + assert "bind_c_adapted_total" in bridge + assert "function bind_c_direct_total" not in bridge diff --git a/tests/fortran/optional_arguments/codegen/test_optional_lowering.py b/tests/fortran/optional_arguments/codegen/test_optional_lowering.py index d7911cbd8..fcdac1381 100644 --- a/tests/fortran/optional_arguments/codegen/test_optional_lowering.py +++ b/tests/fortran/optional_arguments/codegen/test_optional_lowering.py @@ -41,7 +41,7 @@ def test_optional_scalar_lowering_distinguishes_absent_or_none_from_value(): factor = plan.namespaces[0].functions[0].arguments[1] assert factor.binding.optional_mode is OptionalMode.NULLABLE_VALUE - assert factor.bridge.optional_mode is OptionalMode.NULLABLE_VALUE + assert factor.entrypoint.optional_mode is OptionalMode.NULLABLE_VALUE artifacts = WrapperGenerator().generate(plan) c_source = _source(artifacts, ".c") fortran_source = _source(artifacts, ".f90") @@ -69,7 +69,7 @@ def alloc_state(value: Annotated[Float64, Immutable] | None = ...) -> Int32: ... value = plan.namespaces[0].functions[0].arguments[0] assert value.binding.optional_mode is OptionalMode.DESCRIPTOR - assert value.bridge.presence_role == "scalar_optional_descriptors.alloc_state.value:present" + assert value.entrypoint.presence_role == "scalar_optional_descriptors.alloc_state.value:present" assert value.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION assert value.bridge.copy_reason == "materialize owned Fortran allocatable scalar storage from the binding value" artifacts = WrapperGenerator().generate(plan) @@ -87,7 +87,7 @@ def alloc_state(value: Annotated[Float64, Immutable] | None = ...) -> Int32: ... assert "result = native_alloc_state()" in fortran_source -def test_optional_arguments_with_hidden_literals_fail_during_shared_plan_validation(): +def test_optional_arguments_with_hidden_literals_materialize_the_literal_in_the_binding(): module = parse_pyi_text( """ @native_call([Int32(1), Arg(0)]) @@ -95,10 +95,14 @@ def optional_literal(value: Annotated[Float64, Immutable] | None = ...) -> Float """, module_name="optional_literal", ) - complete_semantic_policies(module) + artifacts = _artifacts(module) + c_source = _source(artifacts, ".c") + fortran_source = _source(artifacts, ".f90") - with pytest.raises(ValueError, match="optional-native-literal-combination"): - WrapperGenerator().generate(WrapperPlanner().build(module)) + assert "double bind_c_optional_literal(int32_t literal_0, double * value);" in c_source + assert "bind_c_optional_literal(1, bound_value_nullable);" in c_source + assert "function bind_c_optional_literal(literal_0, bound_value)" in fortran_source + assert "native_optional_literal(literal_0, value=value)" in fortran_source def test_required_descriptor_keeps_python_presence_separate_from_native_state_and_copyout(): @@ -114,9 +118,9 @@ def update(value: Float64 | None) -> Returns["value", Float64] | None: ... value = plan.namespaces[0].functions[0].arguments[0] assert value.binding.optional_mode is OptionalMode.REQUIRED_DESCRIPTOR - assert value.bridge.presence_role is None - assert value.bridge.descriptor_output_role == f"{value.owner_path}:descriptor-output" - assert value.bridge.descriptor_output_presence_role == f"{value.owner_path}:descriptor-output-present" + assert value.entrypoint.presence_role is None + assert value.entrypoint.descriptor_output_role == f"{value.owner_path}:descriptor-output" + assert value.entrypoint.descriptor_output_presence_role == f"{value.owner_path}:descriptor-output-present" artifacts = WrapperGenerator().generate(plan) c_source = _source(artifacts, ".c") @@ -145,7 +149,7 @@ def alloc_state(value: Annotated[Float64, Immutable] | None = ...) -> Int32: ... plan = WrapperPlanner().build(module) function = plan.namespaces[0].functions[0] argument = function.arguments[0] - invalid_argument = replace(argument, bridge=replace(argument.bridge, presence_role=None)) + invalid_argument = replace(argument, entrypoint=replace(argument.entrypoint, presence_role=None)) invalid = _replace_root_function(plan, replace(function, arguments=(invalid_argument,))) with pytest.raises(ValueError, match="missing-descriptor-presence-role"): diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/__init__.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..9386e4b6a --- /dev/null +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import optional_arguments_direct_bind_c_f90 diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/optional_arguments_direct_bind_c_f90.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/optional_arguments_direct_bind_c_f90.pyi new file mode 100644 index 000000000..7177288c4 --- /dev/null +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_direct_bind_c_f90/optional_arguments_direct_bind_c_f90.pyi @@ -0,0 +1,13 @@ +from prik.contracts import Addr, Arg, Float64, Int32, Return, native_abi, native_call + +@native_abi("c") +@native_call([Addr(Arg(0))]) +def optional_state( + value: Float64 = ... +) -> Int32: ... + +@native_abi("c") +@native_call([Addr(Arg(0)), Return('total', 0)]) +def add_optional( + value: Float64 = ... +) -> Float64: ... diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/__init__.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..409f07adb --- /dev/null +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import optional_arguments_mixed_bind_c_f90 diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/optional_arguments_mixed_bind_c_f90.pyi b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/optional_arguments_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..3fdcc4893 --- /dev/null +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/contracts/optional_arguments_mixed_bind_c_f90/optional_arguments_mixed_bind_c_f90.pyi @@ -0,0 +1,11 @@ +from prik.contracts import Addr, Arg, Float64, Int32, native_abi, native_call + +@native_abi("c") +@native_call([Addr(Arg(0))]) +def direct_optional_state( + value: Float64 = ... +) -> Int32: ... + +def adapted_optional_value_state( + value: Float64 = ... +) -> Int32: ... diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/native/optional_arguments_direct_bind_c_f90.f90 b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/native/optional_arguments_direct_bind_c_f90.f90 new file mode 100644 index 000000000..4b64b1fc4 --- /dev/null +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/native/optional_arguments_direct_bind_c_f90.f90 @@ -0,0 +1,24 @@ +module optional_arguments_direct_bind_c_f90 + use iso_c_binding +contains + integer(c_int) function optional_state(value) bind(C) result(state) + real(c_double), optional, intent(in) :: value + + if (present(value)) then + state = 1_c_int + else + state = 0_c_int + end if + end function optional_state + + subroutine add_optional(value, total) bind(C) + real(c_double), optional, intent(in) :: value + real(c_double), intent(out) :: total + + if (present(value)) then + total = 4.0_c_double + value + else + total = 4.0_c_double + end if + end subroutine add_optional +end module optional_arguments_direct_bind_c_f90 diff --git a/tests/fortran/optional_arguments/end_to_end/fixtures/routing/native/optional_arguments_mixed_bind_c_f90.f90 b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/native/optional_arguments_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..0049f4702 --- /dev/null +++ b/tests/fortran/optional_arguments/end_to_end/fixtures/routing/native/optional_arguments_mixed_bind_c_f90.f90 @@ -0,0 +1,23 @@ +module optional_arguments_mixed_bind_c_f90 + use iso_c_binding +contains + integer(c_int) function direct_optional_state(value) bind(C) result(state) + real(c_double), optional, intent(in) :: value + + if (present(value)) then + state = 1_c_int + else + state = 0_c_int + end if + end function direct_optional_state + + integer(c_int) function adapted_optional_value_state(value) result(state) + real(c_double), value, optional, intent(in) :: value + + if (present(value)) then + state = 2_c_int + else + state = 0_c_int + end if + end function adapted_optional_value_state +end module optional_arguments_mixed_bind_c_f90 diff --git a/tests/fortran/optional_arguments/end_to_end/test_optional_direct_entrypoint_routing.py b/tests/fortran/optional_arguments/end_to_end/test_optional_direct_entrypoint_routing.py new file mode 100644 index 000000000..0bfe3d406 --- /dev/null +++ b/tests/fortran/optional_arguments/end_to_end/test_optional_direct_entrypoint_routing.py @@ -0,0 +1,98 @@ +"""Compiled direct and mixed optional-scalar entrypoint evidence.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_optional_all_direct_route_distinguishes_absent_none_and_present( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "optional_arguments_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "optional_arguments_direct_bind_c_f90_wrapper.c", + "optional_arguments_direct_bind_c_f90_wrapper.h", + }, + FIXTURES / "contracts" / "optional_arguments_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.optional_state() == np.int32(0) + assert module.optional_state(None) == np.int32(0) + assert module.optional_state(np.float64(1.5)) == np.int32(1) + assert module.add_optional(None) == np.float64(4.0) + assert module.add_optional(np.float64(1.5)) == np.float64(5.5) + + if pyi_parity_build_mode == "source": + binding = (tmp_path / "source_build" / "optional_arguments_direct_bind_c_f90_wrapper.c").read_text( + encoding="utf-8" + ) + assert "int32_t optional_state(double * value);" in binding + assert "void add_optional(double * value, double * total);" in binding + + +def test_optional_mixed_route_adapts_only_optional_value_dummy( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "optional_arguments_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "optional_arguments_mixed_bind_c_f90_wrapper.c", + "optional_arguments_mixed_bind_c_f90_wrapper.h", + "bind_c_optional_arguments_mixed_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "optional_arguments_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.direct_optional_state() == np.int32(0) + assert module.direct_optional_state(np.float64(2.0)) == np.int32(1) + assert module.adapted_optional_value_state() == np.int32(0) + assert module.adapted_optional_value_state(None) == np.int32(0) + assert module.adapted_optional_value_state(np.float64(2.0)) == np.int32(2) + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_optional_arguments_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_adapted_optional_value_state" in bridge + assert "direct_optional_state" not in bridge + + +def test_optional_mixed_route_matches_edited_source_free_contract(tmp_path: Path): + stem = "optional_arguments_mixed_bind_c_f90" + source = (FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8") + contract = (FIXTURES / "contracts" / stem / f"{stem}.pyi").read_text(encoding="utf-8") + contract = contract.replace("from prik.contracts import ", "from prik.contracts import nogil, ") + contract = contract.replace("def direct_optional_state(", "@nogil\ndef direct_optional_state(").replace( + "def adapted_optional_value_state(", "@nogil\ndef adapted_optional_value_state(" + ) + module, result = _build_inline_pyi_contract_module( + tmp_path, module_name=stem, source_text=source, contract_text=contract + ) + + assert module.direct_optional_state() == np.int32(0) + assert module.direct_optional_state(np.float64(2.0)) == np.int32(1) + assert module.adapted_optional_value_state() == np.int32(0) + assert module.adapted_optional_value_state(np.float64(2.0)) == np.int32(2) + bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_adapted_optional_value_state" in bridge + assert "function bind_c_direct_optional_state" not in bridge diff --git a/tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py b/tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py index 267b99887..13fbaaafc 100644 --- a/tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py +++ b/tests/fortran/optional_arguments/end_to_end/test_optional_runtime.py @@ -102,7 +102,7 @@ def test_optional_scalar_descriptors_distinguish_omitted_none_and_value(tmp_path with pytest.raises(TypeError): function("bad") plan_c = (result.output_dir / "scalar_optional_descriptors_wrapper.c").read_text(encoding="utf-8") - assert "int32_t bind_c_alloc_state(void * value, void * value_present);" in plan_c + assert "int32_t bind_c_alloc_state(double * value, void * value_present);" in plan_c assert "Omit to make the native optional dummy absent." in module.alloc_state.__doc__ assert "Pass None for a present unallocated or unassociated descriptor." in module.alloc_state.__doc__ assert "Default is None." not in module.alloc_state.__doc__ diff --git a/tests/fortran/pointers/semantics/test_pointer_semantics.py b/tests/fortran/pointers/semantics/test_pointer_semantics.py index 645f0e1ed..cfaacccc5 100644 --- a/tests/fortran/pointers/semantics/test_pointer_semantics.py +++ b/tests/fortran/pointers/semantics/test_pointer_semantics.py @@ -36,6 +36,23 @@ def test_fortran_pointer_arrays_and_scalars_preserve_descriptor_semantics(): assert scalar.semantic_type.storage.pointer_depth == 1 +def test_fortran_optional_array_descriptor_preserves_absent_handle_state(): + source = """ +module optional_descriptor_semantics +contains + subroutine inspect(values) + real(8), allocatable, optional, intent(in) :: values(:) + end subroutine inspect +end module optional_descriptor_semantics +""" + + module = fortran_module_to_semantic_module(parse_fortran_source(source)) + values = get_function(module, "inspect").arguments[0] + + assert values.optional is True + assert values.semantic_type.metadata[OPTIONAL_ABSENT_HANDLE_METADATA] is True + + def test_pyi_pointer_handles_preserve_rank_optionality_and_scalar_state(): module = parse_pyi_text( """ diff --git a/tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py b/tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py index 29d4edc1f..280acd744 100644 --- a/tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py +++ b/tests/fortran/pyi_contracts/calls_and_results/codegen/test_call_and_result_lowering.py @@ -47,7 +47,7 @@ def swap_args(x: Float64, y: Float64) -> Float64: ... DatatypeFamily.REAL, DatatypeFamily.REAL, ] - assert reordered.arguments[0].native_call_slot.codegen_action is CodegenAction.CALL_LOCAL_INPUT + assert reordered.arguments[0].projected_call_slot.adapter.codegen_action is CodegenAction.CALL_LOCAL_INPUT hidden = ( _plan( @@ -60,16 +60,18 @@ def scale(x: Float64) -> Float64: ... .namespaces[0] .functions[0] ) - assert [(slot.source_kind, slot.literal_type, slot.literal_value) for slot in hidden.native_call_slots] == [ + assert [ + (slot.source_kind, slot.literal_type, slot.literal_value) for slot in hidden.entrypoint.projected_slots + ] == [ ("literal", "Int32", 1), ("projection", None, None), ("literal", "Bool", False), ("result", None, None), ] assert hidden.results[0].source_kind == "hidden_output" - assert hidden.results[0].bridge.abi_position == 3 + assert hidden.results[0].projected_call_slot.native_position == 3 assert hidden.results[0].bridge.native_action is NativeBarrierAction.PASS_CALL_LOCAL_ADDRESS - assert hidden.results[0].native_call_slot.object_kind is ObjectKind.SCALAR + assert hidden.results[0].projected_call_slot.object_kind is ObjectKind.SCALAR @pytest.mark.parametrize( diff --git a/tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_projected_entrypoint_routes.py b/tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_projected_entrypoint_routes.py new file mode 100644 index 000000000..50b3fb925 --- /dev/null +++ b/tests/fortran/pyi_contracts/calls_and_results/end_to_end/test_projected_entrypoint_routes.py @@ -0,0 +1,100 @@ +"""Compiled binding-owned projection sequences for direct and adapted targets.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_inline_pyi_contract_module + +pytestmark = pytest.mark.fortran_end_to_end + + +def _projection_source(module_name: str, *, bind_c: bool) -> str: + binding = ' bind(C, name="projected_native")' if bind_c else "" + output_binding = ' bind(C, name="projected_output_native")' if bind_c else "" + return f""" +module {module_name} + use iso_c_binding +contains + integer(c_int) function projected(right, left, bias){binding} result(output) + integer(c_int), value, intent(in) :: right + integer(c_int), intent(in) :: left + integer(c_int), value, intent(in) :: bias + output = 100_c_int * right + 10_c_int * left + bias + end function projected + + subroutine projected_output(right, left, bias, output){output_binding} + integer(c_int), value, intent(in) :: right + integer(c_int), intent(in) :: left + integer(c_int), value, intent(in) :: bias + integer(c_int), intent(out) :: output + output = 100_c_int * right + 10_c_int * left + bias + end subroutine projected_output +end module {module_name} +""" + + +def _projection_contract(*, direct: bool) -> str: + marker = '@native_abi("c")\n' if direct else "" + native_name = "projected_native" if direct else "projected" + output_native_name = "projected_output_native" if direct else "projected_output" + return f""" +from prik.contracts import Addr, Arg, Int32, Return, Value, bind, native_abi, native_call + +{marker}@bind("{native_name}") +@native_call([Value(Arg(1)), Addr(Arg(0)), Int32(5)]) +def projected(left: Int32, right: Int32) -> Int32: ... + +{marker}@bind("{output_native_name}") +@native_call([Value(Arg(1)), Addr(Arg(0)), Int32(5), Return("output", 0)]) +def projected_output(left: Int32, right: Int32) -> Int32: ... +""" + + +def test_direct_projection_reorders_value_and_address_actuals_and_materializes_literal(tmp_path: Path): + module, result = _build_inline_pyi_contract_module( + tmp_path, + module_name="direct_projection_runtime", + source_text=_projection_source("direct_projection_runtime", bind_c=True), + contract_text=_projection_contract(direct=True), + ) + + 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) + assert {path.name for path in result.generated_sources} == { + "direct_projection_runtime_wrapper.c", + "direct_projection_runtime_wrapper.h", + } + binding = (result.output_dir / "direct_projection_runtime_wrapper.c").read_text(encoding="utf-8") + assert "int32_t projected_native(int32_t right, int32_t * left, int32_t literal_2);" in binding + assert "result = projected_native(bound_right, &bound_left, 5);" in binding + assert ( + "void projected_output_native(int32_t right, int32_t * left, int32_t literal_2, int32_t * output);" in binding + ) + assert "projected_output_native(bound_right, &bound_left, 5, &output);" in binding + + +def test_adapted_projection_uses_the_same_binding_owned_actual_sequence(tmp_path: Path): + module, result = _build_inline_pyi_contract_module( + tmp_path, + module_name="adapted_projection_runtime", + source_text=_projection_source("adapted_projection_runtime", bind_c=False), + contract_text=_projection_contract(direct=False), + ) + + 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) + assert {path.name for path in result.generated_sources} == { + "adapted_projection_runtime_wrapper.c", + "adapted_projection_runtime_wrapper.h", + "bind_c_adapted_projection_runtime_wrapper.f90", + } + binding = (result.output_dir / "adapted_projection_runtime_wrapper.c").read_text(encoding="utf-8") + bridge = (result.output_dir / "bind_c_adapted_projection_runtime_wrapper.f90").read_text(encoding="utf-8") + assert "bind_c_projected(bound_right, &bound_left, 5)" in binding + assert "bind_c_projected_output(bound_right, &bound_left, 5, &output)" in binding + assert "function bind_c_projected(right, left, literal_2)" in bridge + 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 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 ba5fd9717..19efd7a76 100644 --- a/tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py +++ b/tests/fortran/raw_addresses/codegen/test_raw_array_lowering.py @@ -58,8 +58,8 @@ def test_raw_array_addresses_use_one_shared_transfer_and_shape_plan(): module = _raw_array_module() function = _functions(WrapperPlanner().build(module))["raw_vector"] argument = function.arguments[1] - assert argument.native_call_slot is function.native_call_slots[argument.native_position] - assert argument.array is argument.native_call_slot.array + assert argument.projected_call_slot is function.entrypoint.projected_slots[argument.native_position] + assert argument.array is argument.projected_call_slot.array assert argument.object_kind is ObjectKind.NUMPY_ARRAY assert argument.ownership_owner is OwnershipOwner.CALLER assert argument.transfer_mode is TransferMode.IN_PLACE @@ -70,7 +70,7 @@ def test_raw_array_addresses_use_one_shared_transfer_and_shape_plan(): assert argument.binding.python_action is PythonBarrierAction.RAW_ADDRESS assert argument.binding.codegen_action is CodegenAction.IN_PLACE_ARGUMENT assert argument.bridge.native_action is NativeBarrierAction.PASS_RAW_ADDRESS - assert argument.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS + assert argument.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS assert argument.bridge.data_action is BridgeDataAction.ASSOCIATE_VIEW assert argument.bridge.copy_reason is None @@ -80,7 +80,7 @@ def test_raw_array_addresses_use_one_shared_transfer_and_shape_plan(): assert argument.array.axes == ("dense",) assert argument.array.contiguous is True assert argument.array.category == "raw_address" - assert argument.array.data_role == argument.binding.handoff_role + assert argument.array.data_role == argument.entrypoint.handoff_role assert argument.array.extent_reference_roles == (("raw_array_addresses.raw_vector.n:value",),) assert argument.array.extent_roles == () assert argument.array.upper_bound_roles == () @@ -150,12 +150,12 @@ def test_raw_array_plan_edits_fail_before_backend_lowering(edit: str, diagnostic argument.binding.python_action = PythonBarrierAction.STRING_VALUE elif edit == "native-action": argument.bridge.native_action = NativeBarrierAction.PASS_ARRAY_BUFFER - argument.native_call_slot.native_action = NativeBarrierAction.PASS_ARRAY_BUFFER + argument.projected_call_slot.adapter.native_action = NativeBarrierAction.PASS_ARRAY_BUFFER elif edit == "handoff": - argument.bridge.handoff_mode = ArgumentHandoffMode.ARRAY_BUFFER + argument.entrypoint.handoff_mode = ArgumentHandoffMode.ARRAY_BUFFER elif edit == "data-action": argument.bridge.data_action = BridgeDataAction.DIRECT_TRANSFER - argument.native_call_slot.bridge_data_action = BridgeDataAction.DIRECT_TRANSFER + argument.projected_call_slot.adapter.bridge_data_action = BridgeDataAction.DIRECT_TRANSFER elif edit == "rank": argument.array.rank = 0 elif edit == "shape": @@ -173,7 +173,7 @@ def test_raw_array_plan_edits_fail_before_backend_lowering(edit: str, diagnostic elif edit == "character-length": argument.array.itemsize = None else: - argument.native_call_slot = function.native_call_slots[0] + argument.projected_call_slot = function.entrypoint.projected_slots[0] with pytest.raises(ValueError, match=diagnostic): WrapperGenerator().generate(plan) diff --git a/tests/fortran/raw_addresses/codegen/test_scalar_address_lowering.py b/tests/fortran/raw_addresses/codegen/test_scalar_address_lowering.py index 5a89115f7..479f2d06e 100644 --- a/tests/fortran/raw_addresses/codegen/test_scalar_address_lowering.py +++ b/tests/fortran/raw_addresses/codegen/test_scalar_address_lowering.py @@ -38,20 +38,20 @@ def test_scalar_storage_and_raw_address_plans_keep_explicit_boundary_facts(): direct_result = direct_function.results[0] hidden_result = hidden_function.results[0] - assert storage.native_call_slot is storage_function.native_call_slots[storage.native_position] + assert storage.projected_call_slot is storage_function.entrypoint.projected_slots[storage.native_position] assert storage.object_kind is ObjectKind.NUMPY_ARRAY assert storage.array.rank == 0 assert storage.array.category == "scalar_storage" assert storage.binding.python_action is PythonBarrierAction.SCALAR_STORAGE assert storage.binding.writable is True assert storage.bridge.native_action is NativeBarrierAction.PASS_STORAGE_ADDRESS - assert storage.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS + assert storage.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS assert storage.bridge.data_action is BridgeDataAction.ASSOCIATE_VIEW assert storage.bridge.copy_reason is None - assert raw.native_call_slot is raw_function.native_call_slots[raw.native_position] + assert raw.projected_call_slot is raw_function.entrypoint.projected_slots[raw.native_position] assert raw.binding.python_action is PythonBarrierAction.RAW_ADDRESS assert raw.bridge.native_action is NativeBarrierAction.PASS_RAW_ADDRESS - assert raw.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS + assert raw.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS assert raw.bridge.data_action is BridgeDataAction.ASSOCIATE_VIEW assert raw.bridge.copy_reason is None assert direct_result.object_kind is ObjectKind.NUMPY_ARRAY @@ -60,7 +60,7 @@ def test_scalar_storage_and_raw_address_plans_keep_explicit_boundary_facts(): assert direct_result.binding.codegen_action is CodegenAction.COPY_OUT assert direct_result.bridge.native_action is NativeBarrierAction.NONE assert direct_result.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION - assert direct_result.direct_result_abi is DirectResultABI.NOT_APPLICABLE + assert direct_result.entrypoint.direct_result_abi is DirectResultABI.NOT_APPLICABLE assert hidden_result.object_kind is ObjectKind.NUMPY_ARRAY assert hidden_result.array.rank == 0 assert hidden_result.array.category == "scalar_storage" 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 83aa998a4..5439f9f34 100644 --- a/tests/fortran/raw_addresses/codegen/test_string_address_lowering.py +++ b/tests/fortran/raw_addresses/codegen/test_string_address_lowering.py @@ -59,10 +59,9 @@ def test_string_address_plans_keep_completed_ownership_length_and_copy_facts(): assert argument.destruction_policy is DestructionPolicy.CALLER assert argument.binding.codegen_action is CodegenAction.IN_PLACE_ARGUMENT assert argument.bridge.codegen_action is CodegenAction.IN_PLACE_ARGUMENT - assert argument.bridge.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS + assert argument.entrypoint.handoff_mode is ArgumentHandoffMode.OPAQUE_ADDRESS assert argument.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION - assert argument.binding.length_handoff_role is None - assert argument.bridge.length_handoff_role is None + assert argument.entrypoint.length_handoff_role is None assert argument.mutates_native is True assert argument.projects_result is False @@ -128,16 +127,15 @@ def test_string_address_plan_edits_fail_before_backend_lowering(edit: str, diagn raw = functions["raw"].arguments[0] if edit == "missing-length": storage.character_length = None - storage.native_call_slot.character_length = None + storage.projected_call_slot.character_length = None elif edit == "wrong-owner": storage.ownership_owner = OwnershipOwner.NATIVE elif edit == "runtime-length-role": role = f"{storage.owner_path}:length" - storage.binding.length_handoff_role = role - storage.bridge.length_handoff_role = role + storage.entrypoint.length_handoff_role = role elif edit == "wrong-copy-reason": storage.bridge.copy_reason = "an edited reason" - storage.native_call_slot.bridge_copy_reason = "an edited reason" + storage.projected_call_slot.adapter.bridge_copy_reason = "an edited reason" elif edit == "missing-mutation": storage.mutates_native = False storage.binding.writable = False diff --git a/tests/fortran/raw_addresses/end_to_end/fixtures/routing/contracts/raw_addresses_direct_bind_c_f90.pyi b/tests/fortran/raw_addresses/end_to_end/fixtures/routing/contracts/raw_addresses_direct_bind_c_f90.pyi new file mode 100644 index 000000000..fe8c86dac --- /dev/null +++ b/tests/fortran/raw_addresses/end_to_end/fixtures/routing/contracts/raw_addresses_direct_bind_c_f90.pyi @@ -0,0 +1,11 @@ +from prik.contracts import Addr, Float64, Int32, native_abi + +@native_abi("c") +def pointer_state( + address: Addr(Float64) +) -> Int32: ... + +@native_abi("c") +def increment_pointer( + address: Addr(Float64) +) -> None: ... diff --git a/tests/fortran/raw_addresses/end_to_end/fixtures/routing/contracts/raw_addresses_mixed_bind_c_f90.pyi b/tests/fortran/raw_addresses/end_to_end/fixtures/routing/contracts/raw_addresses_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..035217c88 --- /dev/null +++ b/tests/fortran/raw_addresses/end_to_end/fixtures/routing/contracts/raw_addresses_mixed_bind_c_f90.pyi @@ -0,0 +1,11 @@ +from prik.contracts import Addr, Arg, Float64, Int32, native_abi, native_call + +@native_abi("c") +def pointer_state( + address: Addr(Float64) +) -> Int32: ... + +@native_call([Addr(Arg(0))]) +def adapted_value( + value: Int32 +) -> Int32: ... diff --git a/tests/fortran/raw_addresses/end_to_end/fixtures/routing/native/raw_addresses_direct_bind_c_f90.f90 b/tests/fortran/raw_addresses/end_to_end/fixtures/routing/native/raw_addresses_direct_bind_c_f90.f90 new file mode 100644 index 000000000..323a4eb39 --- /dev/null +++ b/tests/fortran/raw_addresses/end_to_end/fixtures/routing/native/raw_addresses_direct_bind_c_f90.f90 @@ -0,0 +1,17 @@ +module raw_addresses_direct_bind_c_f90 + use iso_c_binding +contains + integer(c_int) function pointer_state(address) bind(C) result(state) + type(c_ptr), value, intent(in) :: address + + state = merge(1_c_int, 0_c_int, c_associated(address)) + end function pointer_state + + subroutine increment_pointer(address) bind(C) + type(c_ptr), value, intent(in) :: address + real(c_double), pointer :: value + + call c_f_pointer(address, value) + value = value + 1.0_c_double + end subroutine increment_pointer +end module raw_addresses_direct_bind_c_f90 diff --git a/tests/fortran/raw_addresses/end_to_end/fixtures/routing/native/raw_addresses_mixed_bind_c_f90.f90 b/tests/fortran/raw_addresses/end_to_end/fixtures/routing/native/raw_addresses_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..0d238b8ff --- /dev/null +++ b/tests/fortran/raw_addresses/end_to_end/fixtures/routing/native/raw_addresses_mixed_bind_c_f90.f90 @@ -0,0 +1,15 @@ +module raw_addresses_mixed_bind_c_f90 + use iso_c_binding +contains + integer(c_int) function pointer_state(address) bind(C) result(state) + type(c_ptr), value, intent(in) :: address + + state = merge(1_c_int, 0_c_int, c_associated(address)) + end function pointer_state + + integer(c_int) function adapted_value(value) result(output) + integer(c_int), intent(in) :: value + + output = value + 3_c_int + end function adapted_value +end module raw_addresses_mixed_bind_c_f90 diff --git a/tests/fortran/raw_addresses/end_to_end/test_raw_address_direct_entrypoint_routing.py b/tests/fortran/raw_addresses/end_to_end/test_raw_address_direct_entrypoint_routing.py new file mode 100644 index 000000000..2e4797947 --- /dev/null +++ b/tests/fortran/raw_addresses/end_to_end/test_raw_address_direct_entrypoint_routing.py @@ -0,0 +1,47 @@ +"""Compiled raw-address lifetime, mutation, nullability, and mixed routing.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import _build_inline_pyi_contract_module + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def _build(tmp_path: Path, stem: str): + return _build_inline_pyi_contract_module( + tmp_path, + module_name=stem, + source_text=(FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8"), + contract_text=(FIXTURES / "contracts" / f"{stem}.pyi").read_text(encoding="utf-8"), + ) + + +def test_raw_addresses_all_direct_route_preserves_pointer_value_and_caller_lifetime(tmp_path: Path): + module, result = _build(tmp_path, "raw_addresses_direct_bind_c_f90") + values = np.array([2.5], dtype=np.float64) + + assert module.pointer_state(0) == np.int32(0) + assert module.pointer_state(values.ctypes.data) == np.int32(1) + assert module.increment_pointer(values.ctypes.data) is None + np.testing.assert_array_equal(values, np.array([3.5], dtype=np.float64)) + assert {path.name for path in result.generated_sources} == { + "raw_addresses_direct_bind_c_f90_wrapper.c", + "raw_addresses_direct_bind_c_f90_wrapper.h", + } + + +def test_raw_addresses_mixed_route_adapts_only_ordinary_operation(tmp_path: Path): + module, result = _build(tmp_path, "raw_addresses_mixed_bind_c_f90") + values = np.array([2.5], dtype=np.float64) + + assert module.pointer_state(values.ctypes.data) == np.int32(1) + assert module.adapted_value(np.int32(4)) == np.int32(7) + bridge = ( + (result.output_dir / "bind_c_raw_addresses_mixed_bind_c_f90_wrapper.f90").read_text(encoding="utf-8").casefold() + ) + assert "bind_c_adapted_value" in bridge + assert "pointer_state" not in bridge diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json index dc81b838a..7ae154940 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json +++ b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/basic_subroutine.json @@ -35,6 +35,8 @@ "origin": { "source_language": "fortran", "native_name": "n", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -59,6 +61,8 @@ "origin": { "source_language": "fortran", "native_name": "n", + "native_abi": null, + "native_symbol": null, "native_scope": "add1", "source_kind": "argument", "source_type": "integer", @@ -128,6 +132,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -158,6 +164,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": "add1", "source_kind": "argument", "source_type": "real(kind=8)", @@ -212,6 +220,8 @@ "origin": { "source_language": "fortran", "native_name": "add1", + "native_abi": null, + "native_symbol": null, "native_scope": "m1", "source_kind": "subroutine", "source_type": null, @@ -229,6 +239,8 @@ "origin": { "source_language": "fortran", "native_name": "m1", + "native_abi": null, + "native_symbol": null, "native_scope": "m1", "source_kind": "module", "source_type": null, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json index 2b1d20c3a..d9af79e23 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json +++ b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_all_exprs.json @@ -59,6 +59,8 @@ "origin": { "source_language": "fortran", "native_name": "x1", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -89,6 +91,8 @@ "origin": { "source_language": "fortran", "native_name": "x1", + "native_abi": null, + "native_symbol": null, "native_scope": "all_exprs", "source_kind": "argument", "source_type": "integer", @@ -166,6 +170,8 @@ "origin": { "source_language": "fortran", "native_name": "x2", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -196,6 +202,8 @@ "origin": { "source_language": "fortran", "native_name": "x2", + "native_abi": null, + "native_symbol": null, "native_scope": "all_exprs", "source_kind": "argument", "source_type": "integer", @@ -273,6 +281,8 @@ "origin": { "source_language": "fortran", "native_name": "x3", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -303,6 +313,8 @@ "origin": { "source_language": "fortran", "native_name": "x3", + "native_abi": null, + "native_symbol": null, "native_scope": "all_exprs", "source_kind": "argument", "source_type": "integer", @@ -380,6 +392,8 @@ "origin": { "source_language": "fortran", "native_name": "x4", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -410,6 +424,8 @@ "origin": { "source_language": "fortran", "native_name": "x4", + "native_abi": null, + "native_symbol": null, "native_scope": "all_exprs", "source_kind": "argument", "source_type": "integer", @@ -487,6 +503,8 @@ "origin": { "source_language": "fortran", "native_name": "x5", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -517,6 +535,8 @@ "origin": { "source_language": "fortran", "native_name": "x5", + "native_abi": null, + "native_symbol": null, "native_scope": "all_exprs", "source_kind": "argument", "source_type": "integer", @@ -596,6 +616,8 @@ "origin": { "source_language": "fortran", "native_name": "x6", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -626,6 +648,8 @@ "origin": { "source_language": "fortran", "native_name": "x6", + "native_abi": null, + "native_symbol": null, "native_scope": "all_exprs", "source_kind": "argument", "source_type": "integer", @@ -703,6 +727,8 @@ "origin": { "source_language": "fortran", "native_name": "x7", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -733,6 +759,8 @@ "origin": { "source_language": "fortran", "native_name": "x7", + "native_abi": null, + "native_symbol": null, "native_scope": "all_exprs", "source_kind": "argument", "source_type": "integer", @@ -810,6 +838,8 @@ "origin": { "source_language": "fortran", "native_name": "x8", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -840,6 +870,8 @@ "origin": { "source_language": "fortran", "native_name": "x8", + "native_abi": null, + "native_symbol": null, "native_scope": "all_exprs", "source_kind": "argument", "source_type": "integer", @@ -917,6 +949,8 @@ "origin": { "source_language": "fortran", "native_name": "x9", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -947,6 +981,8 @@ "origin": { "source_language": "fortran", "native_name": "x9", + "native_abi": null, + "native_symbol": null, "native_scope": "all_exprs", "source_kind": "argument", "source_type": "integer", @@ -1064,6 +1100,8 @@ "origin": { "source_language": "fortran", "native_name": "all_exprs", + "native_abi": null, + "native_symbol": null, "native_scope": "expr_mod", "source_kind": "subroutine", "source_type": null, @@ -1100,6 +1138,8 @@ "origin": { "source_language": "fortran", "native_name": "a", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1127,6 +1167,8 @@ "origin": { "source_language": "fortran", "native_name": "a", + "native_abi": null, + "native_symbol": null, "native_scope": "expr_mod", "source_kind": "variable", "source_type": "integer", @@ -1170,6 +1212,8 @@ "origin": { "source_language": "fortran", "native_name": "b", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1197,6 +1241,8 @@ "origin": { "source_language": "fortran", "native_name": "b", + "native_abi": null, + "native_symbol": null, "native_scope": "expr_mod", "source_kind": "variable", "source_type": "integer", @@ -1240,6 +1286,8 @@ "origin": { "source_language": "fortran", "native_name": "c", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1267,6 +1315,8 @@ "origin": { "source_language": "fortran", "native_name": "c", + "native_abi": null, + "native_symbol": null, "native_scope": "expr_mod", "source_kind": "variable", "source_type": "integer", @@ -1310,6 +1360,8 @@ "origin": { "source_language": "fortran", "native_name": "p_add", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1337,6 +1389,8 @@ "origin": { "source_language": "fortran", "native_name": "p_add", + "native_abi": null, + "native_symbol": null, "native_scope": "expr_mod", "source_kind": "variable", "source_type": "integer", @@ -1380,6 +1434,8 @@ "origin": { "source_language": "fortran", "native_name": "p_sub", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1407,6 +1463,8 @@ "origin": { "source_language": "fortran", "native_name": "p_sub", + "native_abi": null, + "native_symbol": null, "native_scope": "expr_mod", "source_kind": "variable", "source_type": "integer", @@ -1450,6 +1508,8 @@ "origin": { "source_language": "fortran", "native_name": "p_mul", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1477,6 +1537,8 @@ "origin": { "source_language": "fortran", "native_name": "p_mul", + "native_abi": null, + "native_symbol": null, "native_scope": "expr_mod", "source_kind": "variable", "source_type": "integer", @@ -1520,6 +1582,8 @@ "origin": { "source_language": "fortran", "native_name": "p_div", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1547,6 +1611,8 @@ "origin": { "source_language": "fortran", "native_name": "p_div", + "native_abi": null, + "native_symbol": null, "native_scope": "expr_mod", "source_kind": "variable", "source_type": "integer", @@ -1590,6 +1656,8 @@ "origin": { "source_language": "fortran", "native_name": "p_pow", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1617,6 +1685,8 @@ "origin": { "source_language": "fortran", "native_name": "p_pow", + "native_abi": null, + "native_symbol": null, "native_scope": "expr_mod", "source_kind": "variable", "source_type": "integer", @@ -1660,6 +1730,8 @@ "origin": { "source_language": "fortran", "native_name": "p_mix", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1687,6 +1759,8 @@ "origin": { "source_language": "fortran", "native_name": "p_mix", + "native_abi": null, + "native_symbol": null, "native_scope": "expr_mod", "source_kind": "variable", "source_type": "integer", @@ -1712,6 +1786,8 @@ "origin": { "source_language": "fortran", "native_name": "expr_mod", + "native_abi": null, + "native_symbol": null, "native_scope": "expr_mod", "source_kind": "module", "source_type": null, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json index f7bf83b67..d97881abc 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json +++ b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/compile_time_shape_exprs.json @@ -61,6 +61,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -91,6 +93,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": "use_expr", "source_kind": "argument", "source_type": "integer", @@ -168,6 +172,8 @@ "origin": { "source_language": "fortran", "native_name": "y", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real", @@ -198,6 +204,8 @@ "origin": { "source_language": "fortran", "native_name": "y", + "native_abi": null, + "native_symbol": null, "native_scope": "use_expr", "source_kind": "argument", "source_type": "real", @@ -252,6 +260,8 @@ "origin": { "source_language": "fortran", "native_name": "use_expr", + "native_abi": null, + "native_symbol": null, "native_scope": "dims_mod", "source_kind": "subroutine", "source_type": null, @@ -288,6 +298,8 @@ "origin": { "source_language": "fortran", "native_name": "n0", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -315,6 +327,8 @@ "origin": { "source_language": "fortran", "native_name": "n0", + "native_abi": null, + "native_symbol": null, "native_scope": "dims_mod", "source_kind": "variable", "source_type": "integer", @@ -358,6 +372,8 @@ "origin": { "source_language": "fortran", "native_name": "n1", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -385,6 +401,8 @@ "origin": { "source_language": "fortran", "native_name": "n1", + "native_abi": null, + "native_symbol": null, "native_scope": "dims_mod", "source_kind": "variable", "source_type": "integer", @@ -410,6 +428,8 @@ "origin": { "source_language": "fortran", "native_name": "dims_mod", + "native_abi": null, + "native_symbol": null, "native_scope": "dims_mod", "source_kind": "module", "source_type": null, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_type.json b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_type.json index 98e001f60..f565e265d 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_type.json +++ b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_type.json @@ -35,6 +35,8 @@ "origin": { "source_language": "fortran", "native_name": "p", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "type(particle)", @@ -59,6 +61,8 @@ "origin": { "source_language": "fortran", "native_name": "p", + "native_abi": null, + "native_symbol": null, "native_scope": "touch", "source_kind": "argument", "source_type": "type(particle)", @@ -98,6 +102,8 @@ "origin": { "source_language": "fortran", "native_name": "touch", + "native_abi": null, + "native_symbol": null, "native_scope": "particle_mod", "source_kind": "subroutine", "source_type": null, @@ -132,6 +138,8 @@ "origin": { "source_language": "fortran", "native_name": "id", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -156,6 +164,8 @@ "origin": { "source_language": "fortran", "native_name": "id", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "integer", @@ -224,6 +234,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -254,6 +266,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "real(kind=8)", @@ -320,6 +334,8 @@ "origin": { "source_language": "fortran", "native_name": "particle", + "native_abi": null, + "native_symbol": null, "native_scope": "particle_mod", "source_kind": "derived_type", "source_type": null, @@ -334,6 +350,8 @@ "origin": { "source_language": "fortran", "native_name": "particle_mod", + "native_abi": null, + "native_symbol": null, "native_scope": "particle_mod", "source_kind": "module", "source_type": null, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json index 4a17cd54d..26d96c9fa 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json +++ b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/derived_types_and_methods.json @@ -29,6 +29,8 @@ "origin": { "source_language": "fortran", "native_name": "id", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -53,6 +55,8 @@ "origin": { "source_language": "fortran", "native_name": "id", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "integer", @@ -121,6 +125,8 @@ "origin": { "source_language": "fortran", "native_name": "xyz", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -151,6 +157,8 @@ "origin": { "source_language": "fortran", "native_name": "xyz", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "real(kind=8)", @@ -217,6 +225,8 @@ "origin": { "source_language": "fortran", "native_name": "node", + "native_abi": null, + "native_symbol": null, "native_scope": "mesh_mod", "source_kind": "derived_type", "source_type": null, @@ -247,6 +257,8 @@ "origin": { "source_language": "fortran", "native_name": "nnodes", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -271,6 +283,8 @@ "origin": { "source_language": "fortran", "native_name": "nnodes", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "integer", @@ -339,6 +353,8 @@ "origin": { "source_language": "fortran", "native_name": "nodes", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "type(node)", @@ -369,6 +385,8 @@ "origin": { "source_language": "fortran", "native_name": "nodes", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "type(node)", @@ -435,6 +453,8 @@ "origin": { "source_language": "fortran", "native_name": "mesh", + "native_abi": null, + "native_symbol": null, "native_scope": "mesh_mod", "source_kind": "derived_type", "source_type": null, @@ -449,6 +469,8 @@ "origin": { "source_language": "fortran", "native_name": "mesh_mod", + "native_abi": null, + "native_symbol": null, "native_scope": "mesh_mod", "source_kind": "module", "source_type": null, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json index 6deb77adf..ec9b53f6e 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json +++ b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/modern_pyi_example.json @@ -35,6 +35,8 @@ "origin": { "source_language": "fortran", "native_name": "p", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "type(particle)", @@ -59,6 +61,8 @@ "origin": { "source_language": "fortran", "native_name": "p", + "native_abi": null, + "native_symbol": null, "native_scope": "init_particle", "source_kind": "argument", "source_type": "type(particle)", @@ -106,6 +110,8 @@ "origin": { "source_language": "fortran", "native_name": "pid", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -130,6 +136,8 @@ "origin": { "source_language": "fortran", "native_name": "pid", + "native_abi": null, + "native_symbol": null, "native_scope": "init_particle", "source_kind": "argument", "source_type": "integer", @@ -177,6 +185,8 @@ "origin": { "source_language": "fortran", "native_name": "mass", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -201,6 +211,8 @@ "origin": { "source_language": "fortran", "native_name": "mass", + "native_abi": null, + "native_symbol": null, "native_scope": "init_particle", "source_kind": "argument", "source_type": "real(kind=8)", @@ -248,6 +260,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -272,6 +286,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": "init_particle", "source_kind": "argument", "source_type": "real(kind=8)", @@ -319,6 +335,8 @@ "origin": { "source_language": "fortran", "native_name": "y", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -343,6 +361,8 @@ "origin": { "source_language": "fortran", "native_name": "y", + "native_abi": null, + "native_symbol": null, "native_scope": "init_particle", "source_kind": "argument", "source_type": "real(kind=8)", @@ -390,6 +410,8 @@ "origin": { "source_language": "fortran", "native_name": "z", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -414,6 +436,8 @@ "origin": { "source_language": "fortran", "native_name": "z", + "native_abi": null, + "native_symbol": null, "native_scope": "init_particle", "source_kind": "argument", "source_type": "real(kind=8)", @@ -498,6 +522,8 @@ "origin": { "source_language": "fortran", "native_name": "init_particle", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "subroutine", "source_type": null, @@ -537,6 +563,8 @@ "origin": { "source_language": "fortran", "native_name": "p", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "type(particle)", @@ -561,6 +589,8 @@ "origin": { "source_language": "fortran", "native_name": "p", + "native_abi": null, + "native_symbol": null, "native_scope": "kinetic_energy", "source_kind": "argument", "source_type": "type(particle)", @@ -608,6 +638,8 @@ "origin": { "source_language": "fortran", "native_name": "vx", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -632,6 +664,8 @@ "origin": { "source_language": "fortran", "native_name": "vx", + "native_abi": null, + "native_symbol": null, "native_scope": "kinetic_energy", "source_kind": "argument", "source_type": "real(kind=8)", @@ -679,6 +713,8 @@ "origin": { "source_language": "fortran", "native_name": "vy", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -703,6 +739,8 @@ "origin": { "source_language": "fortran", "native_name": "vy", + "native_abi": null, + "native_symbol": null, "native_scope": "kinetic_energy", "source_kind": "argument", "source_type": "real(kind=8)", @@ -750,6 +788,8 @@ "origin": { "source_language": "fortran", "native_name": "vz", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -774,6 +814,8 @@ "origin": { "source_language": "fortran", "native_name": "vz", + "native_abi": null, + "native_symbol": null, "native_scope": "kinetic_energy", "source_kind": "argument", "source_type": "real(kind=8)", @@ -811,6 +853,8 @@ "origin": { "source_language": "fortran", "native_name": "e", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -874,6 +918,8 @@ "origin": { "source_language": "fortran", "native_name": "kinetic_energy", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "function", "source_type": null, @@ -935,6 +981,8 @@ "origin": { "source_language": "fortran", "native_name": "v", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -965,6 +1013,8 @@ "origin": { "source_language": "fortran", "native_name": "v", + "native_abi": null, + "native_symbol": null, "native_scope": "scale_vector", "source_kind": "argument", "source_type": "real(kind=8)", @@ -1018,6 +1068,8 @@ "origin": { "source_language": "fortran", "native_name": "alpha", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -1042,6 +1094,8 @@ "origin": { "source_language": "fortran", "native_name": "alpha", + "native_abi": null, + "native_symbol": null, "native_scope": "scale_vector", "source_kind": "argument", "source_type": "real(kind=8)", @@ -1090,6 +1144,8 @@ "origin": { "source_language": "fortran", "native_name": "scale_vector", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "subroutine", "source_type": null, @@ -1151,6 +1207,8 @@ "origin": { "source_language": "fortran", "native_name": "a", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -1181,6 +1239,8 @@ "origin": { "source_language": "fortran", "native_name": "a", + "native_abi": null, + "native_symbol": null, "native_scope": "dot3", "source_kind": "argument", "source_type": "real(kind=8)", @@ -1256,6 +1316,8 @@ "origin": { "source_language": "fortran", "native_name": "b", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -1286,6 +1348,8 @@ "origin": { "source_language": "fortran", "native_name": "b", + "native_abi": null, + "native_symbol": null, "native_scope": "dot3", "source_kind": "argument", "source_type": "real(kind=8)", @@ -1329,6 +1393,8 @@ "origin": { "source_language": "fortran", "native_name": "s", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -1374,6 +1440,8 @@ "origin": { "source_language": "fortran", "native_name": "dot3", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "function", "source_type": null, @@ -1439,6 +1507,8 @@ "origin": { "source_language": "fortran", "native_name": "a", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -1472,6 +1542,8 @@ "origin": { "source_language": "fortran", "native_name": "a", + "native_abi": null, + "native_symbol": null, "native_scope": "fill_identity3", "source_kind": "argument", "source_type": "real(kind=8)", @@ -1520,6 +1592,8 @@ "origin": { "source_language": "fortran", "native_name": "fill_identity3", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "subroutine", "source_type": null, @@ -1559,6 +1633,8 @@ "origin": { "source_language": "fortran", "native_name": "p", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "type(particle)", @@ -1583,6 +1659,8 @@ "origin": { "source_language": "fortran", "native_name": "p", + "native_abi": null, + "native_symbol": null, "native_scope": "normalize_particle", "source_kind": "argument", "source_type": "type(particle)", @@ -1622,6 +1700,8 @@ "origin": { "source_language": "fortran", "native_name": "normalize_particle", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "subroutine", "source_type": null, @@ -1661,6 +1741,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1685,6 +1767,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": "hidden_proc", "source_kind": "argument", "source_type": "integer", @@ -1724,6 +1808,8 @@ "origin": { "source_language": "fortran", "native_name": "hidden_proc", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "subroutine", "source_type": null, @@ -1758,6 +1844,8 @@ "origin": { "source_language": "fortran", "native_name": "id", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1782,6 +1870,8 @@ "origin": { "source_language": "fortran", "native_name": "id", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "integer", @@ -1819,6 +1909,8 @@ "origin": { "source_language": "fortran", "native_name": "mass", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -1843,6 +1935,8 @@ "origin": { "source_language": "fortran", "native_name": "mass", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "real(kind=8)", @@ -1911,6 +2005,8 @@ "origin": { "source_language": "fortran", "native_name": "position", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -1941,6 +2037,8 @@ "origin": { "source_language": "fortran", "native_name": "position", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "real(kind=8)", @@ -2018,6 +2116,8 @@ "origin": { "source_language": "fortran", "native_name": "particle", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "derived_type", "source_type": null, @@ -2079,6 +2179,8 @@ "origin": { "source_language": "fortran", "native_name": "values", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -2109,6 +2211,8 @@ "origin": { "source_language": "fortran", "native_name": "values", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "real(kind=8)", @@ -2164,6 +2268,8 @@ "origin": { "source_language": "fortran", "native_name": "vector3", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "derived_type", "source_type": null, @@ -2194,6 +2300,8 @@ "origin": { "source_language": "fortran", "native_name": "code", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -2218,6 +2326,8 @@ "origin": { "source_language": "fortran", "native_name": "code", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "integer", @@ -2265,6 +2375,8 @@ "origin": { "source_language": "fortran", "native_name": "hidden_state", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "derived_type", "source_type": null, @@ -2293,6 +2405,8 @@ "origin": { "source_language": "fortran", "native_name": "counter", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -2317,6 +2431,8 @@ "origin": { "source_language": "fortran", "native_name": "counter", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "variable", "source_type": "integer", @@ -2354,6 +2470,8 @@ "origin": { "source_language": "fortran", "native_name": "hidden_scale", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -2378,6 +2496,8 @@ "origin": { "source_language": "fortran", "native_name": "hidden_scale", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "variable", "source_type": "real(kind=8)", @@ -2402,6 +2522,8 @@ "origin": { "source_language": "fortran", "native_name": "modern_math_physics", + "native_abi": null, + "native_symbol": null, "native_scope": "modern_math_physics", "source_kind": "module", "source_type": null, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json index 3b38dc9f6..22dbd792a 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json +++ b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/module_vars_use.json @@ -31,6 +31,8 @@ "origin": { "source_language": "fortran", "native_name": "nmax", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer(kind=c_int)", @@ -58,6 +60,8 @@ "origin": { "source_language": "fortran", "native_name": "nmax", + "native_abi": null, + "native_symbol": null, "native_scope": "constants_mod", "source_kind": "variable", "source_type": "integer(kind=c_int)", @@ -127,6 +131,8 @@ "origin": { "source_language": "fortran", "native_name": "origin", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=c_double)", @@ -157,6 +163,8 @@ "origin": { "source_language": "fortran", "native_name": "origin", + "native_abi": null, + "native_symbol": null, "native_scope": "constants_mod", "source_kind": "variable", "source_type": "real(kind=c_double)", @@ -201,6 +209,8 @@ "origin": { "source_language": "fortran", "native_name": "constants_mod", + "native_abi": null, + "native_symbol": null, "native_scope": "constants_mod", "source_kind": "module", "source_type": null, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json index c573bec3c..328d8afa3 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json +++ b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/procedures_and_functions.json @@ -57,6 +57,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -87,6 +89,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": "norm2", "source_kind": "argument", "source_type": "real(kind=8)", @@ -130,6 +134,8 @@ "origin": { "source_language": "fortran", "native_name": "res", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -166,6 +172,8 @@ "origin": { "source_language": "fortran", "native_name": "norm2", + "native_abi": null, + "native_symbol": null, "native_scope": "math_mod", "source_kind": "function", "source_type": null, @@ -205,6 +213,8 @@ "origin": { "source_language": "fortran", "native_name": "a", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -229,6 +239,8 @@ "origin": { "source_language": "fortran", "native_name": "a", + "native_abi": null, + "native_symbol": null, "native_scope": "scale", "source_kind": "argument", "source_type": "real(kind=8)", @@ -298,6 +310,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real(kind=8)", @@ -328,6 +342,8 @@ "origin": { "source_language": "fortran", "native_name": "x", + "native_abi": null, + "native_symbol": null, "native_scope": "scale", "source_kind": "argument", "source_type": "real(kind=8)", @@ -382,6 +398,8 @@ "origin": { "source_language": "fortran", "native_name": "scale", + "native_abi": null, + "native_symbol": null, "native_scope": "math_mod", "source_kind": "subroutine", "source_type": null, @@ -399,6 +417,8 @@ "origin": { "source_language": "fortran", "native_name": "math_mod", + "native_abi": null, + "native_symbol": null, "native_scope": "math_mod", "source_kind": "module", "source_type": null, diff --git a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json index 98c3e7243..f02813e59 100644 --- a/tests/fortran/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json +++ b/tests/fortran/semantic_ir/semantics/fixtures/general/expected/scope_name_reuse_combinations.json @@ -35,6 +35,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -61,6 +63,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "do_work_i", "source_kind": "argument", "source_type": "integer", @@ -100,6 +104,8 @@ "origin": { "source_language": "fortran", "native_name": "do_work_i", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "subroutine", "source_type": null, @@ -139,6 +145,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real", @@ -163,6 +171,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "do_work_r", "source_kind": "argument", "source_type": "real", @@ -202,6 +212,8 @@ "origin": { "source_language": "fortran", "native_name": "do_work_r", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "subroutine", "source_type": null, @@ -241,6 +253,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "logical", @@ -265,6 +279,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "do_work_l", "source_kind": "argument", "source_type": "logical", @@ -304,6 +320,8 @@ "origin": { "source_language": "fortran", "native_name": "do_work_l", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "subroutine", "source_type": null, @@ -343,6 +361,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -369,6 +389,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "host_one", "source_kind": "argument", "source_type": "integer", @@ -408,6 +430,8 @@ "origin": { "source_language": "fortran", "native_name": "host_one", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "subroutine", "source_type": null, @@ -447,6 +471,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real", @@ -473,6 +499,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "host_two", "source_kind": "argument", "source_type": "real", @@ -512,6 +540,8 @@ "origin": { "source_language": "fortran", "native_name": "host_two", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "subroutine", "source_type": null, @@ -551,6 +581,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -575,6 +607,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "convert_to_complex", "source_kind": "argument", "source_type": "integer", @@ -612,6 +646,8 @@ "origin": { "source_language": "fortran", "native_name": "shared", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "complex", @@ -648,6 +684,8 @@ "origin": { "source_language": "fortran", "native_name": "convert_to_complex", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "function", "source_type": null, @@ -687,6 +725,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real", @@ -711,6 +751,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "convert_to_char", "source_kind": "argument", "source_type": "real", @@ -750,6 +792,8 @@ "origin": { "source_language": "fortran", "native_name": "shared", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "character(kind=len=16)", @@ -786,6 +830,8 @@ "origin": { "source_language": "fortran", "native_name": "convert_to_char", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "function", "source_type": null, @@ -827,6 +873,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "character(kind=len=*)", @@ -851,6 +899,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "convert_to_logical", "source_kind": "argument", "source_type": "character(kind=len=*)", @@ -888,6 +938,8 @@ "origin": { "source_language": "fortran", "native_name": "shared", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "logical", @@ -924,6 +976,8 @@ "origin": { "source_language": "fortran", "native_name": "convert_to_logical", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "function", "source_type": null, @@ -969,6 +1023,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -995,6 +1051,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "do_work_i", "source_kind": "argument", "source_type": "integer", @@ -1038,6 +1096,8 @@ "origin": { "source_language": "fortran", "native_name": "do_work_i", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "subroutine", "source_type": null, @@ -1077,6 +1137,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real", @@ -1101,6 +1163,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "do_work_r", "source_kind": "argument", "source_type": "real", @@ -1144,6 +1208,8 @@ "origin": { "source_language": "fortran", "native_name": "do_work_r", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "subroutine", "source_type": null, @@ -1183,6 +1249,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "logical", @@ -1207,6 +1275,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "do_work_l", "source_kind": "argument", "source_type": "logical", @@ -1250,6 +1320,8 @@ "origin": { "source_language": "fortran", "native_name": "do_work_l", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "subroutine", "source_type": null, @@ -1284,6 +1356,8 @@ "origin": { "source_language": "fortran", "native_name": "payload", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1308,6 +1382,8 @@ "origin": { "source_language": "fortran", "native_name": "payload", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "field", "source_type": "integer", @@ -1355,6 +1431,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "derived_type", "source_type": null, @@ -1383,6 +1461,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name_i", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "integer", @@ -1407,6 +1487,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name_i", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "variable", "source_type": "integer", @@ -1444,6 +1526,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name_r", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "real", @@ -1468,6 +1552,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name_r", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "variable", "source_type": "real", @@ -1505,6 +1591,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name_l", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "logical", @@ -1529,6 +1617,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name_l", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "variable", "source_type": "logical", @@ -1566,6 +1656,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name_c", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "complex", @@ -1590,6 +1682,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name_c", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "variable", "source_type": "complex", @@ -1629,6 +1723,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name_s", + "native_abi": null, + "native_symbol": null, "native_scope": null, "source_kind": "variable", "source_type": "character(kind=len=8)", @@ -1653,6 +1749,8 @@ "origin": { "source_language": "fortran", "native_name": "same_name_s", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "variable", "source_type": "character(kind=len=8)", @@ -1677,6 +1775,8 @@ "origin": { "source_language": "fortran", "native_name": "scope_name_reuse_combinations", + "native_abi": null, + "native_symbol": null, "native_scope": "scope_name_reuse_combinations", "source_kind": "module", "source_type": null, diff --git a/tests/fortran/semantic_ir/semantics/test_compile_time_values.py b/tests/fortran/semantic_ir/semantics/test_compile_time_values.py index ad91de716..0d655502c 100644 --- a/tests/fortran/semantic_ir/semantics/test_compile_time_values.py +++ b/tests/fortran/semantic_ir/semantics/test_compile_time_values.py @@ -15,7 +15,7 @@ ) from prik.semantics.fortran2ir import ( _compile_time_requirement_message, - _iter_fortran_variable_contexts, + _fortran_variable_contexts, collect_semantic_compile_time_requirements, fortran_file_to_semantic_modules, fortran_module_to_semantic_module, @@ -140,7 +140,7 @@ def test_semantic_compile_time_requirements_cover_all_parser_contexts(): ], ) - contexts = {var.name: ctx for var, ctx in _iter_fortran_variable_contexts(parsed)} + contexts = {var.name: ctx for var, ctx in _fortran_variable_contexts(parsed)} requirements = collect_semantic_compile_time_requirements(parsed) supplied = collect_semantic_compile_time_requirements( parsed, @@ -378,7 +378,7 @@ def test_semantic_compile_time_requirements_cover_all_parser_contexts(): assert resolved_kind[0]["expression"] == "8 + 1" named_contexts = { var.name: ctx - for var, ctx in _iter_fortran_variable_contexts( + for var, ctx in _fortran_variable_contexts( FortranFile( filename="units.f90", variables=[FortranVariable(name="file_named")], diff --git a/tests/fortran/semantic_pyi_format/pipeline/test_native_abi_source_round_trip.py b/tests/fortran/semantic_pyi_format/pipeline/test_native_abi_source_round_trip.py new file mode 100644 index 000000000..bd49a9209 --- /dev/null +++ b/tests/fortran/semantic_pyi_format/pipeline/test_native_abi_source_round_trip.py @@ -0,0 +1,62 @@ +from prik.parsers.fortran import parse_fortran_file +from prik.pipeline.pyi import pyi_text_to_semantic_module +from prik.printers import emit_module +from prik.semantics.fortran2ir import fortran_module_to_semantic_module + + +def test_bind_c_source_emits_and_loads_native_abi_with_renamed_label(): + parsed = parse_fortran_file( + """ +module native_api + use iso_c_binding +contains + real(c_double) function scale(value) bind(C, name="scaled_value") result(output) + real(c_double), value, intent(in) :: value + output = value + end function scale +end module native_api +""" + ) + source_module = fortran_module_to_semantic_module(parsed.modules[0]) + + rendered = emit_module(source_module) + loaded = pyi_text_to_semantic_module(rendered, module_name="native_api") + function = loaded.functions[0] + + assert '@native_abi("c")' in rendered + assert '@bind("scaled_value")' in rendered + assert function.native_name == "scale" + assert function.origin.native_name == "scale" + assert function.origin.native_abi == "c" + assert function.origin.native_symbol == "scaled_value" + assert function.origin.source_language == "fortran" + + +def test_bind_c_callable_prototype_emits_and_loads_native_abi(): + parsed = parse_fortran_file( + """ +module callback_api + use iso_c_binding + abstract interface + subroutine callback(value) bind(C) + import c_double + real(c_double), value :: value + end subroutine callback + end interface +contains + subroutine apply(callback_argument) + procedure(callback) :: callback_argument + end subroutine apply +end module callback_api +""" + ) + source_module = fortran_module_to_semantic_module(parsed.modules[0]) + + rendered = emit_module(source_module) + loaded = pyi_text_to_semantic_module(rendered, module_name="callback_api") + prototype = loaded.prototypes[0] + + assert '@native_abi("c")' in rendered + assert prototype.origin.source_language == "fortran" + assert prototype.origin.native_abi == "c" + assert prototype.origin.native_symbol == "callback" diff --git a/tests/fortran/semantic_pyi_format/semantics/test_native_abi.py b/tests/fortran/semantic_pyi_format/semantics/test_native_abi.py new file mode 100644 index 000000000..26708fc34 --- /dev/null +++ b/tests/fortran/semantic_pyi_format/semantics/test_native_abi.py @@ -0,0 +1,171 @@ +import pytest + +from prik.planning import WrapperPlanner +from prik.policy import complete_semantic_policies +from prik.policy.models import NativeEntrypointAction +from prik.printers import emit_module +from tests.fortran._support.pyi_conversion import parse_pyi_text + + +def test_native_abi_keeps_fortran_identity_symbol_and_route_neutral_projection(): + module = parse_pyi_text( + """ +@native_abi("c") +@bind("scaled_value") +@native_call([Arg(0), Int32(4), Return("result", 0)]) +def scale(value: Float64) -> Float64: ... +""", + module_name="native_api", + ) + + function = module.functions[0] + assert function.native_name == "scale" + assert function.origin.source_language == "fortran" + assert function.origin.native_name == "scale" + assert function.origin.native_scope == "native_api" + assert function.origin.native_abi == "c" + assert function.origin.native_symbol == "scaled_value" + assert [item.value_kind for item in function.projection] == ["", "literal", ""] + assert [item.native_position for item in function.projection] == [0, 1, 2] + assert function.projection[2].result_position == 0 + + +def test_native_abi_composes_with_standalone_method_overload_and_prototype(): + module = parse_pyi_text( + """ +@native_abi("c") +@standalone +def external(value: Float64) -> Float64: ... + +@private +@native_abi("c") +def specific(value: Float64) -> Float64: ... + +@native_abi("c") +@bind("generic_label") +@overload("specific") +def generic(value: Float64) -> Float64: ... + +@native_abi("c") +@bind("callback_label") +@prototype +def callback(value: Float64) -> Float64: ... + +class State: + @native_abi("c") + @staticmethod + def reset(value: Float64) -> None: ... +""", + module_name="compositions", + ) + + assert module.functions[0].origin.native_scope is None + candidate = module.overload_sets[0].procedures[0] + assert candidate.origin.native_abi == "c" + assert candidate.origin.native_symbol == "generic_label" + assert module.prototypes[0].origin.native_abi == "c" + assert module.prototypes[0].origin.native_symbol == "callback_label" + assert module.classes[0].methods[0].origin.native_abi == "c" + + +@pytest.mark.parametrize( + ("source", "native_language", "message"), + [ + ('@native_abi("fortran")\ndef bad() -> None: ...', "fortran", 'accepts only "c"'), + ('@native_abi("c")\n@native_abi("c")\ndef bad() -> None: ...', "fortran", "Duplicate"), + ('@native_abi("c")\nclass Bad:\n pass', "fortran", "Unsupported class decorator"), + ('@native_abi("c")\ndef bad() -> None: ...', "c", "only valid for Fortran"), + ], +) +def test_native_abi_rejects_contradictory_or_misplaced_annotations( + source: str, + native_language: str, + message: str, +): + with pytest.raises(ValueError, match=message): + parse_pyi_text(source, module_name="invalid_native_abi", native_language=native_language) + + +def test_native_abi_round_trip_keeps_marker_symbol_and_projection(): + original = parse_pyi_text( + """ +@native_abi("c") +@bind("renamed_entry") +@native_call([Addr(Arg(0)), Arg(0).shape[0], Return("result", 0)]) +def transform(values: Float64[:]) -> Float64: ... +""", + module_name="round_trip_native_abi", + ) + + rendered = emit_module(original) + loaded = parse_pyi_text(rendered, module_name=original.name) + function = loaded.functions[0] + + assert '@native_abi("c")' in rendered + assert '@bind("renamed_entry")' in rendered + assert function.origin.native_abi == "c" + assert function.origin.native_symbol == "renamed_entry" + assert function.origin.source_language == "fortran" + assert function.projection == original.functions[0].projection + + +def test_source_free_native_abi_selects_the_preserved_symbol_and_direct_route(): + module = parse_pyi_text( + """ +@native_abi("c") +@bind("edited_native_symbol") +def transform(value: Int32) -> Int32: ... +""", + module_name="source_free_native_abi", + ) + + complete_semantic_policies(module) + plan = WrapperPlanner().build(module) + function = plan.namespaces[0].functions[0] + + assert function.entrypoint.action is NativeEntrypointAction.DIRECT_C_ABI + assert function.entrypoint.symbol_name == "edited_native_symbol" + assert function.bridge is None + assert plan.native_generated_code_groups == () + + +def test_native_abi_round_trip_preserves_scalar_c_character_value_transport(): + original = parse_pyi_text( + """ +@native_abi("c") +@native_call([Value(Arg(0))]) +def char_code(ch: String[1]) -> Int32: ... +""", + module_name="c_character_value", + ) + + rendered = emit_module(original) + loaded = parse_pyi_text(rendered, module_name=original.name) + + assert "@native_call([Value(Arg(0))])" in rendered + assert loaded.functions[0].arguments[0].metadata["native_by_value"] is True + + +def test_native_abi_source_free_concrete_array_retains_explicit_shape_mechanism(): + module = parse_pyi_text( + """ +@native_abi("c") +def total(n: Int32, values: Float64[n]) -> Float64: ... +""", + module_name="explicit_shape_native_abi", + ) + + array = module.functions[0].arguments[1].semantic_type.storage.array + assert array.category == "explicit_shape" + + +def test_native_abi_rejects_value_transport_for_longer_character_buffer(): + with pytest.raises(ValueError, match=r"String\[1\]"): + parse_pyi_text( + """ +@native_abi("c") +@native_call([Value(Arg(0))]) +def invalid(label: String[8]) -> Int32: ... +""", + module_name="invalid_c_character_value", + ) diff --git a/tests/fortran/strings/codegen/test_character_array_lowering.py b/tests/fortran/strings/codegen/test_character_array_lowering.py index a21dc8ffd..99a7cca13 100644 --- a/tests/fortran/strings/codegen/test_character_array_lowering.py +++ b/tests/fortran/strings/codegen/test_character_array_lowering.py @@ -67,7 +67,7 @@ def test_fixed_width_character_array_results_reuse_the_ordinary_array_copy_plan( assert result.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION assert direct.bridge.native_action is NativeBarrierAction.NONE assert hidden.bridge.native_action is NativeBarrierAction.PASS_ARRAY_BUFFER - assert hidden.native_call_slot.object_kind is ObjectKind.NUMPY_ARRAY + assert hidden.projected_call_slot.object_kind is ObjectKind.NUMPY_ARRAY def test_fixed_width_character_array_results_lower_itemsize_into_both_backends(): 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 a7cba59ae..ade75d567 100644 --- a/tests/fortran/strings/codegen/test_fixed_string_result_lowering.py +++ b/tests/fortran/strings/codegen/test_fixed_string_result_lowering.py @@ -68,14 +68,14 @@ def test_fixed_strings_reuse_ordered_result_plans_with_completed_length_and_copy assert direct.source_kind == "direct_return" assert direct.binding.codegen_action is CodegenAction.COPY_OUT assert direct.bridge.native_action is NativeBarrierAction.NONE - assert direct.native_call_slot is None + assert direct.projected_call_slot is None assert hidden.source_kind == "hidden_output" assert hidden.binding.codegen_action is CodegenAction.COPY_OUT assert hidden.bridge.native_action is NativeBarrierAction.PASS_CALL_LOCAL_ADDRESS - assert hidden.native_call_slot is functions["hidden_label"].native_call_slots[0] - assert hidden.native_call_slot.object_kind is ObjectKind.STRING - assert hidden.native_call_slot.character_length == hidden.character_length + assert hidden.projected_call_slot is functions["hidden_label"].entrypoint.projected_slots[0] + assert hidden.projected_call_slot.object_kind is ObjectKind.STRING + assert hidden.projected_call_slot.character_length == hidden.character_length def test_fixed_string_results_dispatch_to_named_binding_and_bridge_copy_lowering(): @@ -151,7 +151,7 @@ def test_fixed_string_result_plan_edits_fail_before_backend_lowering(edit: str, elif edit == "nullable": direct.nullable = True else: - hidden.native_call_slot.character_length = 7 + hidden.projected_call_slot.character_length = 7 with pytest.raises(ValueError, match=diagnostic): WrapperGenerator().generate(plan) diff --git a/tests/fortran/strings/codegen/test_fixed_string_writeback.py b/tests/fortran/strings/codegen/test_fixed_string_writeback.py index eb47886c7..2e5dad3da 100644 --- a/tests/fortran/strings/codegen/test_fixed_string_writeback.py +++ b/tests/fortran/strings/codegen/test_fixed_string_writeback.py @@ -66,7 +66,7 @@ def test_fixed_replacement_projects_completed_argument_and_lifecycle_facts(): assert argument.result_position == 0 assert argument.binding.codegen_action is CodegenAction.COPY_IN_OUT assert argument.bridge.codegen_action is CodegenAction.COPY_IN_OUT - assert argument.native_call_slot.codegen_action is CodegenAction.COPY_IN_OUT + assert argument.projected_call_slot.adapter.codegen_action is CodegenAction.COPY_IN_OUT assert argument.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION assert argument.bridge.copy_reason == STRING_REPLACEMENT_COPY_REASON assert tuple(action.phase for action in replacement.writeback_actions) == tuple(WritebackPhase) @@ -136,7 +136,7 @@ def optional_identity(label: String = ...) -> None: ... for name in ("assumed", "optional", "optional_identity"): argument = functions[name].arguments[0] assert argument.character_length is None - assert argument.native_call_slot.character_length is None + assert argument.projected_call_slot.character_length is None assert functions["assumed"].arguments[0].binding.optional_mode is OptionalMode.REQUIRED assert functions["optional"].arguments[0].binding.optional_mode is OptionalMode.NULLABLE_VALUE assert functions["optional"].arguments[0].nullable is True @@ -193,7 +193,7 @@ def test_fixed_string_writeback_plan_edits_fail_before_backend_lowering(edit: st argument.ownership_owner = OwnershipOwner.NATIVE elif edit == "wrong-copy-reason": argument.bridge.copy_reason = "an edited copy reason" - argument.native_call_slot.bridge_copy_reason = "an edited copy reason" + argument.projected_call_slot.adapter.bridge_copy_reason = "an edited copy reason" elif edit == "missing-cleanup": function.writeback_actions = tuple( action for action in function.writeback_actions if action.phase is not WritebackPhase.CLEANUP @@ -203,7 +203,7 @@ def test_fixed_string_writeback_plan_edits_fail_before_backend_lowering(edit: st copy_out.semantic_type_name = "Int32" else: argument.binding.optional_mode = OptionalMode.DESCRIPTOR - argument.bridge.optional_mode = OptionalMode.DESCRIPTOR + argument.entrypoint.optional_mode = OptionalMode.DESCRIPTOR with pytest.raises(ValueError, match=diagnostic): WrapperGenerator().generate(plan) diff --git a/tests/fortran/strings/codegen/test_string_input_lowering.py b/tests/fortran/strings/codegen/test_string_input_lowering.py index 5fb29c13c..3e3ffe3ac 100644 --- a/tests/fortran/strings/codegen/test_string_input_lowering.py +++ b/tests/fortran/strings/codegen/test_string_input_lowering.py @@ -40,19 +40,18 @@ def test_required_string_values_reuse_argument_plan_with_character_handoff_facts assert argument.datatype_family is DatatypeFamily.STRING assert argument.binding.python_action is PythonBarrierAction.STRING_VALUE assert argument.bridge.native_action is NativeBarrierAction.PASS_CALL_LOCAL_ADDRESS - assert argument.bridge.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER + assert argument.entrypoint.handoff_mode is ArgumentHandoffMode.CHARACTER_BUFFER assert argument.bridge.data_action is BridgeDataAction.COPY_REPRESENTATION assert argument.bridge.copy_reason == ( "materialize Fortran character storage from the binding UTF-8 byte buffer" ) - assert argument.native_call_slot.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION - assert argument.binding.length_handoff_role == argument.bridge.length_handoff_role - assert argument.binding.length_handoff_role == f"{argument.owner_path}:length" - assert argument.native_call_slot is functions[function_name].native_call_slots[0] - assert argument.native_call_slot.codegen_action is CodegenAction.CALL_LOCAL_INPUT + assert argument.projected_call_slot.adapter.bridge_data_action is BridgeDataAction.COPY_REPRESENTATION + assert argument.entrypoint.length_handoff_role == f"{argument.owner_path}:length" + assert argument.projected_call_slot is functions[function_name].entrypoint.projected_slots[0] + assert argument.projected_call_slot.adapter.codegen_action is CodegenAction.CALL_LOCAL_INPUT - assert fixed.native_call_slot.character_length == 8 - assert assumed.native_call_slot.character_length is None + assert fixed.projected_call_slot.character_length == 8 + assert assumed.projected_call_slot.character_length is None def test_required_string_values_dispatch_to_named_binding_and_bridge_lowering(): @@ -91,15 +90,14 @@ def test_string_handoff_plan_edits_fail_before_backend_lowering(edit: str, diagn plan = _string_input_plan() argument = plan.namespaces[0].functions[0].arguments[0] if edit == "missing-length": - argument.binding.length_handoff_role = None - argument.bridge.length_handoff_role = None + argument.entrypoint.length_handoff_role = None elif edit == "wrong-handoff": - argument.bridge.handoff_mode = ArgumentHandoffMode.TYPED_REFERENCE + argument.entrypoint.handoff_mode = ArgumentHandoffMode.TYPED_REFERENCE else: argument.bridge.data_action = BridgeDataAction.DIRECT_TRANSFER - argument.native_call_slot.bridge_data_action = BridgeDataAction.DIRECT_TRANSFER + argument.projected_call_slot.adapter.bridge_data_action = BridgeDataAction.DIRECT_TRANSFER argument.bridge.copy_reason = None - argument.native_call_slot.bridge_copy_reason = None + argument.projected_call_slot.adapter.bridge_copy_reason = None with pytest.raises(ValueError, match=diagnostic): WrapperGenerator().generate(plan) diff --git a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/__init__.pyi b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..776d6c2dd --- /dev/null +++ b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import strings_direct_bind_c_f90 diff --git a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/strings_direct_bind_c_f90.pyi b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/strings_direct_bind_c_f90.pyi new file mode 100644 index 000000000..4c7c740ba --- /dev/null +++ b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_direct_bind_c_f90/strings_direct_bind_c_f90.pyi @@ -0,0 +1,24 @@ +from prik.contracts import Arg, Int32, Returns, String, Value, native_abi, native_call + +@native_abi("c") +@native_call([Value(Arg(0))]) +def direct_char_code( + ch: String[1] +) -> Int32: ... + +@native_abi("c") +def direct_uppercase( + ch: String[1] +) -> Returns["ch", String[1]]: ... + +@native_abi("c") +def direct_buffer_sum( + n: Int32, + text: String[1][n] +) -> Int32: ... + +@native_abi("c") +def direct_uppercase_buffer( + n: Int32, + text: String[1][n] +) -> None: ... diff --git a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/__init__.pyi b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..109ff5434 --- /dev/null +++ b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import strings_mixed_bind_c_f90 diff --git a/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/strings_mixed_bind_c_f90.pyi b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/strings_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..a0e88f3f9 --- /dev/null +++ b/tests/fortran/strings/end_to_end/fixtures/routing/contracts/strings_mixed_bind_c_f90/strings_mixed_bind_c_f90.pyi @@ -0,0 +1,11 @@ +from prik.contracts import Arg, Int32, String, Value, native_abi, native_call + +@native_abi("c") +@native_call([Value(Arg(0))]) +def direct_char_code( + ch: String[1] +) -> Int32: ... + +def adapted_fixed_code( + text: String[4] +) -> Int32: ... diff --git a/tests/fortran/strings/end_to_end/fixtures/routing/native/strings_direct_bind_c_f90.f90 b/tests/fortran/strings/end_to_end/fixtures/routing/native/strings_direct_bind_c_f90.f90 new file mode 100644 index 000000000..656ba85ad --- /dev/null +++ b/tests/fortran/strings/end_to_end/fixtures/routing/native/strings_direct_bind_c_f90.f90 @@ -0,0 +1,38 @@ +module strings_direct_bind_c_f90 + use iso_c_binding +contains + integer(c_int) function direct_char_code(ch) bind(C) result(code) + character(kind=c_char), value, intent(in) :: ch + + code = iachar(ch, c_int) + end function direct_char_code + + subroutine direct_uppercase(ch) bind(C) + character(kind=c_char), intent(inout) :: ch + + if (ch >= 'a' .and. ch <= 'z') ch = achar(iachar(ch) - 32, kind=c_char) + end subroutine direct_uppercase + + integer(c_int) function direct_buffer_sum(n, text) bind(C) result(total) + integer(c_int), value, intent(in) :: n + character(kind=c_char), intent(in) :: text(n) + integer :: index + + total = 0_c_int + do index = 1, n + total = total + iachar(text(index), c_int) + end do + end function direct_buffer_sum + + subroutine direct_uppercase_buffer(n, text) bind(C) + integer(c_int), value, intent(in) :: n + character(kind=c_char), intent(inout) :: text(n) + integer :: index + + do index = 1, n + if (text(index) >= 'a' .and. text(index) <= 'z') then + text(index) = achar(iachar(text(index)) - 32, kind=c_char) + end if + end do + end subroutine direct_uppercase_buffer +end module strings_direct_bind_c_f90 diff --git a/tests/fortran/strings/end_to_end/fixtures/routing/native/strings_mixed_bind_c_f90.f90 b/tests/fortran/strings/end_to_end/fixtures/routing/native/strings_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..95695c12e --- /dev/null +++ b/tests/fortran/strings/end_to_end/fixtures/routing/native/strings_mixed_bind_c_f90.f90 @@ -0,0 +1,15 @@ +module strings_mixed_bind_c_f90 + use iso_c_binding +contains + integer(c_int) function direct_char_code(ch) bind(C) result(code) + character(kind=c_char), value, intent(in) :: ch + + code = iachar(ch, c_int) + end function direct_char_code + + integer(c_int) function adapted_fixed_code(text) result(code) + character(len=4), intent(in) :: text + + code = iachar(text(1:1), c_int) + end function adapted_fixed_code +end module strings_mixed_bind_c_f90 diff --git a/tests/fortran/strings/end_to_end/test_string_direct_entrypoint_routing.py b/tests/fortran/strings/end_to_end/test_string_direct_entrypoint_routing.py new file mode 100644 index 000000000..408e50e4f --- /dev/null +++ b/tests/fortran/strings/end_to_end/test_string_direct_entrypoint_routing.py @@ -0,0 +1,98 @@ +"""Compiled direct and mixed scalar-character entrypoint evidence.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_strings_all_direct_route_preserves_value_mutation_and_validation( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "strings_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + {"strings_direct_bind_c_f90_wrapper.c", "strings_direct_bind_c_f90_wrapper.h"}, + FIXTURES / "contracts" / "strings_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.direct_char_code("A") == np.int32(65) + assert module.direct_uppercase("b") == "B" + with pytest.raises(TypeError, match="embedded NUL"): + module.direct_char_code("\0") + with pytest.raises(TypeError): + module.direct_char_code("é") + + buffer = np.array([b"a", b"\0", b"z"], dtype="S1") + assert module.direct_buffer_sum(np.int32(buffer.size), buffer) == np.int32(ord("a") + ord("z")) + replacement = module.direct_uppercase_buffer(np.int32(buffer.size), buffer) + np.testing.assert_array_equal(buffer, np.array([b"A", b"\0", b"Z"], dtype="S1")) + if replacement is not None: + np.testing.assert_array_equal(replacement, buffer) + + if pyi_parity_build_mode == "source": + binding = (tmp_path / "source_build" / "strings_direct_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + assert "int32_t direct_char_code(char ch);" in binding + assert "void direct_uppercase(char * ch);" in binding + assert "int32_t direct_buffer_sum(int32_t n, char * text);" in binding + assert "void direct_uppercase_buffer(int32_t n, char * text);" in binding + + +def test_strings_mixed_route_keeps_only_fixed_length_adapter( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "strings_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "strings_mixed_bind_c_f90_wrapper.c", + "strings_mixed_bind_c_f90_wrapper.h", + "bind_c_strings_mixed_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "strings_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.direct_char_code("C") == np.int32(67) + assert module.adapted_fixed_code("D ") == np.int32(68) + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_strings_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_adapted_fixed_code" in bridge + assert "direct_char_code" not in bridge + + +def test_strings_mixed_route_matches_edited_source_free_contract(tmp_path: Path): + stem = "strings_mixed_bind_c_f90" + source = (FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8") + contract = (FIXTURES / "contracts" / stem / f"{stem}.pyi").read_text(encoding="utf-8") + contract = contract.replace("from prik.contracts import ", "from prik.contracts import nogil, ") + contract = contract.replace("def direct_char_code(", "@nogil\ndef direct_char_code(").replace( + "def adapted_fixed_code(", "@nogil\ndef adapted_fixed_code(" + ) + module, result = _build_inline_pyi_contract_module( + tmp_path, module_name=stem, source_text=source, contract_text=contract + ) + + assert module.direct_char_code("C") == np.int32(67) + assert module.adapted_fixed_code("D ") == np.int32(68) + bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_adapted_fixed_code" in bridge + assert "function bind_c_direct_char_code" not in bridge diff --git a/tests/fortran/subroutines/codegen/test_hidden_scalar_outputs.py b/tests/fortran/subroutines/codegen/test_hidden_scalar_outputs.py index ce39ba514..4a8e441ee 100644 --- a/tests/fortran/subroutines/codegen/test_hidden_scalar_outputs.py +++ b/tests/fortran/subroutines/codegen/test_hidden_scalar_outputs.py @@ -23,7 +23,7 @@ def scale(x: Float64) -> Float64: ... function = plan.namespaces[0].functions[0] result = function.results[0] - assert result.native_call_slot is function.native_call_slots[result.bridge.abi_position] + assert result.projected_call_slot is function.entrypoint.projected_slots[result.projected_call_slot.native_position] artifacts = WrapperGenerator().generate(plan) c_source = next(source.text for source in artifacts.sources if source.path.suffix == ".c") diff --git a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/__init__.pyi b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..d3de141ea --- /dev/null +++ b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import subroutines_direct_bind_c_f90 diff --git a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/subroutines_direct_bind_c_f90.pyi b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/subroutines_direct_bind_c_f90.pyi new file mode 100644 index 000000000..ea200cbff --- /dev/null +++ b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_direct_bind_c_f90/subroutines_direct_bind_c_f90.pyi @@ -0,0 +1,13 @@ +from prik.contracts import Addr, Arg, Int32, Return, Returns, native_abi, native_call + +@native_abi("c") +@native_call([Addr(Arg(0))]) +def direct_reference( + value: Int32 +) -> Int32: ... + +@native_abi("c") +@native_call([Addr(Arg(0)), Return('doubled', 1), Return('status', 2)]) +def direct_outputs( + value: Int32 +) -> tuple[Returns["value", Int32], Int32, Int32]: ... diff --git a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/__init__.pyi b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/__init__.pyi new file mode 100644 index 000000000..8c2a6e70d --- /dev/null +++ b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/__init__.pyi @@ -0,0 +1 @@ +from . import subroutines_mixed_bind_c_f90 diff --git a/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/subroutines_mixed_bind_c_f90.pyi b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/subroutines_mixed_bind_c_f90.pyi new file mode 100644 index 000000000..b450f4a73 --- /dev/null +++ b/tests/fortran/subroutines/end_to_end/fixtures/routing/contracts/subroutines_mixed_bind_c_f90/subroutines_mixed_bind_c_f90.pyi @@ -0,0 +1,12 @@ +from prik.contracts import Addr, Arg, Int32, Return, Returns, native_abi, native_call + +@native_abi("c") +@native_call([Addr(Arg(0)), Return('doubled', 1)]) +def direct_outputs( + value: Int32 +) -> tuple[Returns["value", Int32], Int32]: ... + +@native_call([Addr(Arg(0)), Return('doubled', 1)]) +def adapted_outputs( + value: Int32 +) -> tuple[Returns["value", Int32], Int32]: ... diff --git a/tests/fortran/subroutines/end_to_end/fixtures/routing/native/subroutines_direct_bind_c_f90.f90 b/tests/fortran/subroutines/end_to_end/fixtures/routing/native/subroutines_direct_bind_c_f90.f90 new file mode 100644 index 000000000..2be0d840d --- /dev/null +++ b/tests/fortran/subroutines/end_to_end/fixtures/routing/native/subroutines_direct_bind_c_f90.f90 @@ -0,0 +1,19 @@ +module subroutines_direct_bind_c_f90 + use iso_c_binding +contains + integer(c_int) function direct_reference(value) bind(C) result(output) + integer(c_int), intent(in) :: value + + output = value + 10_c_int + end function direct_reference + + subroutine direct_outputs(value, doubled, status) bind(C) + integer(c_int), intent(inout) :: value + integer(c_int), intent(out) :: doubled + integer(c_int), intent(out) :: status + + doubled = 2_c_int * value + value = value + 1_c_int + status = 7_c_int + end subroutine direct_outputs +end module subroutines_direct_bind_c_f90 diff --git a/tests/fortran/subroutines/end_to_end/fixtures/routing/native/subroutines_mixed_bind_c_f90.f90 b/tests/fortran/subroutines/end_to_end/fixtures/routing/native/subroutines_mixed_bind_c_f90.f90 new file mode 100644 index 000000000..87711182e --- /dev/null +++ b/tests/fortran/subroutines/end_to_end/fixtures/routing/native/subroutines_mixed_bind_c_f90.f90 @@ -0,0 +1,19 @@ +module subroutines_mixed_bind_c_f90 + use iso_c_binding +contains + subroutine direct_outputs(value, doubled) bind(C) + integer(c_int), intent(inout) :: value + integer(c_int), intent(out) :: doubled + + doubled = 2_c_int * value + value = value + 1_c_int + end subroutine direct_outputs + + subroutine adapted_outputs(value, doubled) + integer(c_int), intent(inout) :: value + integer(c_int), intent(out) :: doubled + + doubled = 2_c_int * value + value = value + 1_c_int + end subroutine adapted_outputs +end module subroutines_mixed_bind_c_f90 diff --git a/tests/fortran/subroutines/end_to_end/test_subroutine_direct_entrypoint_routing.py b/tests/fortran/subroutines/end_to_end/test_subroutine_direct_entrypoint_routing.py new file mode 100644 index 000000000..38e63c91c --- /dev/null +++ b/tests/fortran/subroutines/end_to_end/test_subroutine_direct_entrypoint_routing.py @@ -0,0 +1,87 @@ +"""Compiled direct and mixed reference/output entrypoint evidence.""" + +from pathlib import Path + +import numpy as np +import pytest + +from tests.fortran._support.wrapper_build import ( + _build_inline_pyi_contract_module, + _build_source_or_generated_pyi_and_import, +) + +FIXTURES = Path(__file__).parent / "fixtures" / "routing" +pytestmark = pytest.mark.fortran_end_to_end + + +def test_subroutine_all_direct_route_preserves_reference_and_projected_results( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "subroutines_direct_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + {"subroutines_direct_bind_c_f90_wrapper.c", "subroutines_direct_bind_c_f90_wrapper.h"}, + FIXTURES / "contracts" / "subroutines_direct_bind_c_f90", + pyi_parity_build_mode, + ) + + assert module.direct_reference(np.int32(2)) == np.int32(12) + assert module.direct_outputs(np.int32(3)) == (np.int32(4), np.int32(6), np.int32(7)) + + if pyi_parity_build_mode == "source": + binding = (tmp_path / "source_build" / "subroutines_direct_bind_c_f90_wrapper.c").read_text(encoding="utf-8") + assert "int32_t direct_reference(int32_t * value);" in binding + assert "void direct_outputs(int32_t * value, int32_t * doubled, int32_t * status);" in binding + + +def test_subroutine_mixed_route_matches_results_and_adapts_only_ordinary_operation( + pyi_parity_build_mode: str, + tmp_path: Path, +): + source = FIXTURES / "native" / "subroutines_mixed_bind_c_f90.f90" + module = _build_source_or_generated_pyi_and_import( + source, + tmp_path, + { + "subroutines_mixed_bind_c_f90_wrapper.c", + "subroutines_mixed_bind_c_f90_wrapper.h", + "bind_c_subroutines_mixed_bind_c_f90_wrapper.f90", + }, + FIXTURES / "contracts" / "subroutines_mixed_bind_c_f90", + pyi_parity_build_mode, + ) + + expected = (np.int32(4), np.int32(6)) + assert module.direct_outputs(np.int32(3)) == expected + assert module.adapted_outputs(np.int32(3)) == expected + + if pyi_parity_build_mode == "source": + bridge = ( + (tmp_path / "source_build" / "bind_c_subroutines_mixed_bind_c_f90_wrapper.f90") + .read_text(encoding="utf-8") + .casefold() + ) + assert "bind_c_adapted_outputs" in bridge + assert "direct_outputs" not in bridge + + +def test_subroutine_mixed_route_matches_edited_source_free_contract(tmp_path: Path): + stem = "subroutines_mixed_bind_c_f90" + source = (FIXTURES / "native" / f"{stem}.f90").read_text(encoding="utf-8") + contract = (FIXTURES / "contracts" / stem / f"{stem}.pyi").read_text(encoding="utf-8") + contract = contract.replace("from prik.contracts import ", "from prik.contracts import nogil, ") + contract = contract.replace("def direct_outputs(", "@nogil\ndef direct_outputs(").replace( + "def adapted_outputs(", "@nogil\ndef adapted_outputs(" + ) + module, result = _build_inline_pyi_contract_module( + tmp_path, module_name=stem, source_text=source, contract_text=contract + ) + + expected = (np.int32(4), np.int32(6)) + assert module.direct_outputs(np.int32(3)) == expected + assert module.adapted_outputs(np.int32(3)) == expected + bridge = (result.output_dir / f"bind_c_{stem}_wrapper.f90").read_text(encoding="utf-8").casefold() + assert "bind_c_adapted_outputs" in bridge + assert "subroutine bind_c_direct_outputs" not in bridge diff --git a/tests/tools/test_direct_entrypoint_benchmark.py b/tests/tools/test_direct_entrypoint_benchmark.py new file mode 100644 index 000000000..5503d9896 --- /dev/null +++ b/tests/tools/test_direct_entrypoint_benchmark.py @@ -0,0 +1,278 @@ +"""Evidence for the separate direct-entrypoint benchmark cohort.""" + +from __future__ import annotations + +import importlib +import json +from pathlib import Path +import runpy +import subprocess +from types import SimpleNamespace + +import numpy as np +import pyperf +import pytest + +from benchmarks import direct_benchmark, direct_build_time + + +def _report(route: direct_benchmark.Route) -> dict[str, object]: + adapters = ("generated/bind_c_wrapper.f90",) if route == "prik-adapted" else () + report = { + "route": direct_benchmark.route_action(route), + "wrapper_mode": direct_benchmark.wrapper_mode(route), + "native_source": direct_benchmark.native_source(route).name, + "generated_fortran_adapter_sources": adapters, + "f2py_fortran_wrapper_sources": (), + "generated_c_sources": ("generated/wrapper.c",), + "compiled_objects": ("generated/native.o", "generated/wrapper.o"), + "linked_extension": f"generated/{direct_benchmark.module_name(route)}.so", + } + if route != "prik-adapted": + report.update( + { + "binding_direct_symbol_object": "generated/wrapper.o", + "binding_direct_symbol_references": direct_benchmark.DIRECT_SYMBOLS, + "native_direct_symbol_object": "generated/native.o", + "native_direct_symbol_definitions": direct_benchmark.DIRECT_SYMBOLS, + "linked_direct_symbol_definitions": direct_benchmark.DIRECT_SYMBOLS, + } + ) + return report + + +def test_direct_pair_uses_one_native_source_equal_labels_and_matching_flags(tmp_path: Path) -> None: + prik = direct_benchmark.build_command("prik-direct", tmp_path / "prik", compiler="/opt/gfortran", jobs=4) + f2py = direct_benchmark.build_command("f2py-direct", tmp_path / "f2py", compiler="/opt/gfortran", jobs=4) + source = str(direct_benchmark.DIRECT_SOURCE.resolve()) + + assert source in prik + assert source in f2py + assert "--no-wrap-functions" in f2py + assert "--skip-empty-wrappers" in f2py + assert str(direct_benchmark.DIRECT_SIGNATURE.resolve()) in f2py + assert all(direct_benchmark.OPTIMIZED_FLAGS in argument for argument in prik[-3:]) + assert all(direct_benchmark.OPTIMIZED_FLAGS in argument for argument in f2py[-3:]) + + native = direct_benchmark.DIRECT_SOURCE.read_text(encoding="utf-8").casefold() + for name in ("noop", "add_scalars", "add_scalars_out"): + assert f"{name}(" in native + assert "bind(c)" in native[native.index(f"{name}(") : native.index(f"{name}(") + 100] + assert 'name="' not in native + + +@pytest.mark.parametrize("route", direct_benchmark.ROUTES) +def test_direct_correctness_preserves_each_tools_natural_result_type(route) -> None: + expected_type = float if route == "f2py-direct" else np.float64 + api = SimpleNamespace( + noop=lambda: None, + add_scalars=lambda _a, _b: expected_type(4.0), + add_scalars_out=lambda _a, _b: expected_type(4.0), + ) + + direct_benchmark.check_api(api, route) + assert direct_benchmark.natural_result_type(route) == ( + "builtins.float" if route == "f2py-direct" else "numpy.float64" + ) + + +def test_artifact_preflight_rejects_wrappers_and_proves_direct_linkage( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + def symbols(path: Path) -> dict[str, str]: + if path.name in {"bench_prik_direct_wrapper.o", "bench_f2py_directmodule.c.o"}: + kind = "U" + elif path.suffix == ".so" or path.name in {"direct_kernels.o", "direct_kernels.f90.o"}: + kind = "T" + else: + return {} + return dict.fromkeys(direct_benchmark.DIRECT_SYMBOLS, kind) + + monkeypatch.setattr(direct_benchmark, "_global_symbols", symbols) + direct = tmp_path / "prik-direct" + direct.mkdir() + (direct / "bench_prik_direct.so").touch() + (direct / "bench_prik_direct_wrapper.c").touch() + (direct / "bench_prik_direct_wrapper.o").touch() + (direct / "direct_kernels.o").touch() + report = direct_benchmark.artifact_report("prik-direct", direct) + assert report["generated_fortran_adapter_sources"] == () + assert report["binding_direct_symbol_object"] == "bench_prik_direct_wrapper.o" + assert report["native_direct_symbol_object"] == "direct_kernels.o" + assert report["binding_direct_symbol_references"] == direct_benchmark.DIRECT_SYMBOLS + assert report["linked_direct_symbol_definitions"] == direct_benchmark.DIRECT_SYMBOLS + + (direct / "bind_c_bench_prik_direct_wrapper.f90").touch() + with pytest.raises(RuntimeError, match="generated user adapters"): + direct_benchmark.artifact_report("prik-direct", direct) + + f2py = tmp_path / "f2py-direct" + f2py.mkdir() + (f2py / "bench_f2py_direct.so").touch() + (f2py / "bench_f2py_directmodule.c").touch() + meson_objects = f2py / "generated" / "bbdir" / "bench_f2py_direct.cpython-312-aarch64-linux-gnu.so.p" + meson_objects.mkdir(parents=True) + (meson_objects / "bench_f2py_directmodule.c.o").touch() + (meson_objects / "direct_kernels.f90.o").touch() + (meson_objects / "fortranobject.c.o").touch() + (f2py / "bench_f2py_direct-f2pywrappers.f90").touch() + with pytest.raises(RuntimeError, match="generated Fortran wrapper sources"): + direct_benchmark.artifact_report("f2py-direct", f2py) + + (f2py / "bench_f2py_direct-f2pywrappers.f90").unlink() + report = direct_benchmark.artifact_report("f2py-direct", f2py) + assert report["f2py_fortran_wrapper_sources"] == () + assert report["binding_direct_symbol_object"].endswith("/bench_f2py_directmodule.c.o") + assert report["native_direct_symbol_object"].endswith("/direct_kernels.f90.o") + assert report["native_direct_symbol_definitions"] == direct_benchmark.DIRECT_SYMBOLS + + +def test_timed_direct_build_keeps_import_and_artifact_checks_outside_timer( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + clock = iter((10.0, 12.5)) + verified = [] + monkeypatch.setattr(direct_build_time.time, "perf_counter", lambda: next(clock)) + monkeypatch.setattr( + direct_build_time.subprocess, + "run", + lambda *_args, **_kwargs: subprocess.CompletedProcess(("compiler",), 0, "", ""), + ) + monkeypatch.setattr( + direct_build_time, + "verify_build", + lambda route, workdir: verified.append((route, workdir)) or _report(route), + ) + + elapsed, report = direct_build_time.timed_build("prik-direct", tmp_path / "build", compiler="/opt/gfortran", jobs=2) + + assert elapsed == 2.5 + assert verified == [("prik-direct", tmp_path / "build")] + assert report["route"] == "direct_c_abi" + + +def test_direct_build_results_are_separate_and_record_route_membership( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + calls: list[direct_benchmark.Route] = [] + reports = {route: _report(route) for route in direct_benchmark.ROUTES} + monkeypatch.setattr( + direct_build_time, + "preflight", + lambda **_kwargs: reports, + ) + + def fake_timed(route, _workdir, *, compiler, jobs): + assert compiler == "/opt/gfortran" + assert jobs == 4 + calls.append(route) + return (1.0 if route == "prik-direct" else 2.0), reports[route] + + monkeypatch.setattr(direct_build_time, "timed_build", fake_timed) + paths = direct_build_time.run_benchmarks( + runs=2, + warmups=1, + first="prik", + compiler="/opt/gfortran", + jobs=4, + results_root=tmp_path, + ) + + assert [path.name for path in paths] == [ + "prik-direct-build.json", + "f2py-direct-build.json", + "prik-adapted-build.json", + ] + assert calls == [ + "prik-direct", + "f2py-direct", + "prik-adapted", + "prik-direct", + "f2py-direct", + "prik-adapted", + "prik-adapted", + "f2py-direct", + "prik-direct", + ] + for route, path in zip(direct_benchmark.ROUTES, paths, strict=True): + suite = pyperf.BenchmarkSuite.load(str(path)) + assert suite.get_benchmark_names() == ["direct.build.optimized.small"] + assert suite.get_metadata()["binding_tool"] == route + assert suite.get_metadata()["route"] == direct_benchmark.route_action(route) + assert suite.get_metadata()["benchmark_cohort"] == "direct_entrypoint" + assert suite.get_metadata()["artifact_membership"] + + +def test_direct_runtime_uses_identical_cases_and_preflight_metadata( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + observed: dict[str, object] = {"names": []} + + class FakeRunner: + def __init__(self, **kwargs): + observed.update(kwargs) + + def timeit(self, name: str, **_kwargs) -> None: + observed["names"].append(name) + + api = SimpleNamespace( + noop=lambda: None, + add_scalars=lambda a, b: np.float64(a + b), + add_scalars_out=lambda a, b: np.float64(a + b), + ) + report_path = tmp_path / "preflight.json" + report_path.write_text(json.dumps({"prik-direct": _report("prik-direct")}), encoding="utf-8") + monkeypatch.syspath_prepend(str(Path("benchmarks").resolve())) + monkeypatch.setenv("PRIK_DIRECT_BENCHMARK_ROUTE", "prik-direct") + monkeypatch.setenv("PRIK_DIRECT_ORDER_PASS", "forward") + monkeypatch.setenv("PRIK_DIRECT_PREFLIGHT_REPORT", str(report_path)) + monkeypatch.setattr(importlib, "import_module", lambda _name: api) + monkeypatch.setattr(pyperf, "Runner", FakeRunner) + + runpy.run_path(Path("benchmarks/direct_runtime.py"), run_name="__main__") + + assert observed["processes"] == 16 + assert observed["values"] == 4 + assert observed["metadata"]["route"] == "direct_c_abi" + assert observed["metadata"]["wrapper_mode"] == "python_c_binding;no_user_fortran_adapter" + assert observed["metadata"]["artifact_membership"] + assert observed["metadata"]["natural_result_type"] == "numpy.float64" + assert observed["names"] == [ + "direct.call.noop", + "direct.call.scalar_function", + "direct.call.scalar_subroutine", + ] + + +def test_run_and_workflows_keep_direct_results_out_of_default_population() -> None: + run_script = Path("benchmarks/run.sh").read_text(encoding="utf-8") + generator = Path("tools/generate_performance_docs.py").read_text(encoding="utf-8") + workflows = "\n".join( + Path(path).read_text(encoding="utf-8") + for path in (".github/workflows/docs.yml", ".github/workflows/merge-validation.yml") + ) + + assert "python3 direct_preflight.py" in run_script + assert "python3 direct_build_time.py" in run_script + assert "direct_runtime.py" in run_script + assert '--output "results/$binding_tool.json"' in run_script + assert '--output "results/$direct_route.json"' in run_script + assert "prik-adapted.json" in run_script + assert "direct_runtime_passes=(forward reverse)" in run_script + assert "direct_routes=(prik-adapted f2py-direct prik-direct)" in run_script + assert "direct" not in generator.partition("DEFAULT_F2PY_RESULTS")[0] + assert workflows.count("name: direct-entrypoint-preflight") == 2 + assert workflows.count("path: benchmarks/build/direct-runtime") == 2 + for name in ( + "f2py-direct.json", + "prik-direct.json", + "prik-adapted.json", + "f2py-direct-build.json", + "prik-direct-build.json", + "prik-adapted-build.json", + ): + assert workflows.count(name) == 4 diff --git a/tests/tools/test_generate_performance_docs.py b/tests/tools/test_generate_performance_docs.py index 4d202ab4d..2b6491b92 100644 --- a/tests/tools/test_generate_performance_docs.py +++ b/tests/tools/test_generate_performance_docs.py @@ -9,8 +9,11 @@ from tools.generate_performance_docs import ( BUILD_SHARED_METADATA, + DirectPerformancePaths, + DirectPerformanceSnapshots, _format_factor, _format_ratio, + _load_direct_snapshots, generate, load_snapshot, render_build_chart, @@ -113,6 +116,81 @@ def _paired_build_suites(tmp_path: Path) -> tuple[Path, Path]: return f2py, prik +def _direct_paths(tmp_path: Path) -> DirectPerformancePaths: + runtime_metadata = { + "benchmark_cohort": "direct_entrypoint", + "compile_flags": "-O3 -march=native -mtune=native", + "gil_policy": "held", + "runtime_order_protocol": "balanced_three_route_forward_reverse", + } + direct_metadata = {**runtime_metadata, "route": "direct_c_abi"} + adapted_metadata = {**runtime_metadata, "route": "generated_fortran_adapter"} + benchmarks = [ + ("direct.call.noop", [2.0e-6, 2.1e-6, 1.9e-6, 2.05e-6, 1.95e-6]), + ("direct.call.scalar_function", [3.0e-6, 3.1e-6, 2.9e-6, 3.05e-6, 2.95e-6]), + ("direct.call.scalar_subroutine", [4.0e-6, 4.1e-6, 3.9e-6, 4.05e-6, 3.95e-6]), + ] + f2py = _write_suite( + tmp_path / "f2py-direct.json", + "f2py-direct", + benchmarks, + extra_metadata=direct_metadata, + ) + prik = _write_suite( + tmp_path / "prik-direct.json", + "prik-direct", + [(name, [value * 0.8 for value in values]) for name, values in benchmarks], + extra_metadata=direct_metadata, + ) + adapted = _write_suite( + tmp_path / "prik-adapted.json", + "prik-adapted", + [(name, [value * 0.81 for value in values]) for name, values in benchmarks], + extra_metadata=adapted_metadata, + ) + + build_metadata = { + "benchmark_cohort": "direct_entrypoint", + "build_order_protocol": "balanced_three_route_forward_reverse", + "build_runs": 4, + "build_scope": "clean small source-to-extension generation, compilation, and linking", + "build_warmups": 1, + "compile_flags": "-O3 -march=native -mtune=native", + "compiler": "/usr/bin/gfortran", + "prik_build_jobs": 4, + } + build_benchmark = [("direct.build.optimized.small", [2.0, 2.1, 1.9, 2.05, 1.95])] + f2py_build = _write_suite( + tmp_path / "f2py-direct-build.json", + "f2py-direct", + build_benchmark, + extra_metadata={**build_metadata, "route": "direct_c_abi"}, + ) + prik_build = _write_suite( + tmp_path / "prik-direct-build.json", + "prik-direct", + [(name, [value * 0.4 for value in values]) for name, values in build_benchmark], + extra_metadata={**build_metadata, "route": "direct_c_abi"}, + ) + adapted_build = _write_suite( + tmp_path / "prik-adapted-build.json", + "prik-adapted", + [(name, [value * 0.41 for value in values]) for name, values in build_benchmark], + extra_metadata={**build_metadata, "route": "generated_fortran_adapter"}, + ) + return DirectPerformancePaths(f2py, prik, adapted, f2py_build, prik_build, adapted_build) + + +def _direct_snapshots(tmp_path: Path) -> DirectPerformanceSnapshots: + return _load_direct_snapshots( + _direct_paths(tmp_path), + operating_system=TEST_OS, + compiler_version="GNU Fortran 13.3.0", + commit="1234567890abcdef", + recorded_date=date(2026, 8, 1), + ) + + def _page_template() -> str: return """before @@ -123,10 +201,18 @@ def _page_template() -> str: old table between table and build + +old direct results + +between direct and build old build results -between build and environment +between build and direct build + +old direct build results + +between direct build and environment old environment @@ -145,8 +231,8 @@ def test_load_snapshot_classifies_results_and_formats_public_values(tmp_path: Pa commit="1234567890abcdef", ) - assert [result.outcome for result in snapshot.results] == ["prik", "f2py", "parity"] - assert snapshot.results[0].f2py_display == "2.00 µs" + assert [result.outcome for result in snapshot.results] == ["candidate", "reference", "parity"] + assert snapshot.results[0].reference_display == "2.00 µs" assert snapshot.results[2].table_label == "Increment vector, 1 element" assert snapshot.recorded_date == date(2026, 8, 1) assert snapshot.compiler_version == "GNU Fortran 13.3.0" @@ -178,8 +264,9 @@ def test_render_page_updates_only_marked_blocks(tmp_path: Path) -> None: commit="1234567890abcdef", metadata_keys=BUILD_SHARED_METADATA, ) + direct_snapshots = _direct_snapshots(tmp_path) - rendered = render_page(_page_template(), snapshot, build_snapshot) + rendered = render_page(_page_template(), snapshot, build_snapshot, direct_snapshots) assert rendered.startswith("before\n") assert rendered.endswith("after\n") @@ -193,6 +280,9 @@ def test_render_page_updates_only_marked_blocks(tmp_path: Path) -> None: assert "Optimized (`-O3 -march=native -mtune=native`) · small module" in rendered assert "Optimized (`-O3 -march=native -mtune=native`) · full reference BLAS" in rendered assert "mean of 4 clean builds after 1 untimed warm-up" in rendered + assert "| Workload | f2py direct | PRIK direct | Relative result |" in rendered + assert "| Workload | PRIK adapted | PRIK direct | Relative result |" in rendered + assert "small direct module (1 source, 3 procedures)" in rendered assert "equal PRIK-first and f2py-first process budgets" in rendered assert "up to 4 concurrent compiler" in rendered assert "f2py uses its normal Meson/Ninja scheduler" in rendered @@ -222,12 +312,14 @@ def test_render_page_rejects_missing_or_duplicate_markers(tmp_path: Path) -> Non commit="1234567890abcdef", metadata_keys=BUILD_SHARED_METADATA, ) + direct_snapshots = _direct_snapshots(tmp_path) with pytest.raises(ValueError, match="exactly one 'summary' marker pair"): render_page( _page_template().replace("", ""), snapshot, build_snapshot, + direct_snapshots, ) @@ -309,9 +401,43 @@ def test_load_snapshot_rejects_swapped_tool_results(tmp_path: Path) -> None: ) +def test_direct_snapshot_rejects_route_metadata_that_disagrees_with_identity(tmp_path: Path) -> None: + paths = _direct_paths(tmp_path) + invalid_f2py = _write_suite( + tmp_path / "invalid-f2py-direct.json", + "f2py-direct", + [("direct.call.noop", [2.0e-6, 2.1e-6, 1.9e-6])], + extra_metadata={ + "benchmark_cohort": "direct_entrypoint", + "compile_flags": "-O3 -march=native -mtune=native", + "gil_policy": "held", + "route": "generated_fortran_adapter", + "runtime_order_protocol": "balanced_three_route_forward_reverse", + }, + ) + invalid_paths = DirectPerformancePaths( + invalid_f2py, + paths.prik, + paths.adapted, + paths.f2py_build, + paths.prik_build, + paths.adapted_build, + ) + + with pytest.raises(ValueError, match="expected metadata route='direct_c_abi'"): + _load_direct_snapshots( + invalid_paths, + operating_system=TEST_OS, + compiler_version="GNU Fortran 13.3.0", + commit="1234567890abcdef", + recorded_date=date(2026, 8, 1), + ) + + def test_generate_writes_page_and_chart(tmp_path: Path) -> None: f2py, prik = _paired_suites(tmp_path) f2py_build, prik_build = _paired_build_suites(tmp_path) + direct_paths = _direct_paths(tmp_path) page = tmp_path / "performance.md" chart = tmp_path / "assets/performance.svg" build_chart = tmp_path / "assets/build-time.svg" @@ -322,6 +448,7 @@ def test_generate_writes_page_and_chart(tmp_path: Path) -> None: prik, f2py_build, prik_build, + direct_paths, page, chart, build_chart, @@ -341,7 +468,7 @@ def test_generate_writes_page_and_chart(tmp_path: Path) -> None: def test_current_performance_page_has_one_complete_marker_pair_per_generated_block() -> None: page = Path("docs/user/performance.md").read_text(encoding="utf-8") - for name in ("summary", "table", "build", "environment"): + for name in ("summary", "table", "direct", "build", "direct-build", "environment"): assert page.count(f"") == 1 assert page.count(f"") == 1 diff --git a/tools/generate_performance_docs.py b/tools/generate_performance_docs.py index 5684e2014..707cadf89 100644 --- a/tools/generate_performance_docs.py +++ b/tools/generate_performance_docs.py @@ -25,13 +25,19 @@ DEFAULT_PRIK_RESULTS = REPOSITORY_ROOT / "benchmarks/results/prik.json" DEFAULT_F2PY_BUILD_RESULTS = REPOSITORY_ROOT / "benchmarks/results/f2py-build.json" DEFAULT_PRIK_BUILD_RESULTS = REPOSITORY_ROOT / "benchmarks/results/prik-build.json" +DEFAULT_F2PY_DIRECT_RESULTS = REPOSITORY_ROOT / "benchmarks/results/f2py-direct.json" +DEFAULT_PRIK_DIRECT_RESULTS = REPOSITORY_ROOT / "benchmarks/results/prik-direct.json" +DEFAULT_PRIK_ADAPTED_RESULTS = REPOSITORY_ROOT / "benchmarks/results/prik-adapted.json" +DEFAULT_F2PY_DIRECT_BUILD_RESULTS = REPOSITORY_ROOT / "benchmarks/results/f2py-direct-build.json" +DEFAULT_PRIK_DIRECT_BUILD_RESULTS = REPOSITORY_ROOT / "benchmarks/results/prik-direct-build.json" +DEFAULT_PRIK_ADAPTED_BUILD_RESULTS = REPOSITORY_ROOT / "benchmarks/results/prik-adapted-build.json" DEFAULT_PAGE = REPOSITORY_ROOT / "docs/user/performance.md" DEFAULT_CHART = REPOSITORY_ROOT / "docs/user/assets/performance-comparison.svg" DEFAULT_BUILD_CHART = REPOSITORY_ROOT / "docs/user/assets/build-time-comparison.svg" COMPILE_FLAGS = "-O3 -march=native -mtune=native" TIMES = "\N{MULTIPLICATION SIGN}" _STANDALONE_C = re.compile(r"(? float: return math.exp(sum(math.log(result.ratio) for result in self.results) / len(self.results)) @property - def prik_wins(self) -> tuple[BenchmarkResult, ...]: - return tuple(result for result in self.results if result.outcome == "prik") + def candidate_wins(self) -> tuple[BenchmarkResult, ...]: + return tuple(result for result in self.results if result.outcome == "candidate") @property - def f2py_wins(self) -> tuple[BenchmarkResult, ...]: - return tuple(result for result in self.results if result.outcome == "f2py") + def reference_wins(self) -> tuple[BenchmarkResult, ...]: + return tuple(result for result in self.results if result.outcome == "reference") @property def parity_results(self) -> tuple[BenchmarkResult, ...]: return tuple(result for result in self.results if result.outcome == "parity") +@dataclass(frozen=True) +class DirectPerformanceSnapshots: + runtime: PerformanceSnapshot + runtime_control: PerformanceSnapshot + build: PerformanceSnapshot + build_control: PerformanceSnapshot + + +@dataclass(frozen=True) +class DirectPerformancePaths: + f2py: Path + prik: Path + adapted: Path + f2py_build: Path + prik_build: Path + adapted_build: Path + + def _format_factor(factor: float) -> str: precision = 3 if factor < 1.01 else 2 return f"{factor:.{precision}f}{TIMES}" @@ -123,6 +170,13 @@ def _procedure_labels(name: str) -> tuple[str, str]: fixed = { "call.noop": ("Empty function call", "Empty call"), "call.add_scalars": ("Add two scalars", "Add scalars"), + "direct.call.noop": ("Empty call", "Empty call"), + "direct.call.scalar_function": ("Scalar function", "Scalar function"), + "direct.call.scalar_subroutine": ("Scalar subroutine", "Scalar subroutine"), + "direct.build.optimized.small": ( + "Optimized (`-O3 -march=native -mtune=native`) · small direct module (1 source, 3 procedures)", + "Optimized · small direct module", + ), "build.development.small_module": ( "Development (`-O0`) · small module (1 source, 5 procedures)", "Development · small module", @@ -159,11 +213,11 @@ def _procedure_labels(name: str) -> tuple[str, str]: return readable, readable -def _outcome(f2py_benchmark: pyperf.Benchmark, prik_benchmark: pyperf.Benchmark) -> Outcome: - significant, _score = is_significant_benchs(f2py_benchmark, prik_benchmark) +def _outcome(reference: pyperf.Benchmark, candidate: pyperf.Benchmark) -> Outcome: + significant, _score = is_significant_benchs(reference, candidate) if not significant: return "parity" - return "prik" if f2py_benchmark.mean() > prik_benchmark.mean() else "f2py" + return "candidate" if reference.mean() > candidate.mean() else "reference" def _format_benchmark_value(benchmark: pyperf.Benchmark, value: float) -> str: @@ -171,21 +225,21 @@ def _format_benchmark_value(benchmark: pyperf.Benchmark, value: float) -> str: def _compatible_metadata( - f2py_suite: pyperf.BenchmarkSuite, - prik_suite: pyperf.BenchmarkSuite, + reference_suite: pyperf.BenchmarkSuite, + candidate_suite: pyperf.BenchmarkSuite, keys: tuple[str, ...], ) -> dict[str, object]: - f2py_metadata = f2py_suite.get_metadata() - prik_metadata = prik_suite.get_metadata() + reference_metadata = reference_suite.get_metadata() + candidate_metadata = candidate_suite.get_metadata() shared: dict[str, object] = {} for key in keys: - f2py_value = f2py_metadata.get(key) - prik_value = prik_metadata.get(key) - if f2py_value is None or prik_value is None: + reference_value = reference_metadata.get(key) + candidate_value = candidate_metadata.get(key) + if reference_value is None or candidate_value is None: raise ValueError(f"paired pyperf results are missing required metadata {key!r}") - if f2py_value != prik_value: + if reference_value != candidate_value: raise ValueError(f"paired pyperf results disagree on metadata {key!r}") - shared[key] = f2py_value + shared[key] = reference_value return shared @@ -195,53 +249,67 @@ def _validate_suite_identity(suite: pyperf.BenchmarkSuite, expected: str) -> Non raise ValueError(f"expected {expected!r} results, found binding_tool={actual!r}") +def _validate_suite_metadata(suite: pyperf.BenchmarkSuite, expected: dict[str, object]) -> None: + metadata = suite.get_metadata() + for key, expected_value in expected.items(): + actual = metadata.get(key) + if actual != expected_value: + raise ValueError(f"expected metadata {key}={expected_value!r}, found {actual!r}") + + def load_snapshot( - f2py_path: Path, - prik_path: Path, + reference_path: Path, + candidate_path: Path, *, operating_system: str, compiler_version: str, commit: str, recorded_date: date | None = None, metadata_keys: tuple[str, ...] = SHARED_METADATA, + reference_identity: str = "f2py", + candidate_identity: str = "prik", + reference_metadata: dict[str, object] | None = None, + candidate_metadata: dict[str, object] | None = None, ) -> PerformanceSnapshot: """Load and validate one paired benchmark snapshot.""" - f2py_suite = pyperf.BenchmarkSuite.load(str(f2py_path)) - prik_suite = pyperf.BenchmarkSuite.load(str(prik_path)) - _validate_suite_identity(f2py_suite, "f2py") - _validate_suite_identity(prik_suite, "prik") - f2py_names = f2py_suite.get_benchmark_names() - prik_names = prik_suite.get_benchmark_names() - if f2py_names != prik_names: + reference_suite = pyperf.BenchmarkSuite.load(str(reference_path)) + candidate_suite = pyperf.BenchmarkSuite.load(str(candidate_path)) + _validate_suite_identity(reference_suite, reference_identity) + _validate_suite_identity(candidate_suite, candidate_identity) + _validate_suite_metadata(reference_suite, reference_metadata or {}) + _validate_suite_metadata(candidate_suite, candidate_metadata or {}) + reference_names = reference_suite.get_benchmark_names() + candidate_names = candidate_suite.get_benchmark_names() + if reference_names != candidate_names: raise ValueError("paired pyperf results must contain the same benchmarks in the same order") - if not f2py_names: + if not reference_names: raise ValueError("paired pyperf results contain no benchmarks") results = [] - for name in f2py_names: - f2py_benchmark = f2py_suite.get_benchmark(name) - prik_benchmark = prik_suite.get_benchmark(name) - f2py_value = f2py_benchmark.mean() - prik_value = prik_benchmark.mean() + for name in reference_names: + reference = reference_suite.get_benchmark(name) + candidate = candidate_suite.get_benchmark(name) + reference_value = reference.mean() + candidate_value = candidate.mean() table_label, chart_label = _procedure_labels(name) results.append( BenchmarkResult( name=name, table_label=table_label, chart_label=chart_label, - f2py_value=f2py_value, - prik_value=prik_value, - f2py_display=_format_benchmark_value(f2py_benchmark, f2py_value), - prik_display=_format_benchmark_value(prik_benchmark, prik_value), - ratio=f2py_value / prik_value, - outcome=_outcome(f2py_benchmark, prik_benchmark), + reference_value=reference_value, + candidate_value=candidate_value, + reference_display=_format_benchmark_value(reference, reference_value), + candidate_display=_format_benchmark_value(candidate, candidate_value), + ratio=reference_value / candidate_value, + outcome=_outcome(reference, candidate), ) ) - latest_date = max(f2py_suite.get_dates()[1], prik_suite.get_dates()[1]).date() + latest_date = max(reference_suite.get_dates()[1], candidate_suite.get_dates()[1]).date() return PerformanceSnapshot( results=tuple(results), - metadata=_compatible_metadata(f2py_suite, prik_suite, metadata_keys), + metadata=_compatible_metadata(reference_suite, candidate_suite, metadata_keys), recorded_date=recorded_date or latest_date, operating_system=operating_system, compiler_version=_compiler_display_name(compiler_version), @@ -269,8 +337,8 @@ def _geometric_sentence(snapshot: PerformanceSnapshot) -> str: def _outcome_sentence(snapshot: PerformanceSnapshot) -> str: total = len(snapshot.results) - prik_count = len(snapshot.prik_wins) - f2py_count = len(snapshot.f2py_wins) + prik_count = len(snapshot.candidate_wins) + f2py_count = len(snapshot.reference_wins) parity_count = len(snapshot.parity_results) comparison = f"Across {total} workloads, PRIK was faster in {prik_count} and f2py in {f2py_count}" if parity_count: @@ -281,7 +349,7 @@ def _outcome_sentence(snapshot: PerformanceSnapshot) -> str: def _summary_markdown(snapshot: PerformanceSnapshot) -> str: geometric_value, geometric_label = _geometric_result(snapshot) - best = max(snapshot.prik_wins, key=lambda result: result.factor, default=None) + best = max(snapshot.candidate_wins, key=lambda result: result.factor, default=None) best_value = _format_factor(best.factor) if best else "—" best_label = "best measured PRIK speedup" if best else "no measured PRIK speedup" total = len(snapshot.results) @@ -301,7 +369,7 @@ def _summary_markdown(snapshot: PerformanceSnapshot) -> str: f" {geometric_label}", " ", '
', - f" {len(snapshot.prik_wins)} of {total}", + f" {len(snapshot.candidate_wins)} of {total}", " workloads faster with PRIK", "
", '
', @@ -313,10 +381,15 @@ def _summary_markdown(snapshot: PerformanceSnapshot) -> str: ) -def _relative_result(result: BenchmarkResult) -> str: +def _relative_result( + result: BenchmarkResult, + *, + reference_label: str = "f2py", + candidate_label: str = "PRIK", +) -> str: if result.outcome == "parity": return "No significant difference" - winner = "PRIK" if result.outcome == "prik" else result.outcome + winner = candidate_label if result.outcome == "candidate" else reference_label return f"{winner} {_format_factor(result.factor)} faster" @@ -324,44 +397,127 @@ def _table_value(value: str, *, winner: bool) -> str: return f"**{value}**" if winner else value -def _geometric_table_result(snapshot: PerformanceSnapshot) -> str: +def _geometric_table_result( + snapshot: PerformanceSnapshot, + *, + reference_label: str = "f2py", + candidate_label: str = "PRIK", +) -> str: ratio = snapshot.geometric_mean_ratio if math.isclose(ratio, 1.0, rel_tol=0.005): return "**At parity**" if ratio > 1.0: - return f"**PRIK {ratio:.2f}{TIMES} faster**" - return f"**f2py {1.0 / ratio:.2f}{TIMES} faster**" + return f"**{candidate_label} {ratio:.2f}{TIMES} faster**" + return f"**{reference_label} {1.0 / ratio:.2f}{TIMES} faster**" -def _table_markdown(snapshot: PerformanceSnapshot) -> str: +def _comparison_table_markdown( + snapshot: PerformanceSnapshot, + *, + reference_label: str, + candidate_label: str, + include_geometric_mean: bool, + first_column: str = "Workload", +) -> str: rows = [ - "| Workload | f2py | PRIK | Relative result |", + f"| {first_column} | {reference_label} | {candidate_label} | Relative result |", "| --- | ---: | ---: | ---: |", ] for result in snapshot.results: - f2py_value = _table_value(result.f2py_display, winner=result.outcome == "f2py") - prik_value = _table_value(result.prik_display, winner=result.outcome == "prik") - rows.append(f"| {result.table_label} | {f2py_value} | {prik_value} | {_relative_result(result)} |") - rows.append(f"| **Geometric mean** | reference | — | {_geometric_table_result(snapshot)} |") + reference_value = _table_value(result.reference_display, winner=result.outcome == "reference") + candidate_value = _table_value(result.candidate_display, winner=result.outcome == "candidate") + relative = _relative_result( + result, + reference_label=reference_label, + candidate_label=candidate_label, + ) + rows.append(f"| {result.table_label} | {reference_value} | {candidate_value} | {relative} |") + if include_geometric_mean: + geometric = _geometric_table_result( + snapshot, + reference_label=reference_label, + candidate_label=candidate_label, + ) + rows.append(f"| **Geometric mean** | reference | — | {geometric} |") return "\n".join(rows) +def _table_markdown(snapshot: PerformanceSnapshot) -> str: + return _comparison_table_markdown( + snapshot, + reference_label="f2py", + candidate_label="PRIK", + include_geometric_mean=True, + ) + + def _build_markdown(snapshot: PerformanceSnapshot) -> str: runs = int(snapshot.metadata["build_runs"]) warmups = int(snapshot.metadata["build_warmups"]) rows = [ f"Each value is the mean of {runs} clean builds after {warmups} untimed warm-up{'s' if warmups != 1 else ''}.", "", - "| Clean build workload | f2py | PRIK | Relative result |", - "| --- | ---: | ---: | ---: |", + _comparison_table_markdown( + snapshot, + reference_label="f2py", + candidate_label="PRIK", + include_geometric_mean=False, + first_column="Clean build workload", + ), ] - for result in snapshot.results: - f2py_value = _table_value(result.f2py_display, winner=result.outcome == "f2py") - prik_value = _table_value(result.prik_display, winner=result.outcome == "prik") - rows.append(f"| {result.table_label} | {f2py_value} | {prik_value} | {_relative_result(result)} |") return "\n".join(rows) +def _direct_markdown(snapshots: DirectPerformanceSnapshots) -> str: + return "\n\n".join( + ( + "### Direct PRIK and f2py\n\n" + + _comparison_table_markdown( + snapshots.runtime, + reference_label="f2py direct", + candidate_label="PRIK direct", + include_geometric_mean=True, + ), + "### PRIK adapter control\n\n" + + _comparison_table_markdown( + snapshots.runtime_control, + reference_label="PRIK adapted", + candidate_label="PRIK direct", + include_geometric_mean=True, + ), + ) + ) + + +def _direct_build_markdown(snapshots: DirectPerformanceSnapshots) -> str: + runs = int(snapshots.build.metadata["build_runs"]) + warmups = int(snapshots.build.metadata["build_warmups"]) + introduction = ( + f"Each value is the mean of {runs} clean builds after {warmups} untimed warm-up{'s' if warmups != 1 else ''}." + ) + return "\n\n".join( + ( + introduction, + "### Direct PRIK and f2py\n\n" + + _comparison_table_markdown( + snapshots.build, + reference_label="f2py direct", + candidate_label="PRIK direct", + include_geometric_mean=False, + first_column="Clean build workload", + ), + "### PRIK adapter control\n\n" + + _comparison_table_markdown( + snapshots.build_control, + reference_label="PRIK adapted", + candidate_label="PRIK direct", + include_geometric_mean=False, + first_column="Clean build workload", + ), + ) + ) + + def _month_date(value: date) -> str: months = ( "January", @@ -402,11 +558,13 @@ def _environment_markdown(snapshot: PerformanceSnapshot, build_snapshot: Perform "- Both interfaces keep the GIL held.", "- OpenMP, OpenBLAS, and MKL are limited to one thread.", f"- `pyperf --rigorous` pins each benchmark to logical CPU `{affinity}`.", - "- Runtime samples combine equal PRIK-first and f2py-first process budgets.", + "- Normal runtime samples combine equal PRIK-first and f2py-first process budgets.", + "- Direct runtime samples use balanced forward and reverse PRIK-direct,", + " f2py-direct, and PRIK-adapted process order.", f"- PRIK build timings use up to {int(build_snapshot.metadata['prik_build_jobs'])} concurrent compiler", " processes; f2py uses its normal Meson/Ninja scheduler.", - "- Build timings alternate tool order, use clean output directories, and exclude", - " post-build import checks.", + "- Normal and three-route direct build timings alternate tool order, use clean", + " output directories, and exclude post-build import checks.", f"- CPU: {_cpu_model_text(snapshot.metadata)}.", f"- Operating system: {operating_system}.", f"- Kernel/platform: `{_metadata_text(snapshot.metadata, 'platform_details')}`.", @@ -437,12 +595,15 @@ def render_page( markdown: str, snapshot: PerformanceSnapshot, build_snapshot: PerformanceSnapshot, + direct_snapshots: DirectPerformanceSnapshots, ) -> str: """Replace only the generated blocks in a Performance page.""" replacements = { "summary": _summary_markdown(snapshot), "table": _table_markdown(snapshot), + "direct": _direct_markdown(direct_snapshots), "build": _build_markdown(build_snapshot), + "direct-build": _direct_build_markdown(direct_snapshots), "environment": _environment_markdown(snapshot, build_snapshot), } for name in MARKER_NAMES: @@ -507,7 +668,7 @@ def x_position(value: float) -> float: ' ', ( f" Relative speed across {len(snapshot.results)} benchmarks. Values above one indicate PRIK is faster. " - f"PRIK is faster in {len(snapshot.prik_wins)} benchmarks." + f"PRIK is faster in {len(snapshot.candidate_wins)} benchmarks." ), " ", f' ', @@ -537,7 +698,7 @@ def x_position(value: float) -> float: ) lines.extend([" ", ' ']) - colors = {"prik": "#0f766e", "f2py": "#b45309", "parity": "#64748b"} + colors = {"candidate": "#0f766e", "reference": "#b45309", "parity": "#64748b"} for index, result in enumerate(snapshot.results): y = row_start + index * row_step point = x_position(result.ratio) @@ -583,7 +744,7 @@ def x_position(value: float) -> float: def _duration_axis_upper(results: tuple[BenchmarkResult, ...]) -> float: - maximum = max(max(result.f2py_value, result.prik_value) for result in results) + maximum = max(max(result.reference_value, result.candidate_value) for result in results) rough_step = maximum / 5.0 magnitude = 10 ** math.floor(math.log10(rough_step)) if rough_step > 0 else 1.0 normalized = rough_step / magnitude @@ -641,17 +802,17 @@ def x_position(value: float) -> float: for index, result in enumerate(snapshot.results): group_y = top + index * group_step - f2py_width = x_position(result.f2py_value) - plot_left - prik_width = x_position(result.prik_value) - plot_left + f2py_width = x_position(result.reference_value) - plot_left + prik_width = x_position(result.candidate_value) - plot_left lines.extend( [ f' {escape(result.chart_label)}', f' f2py', f' ', - f' {escape(result.f2py_display)}', + f' {escape(result.reference_display)}', f' PRIK', f' ', - f' {escape(result.prik_display)}', + f' {escape(result.candidate_display)}', ] ) lines.extend( @@ -682,6 +843,90 @@ def _command_first_line(argv: list[str], *, description: str) -> str: return first_line +def _require_matching_snapshot_metadata( + reference: PerformanceSnapshot, + snapshots: tuple[PerformanceSnapshot, ...], + keys: tuple[str, ...], +) -> None: + for snapshot in snapshots: + for key in keys: + if reference.metadata[key] != snapshot.metadata[key]: + raise ValueError(f"published benchmark cohorts disagree on metadata {key!r}") + if reference.recorded_date != snapshot.recorded_date: + raise ValueError("published benchmark cohorts were not recorded on the same date") + + +def _direct_route_metadata(route: str, *, runtime: bool) -> dict[str, object]: + expected: dict[str, object] = { + "benchmark_cohort": "direct_entrypoint", + "compile_flags": COMPILE_FLAGS, + "route": route, + } + if runtime: + expected["gil_policy"] = "held" + return expected + + +def _load_direct_snapshots( + paths: DirectPerformancePaths, + *, + operating_system: str, + compiler_version: str, + commit: str, + recorded_date: date | None, +) -> DirectPerformanceSnapshots: + common = { + "operating_system": operating_system, + "compiler_version": compiler_version, + "commit": commit, + "recorded_date": recorded_date, + } + direct_metadata = _direct_route_metadata("direct_c_abi", runtime=True) + adapted_metadata = _direct_route_metadata("generated_fortran_adapter", runtime=True) + direct_build_metadata = _direct_route_metadata("direct_c_abi", runtime=False) + adapted_build_metadata = _direct_route_metadata("generated_fortran_adapter", runtime=False) + return DirectPerformanceSnapshots( + runtime=load_snapshot( + paths.f2py, + paths.prik, + **common, + reference_identity="f2py-direct", + candidate_identity="prik-direct", + reference_metadata=direct_metadata, + candidate_metadata=direct_metadata, + ), + runtime_control=load_snapshot( + paths.adapted, + paths.prik, + **common, + reference_identity="prik-adapted", + candidate_identity="prik-direct", + reference_metadata=adapted_metadata, + candidate_metadata=direct_metadata, + ), + build=load_snapshot( + paths.f2py_build, + paths.prik_build, + **common, + metadata_keys=DIRECT_BUILD_SHARED_METADATA, + reference_identity="f2py-direct", + candidate_identity="prik-direct", + reference_metadata=direct_build_metadata, + candidate_metadata=direct_build_metadata, + ), + build_control=load_snapshot( + paths.adapted_build, + paths.prik_build, + **common, + metadata_keys=DIRECT_BUILD_SHARED_METADATA, + reference_identity="prik-adapted", + candidate_identity="prik-direct", + reference_metadata=adapted_build_metadata, + candidate_metadata=direct_build_metadata, + ), + ) + + def _operating_system_name() -> str: try: release = platform.freedesktop_os_release() @@ -695,6 +940,7 @@ def generate( prik_path: Path, f2py_build_path: Path, prik_build_path: Path, + direct_paths: DirectPerformancePaths, page_path: Path, chart_path: Path, build_chart_path: Path, @@ -703,7 +949,7 @@ def generate( compiler_version: str, commit: str, recorded_date: date | None = None, -) -> PerformanceSnapshot: +) -> tuple[PerformanceSnapshot, DirectPerformanceSnapshots]: """Generate the marked page sections and SVG from paired results.""" snapshot = load_snapshot( f2py_path, @@ -722,14 +968,31 @@ def generate( recorded_date=recorded_date, metadata_keys=BUILD_SHARED_METADATA, ) + direct_snapshots = _load_direct_snapshots( + direct_paths, + operating_system=operating_system, + compiler_version=compiler_version, + commit=commit, + recorded_date=recorded_date, + ) + _require_matching_snapshot_metadata( + snapshot, + (direct_snapshots.runtime, direct_snapshots.runtime_control), + PUBLIC_ENVIRONMENT_METADATA, + ) + _require_matching_snapshot_metadata( + build_snapshot, + (direct_snapshots.build, direct_snapshots.build_control), + PUBLIC_BUILD_ENVIRONMENT_METADATA, + ) original_page = page_path.read_text(encoding="utf-8") - generated_page = render_page(original_page, snapshot, build_snapshot) + generated_page = render_page(original_page, snapshot, build_snapshot, direct_snapshots) page_path.write_text(generated_page, encoding="utf-8") chart_path.parent.mkdir(parents=True, exist_ok=True) chart_path.write_text(render_chart(snapshot), encoding="utf-8") build_chart_path.parent.mkdir(parents=True, exist_ok=True) build_chart_path.write_text(render_build_chart(build_snapshot), encoding="utf-8") - return snapshot + return snapshot, direct_snapshots def parse_args(argv: list[str]) -> argparse.Namespace: @@ -738,6 +1001,12 @@ def parse_args(argv: list[str]) -> argparse.Namespace: parser.add_argument("--prik-results", type=Path, default=DEFAULT_PRIK_RESULTS) parser.add_argument("--f2py-build-results", type=Path, default=DEFAULT_F2PY_BUILD_RESULTS) parser.add_argument("--prik-build-results", type=Path, default=DEFAULT_PRIK_BUILD_RESULTS) + parser.add_argument("--f2py-direct-results", type=Path, default=DEFAULT_F2PY_DIRECT_RESULTS) + parser.add_argument("--prik-direct-results", type=Path, default=DEFAULT_PRIK_DIRECT_RESULTS) + parser.add_argument("--prik-adapted-results", type=Path, default=DEFAULT_PRIK_ADAPTED_RESULTS) + parser.add_argument("--f2py-direct-build-results", type=Path, default=DEFAULT_F2PY_DIRECT_BUILD_RESULTS) + parser.add_argument("--prik-direct-build-results", type=Path, default=DEFAULT_PRIK_DIRECT_BUILD_RESULTS) + parser.add_argument("--prik-adapted-build-results", type=Path, default=DEFAULT_PRIK_ADAPTED_BUILD_RESULTS) parser.add_argument("--page", type=Path, default=DEFAULT_PAGE) parser.add_argument("--chart", type=Path, default=DEFAULT_CHART) parser.add_argument("--build-chart", type=Path, default=DEFAULT_BUILD_CHART) @@ -761,11 +1030,19 @@ def main(argv: list[str] | None = None) -> int: ["git", "rev-parse", "HEAD"], description="PRIK revision", ) - snapshot = generate( + snapshot, direct_snapshots = generate( args.f2py_results, args.prik_results, args.f2py_build_results, args.prik_build_results, + DirectPerformancePaths( + f2py=args.f2py_direct_results, + prik=args.prik_direct_results, + adapted=args.prik_adapted_results, + f2py_build=args.f2py_direct_build_results, + prik_build=args.prik_direct_build_results, + adapted_build=args.prik_adapted_build_results, + ), args.page, args.chart, args.build_chart, @@ -779,7 +1056,8 @@ def main(argv: list[str] | None = None) -> int: return 2 print( - f"Generated Performance documentation from {len(snapshot.results)} benchmarks " + f"Generated Performance documentation from {len(snapshot.results)} normal and " + f"{len(direct_snapshots.runtime.results)} direct benchmarks " f"recorded on {snapshot.recorded_date.isoformat()}." ) return 0