Skip to content

Native data

This example shows which Python values cross the boundary as machine values, and which stay boxed. ppy explain says which is which, and why:

  • Scalars, fixed-size tuples, and all-scalar classes are handed over flat.
  • A list[float] is copied into a buffer on the way in.
  • Everything else stays on the Python side.

Run it

python  native_data.ppy
ppy     native_data.ppy
ppy run native_data.ppy

Tuples

@ppy.pure
def norm2(point: tuple[f64, f64, f64]) -> f64:
    return point[0] * point[0] + point[1] * point[1] + point[2] * point[2]


@ppy.pure
def centroid(a: tuple[float, float], b: tuple[float, float]) -> tuple[float, float]:
    return ((a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5)

A tuple[f64, f64, f64] is three doubles in the ABI. Returning a tuple is two result slots, not a heap object, so centroid allocates nothing on the native path. Array[int, 3] is the same idea for a small fixed container.

Lists

dot takes two list[float] and total a list[i64]. Native code cannot walk a Python list, so a homogeneous list is copied into a contiguous buffer at the call. That is why a borrowed Buffer[T] is the faster spelling.

total([10**30, 1]) shows the guard on the element. The first value does not fit an i64, the boundary refuses, and the Python body prints the exact sum.

To see the representation chosen for every parameter:

ppy explain native_data.ppy:dot     # the representation chosen for every parameter

What it prints

python native_data.ppy, ppy native_data.ppy, ppy run native_data.ppy

32.0
15
1000000000000000000000000000001
14.0 (2.0, 3.0)
383

Read on: Tuples · Value classes · Native lowering

native_data.ppy is hand-written; there is no .py source and no conversion step.

08_native_data/native_data.ppy

import ppy
from ppy import Array, f64, i64


@ppy.pure
@ppy.opt(3)
def dot(a: list[float], b: list[float]) -> float:
    product: float = 0.0
    for i in range(len(a)):
        product += a[i] * b[i]
    return product


@ppy.pure
def total(values: list[i64]) -> i64:
    result: i64 = 0
    for value in values:
        result += value
    return result


@ppy.pure
def norm2(point: tuple[f64, f64, f64]) -> f64:
    return point[0] * point[0] + point[1] * point[1] + point[2] * point[2]


@ppy.pure
def centroid(a: tuple[float, float], b: tuple[float, float]) -> tuple[float, float]:
    return ((a[0] + b[0]) * 0.5, (a[1] + b[1]) * 0.5)


@ppy.pure
def brightness(colour: Array[int, 3]) -> int:
    return colour[0] + colour[1] + colour[2]


def main() -> None:
    print(dot([1.0, 2.0, 3.0], [4.0, 5.0, 6.0]))
    print(total([1, 2, 3, 4, 5]))
    print(total([10**30, 1]))
    print(norm2((1.0, 2.0, 3.0)), centroid((0.0, 0.0), (4.0, 6.0)))
    print(brightness((255, 128, 0)))


if __name__ == "__main__":
    main()

Source: examples/08_native_data.