Skip to content

Next-gen builder macro Bon 2.1 release 🎉. Compilation is faster by 36% 🚀

2024-09-01

bon is a Rust crate for generating compile-time-checked builders for functions and structs.

If you don't know about bon, then see the motivational blog post and the crate overview.

New features

Improved compile times of the generated code

This release features an improved compilation time of the code generated by bon. This was tested on a real use case of the frankenstein crate (Telegram bot API client for Rust) which has ~320 structs with #[builder] annotations.

If you wonder how this was achieved, then read the section 'How did we improve compile times' with the details.

cargo build --timings showcase

NOTE

This optimization covers only the code produced by expanding the #[bon::builder] macro. The bon crate itself compiles fast because it's just a proc macro.

Better error messages

The #[builder] macro now benefits from the #[diagnostic::on_unimplemented] attribute. It now generates a readable compile error with additional context for debugging in the following cases:

  • Forgetting to set a required member.
  • Setting the same member twice (unintentional overwrite).

Let's see this in action in the following example of code:

rust
#[bon::builder]
struct Point3D {
    x: f64,
    y: f64,
    z: f64,
}

fn main() {
    // Not all members are set
    let _ = Point3D::builder()
        .x(1.0)
        .build();

    let _ = Point3D::builder()
        .x(2.0)
        .y(3.0)
        .x(4.0) // <--- Oops, `x` was set the second time instead of `z`
        .build();
}

When we compile this code, the following errors are generated (truncated the help and note noise messages):

log
error[E0277]: can't finish building yet
  --> crates/sandbox/src/main.rs:12:10
   |
12 |         .build();
   |          ^^^^^ the member `Point3DBuilder__y` was not set
{...}

error[E0277]: can't finish building yet
  --> crates/sandbox/src/main.rs:12:10
   |
12 |         .build();
   |          ^^^^^ the member `Point3DBuilder__z` was not set
{...}

error[E0277]: can't set the same member twice
  --> crates/sandbox/src/main.rs:17:10
   |
17 |         .x(4.0) // <--- Oops, `x` was set the second time instead of `z`
   |          ^ this member was already set
{...}

error[E0277]: can't finish building yet
  --> crates/sandbox/src/main.rs:18:10
   |
18 |         .build();
   |          ^^^^^ the member `Point3DBuilder__z` was not set
{...}

The previous version of bon already generated compile errors in these cases, but those errors were the default errors generated by the compiler saying that some internal trait of bon was not implemented without mentioning the names of the members and without a targeted error message.

INFO

It became possible after adopting the new design for the generated code (see optimizing compile times). This is because the previous design used where bounds on associated types, for which #[diagnostic::on_unimplemented] just didn't work.

More #[must_use]

We added #[must_use] to the build() method for struct builders. Now this produces a warning because the result of #[build] is not used:

rust
#[bon::builder]
struct Point {
    x: u32
}

Point::builder().x(1).build();

The compiler shows this message:

log
warning: unused return value of `PointBuilder::<(__X,)>::build` that must be used
 --> crates/sandbox/src/main.rs:7:5
  |
7 |     Point::builder().x(1).build();
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: building a struct without using it is likely a bug

bon now also forwards #[must_use] from the original function or method to the finishing builder's call() method:

rust
#[bon::builder]
#[must_use = "Don't forget to star your favourite crates 😺"]
fn important() -> &'static str {
    "Important! Didn't you give `bon` a ⭐ on Github?"
}

important().call();

The warning is the following:

log
warning: unused return value of `ImportantBuilder::call` that must be used
 --> crates/sandbox/src/main.rs:8:5
  |
8 |     important().call();
  |     ^^^^^^^^^^^^^^^^^^
  |
  = note: Don't forget to star your favourite crates 😺
  = note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
  |
8 |     let _ = important().call();

TIP

Btw, if you are using bon or you just like its idea and implementation consider giving it a ⭐ on Github.

Big thanks to @EdJoPaTo for implementing this (#82)!

How did we improve compile times

I was integrating bon into the crate frankenstein. This is a Telegram bot API client for Rust. It has ~320 big structs with #[builder] annotations. For example, the Message struct has 84 fields 🗿.

This project was originally using typed-builder, but one of the contributors suggested migrating to bon. I noticed this suggestion and offered my help with the migration because it helps me in identifying the usage patterns for bon, and if there are any problems with its current design, or if it has bugs or lacks features.

When I worked on this migration I noticed a slowdown in the compilation speed of frankenstein. The difference was about 5 seconds of the compile time. I got frustrated for a moment, but then I started researching. Thanks to the rustc self-profiler and cargo build --timings I quickly detected the bottleneck.

The problem was that type checking of the code generated by bon was much slower than it was for typed-builder. The main thing, that was hard to compile for rustc were the generic associated type references. Here is an example of how the code generated by bon previously looked like (simplified):

rust
// Stores the type state of the builder
trait BuilderState {
    type Member1;
    type Member2;
}

impl<Member1, Member2> BuilderState for (Member1, Member2) {
    type Member1 = Member1;
    type Member2 = Member2;
}

type InitialState = (::bon::private::Unset<String>, ::bon::private::Unset<String>);

struct Builder<State: BuilderState = InitialState> {
    member1: State::Member1,
    member2: State::Member2,
}

// Setter implementations (just one setter is shown)
impl<State: BuilderState<Member1 = ::bon::private::Unset<String>>> Builder<State> {
    fn member1(self, value: String) -> Builder<(
        ::bon::private::Set<String>,
        State::Member2
    )> {
        Builder {
            // Change the type state of `Member` to be `Set`
            member1: ::bon::private::Set(value),
            // Forward all other members unchanged
            member2: self.member2,
        }
    }
}

// ... other `impl` blocks (one impl block per member)

The associated type references to State::MemberN are hard for the compiler to resolve. It spends much more time compiling the code when you use them. Not only that but the old version of bon was generating a separate impl block for every member. This increased the load on the compiler even more.

The new version of the generated code doesn't use associated types and instead uses separate generic parameters. It also generates a single impl block for all builder methods (including setters and the final build() or call() methods).

The generated code now looks like this (simplified):

rust
type InitialState = (::bon::private::Unset, ::bon::private::Unset);

struct Builder<State = InitialState> {
    members: State
}

// Single impl block for all setters
impl<Member1, Member2> Builder<(Member1, Member2)> {
    fn member1(self, value: String) -> Builder<(
        ::bon::private::Set<String>,
        Member2
    )>
    where
        Member1: bon::private::IsUnset
    {
        // Body is irrelevant
    }

    // ...other setter methods in the same `impl` block here
}

After doing this change, the compilation time of frankenstein crate decreased by 36% returning to the level of typed-builder or even better.

This change also improved the rustdoc output a bit. Now that there is a single impl block for the builder, there is less noise in the documentation. Also, this change unlocked the possibility to improve compile error messages.

I am very satisfied with this change and I hope you find your code compiling faster if you are using bon extensively 🐱.

Summary

We are continuing to improve bon and add new features. It's still a young crate and we have big plans for it, so stay tuned!

Also, a huge thank you for 600 stars ⭐ on Github! Consider giving bon a star if you haven't already. Your support and feedback is a big motivation and together we can build a better builder 🐱!

TIP

You can leave comments for this post on the platform of your choice:

Veetaha

Veetaha

Lead developer @ elastio

Creator of bon