Skip to content

Conversation

@odersky
Copy link
Contributor

@odersky odersky commented Nov 2, 2025

Several improvements for error messages signalling level errors.

  • Level errors now come first in the message, followed by the actual type mismatch
  • More information on owners
  • Merge ExistentialSubsumes notes with IncludeFailure notes.

Also, several refactorings that simplify and unify the schemas for processing error notes.

@odersky odersky force-pushed the better-level-errors branch from eac944e to b3a1c7c Compare November 3, 2025 11:31
Copy link
Contributor

@natsukagami natsukagami left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, I just have some comments about the errors. I like the outlive messages!

Comment on lines +5 to 9
| Capability cap outlives its scope: it leaks into outer capture set 's1 which is owned by value local.
| The leakage occurred when trying to match the following types:
|
| Found: scala.util.boundary.Label[Object^'s1]
| Required: scala.util.boundary.Label[Object^]^²
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be unrelated to changes in this PR, but I think it's still not clear where cap comes up in this error.
This could be an exception here due to it being an inline-expanded definition with an inlined local variable

|
| Note that capability x$0 is not included in capture set {cap}.
|Note that capability x$0 is not included in capture set {cap}
|because cap, which is existentially bound in () =>² Unit, cannot subsume x$0 since that capability is not a SharedCapability.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the number 2 in () =>² Unit seems to be out of nowhere compared to the preceding context.

Comment on lines +4 to +13
|Capability cap outlives its scope: it leaks into outer capture set 's1 which is owned by value leak.
|The leakage occurred when trying to match the following types:
|
|Note that capability cap cannot be included in outer capture set 's6.
|Found: (lcap: Cap^'s2) ->'s3 () ->'s4 Id[Cap^'s5]^'s6
|Required: (lcap: Cap^) => () =>² Id[Cap^'s1]^'s7
|
|where: => refers to a fresh root capability created in value leak when checking argument to parameter op of method withCap
| =>² refers to a root capability associated with the result type of (lcap: Cap^): () =>² Id[Cap^'s6]^'s7
| =>² refers to a root capability associated with the result type of (lcap: Cap^): () =>² Id[Cap^'s1]^'s7
| ^ refers to the universal root capability
| cap is a root capability associated with the result type of (): Id[Cap^'s4]^'s5
| cap is a root capability associated with the result type of (): Id[Cap^'s5]^'s6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's quite confusing here that we have "cap cannot outlive 's1" (which is part of the result type), but cap is also "a root capability associated with the result type of (): Id[Cap^'s5]^'s6"

@natsukagami
Copy link
Contributor

For some comparison, I wrote a Rust almost equivalent (link to Rust Playground) of scope-extrusion.scala, we can see the error messages for comparison:

Rust error messages
error[E0521]: borrowed data escapes outside of closure
  --> src/main.rs:10:9
   |
 5 |     let mut v = file;
   |         ----- `v` declared here, outside of the closure body
...
 8 |     |x: &mut File, y: &mut File| {
   |      - `x` is a reference that is only valid in the closure body
 9 |         let g = Box::new(|| println!("{:?}", y));
10 |         v = x;
   |         ^^^^^ `x` escapes the closure body here

error[E0521]: borrowed data escapes outside of closure
  --> src/main.rs:11:9
   |
 6 |     let mut w: Box<dyn FnOnce() -> ()> = Box::new(|| ());
   |         ----- `w` declared here, outside of the closure body
 7 |     // two parameters, otherwise Rust will complain about separation
 8 |     |x: &mut File, y: &mut File| {
   |                    - `y` is a reference that is only valid in the closure body
...
11 |         w = g;
   |         ^ `y` escapes the closure body here

error[E0521]: borrowed data escapes outside of closure
  --> src/main.rs:11:9
   |
 6 |     let mut w: Box<dyn FnOnce() -> ()> = Box::new(|| ());
   |         ----- `w` declared here, outside of the closure body
...
 9 |         let g = Box::new(|| println!("{:?}", y));
   |                                              - borrow is only valid in the closure body
10 |         v = x;
11 |         w = g;
   |         ^ reference to `y` escapes the closure body here

error: lifetime may not live long enough
  --> src/main.rs:24:19
   |
24 |     with_file(|v| v);
   |                -- ^ returning this value requires that `'1` must outlive `'2`
   |                ||
   |                |return type of closure is &'2 mut File
   |                has type `&'1 mut File`

error: lifetime may not live long enough
  --> src/main.rs:28:19
   |
28 |     with_file(|v| ident(v));
   |                -- ^^^^^^^^ returning this value requires that `'1` must outlive `'2`
   |                ||
   |                |return type of closure is &'2 mut File
   |                has type `&'1 mut File`

error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
  --> src/main.rs:36:19
   |
36 |     with_file(|v| || println!("{:?}", v));
   |                   ^^                  - `v` is borrowed here
   |                   |
   |                   may outlive borrowed value `v`
   |
note: closure is returned here
  --> src/main.rs:36:19
   |
36 |     with_file(|v| || println!("{:?}", v));
   |                   ^^^^^^^^^^^^^^^^^^^^^^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
   |
36 |     with_file(|v| move || println!("{:?}", v));
   |                   ++++

error: lifetime may not live long enough
  --> src/main.rs:36:19
   |
36 |     with_file(|v| || println!("{:?}", v));
   |                -- ^^^^^^^^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
   |                ||
   |                |return type of closure `{closure@src/main.rs:36:19: 36:21}` contains a lifetime `'2`
   |                has type `&'1 mut File`
   |
help: consider adding 'move' keyword before the nested closure
   |
36 |     with_file(|v| move || println!("{:?}", v));
   |                   ++++

error: implementation of `FnOnce` is not general enough
  --> src/main.rs:26:5
   |
26 |     with_file(ident);
   |     ^^^^^^^^^^^^^^^^ implementation of `FnOnce` is not general enough
   |
   = note: `fn(&'2 mut File) -> &'2 mut File {ident::<&'2 mut File>}` must implement `FnOnce<(&'1 mut File,)>`, for any lifetime `'1`...
   = note: ...but it actually implements `FnOnce<(&'2 mut File,)>`, for some specific lifetime `'2`

error[E0308]: mismatched types
  --> src/main.rs:34:5
   |
34 |     with_file(id);
   |     ^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected mutable reference `&mut File`
              found mutable reference `&mut File`
note: the lifetime requirement is introduced here
  --> src/main.rs:15:48
   |
15 | fn with_file<T>(fun: impl FnOnce(&mut File) -> T) -> T {
   |                                                ^
Rust source code
#[derive(Debug)]
struct File {}

fn test(file: &mut File) {
    let mut v = file;
    let mut w: Box<dyn FnOnce() -> ()> = Box::new(|| ());
    // two parameters, otherwise Rust will complain about separation
    |x: &mut File, y: &mut File| {
        let g = Box::new(|| println!("{:?}", y));
        v = x;
        w = g;
    };
}

fn with_file<T>(fun: impl FnOnce(&mut File) -> T) -> T {
    let mut f = File {};
    let r = fun(&mut f);
    r
}

fn ident<T>(t: T) -> T { t }

fn main() {
    with_file(|v| v);
    
    with_file(ident);
    
    with_file(|v| ident(v));
    // ^ note that Rust currently doesn't have an explicit syntax to introduce
    // lifetime parameters for closures, so the only thing we can do here is 
    // rely on inference for the type passed to `ident`.
    
    let id: Box<dyn for<'a> Fn(&'a mut File) -> &'a mut File> = Box::new(|v| v);
    with_file(id);
    
    with_file(|v| || println!("{:?}", v));
}

@odersky
Copy link
Contributor Author

odersky commented Nov 4, 2025

@natsukagami Good points about where errors are still unclear! Can you make issues describing these problems? Then we can track them. I don't think we have an immediate fix right now.

@odersky odersky merged commit 81da39a into scala:main Nov 5, 2025
53 checks passed
@odersky odersky deleted the better-level-errors branch November 5, 2025 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants