A Scala stack trace is often hard to interpret because it contains compiler-generated methods with encoded and mangled names, and erased types.
The Binary Decoder
In the latest version of the Scala Debug Adapter (the Scala debugger used by Metals) we developed a binary decoder based on TASTy Query. The binary decoder can take any binary method from a Scala 3 class file and decode it to a tagged TASTy symbol, or TASTy tree (if there is no corresponding symbol, e.g. lifted by-name argument). The tag indicates the nature of the binary symbol: a declared val or def, an accessor, a bridge, a forwarder, a lifted by-name argument, a default argument, an adapted method etc.
The TASTy symbol or tree is fully typed and it contains source names. Thus the debugger can format the method of a stack frame to a typed symbol, with names and types as they appear in the source, and a qualifier if the method is generated. The list of qualifiers are: <init>
, <lazy init>
, <mixin forwarder>
, <static forwarder>
, <outer>
, <super arg>
, <try>
, <anon fun>
, <by-name arg>
, <bridge>
, <setter>
, <super>
, <specialized>
, <adapted>
and <inline accessor>
.
Additionally the debugger can skip and hide the methods that are pure artifacts of the compiler, like the forwarders, bridges and accessors, as they don’t contain any user-written code.
Examples
Decoded names
package example
object && :
private def && = "&&" // breakpoint here
object || :
def || = "||" + &&
@main def mangling: Unit =
println(&&.||.||)
Binary stack trace:
$amp$amp$.example$$amp$amp$$$$amp$amp(): String
$amp$amp$$bar$bar$.$bar$bar(): String
Main$package$.mangling(): void
Decoded stack trace:
&&.&&: String
&&.||.||: String
example.mangling: Unit
Erasure, Anonymous Functions and Bridges
package example
def combine(xs: Seq[String], ys: Seq[String]): Seq[String] =
for
x <- xs
y <- ys
yield
x + y // breakpoint here
@main def combine: Unit =
combine(Seq("a", "b"), Seq("1", "2"))
Binary stack trace:
Main$package$.combine$$anonfun$1(Seq,String): IterableOnce
List.flatMap(Function1): List
List.flatMap(Function1): Object
Main$package$.combine(Seq,Seq): Seq
Main$package$.combine()
Decoded stack trace:
example.combine.<anon fun>(x: String): IterableOnce[String]
List.flatMap[B](f: A => IterableOnce[B]): List[B]
List.flatMap.<bridge>[B](f: A => IterableOnce[B]): List[B]
example.combine(xs: Seq[String], ys: Seq[String]): Seq[String]
example.combine: Unit
Filtered stack trace (we can hide the bridge method):
example.combine.<anon fun>(x: String): IterableOnce[String]
List.flatMap[B](f: A => IterableOnce[B]): List[B]
example.combine(xs: Seq[String], ys: Seq[String]): Seq[String]
example.combine: Unit
Forwarders
package example
trait Show:
def show: String = toString // breakpoint here
case class Position(line: Int, column: Int) extends Show
@main def showPos: Unit =
val pos = Position(5, 12)
println(pos.show)
Binary stack trace:
Show.show(): String
Show.show$(Show): String
Position.show(): String
Main$package$.showPos(): void
Decoded stack trace:
Show.show: String
Show.show.<static forwarder>: String
Position.show.<mixin forwarder>: String
example.showPos: Unit
Filtered stack trace:
Show.show: String
example.showPos: Unit
A Real-world stack trace
Binary stack trace:
Showable.show(Contexts$Context): String
Showable.show$(Showable,Contexts$Context): String
Types$Type.show(Contexts$Context): String
Decorators$.tryToShow(Object,Contexts$Context): String
Formatting$StringFormatter.showArg(Object,Contexts$Context): String
Formatting$StringFormatter.treatArg(Object,String,Contexts$Context): Tuple2
Formatting$StringFormatter.$anonfun$3(Contexts$Context,Object,String): Tuple2
Formatting$StringFormatter$$Lambda$652/0x0000000800f9c1e8.apply(Object,Object): Object
LazyZip2$$anon$1$$anon$2.next(): Object
Growable.addAll(IterableOnce): Growable
Growable.addAll$(Growable,IterableOnce): Growable
ArrayBuilder.addAll(IterableOnce): ArrayBuilder
IterableOnceOps.toArray(ClassTag): Object
IterableOnceOps.toArray$(IterableOnceOps,ClassTag): Object
AbstractIterable.toArray(ClassTag): Object
Array$.from(IterableOnce,ClassTag): Object
ArraySeq$.from(IterableOnce,ClassTag): ArraySeq
ArraySeq$.from(IterableOnce,Object): Object
ClassTagIterableFactory$AnyIterableDelegate.from(IterableOnce): Object
BuildFromLowPriority2$$anon$11.fromSpecific(Iterable,IterableOnce): Iterable
...
Decoded and filtered stack trace:
Showable.show(using Contexts.Context): String
Decorators.tryToShow[T](x: T)(using Contexts.Context): String
Formatting.StringFormatter.showArg(arg: Any)(using Contexts.Context): String
Formatting.StringFormatter.treatArg(arg: Shown, suffix: String)(using Contexts.Context): (String, String)
Formatting.StringFormatter.assemble.<anon fun>(Shown, String): (String, String)
LazyZip2$$anon$1$$anon$2.next(): Object
Growable.addAll(xs: IterableOnce[A]): Growable.this.type
ArrayBuilder.addAll(xs: IterableOnce[T]): ArrayBuilder.this.type
IterableOnceOps.toArray[B](implicit ClassTag[B]): Array[B]
AbstractIterable.toArray(ClassTag): Object
Array.from[A](it: IterableOnce[A])(implicit ClassTag[A]): Array[A]
ArraySeq.from[A](it: IterableOnce[A])(implicit tag: ClassTag[A]): ArraySeq[A]
ClassTagIterableFactory.AnyIterableDelegate.from[A](it: IterableOnce[A]): CC[A]
BuildFromLowPriority2$$anon$11.fromSpecific(Iterable,IterableOnce): Iterable
...
Note that some methods cannot be decoded because they come from Scala 2.
Parametrization of the Debugger
The Step Filters
In the next version of the debugger we will have 3 step-filter parameters:
skipForwardersAndAccessors
: to skip the forwarders and accessorsskipClassLoading
: to skip the implicit class loading, on anew
or the initialization of anobject
skipScalaRuntime
: to skip all the classes in thescala.runtime
package
These 3 step filters are enabled by default and they can be disabled independently.
The Scala Decoder
We will add a enableScalaDecoder
(default is true) to enable or disable the Scala decoder.
If the decoder is disabled, the debugger will fall back to printing the binary methods of the stack traces.
Stack Trace Formatting
The Debug Adapter Protocol defines the StackFrameFormat object to describe how to display a stack frame: with parameter names, parameter types etc.
It is not yet implemented in the Scala Debug Adapter
Collapsing
Instead of completely hiding the bridges, forwarders and accessors we can use the presentationHint
field in StackFrame to deemphasize a frame.
On VSCode, deemphasized frames appear grayed out. Also VSCode collapses consecutive deemphasized frames to shorten the stack trace.
Limitations of the Decoder
-
The binary decoder cannot decode private Scala 2 symbols.
-
It cannot decode methods that are generated by the expansion of a macro.
-
It can fail decoding some symbols because of ambiguity: if they have the same source names, binary signatures and debug lines
def foo(xs: Seq[String]): Seq[String] =
// the decoder cannot disambiguate the two anonfuns
xs.filter(x => x.size < 2) ++ xs.filter(x => x.size >= 3)
Next Steps
The binary decoder is part of the latest release of the Scala Debug Adapter. The features described above will be available in the upcoming version of Metals.
Here are the next steps of this project:
-
Publishing the
scala3-binary-decoder
as an independent library that can be used in other tools of the ecosystem: Scala CLI, Scastie and potentially the IntelliJ Scala debugger. -
Extending the
scala3-binary-decoder
with the ability to decode local variables, for instance to decode captured variables. -
Using the position (line and column) of a decoded symbol to automatically disambiguate the evaluation scope of a debug expression (watch or debug console).