Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Confusing debugger line numbers emitted by compiler around lambdas (change of behavior since Scala 2) #15098

Closed
ghost opened this issue May 4, 2022 · 0 comments · Fixed by #15841
Assignees
Milestone

Comments

@ghost
Copy link

ghost commented May 4, 2022

Compiler version

3.1.2 (but valid for all Scala 3 versions)

Minimized code

object main {
  def main(args: Array[String]): Unit = {
    Array(1).foreach { n =>
      val x = 123
      println(n)
    }
  }
}

Inspecting the bytecode produced by Scala 2 (matches between Scala 2.12 and Scala 2.13)

  public main([Ljava/lang/String;)V
    // parameter final  args
   L0
    GETSTATIC scala/collection/ArrayOps$.MODULE$ : Lscala/collection/ArrayOps$;
   L1
    LINENUMBER 3 L1
    GETSTATIC scala/Predef$.MODULE$ : Lscala/Predef$;
    ICONST_1
    NEWARRAY T_INT
    DUP
    ICONST_0
    ICONST_1
    IASTORE
    INVOKEVIRTUAL scala/Predef$.intArrayOps ([I)Ljava/lang/Object;
<---------- Line added by me, for emphasis, INVOKEDYNAMIC instruction is a part of the L1 label above
    INVOKEDYNAMIC apply$mcVI$sp()Lscala/runtime/java8/JFunction1$mcVI$sp; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.altMetafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      // arguments:
      (I)V, 
      // handle kind 0x6 : INVOKESTATIC
      main$.$anonfun$main$1(I)V, 
      (I)V, 
      1
    ]
    INVOKEVIRTUAL scala/collection/ArrayOps$.foreach$extension (Ljava/lang/Object;Lscala/Function1;)V
    RETURN
   L2
    LOCALVARIABLE this Lmain$; L0 L2 0
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 1
    MAXSTACK = 6
    MAXLOCALS = 2

Notice the line added by me in the output. It signifies that the INVOKEDYNAMIC instruction which runs the lambda expression is part of the L1 label, where the array is initialized. This means that the creation of the array and the INVOKEDYNAMIC instruction are part of the same label. This is important later.

Inspecting the bytecode produced by Scala 3

  public main([Ljava/lang/String;)V
    // parameter final  args
   L0
    LINENUMBER 2 L0
    LINENUMBER 3 L0
    GETSTATIC scala/Predef$.MODULE$ : Lscala/Predef$;
    ICONST_1
    NEWARRAY T_INT
    DUP
    ICONST_0
    ICONST_1
    IASTORE
    INVOKEVIRTUAL scala/Predef$.intArrayOps ([I)Ljava/lang/Object;
    ASTORE 2
    GETSTATIC scala/collection/ArrayOps$.MODULE$ : Lscala/collection/ArrayOps$;
    ALOAD 2
   L1
    LINENUMBER 5 L1
    ALOAD 0
<---------- Line added by me, for emphasis, INVOKEDYNAMIC instruction is a part of the L1 label above
    INVOKEDYNAMIC apply$mcVI$sp(Lmain$;)Lscala/runtime/java8/JFunction1$mcVI$sp; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.altMetafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      // arguments:
      (I)V, 
      // handle kind 0x7 : INVOKESPECIAL
      main$.main$$anonfun$1(I)V, 
      (I)V, 
      1
    ]
    INVOKEVIRTUAL scala/collection/ArrayOps$.foreach$extension (Ljava/lang/Object;Lscala/Function1;)V
    RETURN
   L2
    LOCALVARIABLE this Lmain$; L0 L2 0
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 1
    MAXSTACK = 5
    MAXLOCALS = 3

Notice the line added by me in the output. It signifies that the INVOKEDYNAMIC instruction which runs the lambda expression is part of the L1 label, but in this case, the array is initialized in the L0 label. Thus, the creation of the label and the INVOKEDYNAMIC instruction are parts of different labels, contrasting the Scala 2 case.

Now, this is not an inherent problem in and of itself.

The problem comes from the fact that the different Labels (L0 and L1 in the Scala 3 case) have different LINENUMBERs attached to them (2 and 3 for L0 and 5 for L1). Thus, the INVOKEDYNAMIC instruction is attached to LINENUMBER 5.

Compared to the Scala 2 case, the INVOKEDYNAMIC instruction does not have its own LINENUMBER. (Also notice that the whole bytecode of the main method does not mention LINENUMBER 5 anywhere. This will be important later.)

This distinction can clearly be seen in a debugger. I will give examples in both IntelliJ IDEA and Metals and they both have the same behavior, resulting in bad UX.

IntelliJ IDEA Debugger screenshot with Scala 2.13.8

Screen Shot 2022-05-04 at 11 04 24

Notice that the debugger is stopped on Line 5, which is in the stack frame of the $anonfun$main$1 method (the lambda body) and that the n = 1 and x = 123` local variables are visible.

Metals Debugger screenshot with Scala 2.13

Screen Shot 2022-05-04 at 11 06 44

Same exact situation in Metals, Line 5, stack frame of `$anonfun$main$1` method and `n = 1` and `x = 123` local variables visible.

IntelliJ IDEA Debugger screenshots with Scala 3.1.2

Screen Shot 2022-05-04 at 11 08 57

Notice that the debugger is stopped on Line 5, but this Line 5 is in the stack frame of the main method (stemming from the LINENUMBER 5 emitted in the bytecode above). The local variables n = 1 and x = 123 are not visible. (The red message that the local variable n cannot be found stems from this fact, but its presentation is otherwise a filtering bug in the UI and can be ignored).

Resuming the program gets us where we want to be (and used to be with Scala 2). Line 5 of main$$anonfun$1 (the lambda body) with n = 1 and x = 123 local variables visible.
Screen Shot 2022-05-04 at 11 12 00

Metals Debugger screenshots with Scala 3.1.2

Screen Shot 2022-05-04 at 11 14 20

Notice that the debugger is stopped on Line 5, but again, in the main method, with n = 1 and x = 123 again, not visible.

Screen Shot 2022-05-04 at 11 17 56

Again, resuming the program brings us to the proper breakpoint in the lambda body. Line 5 of main$$anonfun$1 with n = 1 and x = 123 local variables visible.

What if we stop on Line 4?

Line 4 is not an ambiguous line (it only exists in the lambda body bytecode) and thus the debugger stops fine on it on the first try. Both in IntelliJ and in Metals. In this case, we're correctly in the stack frame of main$$anonfun$1 and the lambda argument n = 1 is visible (the x = 123 local variable is not initialized yet, and thus not visible, this is by design).

Screen Shot 2022-05-04 at 11 20 51

Screen Shot 2022-05-04 at 11 23 12

Conclusion

Only the final line of a multi-line lambda is ambiguous, and stopping on it actually stops on 2 breakpoints, with the second one matching the user intentions more closely, resulting in bad UX. From a source code perspective, the final line of a lambda body should be the same as any other, and IMO, not ambiguous in the outer context.

Discussion

I'm not saying that the emitted LINENUMBER is irrelevant and can be dropped, I frankly don't have the whole picture to make a call like that. I would like to know some of the reasoning behind that decision and work with you to arrive at a solution that would benefit the end user the most, in all debuggers.

Thanks for reading and thanks in advance.

@ghost ghost added itype:bug stat:needs triage Every issue needs to have an "area" and "itype" label labels May 4, 2022
@pikinier20 pikinier20 added area:backend and removed stat:needs triage Every issue needs to have an "area" and "itype" label labels May 5, 2022
@Florian3k Florian3k assigned Florian3k and unassigned Kordyjan Aug 5, 2022
@Kordyjan Kordyjan added this to the 3.5.0 milestone May 10, 2024
WojciechMazur added a commit that referenced this issue Jul 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants