Skip to content

Commit

Permalink
Add all unchecked_* operations corresponding to -no-*-checks flags
Browse files Browse the repository at this point in the history
Comparisons: unchecked_less, unchecked_less_eq, unchecked_greater, and unchecked_greater_eq

Division by zero: unchecked_div

Dereference: unchecked_dereference

Subscript: unchecked_subscript

Also fixed a bug in the code that identified attempts to assign to a pointer
  • Loading branch information
hsutter committed Oct 9, 2024
1 parent ccf7011 commit 97ba0be
Show file tree
Hide file tree
Showing 43 changed files with 343 additions and 118 deletions.
129 changes: 129 additions & 0 deletions docs/cpp2/safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@

# Safety and unchecked code

Cpp2 aims to be safe by default, usually entirely at compile time, and when needed at run time.

When Cpp2 rejects unsafe code (e.g., signed/unsigned comparison) or ensuring safety can require run-time checks (e.g., subscripts bounds checks), you can opt out as needed in two ways:

- at a specific place in your code, using `unchecked_*` functions (these are in namespace `cpp2::`, but can be used unqualified from Cpp2 code)
- for a whole source file, using `-no-*-checks` switches

Nearly always, you should opt out at a specific place in your code where you are confident the result is okay, and if there is a run-time check you have measured that the performance difference matters such as in a hot loop.


### <a id="mixed-sign-comparison"></a> Integer mixed-sign `<`, `<=`, `>`, and `>=` (compile-time enforced)

Comparing signed and unsigned integer values directly using `<`, `<=`, `>`, or `>=` can give wrong results, and so such comparisons are rejected at compile time.

To disable this check at a specific place in your code, use the appropriate `unchecked_cmp_*` function instead of the operator notation: `unchecked_cmp_less`, `unchecked_cmp_less_eq`,`unchecked_cmp_greater`, or `unchecked_cmp_greater_eq`.

For example:

``` cpp title="Integer comparisons" hl_lines="7"
main: () = {
x: i32 = 42;
y: u32 = 43;

if x < y { } // unsafe, therefore error by default

if unchecked_cmp_less(x,y) { } // ok, explicit "trust me" opt-out
}
```

To disable these checks for the entire source file, you can use cppfront's `-no-comparison-checks` switch:

``` bash title="Disable prevention of mixed-sign integer comparisons" hl_lines="3"
cppfront myfile.cpp2 # mixed-sign int comparisons banned

cppfront myfile.cpp2 -no-comparison-checks # mixed-sign int comparisons allowed
```


### <a id="division-by-zero"></a> Division by zero (run-time checked)

Dividing integers by zero is undefined behavior, and is rejected at run time by checking the denominator is nonzero.

To disable this check at a specific place in your code, use `unchecked_div`. For example:

``` cpp title="Division by zero" hl_lines="7"
main: () = {
x := 42;
y := 0;

z := x/y; // unsafe, therefore run-time checked

w := unchecked_div(x,y) // ok, explicit "trust me" opt-out
}
```

To disable these checks for the entire source file, you can use cppfront's `-no-div-zero-checks` switch:

``` bash title="Disable prevention of division by zero" hl_lines="3"
cppfront myfile.cpp2 # division by zero checked

cppfront myfile.cpp2 -no-div-zero-checks # division by zero not checked
```


### <a id="null-dereference"></a> Null dereference (run-time checked)

Dereferencing a null pointer is undefined behavior, and is rejected at run time by checking the pointer is nonzero.

To disable this check at a specific place in your code, use `unchecked_dereference`. For example:

``` cpp title="Null dereference" hl_lines="6"
main: () = {
p: *int = cpp1_func(); // could be initialized to null

p* = 42; // unsafe, therefore run-time checked

unchecked_dereference(p) = 42; // ok, explicit "trust me" opt-out
}
```

To disable these checks for the entire source file, you can use cppfront's `-no-null-checks` switch:

``` bash title="Disable prevention of null deference" hl_lines="3"
cppfront myfile.cpp2 # null dereferences checked

cppfront myfile.cpp2 -no-null-checks # null dereferences not checked
```


### <a id="subscript-bounds"></a> Subscript bounds (run-time checked)

Accessing an out of bounds subscript is undefined behavior, and is rejected at run time by checking the subscript is nonzero. For an expression `a[b]` where

- `a` is contiguous and supports `std::size(a)`, and
- `b` is an integral value

the cppfront compiler injects a check that **`0 <= b < std::size(a)`** before the call to `a[b]`.

To disable this check at a specific place in your code, use `unchecked_subscript`. For example:

``` cpp title="Subscript bounds" hl_lines="12 13"
main: () = {
v: std::vector = ( 1, 2, 3, 4, 5 );
s: std::span = v;

idx := calc_index(s);

v[idx] += 42; // unsafe, therefore run-time checked
s[idx] += 84; // unsafe, therefore run-time checked

// manually hoist the check and do it myself
if (0 ..< v.size()).contains(idx) {
unchecked_subscript(v,idx) += 42; // ok, explicit "trust me" opt-out
unchecked_subscript(s,idx) += 84; // ok, explicit "trust me" opt-out
}
}
```

To disable these checks for the entire source file, you can use cppfront's `-no-subscript-checks` switch:

``` bash title="Disable prevention of out-of-bounds subscripts" hl_lines="3"
cppfront myfile.cpp2 # subscript bounds checked

cppfront myfile.cpp2 -no-subscript-checks # subscript bounds not checked
```

89 changes: 78 additions & 11 deletions include/cpp2util.h
Original file line number Diff line number Diff line change
Expand Up @@ -2601,7 +2601,7 @@ range(
) -> range<std::common_type_t<T, U>>;

template<typename T>
constexpr auto contains(range<T> const& r, T const& t)
constexpr auto contains(range<T> const& r, auto const& t)
-> bool
{
if (r.empty()) {
Expand Down Expand Up @@ -2711,6 +2711,64 @@ inline auto fopen( const char* filename, const char* mode ) {




//-----------------------------------------------------------------------
//
// "Unchecked" opt-outs for safety checks
//
//-----------------------------------------------------------------------
//

CPP2_FORCE_INLINE constexpr auto unchecked_cmp_less(auto&& t, auto&& u)
-> decltype(auto)
requires requires {CPP2_FORWARD(t) < CPP2_FORWARD(u);}
{
return CPP2_FORWARD(t) < CPP2_FORWARD(u);
}

CPP2_FORCE_INLINE constexpr auto unchecked_cmp_less_eq(auto&& t, auto&& u)
-> decltype(auto)
requires requires {CPP2_FORWARD(t) <= CPP2_FORWARD(u);}
{
return CPP2_FORWARD(t) <= CPP2_FORWARD(u);
}

CPP2_FORCE_INLINE constexpr auto unchecked_cmp_greater(auto&& t, auto&& u)
-> decltype(auto)
requires requires {CPP2_FORWARD(t) > CPP2_FORWARD(u);}
{
return CPP2_FORWARD(t) > CPP2_FORWARD(u);
}

CPP2_FORCE_INLINE constexpr auto unchecked_cmp_greater_eq(auto&& t, auto&& u)
-> decltype(auto)
requires requires {CPP2_FORWARD(t) >= CPP2_FORWARD(u);}
{
return CPP2_FORWARD(t) >= CPP2_FORWARD(u);
}

CPP2_FORCE_INLINE constexpr auto unchecked_div(auto&& t, auto&& u)
-> decltype(auto)
requires requires {CPP2_FORWARD(t) / CPP2_FORWARD(u);}
{
return CPP2_FORWARD(t) / CPP2_FORWARD(u);
}

CPP2_FORCE_INLINE constexpr auto unchecked_dereference(auto&& p)
-> decltype(auto)
requires requires {*CPP2_FORWARD(p);}
{
return *CPP2_FORWARD(p);
}

CPP2_FORCE_INLINE constexpr auto unchecked_subscript(auto&& a, auto&& b)
-> decltype(auto)
requires requires {CPP2_FORWARD(a)[b];}
{
return CPP2_FORWARD(a)[b];
}


namespace impl {

//-----------------------------------------------------------------------
Expand All @@ -2729,7 +2787,8 @@ CPP2_FORCE_INLINE constexpr auto cmp_mixed_signedness_check() -> void
{
static_assert(
program_violates_type_safety_guarantee<T, U>,
"comparing bool values using < <= >= > is unsafe and not allowed - are you missing parentheses?");
"comparing bool values using < <= >= > is unsafe and not allowed - are you missing parentheses?"
);
}
else if constexpr (
std::is_integral_v<T> &&
Expand All @@ -2745,20 +2804,22 @@ CPP2_FORCE_INLINE constexpr auto cmp_mixed_signedness_check() -> void
// static_assert to reject the comparison is the right way to go.
static_assert(
program_violates_type_safety_guarantee<T, U>,
"mixed signed/unsigned comparison is unsafe - prefer using .ssize() instead of .size(), consider using std::cmp_less instead, or consider explicitly casting one of the values to change signedness by using 'as' or 'cpp2::unchecked_narrow'"
"mixed signed/unsigned comparison is unsafe - prefer using .ssize() instead of .size(), consider using std::cmp_less or similar instead, or consider explicitly casting one of the values to change signedness by using 'as' or 'cpp2::unchecked_narrow'"
);
}
}


CPP2_FORCE_INLINE constexpr auto cmp_less(auto&& t, auto&& u) -> decltype(auto)
CPP2_FORCE_INLINE constexpr auto cmp_less(auto&& t, auto&& u)
-> decltype(auto)
requires requires {CPP2_FORWARD(t) < CPP2_FORWARD(u);}
{
cmp_mixed_signedness_check<CPP2_TYPEOF(t), CPP2_TYPEOF(u)>();
return CPP2_FORWARD(t) < CPP2_FORWARD(u);
}

CPP2_FORCE_INLINE constexpr auto cmp_less(auto&& t, auto&& u) -> decltype(auto)
CPP2_FORCE_INLINE constexpr auto cmp_less(auto&& t, auto&& u)
-> decltype(auto)
{
static_assert(
program_violates_type_safety_guarantee<decltype(t), decltype(u)>,
Expand All @@ -2768,14 +2829,16 @@ CPP2_FORCE_INLINE constexpr auto cmp_less(auto&& t, auto&& u) -> decltype(auto)
}


CPP2_FORCE_INLINE constexpr auto cmp_less_eq(auto&& t, auto&& u) -> decltype(auto)
CPP2_FORCE_INLINE constexpr auto cmp_less_eq(auto&& t, auto&& u)
-> decltype(auto)
requires requires {CPP2_FORWARD(t) <= CPP2_FORWARD(u);}
{
cmp_mixed_signedness_check<CPP2_TYPEOF(t), CPP2_TYPEOF(u)>();
return CPP2_FORWARD(t) <= CPP2_FORWARD(u);
}

CPP2_FORCE_INLINE constexpr auto cmp_less_eq(auto&& t, auto&& u) -> decltype(auto)
CPP2_FORCE_INLINE constexpr auto cmp_less_eq(auto&& t, auto&& u)
-> decltype(auto)
{
static_assert(
program_violates_type_safety_guarantee<decltype(t), decltype(u)>,
Expand All @@ -2785,14 +2848,16 @@ CPP2_FORCE_INLINE constexpr auto cmp_less_eq(auto&& t, auto&& u) -> decltype(aut
}


CPP2_FORCE_INLINE constexpr auto cmp_greater(auto&& t, auto&& u) -> decltype(auto)
CPP2_FORCE_INLINE constexpr auto cmp_greater(auto&& t, auto&& u)
-> decltype(auto)
requires requires {CPP2_FORWARD(t) > CPP2_FORWARD(u);}
{
cmp_mixed_signedness_check<CPP2_TYPEOF(t), CPP2_TYPEOF(u)>();
return CPP2_FORWARD(t) > CPP2_FORWARD(u);
}

CPP2_FORCE_INLINE constexpr auto cmp_greater(auto&& t, auto&& u) -> decltype(auto)
CPP2_FORCE_INLINE constexpr auto cmp_greater(auto&& t, auto&& u)
-> decltype(auto)
{
static_assert(
program_violates_type_safety_guarantee<decltype(t), decltype(u)>,
Expand All @@ -2802,14 +2867,16 @@ CPP2_FORCE_INLINE constexpr auto cmp_greater(auto&& t, auto&& u) -> decltype(aut
}


CPP2_FORCE_INLINE constexpr auto cmp_greater_eq(auto&& t, auto&& u) -> decltype(auto)
CPP2_FORCE_INLINE constexpr auto cmp_greater_eq(auto&& t, auto&& u)
-> decltype(auto)
requires requires {CPP2_FORWARD(t) >= CPP2_FORWARD(u);}
{
cmp_mixed_signedness_check<CPP2_TYPEOF(t), CPP2_TYPEOF(u)>();
return CPP2_FORWARD(t) >= CPP2_FORWARD(u);
}

CPP2_FORCE_INLINE constexpr auto cmp_greater_eq(auto&& t, auto&& u) -> decltype(auto)
CPP2_FORCE_INLINE constexpr auto cmp_greater_eq(auto&& t, auto&& u)
-> decltype(auto)
{
static_assert(
program_violates_type_safety_guarantee<decltype(t), decltype(u)>,
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ nav:
- 'Types and inheritance': cpp2/types.md
- 'Metafunctions and reflection': cpp2/metafunctions.md
- 'Namespaces': cpp2/namespaces.md
- 'Safety and "unchecked"': cpp2/safety.md
# - 'Modules': cpp2/modules.md
- 'Cppfront reference':
- 'Using Cpp1 (today''s syntax) and Cpp2 in the same source file': cppfront/mixed.md
Expand Down
2 changes: 1 addition & 1 deletion regression-tests/mixed-bounds-safety-with-assert-2.cpp2
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ main: () -> int = {
std::cout << i << "\n";
}

add_42_to_subrange: (inout rng:_, start:int, end:int)
add_42_to_subrange: (inout rng, start:int, end:int)
= {
assert<bounds_safety>( 0 <= start );
assert<bounds_safety>( end <= rng.ssize() );
Expand Down
2 changes: 1 addition & 1 deletion regression-tests/mixed-bounds-safety-with-assert.cpp2
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ main: () -> int = {
print_subrange(v, 1, 13);
}

print_subrange: (rng:_, start:int, end:int) = {
print_subrange: (rng, start:int, end:int) = {
assert<bounds_safety>( 0 <= start );
assert<bounds_safety>( end <= rng.ssize() );

Expand Down
2 changes: 1 addition & 1 deletion regression-tests/mixed-fixed-type-aliases.cpp2
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace my {
using u16 = float;
}

test: (x:_) = {
test: (x) = {
std::cout
<< std::is_floating_point_v<CPP2_TYPEOF(x)> as std::string
<< "\n";
Expand Down
2 changes: 1 addition & 1 deletion regression-tests/mixed-initialization-safety-1-error.cpp2
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ main: () -> int =

// Print! A one-expression function body...
//
print_decorated: (x:_) = std::cout << ">> [" << x << "]\n";
print_decorated: (x) = std::cout << ">> [" << x << "]\n";

// Flip a coin! Exercise <mutex> <cstdlib> <ctime> and 'as'...
//
Expand Down
2 changes: 1 addition & 1 deletion regression-tests/mixed-initialization-safety-2-error.cpp2
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ main: () -> int =

// Print! A one-expression function body...
//
print_decorated: (x:_) = std::cout << ">> [" << x << "]\n";
print_decorated: (x) = std::cout << ">> [" << x << "]\n";

// Flip a coin! Exercise <mutex> <cstdlib> <ctime> and 'as'...
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fill: (
x = value.substr(0, count);
}

print_decorated: (x:_) = {
print_decorated: (x) = {
std::cout << ">> [" << x << "]\n";
}

Expand Down
2 changes: 1 addition & 1 deletion regression-tests/mixed-initialization-safety-3.cpp2
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ fill: (
x = value.substr(0, count);
}

print_decorated: (x:_) = {
print_decorated: (x) = {
std::cout << ">> [" << x << "]\n";
}

Expand Down
2 changes: 1 addition & 1 deletion regression-tests/mixed-inspect-values.cpp2
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ main: ()->int = {
test(3.14);
}

test: (x:_) = {
test: (x) = {
forty_two := 42;
std::cout << inspect x -> std::string {
is 0 = "zero";
Expand Down
10 changes: 5 additions & 5 deletions regression-tests/mixed-intro-example-three-loops.cpp2
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
#include <span>
#include <memory>

auto print(auto const& thing) -> void {
std::cout << ">> " << thing << "\n";
auto print(auto const& x) -> void {
std::cout << ">> " << x << "\n";
}

auto decorate_and_print(auto& thing) -> void {
thing = "[" + thing + "]";
print(thing);
auto decorate_and_print(auto& x) -> void {
x = "[" + x + "]";
print(x);
}

auto main() -> int {
Expand Down
Loading

0 comments on commit 97ba0be

Please sign in to comment.