Dudu manual
Dudu is a statically typed systems language with Python syntax, readable C++ output, and direct C and C++ interop.
Manual for 0.1.0-alpha.13. Alpha syntax and generated ABI may change.Quickstart
Install a tagged source release on Linux or macOS:
curl --proto '=https' --tlsv1.2 -sSf https://dudulang.org/install.sh | sh
dudu --versionThe installer checks native build dependencies, offers to install them through the system package manager, verifies the release checksum, and installs under ~/.local.
Create a project
dudu new hello
cd hello
dudu runThe generated entry file is ordinary Dudu:
def main() -> i32:
print("hello from dudu")
return 0Normal edit-loop commands are:
dudu fmt
dudu check
dudu run
dudu testdudu is the project driver. duc is the lower-level compiler driver, comparable to the distinction between Cargo and rustc.
Projects and workflow
A normal project has a manifest, source directory, and user-owned CMake file:
hello/
├── CMakeLists.txt
├── dudu.toml
└── src/
└── main.dddudu.toml names the Dudu entry and common native settings. CMake remains available for native details that do not belong in a language manifest.
name = "hello"
entry = "src/main.dd"
[cxx]
standard = "c++20"
[build]
dir = "build"
[include]
paths = ["include"]dudu build, dudu run, and dudu test use the CMake backend. Generated files live below the build directory and may be overwritten. User-owned CMakeLists.txt files are not rewritten after project creation.
Named targets
name = "tools"
[targets.app]
entry = "src/main.dd"
kind = "executable"
[targets.tests]
entry = "tests/main.dd"
kind = "executable"
[targets.app.pkg]
libs = ["raylib"]dudu run app
dudu build testsManifest-relative paths resolve from the directory containing dudu.toml. Explicit local header imports resolve from the source file containing the import.
Syntax basics
Dudu files use the .dd extension. Indentation defines blocks. Comments begin with #.
# Bindings are snake_case. Types are PascalCase.
MAX_PLAYERS: i32 = 64
class Player:
name: str
hp: i32
def damage(self, amount: i32):
self.hp -= amountAssignments use =. Augmented assignments include +=, -=, *=, /=, and %=. A binding has one static type for its lifetime.
| Form | Rule |
|---|---|
local_name | Variables, functions, methods, and modules use snake_case. |
TypeName | Classes, enums, variants, and type aliases use PascalCase. |
MAX_COUNT | Constants use upper snake case and cannot be rebound. |
_ | The wildcard discards a value. A named binding beginning with _ is still an ordinary binding. |
Types and inference
Dudu uses fixed-width scalar names. It does not provide ambiguous native aliases such as int, float, or double in Dudu source.
| Group | Types |
|---|---|
| Signed integers | i8, i16, i32, i64, isize |
| Unsigned integers | u8, u16, u32, u64, usize |
| Floating point | f32, f64 |
| Other scalars | bool, char, void |
| Strings | str owns text; cstr is a native C string view. |
Local inference
Initializers infer local types when the answer is unambiguous:
count = 10
scale = 2.0
name = "dudu"
player = Player("Ada", 100)Annotations remain required at public boundaries, for class fields, and where there is no initializer to inspect:
window: *SDL_Window = None
pixels: array[Color][320, 240]
def update(player: &Player, dt: f32):
speed = 240.0
player.x += speed * dtConversions
Dudu-native assignments do not silently change types. Cast with type-call syntax:
x: i32 = 10
y: i64 = i64(x)
z: f32 = f32(y)Functions
Function parameters and returned values are typed. Omit the return annotation for functions that return no value.
def clamp(value: i32, lo: i32, hi: i32) -> i32:
if value < lo:
return lo
if value > hi:
return hi
return value
def log_value(value: i32):
print(value)Calls and multiline calls follow Python syntax. Dudu uses explicit return; the last expression is not returned implicitly.
Function values
def less(a: i32, b: i32) -> bool:
return a < b
compare: fn(i32, i32) -> bool = less
ordered = compare(3, 8)Dudu does not have lambda or anonymous def expressions. Capturing callables use explicit library types such as imported std.function.
Multiple return values
def divmod_i32(a: i32, b: i32) -> tuple[i32, i32]:
return a / b, a % b
quotient, remainder = divmod_i32(10, 3)Control flow
Conditionals and loops are statement-oriented. Conditions must be bool.
if health <= 0:
state = State.Dead
elif health < 25:
state = State.Hurt
else:
state = State.Ready
for i in range(count):
total += values[i]
while queue.not_empty():
process(queue.pop())Loop variable types are inferred, or may be explicit when copy/reference behavior matters:
for player: &Player in players:
player.hp -= 1
for player: &const[Player] in players:
draw_player(player)Labeled loops
outer: for y in range(height):
for x in range(width):
if blocked(x, y):
break outer
if skip_column(x):
continue outerClasses
A Dudu class is a C++ struct-like value type by default. Fields define layout; construction follows field order or named-field syntax.
class Vec2:
x: f32
y: f32
def length_squared(self: &const[Self]) -> f32:
return self.x * self.x + self.y * self.y
def scale_in_place(self, scale: f32) -> &Self:
self.x *= scale
self.y *= scale
return self
a = Vec2(3.0, 4.0)
b = Vec2(x=8.0, y=2.0)Bare self means self: &Self. Spell self: &const[Self] for a read-only receiver. Returning Self returns a value; returning &Self returns a mutable reference; returning &const[Self] returns a read-only reference.
Static members
class Counter:
count: static[i32] = 0
MAX_COUNT: i32 = 1024
def bump() -> i32:
Counter.count += 1
return Counter.countA class function without self is static. Mutable shared fields use static[T]. Upper-case class constants are static constants.
Operators and swizzling
Dudu uses explicit decorators instead of Python dunder names for operator overloads.
class Vec2:
x: f32
y: f32
@operator("+")
def add(self: &const[Self], other: &const[Vec2]) -> Vec2:
return Vec2(self.x + other.x, self.y + other.y)
@operator("+=")
def add_assign(self, other: &const[Vec2]) -> &Self:
self.x += other.x
self.y += other.y
return selfSupported overload families include arithmetic, comparison, compound assignment, conversion to bool, and indexing through @operator("[]") and @operator("[]=").
GLSL-style swizzling
xy = position.xy
rgb = color.rgb
repeated = position.xx
position.xy = Vec2(10.0, 20.0)Read swizzles may repeat components. Assignment swizzles may not, because writing the same component twice is ambiguous.
Enums, match, and errors
Plain enums
enum Direction: u8
North = 1
South = 2
East = 3
West = 4Payload enums and match
enum Token:
Eof
Ident:
text: str
IntLit:
value: i64
Punct:
ch: u8
def score(token: Token) -> i32:
match token:
case Token.Eof:
return 0
case Token.Ident(text):
return i32(len(text))
case Token.IntLit(value) if value > 40:
return i32(value)
case Token.IntLit(value):
return i32(value) + 1
case Token.Punct(ch=value):
return i32(value)Payload declarations always use named fields. Constructors and match patterns may use those fields positionally or by name. Match checking understands payload arity, guards, wildcard reachability, and exhaustiveness.
Recursive variants
enum Expr:
Number:
value: f64
Add:
left: *Expr
right: *Expr
def evaluate(expr: Expr) -> f64:
match expr:
case Expr.Number(value):
return value
case Expr.Add(left, right):
return evaluate(*left) + evaluate(*right)Recursive variants use pointer indirection because a value cannot contain itself directly. Payload fields may otherwise contain any valid Dudu type.
Result and Option
enum ReadError:
NotFound
PermissionDenied
def read_config(path: str) -> Result[str, ReadError]:
if not fs.exists(path):
return Err(ReadError.NotFound)
return Ok(fs.read_all(path))
def choose(flag: bool) -> Option[i32]:
if flag:
return 42
return NoneUse Result and Option for normal Dudu APIs. try, except, and raise exist for C++ exception interop; Dudu does not add finally.
Containers and arrays
| Type | Meaning |
|---|---|
list[T] | Dynamic owning sequence, normally lowered through std::vector<T>. |
dict[K, V] | Dynamic key/value container. |
set[T] | Dynamic set. |
tuple[T...] | Fixed heterogeneous product value with destructuring. |
array[T][N] | Fixed contiguous array with compile-time extent. |
array[T][M, N] | Fixed contiguous multidimensional array. |
Lists
numbers = [1, 2, 3]
# inferred: list[i32]
players: list[Player] = []
players.append(Player("Ada", 100))
players.append(Player("Lin", 80))
first = players[0]
last = players.back()
for player in players:
print(player.name)Nonempty literals infer their element type. Empty collections need an explicit type unless a surrounding expected type supplies it. Conflicting element constraints are errors; use an explicit payload enum when mixed values are intentional.
Collection inference and diagnosticsDictionaries
scores = {
"ada": 99,
"lin": 80,
}
# inferred: dict[str, i32]
scores["grace"] = 100
if scores.contains("ada"):
print(scores["ada"])Sets
visited = {"start", "hall"}
# inferred: set[str]
visited.insert("vault")
if visited.contains("vault"):
print(visited.size())Tuples and destructuring
def divmod_i32(value: i32, divisor: i32) -> tuple[i32, i32]:
return value / divisor, value % divisor
quotient, remainder = divmod_i32(43, 10)Fixed arrays and inferred shapes
kernel: array[f32] = [
[1.0, 0.0, -1.0],
[1.0, 0.0, -1.0],
[1.0, 0.0, -1.0],
]
# inferred: array[f32][3, 3]
scratch: array[f32][64, 64]The initializer supplies shape when present. An uninitialized fixed array needs explicit extents.
Fixed-array and numeric-literal referenceIndexing and views
Dudu parses Python/NumPy-shaped index items without hard-coding tensor policy into the compiler. Built-in arrays and library types dispatch through the same operator surface.
pixel = image[y, x, channel]
red = image[:, :, 0]
patch = image[y0:y1:2, x0:x1:2, :]
last = scores[..., -1]
expanded = bias[None, :]
train_x = x[mask, :]
weights[mask, :] = 0.0
logits[rows, cols] += 1.0A scalar index removes an axis, a slice preserves one, None adds an axis, and ... fills remaining axes. Result shape is available to the type checker and editor when it can be proven statically; runtime-shaped libraries may retain shape in their own values.
C++ standard library containers
Dudu's built-in collection names cover common Python-shaped code. They do not restrict native code to that set. Import a C++ header and use its real container, iterator, and algorithm APIs directly.
from cpp import algorithm
from cpp import array
from cpp import deque
from cpp import span
from cpp import vector
values: std.vector[i32]
values.push_back(4)
values.push_back(1)
std.sort(values.begin(), values.end())
deck: std.deque[i32]
deck.push_front(2)
deck.push_back(40)
fixed: std.array[i32, 3] = std.array[i32, 3]({1, 2, 3})
view: std.span[i32] = std.span[i32](fixed)from cpp import map
from cpp import queue
from cpp import unordered_set
counts: std.map[i32, i32]
counts[7] = 42
seen: std.unordered_set[i32]
seen.insert(7)
jobs: std.priority_queue[i32]
jobs.push(10)
jobs.push(42)
highest = jobs.top()The same import model covers std::list, std::forward_list, std::multimap, std::multiset, container adapters, ranges, and third-party C++ container libraries. Dudu does not wrap them into a separate ecosystem.
Memory and references
Dudu exposes the native memory model. It does not impose Rust ownership or lifetime syntax.
| Form | Meaning |
|---|---|
*T | Raw pointer to T. |
&T | Mutable reference to T. |
&const[T] | Read-only reference to T. |
*const[T] | Pointer to read-only T. |
const[*T] | Const-qualified pointer binding when that distinction is required. |
Pointer versus reference
Both forms provide indirect access to another object, but they carry different native contracts:
- A reference must bind to a valid object, cannot be
None, and cannot be reseated. Use normal member and value syntax through it. - A pointer stores an address. It may be
None, may be assigned a different address, and must be dereferenced with*when accessing the pointed-to value. - Neither form owns an object by itself. A pointer returned by
new[T]has an explicit allocation contract and must eventually be passed todelete. Native handles and borrowed pointers follow the imported API's contract. - Use references for required borrowed parameters. Use pointers for nullable values, native handles, arrays, allocation, pointer arithmetic, and APIs that specifically expose pointers.
Read qualifiers from the inside out. *const[T] is a pointer to read-only T. const[*T] is a pointer binding that cannot be reseated. const[*const[T]] is a const pointer to read-only T. These map to const T*, T* const, and const T* const in generated C++.
value: i32 = 42
pointer: *i32 = &value
*pointer = 99
alias: &i32 = value
alias += 1
read_only: &const[i32] = value
maybe_value: *i32 = None
window: *SDL_Window = NoneAllocation
# Value construction and RAII are the default.
player = Player(100, "Ada")
# Construct one heap object.
heap_player: *Player = new[Player](100, "Ada")
delete heap_player
# Raw storage for native or allocator work.
bytes: *u8 = malloc[u8](1024)
free(bytes)Value containers own their values. Pointer containers do not own pointees. Custom arenas and allocators are ordinary libraries. Lexical scope and C++ destructors provide cleanup; Dudu does not add a general defer statement.
Generics and compile time
Native generics
def clamp[T](value: T, lo: T, hi: T) -> T:
if value < lo:
return lo
if value > hi:
return hi
return value
class Box[T]:
value: T
def get(self: &const[Self]) -> &const[T]:
return self.valueGeneric functions and classes lower to C++ templates. Compile-time value parameters may describe fixed extents and participate in checked constant expressions.
Generics and value-parameter referenceConstants and build values
WIDTH: i32 = 320
HEIGHT: i32 = 240
PIXELS: i32 = WIDTH * HEIGHT
@constexpr
def align_up(value: usize, align: usize) -> usize:
return (value + align - 1) & ~(align - 1)
ROW_BYTES: usize = align_up(usize(WIDTH) * 4, 64)
static_assert(PIXELS == 76800)
if build.DEBUG:
enable_validation()Build values come from dudu.toml or compiler definitions. Branches depending only on build.* values are selected at compile time.
Modules and imports
Dudu modules use Python-shaped imports:
import camera
import renderer.camera as render_camera
from vec3 import Vec3
from raytrace import shade_pixel as shadeImports are qualified by default. Selective imports bind names directly. Ambiguous bindings are compile errors rather than order-dependent shadowing.
Native header forms
# System/header-search includes: <thread>, <math.h>
from cpp import thread
from c import math.h
# Explicit source-relative quoted includes.
from cpp.path import vendor/math.hpp as vendor_math
from c.path import local/device.h as deviceThe c, cxx, cpp, and path portions are semantic import markers. Editor navigation can open the header target and resolve individual imported declarations.
C interop
Imported C declarations come from real headers scanned by Clang. Types, structs, enums, functions, constants, and callable macros retain their native identity.
from c import SDL3/SDL.h
def main() -> i32:
if not SDL_Init(SDL_INIT_VIDEO):
return 1
window = SDL_CreateWindow("Dudu", 800, 450, 0)
if window == None:
SDL_Quit()
return 2
SDL_DestroyWindow(window)
SDL_Quit()
return 0C libraries often prefix their globals, so an unaliased import is readable. Alias a header when it would collide or expose an unmanageable global namespace:
from c import sys/stat.h as stat
info: stat.stat
if stat.stat("asset.bin", &info) != 0:
return 1The compiler remembers C tag spelling internally. Normal Dudu source uses SDL_Event or stat.stat, not struct SDL_Event.
C++ interop
C++ namespaces remain their real namespaces. Importing more than one standard header merges declarations into std as C++ does.
from cpp import algorithm
from cpp import string
from cpp import thread
from cpp import vector
names: std.vector[std.string]
workers: list[std.thread] = []
std.sort(names.begin(), names.end())Header aliases are collision boundaries, not fake replacements for namespaces:
from cpp.path import vendor/math.hpp as vendor_math
point: vendor_math.vendor.Vec3
length = vendor_math.vendor.length(point)Header awareness handles overloads, constructors, methods, operators, templates, const/reference forms, macros that represent complete expressions, and source locations for editor navigation. If a native API cannot be represented directly, cpp(...) is the explicit escape hatch, not normal interop style.
Dudu called from C++
Top-level Dudu classes and functions generate ordinary C++ declarations. Use duc --emit-header for a C++ header and @extern_c plus --emit-c-header for a C ABI.
duc emit src/library.dd -o build/library.cpp
duc src/library.dd --emit-header build/library.hpp
duc src/library.dd --emit-c-header build/library.hBuild configuration
Dudu uses CMake as its native ecosystem backend. Small projects can describe common settings in dudu.toml; large projects can keep a user-owned CMake build.
name = "viewer"
entry = "src/main.dd"
[pkg]
libs = ["sdl3", "sqlite3"]
[include]
paths = ["include", "third_party/include"]
[sources]
cpp = ["src/native_backend.cpp"]
[deps]
ndad = { git = "https://github.com/dudu-language/ndad.git", tag = "v0.1.0" }
local_math = { path = "../local_math" }
[build]
DEBUG = true
RENDER_BACKEND = "vulkan"Use CMake directly for toolchain files, platform conditionals, installed native packages, generated sources, and complex target graphs. dudu cmake exposes generated integration when inspection is useful.
Git and path dependencies
Missing source dependencies are resolved before normal project commands and pinned in dudu.lock. Existing pins are not silently advanced to a newer remote revision.
dudu deps fetch
dudu buildDudu is alpha software. Commit the manifest and dependency lock metadata used by your project, and inspect generated native build files when shipping production binaries.
Tests
Tests are free functions marked with @test. They take no arguments and return no value, bool, or i32.
@test
def add_works():
assert add(20, 22) == 42
@test
def status_code() -> i32:
return add(2, 2) - 4
@test.ignore
def slow_case():
run_slow_fixture()dudu test
dudu test add
dudu test --filter add
dudu test --no-captureassert is always checked. debug_assert lowers to native C++ assertion behavior and may disappear when NDEBUG is defined.
Editor support
The Dudu VS Code extension starts dudu-lsp and supplies semantic highlighting, diagnostics, formatting, completion, signature help, hover, inlay hints, go to definition, references, rename, and native header navigation.
- Install the extension from the Visual Studio Marketplace or Open VSX.
- Open the directory containing
dudu.toml, not only an isolated source file. - Run
dudu fmtfrom a project root to format every.ddfile. - Use the extension's inlay-hint toggle to show inferred types and parameter names.
Native hover and navigation depend on Clang header scanning and the same include configuration used by the build. If an imported symbol is unresolved in the editor but builds in CMake, check that the manifest and CMake compile context expose the same include paths and definitions.
Command reference
| Command | Purpose |
|---|---|
dudu init [path] | Initialize a project in an existing or specified directory. |
dudu new <name> | Create a new project directory. |
dudu check [input] | Parse and type-check without a native link step. |
dudu build [target] | Generate modules and compile through CMake. |
dudu run [target] -- [args] | Build and run an executable target. |
dudu test [filter] | Build and run matching @test functions. |
dudu fmt [path] [--check] | Apply or verify canonical formatting. |
dudu clean | Remove project build outputs. |
dudu clean-cache | Remove compiler analysis caches. |
dudu deps fetch | Fetch declared path/Git dependencies. |
dudu update | Update an installer-owned toolchain; supports --check and --rollback. |
dudu cmake | Emit inspectable CMake integration. |
dudu bench compiler | Run compiler benchmarks. |
Compiler driver
duc check src/main.dd
duc emit src/main.dd -o build/main.cpp
duc emit-modules src/main.dd -o build/generated
duc fmt src/main.dd --check
duc src/library.dd --emit-header build/library.hppAdd --timings to project commands when a build feels slow. Dudu then reports elapsed analysis, generation, CMake, compile, and run stages separately.
Differences from Python
Dudu keeps Python's common statement and expression shapes, not Python's dynamic object model.
| Not in Dudu | Use instead |
|---|---|
| Runtime type changes and monkeypatching | Static bindings, fields, and explicit variants. |
lambda | Named functions stored as function values. |
| List/dict/set comprehensions | Explicit containers and loops. |
Generator expressions and yield | Containers, callbacks, or imported iterator libraries. |
| Ternary expressions | An explicit binding plus if/else. |
with context managers | RAII values and lexical scope. |
async/await | Threads, atomics, event loops, callbacks, or native task libraries. |
Metaclasses, descriptors, eval, and dynamic attributes | Compile-time declarations and ordinary functions. |
| Arbitrary-precision default integers | Explicit fixed-width integer types. |
See the language tour for side-by-side Python, C, C++, Rust, GLSL, and array-programming examples.
Formal reference
This manual is the public learning and working guide. Exact language references remain in the repository:
- Appearance spec: canonical source syntax and semantics.
- Collections and literal inference: list, dictionary, set, expected-type, and heterogeneous-value rules.
- Fixed arrays and numeric literals: shape inference, numeric context, layout, and diagnostics.
- Compile-time programming: constants,
@constexpr, build values, and layout checks. - Generics and value parameters: type inference, value arithmetic,
dyn, and generated C++ templates. - Import semantics: Dudu modules, native headers, aliases, collisions, and editor ranges.
- Native templates and macros: imported C++ templates, callable macros, and boundary cases.
- Allocation and lifetimes: values, RAII, raw storage, custom allocators, and pointer escape diagnostics.
- Arrays, views, and indexing: arbitrary-rank slicing, views, library operators, and shape diagnostics.
- Known limitations: current alpha constraints.
- Native compatibility matrix: locally validated C and C++ libraries.
Dudu is alpha software. The reference pages describe tested current behavior; the limitations page records known boundaries.