Skip to content

kmizu/Pytra

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2,039 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Read in Japanese

Pytra Code Alchemist

Pytra

Python (subset) is the source language for Pytra, which transpiles code into multiple target languages.

Read the Docs

C++ Rust C# JS TS Go
Java Swift Kotlin Ruby Lua PHP

Latest News

2026-02-28 | v0.4.0 Released
Version 0.4.0 was released, adding Ruby as a supported target language.

2026-02-27 | v0.3.0 Released
Version 0.3.0 reorganizes EAST (intermediate representation) into staged processing (EAST1 -> EAST2 -> EAST3) and performs a large-scale split/slimming of the C++ CodeEmitter.

Features

Pytra's features

  • Python to multi-language transpiler

    • Supports conversion to C++, C#, Rust, JavaScript, TypeScript, Go, Java, Swift, Kotlin, and Ruby.
    • Converts code to output in a form extremely close to the original source.
  • Write Python code that targets C++-level output quality

    • int defaults to 64-bit signed integer.
    • No dynamic typing.
  • Simple language model

    • Basically a subset of Python.
    • Can be developed with existing tools such as VS Code.
    • Drops multiple inheritance and keeps only single inheritance.
  • High extensibility

    • The transpiler core is implemented in Python, making extension and customization easy.
    • The transpiler's own source code can be transpiled into other languages by this transpiler, enabling self-hosting.

We also prioritize practical operational benefits.

WARNING: This project is still under active development and may be far from production-ready. Review sample code first and use at your own risk.

WARNING: Do not expect entire Python applications to be portable as-is. A realistic expectation is: if the core logic you wrote in Python transpiles well, that is a good outcome.

Runtime Performance Comparison

Execution times for sample programs written in Python and their transpiled versions (unit: seconds). In the table, Python is the original code and PyPy is for reference.

No. Workload Python PyPy C++ Rust C# JS TS Go Java Swift Kotlin Ruby
01 Mandelbrot set (PNG) 18.647 1.091 0.790 0.781 0.383 0.768 0.806 0.753 0.756 0.760 0.756 0.511
02 Simple sphere ray tracer (PNG) 6.890 0.529 0.202 0.165 0.918 0.277 0.288 0.256 0.260 0.289 0.258 6.656
03 Julia set (PNG) 22.770 1.959 0.861 0.823 1.468 1.210 1.127 1.126 1.136 1.125 1.151 2.025
04 Orbit-trap Julia set (PNG) 11.950 1.081 0.380 0.358 0.416 0.473 0.504 0.466 0.471 0.482 0.469 1.063
05 Mandelbrot zoom (GIF) 14.538 1.262 0.555 0.569 1.710 0.703 0.680 0.691 0.689 0.695 0.687 12.224
06 Julia parameter sweep (GIF) 9.627 0.507 0.546 0.407 0.329 0.626 0.619 0.622 0.621 0.624 0.629 0.607
07 Game of Life (GIF) 5.134 0.685 0.363 0.369 1.530 1.364 1.311 1.191 1.248 1.290 1.267 3.050
08 Langton's Ant (GIF) 5.220 0.767 0.452 0.483 2.213 2.031 1.997 1.912 2.011 1.886 2.019 7.360
09 Flame simulation (GIF) 10.895 1.167 0.611 0.661 6.566 2.374 2.290 2.368 2.265 2.306 2.358 19.139
10 Plasma effect (GIF) 6.194 0.876 0.684 0.554 2.646 1.444 1.886 1.397 1.414 1.444 1.319 6.640
11 Lissajous particles (GIF) 3.582 0.532 0.356 0.359 0.714 1.425 1.406 1.389 1.365 1.371 1.413 0.556
12 Sorting visualization (GIF) 3.864 0.552 0.344 0.362 0.680 1.341 1.343 1.309 1.348 1.328 1.306 0.466
13 Maze generation steps (GIF) 3.402 0.533 0.287 0.298 1.037 1.038 1.035 0.985 1.025 0.997 0.987 1.211
14 Simple ray marching (GIF) 2.670 0.300 0.160 0.159 0.606 0.489 0.573 0.490 0.513 0.503 0.492 2.002
15 Wave interference loop (GIF) 2.631 0.402 0.299 0.252 1.196 0.616 0.794 0.609 0.614 0.629 0.612 2.854
16 Chaos rotation of glass sculpture (GIF) 6.847 0.606 0.277 0.246 1.220 0.650 0.822 0.638 0.643 0.667 0.643 6.882
17 Monte Carlo Pi approximation 2.981 0.105 0.019 0.018 0.098 0.431 0.433 0.432 0.433 0.436 0.436 1.949
18 Mini-language interpreter 2.037 0.601 0.610 0.427 0.735 0.446 0.446 0.405 0.417 0.423 0.417 8.272

06_julia_parameter_sweep

Sample code: 06_julia_parameter_sweep.py
# 06: Sweep Julia-set parameters and output an animated GIF.

from __future__ import annotations

import math
from time import perf_counter

from pylib.tra.gif import save_gif


def julia_palette() -> bytes:
    # Keep index 0 black for points inside the set; use vivid gradients for the rest.
    palette = bytearray(256 * 3)
    palette[0] = 0
    palette[1] = 0
    palette[2] = 0
    for i in range(1, 256):
        t = (i - 1) / 254.0
        r = int(255.0 * (9.0 * (1.0 - t) * t * t * t))
        g = int(255.0 * (15.0 * (1.0 - t) * (1.0 - t) * t * t))
        b = int(255.0 * (8.5 * (1.0 - t) * (1.0 - t) * (1.0 - t) * t))
        palette[i * 3 + 0] = r
        palette[i * 3 + 1] = g
        palette[i * 3 + 2] = b
    return bytes(palette)


def render_frame(width: int, height: int, cr: float, ci: float, max_iter: int, phase: int) -> bytes:
    frame = bytearray(width * height)
    idx = 0
    for y in range(height):
        zy0 = -1.2 + 2.4 * (y / (height - 1))
        for x in range(width):
            zx = -1.8 + 3.6 * (x / (width - 1))
            zy = zy0
            i = 0
            while i < max_iter:
                zx2 = zx * zx
                zy2 = zy * zy
                if zx2 + zy2 > 4.0:
                    break
                zy = 2.0 * zx * zy + ci
                zx = zx2 - zy2 + cr
                i += 1
            if i >= max_iter:
                frame[idx] = 0
            else:
                # Add frame phase so color flow stays smooth across frames.
                color_index = 1 + (((i * 224) // max_iter + phase) % 255)
                frame[idx] = color_index
            idx += 1
    return bytes(frame)


def run_06_julia_parameter_sweep() -> None:
    width = 320
    height = 240
    frames_n = 72
    max_iter = 180
    out_path = "sample/out/06_julia_parameter_sweep.gif"

    start = perf_counter()
    frames: list[bytes] = []
    # Circle around a known good region using an elliptical path to avoid flat blown-out frames.
    center_cr = -0.745
    center_ci = 0.186
    radius_cr = 0.12
    radius_ci = 0.10
    # Add offsets so GitHub thumbnails do not look too dark.
    # Tuned to start from a red-leaning color region.
    start_offset = 20
    phase_offset = 180
    for i in range(frames_n):
        t = ((i + start_offset) % frames_n) / frames_n
        angle = 2.0 * math.pi * t
        cr = center_cr + radius_cr * math.cos(angle)
        ci = center_ci + radius_ci * math.sin(angle)
        phase = (phase_offset + i * 5) % 255
        frames.append(render_frame(width, height, cr, ci, max_iter, phase))

    save_gif(out_path, width, height, frames, julia_palette(), delay_cs=8, loop=0)
    elapsed = perf_counter() - start
    print("output:", out_path)
    print("frames:", frames_n)
    print("elapsed_sec:", elapsed)


if __name__ == "__main__":
    run_06_julia_parameter_sweep()
Transpiled code (C++ | Rust | C# | JavaScript | TypeScript | Go | Java | Swift | Kotlin | Ruby)

16_glass_sculpture_chaos

Sample code: 16_glass_sculpture_chaos.py
# 16: Render chaotic rotation of glass sculptures with ray tracing and output a GIF.

from __future__ import annotations

import math
from time import perf_counter

from pylib.tra.gif import save_gif


def clamp01(v: float) -> float:
    if v < 0.0:
        return 0.0
    if v > 1.0:
        return 1.0
    return v


def dot(ax: float, ay: float, az: float, bx: float, by: float, bz: float) -> float:
    return ax * bx + ay * by + az * bz


def length(x: float, y: float, z: float) -> float:
    return math.sqrt(x * x + y * y + z * z)


def normalize(x: float, y: float, z: float) -> tuple[float, float, float]:
    l = length(x, y, z)
    if l < 1e-9:
        return 0.0, 0.0, 0.0
    return x / l, y / l, z / l


def reflect(ix: float, iy: float, iz: float, nx: float, ny: float, nz: float) -> tuple[float, float, float]:
    d = dot(ix, iy, iz, nx, ny, nz) * 2.0
    return ix - d * nx, iy - d * ny, iz - d * nz


def refract(ix: float, iy: float, iz: float, nx: float, ny: float, nz: float, eta: float) -> tuple[float, float, float]:
    # Simple IOR-based refraction. Falls back to reflection for total internal reflection.
    cosi = -dot(ix, iy, iz, nx, ny, nz)
    sint2 = eta * eta * (1.0 - cosi * cosi)
    if sint2 > 1.0:
        return reflect(ix, iy, iz, nx, ny, nz)
    cost = math.sqrt(1.0 - sint2)
    k = eta * cosi - cost
    return eta * ix + k * nx, eta * iy + k * ny, eta * iz + k * nz


def schlick(cos_theta: float, f0: float) -> float:
    m = 1.0 - cos_theta
    return f0 + (1.0 - f0) * (m * m * m * m * m)


def sky_color(dx: float, dy: float, dz: float, tphase: float) -> tuple[float, float, float]:
    # Sky gradient plus neon banding.
    t = 0.5 * (dy + 1.0)
    r = 0.06 + 0.20 * t
    g = 0.10 + 0.25 * t
    b = 0.16 + 0.45 * t
    band = 0.5 + 0.5 * math.sin(8.0 * dx + 6.0 * dz + tphase)
    r += 0.08 * band
    g += 0.05 * band
    b += 0.12 * band
    return clamp01(r), clamp01(g), clamp01(b)


def sphere_intersect(
    ox: float,
    oy: float,
    oz: float,
    dx: float,
    dy: float,
    dz: float,
    cx: float,
    cy: float,
    cz: float,
    radius: float,
) -> float:
    lx = ox - cx
    ly = oy - cy
    lz = oz - cz
    b = lx * dx + ly * dy + lz * dz
    c = lx * lx + ly * ly + lz * lz - radius * radius
    h = b * b - c
    if h < 0.0:
        return -1.0
    s = math.sqrt(h)
    t0 = -b - s
    if t0 > 1e-4:
        return t0
    t1 = -b + s
    if t1 > 1e-4:
        return t1
    return -1.0


def palette_332() -> bytes:
    # 3-3-2 quantized palette: lightweight quantization and fast after transpilation.
    p = bytearray(256 * 3)
    for i in range(256):
        r = (i >> 5) & 7
        g = (i >> 2) & 7
        b = i & 3
        p[i * 3 + 0] = int((255 * r) / 7)
        p[i * 3 + 1] = int((255 * g) / 7)
        p[i * 3 + 2] = int((255 * b) / 3)
    return bytes(p)


def quantize_332(r: float, g: float, b: float) -> int:
    rr = int(clamp01(r) * 255.0)
    gg = int(clamp01(g) * 255.0)
    bb = int(clamp01(b) * 255.0)
    return ((rr >> 5) << 5) + ((gg >> 5) << 2) + (bb >> 6)


def render_frame(width: int, height: int, frame_id: int, frames_n: int) -> bytes:
    t = frame_id / frames_n
    tphase = 2.0 * math.pi * t

    # Camera slowly orbits the scene.
    cam_r = 3.0
    cam_x = cam_r * math.cos(tphase * 0.9)
    cam_y = 1.1 + 0.25 * math.sin(tphase * 0.6)
    cam_z = cam_r * math.sin(tphase * 0.9)
    look_x = 0.0
    look_y = 0.35
    look_z = 0.0

    fwd_x, fwd_y, fwd_z = normalize(look_x - cam_x, look_y - cam_y, look_z - cam_z)
    right_x, right_y, right_z = normalize(fwd_z, 0.0, -fwd_x)
    up_x, up_y, up_z = normalize(
        right_y * fwd_z - right_z * fwd_y,
        right_z * fwd_x - right_x * fwd_z,
        right_x * fwd_y - right_y * fwd_x,
    )

    # Moving glass sculpture (3 spheres) and an emissive light sphere.
    s0x = 0.9 * math.cos(1.3 * tphase)
    s0y = 0.15 + 0.35 * math.sin(1.7 * tphase)
    s0z = 0.9 * math.sin(1.3 * tphase)
    s1x = 1.2 * math.cos(1.3 * tphase + 2.094)
    s1y = 0.10 + 0.40 * math.sin(1.1 * tphase + 0.8)
    s1z = 1.2 * math.sin(1.3 * tphase + 2.094)
    s2x = 1.0 * math.cos(1.3 * tphase + 4.188)
    s2y = 0.20 + 0.30 * math.sin(1.5 * tphase + 1.9)
    s2z = 1.0 * math.sin(1.3 * tphase + 4.188)
    lr = 0.35
    lx = 2.4 * math.cos(tphase * 1.8)
    ly = 1.8 + 0.8 * math.sin(tphase * 1.2)
    lz = 2.4 * math.sin(tphase * 1.8)

    frame = bytearray(width * height)
    aspect = width / height
    fov = 1.25

    i = 0
    for py in range(height):
        sy = 1.0 - (2.0 * (py + 0.5) / height)
        for px in range(width):
            sx = (2.0 * (px + 0.5) / width - 1.0) * aspect
            rx = fwd_x + fov * (sx * right_x + sy * up_x)
            ry = fwd_y + fov * (sx * right_y + sy * up_y)
            rz = fwd_z + fov * (sx * right_z + sy * up_z)
            dx, dy, dz = normalize(rx, ry, rz)

            # Search nearest hit.
            best_t = 1e9
            hit_kind = 0  # 0: sky, 1: floor, 2/3/4: glass spheres
            r = 0.0
            g = 0.0
            b = 0.0

            # Floor plane y=-1.2
            if dy < -1e-6:
                tf = (-1.2 - cam_y) / dy
                if tf > 1e-4 and tf < best_t:
                    best_t = tf
                    hit_kind = 1

            t0 = sphere_intersect(cam_x, cam_y, cam_z, dx, dy, dz, s0x, s0y, s0z, 0.65)
            if t0 > 0.0 and t0 < best_t:
                best_t = t0
                hit_kind = 2
            t1 = sphere_intersect(cam_x, cam_y, cam_z, dx, dy, dz, s1x, s1y, s1z, 0.72)
            if t1 > 0.0 and t1 < best_t:
                best_t = t1
                hit_kind = 3
            t2 = sphere_intersect(cam_x, cam_y, cam_z, dx, dy, dz, s2x, s2y, s2z, 0.58)
            if t2 > 0.0 and t2 < best_t:
                best_t = t2
                hit_kind = 4

            if hit_kind == 0:
                r, g, b = sky_color(dx, dy, dz, tphase)
            elif hit_kind == 1:
                hx = cam_x + best_t * dx
                hz = cam_z + best_t * dz
                cx = int(math.floor(hx * 2.0))
                cz = int(math.floor(hz * 2.0))
                checker = 0 if (cx + cz) % 2 == 0 else 1
                base_r = 0.10 if checker == 0 else 0.04
                base_g = 0.11 if checker == 0 else 0.05
                base_b = 0.13 if checker == 0 else 0.08
                # Emissive sphere contribution
                lxv = lx - hx
                lyv = ly - (-1.2)
                lzv = lz - hz
                ldx, ldy, ldz = normalize(lxv, lyv, lzv)
                ndotl = max(ldy, 0.0)
                ldist2 = lxv * lxv + lyv * lyv + lzv * lzv
                glow = 8.0 / (1.0 + ldist2)
                r = base_r + 0.8 * glow + 0.20 * ndotl
                g = base_g + 0.5 * glow + 0.18 * ndotl
                b = base_b + 1.0 * glow + 0.24 * ndotl
            else:
                cx = 0.0
                cy = 0.0
                cz = 0.0
                rad = 1.0
                if hit_kind == 2:
                    cx = s0x
                    cy = s0y
                    cz = s0z
                    rad = 0.65
                elif hit_kind == 3:
                    cx = s1x
                    cy = s1y
                    cz = s1z
                    rad = 0.72
                else:
                    cx = s2x
                    cy = s2y
                    cz = s2z
                    rad = 0.58
                hx = cam_x + best_t * dx
                hy = cam_y + best_t * dy
                hz = cam_z + best_t * dz
                nx, ny, nz = normalize((hx - cx) / rad, (hy - cy) / rad, (hz - cz) / rad)

                # Simplified glass shading (reflection + refraction + light highlight)
                rdx, rdy, rdz = reflect(dx, dy, dz, nx, ny, nz)
                tdx, tdy, tdz = refract(dx, dy, dz, nx, ny, nz, 1.0 / 1.45)
                sr, sg, sb = sky_color(rdx, rdy, rdz, tphase)
                tr, tg, tb = sky_color(tdx, tdy, tdz, tphase + 0.8)
                cosi = max(-(dx * nx + dy * ny + dz * nz), 0.0)
                fr = schlick(cosi, 0.04)
                r = tr * (1.0 - fr) + sr * fr
                g = tg * (1.0 - fr) + sg * fr
                b = tb * (1.0 - fr) + sb * fr

                lxv = lx - hx
                lyv = ly - hy
                lzv = lz - hz
                ldx, ldy, ldz = normalize(lxv, lyv, lzv)
                ndotl = max(nx * ldx + ny * ldy + nz * ldz, 0.0)
                hvx, hvy, hvz = normalize(ldx - dx, ldy - dy, ldz - dz)
                ndoth = max(nx * hvx + ny * hvy + nz * hvz, 0.0)
                spec = ndoth * ndoth
                spec = spec * spec
                spec = spec * spec
                spec = spec * spec
                glow = 10.0 / (1.0 + lxv * lxv + lyv * lyv + lzv * lzv)
                r += 0.20 * ndotl + 0.80 * spec + 0.45 * glow
                g += 0.18 * ndotl + 0.60 * spec + 0.35 * glow
                b += 0.26 * ndotl + 1.00 * spec + 0.65 * glow

                # Slight per-sphere tint variation.
                if hit_kind == 2:
                    r *= 0.95
                    g *= 1.05
                    b *= 1.10
                elif hit_kind == 3:
                    r *= 1.08
                    g *= 0.98
                    b *= 1.04
                else:
                    r *= 1.02
                    g *= 1.10
                    b *= 0.95

            # Slightly stronger tone mapping.
            r = math.sqrt(clamp01(r))
            g = math.sqrt(clamp01(g))
            b = math.sqrt(clamp01(b))
            frame[i] = quantize_332(r, g, b)
            i += 1

    return bytes(frame)


def run_16_glass_sculpture_chaos() -> None:
    width = 320
    height = 240
    frames_n = 72
    out_path = "sample/out/16_glass_sculpture_chaos.gif"

    start = perf_counter()
    frames: list[bytes] = []
    for i in range(frames_n):
        frames.append(render_frame(width, height, i, frames_n))

    save_gif(out_path, width, height, frames, palette_332(), delay_cs=6, loop=0)
    elapsed = perf_counter() - start
    print("output:", out_path)
    print("frames:", frames_n)
    print("elapsed_sec:", elapsed)


if __name__ == "__main__":
    run_16_glass_sculpture_chaos()
Transpiled code (C++ | Rust | C# | JavaScript | TypeScript | Go | Java | Swift | Kotlin | Ruby)

License

Apache License 2.0

About

Ultimate transpiler: converts Python to C++, Rust, C#, JavaScript, TypeScript, Go, Java, Swift, Kotlin, Ruby, Lua, and PHP.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Python 80.5%
  • C++ 14.7%
  • C# 0.9%
  • Java 0.8%
  • Go 0.7%
  • TypeScript 0.6%
  • Other 1.8%