Skip to content
Said Hadjout edited this page Aug 25, 2026 · 6 revisions

PRIK Vision

One Semantic Contract. Many Native Languages. Predictable Python Bindings.

PRIK makes accepted values, conversions, validation, ownership, and the public Python API explicit before it generates code.

flowchart LR
    native["Native software<br/>Fortran · C · C++ · Rust · CUDA"]
    contract["Semantic contract<br/>editable .pyi"]
    policy["Completed policy<br/>safe and explicit"]
    python["Python binding<br/>generated"]

    native --> contract --> policy --> python

    classDef nativeStage fill:#8250df,color:#fff,stroke:#6639ba
    classDef contractStage fill:#0969da,color:#fff,stroke:#0550ae
    classDef policyStage fill:#bc4c00,color:#fff,stroke:#953800
    classDef pythonStage fill:#1a7f37,color:#fff,stroke:#116329
    class native nativeStage
    class contract contractStage
    class policy policyStage
    class python pythonStage
Loading

The Goal

Connect native software to Python through one reviewable semantic interface.

PRIK should:

  • wrap native code without requiring implementation changes;
  • expose a natural Python API without losing the exact native call;
  • make conversions, validation, ownership, and lifetime explicit;
  • work from source or from an authoritative ABI contract;
  • connect components from different native toolchains; and
  • reject unsupported or unsafe behavior early.

The intended reach includes Fortran, C, C++, Rust, CUDA and other accelerator models, and more native ecosystems as the shared model matures. The same architecture applies to numerical, infrastructure, database, media, simulation, device, machine-learning, and domain-specific software.

Language names express direction—not current support or implementation order.

Why PRIK?

Calling a native symbol is only the beginning. The difficult part is preserving meaning across Python and native code.

The gap: one editable contract that connects API shape, coercion, validation, ownership, lifetime, destruction, and native lowering.

  • One contract: source frontends and edited .pyi files converge on the same semantic IR.
  • Explicit behavior: conversions, checks, ownership, and API projection are visible and reviewable.
  • Shared architecture: policy is completed once, before any backend emits code.
  • Evidence: public support requires generated, compiled, and executed behavior.
How this complements existing tools

Existing tools solve important problems and may be the simplest choice for a small or manually curated binding.

Approach What it does well
f2py Quickly exposes many numerical Fortran interfaces to Python and NumPy.
SWIG Generates bindings for several source and target languages from interface descriptions.
pybind11 Gives C++ developers detailed control over carefully handwritten Python bindings.
ctypes and CFFI Provide flexible runtime access to C-compatible ABIs.

PRIK does not need to replace every wrapper tool. It is most valuable when the hard problem is preserving meaning across APIs, objects, arrays, ownership, validation, and multiple native implementations—not merely locating a symbol.

Today and Direction

✅ Present foundation 🔭 Design direction
Source-first and contract-first inputs General contract-authorized coercions
Editable .pyi and shared semantic IR General constraints and custom validators
Completed policy and shared planning Broader contract-first parity
Generated CPython bindings Additional frontends and backends
API projection and lifetime policy Reviewable API-style proposals
Runtime checks and compiled evidence Versioned ecosystem extensions

A direction becomes support only after policy, planning, lowering, diagnostics, and compiled runtime evidence exist.

Architecture

All input routes first produce the same semantic IR. Policy completion then turns that shared meaning into an enforceable wrapper contract.

Native sources or interface descriptions      Semantic .pyi contract
                    |                                  |
                    v                                  v
             Source frontends                  Contract frontend
                    +------------------+---------------+
                                       |
                                       v
                                  Semantic IR
                                       |
                                       v
                          Post-IR policy completion
                     +----------------------------------------+
                     | Completes the enforceable              |
                     | semantic contract:                     |
                     |                                        |
                     | - coercions                            |
                     | - constraints and validation           |
                     | - ownership, lifetime and destruction  |
                     | - contract enforcement                 |
                     +----------------------------------------+
                                       |
                                       v
                             Shared wrapper plan
                                       |
                                       v
                              Backend lowering
                                       |
                                       v
                          Generated Python binding
                                       |
                                       v
                                  Python API

The stages have deliberately different responsibilities:

Layer Responsibility
Frontends Record source declarations or load an explicit semantic contract.
Semantic IR Give every frontend's declarations one language-neutral meaning.
Policy completion Decide the enforceable behavior of the Python/native boundary.
Wrapper planning Project completed decisions into a deterministic implementation plan.
Backend lowering Implement the selected call, conversion, validation, and adaptation mechanisms.
Generated extension Execute the completed contract and expose the Python API.

Meaning moves forward. Planning and backend code must not silently reinterpret the contract or invent policy that should have been decided earlier.

The Two Canonical Representations

PRIK has an external representation and an internal representation:

  • The semantic .pyi file is the canonical, editable external contract.
  • Semantic IR is the canonical internal representation consumed by policy.

A source-first workflow can generate a starter contract:

Native declaration
        |
        v
Source frontend
        |
        v
Semantic IR
        |
        v
Generated semantic .pyi
        |
        v
User review or edit
        |
        v
Contract frontend
        |
        v
Semantic IR

The generated contract is not merely a typing stub. It records the public surface and the native relationship needed to implement it. When an edited .pyi is supplied as the build input, it is authoritative: declarations that the user removes are not silently recovered from source.

The contract can describe:

  • public modules, classes, functions, methods, properties, and overloads;
  • Python-facing names and native symbols;
  • parameter and result types, shapes, layouts, and optionality;
  • how Python arguments map to native arguments;
  • hidden inputs and projected outputs;
  • mutation, writeback, and error projection;
  • ownership, borrowing, transfer, and destruction; and
  • allowed coercions and constraints as those capabilities become available.

The contract is language-neutral at the Python boundary, but it does not erase native facts. Exact symbol names, argument order, ABI identity, storage category, and target-derived type information must remain available wherever correct lowering requires them.

Coercions

A coercion is a contract-authorized transformation of a caller-provided Python value into the semantic value accepted by an argument.

A coercion may:

  • cast a scalar to an accepted numeric type;
  • convert a Python sequence into an array;
  • change an array dtype when the contract permits it;
  • copy an array to satisfy layout, alignment, or mutability requirements;
  • encode text into an accepted native character representation;
  • normalize a value into another accepted representation; or
  • construct an appropriate semantic object from another Python object.

The target type remains the function annotation. An allowed source type is additional contract metadata:

Illustrative vision syntax: From(...) and its options below show the intended contract model. General contract-authored coercions are not current runtime support.

import numpy as np

from prik.contracts import Annotated, Float64, From

def normalize(
    scale: Annotated[Float64, From(int)],
    values: Annotated[
        Float64[:],
        From(list),
        From(np.ndarray),
    ],
) -> Float64[:]: ...

Here Float64 and Float64[:] are the semantic target types. The contract allows int -> Float64, list -> Float64[:], and np.ndarray -> Float64[:] only through the declared policies. A list must be copied into array storage; an already compatible array may remain zero-copy. No other input type becomes acceptable merely because a conversion exists somewhere in the runtime.

Coercion is not permission for arbitrary conversion. Every allowed path must be:

  • explicit in the contract or selected profile;
  • deterministic;
  • safe with respect to precision, shape, and meaning;
  • transparent about allocation and copying;
  • explicit about ownership, lifetime, mutation, and writeback; and
  • rejected with a useful diagnostic when it cannot preserve the contract.

Constraints and Validation

A constraint expresses a condition that must be true. Validation is the act of checking that condition at a defined boundary.

Constraints may describe:

  • dtype, rank, shape, or layout;
  • alignment, contiguity, mutability, or device placement;
  • nullability, presence, or storage capacity;
  • finiteness, positivity, bounds, or another value-domain rule;
  • relationships between multiple arguments;
  • conditions promised for a result;
  • allowed aliasing or mutation; and
  • invariants that an object must preserve over its lifetime.

A local constraint appears directly beside the semantic type:

from prik.contracts import Annotated, Bounded, Finite, Float64, Int32

def interpolate(
    values: Annotated[Float64[:], Finite],
    order: Annotated[Int32, Bounded(1, 8)],
) -> Annotated[Float64[:], Finite]: ...

This contract says that every input element and returned element must be finite, while order must be between 1 and 8. PRIK can parse and preserve this constraint vocabulary today.

Validation is phase-aware. Not every condition belongs after coercion.

Python input
    |
    v
Check whether an allowed coercion applies
    |
    v
Perform contract-authorized coercion
    |
    v
Validate call preconditions
    |
    v
Marshal the accepted value for the native ABI
    |
    v
Native call
    |
    v
Interpret native outputs and mutations
    |
    v
Validate postconditions and invariants
    |
    v
Expose Python results and complete writeback

A check may run before coercion when it determines whether a conversion is applicable or whether conversion would lose meaning. Preconditions on the adapted semantic value run before the native call. Result conditions, postconditions, and object invariants run after the native call and before the result is exposed where practical.

Built-in constraints

Common constraints should use declarative vocabulary understood by PRIK. This allows policy to validate combinations early and lets generated code implement efficient checks with consistent diagnostics.

PRIK already enforces concrete boundary checks for supported forms, including documented dtype, rank, shape, layout, alignment, contiguity, and mutability requirements. General value constraints such as Finite and Bounded, and a general-purpose runtime validator system, remain design directions.

User-defined validation

The vision also permits domain-specific validation through named, reusable validators. A validator may inspect one value or express a relationship among arguments, results, or object state.

Inline preconditions and postconditions make cross-value rules visible where the callable is declared:

Illustrative vision syntax: @contract, its context object, and lambda predicates below describe the intended model.

from prik.contracts import Float64, contract

@contract(
    pre=(
        lambda ctx: ctx.args.A.shape[0] == ctx.args.A.shape[1],
        lambda ctx: ctx.args.b.shape == (ctx.args.A.shape[0],),
    ),
    post=(
        lambda ctx: ctx.result.shape == ctx.args.b.shape,
    ),
)
def solve(A: Float64[:, :], b: Float64[:]) -> Float64[:]: ...

The two preconditions run after allowed coercions and before the native call: A must be square and b must match its row count. The postcondition checks the projected result after the call.

Larger rules can be implemented once and referenced by name instead of being repeated as lambdas:

from project.validators import solution_matches_rhs, square_linear_system
from prik.contracts import Float64, contract

@contract(
    pre=(square_linear_system,),
    post=(solution_matches_rhs,),
)
def solve(A: Float64[:, :], b: Float64[:]) -> Float64[:]: ...

The named functions would live in an explicitly loaded project module or validator registry. The .pyi records which rules apply; their implementation does not become an unrestricted function body hidden inside the contract.

Each custom validator must make the following explicit:

  • the values it may inspect;
  • whether it runs before coercion, before the call, after the call, or at more than one phase;
  • whether it is a precondition, postcondition, or invariant;
  • the diagnostic produced when it fails; and
  • whether it requires Python execution and the GIL.

Validators should normally be deterministic and free of side effects. They may verify contract behavior, but they may not silently change ABI, ownership, or lifetime policy. A .pyi contract should reference a validator through a stable declaration or registry rather than embedding unrestricted executable function bodies inside the interface file.

Ownership, Lifetime, and Destruction

Native interoperability is unsafe when a wrapper cannot answer three questions:

  1. Who owns this storage?
  2. How long must it remain alive?
  3. Who destroys or releases it?

PRIK treats those answers as semantic policy, not backend cleanup guesses.

The contract and completed policy distinguish:

  • wrapper-owned and native-owned values;
  • borrowed views and transferred ownership;
  • owner retention for dependent views;
  • temporary storage created by coercion or ABI adaptation;
  • deterministic destruction of owned native objects;
  • protection against destroying borrowed objects; and
  • cleanup after partial construction, native errors, or validation failures.

Current contract vocabulary can make caller ownership and in-place mutation explicit:

from prik.contracts import Annotated, Destruction, Float64, Ownership, Transfer

def scale_in_place(
    values: Annotated[
        Float64[:],
        Ownership("caller"),
        Transfer("in_place"),
        Destruction("caller"),
    ],
) -> None: ...

The native call may mutate values, but the wrapper must not destroy its storage.

An owning Workspace wrapper invokes release when its native instance must be destroyed. A borrowed wrapper does not.

Coercion, validation, and zero-copy behavior cannot be designed independently of ownership. A zero-copy view is only correct while its owner remains alive; a copied conversion needs a defined owner; and a returned native resource needs a release or destruction rule before Python can safely own it.

API Projection

Native APIs and good Python APIs often have different shapes. Semantic API projection records how the public Python surface reduces to the exact native call.

Projection can express transformations such as:

  • output parameters becoming return values;
  • status values becoming Python exceptions;
  • native procedures becoming methods;
  • allocation and release families becoming owned classes;
  • pointer-and-length pairs becoming array arguments; and
  • getter/setter pairs becoming properties.

For example, a native routine can take an address and an output status while the Python API accepts one value and returns the status:

from prik.contracts import Addr, Arg, Int32, Return, Returns, native_call

@native_call([Addr(Arg(0)), Return("status", 0)])
def scalar_status(base: Int32) -> Returns["status", Int32]: ...

@native_call(...) preserves the exact native argument order. Arg(0) maps the Python argument into the call, Return("status", 0) supplies hidden native output storage, and Returns[...] exposes that output as the Python result.

Projection must preserve the exact native operation. It can rename, group, hide, reorder, or expose values only when the resulting contract still records how to make the real call.

Exact contracts and style proposals

There is no single universally Pythonic API. A numerical kernel, a stateful native handle, and a device runtime need different public surfaces.

The exact semantic contract should therefore remain the auditable baseline. An optional style proposal can be derived from it:

Exact semantic contract
        |
        v
Deterministic style rules or an assisted proposal
        |
        v
Reviewable proposed contract
        |
        v
User review and edits
        |
        v
Deterministic validation against the exact native call

Mechanical patterns can use deterministic rules. Naming, grouping, docstrings, and API organization may benefit from an assisted proposal. An AI-generated proposal must remain an authoring artifact: it is reviewed and checked in, and the wrapper build never calls a model.

No proposal reaches code generation unless a deterministic checker can reduce it to the exact contract without inventing symbols, types, ownership, or native operations.

Source-First and Contract-First Wrapping

Source frontends are valuable because they recover declarations, locations, dependencies, and target-specific facts. They can generate a starter semantic contract and keep routine native declarations from being rewritten by hand.

They are not the only possible input. When source is unavailable, a user can provide an authoritative semantic .pyi contract together with the required native objects or libraries and accurate build/link information.

Source-free wrapping does not remove the ABI contract. It makes the user responsible for stating it accurately. PRIK can validate the internal consistency of the contract, but it cannot prove that an arbitrary binary has the interface the user claimed. A false symbol, calling convention, datatype, or storage declaration can fail during linking, import, or execution.

The goal is therefore not to eliminate compiler and target dependence. It is to make that dependence explicit, probe relevant target facts, and avoid depending on compiler-internal APIs when stable source and ABI facts suffice.

Mixed-Language Libraries

A Python package may combine native components implemented by different toolchains. PRIK's language-neutral layer should allow their public behavior to be described together without pretending that their ABIs are identical.

For example, one package could expose a numerical kernel written in Fortran, a C runtime layer, a C++ object model, a Rust component, and CUDA device kernels through one coherent Python API. Each component still uses its own explicit native ABI route; the shared semantic layer connects their public meaning.

Each native callable keeps:

  • an explicit symbol and ABI route;
  • target-specific type and storage facts;
  • explicit native build artifacts; and
  • a backend capable of implementing its completed plan.

The semantic layer unifies their Python-facing meaning, coercion rules, constraints, ownership, and API projection. It does not erase the native boundaries underneath them.

Additional frontends and backends should be added only when they can preserve these invariants and demonstrate end-to-end evidence. A language name in the vision is a possible direction, not a support promise or implementation order.

Zero-Copy Interoperability

Avoiding a copy can be crucial for large arrays, device buffers, messages, mapped files, and shared native state. Zero-copy is therefore an important outcome, but never an assumption.

A zero-copy path is valid only when the contract, runtime value, and native ABI agree on:

  • dtype and element representation;
  • shape, strides, and layout;
  • alignment and device placement;
  • mutability and aliasing; and
  • ownership and lifetime.

When they do not agree, completed policy must select one of three visible outcomes: an authorized conversion or copy, a different explicitly supported path, or a clear rejection. An optimizer may prefer a zero-copy path among semantically equivalent choices, but it must not change contract behavior to obtain one.

Diagnostics and Explainability

The wrapper boundary should explain what it expected, what it observed, which policy it selected, and where failure occurred.

Useful diagnostics include:

  • the public callable and parameter;
  • the declared semantic requirement;
  • the observed Python value or native result;
  • the selected coercion and whether it copied;
  • the failed constraint or contract condition;
  • ownership and lifetime context where relevant;
  • the native route selected by planning; and
  • a focused corrective action.

A future coercion or validation failure should be explainable in this form:

ContractError in solve(A, b)
  parameter: A
  expected: a finite, writable matrix with the declared dtype and layout
  observed: shape=(10, 10), dtype=float64, layout=C
  coercion: layout conversion was not authorized by this contract
  action: pass a compatible value or explicitly permit the conversion

Diagnostics should expose completed decisions, not reconstruct a speculative explanation after backend failure.

Extensibility

External ecosystems may eventually contribute:

  • semantic type vocabulary;
  • coercion implementations;
  • built-in constraints and validators;
  • frontend capabilities;
  • backend adapters; and
  • integrations for arrays, sparse matrices, device buffers, or domain objects.

Extension points must be versioned, declarative where possible, and validated before they participate in planning. A plugin must not bypass the same policy completion, ABI identity, ownership, lifetime, or evidence requirements that the built-in paths follow.

Extensions may add mechanisms. They must not silently override the meaning of an existing contract.

Design Directions

The following are directions, not ordered roadmap phases:

  • Extend contract-authorized coercion beyond the currently planned conversion mechanisms, with explicit safety, copy, cost, ownership, and diagnostic metadata.
  • Enforce general built-in value constraints and add reusable custom preconditions, postconditions, and invariants.
  • Expand contract-first builds until supported source-first features can be represented and rebuilt without hidden source recovery.
  • Provide deterministic, reviewable API-style proposals without placing AI or heuristics in the build path.
  • Add source frontends and native backends only through the shared semantic, policy, planning, and evidence boundaries.
  • Define versioned extension protocols for external array, device, sparse, and domain-specific ecosystems.
  • Improve diagnostics until users can see which conversion, validation, ownership, and backend decisions produced an outcome.

Their order should follow demonstrated user value and the maturity of the shared contract—not a fixed language or backend sequence.

Trust Boundaries

The vision depends on a small set of durable rules:

  • The semantic contract is explicit and reviewable.
  • Semantic IR is the only input to policy completion.
  • Planning projects completed decisions instead of inventing new ones.
  • Backend lowering implements the plan instead of inferring policy.
  • Coercions never become implicit permission for arbitrary conversion.
  • Copies, precision changes, ownership transfers, and destruction are visible.
  • Target-specific ABI identities are preserved rather than guessed from width.
  • Unsupported behavior fails before compilation whenever sufficient facts are available.
  • AI may propose an API, but deterministic validation remains authoritative.
  • Current support claims require compiled evidence and belong in maintained documentation, not in aspirational examples.

What Success Looks Like

PRIK succeeds when a user can inspect one semantic contract and answer:

  • What Python API will I receive?
  • Which Python values will it accept?
  • Which conversions may happen, and will they copy?
  • Which conditions are checked before and after the native call?
  • Who owns every native resource, and when is it destroyed?
  • Which exact native symbol and ABI will be invoked?
  • Why was a value accepted, converted, or rejected?

It also succeeds when a maintainer can add a new frontend or backend without creating a second ownership model, a second validation policy, or a second interpretation of the same semantic contract.

That is the central idea:

Native interoperability should be driven by explicit semantic contracts and completed policy—not by backend guesses hidden inside generated code.

For implemented behavior, continue with the feature matrix. For the current stage ownership and pipeline, see the developer architecture. For the supported editable contract workflow, see Editing .pyi Contracts.

Clone this wiki locally