Skip to content
Open
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,5 @@ If you want to check it out, you can opt into it with `-Dklint::atomic_context`.
* [`build_error` checks](doc/build_error.md)
* [Stack frame size check](doc/stack_size.md)
* [Prelude check](doc/not_using_prelude.md)
* [`build_assert` not inlined](doc/build_assert_not_inlined.md)
* [`build_assert` can be const](doc/build_assert_can_be_const.md)
87 changes: 87 additions & 0 deletions doc/build_assert_can_be_const.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<!--
SPDX-License-Identifier: MIT OR Apache-2.0
-->

# `build_assert_can_be_const`

This lint warns when a `build_assert!` condition is already effectively constant and can therefore
be written as a const assertion instead:

```rust
const {
assert!(OFFSET < N, "offset must stay in bounds");
}
```

`build_assert!` is meant for conditions that cannot be checked in a plain const context, such as
conditions depending on function arguments that need to be optimized through an inline call chain.
If the condition does not depend on runtime values, using a const assert is clearer and fails
earlier.

## Literal and const-only cases

These trigger the lint because the assertion is already constant:

```rust
fn literal_const_only() {
build_assert!(1 < LIMIT);
}
```

```rust
fn const_only_direct<const N: usize>() {
build_assert!(OFFSET < N, "offset must stay in bounds");
}
```

## Wrapper macros

Simple wrapper macros do not hide the const-only case:

```rust
macro_rules! forward_build_assert {
($cond:expr) => {
build_assert!($cond);
};
}

fn const_only_wrapper() {
forward_build_assert!(OFFSET < LIMIT);
}
```

## Local const-only helpers

The lint also tracks local helper return values:

```rust
fn helper<const N: usize>() -> usize {
N - 1
}

fn const_only_helper<const N: usize>() {
build_assert!(helper::<N>() < N);
}
```

Because the helper result still depends only on compile-time values, this should also use a const
assert instead of `build_assert!`.

## Runtime-dependent cases

These do not trigger `build_assert_can_be_const`:

```rust
fn runtime_direct(offset: usize, n: usize) {
build_assert!(offset < n);
}
```

```rust
fn runtime_param_const_generic<const N: usize>(offset: usize) {
build_assert!(offset < N);
}
```

Those cases are the domain of [`build_assert_not_inlined`](build_assert_not_inlined.md), which
checks whether the non-constant assertion still has the required `#[inline(always)]` call chain.
187 changes: 187 additions & 0 deletions doc/build_assert_not_inlined.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
<!--
SPDX-License-Identifier: MIT OR Apache-2.0
-->

# `build_assert_not_inlined`

This lint warns when a `build_assert!` condition depends on non-static values, but the function
containing that dependency is not marked `#[inline(always)]`.

`build_assert!` is only valid when the compiler can optimize away its error path. Const-only uses
do not need forced inlining, but once the condition depends on values flowing through a function
boundary, the surrounding call chain must stay inlineable.

## Const-only and const-generic cases

These do not trigger the lint because the condition is already effectively constant:

```rust
fn literal_const_only() {
build_assert!(1 < 2);
}

fn const_only_direct<const N: usize>() {
build_assert!(OFFSET < N);
}

fn const_only_wrapper() {
helper_macro!(OFFSET < LIMIT);
}
```

These cases are covered by the separate
[`build_assert_can_be_const`](build_assert_can_be_const.md) lint, which suggests replacing
`build_assert!` with `const { assert!(...) }`.

## Runtime-dependent parameter flow

This does trigger the lint:

```rust
fn runtime_direct(offset: usize, n: usize) {
build_assert!(offset < n);
}
```

The same applies when only part of the condition is dynamic:

```rust
fn runtime_param_const_generic<const N: usize>(offset: usize) {
build_assert!(offset < N);
}
```

## Local helper return-value flow

The lint tracks values through local helpers instead of treating every helper call as opaque:

```rust
fn passthrough(x: usize) -> usize {
x
}

fn runtime_helper_call<const N: usize>(offset: usize) {
build_assert!(passthrough(offset) < N);
}
```

By contrast, helpers that return only const-derived values do not trigger the lint:

```rust
fn const_helper<const N: usize>() -> usize {
N - 1
}

fn const_only_helper_call<const N: usize>() {
build_assert!(const_helper::<N>() < N);
}
```

## Wrapper macros

The lint identifies `build_assert!` through macro ancestry, so simple wrapper macros do not hide
the dependency:

```rust
macro_rules! helper_macro {
($cond:expr) => {
build_assert!($cond);
};
}

fn runtime_wrapper(offset: usize, n: usize) {
helper_macro!(offset < n);
}
```

## Function pointers

The analysis also handles function pointers when it can resolve the local target:

```rust
fn runtime_fnptr_target(offset: usize) {
runtime_direct(offset, LIMIT);
}

fn fn_pointer_entry(offset: usize) {
let f: fn(usize) = runtime_fnptr_target;
f(offset);
}
```

Const-only calls through function pointers stay quiet:

```rust
fn fn_pointer_const_entry() {
let f: fn(usize) = runtime_fnptr_target;
f(1);
}
```

## Dynamic dispatch

The lint uses monomorphized use edges to recover dyn-dispatch callsites:

```rust
trait RuntimeDispatch {
fn run(&self, offset: usize);
}

trait ConstRuntimeDispatch {
fn run(&self);
}

impl RuntimeDispatch for RuntimeChecker {
fn run(&self, offset: usize) {
runtime_direct(offset, LIMIT);
}
}

impl ConstRuntimeDispatch for ConstRuntimeChecker {
fn run(&self) {
build_assert!(OFFSET < LIMIT);
}
}

fn dyn_dispatch_entry(offset: usize) {
let checker: &dyn RuntimeDispatch = &RuntimeChecker;
checker.run(offset);
}

fn dyn_dispatch_ambiguous_names(offset: usize) {
let runtime_checker: &dyn RuntimeDispatch = &RuntimeChecker;
let const_checker: &dyn ConstRuntimeDispatch = &ConstRuntimeChecker;
const_checker.run();
runtime_checker.run(offset);
}
```

This also shows the ambiguous same-name trait-method case: a const-only `run()` method does not
hide the runtime-dependent `run(offset)` call.

## Propagation to callers

The lint is not limited to the function that directly contains `build_assert!`. If a callee's
`build_assert!` still depends on caller-provided values, the requirement propagates upward:

```rust
fn runtime_direct(offset: usize, n: usize) {
build_assert!(offset < n);
}

fn runtime_caller(offset: usize, n: usize) {
runtime_direct(offset, n);
}
```

Both functions should be `#[inline(always)]`.

If a caller passes only effectively constant values, propagation stops there:

```rust
fn runtime_entry() {
runtime_direct(1, 4);
}
```

This does not trigger the lint.
Loading