langref: embrace the term "illegal behavior"

Also standardise the terms "safety-checked" and "unchecked".
This commit is contained in:
mlugg 2025-02-01 16:47:26 +00:00
parent def7e2f20a
commit f0b331e95a
No known key found for this signature in database
GPG key ID: 3F5B7DCCBF4AF02E
3 changed files with 66 additions and 50 deletions

View file

@ -1049,12 +1049,12 @@
{#header_close#}
{#header_open|Runtime Integer Values#}
<p>
Integer literals have no size limitation, and if any undefined behavior occurs,
Integer literals have no size limitation, and if any Illegal Behavior occurs,
the compiler catches it.
</p>
<p>
However, once an integer value is no longer known at compile-time, it must have a
known size, and is vulnerable to undefined behavior.
known size, and is vulnerable to safety-checked {#link|Illegal Behavior#}.
</p>
{#code|runtime_vs_comptime.zig#}
@ -1064,7 +1064,7 @@
{#link|Division by Zero#}.
</p>
<p>
Operators such as {#syntax#}+{#endsyntax#} and {#syntax#}-{#endsyntax#} cause undefined behavior on
Operators such as {#syntax#}+{#endsyntax#} and {#syntax#}-{#endsyntax#} cause {#link|Illegal Behavior#} on
integer overflow. Alternative operators are provided for wrapping and saturating arithmetic on all targets.
{#syntax#}+%{#endsyntax#} and {#syntax#}-%{#endsyntax#} perform wrapping arithmetic
while {#syntax#}+|{#endsyntax#} and {#syntax#}-|{#endsyntax#} perform saturating arithmetic.
@ -2029,7 +2029,7 @@ or
</p>
<p>
Slices have bounds checking and are therefore protected
against this kind of undefined behavior. This is one reason
against this kind of Illegal Behavior. This is one reason
we prefer slices to pointers.
</p>
{#code|test_slice_bounds.zig#}
@ -2048,7 +2048,7 @@ or
<p>
{#link|@ptrCast#} converts a pointer's element type to another. This
creates a new pointer that can cause undetectable illegal behavior
creates a new pointer that can cause undetectable Illegal Behavior
depending on the loads and stores that pass through it. Generally, other
kinds of type conversions are preferable to
{#syntax#}@ptrCast{#endsyntax#} if possible.
@ -2164,7 +2164,7 @@ or
<p>
Sentinel-terminated slicing asserts that the element in the sentinel position of the backing data is
actually the sentinel value. If this is not the case, safety-protected {#link|Undefined Behavior#} results.
actually the sentinel value. If this is not the case, safety-checked {#link|Illegal Behavior#} results.
</p>
{#code|test_sentinel_mismatch.zig#}
@ -2425,7 +2425,7 @@ or
or use an {#link|extern union#} or a {#link|packed union#} which have
guaranteed in-memory layout.
{#link|Accessing the non-active field|Wrong Union Field Access#} is
safety-checked {#link|Undefined Behavior#}:
safety-checked {#link|Illegal Behavior#}:
</p>
{#code|test_wrong_union_access.zig#}
@ -3023,11 +3023,11 @@ or
{#syntax#}const number = parseU64("1234", 10) catch unreachable;{#endsyntax#}
<p>
Here we know for sure that "1234" will parse successfully. So we put the
{#syntax#}unreachable{#endsyntax#} value on the right hand side. {#syntax#}unreachable{#endsyntax#} generates
a panic in {#link|Debug#} and {#link|ReleaseSafe#} modes and undefined behavior in
{#link|ReleaseFast#} and {#link|ReleaseSmall#} modes. So, while we're debugging the
application, if there <em>was</em> a surprise error here, the application would crash
appropriately.
{#syntax#}unreachable{#endsyntax#} value on the right hand side.
{#syntax#}unreachable{#endsyntax#} invokes safety-checked {#link|Illegal Behavior#}, so
in {#link|Debug#} and {#link|ReleaseSafe#}, triggers a safety panic by default. So, while
we're debugging the application, if there <em>was</em> a surprise error here, the application
would crash appropriately.
</p>
<p>
You may want to take a different action for every situation. For that, we combine
@ -4034,7 +4034,7 @@ fn performFn(start_value: i32) i32 {
</p>
<p>
Luckily, we used an unsigned integer, and so when we tried to subtract 1 from 0, it triggered
undefined behavior, which is always a compile error if the compiler knows it happened.
{#link|Illegal Behavior#}, which is always a compile error if the compiler knows it happened.
But what would have happened if we used a signed integer?
</p>
{#code|fibonacci_comptime_infinite_recursion.zig#}
@ -4239,7 +4239,7 @@ pub fn print(self: *Writer, arg0: []const u8, arg1: i32) !void {
</p>
<p>
Failure to declare the full set of clobbers for a given inline assembly
expression is unchecked {#link|Undefined Behavior#}.
expression is unchecked {#link|Illegal Behavior#}.
</p>
{#header_close#}
@ -4805,7 +4805,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
</p>
<p>
Attempting to convert an integer with no corresponding value in the enum invokes
safety-checked {#link|Undefined Behavior#}.
safety-checked {#link|Illegal Behavior#}.
Note that a {#link|non-exhaustive enum|Non-exhaustive enum#} has corresponding values for all
integers in the enum's integer tag type: the {#syntax#}_{#endsyntax#} value represents all
the remaining unnamed integers in the enum's tag type.
@ -4824,7 +4824,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
</p>
<p>
Attempting to convert an integer that does not correspond to any error results in
safety-protected {#link|Undefined Behavior#}.
safety-checked {#link|Illegal Behavior#}.
</p>
{#see_also|@intFromError#}
{#header_close#}
@ -4856,7 +4856,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
<p>
Converts an error set or error union value from one error set to another error set. The return type is the
inferred result type. Attempting to convert an error which is not in the destination error
set results in safety-protected {#link|Undefined Behavior#}.
set results in safety-checked {#link|Illegal Behavior#}.
</p>
{#header_close#}
@ -4912,7 +4912,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
</p>
<p>
If {#syntax#}field_ptr{#endsyntax#} does not point to the {#syntax#}field_name{#endsyntax#} field of an instance of
the result type, and the result type has ill-defined layout, invokes unchecked {#link|Undefined Behavior#}.
the result type, and the result type has ill-defined layout, invokes unchecked {#link|Illegal Behavior#}.
</p>
{#header_close#}
@ -5029,7 +5029,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
Converts an integer to another integer while keeping the same numerical value.
The return type is the inferred result type.
Attempting to convert a number which is out of range of the destination type results in
safety-protected {#link|Undefined Behavior#}.
safety-checked {#link|Illegal Behavior#}.
</p>
{#code|test_intCast_builtin.zig#}
@ -5090,7 +5090,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
</p>
<p>
If the integer part of the floating point number cannot fit in the destination type,
it invokes safety-checked {#link|Undefined Behavior#}.
it invokes safety-checked {#link|Illegal Behavior#}.
</p>
{#see_also|@floatFromInt#}
{#header_close#}
@ -5250,7 +5250,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
<p>
The {#syntax#}ptr{#endsyntax#} argument may be any pointer type and determines the memory
address to prefetch. This function does not dereference the pointer, it is perfectly legal
to pass a pointer to invalid memory to this function and no illegal behavior will result.
to pass a pointer to invalid memory to this function and no Illegal Behavior will result.
</p>
<p>{#syntax#}PrefetchOptions{#endsyntax#} can be found with {#syntax#}@import("std").builtin.PrefetchOptions{#endsyntax#}.</p>
{#header_close#}
@ -5262,7 +5262,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
</p>
<p>
{#link|Optional Pointers#} are allowed. Casting an optional pointer which is {#link|null#}
to a non-optional pointer invokes safety-checked {#link|Undefined Behavior#}.
to a non-optional pointer invokes safety-checked {#link|Illegal Behavior#}.
</p>
<p>
{#syntax#}@ptrCast{#endsyntax#} cannot be used for:
@ -5286,7 +5286,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
</p>
<p>
If the destination pointer type does not allow address zero and {#syntax#}address{#endsyntax#}
is zero, this invokes safety-checked {#link|Undefined Behavior#}.
is zero, this invokes safety-checked {#link|Illegal Behavior#}.
</p>
{#header_close#}
@ -5361,8 +5361,8 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
<li>
{#syntax#}Optimized{#endsyntax#} - Floating point operations may do all of the following:
<ul>
<li>Assume the arguments and result are not NaN. Optimizations are required to retain defined behavior over NaNs, but the value of the result is undefined.</li>
<li>Assume the arguments and result are not +/-Inf. Optimizations are required to retain defined behavior over +/-Inf, but the value of the result is undefined.</li>
<li>Assume the arguments and result are not NaN. Optimizations are required to retain legal behavior over NaNs, but the value of the result is undefined.</li>
<li>Assume the arguments and result are not +/-Inf. Optimizations are required to retain legal behavior over +/-Inf, but the value of the result is undefined.</li>
<li>Treat the sign of a zero argument or result as insignificant.</li>
<li>Use the reciprocal of an argument rather than perform division.</li>
<li>Perform floating-point contraction (e.g. fusing a multiply followed by an addition into a fused multiply-add).</li>
@ -5401,7 +5401,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
</p>
<p>
The type of {#syntax#}shift_amt{#endsyntax#} is an unsigned integer with {#syntax#}log2(@typeInfo(T).int.bits){#endsyntax#} bits.
This is because {#syntax#}shift_amt >= @typeInfo(T).int.bits{#endsyntax#} is undefined behavior.
This is because {#syntax#}shift_amt >= @typeInfo(T).int.bits{#endsyntax#} triggers safety-checked {#link|Illegal Behavior#}.
</p>
<p>
{#syntax#}comptime_int{#endsyntax#} is modeled as an integer with an infinite number of bits,
@ -5418,7 +5418,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
</p>
<p>
The type of {#syntax#}shift_amt{#endsyntax#} is an unsigned integer with {#syntax#}log2(@typeInfo(@TypeOf(a)).int.bits){#endsyntax#} bits.
This is because {#syntax#}shift_amt >= @typeInfo(@TypeOf(a)).int.bits{#endsyntax#} is undefined behavior.
This is because {#syntax#}shift_amt >= @typeInfo(@TypeOf(a)).int.bits{#endsyntax#} triggers safety-checked {#link|Illegal Behavior#}.
</p>
{#see_also|@shlExact|@shrExact#}
{#header_close#}
@ -5431,7 +5431,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
</p>
<p>
The type of {#syntax#}shift_amt{#endsyntax#} is an unsigned integer with {#syntax#}log2(@typeInfo(T).int.bits){#endsyntax#} bits.
This is because {#syntax#}shift_amt >= @typeInfo(T).int.bits{#endsyntax#} is undefined behavior.
This is because {#syntax#}shift_amt >= @typeInfo(T).int.bits{#endsyntax#} triggers safety-checked {#link|Illegal Behavior#}.
</p>
{#see_also|@shlExact|@shlWithOverflow#}
{#header_close#}
@ -5706,7 +5706,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
{#header_open|@tagName#}
<pre>{#syntax#}@tagName(value: anytype) [:0]const u8{#endsyntax#}</pre>
<p>
Converts an enum value or union value to a string literal representing the name.</p><p>If the enum is non-exhaustive and the tag value does not map to a name, it invokes safety-checked {#link|Undefined Behavior#}.
Converts an enum value or union value to a string literal representing the name.</p><p>If the enum is non-exhaustive and the tag value does not map to a name, it invokes safety-checked {#link|Illegal Behavior#}.
</p>
{#header_close#}
@ -5943,7 +5943,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
<li>Reproducible build</li>
</ul>
{#header_close#}
{#see_also|Compile Variables|Zig Build System|Undefined Behavior#}
{#see_also|Compile Variables|Zig Build System|Illegal Behavior#}
{#header_close#}
{#header_open|Single Threaded Builds#}
@ -5958,20 +5958,36 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
</ul>
{#header_close#}
{#header_open|Undefined Behavior#}
{#header_open|Illegal Behavior#}
<p>
Zig has many instances of undefined behavior. If undefined behavior is
detected at compile-time, Zig emits a compile error and refuses to continue.
Most undefined behavior that cannot be detected at compile-time can be detected
at runtime. In these cases, Zig has safety checks. Safety checks can be disabled
on a per-block basis with {#link|@setRuntimeSafety#}. The {#link|ReleaseFast#}
and {#link|ReleaseSmall#} build modes disable all safety checks (except where overridden
by {#link|@setRuntimeSafety#}) in order to facilitate optimizations.
Many operations in Zig trigger what is known as "Illegal Behavior" (IB). If Illegal Behavior is detected at
compile-time, Zig emits a compile error and refuses to continue. Otherwise, when Illegal Behavior is not caught
at compile-time, it falls into one of two categories.
</p>
<p>
When a safety check fails, Zig crashes with a stack trace, like this:
Some Illegal Behavior is <em>safety-checked</em>: this means that the compiler will insert "safety checks"
anywhere that the Illegal Behavior may occur at runtime, to determine whether it is about to happen. If it
is, the safety check "fails", which triggers a panic.
</p>
{#code|test_undefined_behavior.zig#}
<p>
All other Illegal Behavior is <em>unchecked</em>, meaning the compiler is unable to insert safety checks for
it. If Unchecked Illegal Behavior is invoked at runtime, anything can happen: usually that will be some kind of
crash, but the optimizer is free to make Unchecked Illegal Behavior do anything, such as calling arbitrary functions
or clobbering arbitrary data. This is similar to the concept of "undefined behavior" in some other languages. Note that
Unchecked Illegal Behavior still always results in a compile error if evaluated at {#link|comptime#}, because the Zig
compiler is able to perform more sophisticated checks at compile-time than at runtime.
</p>
<p>
Most Illegal Behavior is safety-checked. However, to facilitate optimizations, safety checks are disabled by default
in the {#link|ReleaseFast#} and {#link|ReleaseSmall#} optimization modes. Safety checks can also be enabled or disabled
on a per-block basis, overriding the default for the current optimization mode, using {#link|@setRuntimeSafety#}. When
safety checks are disabled, Safety-Checked Illegal Behavior behaves like Unchecked Illegal Behavior; that is, any behavior
may result from invoking it.
</p>
<p>
When a safety check fails, Zig's default panic handler crashes with a stack trace, like this:
</p>
{#code|test_illegal_behavior.zig#}
{#header_open|Reaching Unreachable Code#}
<p>At compile-time:</p>
@ -6337,7 +6353,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
<p>
{#syntax#}var{#endsyntax#} declarations inside functions are stored in the function's stack frame. Once a function returns,
any {#link|Pointers#} to variables in the function's stack frame become invalid references, and
dereferencing them becomes unchecked {#link|Undefined Behavior#}.
dereferencing them becomes unchecked {#link|Illegal Behavior#}.
</p>
<p>
{#syntax#}var{#endsyntax#} declarations at the top level or in {#link|struct#} declarations are stored in the global
@ -6445,7 +6461,7 @@ fn cmpxchgWeakButNotAtomic(comptime T: type, ptr: *T, expected_value: T, new_val
The API documentation for functions and data structures should take great care to explain
the ownership and lifetime semantics of pointers. Ownership determines whose responsibility it
is to free the memory referenced by the pointer, and lifetime determines the point at which
the memory becomes inaccessible (lest {#link|Undefined Behavior#} occur).
the memory becomes inaccessible (lest {#link|Illegal Behavior#} occur).
</p>
{#header_close#}
@ -6733,10 +6749,10 @@ int foo(void) {
<li>Supports all the syntax of the other two pointer types ({#syntax#}*T{#endsyntax#}) and ({#syntax#}[*]T{#endsyntax#}).</li>
<li>Coerces to other pointer types, as well as {#link|Optional Pointers#}.
When a C pointer is coerced to a non-optional pointer, safety-checked
{#link|Undefined Behavior#} occurs if the address is 0.
{#link|Illegal Behavior#} occurs if the address is 0.
</li>
<li>Allows address 0. On non-freestanding targets, dereferencing address 0 is safety-checked
{#link|Undefined Behavior#}. Optional C pointers introduce another bit to keep track of
{#link|Illegal Behavior#}. Optional C pointers introduce another bit to keep track of
null, just like {#syntax#}?usize{#endsyntax#}. Note that creating an optional C pointer
is unnecessary as one can use normal {#link|Optional Pointers#}.
</li>
@ -7051,8 +7067,8 @@ fn readU32Be() u32 {}
<ul>
<li>Omit any information that is redundant based on the name of the thing being documented.</li>
<li>Duplicating information onto multiple similar functions is encouraged because it helps IDEs and other tools provide better help text.</li>
<li>Use the word <strong>assume</strong> to indicate invariants that cause {#link|Undefined Behavior#} when violated.</li>
<li>Use the word <strong>assert</strong> to indicate invariants that cause <em>safety-checked</em> {#link|Undefined Behavior#} when violated.</li>
<li>Use the word <strong>assume</strong> to indicate invariants that cause <em>unchecked</em> {#link|Illegal Behavior#} when violated.</li>
<li>Use the word <strong>assert</strong> to indicate invariants that cause <em>safety-checked</em> {#link|Illegal Behavior#} when violated.</li>
</ul>
{#header_close#}
{#header_close#}
@ -7448,8 +7464,8 @@ fn readU32Be() u32 {}
In particular, inside a {#syntax#}nosuspend{#endsyntax#} scope:
<ul>
<li>Using the {#syntax#}suspend{#endsyntax#} keyword results in a compile error.</li>
<li>Using {#syntax#}await{#endsyntax#} on a function frame which hasn't completed yet results in safety-checked {#link|Undefined Behavior#}.</li>
<li>Calling an async function may result in safety-checked {#link|Undefined Behavior#}, because it's equivalent to <code>await async some_async_fn()</code>, which contains an {#syntax#}await{#endsyntax#}.</li>
<li>Using {#syntax#}await{#endsyntax#} on a function frame which hasn't completed yet results in safety-checked {#link|Illegal Behavior#}.</li>
<li>Calling an async function may result in safety-checked {#link|Illegal Behavior#}, because it's equivalent to <code>await async some_async_fn()</code>, which contains an {#syntax#}await{#endsyntax#}.</li>
</ul>
Code inside a {#syntax#}nosuspend{#endsyntax#} scope does not cause the enclosing function to become an {#link|async function|Async Functions#}.
<ul>

View file

@ -2,7 +2,7 @@ test "@setRuntimeSafety" {
// The builtin applies to the scope that it is called in. So here, integer overflow
// will not be caught in ReleaseFast and ReleaseSmall modes:
// var x: u8 = 255;
// x += 1; // undefined behavior in ReleaseFast/ReleaseSmall modes.
// x += 1; // Unchecked Illegal Behavior in ReleaseFast/ReleaseSmall modes.
{
// However this block has safety enabled, so safety checks happen here,
// even in ReleaseFast and ReleaseSmall modes.
@ -15,7 +15,7 @@ test "@setRuntimeSafety" {
// would not be caught in any build mode.
@setRuntimeSafety(false);
// var x: u8 = 255;
// x += 1; // undefined behavior in all build modes.
// x += 1; // Unchecked Illegal Behavior in all build modes.
}
}
}