Skip to content

Bugs C++ lets through

Each mistake below is written twice: in C++, as a reviewer might miss it, and in Dazzle. Every output on this page was produced by the real compilers: the C++ by g++ and clang++ at -std=c++17 -O2, the Dazzle by dz. A script (scripts/capture.mjs in the site’s repository) records them, and the site’s build fails if a Dazzle message shown here stops matching what dz prints.

The first eight are undefined behaviour in C++: the program may do anything, and what it does changes with the compiler and the flags. The rest are mistakes C++ defines but does not stop. In Dazzle, each one is either a compile error or has one defined meaning, the same in every build and on every compiler.

Reading past the end of an array

C++Undefined behaviour
#include <cstdio>
int main() {
int scores[3] = {10, 20, 30};
size_t i = 5;
std::printf("%d\n", scores[i]);
}

It compiles: g++ 16 warns only with -Wall; clang 23 gives no warning, even with -Wall -Wextra.

$ ./a.out
1567081552
$ ./a.out
181667128
DazzlePanics, in every build
fn main() {
let scores: [3]i32 = [10, 20, 30]
let i: usize = 5
println!("{}", scores[i])
}
$ dz run --release .
panic: index 5 is out of bounds for length 3
  at main (src/main.dz:4:20)
exit status 134 (aborted)

An index past the end reads whatever lies beyond the array. g++ warns here only because it can see that the index is the constant 5. Indexing an array or a slice in Dazzle is checked in every build, release included, and a bad index stops the program and names the line.

Spec §5.3, bounds checks →

Reading memory nobody initialised

C++Undefined behaviour
#include <cstdio>
struct Player {
int health = 100;
int score;
};
int main() {
int count;
Player p;
count += 1;
std::printf("%d %d %d\n", count, p.health, p.score);
}

It compiles: g++ 16 and clang 23 warn only with -Wall.

$ ./a.out
1 100 0
$ ./a.out
1503104296 100 -2041975384
DazzleDefined: zero or the default
struct Player {
pub health: i32 = 100
pub score: i32
}
fn main() {
let mut count: i32
let p: Player
count += 1
println!("{} {} {}", count, p.health, p.score)
}
$ dz run --release .
1 100 0

A local int and a field without an initialiser hold whatever was in memory. Dazzle has no uninitialised memory. A value starts at its type’s zero, and a field takes its declared default, so count starts at 0 and score is 0.

Spec §6.2, default initialisation →

Using a pointer that may be null

C++Undefined behaviour
#include <cstdio>
#include <string_view>
struct Player {
std::string_view name;
int health = 100;
};
Player* find(Player (&players)[2], std::string_view name) {
for (Player& p : players) {
if (p.name == name) return &p;
}
return nullptr;
}
int main() {
Player players[2] = {{"ada"}, {"bob"}};
Player* p = find(players, "eve");
std::printf("%d\n", p->health);
}

It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.

$ ./a.out

exit status 139 (segmentation fault)
$ ./a.out

exit status 40
DazzleCompile error
struct Player {
pub name: const &str = ""
pub health: i32 = 100
}
fn find(players: &[2]Player, name: &str) -> ^Player in players {
for i in 0..2 {
if players[i].name == name {
return &players[i]
}
}
return null
}
fn main() {
let players: [2]Player = [Player{ name: "ada" }, Player{ name: "bob" }]
let p = find(&players, "eve")
println!("{}", p.health)
}
$ dz check .
error[DZ0528]: reaching through a `^T` with `.` is unsafe
   ┌─ src/main.dz:18:20
   │
18 │     println!("{}", p.health)
   │                    ^^^^^^^^ outside an `unsafe` block
   │
   = help: wrap it in `unsafe { … }` — nothing checks that the memory is still there. To use a pointer safely, turn it into a reference once with `p or …` (spec §5.2)


1 error emitted

find returns nullptr when there is no match, and nothing makes the caller check. The g++ build crashes, and the clang build prints nothing and exits with a meaningless status. In Dazzle, a ^Player cannot be read with . outside unsafe. The safe way is to turn it into a reference once, with let p = find(&players, "eve") or return, which handles the null case where it can happen.

Spec §5.2, pointers and references →

Copying a struct that owns memory

C++Undefined behaviour
#include <cstdio>
struct Buffer {
int* data;
Buffer() : data(new int[4]{}) {}
~Buffer() { delete[] data; }
};
int main() {
Buffer a;
Buffer b = a;
b.data[0] = 7;
std::printf("copied, a sees %d\n", a.data[0]);
}

It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.

$ ./a.out
copied, a sees 7
DazzleCompile error
struct Buffer {
m_data: own ^mut i32 = null
Buffer() {
m_data = unsafe { Allocators::Global.alloc(16, 4) as ^mut i32 }
}
}
impl Drop for Buffer {
fn drop(&mut self) {
Allocators::Global.free(unsafe { m_data as ^mut u8 }, 16, 4)
}
}
fn main() {
let a = Buffer()
let b = a
println!("copied")
}
$ dz check .
warning[DZ0406]: variable `b` is never used
   ┌─ src/main.dz:17:9
   │
17 │     let b = a
   │         ^ nothing reads this
   │
   = help: if that is deliberate, name it `_b`; otherwise remove it

error[DZ0403]: `Buffer` is move-only and cannot be copied
   ┌─ src/main.dz:17:13
   │
17 │     let b = a
   │             ^ this would copy it
   │
   = help: it declares a `drop`, so copying it would release the same resource twice — write `move(…)` to transfer ownership, or give `Buffer` a `copy(other: &Self)` to make it copyable (spec §6.3)


1 error emitted

C++ generates a copy constructor that copies the pointer, so a and b share one array, and both destructors free it. This is the rule of three, broken silently. Both builds show the shared memory (a sees 7), and the second delete[] is a double free. This -O2 build let it pass unnoticed; built with -O0, the same program aborts. In Dazzle, a type with a drop is move-only unless it declares how to copy, so the copy is error DZ0403.

Spec §6.3, copyability →

Signed overflow

C++Undefined behaviour
#include <climits>
#include <cstdio>
bool wraps(int x) {
return x + 1 < x;
}
int main() {
std::printf("%s\n", wraps(INT_MAX) ? "true" : "false");
}

It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.

$ ./a.out
false
DazzleDefined: it wraps
fn wraps(x: i32) -> bool {
return x + 1 < x
}
fn main() {
println!("{}", wraps(2147483647))
}
$ dz run --release .
true

Signed overflow is undefined in C++, so an optimiser may assume x + 1 < x is never true and delete the check. Both builds print false for the largest int. Dazzle integers wrap in every build, signed ones included, so the comparison means what it says.

D-0063, integer overflow →

Dividing by zero

C++Undefined behaviour
#include <cstdio>
int average(int total, int count) {
return total / count;
}
int main() {
int players = 0;
std::printf("average %d\n", average(120, players));
}

It compiles: g++ 14 and clang 19 give no warning, even with -Wall -Wextra.

$ ./a.out

exit status 132 (illegal instruction)
$ ./a.out
average 2005099624
DazzlePanics, naming the line
fn average(total: i32, count: i32) -> i32 {
return total / count
}
fn main() {
let players = 0
println!("average {}", average(120, players))
}
$ dz run --release .
panic: division by zero
  at average (src/main.dz:2:12)
exit status 134 (aborted)

Nobody has joined, so the average divides by zero. Neither compiler warns, and the two builds disagree: the g++ build stops on an illegal instruction, and the clang build prints an average that was never computed and carries on, with a different number on each run. In Dazzle, integer division by zero panics in every build and names the line, and a divisor that is a constant zero is a compile error. The rest of the arithmetic C++ leaves undefined is defined too: MIN / -1 wraps, a shift count is taken modulo the width, and a float converted to an integer saturates.

Spec §5.1, primitives →

Falling off the end of a function

C++Undefined behaviour
#include <cstdio>
int sign(int x) {
if (x > 0) return 1;
if (x < 0) return -1;
}
int main() {
std::printf("%d\n", sign(0));
}

It compiles: g++ 16 and clang 23 warn by default.

$ ./a.out

exit status 132 (illegal instruction)
$ ./a.out
-1
DazzleCompile error
fn sign(x: i32) -> i32 {
if x > 0 {
return 1
}
if x < 0 {
return -1
}
}
fn main() {
println!("{}", sign(0))
}
$ dz check .
error[DZ0405]: this function must return `i32` and does not on every path
  ┌─ src/main.dz:1:4
  │
1 │ fn sign(x: i32) -> i32 {
  │    ^^^^ some path reaches the end without a value
  │
  = help: add a `return` at the end, or give the last `if` an `else` so every branch produces one


1 error emitted

Reaching the end of a function that returns a value is undefined behaviour in C++. Both compilers warn, but the program still builds. Here the g++ build stops on an illegal instruction, and the clang build prints -1 for sign(0). Dazzle’s flow analysis requires a value on every path.

Spec §6.4, definite return →

Keeping a reference across a push_back

C++Undefined behaviour
#include <cstdio>
#include <vector>
int main() {
std::vector<int> scores;
scores.push_back(10);
int& first = scores[0]; // a reference into the vector
scores.push_back(20); // may move the array
first += 1;
std::printf("%d\n", scores[0]);
}

It compiles: g++ 14 and clang 19 give no warning, even with -Wall -Wextra.

$ ./a.out
10
DazzleCompile error
fn main() {
let mut scores = List<i32>()
scores.push(10)
let first = &mut scores[0] // a reference into the list
scores.push(20) // may move the array
first += 1
println!("{}", scores[0])
}
$ dz check .
error[DZ0413]: used after the memory it points into was killed: `scores@items`
  ┌─ src/main.dz:6:5
  │
5 │     scores.push(20)                  // may move the array
  │     --------------- this call kills `scores@items`
6 │     first += 1
  │     ^^^^^ points into `scores@items`
  │
  = help: a call, write, move or drop that can move or free memory ends every reference into it: use this before that happens, or get it again after (spec §6.7)


1 error emitted

first refers into the vector’s array, and the second push_back may move that array, so first += 1 writes into memory the vector has freed. Neither compiler warns, even with -Wall -Wextra, and both builds print 10: the increment went nowhere anyone will read it. In Dazzle, List::push says in its signature that it may move the list’s array (kills self@items), so the compiler knows the call ends first, and using it afterwards is error DZ0413, naming the call that ended it.

Spec §6.7, ties and kills clauses →

Using a value after moving it

C++Valid but unspecified
#include <cstdio>
#include <string>
#include <utility>
#include <vector>
int main() {
std::vector<std::string> names;
names.push_back("ada");
std::vector<std::string> kept = std::move(names);
std::printf("%zu %zu\n", kept.size(), names.size());
}

It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.

$ ./a.out
1 0
DazzleCompile error
fn main() {
let mut names = List<String>()
names.push(String("ada"))
let kept = move(names)
println!("{} {}", kept.len(), names.len())
}
$ dz check .
error[DZ0401]: `names` was moved and cannot be used again
  ┌─ src/main.dz:5:35
  │
4 │     let kept = move(names)
  │                ----------- moved here
5 │     println!("{} {}", kept.len(), names.len())
  │                                   ^^^^^ used here after the move
  │
  = help: a moved-from value is left at its defaults, and reading it back would almost never be what was meant (spec §6.4)


1 error emitted

After std::move, a standard container is “valid but unspecified”: the C++ program may read names and gets whatever the library left there. In Dazzle, a moved-from value is reset to its defaults, and reading it again is error DZ0401, which shows where it was moved.

Spec §6.4, move semantics →

A conversion that loses the value

C++Silently truncated
#include <cstdint>
#include <cstdio>
int main() {
int health = 300;
uint8_t stored = health;
std::printf("%d\n", stored);
}

It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.

$ ./a.out
44
DazzleCompile error
fn main() {
let health: i32 = 300
let stored: u8 = health
println!("{}", stored)
}
$ dz check .
error[DZ0501]: expected `u8`, found `i32`
  ┌─ src/main.dz:3:22
  │
3 │     let stored: u8 = health
  │                      ^^^^^^ this is `i32`
  │
  = help: Dazzle has no implicit numeric conversions, not even widening (spec §5.1) — write `... as u8`


1 error emitted

Assigning an int to a uint8_t keeps the low eight bits, so 300 becomes 44, and neither compiler warns even with -Wall -Wextra: that needs -Wconversion. Dazzle has no implicit numeric conversions, not even widening. A conversion is written as u8, where a reader can see it.

Spec §5.1, no implicit conversions →

Arguments evaluated in an unspecified order

C++Unspecified
#include <cstdio>
int say(const char* word) {
std::puts(word);
return 0;
}
int both(int a, int b) {
return a + b;
}
int main() {
both(say("first"), say("second"));
}

It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.

$ ./a.out
second
first
$ ./a.out
first
second
DazzleLeft to right
fn say(word: &str) -> i32 {
println!("{}", word)
return 0
}
fn both(a: i32, b: i32) -> i32 {
return a + b
}
fn main() {
let _ = both(say("first"), say("second"))
}
$ dz run --release .
first
second

C++ does not say which argument is evaluated first, and the two compilers disagree: the same source prints in a different order depending on which one built it. Dazzle evaluates arguments left to right, the receiver first, as they are written.

D-0049, evaluation order →

A case that falls into the next

C++Falls through
#include <cstdio>
void run(int command) {
switch (command) {
case 1: std::puts("save");
case 2: std::puts("quit"); break;
}
}
int main() {
run(1);
}

It compiles: g++ 16 warns only with -Wall; clang 23 gives no warning, even with -Wall -Wextra.

$ ./a.out
save
quit
DazzleNo fall-through, ever
fn run(command: i32) {
switch command {
case 1 { println!("save") }
case 2 { println!("quit") }
}
}
fn main() {
run(1)
}
$ dz run --release .
save

A case without a break runs on into the next one, so command 1 saves and then quits. A Dazzle switch never falls through: each arm is a block, and only its own block runs. To match several values in one arm, write case 1, 2 { … }.

Spec §10.3, switch →

A missing case

C++Compiles
#include <cstdio>
enum class Weapon { Sword, Bow, Staff };
int damage(Weapon w) {
switch (w) {
case Weapon::Sword: return 10;
case Weapon::Bow: return 6;
}
return 0;
}
int main() {
std::printf("%d\n", damage(Weapon::Staff));
}

It compiles: g++ 16 warns only with -Wall; clang 23 warns by default.

$ ./a.out
0
DazzleCompile error
enum Weapon {
Sword
Bow
Staff
}
fn damage(w: Weapon) -> i32 {
switch w {
case Sword { return 10 }
case Bow { return 6 }
}
return 0
}
fn main() {
println!("{}", damage(Weapon::Staff))
}
$ dz check .
error[DZ0513]: `switch` on `Weapon` is not exhaustive
   ┌─ src/main.dz:8:5
   │  
 8 │ ╭     switch w {
 9 │ │         case Sword { return 10 }
10 │ │         case Bow { return 6 }
11 │ │     }
   │ ╰─────^ `Staff` not covered
   │  
   = help: add the missing arms, or a `default` arm (spec §10.3)


1 error emitted

A switch on an enum that forgets a value compiles in C++, and that value runs none of the cases. A Dazzle switch on an enum or a union must cover every variant, and the error names the missing ones. A default arm says on purpose that the rest share one arm.

Spec §10.3, switch →

An error nobody looks at

C++Silently ignored
#include <cstdio>
enum class SaveError { None, DiskFull };
SaveError save(const char* name) {
if (name[0] == '\0') return SaveError::DiskFull;
return SaveError::None;
}
int main() {
save("slot1");
std::puts("saved");
}

It compiles: g++ 16 and clang 23 give no warning, even with -Wall -Wextra.

$ ./a.out
saved
DazzleCompile error
enum SaveError : u32 {
DiskFull = 1
}
fn save(name: &str) !-> void {
if name.len == 0 {
return error(SaveError::DiskFull, "no room for {}", name)
}
}
fn main() {
save("slot1")
println!("saved")
}
$ dz check .
error[DZ0510]: fallible call not consumed
   ┌─ src/main.dz:12:5
   │
12 │     save("slot1")
   │     ^^^^^^^^^^^^^ this may fail, and the failure is discarded
   │
   = help: add an `or` clause: `or return` to propagate, `or <value>` to fall back, or `or ignore` to drop it deliberately (spec §9.7)


1 error emitted

A returned error code is just a value, and C++ lets the caller drop it. [[nodiscard]] helps only where someone wrote it. A Dazzle function that can fail says so with !->, and the call has to be handled in the same statement: or return, a fallback, or an explicit or ignore.

Spec §9.7, consumption →