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.
def clamp(value: int, low: int, high: int) -> int:
if value < low:
return low
if value > high:
return high
return value
def clamp(value: i32, low: i32, high: i32) -> i32:
if value < low:
return low
if value > high:
return high
return value
class Player:
def __init__(self, name: str, hp: int):
self.name = name
self.hp = hp
def damage(self, amount: int):
self.hp -= amount
class Player:
name: str
hp: i32
def damage(self, amount: i32):
self.hp -= amount
player = Player(name="Ada", hp=100)
from renderer.camera import Camera
camera = Camera()
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.
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)
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.
def load_mesh(
path: str,
flags: LoadFlags,
) -> Result[Mesh, LoadError]:
...
vertices: list[Vertex] = []
lookup: dict[str, Entity] = {}
scratch: array[u8][4096]
# The element and storage types
# cannot come from empty values.
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.
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: floatclass 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.
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)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.
health: int = "probably fine"
health += 1 # fails only when executedhealth: i32 = "probably fine"
# cannot assign str to i32Dudu 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.
value = 10
value = "ten"
value = Player()value = 10
value = "ten"
# cannot assign str to i32Lists 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.
players: list[Player] = [
Player("Ada", 100),
Player("Lin", 80),
]
players.append(Player("Grace", 90))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.
items = [10, "ten", Player("Ada", 100)]
items.append(4.5)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.
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_damageclass 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 runtimeFields, 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.
from collections.abc import Callable, Mapping, Sequence
Parser = Callable[
[Mapping[
str,
Sequence[Callable[[bytes], tuple[int, str | None]]],
]],
list[tuple[str, int]],
]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.
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.nodesfrom 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.nodesNo 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.
def scalar_sum(n: int) -> int:
total = 0
for i in range(n):
total += i * 3
return totaldef scalar_sum(n: i32) -> i64:
total: i64 = 0
for i in range(n):
total += i64(i) * 3
return totalint64_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;
}| Language | Runtime | Peak RSS | vs CPython |
|---|---|---|---|
| CPython 3.12 | 1.094 s | 12.0 MB | 1x |
| Dudu | 9.22 ms | 3.3 MB | 119x |
| C++ | 9.18 ms | 3.6 MB | 119x |
Build and sum an integer list
Ten million integers, five list constructions and sums in one process.
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 totaldef 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 totalint64_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;
}| Language | Runtime | Peak RSS | vs CPython |
|---|---|---|---|
| CPython 3.12 | 1.732 s | 325 MB | 1x |
| Dudu | 104 ms | 68.9 MB | 16.6x |
| C++ | 106 ms | 68.7 MB | 16.4x |
Construct and update a particle array
500,000 particles updated for 20 steps. The Python class uses __slots__ to avoid per-instance dictionaries.
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.vyclass 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.vystruct 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;
}
}| Language | Runtime | Peak RSS | vs CPython |
|---|---|---|---|
| CPython 3.12 | 407 ms | 78.2 MB | 1x |
| Dudu | 4.57 ms | 11.6 MB | 89x |
| C++ | 4.68 ms | 11.6 MB | 87x |
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.
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)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()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();
}| Language | Runtime | Peak RSS | vs CPython |
|---|---|---|---|
| CPython 3.12 | 3.698 s | 11.8 MB | 1x |
| Dudu | 2.70 ms | 4.1 MB | 1,370x |
| C++ | 2.35 ms | 4.1 MB | 1,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 comparisonOther 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.
WebAssembly
Generate C++ and use an Emscripten-style toolchain for browser or standalone WASM targets.
Toolchain path; distribution validation pendingEmbedded C++
Use freestanding or embedded mode with fixed arrays, raw pointers, memory-mapped registers, and a board toolchain.
Embedded fixtures exist; board matrix pendingRISC-V
Point generated C++ at a RISC-V cross compiler and keep target-specific APIs in normal native headers.
Toolchain path; hardware validation pendingLess 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
names = [item.name for item in items if item.enabled]
names: list[str] = []
for item in items:
if item.enabled:
names.append(item.name)
Dict comprehension
scores = {player.id: player.score
for player in players
if player.connected}
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.
button.on_click(
lambda event: save_document(event.document)
)
def save_clicked(event: ClickEvent):
save_document(event.document)
button.on_click(save_clicked)
Generator
def enabled_names(items: list[Item]):
for item in items:
if item.enabled:
yield item.name
for name in enabled_names(items):
print(name)
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
label = "ready" if connected else "offline"
label: str
if connected:
label = "ready"
else:
label = "offline"
with
with lock:
update_shared_state()
with open("state.bin", "wb") as output:
output.write(data)
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.
struct Particle {
Vec2 position;
Vec2 velocity;
float lifetime;
};
auto particle = Particle{
.position = spawn_position,
.velocity = random_velocity(),
.lifetime = 2.0F,
};
class Particle:
position: Vec2
velocity: Vec2
lifetime: f32
particle = Particle(
position=spawn_position,
velocity=random_velocity(),
lifetime=2.0,
)
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);
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
)
Vec2 operator+(const Vec2& other) const {
return {x + other.x, y + other.y};
}
@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.
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())from c import SDL3/SDL.h
window = SDL_CreateWindow(
"dudu", 1280, 720,
SDL_WINDOW_RESIZABLE,
)
renderer = SDL_CreateRenderer(window, None)from c import raylib.h
InitWindow(1280, 720, "dudu")
while not WindowShouldClose():
BeginDrawing()
DrawCircleV(position, 24.0, RED)
EndDrawing()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))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)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)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)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)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,
)from cpp import imgui.h
ImGui.Begin("Profiler")
ImGui.Text("frame %.2f ms", frame_ms)
ImGui.PlotLines(
"history", samples.data(), len(samples),
)
ImGui.End()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)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.
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
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.
middle = samples[10:100]
every_other = samples[::2]
thirds = samples[1::3]
patch = image[
y0:y1:2,
x0:x1:2,
:,
]
train_x = x[mask, :]
correct = logits[
arange(batch), labels,
]
tokens = embeddings[token_ids, :]
last_vocab = scores[..., -1]
normalized = (
x - mean[None, :]
) * inv_std[None, :]
weights[mask, :] = 0.0
logits[rows, cols] += 1.0
Libraries define what an index means.
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
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.
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))
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.
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]
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.
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
| Area | Status | What is covered |
|---|---|---|
| Language syntax | Implemented | Comma indexing, scalar items, slices, steps, ellipsis, None axes, assignment, and compound assignment. |
| Fixed arrays | Implemented | Literal shape inference, arbitrary-rank contiguous storage, rank-independent views, result-shape inference, and shape mismatch diagnostics. |
| Library hooks | Implemented | Fixed and variadic @operator("[]")/@operator("[]=") overloads with typed index packs. |
| Optional static extents | Implemented | Runtime-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 stack | Validated | Arbitrary-rank storage, views, masks, gathers, scatter, broadcasting, reductions, a BLAS comparison, and OpenCL add/matmul probes. |
| Production array/ML library | Not bundled | ndad 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.
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)
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.
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],
)
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.
class GemmaBlock:
norm: RMSNorm
qkv: Linear
attention_out: Linear
gate: Linear
up: Linear
down: Linear
heads: i32
head_dim: i32
def forward(
self,
x: &Tensor[f16],
cache: &KVCache,
positions: &Tensor[i32],
) -> Tensor[f16]:
residual = x
hidden = self.norm(x)
qkv = self.qkv(hidden)
q, k, v = qkv.split(3, axis=-1)
q = q.reshape(batch, tokens, self.heads, self.head_dim)
k = k.reshape(batch, tokens, self.heads, self.head_dim)
v = v.reshape(batch, tokens, self.heads, self.head_dim)
q = q.transpose(0, 2, 1, 3)
k = apply_rope(k, positions)
cache.keys[:, :, positions, :] = k
cache.values[:, :, positions, :] = v
end = positions[-1] + 1
context = flash_attention(
q,
cache.keys[:, :, :end, :],
cache.values[:, :, :end, :],
causal=True,
)
attended = residual + self.attention_out(context.flatten(2, 3))
hidden = self.norm(attended)
gated = gelu(self.gate(hidden)) * self.up(hidden)
return attended + self.down(gated)
The model code doesn't contain CUDA calls. The tensor library dispatches Linear, flash_attention, normalization, and elementwise operations to its selected backend.
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,
))
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.
class Rollout:
observations: Tensor[f32]
actions: Tensor[i64]
rewards: Tensor[f32]
dones: Tensor[bool]
values: Tensor[f32]
def collect(envs: &VectorEnv, policy: &Policy, steps: i32) -> Rollout:
rollout = Rollout.allocate(steps, envs.count, envs.observation_shape)
observation = envs.reset()
for step in range(steps):
action, value = policy.sample(observation)
next_observation, reward, done = envs.step(action)
rollout.observations[step, :, :] = observation
rollout.actions[step, :] = action
rollout.rewards[step, :] = reward
rollout.dones[step, :] = done
rollout.values[step, :] = value
observation = where(done[:, None], envs.reset_done(done), next_observation)
return rollout
def minibatch(rollout: &Rollout, ids: &Tensor[i64]) -> Batch:
flat_observations = rollout.observations.reshape(-1, rollout.observations.shape[-1])
flat_actions = rollout.actions.reshape(-1)
flat_returns = compute_returns(rollout).reshape(-1)
return Batch(
observations=flat_observations[ids, :],
actions=flat_actions[ids],
returns=flat_returns[ids],
)
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.
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
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()
)
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.
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
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,
}
}
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
fn read_port(text: &str) -> Result<u16, ParseError> {
text.parse::<u16>()
}
match read_port(input) {
Ok(port) => start(port),
Err(error) => report(error),
}
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
using Matrix4 = std::array<
std::array<float, 4>,
4
>;
Matrix4 transform{};
transform[2][3] = 1.0F;
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
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);
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)
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)
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
class Color4:
r: f32
g: f32
b: f32
a: f32
bgra = color.bgra
solid_red = color.rrrr
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.