Skip to content

21. Changelog

Revisions of the language. 0.1 was released at r117, on 2026-11-04; every row newer than r117 came after it. Every change to the language — syntax, semantics, or a §18 decision — gets a row here, whether or not the compiler has caught up with it yet. Compiler-only work belongs in docs/tasks/ while open and docs/log/ once done, not here.

Revision Date Change
r117 2026-11-04 Elements whose indices are known apart are separate places (§6.7; T-0168, asked for by the user). A kill through one element of a container ended a view of any other, since xs[i] and xs[j] may be one element. Two indices are now known apart when both are literals that differ (xs[0], xs[1]), or both are the same local that is not mut plus or minus different literals (xs[i], xs[i + 1], xs[i - 1]); such a local keeps its value until its binding runs again, and from then on an element it indexed is any element. So names[i].push_str(names[i + 1].as_str()) compiles, and so does a view of names[1] kept while names[0] grows, or of nest[0] while nest[1] grows. The same element, two different locals, a mut local and any other index expression are refused as before, and a kills clause still names elements as [*].
r116 2026-11-04 A static whose value takes code is built when the program starts, in a fixed order (§7.7; D-0094; T-0166, the user’s choice of option B). A static’s initialiser is still made of constants, but the fields a struct literal leaves out take their defaults, and a default may call a constructor that reads another static. C++ built such a static before main, in an order it does not promise across files, so a safe program could read a static C++ had not built yet. Every such static is now built before main’s body (and before a test or benchmark runs): a module’s statics after those of the modules it uses, and in order of declaration within one, the reverse of the order they are dropped in. A static read before its turn is built where it is read, and one read while it is being built panics, naming it. A static of constants alone is unchanged, and costs nothing to read.
r115 2026-11-04 A loop runs as written, and a reinterpreted pointer reads the bytes as its type (§1.1, §10.6, §16.5; T-0133). An audit of the generated C++ found that clang++ and g++ both deleted a loop with no side effects that never ends, while x < 10 {} with x unchanged, and carried on past it, since C++ lets an optimiser assume such a loop ends. §1.1 now promises a loop runs as written, and the generated code is built with -fno-finite-loops. A pointer cast with as (only in unsafe) now reads the memory there as the type it names, whatever was stored, rather than leaving the optimiser free to assume two types never share memory (-fno-strict-aliasing); alignment, and a valid value of the type, are still the unsafe block’s to vouch for. §10.6 also states what was already true on every compiler Dazzle supports: narrowing to a signed integer keeps the low bits in two’s complement (300 as i8 is 44), and an f64 too large for an f32 becomes infinity.
r114 2026-11-04 A String keeps short text in place, and a view inside an element dies with the array (§6.7, §14.2; D-0095; T-0156). Text of up to 15 bytes is kept inside the String, allocating nothing, as std::string keeps it; a String is still 32 bytes. Short text moves with its String, so a view of it dies when the string moves, as before, and now also when a list holding it grows, sorts or swaps: let v = names[0].as_str() kept across names.push(…) is DZ0413, where §6.7 said a view into one element’s text survived pushing another. The rule is general: what a path reaches from an element through no pointer’s storage is in the element’s own bytes and dies with the array, so a method’s reference into an element (list[i].left_of()) does too, as &list[i].left already did; that was a hole for any type. What an element owns behind a pointer stays where it is. A tie naming plain data, tie @text = m_store on an array of numbers, names those bytes, so a kills clause can name them.
r113 2026-11-03 Safe Dazzle has no undefined behaviour on one thread (§1.1, §0.1, §7.7; D-0094 point 8; T-0160, T-0163). §1.1 listed memory safety as a non-goal; it now promises that a program with no unsafe reads or writes no freed memory, uses no payload under another variant, and reaches nothing C++ leaves undefined, while keeping the aliasing Rust refuses. It names where the promise ends: unsafe, std’s trusted bodies (checked in debug builds), the C and C++ a program calls with what its recipe vouches for, threads, and running out of stack. One hole was found and closed on the way: every static is dropped at exit in an order a drop can rely on, a module’s statics before those of the modules it uses and in reverse order of declaration within one, and each is left at its default. C++ destroyed statics in different files in no promised order, so a static’s drop that read another could read freed memory. A drop that reads a static already dropped now sees an empty value.
r112 2026-11-03 A kills clause may name a static, and changing a static is safe (§6.7, §7.7; D-0094 point 7, revising D-0046; T-0136). Writing a static was unsafe, so a safe function could grow a static List while its caller held a reference into it, and nothing noticed. Now a tie path may start at a static, named as an expression names it, SCORES or world::SCORES, and a static is a place the ties check follows like any other. A function that kills memory a static owns names it in its clause, fn add(v: i32) kills SCORES.list@items (DZ0415 otherwise, with the fix), and so does every caller, up to an entry point (main, a #[test] or a #[bench]), which nothing calls. A reference into the static dies at such a call (DZ0413). A write that kills nothing, FRAMES += 1, needs no clause and no unsafe. A kill inside unsafe is vouched for there and needs no clause, which is how a C callback changes one. A pub function’s clause names only a pub static (DZ0563). A result in a static says so with in, -> &i32 in SCORES.list@items, and a function type or trait method takes a function whose clause names a static only if its own names it too.
r111 2026-11-03 A C or C++ call that can be handed a pointer is vouched for in its recipe (§15.1, §15.2; D-0094 point 6; T-0135). §15 said every call into a header was ordinary safe code, so sapp_set_userdata(&mut state) handed C a pointer to a local with nothing to mark it. Now a function that takes only values (numbers, enums, a C string, a callback, a handle, a struct by value with no pointer inside) is safe to call, as before. One that takes a pointer, a reference or such a struct, and a C++ method or constructor, which C++ hands this, is unsafe to call (DZ0528) until the library’s recipe vouches for it with keeps = […], naming the functions that keep a pointer past the call. Those stay unsafe, and the rest become safe; keeps = [] says none keep one. A name keeps lists must be one the headers declare (DZ0004). A void* a library keeps needs no entry, since only an unsafe cast makes one. raylib’s calls take only values and need nothing; the examples’ other recipes gained a keeps line, and ma_device_init and RGFW_window_createSurface are now called in unsafe.
r110 2026-11-03 A defer runs among its scope’s drops (§6.5, §10.5; T-0130). A scope’s drops and defers run together in reverse order of declaration, so a defer sees every local declared before it alive, and a local declared after it is already dropped. §6.5 and §10.5 said a defer ran after all of its scope’s drops, which would have let it read dropped locals; the compiler always ran it in the new order, so no program behaves differently. A pointer read in a defer is now checked against that order: one into a local declared after the defer is DZ0413.
r109 2026-11-03 Arithmetic is defined (§5.1, §10.6; D-0094 point 5, extending D-0063; T-0129). What C++ leaves undefined now has an answer in every build. An integer / or % by zero panics, naming the line, and a divisor that is a constant zero is an error (DZ0580). MIN / -1 wraps to MIN and MIN % -1 is 0, as overflow wraps. A shift count is taken modulo the width (1 << 40 on an i32 is 1 << 8), and a negative value shifted left wraps. A float converted to an integer saturates at the type’s MIN or MAX, and NaN becomes 0. A float divided by zero is still an infinity or NaN.
r108 2026-11-03 or unreachable panics in every build (§9.4; D-0094 point 4, revising D-0020; T-0128). Reached, it panics with its location in release and shipping builds too, where it used to tell the optimiser the failure could not happen: a failed search written find(&xs, v) or unreachable ran its loop past the end of the list. The assumption is kept inside unsafe, a block or an unsafe fn’s body: unsafe { x or unreachable } is still checked in debug and free in release.
r107 2026-11-03 Safe code has no undefined behaviour (D-0094 points 4–8, revising D-0020 and D-0046; will revise §1.1, §5.1, §5.2, §6.7, §7.7, §9.4, §10.6, §11 and §15). Decided; not yet implemented (T-0128, T-0129, T-0135, T-0136, then T-0160). x or unreachable panics in every build; the optimiser’s assumption is written unsafe { x or unreachable }. Arithmetic is defined: integer / and % by zero panic, and a constant one is an error; MIN / -1 wraps to MIN and MIN % -1 is 0, as D-0063 has overflow wrap; a shift count is taken modulo the width; a float converted to an integer saturates, and NaN becomes 0. A C or C++ call is trusted where its header is imported: a function taking only values is safe to call, and otherwise the importing module trusts the header once in its manifest, listing the functions that keep a pointer, which are unsafe to call. A kills clause may name a static, and a write to a static that its clause covers is safe, so a reference into a static is checked across a call like any other. Once these are built, §1.1 says safe Dazzle has no undefined behaviour on one thread, the exceptions being unsafe blocks, std’s trusted bodies and threads.
r106 2026-11-03 #[inline] is implemented (§12.8; T-0153). On a function or a method with a body, it makes the body visible to every module that calls it, so an optimised build can inline the call; it changes nothing a program does. It was DZ0316, described but not built; on a function with no body to show (a trait method without a default, a body-less virtual fn, an extern fn) or on anything but a function or method it is DZ0315. std marks its small, often-called functions with it: String‘s len, is_empty, as_str, push, push_str and clear, hash_combine, and the text iterators’ steps.
r105 2026-11-02 A copy is built where it is placed (§14, §14.6; D-0091; T-0140). A copy is a value of its own, so the compiler places it like any other: let names = other.names, or counts.insert(word, 1), builds the copy in a region when it never outlives one, never where the original lives. #placement in a copy is where the copy goes, as it is in a constructor (it was the heap): a container’s copy builds from it, which is how std’s List and HashMap copy now. A container that builds a copy from the original’s allocator instead still has to get that right itself, as before. A copy made by assignment, or by C++ code the compiler does not see, is on the heap. Nothing changes what a program prints.
r104 2026-11-02 No allocator in ordinary code (§12.7, §12.8, §12.9, §14, §14.6; D-0091; T-0119). std’s constructors take no allocator: List<T>(capacity), HashMap<K, V>(capacity), HashSet<T>(capacity), SlotMap<T>(capacity), String(s), String::from(s), String::from_utf8(bytes), Path(text), Box(value) and Rc(value). The compiler places what they build (§14.6), and format! takes no allocator = … (DZ0558), so no formatting macro takes anything by name. A container learns where it was placed from the new intrinsic #placement, a ^mut Allocator read only in unsafe: in a constructor, where the value being made goes; in a function returning something that owns memory, where its result goes; anywhere else, the heap. A program’s own container that stores it and allocates from it is placed as std’s are, and freeing into the same allocator and building a copy on the heap are its own to get right. #placement cannot be a parameter’s default (DZ0579), where it would be each caller’s, and placement joins the reserved macro names. An Rc is always on the heap, since a Weak can outlive any scope. Allocator, Allocators::Global and Allocators::Temp stay, for raw memory.
r103 2026-11-02 Three holes in the ties check closed (§6.7, §14.5; D-0094 points 1–3, revising D-0062; T-0123, T-0124, T-0125). Each refused program read freed memory when it ran. A call may reach what it kills through one argument only: append_copy(&mut ys, &ys), where the clause kills dst@items and src points at the list that owns it, is DZ0420, as is a second argument naming the same kill. Arguments may still share memory the call does not kill, or memory that cannot die. Every Rc<T> and Weak<T> holds one value to the check, since any two may share it: a view through one is ended by a kill through another, and a call given two of them, one to change, is refused. A Box’s value is in the box, @value: emptying, replacing or moving the box ends a reference if let or or took to its value, where D-0062 let it dangle; a clause names what is inside as h.node@value.name@text. None costs anything at run time. Also enforced, with no rule changed: a value with a drop uses its views when it is dropped (T-0127).
r102 2026-10-31 std’s trusted clauses are checked at run time (§6.7, §14.2; T-0116). A debug build traps when a function’s kills clause leaves a &mut input’s pointer tie out (@items) and the pointer changed by the time it returns. The new manifest key [build] check_trusted = true adds the element half, for test runs: a function that does not kill [*] traps if a block its input’s elements owned on entry was freed or reused by the time it returns. It turns track_memory on, and each tracked allocation is now numbered. Release builds check nothing.
r101 2026-10-31 An untied pointer becomes a reference only in unsafe (§5.2, §6.7, D-0093 point 11; T-0114). p or … on a pointer — ^T, own ^T, ^cstr, ^str, ^[]T — is safe only when the pointer is tied: made from a place (&mut world.players[0]), or returned by a safe call under a tie, in or elided, whose body is held to it. A pointer parameter, a pointer field with no tie, a cast and C’s pointers are untied, and converting one outside unsafe is DZ0418: unsafe { ctx.world or return }. A function returning a pointer whose tie is unknown under a tie it declares, return ctx.world from -> ^mut World in ctx, is DZ0419, since its callers would convert it safely; return unsafe { ctx.world } vouches for it. An editor offers the unsafe for both; dz fix never adds one. This closes the last unchecked conversion that needed no unsafe: a context struct’s ^mut World now needs unsafe to use, which is honest.
r100 2026-10-31 A C string is borrowed: &cstr, const &cstr and ^cstr (§5.3, §15.2, D-0093; T-0112). &cstr views what owns it and is tied to it (String::as_cstr() -> &cstr in self@text), so it is a view and cannot be kept in a field; a literal’s is const &cstr, which may be stored; C’s own pointer is ^cstr, untied and possibly null. An imported C const char* is ^cstr: a C function returns one, and a parameter takes any of the three, or a String or Path through ImplicitAs<&cstr>. or turns a ^cstr into a &cstr, and null empties one; measuring C’s ^cstr as &str is unsafe. A bare cstr, const cstr, &mut cstr and ^mut cstr are DZ0578, respelled by dz fix. A &cstr result, or an extern fn’s text result, must now be tied to an input like any view; text an extern fn does not own is ^str (std’s dir_next, null at the end) or, when it lives for the whole run, const &str (process_arg). SourceLocation’s file and function are const &cstr.
r99 2026-10-31 Slices are borrowed: &[]T, &mut []T, const &[]T and ^[]T (§5.3, §5.6, §6.7, D-0093; T-0110). A slice is written as the reference it is: []T becomes &[]T and []mut T becomes &mut []T, the writability being the reference’s; a bare []T, []mut T, const []T and &[]mut T are DZ0577, respelled by dz fix. A slice is a view, so a slice field (outside a view struct), container element or type argument is DZ0517. A slice that may be absent is ^[]T, empty as null. A table in static storage is const &[]T, which may be stored; reflection’s TypeInfo::fields and variants and VariantInfo::fields are one. A &mut []T parameter may kill through its elements, as a &mut T may. Fixed arrays [N]T stay values, and slicing xs[a..b] gives a &[]T. C++ is unchanged.
r98 2026-10-31 Text is borrowed: &str, const &str and ^str (§5.3, §5.6, §6.7, D-0093; T-0109). str is written &str, static text const &str (was const str), and text that may be absent ^str (was str?), whose empty case is null; a bare str, const str, str?, &mut str, and none for a ^str are DZ0575, and dz fix respells each. An optional of a reference written out, (&T)?, is DZ0576, respelled ^T. Text is a view, so it lives only where a reference does: a &str field (outside a view struct), container element or type argument is DZ0517 (List<&str> is an error; List<const &str> and HashMap<String, V> are the ways to keep text). A trait’s argument may be a view, since it stores nothing (ImplicitAs<&str>, Div<&str>). A String passed where a &str is wanted is its text, borrowed for the call, through std’s new impl ImplicitAs<&str> for String. An implementation for text is written for &str. With no plain T a view, a result that was a view only through T is no longer exempt from ties. C++ is unchanged.
r97 2026-10-31 T::Item? whose instance is a reference is a pointer (§5.6, D-0093; T-0111, T-0085’s option 3). A generic function or a method of a generic type returning an optional of a projection, fn last<view T: Iterate>(it: T) -> T::Item?, now returns ^i32 where the instance makes it an optional of &i32 (^mut i32 for &mut i32), tied as the reference would be: let p: ^i32 = last(xs.iter()), last(xs.iter()) or return. Before, it was an optional of a reference, which C++ refused (DZ0803). Inside the generic body nothing changes: it is an optional for every instance.
r96 2026-10-31 A view struct may hold a reference (§5.2, §6.2, §6.7, D-0093; T-0111, the user’s choice). A view struct may now have a &T or &mut T field, as D-0093 allowed once its C++ existed. A reference has no zero value, so such a struct has no defaults: a literal names each reference field, let it: T with no value is an error, and a reference field takes no = default. A constructor binds each reference field in its opening statements, before any other (m_map = map); anywhere else, assigning to a reference field writes through it, and through a &T field that is an error. A reference field may not refer into its own struct. Each of these is DZ0574. A tie on a reference field names what it refers to, and a view struct’s constructor is in its one view input, as a function returning the struct is. std’s HashMapIter, HashMapIterMut, HashMapKeys, HashMapValues, HashMapValuesMut, SlotMapIter and SlotMapIterMut hold a & or &mut to their map where they held an untied ^, so what they hand out is in the map.
r95 2026-10-31 A view struct given to a function stands for what it views (§6.7, D-0093; T-0111, the user’s choice of option A). A view struct taken by value, or a value of a view parameter, is now a view input, as a reference or a slice is, and a result elided to a view struct (or in it whole) is in what the struct views rather than in the struct: let a = first(&xs.iter()) keeps the item past the temporary iterator, and it dies when xs grows. Such a result may not be the struct’s own storage, a field holding no view or the struct behind a reference (DZ0414); in p.at names that instead. A result of a view parameter’s type or a view type projection (T::Item) now holds a view and is tied, where before it was unchecked. Several view inputs counted this way and no in is DZ0570, as before.
r94 2026-10-31 An implementation promises no more than its trait (§6.7). §6.7 already said an implementation’s kills clause may name no more than the trait method’s; it is now checked, and so is the result’s in: every path an implementation names is one the trait names, or under one, parameter by position, and with no in either side’s result is in its one view input. Otherwise it is DZ0573, since a call through the trait, in generic code or on a trait object, reads only the trait’s. A trait method with no clause promises to kill nothing, so an implementation that kills is now an error until the trait says so, fn sort(&mut self) kills self.
r93 2026-10-31 An item outlives its iterator (§6.7, D-0093 point 3; T-0111). A tie a view struct declares on a view field names what that field views, not the field, so a method returning through it gives a result in whatever the struct was made from: with tie @items = m_slice and fn get(&self) -> &T in self@items, let it = xs.iter(); return it.get() is in xs, where before it was refused as a view of the local it. It still dies when xs’s array does. std’s SliceIter, SliceIterMut, Split, Lines and SplitWhitespace declare such ties. Also: a union’s payload and a static may not hold a view (DZ0517).
r92 2026-10-31 View structs and view parameters (§5.2, §6.7, §8.5, §20; D-0093 points 3 and 8 as r90 refined them; T-0111, first part). A struct that holds a view is declared view struct, and is itself a view: it lives only where a reference does, never in a field, a container or a type argument. std’s iterators are view structs — SliceIter, SliceIterMut, Chars, Split, Lines, SplitWhitespace and the map iterators. A type parameter written view T may be given a view, and its body treats a value of it as one; a plain T never is, so keep<T>(xs.iter()) and List<SliceIter<i32>> are now errors (DZ0517). A struct with a view parameter must be a view struct, and an implementation that reaches one takes the parameter as view too: view struct Numbered<view I: Iterate>, impl<view I: Iterate> Iterate for Numbered<I>. A trait’s associated type that may be a view is a view type: Iterate::Item, Iterable::Iter and IterableMut::IterMut are, and a view chosen for any other is DZ0517. view is a keyword only before struct, type and a type parameter, so it is still a name elsewhere. Not yet: a &T field in a view struct, and a method result that outlives the view struct it came from (in self@items through a field’s tie).
r91 2026-10-30 A function type says where its result points (§6.7, §20, D-0093 point 9; T-0113). A function type writes in after its result as a declaration does: fn(a: &Enemy, b: &Enemy) -> &Enemy in (a, b). With none, its result is in its one view input; several view inputs and no in is now an error (DZ0570), and so is a view result from none (DZ0571), so fn() -> str must be fn() -> const str. A call through a value of the type ties its result as the type says, so keeping it past the death of either input is a use after a kill. A function fits a function type only when the type’s in covers its own: first(a, b) -> &T in a fits -> &T in (a, b), the reverse is DZ0572. Before, such results were left unchecked.
r90 2026-10-30 Views through generics, and the optional view (D-0093 refined; will revise §5.2, §5.6, §8.5). Decided; not yet implemented (T-0111 first, then T-0109). A type parameter or an associated type written view may be a view, and the generic body checks it as one: fn count<view T: Iterate>(it: T), trait Iterate { view type Item }, so iterators and their adapters stay generic while a plain T is never a view; a struct with a view parameter is a view struct. view is a keyword only before struct, type and a type parameter, so let view = … stays valid. An optional view is a pointer: an element or nothing is -> ^T in self@items, text that may be absent is ^str rather than str?, and (&T)? is an error; a T::Item? whose item is a reference is that pointer. A view struct may hold references (refining D-0003). A pointer parameter is untied, so turning one into a reference will need unsafe.
r89 2026-10-30 Views are borrowed (D-0093; will revise §5.3, §6.7, §15, §20). Decided; not yet implemented (T-0109 and the tasks it lists, in order). Text is &str and static text const &str; slices are &[]T, &mut []T and const &[]T; cstr is &cstr, const &cstr or C’s untied ^cstr. A view lives only where a reference does, so List<&str> and a &str field are errors, while const & views, which point at static memory, may be stored anywhere. A struct that holds a view is declared view struct, and is placed and tied as a reference is: iterators are view structs. A function type may write in. A String passes as a &str through ImplicitAs. Turning an untied ^T into a &T with or needs unsafe. Together these leave nothing unchecked but raw pointers, unsafe code and std’s trusted bodies.
r88 2026-10-30 Every returned reference says where it points (§6.7, §20, D-0091). A function returning a reference, pointer, slice or str — or a struct, tuple or optional holding one, such as an iterator — is in its one view input when it writes no in, as before; but with several view inputs it must now write in (DZ0570), and with none, a reference, slice or str result is an error (DZ0571): return a const str, something owned, or take what it points into. in may name several paths in parentheses, -> str in (a, b), for a result that may be in any of them; a kill of any ends it. Before, such results were silently unchecked, so let it = xs.iter(), xs.push(1), then using it compiled and read freed memory, as did keeping half of s.split_once("=") across a change to s. Still untied: a result that holds a view only through a type parameter (pop() -> T?), a raw pointer or cstr with no input, and an extern fn’s with no input. In std, strip_prefix, strip_suffix, split_once, split and tag_value are in self, an Allocator’s realloc is in (self, p) (an arena may hand p back), and contents_allocator is in (self, fallback).
r87 2026-10-29 const str: text that never dies (§5.3, §6.7, §20, D-0092). Text in static storage, a literal’s or a constant’s, has its own type, const str: the same { ptr, len } and C++ type as str, reading as a str wherever a const str is not wanted, but a str never becomes one. A string literal is a const str wherever one is wanted. Nothing kills one, so it carries no tie: fn name(&self) -> const str returning literals no longer ties its result to self, a name can be held across a change to what it named, and a field or a container can hold one safely. A let with no type keeps a const str value’s type. Only str may follow const (DZ0569).
r86 2026-10-29 A function type’s kills clause is checked (§6.7, D-0091). A function is stored in, or passed as, a function type only when the type’s clause covers its own, parameter by position; a function type with no clause promises to kill nothing, so a function that kills something no longer fits one (DZ0568). A call through a value of the type kills what the type’s clause names, at the call’s arguments, as a direct call does. A function type’s paths must name its parameters and kill only through &mut or ^mut ones (DZ0562, DZ0564). The clause is not part of the type’s identity. A pointer to a C function, whose kills nobody declared, fits only a function type with no &mut or ^mut parameter.
r85 2026-10-29 Taking an element out of a container kills its [*] (§6.7, D-0091). List‘s pop, remove and swap_remove, the maps’ and sets’ remove, and a map’s insert and replace (which hand back or drop the value they replace) now kill self[*] beside self@items. A view into an element’s own memory — names[0].as_str() — used to survive taking that element out, and then dangled once the caller dropped what it was given, with nothing to catch it. Now it ends at the call, and so does a view into any other element’s own memory, since they share [*]. A container of plain data owns nothing in [*], so nothing changes for it. Moving elements within a container (swap, sort_by, reverse) still leaves [*] alone.
r84 2026-10-29 A struct declares its ties itself (§6.7, §2.7, §20, D-0091; replaces r81’s field ties and r83’s way of tying fields together). A tie is a member, tie @items = m_data, listing the memory of the struct’s own that it stands for, and tie is a keyword. Its paths start at the struct’s fields, private ones included, and may name another type’s tie, so a container built from containers has ties of its own: HashSet has tie @items = m_map@items and tie [*] = m_map[*], and a SlotMap, three lists, tie @items = m_values@items, m_slot_of@items, m_slots@items. Places listed together are one tie, as r83’s tied fields were: a kill through one ends a view into the others, and a clause naming the tie is exact for a body that kills only one. tie [*] = m_data[*] says where a type’s elements live ([*] on a pointer field names the array it points at), which is how a List’s own body reads m_data[i] as self[*]. in @name after a field’s type is now an error (DZ0219) that says which tie to write; a tie declared twice is DZ0567. Syntax provisional, by the user’s choice: what matters first is that it works.
r83 2026-10-29 Fields may be tied together (§6.7, D-0091). Several fields may declare the same tie, and a path naming it names all of them: a HashMap’s hashes, keys and values, three arrays that grow as one, are all in @items. They live and die together, so a kill through one ends a view into another. A path goes on past such a tie only where its fields share a type. Before, a tie declared on a second field was an error (DZ0566, now retired).
r82 2026-10-29 Writing plain data kills nothing (§6.7, D-0091). r81 said writing a place kills every tie under it, inline storage included, which made every setter of plain data (ball = Vec2{…}) need a kills clause although nothing moved. Now moving or dropping a place still kills everything under it, but writing one kills only what the old value owned (a String’s text, a List’s array) and, for a tagged union or an optional, references into its payload. A reference into a plain value survives the write, as it survives p.x += 1, and a function that only writes plain data needs no clause. A call’s clause reaches as a write does: kills w can free what w owns or change its payloads, but leaves a reference to a plain field of w valid.
r81 2026-10-27 Ties: a reference used after a call moved its memory is an error, and the compiler places memory (§6.7, §14.6, D-0091; revises §1.1, §5.2, §6.4, §14, §14.1, §2.7, §20; supersedes D-0010 and, in part, D-0047). A type names the storage it owns with in @name on a field (m_data: own ^mut T in @items), and every function declares in a kills clause which ties it ends — fn push(&mut self, v: T) kills self@items; no clause kills nothing, and the clause must say exactly what the body kills: a kill it leaves out, a path the body never kills, and a path wider than what dies are all errors, each with the clause the body needs, which dz fix writes and the editor offers. A trait method without a body, an extern fn and a function type have nothing to hold a clause to, so theirs is a promise: the most an implementation may kill. Every reference carries a tie, so using one after a call killed it is an error: let n = counts.get_or_insert(k, 0), then counts.insert(o, 1), then n += 1 no longer compiles. A function returning a reference says where it points with in (-> ^T in self@items); a tie path reaches elements with [*] — of an array, a slice, or a type with an Index<usize> or Iterable implementation, and only Index gives a path anything to go on into; kills is a keyword; a function type names its parameters where its clause needs them, and parenthesises a clause of more than one path (fn(w: &mut World) kills (w.a@items, w.b@items)). Aliasing stays legal: &mut xs[i] beside &xs[j] is fine, and only a call that can move the memory makes a later use an error. Ordinary code names no allocator: no allocator parameters and no Allocators::Temp in user code; each value goes in the most efficient place the compiler can prove safe — its scope’s region, wherever the caller wants a returned value, else the heap. Decided; not yet implemented (T-0107): until it is, containers still take allocators and nothing is checked.
r80 2026-10-24 A recipe can build a library with its own CMake (§15.1, D-0090). cmake = "vendor/SDL" in a [cpp.libs.<name>] recipe replaces its sources, include_dirs and defines: dz configures, builds and installs the library with CMake and its own C and C++ compilers, as a static library. import reads the headers it installed, and the pkg-config file it installed gives the link line. options = { SDL_TEST_LIBRARY = false } are its -D settings. Debug builds CMake’s Debug, release and shipping its Release. CMake is needed only by a project with such a recipe. Before, a library with a configure step had to have that step’s output generated by hand and vendored.
r79 2026-10-24 A C field whose struct has no name is imported (§15.2). struct { … } playback; inside a C struct, as miniaudio’s ma_device_config has, was left out of the import, with no way to set what it holds; it is now an ordinary field, so config.playback.format = ma::ma_format::ma_format_f32 works as it does in C. Its struct is named after the owner and the field, ma_device_config_playback, should a program need to write the type.
r78 2026-10-23 Arguments convert through ImplicitAs<T> (§7.2, §7.6, §14.4, D-0087, D-0088, D-0089). A type implementing std’s new ImplicitAs<T> is converted to a T, by its convert(&self) -> T, where it is passed as an argument to a parameter of type T; std implements it for String and Path to cstr, so rl::DrawText(format!("score {}", n), …) and rl::LoadTexture(path) work without .as_cstr(). Only arguments convert, never a let, a field or a return; the argument is borrowed, not moved; one conversion at most; and none to choose a C++ overload or infer a generic. The orphan rule counts a trait’s type arguments, as Rust’s does: impl ImplicitAs<Vec2> for rl::Vector2 and impl Mul<Vec2> for f32 belong with Vec2. An imported header’s types belong to the module carrying its recipe, which may implement any trait for them — impl Mul for rl::Matrix.
r77 2026-10-23 Projections name their depth range (§14.4, D-0086). Mat4::perspective and Mat4::orthographic take a DepthRange as their last argument, with no default: .MinusOneToOne for OpenGL and raylib, .ZeroToOne for Vulkan, Direct3D, Metal and SDL3’s GPU API. Before, both always produced OpenGL’s -1..1, which draws wrongly, with no error, on any of the others. Existing calls add .MinusOneToOne to keep what they did.
r76 2026-10-23 A struct’s layout is C++’s (§7.3, §5.5, D-0085). Fields are in memory in declaration order, a base’s first, with the padding and alignment the target’s C++ compiler gives them; nothing is reordered to save space. A Dazzle struct is therefore layout-identical to a C or C++ struct with the same fields in the same order, and T::size and T::align are that compiler’s answer. Programs already behaved this way; the spec now promises it. To make a struct smaller, order its fields largest alignment first.
r75 2026-10-23 Associated-type projection (§8.5). A generic body may name an associated type through a bound — T::Item for a T: Iterate, C::Iter::Item for a C: Iterable, T::Output for a T: Add — in a signature, a local, a field or another implementation’s choice (type Item = (usize, I::Item)). Inside the body it is one type, checked once against what the trait says of it: Iterate’s Item has no bounds, so it is moved and never copied. At each instance it is what the implementation chose. A name no bound declares is DZ0560; one that two bounds declare, each with its own answer, is DZ0561. So for over a generic T: Iterate or C: Iterable now works, where it was refused (DZ0801).
r74 2026-10-22 No alloc or free builtins (§5.2, §14, D-0084). Memory comes only from an Allocator: write Allocators::Global (or a container’s ^mut Allocator) and call its alloc and free, which are told the size and alignment. alloc(…) and free(…) are now unknown names (DZ0301). Underneath, Allocators::Global calls C’s malloc and free through the standard library’s own import of stdlib.h, which a program cannot name. A system header can be imported (§15.1): import = ["stdlib.h"] in a recipe brings in its declarations and those of the system headers it includes, where before it brought in nothing; a system header reached through a library’s own header is still left out. §5.2’s list of what is unsafe now names move_out and memmove, which already were.
r73 2026-10-22 SlotMap<T> and Key<T> (§14.4, §14.5, D-0083). A map that hands out a Key<T> for each value it takes: insert, get and get_mut (a ^T, null once the value is removed), remove (a T?), contains, retain, clear, for k, &v in m, and values() as a slice. A key is eight copyable bytes — Eq, Hash, Format, rebuilt from index() and version() — that a struct can keep in a field where it cannot keep a reference; a key to a removed value reads as absent, even after its slot is reused. Key<Enemy> is not Key<Bullet>. There is no reference to a reference (§5.2): &r and &mut r on a reference r, and &mut list[i] through IndexMut, refer to what r refers to. They had been typed as a reference to a reference, which no generated C++ could express.
r72 2026-10-22 A generic base (§7.3). A struct may inherit an instance of a generic struct — struct Button : Panel<i32> — getting its fields and methods with the base’s parameters read as its arguments, and constructing it with : Panel(…); a generic derived type may pass its own parameter on, struct Slider<T> : Panel<T>. A reference converts to the base at those arguments only. Before, a base with type arguments was an error (DZ0541), and &Slider<T> did not convert even to a non-generic base.
r71 2026-10-22 Constructors are chosen by their arguments (§7.2, §7.3). A call to a type with several constructors, and a base initialiser : Base(…), reaches the one that takes that many arguments — defaults counted — and whose parameters they fit, exact types breaking a tie; one still left with more than one candidate is an error (DZ0538). Before, every call took the first constructor declared, so the others could not be reached. A defaulted #location has the call’s column, and its function is the caller’s Dazzle name (Counter::bump), where it had been column 0 and C++’s unqualified name (§12.7).
r70 2026-10-22 No impl for a pointer or reference type (§7.6). impl Hash for ^Node, impl<T> Show for ^T and the like are errors (DZ0539): a method call reaches through a pointer to what it points at (§5.2), so such an implementation could never be called directly, and through a bound it reached C++ as a call on the pointee. To give a pointer behaviour of its own — keying a map by identity, say — wrap it in a struct and implement the trait for that. Formatting a pointer is unaffected; it is done by the formatting macros (r69).
r69 2026-10-22 Formatting a pointer (§12.9, D-0080, D-0082). A pointer argument of a formatting macro is written as what it points at, as a reference is — one level down, with a spec applying to what it points at — and a null one as null, where it used to be an error. {:p} writes a pointer’s address, in hex after 0x on every platform, padded by a width as a number is; it suits a pointer only, and takes no zero fill. In error(…) a pointer is written only by {:p}, and {} on one is now an error. A type parameter bounded by Format is still not satisfied by a pointer.
r68 2026-10-21 An operator reads through a reference (§5.2, D-0081): an operand of a comparison, arithmetic or bitwise operator that is a &T of a number, bool, char, str or enum is its value, whether or not a type is wanted of the result — x > 2 and let y = x * 2 on a &i32, which were errors. Keeping a view of a temporary is an error, DZ0407 (§6.5): a temporary is dropped at the end of its statement, so a str, cstr, slice, reference or pointer taken from one through a method, field, index or slice may be passed to a call in that statement but not kept by let, assignment or return — let c = String::from(name).as_cstr(), which compiled and read freed memory.
r67 2026-10-21 The collections pass (§5.3, §14.2, §14.3, §14.4, D-0081). List gains first, last, first_mut, last_mut (a ^T, null when empty), remove_range(from, to), retain(keep), append(&mut other), reverse and shrink_to_fit; Search<T> gives a list of Eq elements contains and index_of, and Extend<T> a list of Copy ones extend, insert_slice and fill; a list of Eq elements is Eq and of Hash ones Hash. HashMap gains get_or_insert(key, default) (a &mut V), retain(keep), keys(), values() and values_mut(), and a HashMap<String, V> is looked up by str with get_str, get_mut_str, contains_str and remove_str (StrKeys). HashSet<T> is new: insert, contains, remove, retain, for &v in set, and contains_str/remove_str on a HashSet<String>. String gains insert, insert_str and remove; Text gains rfind, strip_prefix, strip_suffix, split_once, split_whitespace, eq_ignore_ascii_case and repeat; Join writes names.join(", ") for a []str, List<str> or List<String>. memmove(dst, src, n) joins the placement primitives as an unsafe overlapping copy, and List moves byte-relocatable elements with it rather than one at a time. A runtime str reaches C as String::from(s) and then as_cstr() — the String is the owning C string 18.39 promised. §14.4 no longer says plain integer arithmetic makes no promise on overflow: it wraps (18.63).
r66 2026-10-21 Widths in format specs (§12.9). {:N} writes a value in at least N characters, padded with spaces, and {:0N} a number padded with zeros after its sign; the width comes first and combines with the rest, {:8x}, {:08X}, {:10.3}. A number is right-aligned and anything else left-aligned; the width counts characters, never cuts, and pads a type’s own Format as a whole. It holds in error(…) too. A width of 0, a zero fill with no width, and a zero fill on anything but a number are errors at the placeholder. Formatter gains width() and fill(). Choosing the alignment ({:<8}, {:>8}) is left out.
r65 2026-10-21 A macro’s kind decides its spelling (§12.5, §12.7, §12.8, §12.9, 18.9, 18.80). What reads like a call takes !, one bracketed group: an expr macro is max!(a, b), in any expression, a statement included; a stmt macro is log!("warn", "hp is {}", hp), as a whole statement, and its pattern now matches its brackets as an expr macro’s does; the formatting macros are println!(…), format!(…), panic!(…) and the rest. A decl macro keeps #, running to the end of the line: #machine ButtonState(…) { … }. A dependency’s is dep::max!(…) or #dep::machine …. The other intrinsics stay #file, #line, #location, #expr_text(e). Each macro has one spelling, and writing the other is an error that says which to use.
r64 2026-10-20 Formatting is a set of macros (§12.5, §12.8, §12.9, §9.8, 18.80). print(…), println(…), eprint(…), eprintln(…) and panic(…) are now written #print(…), #println(…), #eprint(…), #eprintln(…) and #panic(…), and String::format(…) is #format(…), still taking allocator = … by name — the compiler’s own macros, beside the intrinsics, whose names are now reserved too. The format string is checked as before; what changes is what may be formatted: any type that implements Format, where println used to take only the built-in kinds — so #println("{}", v) prints a Vec2 with its own Format, and a String directly. The arguments are evaluated as a call’s are. A pointer is no longer printable as an address; write what it points at. #panic’s message is cut at a few hundred bytes, since it is built without allocating. error(…) is unchanged. In std, Formatter is built with Formatter::string(&mut text), Formatter::stdout() or Formatter::stderr(), and gains flush and set_spec. A string literal written "'a'" keeps its quotes, which it used to lose.
r63 2026-10-20 Macros, settled for M10 (§12, 18.9, 18.74–18.79). One invocation spelling, #name, everywhere — name!(…) is gone: in declaration and statement position an invocation runs to the end of the line, in expression position it is one bracketed group, and a dependency’s macro is #dep::name. The intrinsics are the compiler’s own macros, and error joins their reserved names. Patterns read like parameters and types: $name: ident, $t: type, $xs: [,]Field, $xs: []State, $d: expr?, $( … )? for an optional group; kinds are ident, literal, type, expr, stmt, block, code, decl or a syntax rule, and a macro is -> decl, -> stmt or -> expr (type reserved). Matching never backtracks. expand is shaped by #for v in $xs[,] { $v.name }, #if $x { … } #else { … } and #error($x, "…"), replacing $xs[,] { $.name } and $x? { … }; an #if on anything but a capture is still the build-flag #if. Hygiene hides an expansion’s locals but not its declarations, and other names in expand resolve where the macro is declared. Macros expand on syntax, before resolution, so T::fields of a type parameter is deferred and a per-field macro takes the declaration instead (§12.11, §13.4). pub macro exports a macro; an expansion may invoke macros but not declare one. A diagnostic in captured code points at that code, one in generated code at the invocation with an “in expansion of” note. Unions may declare methods (§7.5, 18.78), which #machine’s step needed.
r62 2026-10-20 Arguments passed by name, and library formatting (§7.2, §12.9, §14.4, 18.72, 18.73). The parameters after a variadic one are passed by name, name = value, after the other arguments and in any order; nothing else is. So “variadic last” (18.56) becomes “at most one variadic”. The global format is gone: String::format("…", args, allocator = …) returns a String — no more four shared 256-byte buffers, so a long or a fifth message is no longer cut or overwritten. Format is implemented for the numbers, bool, char, str, cstr and String, and a type of your own can implement it; Formatter appends text and reports the placeholder’s spec. A format string is checked at compile time in String::format as in println; a formatting function of one’s own waits for macros (M10). println and friends are unchanged.
r61 2026-09-15 Process (§14.4): Process::args(), Process::env(name), Process::exit(code).
r60 2026-09-15 Text operations (§14.4, 18.71): the Text trait gives str and String starts_with, ends_with, find, contains, trim, trim_start, trim_end, split, lines, replace, to_ascii_lowercase, to_ascii_uppercase, parse_i64, parse_u64, parse_f64, parse_f32.
r59 2026-09-15 r54’s decisions, implemented. Integer overflow wraps in every build — i32::MAX + 1 and 250u8 + 10 included, where the generated C++ had been undefined or had not wrapped at all (§5.1, 18.63). or unreachable assumes success in release and shipping builds (§9.4, 18.20). Rc<Derived>, Weak<Derived> and Box<Derived> convert to their base’s, sharing one count and dropping the whole value (§14.5, 18.64). A generic function may be a value: xs.sort_by(greater<i32>), or inferred from the function type wanted (§8.1). Sorting (§14.4, 18.70): List::sort, sort_by, swap; String and Path are Ord. Directory::list is sorted; Path::modified and Path::as_cstr. Output to a Windows console is shown as UTF-8.
r58 2026-09-15 A literal’s suffix is its type (§2.5): let n = 5u8 is a u8 — it had been an i32 — and 300u8 or 5u8 where a u16 is wanted are errors. UTF-8 text by character (§14.4, 18.69): String::chars, for c in s, Chars(text), char_count, pop, is_char_boundary, slice, truncate, String::from_utf8; a char printed with {} is UTF-8 (it had come out as one Latin-1 byte, or nothing).
r57 2026-09-15 Files (§14.4, 18.68): Path (/, join, file_name, stem, extension, parent, with_extension, exists, is_file, is_dir), File (move-only, closed on drop: open with a FileMode, read, write, seek, position, size, flush; File::read_bytes, read_text, write_bytes, write_text, remove) and Directory (create, remove, list), every failure an Error whose code is an IoError. A method may be called on a number literal: 5.hash(), 2.25.sqrt() — the literal takes the type let would give it.
r56 2026-09-15 extern fn replaces #[native] (§7.2, 18.65): the standard library’s body-less functions are written extern fn name(…) -> T; the attribute is gone, and extern fn outside std is DZ0317. Random (§14.4, 18.67): one generator per thread — Random::seed, next_u32/next_u64/next_f32/next_f64, Random::range<T: Number>(lo, hi) in [lo, hi), chance, unit_vec2, unit_vec3. Number, a trait every integer and float implements (to_f64, uniform), now required by Integer and Float.
r55 2026-09-15 Generic methods (§8.1, 18.66). A method may have type parameters of its own — fn fold<A: Copy>(&self, start: A, …), and likewise a function without a receiver — inferred from the arguments or written out as xs.fold<usize>(0, f) and List<Cell>::filled<u8>(…). A method’s bounds are now checked, at the call and in its body; before, they were silently dropped. Not on a virtual or override method, nor in a trait (DZ0557).
r54 2026-09-15 eprint and eprintln (§12.9): print/println to standard error, formatted the same way. Time in std (§14.4): Duration, Instant and Time::sleep/Time::unix. #[native] (§12.8, 18.65): a body-less function whose body is the compiler’s C++, allowed only in std. A function with no body that is not a trait’s or a virtual method is now an error (DZ0556) rather than a link failure. Decided, not yet implemented: integer overflow wraps in every build and never panics (§5.1, 18.63), which replaces the proposed debug panic; or unreachable in a release build lets the compiler assume success (§9.4, 18.20, no longer open); an Rc<Derived>, Weak<Derived> or Box<Derived> converts to one of its base (§14.5, 18.64).
r53 2026-09-14 Handles: Rc<T>, Weak<T>, Box<T> (§14.5, 18.62). Shared ownership, a reference that notices its value is gone, and sole ownership of a heap value, in std. Each may be null — the default, and the moved-from state — and its value is reached only through h == null, h or … and if let, as a pointer’s is; Weak<T>(&rc) makes a weak one, and if let takes it directly. or and if let on an Rc or Weak keep the value alive while it is used, even if the handle is set to null meanwhile. Any type can be a handle by implementing Nullable. if let bindings follow for and case (§9.4, 18.61): if let v copies, if let &v refers, if let &mut v refers and writes — so if let p = ptr now copies the pointee, and code that wrote through p must say if let &mut p = ptr. or and if let also read through a &T?.
r52 2026-09-14 Attributes are checked (§12.8, 18.60). An attribute the language does not have is an error (DZ0314), and so is #[test], #[bench] or #[thread_local] on a declaration it does not apply to, given arguments, or repeated (DZ0315). The attributes the specification describes but the compiler does not yet implement — #[inline], #[align], #[packed], #[deprecated], #[export], #[derive], #[persist], #[raw] — are errors saying so (DZ0316) instead of being ignored. §12.8’s #[cfg(…)] example is gone: conditional compilation is #if (§12.10).
r51 2026-09-14 Reflection (§13, 18.59). typeinfo_of<T>(), typeid_of<T>() and typeinfo(id) return the standard library’s TypeInfo, which describes every kind of type: fields with offsets for structs, tuples, str, slices and optionals; variants with values for enums and with payload fields for unions; the element type of pointers, slices, arrays and optionals. Field tags: a field may end in a raw string, pub hp: i32 = 100 json:“health”``, carried into FieldInfo::tag and read with f.tag_value("json"). A type is in the table when typeinfo_of or typeid_of names it, or it is reached from one (type_info = "used") — no attribute opts a type in; type_info = "none" makes reflection an error (DZ0555).
r50 2026-09-14 Imports move into dazzle.toml (§3.2, §14.4, §15.1, 18.57–18.58). A module’s [dependencies] are what every one of its files can name, by their key — no import line per file. open = true makes a dependency’s exports bare (Vec2, and std::Vec2 still works); a bare name two opened dependencies both export is an error on the line that writes it (DZ0310), settled by qualifying it or by [names] Vec2 = "std". The prelude is now std, one flat module holding the language traits, allocators, String, List, HashMap and the math: std::Vec2, not std::math::Vec2. It must be listed — std = { open = true } — or its names cannot be written. import cpp is gone: a recipe lists the headers Dazzle sees in import = ["raylib.h"], and the module depends on it with rl = { cpp = "raylib" }; several headers are read together as one module. A dependency nothing names is a warning (DZ0313). Dependencies are private; pub = true re-exports one as lib::dep, a dependent can name it with via, and a private dependency’s type in a pub signature is an error (DZ0311). In a file, import m::{ A, B } and import m as x remain, for one file’s exceptions; import m alone and import cpp are errors.
r49 2026-09-14 Variadic parameters (§7.2, 18.56): fn sum(xs: ..i32) takes any number of i32s and sees a read-only []i32; sum(..xs) spreads a slice or array, as the only variadic argument. Must be last, with no default. ..&Trait takes arguments of different types, each borrowed for the call. Works on methods, constructors and generic functions; as a value the function takes its slice.
r48 2026-09-13 std::math (§14.4, 18.55): the first standard-library module beyond the prelude, carried by the compiler and loaded by import std::math; std and names under it are reserved for the standard library. Float and Integer methods on numbers (x.sqrt(), n.wrapping_add(m), checked_add, saturating_sub), Vec2/Vec3/Vec4, Vec2i/Vec3i, Quat and a column-major Mat4. Also: unary - on a struct is its Neg (§11.2), == accepts a temporary on its right as well as a place, and the most negative value of a signed type is a literal — let x: i8 = -128 was rejected because 128 was checked before the minus (§2.5).
r47 2026-09-13 Four design decisions, 18.51–18.54. Iterable and IterableMut (§10.4): for over a container calls iter(), or iter_mut() for a &mut binding; List and HashMap implement both, and C: Iterable is a bound generic code can loop over. A union owns its active payload (§7.5): variants may hold resources, dropped with the union or when another variant is assigned; a union is copyable only when every variant is. T converts to T? wherever a T? is expected (§9.1) — let, arguments, assignment, and if/switch arms joined with a T? or none — not only at return. Enums implement Eq, Hash and Ord without an impl (§7.4). Decided now, implemented in the order: enums, T?, unions, iteration.
r46 2026-09-13 assert is an ordinary prelude function, assert(cond: bool, msg: str = "", loc: SourceLocation = #location), no longer a builtin; the message is optional. panic takes a location as its last argument (§9.8): panic("missing {}", what, loc) reports loc instead of its own line, which is how assert — and any helper taking loc = #location — names its caller’s line.
r45 2026-09-13 HashMap<K: Hash + Eq, V> and Hash in the prelude (§14.2). insert adds or replaces and drops the old value; replace returns it as a V?; get/get_mut return ^V/^mut V, null when absent (§9.1); remove returns a V?; entries are visited by slot. Hash (fn hash(&self) -> u64) and Eq are built in for integers, bool, char and str — str gains Eq — and prelude trait methods can be called on those types directly: n.hash(), s.eq(&t), a.cmp(&b). Floats are not Hash. hash_combine(seed, h) combines hashes for a struct’s impl Hash. & of a value that is not a place (&7, &make()) refers to a temporary living to the end of the block, and a literal there takes its type from the reference’s context.
r44 2026-09-13 Evaluation order covers every operand, not only arguments (§7.2, 18.49). Both sides of a binary operator, a container and its index, slice and range bounds, and tuple and array elements are evaluated left to right; a struct or union literal evaluates its fields in the order they are written, not the order declared. Assignment evaluates the value first, then the place — xs[next()] = compute() runs compute() before next() — for = and compound assignment alike. Why: read_u8(r) << 8 | read_u8(r) decoded bytes backwards on some compilers, and a rule that stopped at arguments would have left that to C++.
r43 2026-09-13 The prelude’s growable array is List<T>, renamed from Buffer<T> (§14, §14.2). Same type, same methods; only the name changes. “Buffer” reads as raw bytes or a fixed block, which a growable list of elements is not, and List<T> is what a C# or Unity programmer already calls it. A program that declares its own List shadows the prelude’s, as before. The examples that teach how to write a container (§0.1, §6, §8) keep a user-defined Buffer<T>.
r42 2026-09-13 Buffer<T> and String in the prelude (§14.2), and what they needed. Slicing with a range: xs[a..b], xs[a..], xs[..b] — checked on arrays and slices, unsafe on a pointer (§5.3). str has .len and .ptr; s as []u8 views its bytes; str::from_utf8(bytes) is a str? that checks them, and bytes as str and p as cstr are the unchecked, unsafe conversions. IndexMut, so b[i] = v writes through a user type (§11.2). move_out(dst), the third placement primitive (§14.3).
r41 2026-09-13 Arguments are evaluated left to right (§7.2): f(a(), b) runs a() before it reads b, whatever C++ does. A pointer converts to usize with as, and a usize to a pointer inside unsafe (§10.6) — what an allocator needs to align and to key a table by address. Associated constants (§7.3): const NAME: T = value in a type, named Type::NAME, a constant or a static’s address, evaluated where named. The standard allocators (§14): the Allocator trait on &mut self with a trailing loc: SourceLocation = #location; Allocators::Global (&mut Allocator, the C heap) and Allocators::Temp (&mut TempArena, reset() and used()); tracking under --track-memory. A trait’s default arguments apply to calls through a trait object and to its implementations’ methods that do not repeat them.
r40 2026-09-13 Compile-time conditionals (§12.10, 18.48). #if FLAG { … } #else #if OTHER { … } #else { … } around declarations, a type’s members, or statements, over registered flags joined by !, &&, ||. Built in: DEBUG, RELEASE, SHIPPING, OS_WINDOWS, OS_LINUX, OS_MACOS. A package declares its own under [flags] in dazzle.toml, reads a dependency’s as engine::PROFILE, and turns a dependency’s on with flags = [...] on the dependency; dz … --flag NAME and --track-memory set them for one build. An unregistered flag is an error. The inactive branch is parsed but not checked. #location is a SourceLocation, and as a default argument it is the caller’s location (§12.7).
r39 2026-09-13 static (§7.7, 18.46), and the allocator decisions M12 builds on (§14, 18.47). static is a keyword: static NAME: T = value declares one variable for the whole program, #[thread_local] one per thread. Its type is written, its initialiser is made of constants (DZ0553), reading it is safe, and changing it — assigning, &mut, a &mut self method, a &mut binding — needs unsafe. Nothing is moved or copied out of one. Associated functions (§7.3): a fn in a type with no receiver is called through the type, Quat::identity(), and never on a value; the type’s fields are not in scope in it. For allocators: the Allocator trait’s methods take &mut self and end in loc: SourceLocation = #location; the standard ones are Allocators::Global and Allocators::Temp; memory tracking is opt-in.
r38 2026-09-13 A union variant can be an existing struct (§7.5, 18.45). A bare variant name now always names a struct in scope and carries a value of it: union ButtonState { DefaultState TransitionState } is built with ButtonState::DefaultState{ … } or .DefaultState{} exactly as a DefaultState is, and case DefaultState(d) binds a DefaultState, so d.update() calls its method. A variant with no payload must now be written with braces, Point {} — a bare Point with no struct of that name is DZ0551, as is one naming an enum, a union or a generic struct. Shape::Point without braces still builds it. Also: return .Variant{} from a -> T? function infers from T, as returning a plain T there already did (§9.1); and a base initialiser’s arguments may not return (DZ0552) — the base is built before the constructor begins (§7.3).
r37 2026-09-13 How for and case bindings hold what they name (§10.3, §10.4, 18.42–18.44). for e in xs copies each element and e cannot be changed; for &e in xs refers to it; for &mut e in xs refers to it and writes into xs, which must be writable. The forms for e in &xs and for e in &mut xs are gone. A move-only element is taken with &e, or consumed with for e in move(xs). Several names take a tuple element apart — for id, &btn, &mut state in query — and the pass count moves to for i, e in xs.indexed(), so for i, e in xs no longer binds an index. case bindings follow the same rules: case Circle(c) is a copy, &c a read-only reference, and &mut c changes the payload, on a writable value only. Assigning to a &mut T writes what it refers to. A generic union’s variant infers its type arguments from its payload: Choice::One{ value: 4 } is a Choice<i32>.
r36 2026-09-13 switch on an integer, char, bool or str (§10.3). Labels are constants — literals, consts, and arithmetic on them — of the subject’s type: case 1, 2, case "quit", case MAX + 1. There is no exhaustiveness requirement, and a float cannot be a subject. A bare name in a label is a variant on an enum or union and a constant otherwise. Two related rules now stated: the same variant or value in two labels is an error, since the later one can never match; and break and continue inside an arm act on the enclosing loop, since switch has no fallthrough to break out of — the compiler had been leaving only the switch, which was a bug.
r35 2026-09-13 The built program is in build/bin/, not build/ (§16.1). build/ now holds only directories dz names itself — bin, cpp, obj, and the roots dz test and dz bench create — because the two could collide: a module named bench produced the file build/bench, which is exactly the directory dz bench wants, and the build failed with “Not a directory” on every platform whose executables have no extension. dz run is unaffected; dz build prints the path it wrote.
r34 2026-09-12 flags in a library’s recipe (§15.1): raw flags passed to the compiler for that library’s sources, after the ones dz chooses, so the last one wins. Needed because some libraries require something no defines or include_dirs can say — raylib’s rglfw.c includes GLFW’s Cocoa backend, so on macOS it must be compiled as Objective-C despite its .c name, and flags = ["-x", "objective-c"] in a target.macos section is how a recipe says that. It applies to a vendored library only, never to Dazzle’s own generated code: invariant 7 fixes the flags that must compile warning-clean, and a manifest does not get to weaken them.
r33 2026-09-12 Which C++ compiler builds a project is configurable (§16.5). cxx = "g++" in [build], or in [build.target.<os>] for one platform, says what a project is known to build with; --cxx <compiler> on run, build, cpp, test and bench overrides it for one command, and DZ_CXX sits between them. With none of them dz still picks the first of clang++, g++, c++ it finds. A compiler named explicitly is not probed first: if it is not there the build says so, rather than quietly using a different one.
r32 2026-09-12 frameworks in a library’s recipe (§15.1). A macOS system library is a framework, and link_libs becomes -l<name>, which does not find one — so frameworks = ["Cocoa", "IOKit"] in a [cpp.libs.<name>.target.macos] section is linked as -framework <name>, and ignored on every other platform. Without it no recipe for a windowing library could link on macOS, and the failure named undefined Objective-C symbols rather than anything about a framework.
r31 2026-09-12 Tests and benchmarks are language features (§19 M14, M15). #[test] fn name() { … } is a test: it passes by returning and fails by panicking. #[bench] fn name() { … } is a benchmark: the runner times its body over as many iterations as it takes to measure. Both take no arguments, return nothing, are not fallible, are not generic and have a body — anything else is DZ0546 — and both are written beside the code they cover. A normal build contains neither: dz test and dz bench each build a binary of their own, so a shipped program never carries them. New builtin blackbox(v), which returns v as a value the optimiser knows nothing about. It exists because a benchmark has two ways to measure nothing — its result is never read, so the work computing it may be deleted; its input is usually a literal, so the answer may be computed at compile time — and passing the value through blackbox stops both. It is the only new name a program can collide with.
r30 2026-09-12 Struct inheritance (§7.3, §4, 18.32). struct Button : UiElement inherits from one struct. The base’s fields come first, so &Derived → &Base and ^Derived → ^Base are implicit and free; a by-value Derived where a Base is wanted is an error, because copying one would drop what the derived type adds. A base’s fields and methods are the derived type’s, in scope unqualified inside its methods, and neither may be redeclared. A base with a constructor is constructed through it — Button(r: Rect, text: str) : UiElement(r) { … } — and, as part of the same rule, a type with a constructor is no longer built with a struct literal: the constructor is what establishes its invariant. Dispatch is static unless written: virtual fn opts a method into runtime dispatch, override fn replaces one and must match its signature, both checked in both directions, and a body-less virtual fn makes the struct abstract — no value of it exists, only a & or a ^ to one. Base::method(self) calls the base’s version without dispatch, so an override can build on what it replaces; drop becomes virtual as soon as anything in the chain has a virtual method. protected is a fourth visibility (§4), opening a member to the structs that inherit it and to nothing else — a different axis from the other three, which is why it is not a pub(…) form, and it is refused at module scope. Implementations are inherited: a Button is Inspect if UiElement is, and a more specific impl Inspect for Button replaces the inherited one, statically. virtual, override and protected are new keywords and are no longer usable as identifiers.
r29 2026-09-12 header in a library’s recipe, for a single-header library (§15.1). header = "RGFW.h" replaces sources: dz writes and compiles the one translation unit that includes it with the recipe’s defines in force, so RGFW, stb and their kind need no hand-written one-line .c beside the header. Forgetting that file used to produce an unresolved symbol at link time, which named nothing that was actually missing.
r28 2026-09-12 C++ references import: const T& is a &T and T& a &mut T, so a C++ API that takes a reference — which is most of them — is callable: shapes::length_sq(&p), shapes::translate(&mut p, 1, 1). An rvalue reference T&& is not imported: it is C++’s spelling of a move and §6.4 has its own (§15.2).
r27 2026-09-12 C++ imports: namespaces, classes, overloaded methods, and named template instances (§15.2, §15.3, §15.4). A namespace is a scope reached through, one segment per level — shp::shapes::geom::manhattan(a, b) — with an inline namespace adding none, as in C++. A class is a struct: its public fields are fields and its public member functions are methods, const taking &self and any other &mut self; private and protected members stay in the type as padding and cannot be named. A constructor is a constructor call, shapes::Counter(10). A destructor stays C++’s: Dazzle emits no drop for an imported type, and makes it move-only instead so it is never destroyed twice — unless the class also has a public copy constructor, which makes it an ordinary value again. Two member functions of one name are an overload set, resolved at the call site by argument type (§15.3); an overloaded free function still imports only its first declaration. A template is imported only where [cpp.instantiate] names an instantiation — "shapes::Pair<int>" = "IntPair" — which becomes an ordinary concrete type whose members have the template’s parameters bound to those arguments (§15.4). A macro whose value is not a number but which clang can type now keeps that type, so raylib’s RAYWHITE is a Color (§18.38). dazzle.toml accepts a quoted key, which [cpp.instantiate] needs.
r26 2026-09-12 C callbacks import. A function-pointer parameter, field or return — void (*)(const RGFW_event *) — is a Dazzle fn(^RGFW_event) (§5.4), so a Dazzle function can be given to a C library as a callback: rgfw::RGFW_setEventCallback(rgfw::RGFW_windowResized, on_event). A variadic one is still left out (§15.2).
r25 2026-09-12 A C struct’s array members and bit-fields import. float m[16] is a [16]f32: indexed, iterated with for, asked for .len and .ptr, and bounds-checked like any other array — but not taken as a whole value, which C cannot do either (DZ0707). unsigned on : 1 is an ordinary member to read and assign, though & on one is refused (DZ0706): C++ has no reference to a few bits inside another member (§15.2).
r24 2026-09-12 A C union imports. Its members come in as members of one piece of memory, exactly as C has them — so RGFW’s RGFW_event can be held, filled in through &mut, and read with e.type. A literal sets one member (DZ0705); it is not a Dazzle union, which is tagged (§15.2, 18.41).
r23 2026-09-12 A C type the header only declares is a handle. import cpp brings in RGFW_window, sqlite3 or FILE as an opaque type, so the functions that take or return ^T are callable exactly as in C. Holding one by value — a local, a field, a parameter, p^ — is DZ0704: nothing outside the library knows its size (§15.2, 18.40).
r22 2026-09-12 A vendored library’s defines are its own. They build its sources and are no longer given to everything that includes its header, so a single-header library’s RGFW_IMPLEMENTATION compiles the implementation once instead of in every translation unit. A module’s [cpp] defines are the shared ones, and now reach its libraries’ sources as well as its generated C++ — that is where a define both sides need belongs (§15.1).
r21 2026-09-11 A string literal is a cstr where one is wanted. show("aaa") for a cstr parameter, let c: cstr = "aaa" and "aaa" as cstr all work, cost nothing, and reach C++ as the literal. c as str measures a C string. A runtime str still cannot become a cstr: that needs an owned copy, which arrives with String (§5.3, 18.39).
r20 2026-09-11 import cpp reads C headers. import cpp "mini.h" as mini makes the header’s functions, structs, enums, typedefs and numeric macros names under mini::, called and constructed like Dazzle’s own and emitted as the C they name. A C struct’s fields default to zero; an unscoped enumerator is also an untyped integer constant, and an integer macro is one too (18.38); size_t is usize; a call into C is safe code. An import without as is named after the header’s file (§15.1, §15.2).
r19 2026-09-11 C and C++ libraries are vendored as source and built by dz. A [cpp.libs.<name>] table in dazzle.toml is a library’s recipe — sources (paths or globs), include_dirs, defines, lib_dirs, link_libs, and a root they are relative to — and [cpp.target.<os>] / [cpp.libs.<name>.target.<os>] add to it for one platform. Every manifest path is now relative to the manifest; include_dirs had been passed to the C++ compiler as written. A module’s include dirs and defines apply to the modules that depend on it (§3.1, §15.1).
r18 2026-09-11 ^T is read-only; ^mut T is writable. A pointer now says whether it may be written through, as a reference and a slice already do: p^ = v needs a ^mut T, and an owning field that is written into is own ^mut T. In exchange &T → ^T is allowed, a ^mut T passes where a ^T is wanted, or on a ^T gives a &T, and .ptr works on a read-only view, giving a ^T. alloc returns ^mut u8. This is what C’s const T* maps to (§15.2), which is why it changed. ^^T now parses as a pointer to a pointer (§5.2, §5.3, §15.2, 18.16, 18.37).
r17 2026-09-11 if let. if let m = find(...) { … } else { … } takes the value out of a T? (or a ^T, giving a &mut T) for the first block, so “if it is there do this, otherwise that, then carry on” has a spelling — or must produce a value or diverge. Not for a fallible call, which still goes through or. Also written down: x == none and x != none test an optional, and nothing else compares one (§9.4, §10.1, 18.36).
r16 2026-09-11 A reference reads as a value. Where a T is wanted, a &T or &mut T is accepted and the value copied — return m from inside for m in list.iter() returns the element itself. Only for a copyable T; a move-only one is still an error, since nothing moves out of a borrow. Where no type is wanted, a reference stays a reference (§5.2, 18.35).
r15 2026-09-11 mut fn is removed. Whether a method mutates is said once, by its receiver: fn push(&mut self, v: T) mutates, fn len(&self) reads. mut fn repeated that and could contradict it; it is now a parse error (DZ0214) whose fix is to delete mut. Trait methods follow suit — Drop is fn drop(&mut self), Iterate::advance is fn advance(&mut self) (§7.3, 18.34).
r14 2026-09-11 move always moves, whatever the type. move on a copyable value — an i32, a str — leaves the source moved-from, so a later use is an error, and resets it to its defaults at runtime, exactly as for a resource. The compiler had been treating it as a copy, which §6.4 never said. To keep using a copyable value, leave out move (§6.4, 18.33).
r13 2026-09-11 Implementations. A trait is implemented only by impl Trait for Type { … }, written in the trait’s module or the type’s (the orphan rule), so a module can add a trait to a type it did not write — an editor’s Inspect for an engine’s structs, or for f32 — while two libraries can never both implement a third’s trait for a fourth’s type (§7.6, 18.31). impl is a keyword. An implementation holds exactly the trait’s methods and chooses the trait’s associated types, each for itself, so Mat4 × Vec4 can be a Vec4 while Mat4 × f32 is a Mat4 (§8.5, 18.29); a trait implemented at several arguments is one impl per argument (§11.2). Generic implementations (impl<T: X> X for Buffer<T>) are allowed; one for every type at once is not; two that could apply to the same type are an error; a type’s own method wins over a trait’s, and Trait::method(&v) chooses between two traits. Drop and Copy are prelude traits rather than special members: impl Drop for File { mut fn drop(&mut self) … }, impl<T: Copy> Copy for Buffer<T> { mut fn copy(&mut self, other: &Self) … } — only for a struct, in its own module, for every instance, never called by name; copy and drop stop being keywords (§6.1). A struct no longer lists traits after :; struct Button : UiElement is reserved for inheritance, whose design is recorded as 18.32 and lands with the next revision (§7.3). Error.location is a &SourceLocation, read as e.location.line — the one stored reference, since it points into static storage (§9.2, 18.30).
r12 2026-09-11 Safety and generics pass, settling r11’s open questions. unsafe: a new keyword, formerly reserved. Forming, copying, storing, comparing and passing a pointer are safe; reading or writing through a ^T (p^, ., [i]), a pointer cast, construct/destroy/memcpy/free, and calling an unsafe fn are allowed only inside unsafe { … } or an unsafe fn (§5.2). It marks raw memory and is not a safety proof, and it relaxes no other rule. &T → ^T is forbidden, reversing half of 18.17: a ^T may be written through, so widening a read-only reference would launder it. &mut T → ^T stays implicit, and p or … now yields a &mut T, so a context struct’s pointer can be written through safely once checked (18.16). Indexing an array or slice is bounds-checked in every build, not just debug; xs.ptr is the unchecked way in, on a writable view only (§5.3). Copy: a prelude trait with no methods, satisfied by every copyable type without declaring it, so fn dup<T: Copy> may copy its T; a generic struct’s own copy exists only for instances whose arguments are Copy, so Buffer<f32> copies and Buffer<File> is move-only (§8.2, 18.26). A type may implement one trait at several arguments — Vec2 : Mul<f32> + Mul<Vec2> with two muls, picked by the argument’s type; the only overloading there is (§11.2, 18.27). break takes no value (§10.2, 18.28). §8.1’s max example now writes move, as r11’s rule requires, and §11.2’s example uses + between traits, which is the syntax. Opened 18.29 (an associated type per implementation) and 18.30 (Error.location is a ^T).
r11 2026-09-11 Generics pass, second half — what implementing M6 turned up. The primitives implement the operator traits: every number is Eq, Ord and the arithmetic traits with Rhs = Self, so fn largest<T: Ord> accepts an i32 (§11.2); Rem, already in the prelude, joins §11.2’s listing. An unbounded T is move-only inside its body, so returning or passing one by value is written move(x), as for any move-only type (§8.2). What a compile-time branch may test is spelled out — type queries, bool constants, and !, &&, `
r10 2026-09-10 The prelude exists. Ordering, Eq, Ord, Add/Sub/Mul/Div/Rem, Neg, Index, Iterate, Format, Formatter and Allocator are ordinary Dazzle declarations in a module dz, in scope in every file with no import, and dz::Ord names the same trait explicitly (§3.2, §18.23). A local declaration of the same name shadows the unqualified spelling. The arithmetic traits take their right-hand side by value — fn mul(&self, rhs: Rhs) -> Output — because Rhs may be a scalar and a &T is not readable as a T (§5.2, §18.25); Eq and Ord keep &Self, since comparing a move-only type must not consume it. A signed enum may have negative values: Less = -1 on an i8-backed enum was rejected, because unary minus is an operator rather than part of the literal (§7.4).
r9 2026-09-10 Array literals: [1, 2, 3] is an array of exactly the length written, taking its element type from an annotation when there is one (§5.3). No repeat form — §6.2 already zeroes let mut a: [1024]f32, and ; cannot separate one because §2.2 makes it a terminator — and no short form, since “the rest is zero” already has a spelling. A literal may be passed where a slice is wanted but not stored as one, because the array is a temporary and the slice would certainly dangle. An enum variant may be written .Selected wherever the enum is already known — annotation, argument, either side of a comparison, return, a struct-literal field, an if branch — with Sprite::Selected always available for a position with nothing to infer from (§7.4). This is the mechanism union variants already had; Sprite.Selected remains invalid, since . is the runtime operator (§3.3).
r8 2026-09-10 Generics pass, settling what M6 needs before it can start. Iterate loses next and becomes valid / get / advance (§8.5). An Option-returning next puts the end-of-iteration signal inside the item’s own value domain, so a []^Entity would stop at its first null element and an Item of T? would stop at its first none; separating “is there one” from “what is it” removes the overlap and lets Item be any type, including &T and &mut T. This reverses the decided half of 18.21. Slices carry their writability: []T is a read-only view and []mut T a writable one, standing to each other as &T and &mut T do, so fn fill(dst: []mut f32) is expressible and a read-only view has a spelling (§5.3, §5.6). A prelude exists: Error, SourceLocation, the §11.2 operator traits, Ordering, Iterate and Format are ordinary Dazzle declarations in a module dz that is in scope everywhere with no import (§3.2). for gains a written-out desugaring (§10.4). §18 gains 18.21–18.23 resolved; the four open items are unchanged.
r7 2026-09-10 Ownership pass, settling what §6 left implicit. own describes the struct, not the pointer: an own ^T value is one pointer and reading one copies a pointer, so if handle != null inside a drop is not a move; what is move-only is the struct that declares the field (§6.3). Ownership is acquired by ordinary assignment — a plain ^T stores into an own ^T field, which is what alloc returning ^u8 has to mean (§6.3). move needs a place the function owns: move(p^), move(xs[i]) and a move through a & are errors, because a move resets its source and a borrow does not confer that right (§6.4). Definite return is now a rule: a function that promises a value must produce one on every path, and an exhaustive switch counts as returning if every arm does (§6.4). Error and SourceLocation are language-provided types, in scope everywhere with no import, since or { e -> … } binds one and there is nowhere for an import to come from (§9.2).
r6 2026-09-10 or is non-associative: a() or b() or c is a syntax error rather than a fallback chain (§9.4, §11.1). The first clause has already produced a value or diverged, so a second or could only apply to something that can no longer fail. Same reasoning as non-associative comparison. A fallback that can itself fail gets a clause of its own — a() or { b() or c } — which is how “try a, else try b, else c” is written.
r5 2026-09-10 Access/modality split. ? becomes a postfix suffix on the type name (T?, ^T?, []T?) and applies to value types only — ?^T, ?&T, !&T and the rest are gone, along with the niche-optimisation table that depended on them. ! leaves the type system entirely and becomes a signature marker (!-> T), so a fallible value cannot be stored, annotated, or pointed at. with/else, try and catch are replaced by one postfix or clause governed by a single rule: produce a T or diverge. Bare or return is reserved for propagation. ^T → &T now requires an or clause, which is what makes &T’s non-null property mean anything. §18 reopens with four items.
r4 2026-09-10 All open questions closed. 18.3: &T in a struct field is banned outright (no ref struct); context structs use ^T, and §5.2 now says honestly that this is a heuristic rather than a proof. 18.6: ^Trait is a fat pointer, with a stable deterministic vtable id added for hot-reload re-resolution. §18 has no open items.
r3 2026-09-10 Decisions pass. Resolved 18.1 (f64 default), 18.2 (attributes are #[...], unified with macros under the # sigil), 18.4, 18.5 (overloading deferred, not rejected), 18.9, 18.10, 18.11, 18.12, 18.13 (machine is a user macro, not stdlib), 18.14. Added §17.6 hot-reload constraints and roadmap M13. §18 restructured into a decisions log; 18.3 and 18.6 remain open with concrete proposals (ref struct, and the fat-vs-thin trait pointer trade).
r2 2026-09-10 Review pass. :: is now the only scope separator (§3.2/3.3); turbofish removed, justified by non-associative comparison; ?T/!T are built-in type constructors — Maybe<T>/Result<T,E> deleted, one Error type, none/error() literals, niche-optimised ?^T (§9); error style resolved to Option C (with + try + catch); associated types explained properly with worked examples (§8.5); new §8.6 arguing monomorphization over C++ templates; added §0.1 one-page summary.
r1 2026-09-10 First draft. Decisions taken: Rust compiler, project-as-module, three visibility levels, &/&mut/^/own ^, definition-site generics, declarative macros, static type-table reflection, C++17 output via libclang interop. §18 lists what is still open, with §18.8 (error propagation) as the one that most needs an answer.