rust-lang/rust

Improve readability and customizability of type-level error messages

bionicles opened this issue · 2 comments

Code

https://github.com/bionicles/broadcastable is ready to roll.

here's the one-liner to get it going

gh repo clone bionicles/broadcastable && cd broadcastable && cargo test

here are the complete contents of that crate

Cargo.toml:

[package]
name = "broadcastable"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
typenum = "1.17.0"

[dev-dependencies]
trybuild = "1.0.85"

src/lib.rs:

use typenum::{Cmp, Equal, Greater, IsEqual, Less, B1, U1, U2, U3};

// Define the Bound trait
pub trait Bound: typenum::Unsigned + std::fmt::Debug {
    fn bound() -> usize;
}

// Implement Bound for U1, U2, U3
impl Bound for U1 {
    fn bound() -> usize {
        1
    }
}
impl Bound for U2 {
    fn bound() -> usize {
        2
    }
}
impl Bound for U3 {
    fn bound() -> usize {
        3
    }
}

/// user-friendly bounds check on tuples
pub trait Compatible {
    /// Resulting bound if compatible
    type Bound: Bound;
}

impl<N, M> Compatible for (N, M)
where
    N: Bound + Cmp<M>,
    M: Bound,
    <N as Cmp<M>>::Output: CompatibleBounds<N, M>,
{
    // the output here is the bound to use in the new result shape
    type Bound = <<N as Cmp<M>>::Output as CompatibleBounds<N, M>>::Output;
}

/// Trait to check if two bounds can broadcast and to get the resulting shape after broadcasting.
pub trait CompatibleBounds<N, M>
where
    N: Bound,
    M: Bound,
{
    /// resulting bound if compatible
    type Output: Bound;
}
// N = M, output will be N
impl<N, M> CompatibleBounds<N, M> for Equal
where
    N: Bound + Cmp<M, Output = Self>,
    M: Bound,
{
    type Output = N;
}

// N < M and N is 1, output will be M
impl<N, M> CompatibleBounds<N, M> for Less
where
    N: Bound + IsEqual<U1, Output = B1>,
    M: Bound + Cmp<N, Output = Greater>,
{
    type Output = M;
}
// N > M and M is 1, output will be N
impl<N, M> CompatibleBounds<N, M> for Greater
where
    N: Bound + Cmp<M, Output = Self>,
    M: Bound + IsEqual<U1, Output = B1>,
{
    type Output = N;
}

#[cfg(test)]
mod tests_for_nd {

    #[test]
    fn test_compile_failures() {
        // Create a new instance of TestCases
        let t = trybuild::TestCases::new();
        // Compatible should not compile with incompatible bounds
        t.compile_fail("tests/ui/compatibility_fail.rs");
    }
}
// .. omitting compile_error! test which didn't work

tests/ui/compatibility_fail.rs

use broadcastable::{Bound, Compatible};
use typenum::{U2, U3};

fn main() {
    // 2 and 3 are incompatible because they are not equal and neither is 1
    type FailedAlready = <(U2, U3) as Compatible>::Bound;
    println!("Bound: {}", FailedAlready::bound());
}

Current output

┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error[E0271]: type mismatch resolving `<UInt<UInt<UTerm, B1>, B0> as IsEqual<UInt<UTerm, B1>>>::Output == B1`
 --> tests/ui/compatibility_fail.rs:7:27
  |
7 |     println!("Bound: {}", FailedAlready::bound());
  |                           ^^^^^^^^^^^^^ expected `B0`, found `B1`
  |
  = note: required for `typenum::Less` to implement `CompatibleBounds<UInt<UInt<UTerm, B1>, B0>, UInt<UInt<UTerm, B1>, B1>>`
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈

Desired output

┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
error[E0271]: type mismatch resolving `<U2 as IsEqual<U3>>::Output == B1`
 --> tests/ui/compatibility_fail.rs:7:27
  |
7 |     println!("Bound: {}", FailedAlready::bound());
  |                           ^^^^^^^^^^^^^ expected `True`, found `False`
  |
  = note: required for `typenum::Less` to implement `CompatibleBounds<U2, U3>`
┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈

Rationale and extra context

Motivation

Type Level Rust is crucial to ensure correct programs before runtime crashes. While it is possible in many cases to achieve compile-time errors, they often involve HLists (heterogeneous lists) to make a "Russian Doll" nesting of types to recursively represent complex abstractions as types.

Unfortunately, rustc tends to verbosely log the recursive nested type (not readable concrete type) instead of the high-level abstraction (readable desired alias) ... so for example we might as humans expect to see "U3" for a type level unsigned number three, but instead we see UInt<UInt<UTerm, B1>, B1>>

you can imagine a more realistic application of HList and TypeNum:
Cons<UInt<UInt<UTerm, B1>, B1>>, Cons<UInt<UInt<UTerm, B1>, B1>>, Cons<UInt<UInt<UTerm, B1>, B1>>, Nil>>>
makes something which could be a real strength of rust (compile time correctness checking) into an unreadable mess.

I think if these kind of compile failures were easier to code and easier to read, then it would be a major improvement for the Rust 2024 edition release, because it would enable us to shift a ton of runtime errors into compile time errors, thereby amortizing (do work once, reuse result many times) computation to dramatically improve performance. The big blockers are, it's hard to implement these sorts of things, and it's hard to read the error messages when you do implement them.

Proposed Improvements:

Improved Error Message Readability: Enhance the compiler's ability to present type-level errors in a more digestible and human-readable format. This could involve better summarization of the core issue, reducing the verbosity of nested type information, or providing contextual hints.

Customizable Compile-Time Errors: Develop a mechanism that allows library authors to provide custom compile-time error messages under specific type-level conditions. This feature would enable developers to create more informative and user-friendly error messages, tailored to the specific constraints and requirements of their libraries.

Enhanced Type-Level Debugging Tools: Introduce tools or compiler features that aid in debugging complex type-level code, possibly integrating with existing IDEs or Rust toolchains.

Other cases

No response

Anything else?

No response

I think if these kind of compile failures were easier to code and easier to read, then it would be a major improvement for the Rust 2024 edition release, because it would enable us to shift a ton of runtime errors into compile time errors,

@bionicles I am strongly in favor of more to-the-point error messages in roughly the way you mentioned, but nothing you described requires a new edition to be released.

Yes, you're right @workingjubilee