Skip to content

Add two build_assert! lints, build_assert_not_inlined and build_assert_can_be_const#13

Open
mqqz wants to merge 12 commits intoRust-for-Linux:trunkfrom
mqqz:build-assert-lints
Open

Add two build_assert! lints, build_assert_not_inlined and build_assert_can_be_const#13
mqqz wants to merge 12 commits intoRust-for-Linux:trunkfrom
mqqz:build-assert-lints

Conversation

@mqqz
Copy link
Copy Markdown

@mqqz mqqz commented Mar 25, 2026

Apologies for dropping a sizeable PR here with no prior heads-up; there's no expectation to review it quickly. I’m very happy to split it into smaller pieces or reorganize it in whatever way would make it easier to review.

This is my attempt at adding two (very related) lints:

warns when a build_assert! condition depends on non-static values and the surrounding function or propagated call chain is not marked #[inline(always)].

warns when a build_assert! condition is already effectively constant and should use a const assert instead of build_assert!.

The implementation ended up needing a small analysis framework due to the non-local nature of the lints. Requirements can propagate through local helpers, wrappers, function pointers, and other indirect calls.

Implementation

The implementation is built around a shared build_assert! analysis pipeline rather than two unrelated lint passes. The core idea is:

  1. identify build_assert! reliably

    collect explicit #[klint::diagnostic_item] annotations from source, if an item is still missing, fallback to existing out-of-band inference.

  2. recover the asserted condition at the source level

    • walk macro ancestry to confirm an expression came from build_assert!
    • map that back to the source callsite of the macro invocation
    • recover the asserted condition span from the expanded HIR shape
  3. classify what that condition depends on

    either Constant, Runtime or Param(FxHashset<usize>).

  4. summarize that per function

    each function body is analyzed into a FunctionSummary:

    • requirement: whether non-constant values flow into build_assert!
    • return_dependency: how the function’s return value depends on inputs
    • const_only_build_asserts: source spans of constant assertions that should trigger the const lint

    we keep recomputing summaries up to a fixed-point i.e. when no other dependencies can be inferred.

  5. propagate requirements through local call relationships

    I reused/refactored and extended the work done to generate a monomorphised use graph for infallible_allocation into mono_graph.rs needed to analyze calls.

    direct local calls:

    • if a local callee summary exists, map callee parameter dependencies onto caller arguments
    • only propagate dependencies for parameters that actually matter to the callee’s build_assert!

    return dependencies:

    • if a local helper returns a value that depends on parameters, callers retain that precision instead of collapsing to generic runtime

    indirect calls:

    • local function-pointer-like flows are tracked within a body
    • mono-level call graph information is used to resolve indirect and dyn-dispatch cases back to source callsites
  6. emit either lint (or none) accordingly :)

Tests

UI coverage was added for both lints.

tests/ui/build_assert_not_inlined.rs tests cover:

  • direct runtime-dependent assertions
  • parameter + const generic mixtures
  • helper calls
  • caller propagation
  • wrapper macros
  • local rebinding
  • function pointers
  • mixed constant/runtime function-pointer calls
  • dyn-dispatch
  • partially-constant callers
  • runtime-dependent match

tests/ui/build_assert_can_be_const tests cover:

  • direct literal const expressions
  • const generics
  • wrapper macros
  • local const-returning helpers
  • const-only match
  • comment/comma conditions
  • message-form const-only assertions

Docs were also added:

  • doc/build_assert_not_inlined.md
  • doc/build_assert_can_be_const.md
    and README entries in the "Implemented Lints" section.

Closes: #7

mqqz added 12 commits March 23, 2026 21:49
Move the mono-item forward/backward graph construction out of
`infallible_allocation` into a shared `mono_graph` module.

This keeps the graph-building logic in one place and makes it reusable
for later analyses that need monomorphized use edges with span
information.

No intended behavioral change.

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
Recognize `build_assert` as a special item, preferring explicit
`#[klint::diagnostic_item]` annotations and keeping a conservative
kernel-specific fallback for older trees.

This extends the existing diagnostic-item discovery pattern so later
lints can identify `build_assert!` semantically instead of relying on
ad-hoc matching.

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
Later build_assert lints need to identify the asserted condition
semantically, not by depending on one particular macro expansion
shape.

Add the shared helper that:
- finds `build_assert!` through diagnostic-item-backed macro ancestry
- recovers the first macro argument span from the source call site

This gives later lints a stable source-level handle on the condition
even if the macro body changes.

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
The `build_assert` analysis needs to track two things cleanly:
- what values a local expression depends on
- what requirement a function imposes on its callers
including scoped restoration of local bindings.

Keeping this data model separate makes the later analysis code more
manageable and avoids mixing state mechanics with propagation logic.

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
Add the local analysis that decides whether a build_assert condition
is effectively constant or still depends on caller-visible values.

This includes:
- expression dependency classification
- local bindings and assignments
- direct-call return dependency mapping
- per-body summary computation

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
Extends local analysis with MIR monomorphized graph to resolve
indirect calls and handle value/requirement flows through helpers,
`fn` pointers, and `dyn` dispatch.

This adds:
- indirect calls resolution using the monomorphized graph
- `dyn`-dispatch `impl` methods matching back to source
- function summary computation up to a fixed point

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
`ClosureDiag` is a generic adapter for building diagnostics inline
from a closure rather than `infallible_allocation`-specific logic.

Move it into `diagnostic` and make it `pub` so later lints can reuse
it instead of defining it locally.

No intended behavioural change.

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
UI test coverage includes:
  - `const`-only and `const`-generic cases
  - runtime-dependent parameter flow
  - local helper return-value flow
  - wrapper macros
  - function pointer calls
  - `dyn` dispatch, including same-name trait-method cases

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
Add user-facing documentation for the new `build_assert_not_inlined`
lint in `doc/build_assert_not_inlined.md`.

Including:
  - which kinds of value flow are tracked
  - where propagation stops
  - the distinction from `const`-only `build_assert` uses

Add corresponding entry in the "Implemented Lints" section of README.

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
Add a lint for `build_assert!` uses whose condition is already
effectively constant.

When the asserted condition does not depend on runtime values,
the lint suggests using a `const` assert instead of relying on
`build_assert!`.

This reuses the shared build_assert analysis and emits alongside
`build_assert_not_inlined`.

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
UI test coverage includes:
- `literals`
- `const` generics
- wrapper macros
- local `const`-only helpers

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
Add user-facing documentation for the new `build_assert_can_be_const`
lint in `doc/build_assert_can_be_const.md`.

Including:
  - const-only and const-generic `build_assert!` uses
  - wrapper-macro and local-helper cases
  - the distinction from runtime-dependent `build_assert!` uses

Add corresponding entry in the "Implemented Lints" section of README,
and cross-reference it from the `build_assert_not_inlined` docs.

Signed-off-by: Mohamad Alsadhan <mo@sdhn.cc>
@nbdd0121
Copy link
Copy Markdown
Member

I haven't looked this in detail, but I don't think (by using mono graph & HIR) this will work across crates.

Another thing is that this looks like it's handling build_assert!() specifically, while I'd like the ability to also support

if cond {
    build_error!()
}

A more complicated case (where it actually occurs in RfL) is

if !cond { diverge!() }
build_assert!(cond);

This can happen where code first handles error path, and the second part (build_assert!) call is from inlining or macro-expansion. Even if cond refers to parameters, it does not need to actually be #[inline].

Have you run through your changes on Linux codebase, and what's the result like?

@mqqz
Copy link
Copy Markdown
Author

mqqz commented Mar 29, 2026

I haven't looked this in detail, but I don't think (by using mono graph & HIR) this will work across crates.

That's right. I didn't actually consider cross-crate analysis, apologies.

Thinking about how to fix that, perhaps generating metadata/summaries for crates and switching to MIR for propagation might work. Would that be right approach? If you think it's worthwhile, I can give that a try and push updates.

Another thing is that this looks like it's handling build_assert!() specifically, while I'd like the ability to also support

if cond {
    build_error!()
}

This seems fairly straightforward to support, most of the work to analyse cond is already in-place.

A more complicated case (where it actually occurs in RfL) is

if !cond { diverge!() }
build_assert!(cond);

This seems trickier, since that needs some notion of a previously established dominating guard so the later build_assert! does not by itself imply an inline requirement. I think I can likely address the former directly, and possibly handle the latter in a limited way for obvious patterns, but a fully general solution would be a bigger step.

Have you run through your changes on Linux codebase, and what's the result like?

A lot of instances where build_assert_can_be_const is triggered and they all seem reasonable/valid on first glance. However, no build_assert_not_inlined instances.

Lint Output on Linux Codebase

error: this `build_assert!` does not depend on runtime values; prefer `const { assert!(...) }` instead
   --> rust/kernel/dma.rs:404:9
    |
404 | /         build_assert!(
405 | |             core::mem::size_of::<T>() > 0,
406 | |             "It doesn't make sense for the allocated type to be a ZST"
407 | |         );
    | |_________^
    |
note: this assertion is already effectively constant, so it does not need `build_assert!` to optimize away an error path
   --> rust/kernel/dma.rs:404:9
    |
404 | /         build_assert!(
405 | |             core::mem::size_of::<T>() > 0,
406 | |             "It doesn't make sense for the allocated type to be a ZST"
407 | |         );
    | |_________^
    = note: `-D klint::build-assert-can-be-const` implied by `-D warnings`
    = help: to override `-D warnings` add `#[allow(klint::build_assert_can_be_const)]`

error: this `build_assert!` does not depend on runtime values; prefer `const { assert!(...) }` instead
   --> rust/kernel/i2c.rs:114:9
    |
114 | /         build_assert!(
115 | |             T::ACPI_ID_TABLE.is_some() || T::OF_ID_TABLE.is_some() || T::I2C_ID_TABLE.is_some(),
116 | |             "At least one of ACPI/OF/Legacy tables must be present when registering an i2c driver"
117 | |         );
    | |_________^
    |
note: this assertion is already effectively constant, so it does not need `build_assert!` to optimize away an error path
   --> rust/kernel/i2c.rs:114:9
    |
114 | /         build_assert!(
115 | |             T::ACPI_ID_TABLE.is_some() || T::OF_ID_TABLE.is_some() || T::I2C_ID_TABLE.is_some(),
116 | |             "At least one of ACPI/OF/Legacy tables must be present when registering an i2c driver"
117 | |         );
    | |_________^

error: this `build_assert!` does not depend on runtime values; prefer `const { assert!(...) }` instead
   --> rust/kernel/list/arc.rs:254:9
    |
254 |         build_assert!(ID != ID2);
    |         ^^^^^^^^^^^^^^^^^^^^^^^^
    |
note: this assertion is already effectively constant, so it does not need `build_assert!` to optimize away an error path
   --> rust/kernel/list/arc.rs:254:9
    |
254 |         build_assert!(ID != ID2);
    |         ^^^^^^^^^^^^^^^^^^^^^^^^

error: this `build_assert!` does not depend on runtime values; prefer `const { assert!(...) }` instead
   --> rust/kernel/sync/locked_by.rs:103:9
    |
103 | /         build_assert!(
104 | |             size_of::<Lock<U, B>>() > 0,
105 | |             "The lock type cannot be a ZST because it may be impossible to distinguish instances"
106 | |         );
    | |_________^
    |
note: this assertion is already effectively constant, so it does not need `build_assert!` to optimize away an error path
   --> rust/kernel/sync/locked_by.rs:103:9
    |
103 | /         build_assert!(
104 | |             size_of::<Lock<U, B>>() > 0,
105 | |             "The lock type cannot be a ZST because it may be impossible to distinguish instances"
106 | |         );
    | |_________^

error: this `build_assert!` does not depend on runtime values; prefer `const { assert!(...) }` instead
   --> rust/kernel/sync/locked_by.rs:129:9
    |
129 | /         build_assert!(
130 | |             size_of::<U>() > 0,
131 | |             "`U` cannot be a ZST because `owner` wouldn't be unique"
132 | |         );
    | |_________^
    |
note: this assertion is already effectively constant, so it does not need `build_assert!` to optimize away an error path
   --> rust/kernel/sync/locked_by.rs:129:9
    |
129 | /         build_assert!(
130 | |             size_of::<U>() > 0,
131 | |             "`U` cannot be a ZST because `owner` wouldn't be unique"
132 | |         );
    | |_________^

error: this `build_assert!` does not depend on runtime values; prefer `const { assert!(...) }` instead
   --> rust/kernel/sync/locked_by.rs:158:9
    |
158 | /         build_assert!(
159 | |             size_of::<U>() > 0,
160 | |             "`U` cannot be a ZST because `owner` wouldn't be unique"
161 | |         );
    | |_________^
    |
note: this assertion is already effectively constant, so it does not need `build_assert!` to optimize away an error path
   --> rust/kernel/sync/locked_by.rs:158:9
    |
158 | /         build_assert!(
159 | |             size_of::<U>() > 0,
160 | |             "`U` cannot be a ZST because `owner` wouldn't be unique"
161 | |         );
    | |_________^

error: this `build_assert!` does not depend on runtime values; prefer `const { assert!(...) }` instead
   --> rust/kernel/xarray.rs:233:9
    |
233 | /         build_assert!(
234 | |             T::FOREIGN_ALIGN >= 4,
235 | |             "pointers stored in XArray must be 4-byte aligned"
236 | |         );
    | |_________^
    |
note: this assertion is already effectively constant, so it does not need `build_assert!` to optimize away an error path
   --> rust/kernel/xarray.rs:233:9
    |
233 | /         build_assert!(
234 | |             T::FOREIGN_ALIGN >= 4,
235 | |             "pointers stored in XArray must be 4-byte aligned"
236 | |         );
    | |_________^

@ojeda
Copy link
Copy Markdown
Member

ojeda commented Mar 29, 2026

Lint Output on Linux Codebase

This is nice, thanks! Please feel free to submit them (otherwise, we can submit them or create "good first issues" for them).

By the way, we may want to suggest const_assert! instead of const { assert! } -- I just applied Gary's patch: Rust-for-Linux/linux@59bc32d ("rust: add const_assert! macro").

@mqqz
Copy link
Copy Markdown
Author

mqqz commented Mar 29, 2026

@ojeda Nw I'll verify the lints and send a patch

@nbdd0121
Copy link
Copy Markdown
Member

Could you split the PR into two parts, one that adds the build_assert -> const_assert lint (I think a good name for the lint is build_assert_hierarchy (which we can augment to add lint that suggests static_assert too). That lint only requires syntactical analysis and can be complete local.

The build_assert_not_inlined lint would need some additional work, and probably a careful thoughts/discussion about what to do and how to do, and would more time/iterations.

@ojeda
Copy link
Copy Markdown
Member

ojeda commented Mar 30, 2026

By the way, we may want to suggest const_assert! instead of const { assert! } -- I just applied Gary's patch: Rust-for-Linux/linux@59bc32d ("rust: add const_assert! macro").

@ojeda Nw I'll verify the lints and send a patch

Thanks for sending it! I mentioned Klint and this PR in the maling list :)

https://lore.kernel.org/rust-for-linux/20260330020234.16893-1-mo@sdhn.cc/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Warn on build_assert calls without #[inline(always)]

3 participants