The gist of it is I wanted C+- but with Python syntax. So I made that. It is nice.

I also want native performance, predictable memory use, and access to C and C++ libraries.

Dudu is a statically typed language with Python-shaped syntax that compiles to C++.

What I wanted from existing languages.

Python

I like Python's basic syntax. CPython is too slow and uses too much memory for many programs I want to write. Python annotations aren't checked at runtime, and the language has accumulated more syntax over time.

C

I like C's direct memory model, portability, and lack of forms that bait programmers into trying to be clever. It doesn't have operator overloading, which sucks for graphics and game development. Header files kind of suck, and I'd like a better match statement.

Rust

I like Rust's enums, matching, results, tooling, and compile-time checks. I find Rust's ownership semantics more annoying than helpful. I think traits and dyn are great in narrow cases, but unnecessarily clever and footgunish in many others. C and C++ libraries are accessed through FFI bindings. That's a foreign-language boundary, often with conversion and copying, not direct use of the native library. Rust libraries generally need Rust-facing bindings, so the ecosystem has a bootstrapping problem and feels like a walled garden.

C++

C++ has a great ecosystem and libraries, mature tools, operator overloading, and nearly every feature you could want. It can be hard to read sometimes, and I don't have the muscle memory to write it quickly. C++ code is also full of boilerplate noise that distracts from the actual logic. The same logic expressed in Python is often easier to hold in your head.

Dudu is C+- in the shape of Python.

Dudu looks like Python.

Dudu uses Python-style indentation, functions, calls, classes, loops, named arguments, imports, and indexing.

Python
def clamp(value: int, low: int, high: int) -> int:
    if value < low:
        return low
    if value > high:
        return high
    return value
Dudu
def clamp(value: i32, low: i32, high: i32) -> i32:
    if value < low:
        return low
    if value > high:
        return high
    return value
Python
class Player:
    def __init__(self, name: str, hp: int):
        self.name = name
        self.hp = hp

    def damage(self, amount: int):
        self.hp -= amount
Dudu
class Player:
    name: str
    hp: i32

    def damage(self, amount: i32):
        self.hp -= amount

player = Player(name="Ada", hp=100)
Python module
from renderer.camera import Camera
camera = Camera()
Dudu module
from renderer.camera import Camera
camera = Camera()

This is source familiarity, not Python compatibility. Dudu is statically typed and has native value, reference, pointer, and lifetime behavior.

Type annotations are optional when the type is clear.

Dudu is statically typed, but that doesn't mean every binding needs an annotation. The compiler follows types through literals, expressions, function calls, constructors, operators, indexing, and generic substitutions. The editor can display those inferred types as inlay hints without putting them in the source.

Explicit but repetitive
direction: Vec3 = normalize(player.aim)
velocity: Vec3 = direction * launch_speed
position: Vec3 = player.position + muzzle_offset
projectile: Projectile = Projectile(
    position=position,
    velocity=velocity,
    owner=player.id,
)
active: list[Projectile] = projectiles_for(scene)
active.append(projectile)
Normal Dudu
direction = normalize(player.aim)
velocity = direction * launch_speed
position = player.position + muzzle_offset
projectile = Projectile(
    position=position,
    velocity=velocity,
    owner=player.id,
)
active = projectiles_for(scene)
active.append(projectile)

These compile to the same native types and operations. An annotation doesn't make an inferred binding safer after the compiler already knows its exact type.

Write the type when it supplies information.

Function contract
def load_mesh(
    path: str,
    flags: LoadFlags,
) -> Result[Mesh, LoadError]:
    ...
No initializer to inspect
vertices: list[Vertex] = []
lookup: dict[str, Entity] = {}
scratch: array[u8][4096]

# The element and storage types
# cannot come from empty values.
Native layout or intent
pixels: *u8 = map_framebuffer()
timeout_ms: u32 = 250
weights: list[f32] = []

# Width, pointer behavior, and ABI
# are part of the requested type.

Usually inferred

Local call results, constructors, arithmetic, loop variables, indexed values, views, generic results, and non-empty container literals.

Usually written

Function parameters and value returns, public fields, empty containers, uninitialized storage, raw pointers, fixed-width ABI values, and compile-time shape contracts.

Differences from Python.

Some differences are required for static compilation. Others remove syntax that I don't want in Dudu.

Class fields provide aggregate construction.

Python
from dataclasses import dataclass

@dataclass
class Point:
    x: float
    y: float
Dudu
class Point:
    x: f32
    y: f32

point = Point(x=2.0, y=4.0)

No dunder protocol.

Python routes a large part of the language through reserved method names. Dudu uses ordinary methods and explicit operator declarations.

Python
class Buffer:
    def __init__(self, values: list[int]):
        self.values = values

    def __len__(self):
        return len(self.values)

    def __getitem__(self, index):
        return self.values[index]

    def __setitem__(self, index, value):
        self.values[index] = value

    def __add__(self, other):
        return Buffer(self.values + other.values)
Dudu
class Buffer:
    values: list[i32]

    def size(self) -> usize:
        return len(self.values)

    @operator("[]")
    def get(self, index: usize) -> i32:
        return self.values[index]

    @operator("[]=")
    def set(self, index: usize, value: i32):
        self.values[index] = value

    @operator("+")
    def add(self, other: Buffer) -> Buffer:
        combined: list[i32] = []
        for value in self.values:
            combined.append(value)
        for value in other.values:
            combined.append(value)
        return Buffer(values=combined)

Type annotations are checked.

Valid Python at runtime
health: int = "probably fine"
health += 1  # fails only when executed
Dudu compile error
health: i32 = "probably fine"
# cannot assign str to i32

Dudu uses explicit widths such as i32, u64, f32, and f64. There is no platform-dependent native int hiding in Dudu-owned APIs.

Variables keep one type.

Valid Python
value = 10
value = "ten"
value = Player()
Dudu compile error
value = 10
value = "ten"
# cannot assign str to i32

Lists have one element type.

Most Python lists already contain one kind of value. The Dudu form makes that element type part of the list type.

Python homogeneous list
players: list[Player] = [
    Player("Ada", 100),
    Player("Lin", 80),
]
players.append(Player("Grace", 90))
Dudu homogeneous list
players: list[Player] = [
    Player(name="Ada", hp=100),
    Player(name="Lin", hp=80),
]
players.append(Player(name="Grace", hp=90))

Heterogeneous lists

Python can put unrelated values in the same list. Dudu doesn't do that implicitly, and most lists shouldn't be heterogeneous anyway. When several forms belong together, use a payload enum so every case is explicit and checked.

Python heterogeneous list
items = [10, "ten", Player("Ada", 100)]
items.append(4.5)
Dudu variant list
enum Item:
    Integer:
        value: i32
    Text:
        value: str
    PlayerValue:
        value: Player

items: list[Item] = [
    Item.Integer(value=10),
    Item.Text(value="ten"),
    Item.PlayerValue(value=Player(name="Ada", hp=100)),
]

Types don't change at runtime.

Valid Python
class Player:
    name: str
    extra_state: dict[str, bool]

    def __init__(self, name: str):
        self.name = name
        self.extra_state = {}

    def damage(self, amount: int):
        self.extra_state["damage"] = True

player = Player("Ada")
Player.debug_name = lambda self: self.name.upper()
player.noclip = True
Player.damage = patched_damage
Dudu compile errors
class Player:
    name: str
    extra_state: dict[str, bool]

    def damage(self, amount: i32):
        self.extra_state["damage"] = True

player = Player(name="Ada", extra_state={})

Player.debug_name = debug_name
# error: Player has no class member debug_name

player.noclip = True
# error: Player has no field noclip

Player.damage = patched_damage
# error: methods can't be replaced at runtime

Fields, methods, and layouts are fixed when the program is compiled. Dudu doesn't support ordinary runtime monkey-patching.

Function types use the same function syntax.

Function values use fn(...), the same argument and return-type shape used by function declarations.

Python typing
from collections.abc import Callable, Mapping, Sequence

Parser = Callable[
    [Mapping[
        str,
        Sequence[Callable[[bytes], tuple[int, str | None]]],
    ]],
    list[tuple[str, int]],
]
Dudu
type Parser = fn(
    dict[
        str,
        list[fn(array[u8]) -> tuple[i32, Option[str]]],
    ]
) -> list[tuple[str, i32]]

Type-heavy code and forward references

Python often uses quoted forward references when annotations mention types that aren't available yet. Dudu resolves the imported and declared types during compilation.

Python string annotations
from collections.abc import Callable
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from engine.assets import AssetStore
    from engine.events import Event
    from engine.scene import Node, Scene

class Editor:
    scene: "Scene"
    selected: "Node | None"
    assets: "AssetStore"
    listeners: "dict[str, list[Callable[[Event], None]]]"

    def open_scene(
        self,
        scene: "Scene",
        selected: "Node | None",
    ) -> "dict[str, Node]":
        self.scene = scene
        self.selected = selected
        return scene.nodes
Dudu checked types
from engine.assets import AssetStore
from engine.events import Event
from engine.scene import Node, Scene

class Editor:
    scene: Scene
    selected: Option[Node]
    assets: AssetStore
    listeners: dict[str, list[fn(Event)]]

    def open_scene(
        self,
        scene: Scene,
        selected: Option[Node],
    ) -> dict[str, Node]:
        self.scene = scene
        self.selected = selected
        return scene.nodes

No async/await syntax.

Use threads, atomics, event loops, callbacks, nonblocking native APIs, or an imported C++ task library. Dudu doesn't define its own async runtime.

from cpp import thread

workers: list[std.thread] = []
for worker_id in range(worker_count):
    workers.append(std.thread(run_worker, worker_id))

Performance and memory use.

Dudu emits C++20 and uses the native optimizer. The generated program doesn't require a Python interpreter or Python objects. It can use normal inlining, autovectorization, SIMD intrinsics, and native CPU threads. Each case below shows the measured Python, Dudu, and handwritten C++ implementation.

Scalar accumulation

Ten million iterations, five calls in one process.

Python
def scalar_sum(n: int) -> int:
    total = 0
    for i in range(n):
        total += i * 3
    return total
Dudu
def scalar_sum(n: i32) -> i64:
    total: i64 = 0
    for i in range(n):
        total += i64(i) * 3
    return total
C++
int64_t scalar_sum(int32_t n) {
    int64_t total = 0;
    for (int32_t i = 0; i < n; ++i) {
        total += int64_t(i) * 3;
    }
    return total;
}
LanguageRuntimePeak RSSvs CPython
CPython 3.121.094 s12.0 MB1x
Dudu9.22 ms3.3 MB119x
C++9.18 ms3.6 MB119x

Build and sum an integer list

Ten million integers, five list constructions and sums in one process.

Python
def list_accum(n: int) -> int:
    values: list[int] = []
    for i in range(n):
        values.append(i & 1023)

    total = 0
    for value in values:
        total += value
    return total
Dudu
def list_accum(n: i32) -> i64:
    values: list[i32] = []
    for i in range(n):
        values.append(i & 1023)

    total: i64 = 0
    for value in values:
        total += i64(value)
    return total
C++
int64_t list_accum(int32_t n) {
    std::vector<int32_t> values{};
    for (int32_t i = 0; i < n; ++i) {
        values.push_back(i & 1023);
    }
    int64_t total = 0;
    for (int32_t value : values) {
        total += int64_t(value);
    }
    return total;
}
LanguageRuntimePeak RSSvs CPython
CPython 3.121.732 s325 MB1x
Dudu104 ms68.9 MB16.6x
C++106 ms68.7 MB16.4x

Construct and update a particle array

500,000 particles updated for 20 steps. The Python class uses __slots__ to avoid per-instance dictionaries.

Python
class Particle:
    __slots__ = ("x", "y", "vx", "vy")

    def __init__(self, x, y, vx, vy):
        self.x = x
        self.y = y
        self.vx = vx
        self.vy = vy

particles: list[Particle] = []
for i in range(count):
    particles.append(Particle(
        float(i & 1023),
        float((i * 3) & 1023),
        0.25, -0.125,
    ))

for _ in range(steps):
    for particle in particles:
        particle.x += particle.vx
        particle.y += particle.vy
Dudu
class Particle:
    x: f32
    y: f32
    vx: f32
    vy: f32

particles: list[Particle] = []
for i in range(count):
    particles.append(Particle(
        x=f32(i & 1023),
        y=f32((i * 3) & 1023),
        vx=0.25, vy=-0.125,
    ))

for _ in range(steps):
    for particle: &Particle in particles:
        particle.x += particle.vx
        particle.y += particle.vy
C++
struct Particle {
    float x, y, vx, vy;
};

std::vector<Particle> particles;
for (int32_t i = 0; i < count; ++i) {
    particles.push_back(Particle{
        float(i & 1023),
        float((i * 3) & 1023),
        0.25F, -0.125F,
    });
}

for (int32_t step = 0; step < steps; ++step) {
    for (Particle& particle : particles) {
        particle.x += particle.vx;
        particle.y += particle.vy;
    }
}
LanguageRuntimePeak RSSvs CPython
CPython 3.12407 ms78.2 MB1x
Dudu4.57 ms11.6 MB89x
C++4.68 ms11.6 MB87x

CPU-bound threads

Sixteen workers, ten million scalar iterations per worker. CPython threads remain serialized by the GIL for this workload. Dudu and C++ use std::thread.

Python
def threaded_accum(workers: int, n: int):
    output = [0] * workers

    def worker(index: int):
        total = 0
        for i in range(n):
            total += i * 3
        output[index] = total

    threads = []
    for index in range(workers):
        thread = threading.Thread(
            target=worker, args=(index,)
        )
        threads.append(thread)
        thread.start()
    for thread in threads:
        thread.join()
    return sum(output)
Dudu
from cpp import functional
from cpp import thread

def worker(index: i32, n: i32, out: &list[i64]):
    total: i64 = 0
    for i in range(n):
        total += i64(i) * 3
    out[index] = total

out: list[i64] = []
out.resize(worker_count)
threads: list[std.thread] = []
for index in range(worker_count):
    threads.append(std.thread(
        worker, index, n, std.ref(out)
    ))
for thread: &std.thread in threads:
    thread.join()
C++
void worker(int32_t index, int32_t n,
            std::vector<int64_t>& out) {
    int64_t total = 0;
    for (int32_t i = 0; i < n; ++i) {
        total += int64_t(i) * 3;
    }
    out[index] = total;
}

std::vector<int64_t> out(worker_count);
std::vector<std::thread> threads;
for (int32_t index = 0;
     index < worker_count; ++index) {
    threads.emplace_back(
        worker, index, n, std::ref(out));
}
for (std::thread& thread : threads) {
    thread.join();
}
LanguageRuntimePeak RSSvs CPython
CPython 3.123.698 s11.8 MB1x
Dudu2.70 ms4.1 MB1,370x
C++2.35 ms4.1 MB1,574x

Benchmark details

Local microbenchmarks, July 2026: Ryzen 9 9950X, CPython 3.12.3, Dudu 0.1.0-alpha.13, C++20 with -O3 -DNDEBUG. Peak RSS is from /usr/bin/time. Results vary by machine and workload.

Across scalar, pointer, field, fixed-array, vector, tuple, and callback cases, median Dudu-to-handwritten-C++ ratios ranged from 0.97x to 1.05x in the same run.

Read and rerun the Python comparison

Other compilation targets.

Dudu emits ordinary C++. A target still needs a working C++ toolchain and appropriate libraries. The following targets are possible through existing C++ toolchains, but are not all validated release targets yet.

wasm32

WebAssembly

Generate C++ and use an Emscripten-style toolchain for browser or standalone WASM targets.

Toolchain path; distribution validation pending
mcu

Embedded C++

Use freestanding or embedded mode with fixed arrays, raw pointers, memory-mapped registers, and a board toolchain.

Embedded fixtures exist; board matrix pending
rv64

RISC-V

Point generated C++ at a RISC-V cross compiler and keep target-specific APIs in normal native headers.

Toolchain path; hardware validation pending

Less code golf.

I used to like these forms. I have worked on code where tools converted loops into comprehensions, then I converted them back to edit them, then converted them into comprehensions again before committing. Dudu uses ordinary declarations, functions, loops, and scopes instead.

List comprehension

Python
names = [item.name for item in items if item.enabled]
Dudu
names: list[str] = []
for item in items:
    if item.enabled:
        names.append(item.name)

Dict comprehension

Python
scores = {player.id: player.score
          for player in players
          if player.connected}
Dudu
scores: dict[PlayerId, i32] = {}
for player in players:
    if player.connected:
        scores[player.id] = player.score

Lambda

Lambda is anonymous-function syntax inherited from Lisp and jury-rigged into modern languages. Functions are first-class values in Dudu, so named function declarations cover callback use cases without adding a second function syntax.

Python
button.on_click(
    lambda event: save_document(event.document)
)
Dudu
def save_clicked(event: ClickEvent):
    save_document(event.document)

button.on_click(save_clicked)

Generator

Python
def enabled_names(items: list[Item]):
    for item in items:
        if item.enabled:
            yield item.name

for name in enabled_names(items):
    print(name)
Dudu callback
def visit_enabled_names(
    items: &const[list[Item]],
    visit: fn(&const[str]),
):
    for item: &const[Item] in items:
        if item.enabled:
            visit(item.name)

visit_enabled_names(items, print_name)

Ternary expression

Python
label = "ready" if connected else "offline"
Dudu
label: str
if connected:
    label = "ready"
else:
    label = "offline"

with

Python context manager
with lock:
    update_shared_state()

with open("state.bin", "wb") as output:
    output.write(data)
Dudu scope and C++ RAII
from cpp import mutex

def update_locked(lock: &std.mutex):
    guard = std.lock_guard[std.mutex](lock)
    update_shared_state()
    # guard unlocks when this scope ends

output = File("state.bin", "wb")
output.write(data)
# output closes when its scope ends

Some comparisons between C++ and Dudu.

C++
struct Particle {
    Vec2 position;
    Vec2 velocity;
    float lifetime;
};

auto particle = Particle{
    .position = spawn_position,
    .velocity = random_velocity(),
    .lifetime = 2.0F,
};
Dudu
class Particle:
    position: Vec2
    velocity: Vec2
    lifetime: f32

particle = Particle(
    position=spawn_position,
    velocity=random_velocity(),
    lifetime=2.0,
)
C++ inferred bindings
auto monitor = setup_monitor_rect();
auto window_size = make_16_9_half(monitor);
auto window = SDL_CreateWindow(
    "renderer", window_size.width, window_size.height, flags);
auto renderer = SDL_CreateRenderer(window, nullptr);
auto texture = SDL_CreateTexture(
    renderer, SDL_PIXELFORMAT_RGBA8888,
    SDL_TEXTUREACCESS_STREAMING, render_w, render_h);
auto format = SDL_GetPixelFormatDetails(
    SDL_PIXELFORMAT_RGBA8888);
Dudu inferred bindings
monitor = setup_monitor_rect()
window_size = make_16_9_half(monitor)
window = SDL_CreateWindow(
    "renderer", window_size.width, window_size.height, flags,
)
renderer = SDL_CreateRenderer(window, None)
texture = SDL_CreateTexture(
    renderer, SDL_PIXELFORMAT_RGBA8888,
    SDL_TEXTUREACCESS_STREAMING, render_w, render_h,
)
format = SDL_GetPixelFormatDetails(
    SDL_PIXELFORMAT_RGBA8888
)
C++ operator
Vec2 operator+(const Vec2& other) const {
    return {x + other.x, y + other.y};
}
Dudu operator
@operator("+")
def add(self, other: Vec2) -> Vec2:
    return Vec2(self.x + other.x, self.y + other.y)

self is an inferred reference to the current object. Larger inputs can spell &const[Vec2] explicitly to avoid a copy.

The editor shows inferred types, native definitions, documentation, references, and layout information. The compiler can emit the generated C++ for inspection.

Import C and C++ libraries directly.

Dudu's native imports expose declarations scanned from the real header. Types, overloads, templates, macros, namespaces, source locations, and documentation remain available to the compiler and editor. This isn't a separate wrapper language or a second package ecosystem.

The examples below are representative of the native compatibility suite. The corresponding SDK or development package still has to be installed and linked through the project's normal CMake configuration.

C++ standard library
from cpp import algorithm
from cpp import vector

values: std.vector[i32]
values.push_back(4)
values.push_back(1)
std.sort(values.begin(), values.end())
SDL3
from c import SDL3/SDL.h

window = SDL_CreateWindow(
    "dudu", 1280, 720,
    SDL_WINDOW_RESIZABLE,
)
renderer = SDL_CreateRenderer(window, None)
raylib
from c import raylib.h

InitWindow(1280, 720, "dudu")
while not WindowShouldClose():
    BeginDrawing()
    DrawCircleV(position, 24.0, RED)
    EndDrawing()
Vulkan
from c import vulkan/vulkan.h

app = VkApplicationInfo()
app.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO
create_info = VkInstanceCreateInfo()
create_info.pApplicationInfo = &app
instance: VkInstance = None
check_vk(vkCreateInstance(&create_info, None, &instance))
OpenCV
from cpp import opencv2/opencv.hpp as cv

image = cv.imread("frame.png", cv.IMREAD_COLOR)
edges = cv.Mat()
cv.Canny(image, edges, 100.0, 200.0)
cv.imwrite("edges.png", edges)
SQLite
from c import sqlite3.h

db: *sqlite3 = None
if sqlite3_open("app.db", &db) != SQLITE_OK:
    return Err(DbError.open)
sqlite3_exec(db, schema, None, None, None)
sqlite3_close(db)
FFmpeg
from c import libavcodec/avcodec.h

packet = av_packet_alloc()
frame = av_frame_alloc()
decode_packets(codec, packet, frame)
av_frame_free(&frame)
av_packet_free(&packet)
libcurl
from c import curl/curl.h

request = curl_easy_init()
curl_easy_setopt(request, CURLOPT_URL, url)
curl_easy_setopt(request, CURLOPT_WRITEFUNCTION, on_data)
result = curl_easy_perform(request)
curl_easy_cleanup(request)
Zstandard
from c import zstd.h

capacity = ZSTD_compressBound(len(source))
compressed = allocate_bytes(capacity)
written = ZSTD_compress(
    compressed.data(), capacity,
    source.data(), len(source), 3,
)
Dear ImGui
from cpp import imgui.h

ImGui.Begin("Profiler")
ImGui.Text("frame %.2f ms", frame_ms)
ImGui.PlotLines(
    "history", samples.data(), len(samples),
)
ImGui.End()
GLM
from cpp import glm/glm.hpp

a = glm.vec3(1.0, 2.0, 3.0)
b = glm.vec3(4.0, 5.0, 6.0)
normal = glm.normalize(glm.cross(a, b))
similarity = glm.dot(a, b)
Boost.Asio
from cpp import boost/asio.hpp as asio

context = asio.io_context()
resolver = asio.ip.tcp.resolver(context)
socket = asio.ip.tcp.socket(context)
endpoints = resolver.resolve(host, service)
asio.connect(socket, endpoints)

Array and tensor indexing.

Dudu has Python-shaped multidimensional indexing in the language. Fixed arrays use it directly. Numeric libraries can use the same syntax without the compiler knowing about NumPy, PyTorch, BLAS, or a particular GPU backend.

Fixed arrays infer their shape.

Literal and inferred shape
image: array[u8] = [
    [[255, 0, 0], [0, 255, 0]],
    [[0, 0, 255], [255, 255, 255]],
]

# inferred: array[u8][2, 2, 3]
blue = image[1, 0, 2]
# inferred: u8
Views preserve result shapes
red = image[:, :, 0]
# array_view[u8][2, 2]

row = image[1, :, :]
# array_view[u8][2, 3]

rgb = image[0, 1, 0:3]
# array_view[u8][3]

expanded = image[None, ..., 2]
# array_view[u8][1, 2, 2]

A scalar index removes an axis. A slice preserves an axis. None adds one. ... fills the remaining axes. The result is computed by one rank-independent view path.

Ranges and steps
middle = samples[10:100]
every_other = samples[::2]
thirds = samples[1::3]

patch = image[
    y0:y1:2,
    x0:x1:2,
    :,
]
Common ML indexing
train_x = x[mask, :]
correct = logits[
    arange(batch), labels,
]
tokens = embeddings[token_ids, :]
last_vocab = scores[..., -1]
Broadcast and assignment
normalized = (
    x - mean[None, :]
) * inv_std[None, :]

weights[mask, :] = 0.0
logits[rows, cols] += 1.0

Libraries define what an index means.

Simple container
class Grid:
    @operator("[]")
    def at(self, y: i32, x: i32) -> Cell:
        return self.cells[(y * self.width) + x]

    @operator("[]=")
    def set_at(self, y: i32, x: i32, value: Cell):
        self.cells[(y * self.width) + x] = value
Arbitrary-rank tensor library
class Tensor[T]:
    @operator("[]")
    def at[Idx...](
        self, *idx: Idx,
    ) -> TensorSelection[T, Idx...]:
        return select(self, idx...)

    @operator("[]=")
    def set_at[Idx...](
        self, *idx: Idx, value: T,
    ):
        scatter(self, value, idx...)

The compiler passes typed scalar, slice, ellipsis, new-axis, mask, and index-array arguments to the selected operator. The library decides whether the result is a view, copy, gather, lazy value, or device operation.

Compile-time extents are optional.

A tensor library can keep every shape at runtime, just as NumPy and PyTorch do. Adding extents to a type is optional. It is useful at model, kernel, buffer, and API boundaries where a bad pipeline connection should fail during compilation instead of during a training run.

Runtime-shaped tensor
def classify(
    images: &Tensor[f32],
    weights: &Tensor[f32],
) -> Result[Tensor[f32], ShapeError]:
    if images.shape[1] != weights.shape[0]:
        return Err(ShapeError.matmul)
    return Ok(matmul(images, weights))
Optional compile-time contract
def classify[Batch, Input, Classes](
    images: &Tensor[f32][Batch, Input],
    weights: &Tensor[f32][Input, Classes],
) -> Tensor[f32][Batch, Classes]:
    return matmul(images, weights)

The first form accepts ordinary runtime-shaped tensors. The second carries dimensions through the type system. They can coexist in the same library and program.

Matrix multiplication
def matmul[M, K, N](
    left: Tensor[f32][M, K],
    right: Tensor[f32][K, N],
) -> Tensor[f32][M, N]:
    return backend_matmul(left, right)

left: Tensor[f32][32, 784]
right: Tensor[f32][784, 10]
logits = matmul(left, right)
# inferred: Tensor[f32][32, 10]
Convolution extent arithmetic
def conv2d[H, W, K](
    image: &array[f32][H, W],
    kernel: &array[f32][K, K],
) -> array[f32][H - K + 1, W - K + 1]:
    return conv_backend[H, W, K](image, kernel)

image: array[f32][4, 4]
kernel: array[f32][3, 3]
output = conv2d(image, kernel)
# inferred: array[f32][2, 2]

The caller doesn't write matmul[32, 784, 10](...) or conv2d[4, 4, 3](...). Dudu infers extents from the arguments. dyn marks only the dimensions known at runtime. An explicit assume_shape[...] check can validate runtime metadata once and then narrow it before calling an API that requires a fixed shape.

Shape error
weights: Tensor[f32][512, 10]
logits = matmul(images, weights)

# error: argument 2 for matmul expects Tensor[f32][784, 10],
#        got Tensor[f32][512, 10]

Implementation status

AreaStatusWhat is covered
Language syntaxImplementedComma indexing, scalar items, slices, steps, ellipsis, None axes, assignment, and compound assignment.
Fixed arraysImplementedLiteral shape inference, arbitrary-rank contiguous storage, rank-independent views, result-shape inference, and shape mismatch diagnostics.
Library hooksImplementedFixed and variadic @operator("[]")/@operator("[]=") overloads with typed index packs.
Optional static extentsImplementedRuntime-shaped tensors remain valid. Symbolic dimensions, inference from calls, dyn, arithmetic in result shapes, editor hovers, and inlay result types add compile-time contracts where requested.
Reference numeric stackValidatedArbitrary-rank storage, views, masks, gathers, scatter, broadcasting, reductions, a BLAS comparison, and OpenCL add/matmul probes.
Production array/ML libraryNot bundledndad and mald are dogfooding implementations. They aren't a released NumPy/PyTorch replacement or a packaged GPU stack.

Machine learning code can stay Python-shaped.

Dudu supplies the static language, indexing syntax, generics, native interop, and optional shape contracts. A numeric library supplies tensors, devices, kernels, autograd, optimizers, serialization, and distributed execution. Those layers don't have to be compiler built-ins.

A normal model and training loop.

Two-layer network
class MLP:
    hidden: Linear
    output: Linear

    def forward(self, x: &Tensor[f32]) -> Tensor[f32]:
        x = self.hidden(x)
        x = relu(x)
        return self.output(x)

model = MLP(
    hidden=Linear(784, 512),
    output=Linear(512, 10),
).to(device)
Training
for batch in train_loader:
    optimizer.zero_grad()

    images = batch.images.to(device)
    labels = batch.labels.to(device)
    logits = model.forward(images)
    loss = cross_entropy(logits, labels)

    loss.backward()
    clip_grad_norm(model.parameters(), 1.0)
    optimizer.step()

These tensors may use runtime shapes. Fixed extents are only needed where a compile-time shape contract is useful.

Autograd can be ordinary library code.

Small scalar autograd value
class Value:
    data: f32
    grad: f32
    parents: list[*Value]
    local_grads: list[f32]

    @operator("*")
    def mul(self, other: &Value) -> Value:
        return Value(
            data=self.data * other.data,
            grad=0.0,
            parents=[&self, &other],
            local_grads=[other.data, self.data],
        )
Reverse pass
def backward(root: &Value):
    order = topological_order(root)
    root.grad = 1.0

    i = len(order) - 1
    while i >= 0:
        node = order[i]
        for edge in range(len(node.parents)):
            parent = node.parents[edge]
            parent.grad += node.grad * node.local_grads[edge]
        i -= 1

A tensor autograd library uses the same idea with tensor-valued local derivatives, saved operations, device buffers, and backend kernels.

A Gemma-shaped decoder block.

The model code doesn't contain CUDA calls. The tensor library dispatches Linear, flash_attention, normalization, and elementwise operations to its selected backend.

CUDA backend implementation
from c import cuda_runtime_api.h
from c import cublas_v2.h

class CudaBackend:
    handle: cublasHandle_t

    def matmul(self, out: &Tensor[f32], a: &Tensor[f32], b: &Tensor[f32]):
        check(cublasSgemm(
            self.handle, CUBLAS_OP_N, CUBLAS_OP_N,
            b.cols, a.rows, a.cols,
            &ONE, b.data, b.cols, a.data, a.cols,
            &ZERO, out.data, out.cols,
        ))
Backend-neutral call site
device = Device.cuda(0)
model = load_gemma("gemma-2b.weights").to(device)
tokens = tokenizer.encode(prompt).to(device)
cache = KVCache.allocate(model.config, device)

while tokens[-1] != tokenizer.eos:
    logits = model.forward(tokens[-1:, None], cache)
    next_token = sample(logits[:, -1, :], temperature=0.8)
    tokens.append(next_token)

print(tokenizer.decode(tokens))

A CUDA backend is one library choice. The same model surface can dispatch to ROCm, Metal, Vulkan compute, OpenCL, BLAS, or CPU kernels.

PPO rollout collection and minibatch gathers.

Masks, gathers, new axes, ellipsis, slices, scatter assignment, and broadcasting all use the ordinary indexing operator. The tensor library decides which operations are views and which launch gathers or kernels.

Existing ML runtimes are still available.

A new systems language normally starts without a tensor ecosystem. Dudu can consume native ML libraries through their existing C or C++ APIs. That is useful immediately, but it depends on the API the upstream project actually publishes.

LibTorch C++ API
from cpp import torch/torch.h

def train_step(
    model: &Classifier,
    optimizer: &torch.optim.Optimizer,
    images: torch.Tensor,
    labels: torch.Tensor,
) -> torch.Tensor:
    optimizer.zero_grad()
    logits = model.forward(images)
    loss = torch.nn.functional.cross_entropy(logits, labels)
    loss.backward()
    optimizer.step()
    return loss
Python: export JAX once
import jax
from jax import export
from pathlib import Path

compiled = export.export(
    jax.jit(policy_step)
)(observation_spec, weights_spec)

Path("policy.stablehlo.mlir").write_text(
    compiled.mlir_module()
)
Dudu: execute through OpenXLA
from c import xla/pjrt/c/pjrt_c_api.h

module = read_bytes("policy.stablehlo.mlir")
client = create_pjrt_client()
executable = compile_stablehlo(client, module)

inputs = upload_arguments(client, observation, weights)
outputs = execute(executable, inputs)
policy_output = download(outputs[0])

LibTorch is a native C++ frontend and can be imported directly. JAX is a Python frontend, so Dudu doesn't pretend it is a C++ header library. A native deployment can export StableHLO and execute it through a compatible OpenXLA runtime, but raw StableHLO and custom calls need version-compatible handling. See the JAX export documentation for its actual portability guarantees.

Current status: the language features used here are implemented and exercised by tensor-indexing, BLAS, OpenCL, generics, native-header, and shape-diagnostic fixtures. The full mald/ddtorch-style production framework shown by these examples is not bundled with Dudu.

Features taken from other languages.

Dudu includes payload enums, exhaustive matching, results, options, fixed-width scalar types, fixed-shape arrays, and GLSL-style swizzling.

Payload enums and exhaustive match

Rust
enum Event {
    Quit,
    KeyDown { key: i32 },
    MouseMove { x: i32, y: i32 },
}

fn score(event: Event) -> i32 {
    match event {
        Event::Quit => 0,
        Event::KeyDown { key } => key,
        Event::MouseMove { x, y } => x + y,
    }
}
Dudu
enum Event:
    Quit

    KeyDown:
        key: i32

    MouseMove:
        x: i32
        y: i32

def score(event: Event) -> i32:
    match event:
        case Event.Quit:
            return 0
        case Event.KeyDown(key):
            return key
        case Event.MouseMove(x, y):
            return x + y

The compiler checks that every variant is handled and reports unreachable cases.

Result values

Rust
fn read_port(text: &str) -> Result<u16, ParseError> {
    text.parse::<u16>()
}

match read_port(input) {
    Ok(port) => start(port),
    Err(error) => report(error),
}
Dudu
def read_port(text: &const[str]) -> Result[u16, ParseError]:
    return parse_port(text)

match read_port(input):
    case Ok(port):
        start(port)
    case Err(error):
        report(error)

Option[T] handles absence in the same style. C++ exceptions remain available for imported-library compatibility.

Fixed-shape arrays

C++
using Matrix4 = std::array<
    std::array<float, 4>,
    4
>;

Matrix4 transform{};
transform[2][3] = 1.0F;
Dudu
type Matrix4 = array[f32][4, 4]

transform: Matrix4
transform[2, 3] = 1.0

Array extents are part of the type. Numeric libraries can define Python-shaped slicing and advanced indexing through the normal indexing operators.

GLSL-style swizzling

GLSL
vec4 position = vec4(1.0, 2.0, 3.0, 4.0);
vec2 xy = position.xy;
vec2 yx = position.yx;
vec2 xx = position.xx;
position.yx = vec2(8.0, 9.0);
Dudu
position = Vec4(1.0, 2.0, 3.0, 4.0)
xy = position.xy
yx = position.yx
xx = position.xx
position.yx = Vec2(8.0, 9.0)
Define vector types
class Vec2:
    x: f32
    y: f32

class Vec4:
    x: f32
    y: f32
    z: f32
    w: f32

position = Vec4(1.0, 2.0, 3.0, 4.0)
Read and write swizzles
xy = position.xy       # Vec2(1, 2)
yx = position.yx       # Vec2(2, 1)
xx = position.xx       # Vec2(1, 1)

position.yx = Vec2(8.0, 9.0)
# position is now Vec4(9, 8, 3, 4)

position.xx = Vec2(1.0, 2.0)
# error: swizzle assignment
# cannot repeat component: xx
Color components
class Color4:
    r: f32
    g: f32
    b: f32
    a: f32

bgra = color.bgra
solid_red = color.rrrr
Texture components
class TexCoord4:
    s: f32
    t: f32
    p: f32
    q: f32

rotated = uv.tspq
corner = uv.qqqq

There is no special Vec4 compiler type and no swizzle decorator. Compatible xyzw, rgba, or stpq fields opt a Dudu or imported native vector type into the feature. Reads can repeat components. Writes can't.

Current boundaries.

  • Not Python with a faster interpreter.Dynamic Python semantics are outside the goal.
  • Not Rust without braces.Dudu doesn't impose ownership or lifetime rules.
  • Not a new native ecosystem silo.C, C++, CMake, headers, linkers, debuggers, and profilers remain part of the normal workflow.
  • Not stable yet.The current release is a pre-alpha and source compatibility can change.
  • Not every C++ feature rewritten in Python syntax.Direct interop and explicit native escape hatches remain available for unusual cases.