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.
