Skip to main content

libjay for Python

Independent, modern implementations of the J and APL array languages, embedded in Python. Not a framework: the relationship to your code is the one re has — a small language inside a string literal, compiled once, run many times.

import jay

jay.j("+/ 1 2 3 4")        # 10  — "+/" inserts + between the numbers
jay.j("(+/ % #) {x}", {"x": [3.0, 1.0, 4.0, 1.0, 5.0]})   # 2.8 — the mean

The mean is written as a fork: sum (+/) divided by (%) count (#). No loops, no axis keyword arguments, no intermediate allocations to name — the expression is the dataflow graph, which is what lets libjay fuse and parallelise it. jay.apl is the same entry point for APL, with its own semantics (J reduces along the leading axis, APL along the trailing one).

Install

uvx libjay -e '(+/ % #) 3 1 4 1 5'      # try the CLI with no install
uv add libjay                            # or: pip install libjay

From a checkout today (Rust toolchain required):

uv venv && uv pip install maturin
uv run maturin develop

The names follow the pillow/PIL convention: the package (and the CLI) is libjay, the import is jay — matching Rust (use jay::) and C (-ljay, jay.h). Wheels are abi3, Python 3.10+, and have no runtime dependencies.

Compile once, bind data, run

import jay

k = jay.j.compile("+/ {weights} * {data}")
k({"weights": w, "data": chunk1})
k({"weights": w, "data": chunk2})

k2 = k.bind({"weights": w})      # a new kernel; w rides along
k2({"data": chunk3})             # only the changing part at call time

jay.j(...) is the one-shot form: compile, bind and execute in one call. Kernels are immutable — bind returns a new one — and the compiled program is shared and safe to run from several threads.

Compiling the same source twice does not compile it twice: programs are memoised in the process, so the one-shot form is cheap to call in a loop. jay.clear_cache() empties the table if you ever need it emptied; nothing is written to disk.

On Python 3.14+, t-strings make the same thing typo-safe — interpolated values become both the type contract and the defaults:

k = jay.j.compile(t"+/ {weights} * {data}")
k()                              # computes on the interpolated samples
k({"data": other})               # override at call time

Braces always mean data binding, never splicing text into the program.

Errors point into your expression, in both languages:

length error: arguments do not agree: left shape 2, right shape 3
  1 2 + 1 2 3
  ^^^^^^^^^^^
note: frames first differ at axis 0: 2 vs 3

Real data, zero-copy

Polars, pandas 2, PyArrow and numpy work natively — no dependency on any of them, via the Arrow C data interface and __array_interface__. libjay is not a replacement for Polars or pandas: you stay in them for everything tabular and hand libjay the numeric block where the heavy mathematics lives.

import numpy as np, polars as pl
df = pl.DataFrame({"open": [...], "close": [...]})   # M rows × N columns
jay.j("+/ {df}", {"df": df})       # each column summed over all rows
jay.j('+/"1 {df}', {"df": df})     # each row summed

v = jay.j("2 * {x}", {"x": np.arange(10**8)})     # zero-copy in
pl.Series(v)                                      # zero-copy out

int64/float64 data (and timestamps/durations, which are physically int64) crosses the boundary without copying, and the kernel keeps the source alive. Narrower types widen with one copy. Columns with nulls, tables mixing int64 with float64, and non-contiguous numpy views are refused with an error that names the column and suggests the cast — where information is missing, libjay reports and stops rather than guessing on your behalf. The full table of what is zero-copy, copied, refused and not supported yet is in docs/coverage.md.

Boxes, and lists of strings

A Python list whose items don't share one shape — a list of strings, a ragged list of lists — becomes a boxed array on the way in, and a boxed result converts back to nested Python data on the way out:

jay.j("# &.> {names}", {"names": ["ab", "cde"]}).tolist()   # [2, 3]
jay.j("{names}", {"names": ["ab", "cde"]}).tolist()          # ['ab', 'cde']

Complex numbers

Both languages' arithmetic runs on complex values; numpy.complex128 crosses the boundary zero-copy and a scalar result is a Python complex:

jay.j("{z} * {z}", {"z": 3 + 4j})   # (-7+24j)
jay.j("%: _4")                       # 2j — square root of a negative

Big integers and exact rationals

J's exact types — x: for arbitrary-precision integers, r for exact ratios — cross as Python's int and fractions.Fraction, both ways:

jay.j("! 30x")          # 265252859812191058636308480000000, a plain int
jay.j("1r2 + 1r3")      # Fraction(5, 6)

Seeing what an expression became

A compiled expression is not the string you wrote. +/ % # is a fork; +/ w * x is one blockwise kernel with the sum folded into it. explain prints that structure, one section per sentence:

k = jay.j.compile("+/ {w} * {x}", {"w": [1.0, 2.0, 3.0]})
print(k.explain({"x": [4.0, 5.0, 6.0]}))
source:
  +/ {w} * {x}
parameters: w, x

sentence 1  |  +/ {w} * {x}
  fused kernel (1 op: *; +/ absorbed; block 8192)  → scalar float  [kernel ran]
    in 0:
      {x}  → 3 $ float
    in 1:
      {w}  → 3 $ float
    falls back to:
      monad +/
        ...

Values follow the same cascade as a call — interpolated, bound, call-time. With every parameter filled the program is run and each node is annotated with the shape and dtype it produced, and each fused node with whether its kernel ran or handed the work back to the chain, and why. With a parameter missing, the structure is printed alone. libjay --explain -e '...' is the same thing from the shell.

Device placement

Where an expression runs is separate from what it is bound to. bind gives a kernel data; deploy gives it a processor. Both return a new kernel, and neither changes the answer.

jay.devices()
# [Device(name='AMD Radeon Pro 560', backend='metal',
#         kind='discrete GPU', f64=False),
#  Device(name='Intel(R) HD Graphics 630', backend='metal',
#         kind='integrated GPU', f64=False)]

k = jay.j.compile("+/ {w} * {x}").bind({"w": w, "x": x})
g = k.deploy("gpu")
g()                                  # the same value, computed on the GPU

What reaches the GPU is the fused elementwise chains — the same blockwise kernels explain shows, generated as shader code at run time. Everything else runs on the CPU, and so does any chain the device cannot take; explain says which and why (device: gpu, device: cpu (…)). Nothing here is a separate build: the backend is in the ordinary wheel and is dormant on a machine with no adapter.

Precision is not silently traded away. libjay computes floats in f64, and most adapters have no f64 in shaders at all — Metal has none. On those an f64 chain simply stays on the CPU. Single precision is available by asking for it:

g = k.deploy("gpu", precision="f32")   # yes, I want f32

Data can stay where it is computed. upload returns a value that carries its own location, so calling a kernel repeatedly over it uploads nothing after the first time:

g = jay.j.compile("+/ {w} * {x}").deploy("gpu")
pinned = g.bind({"w": g.upload(w), "x": g.upload(x)})
pinned()                              # no upload

The one-call shortcut jay.j("...") has no device: there is nowhere in one call to say where, and uploading data for a single run rarely pays for itself.

The CLI

libjay -e '(+/ % #) 3 1 4 1 5'                   # 2.8
libjay -e "⎕←'Hello, world!'" --lang apl         # APL
libjay examples/hello.apl                        # a file; the extension
                                                 # picks the language
libjay --explain -e '+/ {w} * {x}'               # the structure, not a result

.ijs/.j are J, .apl is APL; --lang overrides. -e defaults to J.

More

MIT licensed.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

libjay-0.1.0.tar.gz (295.1 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

libjay-0.1.0-cp310-abi3-win_amd64.whl (3.8 MB view details)

Uploaded CPython 3.10+Windows x86-64

libjay-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (3.9 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ x86-64

libjay-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (3.9 MB view details)

Uploaded CPython 3.10+manylinux: glibc 2.17+ ARM64

libjay-0.1.0-cp310-abi3-macosx_11_0_arm64.whl (3.4 MB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

libjay-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl (3.6 MB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

Details for the file libjay-0.1.0.tar.gz.

File metadata

  • Download URL: libjay-0.1.0.tar.gz
  • Upload date:
  • Size: 295.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for libjay-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d6fce3fe792b1664b7f0a53b609160ad90fdd6c24925c066596777c6aa3d1656
MD5 a03ffe5b7a3f8ab7ec567f6a69de78ac
BLAKE2b-256 bb453e68542f35e6340d8f379148a18a128f76838c866c385f9b0812c72de424

See more details on using hashes here.

Provenance

The following attestation bundles were made for libjay-0.1.0.tar.gz:

Publisher: publish.yml on amyodov/libjay

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libjay-0.1.0-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: libjay-0.1.0-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 3.8 MB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for libjay-0.1.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 c786357913ca4dfc25bffbbe4fe74622b557df3200c20cad92e6c7041d355b05
MD5 4626d64205225b639428eb705b78fec5
BLAKE2b-256 59e0a442941bf593937b6c66a4e50c53f173528ef2b163ebefe86ba457cb7a54

See more details on using hashes here.

Provenance

The following attestation bundles were made for libjay-0.1.0-cp310-abi3-win_amd64.whl:

Publisher: publish.yml on amyodov/libjay

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libjay-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for libjay-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 e6ca8697d537740c5b8d03b84ebb05c9d65d2eb3af4e49c75861a189b97cff2e
MD5 02ceab2ce261bfcbf8b311906a34be1f
BLAKE2b-256 9b14a754c1c3af94289128bf3b77154e6e2143544af45cfcd82b43634fd5026f

See more details on using hashes here.

Provenance

The following attestation bundles were made for libjay-0.1.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: publish.yml on amyodov/libjay

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libjay-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for libjay-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 2ee2ab4af2e87c9bce7237bd9026cef7f9a83f2e25ca4551be0243c0b8270410
MD5 03dbd92e67cc11f51f32327f5b605a27
BLAKE2b-256 559e5a25507002f4ea8c14ae8a19e90bb8757d545023dbb63028580d9f0ccad3

See more details on using hashes here.

Provenance

The following attestation bundles were made for libjay-0.1.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: publish.yml on amyodov/libjay

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libjay-0.1.0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for libjay-0.1.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 9945859597116b9d16e759251436fa048b915a489206c4c606f33a3b62611980
MD5 99745ef735ff012509b74bf9b04aee1a
BLAKE2b-256 7329023be52053fe518b3fe4459ffdb478d2547ae7993e620175db2ec05d99ce

See more details on using hashes here.

Provenance

The following attestation bundles were made for libjay-0.1.0-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: publish.yml on amyodov/libjay

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file libjay-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for libjay-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 3ed0dabb3e7457eff449b4db8f4a4c270d3f8fbdf13dedf73529ac1fdff794cb
MD5 f7ade351617cadce30221f26923cbea2
BLAKE2b-256 1bacd13ce024439d25adb8d1349996eb3a54d37f61f1b79bf5576e2d195b1045

See more details on using hashes here.

Provenance

The following attestation bundles were made for libjay-0.1.0-cp310-abi3-macosx_10_12_x86_64.whl:

Publisher: publish.yml on amyodov/libjay

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.0

6 files

This release

0.1.0 This release

6 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page