Skip to content

19. Roadmap

Each milestone ends with something you can actually run.

# Milestone Ends when
M0 ✅ Rust workspace, spans, diagnostics, lexer port done — dz_span / dz_diag / dz_lex / dz build clean, 16 tests green, 10 snapshot corpora in tests/lex/, and examples/example.dz and examples/state.dz lex with zero errors
M1 ✅ Parser + AST + dz fmt done — full §20 grammar parses with error recovery; dz parse and dz fmt work; 40 tests green. Round-tripping is pinned as three properties over the corpus: formatting is idempotent, preserves the tree, and re-parses cleanly. Note: examples/example.dz and examples/state.dz parse only up to the point where they switch to pre-spec sketches (nested struct inside union, native machine, def, @(...)); lines 1–121 of example.dz parse with zero errors
M2 ✅ Projects, modules, imports, visibility done — dazzle.toml loading with span-accurate manifest diagnostics, source globs, dependency graph with cycle detection, module symbol tables, import resolution, :: path resolution and visibility enforcement. dz check runs it. A three-module workspace resolves clean; violations are rejected with cross-file labels and “did you mean”. Scope note: .field / .method() resolution needs the receiver’s type and belongs to M3 — :: is the compile-time operator, . the runtime one (§3.3), and the phase split falls exactly there
M3 ✅ Type checker: primitives, structs, functions, enums done — literals and their contexts, no implicit numeric conversions, casts, structs/fields/methods with .-side visibility, mutability, enums and tagged unions with exhaustive switch, T? and fallible returns with the or clause and the consumption rule, arrays, slices, ranges and tuples. Note: M3 shipped against the v0.4 sigils; the v0.5 syntax is a re-spelling of the same checks plus the ^T → &T conversion rule. dz check runs it. Generic bodies are skipped rather than misreported, since generics are M6
M4 ✅ Codegen + runtime shim + dz run done — hello world runs. C++17 emission with #line mapping back to .dz, the runtime shim, dz cpp / dz build / dz run, and end-to-end tests that compile and execute real programs. Output is warning-clean under -Wall -Wextra -fno-exceptions -fno-rtti on both clang++ and g++. Codegen covers functions, structs, enums, control flow, pointers, arrays and defer; T?, fallible returns, and tagged unions report DZ0801 and are owed by M5
M5 ✅ Ownership: own, copy, drop, move, flow analysis done for everything that is not generic. copy and drop become C++ special members, move steals and resets, move-only types have their copy operations deleted, and flow analysis reports use-after-move, partial moves, moves out of a borrow, implicit copies and missing returns. Codegen moved onto the HIR, which unblocked T?, fallible returns, tagged unions, tuples and block-valued expressions — the DZ0801 rows M4 left behind. tests/run/ownership compiles and runs clean under ASan on clang++ and g++. Buffer<T> and String themselves are generic, so the literal criterion moves to M6; nothing about them is blocked on ownership any more
M6 ✅ Generics + traits + monomorphization done. Buffer<T> runs as a generic struct with an own ^T field, a drop, a folded compile-time branch on T::is_trivially_copyable, and construct/destroy, allocating through alloc/free — instantiated for a trivially-copyable and a resource-owning T in tests/run/generics. Generic bodies are checked once against their bounds; traits, conformance, associated types, default methods and bounds work; operators dispatch through the prelude’s traits; for runs over a user Iterate; format strings are checked. Buffer<T> has a copy exactly when T is Copy (§18.26, settled in r12). Static dispatch only: ^Trait fat pointers (§7.6, §18.6) and therefore a stored ^Allocator are not in this milestone, so Allocator exists as a trait and a bound while Arena and Allocators::Global wait for the stdlib pass
M7a ✅ C interop: a vendored C library, built from source done — tests/run/cinterop calls a two-file C library (mini.h + mini.c) that dz compiles from source from a [cpp.libs] recipe, with no CMake and no prebuilt binary, on clang++ and g++. import cpp reads the header through libclang into ordinary Dazzle names; ^T/^mut T (r18), recipes (r19) and the import itself (r20) got there
M7b ✅ Full C and C++ interop done — a raylib window opens. examples/raylib opens one with raylib 6.0 built from its own seven .c files. r21–r28 added what that needed and then the C++ half: handles, C unions, array members and bit-fields, callbacks, compound-literal macros, then namespaces, classes, methods, constructors, C++-owned destructors, overloaded methods, references and [cpp.instantiate] template instances, covered by tests/run/cppinterop on both compilers. What is left is narrower than the milestone and is in the deferral tasks in docs/tasks/: overloaded free functions (methods do resolve), the boundary cast for an overloaded method, and inheritance between imported C++ classes — which waits on struct inheritance (18.32)
M8 ✅ Unions, switch, T?, !->, the or clause done — tests/run/button runs the ButtonState sample as the language specifies it: a union whose variants are the existing structs DefaultState, TransitionState and ChangeSprite (r38), each state’s update returning the next as a ButtonState?, dispatched by a switch that calls the payload’s own methods, around a fallible !-> load handled by or return, a fallback and { e -> … }. switch on constants, generic unions and statement-built values landed with r36, and a base initialiser whose arguments need statements with r38. A variant carrying a struct that owns a resource or has special members waits on variant drops, with the M12 stdlib
M9 LSP: diagnostics, hover, go-to-definition, completion usable in VS Code for real work
M10 ✅ Macros done — tests/run/machine is tests/run/button with its machine written by a user #machine (§12.1), and prints exactly what button prints; inside the invocation the editor completes a state’s parameters, fields and members, and hovers on them. r63 settled the notation: #name everywhere, captures like parameters, shapes like types, #for/#if/#error, expansion before resolution, hygiene for locals only, and methods on unions. Completion from the macro’s grammar — offering state — is left in the tasks in docs/tasks/, with type macros, attribute macros and T::fields of a parameter
M11 ✅ Reflection + type table done — tests/run/reflect is the criterion: a JSON serializer of about 50 lines, written once against typeinfo(id), writes structs (by json:"…" tag), enums by name, a union’s active variant, arrays, a slice and optionals, and skips private fields. The table describes every kind of type (18.59), is filled once by a function-local static rather than a global constructor, takes offsets from offsetof, and compiles warning-clean on clang++ and g++. Compile-time T::fields of a type parameter is deferred (§12.11)
M12 Stdlib pass: List, String, HashMap, allocators, math you can write a small game without touching C++
M13 Hot reload (§17.6) dz run --hot swaps a rebuilt module and migrates world state without restarting the game
M14 ✅ dz test — tests as a language feature done — #[test] fn name() { … } is written beside the code it tests; it passes by returning and fails by panicking. dz test [path] [filter] builds a separate test binary, runs each test in its own process — a panic aborts, so one process could report only the first failure — and prints a per-test result, the failing test’s own panic message, and a count, exiting non-zero if anything failed. A #[test] that takes an argument, returns a value, is fallible, is generic or has no body is refused (DZ0546). A normal build leaves every test out, so a program never carries them
M15 ✅ dz bench, and the C++ comparison done — #[bench] fn is timed by dz bench, which calibrates the iteration count until a batch lasts 50ms, runs five batches and reports the fastest (noise only ever adds time). It builds --release by default, since timing -O0 measures a build nobody ships. tests/bench is the comparison: each benchmark is a pair, one Dazzle and one calling hand-written C++ vendored beside it, compiled at the same optimisation level into the same binary and timed by the same clock. Covers bounds-checked indexing against unchecked, drop against a C++ destructor, and virtual/override against C++’s own. tests/bench_programs adds whole programs, each written in both languages and timed end to end with peak memory and build time (T-0090). Still to add: generic instantiation, and tracking the numbers over time
M16 Packages: dependencies that are fetched dependencies accepts a git source pinned to an exact commit, resolved into a lockfile that is checked in, fetched into a local cache and built from source like any path dependency (§3.1)
M17 Freestanding targets: no heap, no operating system dz build --target <board> builds for a microcontroller — an Arduino Uno (AVR), an STM32 and an nRF53 — and one program blinks an LED on each, its board support an ordinary package over the vendor’s C SDK through import cpp (§15). A target is a set of capabilities — heap, operating system, files, threads, pointer width — that #if reads (§12.10). std stays one module (§14.4): its heap and OS parts are refused on a target without them, by an error naming what is missing rather than an unknown name, so List(8) asks for an allocator where there is no Allocators::Global; what differs per target is the extern fn backend (§18.65), not the Dazzle above it. The C++ runtime shim includes only what the target has, and a panic goes to a handler instead of stdio. Text and lists of a fixed capacity, held inline with no allocator, make String- and List-style code work without a heap; they need value parameters in generics (a String<32>), which §8 does not have yet
M18 Web examples/breakout runs in a browser, built by dz build --target web through Emscripten from the same source that runs on the desktop. The target model is M17’s, built by whichever of the two comes first; this one has OS_WEB and a 32-bit usize. The open question is the frame loop: a browser cannot block in while !window_should_close(), so either the loop becomes a per-frame callback that the standard library or an engine drives — which changes how a game is written — or Asyncify keeps the loop as it is, at a cost in code size and speed. Time::sleep cannot block the page, files come from a preloaded virtual file system and saves from IndexedDB, and threads need SharedArrayBuffer (§18.11)

Milestone order is not release order. M14 and M16 are tooling rather than language, and either may be pulled forward when it starts costing more to do without — as M7 and most of M9 already were.

M4 is the one to aim at hard. Everything before it is invisible; the moment dz run prints something, every later milestone becomes testable end to end.