Work in Progress

We are still in the process of writing the documentation for Scala 3. You can help us to improve the documentation.

Are you searching for the Scala 2 documentation?

Guide to Scala 3 Compiler Contribution

Finding the Cause of an Issue

Language

In this section, you will be able to answer questions such as:

  • where does an error happen in a codebase?
  • when during compilation was a particular tree introduced?
  • where is a particular object created?
  • where is a particular value assigned to a variable?

You may be able to quickly find the source responsible for an issue by consulting common issue locations

What phase generated a particular tree?

As described in the compiler lifecycle, each phase transforms the trees and types that represent your code in a certain way.

To print the code as it is transformed through the compiler, use the compiler flag -Xprint:all. After each phase group is completed, you will see the resulting trees representing the code.

It is recommended to test -Xprint:all on a single, small file, otherwise a lot of unnecessary output will be generated.

Trace a Tree Creation Site

When you see a problematic tree appear after a certain phase group, you know to isolate the rest of your search to the code of that phase. For example if you found a problematic tree after phase posttyper, the problem most likely appears in the code of PostTyper. We can trace the exact point the tree was generated by looking for its unique ID, and then generating a stack trace at its creation:

  1. Run the compiler with -Xprint:posttyper and -Yshow-tree-ids flags. This will only print the trees of the posttyper phase. This time you should see the tree in question be printed alongside its ID. You’ll see something like println#223("Hello World"#37).
  2. Copy the ID of the desired tree.
  3. Run the compiler with -Ydebug-tree-with-id <tree-id> flag. The compiler will print a stack trace pointing to the creation site of the tree with the provided ID.

Enhanced Tree Printing

As seen above -Xprint:<phase> can be enhanced with further configuration flags, found in ScalaSettings. For example, you can additionally print the type of a tree with -Xprint-types.

Increasing Logging Output

Once you have identified the phase that generated a certain tree, you can then increase logging in that phase, to try and detect erroneous states:

  • general logging within a phase can be enabled with the -Ylog compiler flag, such as
    • -Ylog:<phase1>,<phase2>,... for individual phases
    • -Ylog:all for all phases.
  • Additionally, various parts of the compiler have specialised logging objects, defined in Printers. Change any of the printers of interest from noPrinter to default and increase output specialised to that domain.

The compiler issues user facing errors for code that is not valid, such as the type mismatch of assigning an Int to a Boolean value. Sometimes these errors do not match what is expected, which could be a bug.

To discover why such a spurious error is generated, you can trace the code that generated the error by adding the -Ydebug-error compiler flag, e.g. scala3/scalac -Ydebug-error Test.scala. This flag forces a stack trace to be printed each time an error happens, from the site where it occurred.

Analysing the trace will give you a clue about the objects involved in producing the error. For example, you can add some debug statements before the error is issued to discover the state of the compiler. See some useful ways to debug values.

Where was a particular object created?

If you navigate to the site of the error, and discover a problematic object, you will want to know why it exists in such a state, as it could be the cause of the error. You can discover the creation site of that object to understand the logic that created it.

You can do this by injecting a tracer into the class of an instance in question. A tracer is the following variable:

val tracer = Thread.currentThread.getStackTrace.mkString("\n")

When placed as a member definition at a class, it will contain a stack trace pointing at where exactly its particular instance was created.

Once you’ve injected a tracer into a class, you can println that tracer from the error site or other site you’ve found the object in question.

Procedure

  1. Determine the type of the object in question. You can use one of the following techniques to do so:
    • Use an IDE to get the type of an expression, or save the expression to a val and see its inferred type.
    • Use println to print the object or use getClass on that object.
  2. Locate the type definition for the type of that object.
  3. Add a field val tracer = Thread.currentThread.getStackTrace.mkString("\n") to that type definition.
  4. println(x.tracer) (where x is the name of the object in question) from the original site where you encountered the object. This will give you the stack trace pointing to the place where the constructor of that object was invoked.

Where was a particular value assigned to a variable?

Say you have a certain type assigned to a Denotation and you would like to know why it has that specific type. The type of a denotation is defined by var myInfo: Type, and can be assigned multiple times. In this case, knowing the creation site of that Type, as described above, is not useful; instead, you need to know the assignment (not creation) site.

This is done similarly to how you trace the creation site. Conceptually, you need to create a proxy for that variable that will log every write operation to it. Practically, if you are trying to trace the assignments to a variable myInfo of type Type, first, rename it to myInfo_debug. Then, insert the following at the same level as that variable:

var tracer = "",
def myInfo: Type = myInfo_debug,
def myInfo_=(x: Type) = { tracer = Thread.currentThread.getStackTrace.mkString("\n"); myInfo_debug = x }

Contributors to this page: