Submitted by Jacob Kreindl, BSc.

Submitted at Institut f¨ur Systemsoftware

Supervisor o.Univ.-Prof. Dipl.-Ing. Dr.Dr.h.c. Hanspeter Source-Level M¨ossenb¨ock Co-Supervisors Debugging Support Dipl.-Ing. Dr. Matthias Grimmer Dipl.-Ing. Manuel in an LLVM-IR Rigger Interpreter April 2018

Master Thesis to obtain the academic degree of Diplom-Ingenieur in the Master’s Program

JOHANNES KEPLER UNIVERSITY LINZ Altenbergerstraße 69 4040 Linz, Osterreich¨ www.jku.at DVR 0093696 2

Abstract

Sulong executes programs that are compiled to LLVM IR, the language-independent intermedi- ate representation of source code used by the LLVM infrastructure, on the Java Virtual Machine (JVM). The interpreter is based on the Truffle language implementation framework and is part of the GraalVM project. Truffle provides a versatile backend which en- ables source-level inspection of programs it executes even across language boundaries. This thesis describes how Sulong leverages debug information available in LLVM IR bitcode files to support this feature.

First, the thesis describes how Sulong relates programs it executes to locations in their original source code. It further presents the necessary extensions to the interpreter’s execution data structures that enable the debugger backend to facilitate source-level single-stepping and set breakpoints.

Next, the thesis defines multiple layers of abstraction from LLVM IR as well as Sulong’s internal data model that provide a source-level view of an interpreted program’s runtime state which includes language-specific display of types, scopes and values. It also introduces specialized data structures to efficiently represent runtime debug information.

This thesis demonstrates the capabilities of the presented approach by inspecting a native Ruby extension implemented in C++ at runtime. A performance evaluation further shows that runtime overhead in terms of execution time introduced by instrumentation and symbol inspection is negligible in many cases. 3

Kurzfassung

Sulong führt Programme auf der Java Virtual Machine (JVM) aus, die zu LLVM IR kompiliert wurden, der sprachunabhängigen Repräsentation von Quellcode, die die LLVM Compiler Infras- truktur verwendet. Der Interpreter basiert auf dem Truffle Framework zur Implementierung von Programmiersprachen und ist Teil des GraalVM Projekts. Truffle stellt ein vielseitiges Debugger Backend zur Verfügung, welches das Inspizieren von Programmen, die es ausführt, ermöglicht, auch über mehrere Programmiersprachen hinweg. Diese Masterarbeit beschreibt, wie Sulong Debug Information in LLVM IR Bitcode Dateien verwendet, um dieses Feature zu unterstützen.

Die Masterarbeit beschreibt zunächst, wie Sulong die Programme, die es ausführt, auf den ur- sprünglichen Quellcode zurückführt. Des weiteren präsentiert sie die notwendigen Erweiterun- gen zu den Datenstrukturen des Interpreters, die es dem Debugger Backend ermöglichen, Gast- programme auf Level derer ursprünglichen Programmiersprache Schritt-für-Schritt auszuführen und Haltepunkte in ihnen zu setzen.

Des weiteren definiert die Masterarbeit mehrere Abstraktionsschichten von LLVM IR und Su- longs internem Datenmodell, die eine Sicht auf den Laufzeitzustand eines Gastprogramms ermöglichen, die dessen ursprünglicher Programmiersprache entspricht, was sprachspezifische Darstellung von Typen, Scopes und Werten beinhaltet. Sie beschreibt auch spezialisierte Daten- strukturen zur effizienten Repräsentation von Debug Information zur Laufzeit.

Diese Masterarbeit demonstriert die Fähigkeiten des präsentierten Ansatzes durch das In- spizieren einer Ruby Erweiterung zur Laufzeit, die in C++ implementiert wurde. Eine Performance- Evaluierung zeigt des weiteren, dass der Laufzeit Overhead in Form von Ausführungszeit, den Instrumentierung und Symbolinspektion erzeugen, in vielen Fällen vernachlässigbar ist. Contents 4

Contents

1 Introduction7 1.1 Motivation ...... 7 1.2 Goals and Scope ...... 8 1.3 Thesis Structure ...... 9

2 System Overview 10 2.1 The LLVM Compiler Infrastructure ...... 10 2.2 GraalVM ...... 12 2.2.1 Truffle ...... 12 2.2.2 Sulong ...... 16

3 LLVM IR 17 3.1 General Structure ...... 17 3.1.1 Scopes ...... 18 3.1.2 Data flow ...... 19 3.1.3 Control flow ...... 20 3.1.4 Type system ...... 20 3.2 Representations ...... 21 3.3 Binary Encoding ...... 22 3.3.1 Symbol Table ...... 23 3.3.2 High-Level File Structure ...... 24

4 Debug Information 26 4.1 LLVM IR Metadata ...... 26 4.1.1 Structure ...... 27 4.1.2 Encoding ...... 28 4.2 LLVM IR Debug Information ...... 31 4.2.1 Locations ...... 32 Contents 5

4.2.2 Symbols ...... 32 4.2.3 Types ...... 33 4.2.4 Scopes ...... 35 4.3 Value Mapping ...... 37 4.3.1 Locals ...... 37 4.3.2 Globals ...... 38

5 Stepping & Breakpoints 40 5.1 Location Information ...... 40 5.1.1 Representation in Truffle ...... 41 5.1.2 Representation in Sulong ...... 42 5.2 Truffle Instrumentation ...... 44 5.2.1 Node Implementation ...... 45 5.2.2 Wrapper Implementation ...... 47 5.3 Dynamically Halting Execution in an Instrumented AST ...... 49

6 Source-Level Symbol Inspection 52 6.1 Symbol Information ...... 52 6.2 Symbol Inspection in Truffle ...... 55 6.3 Symbol Inspection in Sulong ...... 57 6.3.1 Value Layer ...... 57 6.3.2 Representation Layer ...... 60 6.3.3 Value Tracking ...... 62

7 Case Study 64 7.1 Program Description ...... 65 7.2 Analysis ...... 69 7.2.1 Stepping ...... 69 7.2.2 Symbol Inspection ...... 73

8 Evaluation 82

9 Future Work 86

10 Related Work 88

11 Conclusion 90 Contents 6

Bibliography 95 Introduction 7

Chapter 1

Introduction

This chapter states the motivation for implementing support for source-level debug- ging in an LLVM IR interpreter. It also defines the scope of this thesis and provides an outline of its structure.

1.1 Motivation

Real world software systems tend to be highly complex collections of source code frequently written in multiple programming languages. They are also often implemented and maintained by many different people. Intricate semantic nuances of many programming languages and necessarily subjective interpretation of even well-defined requirements already make it hard for developers to specify large programs entirely correctly. Different execution environments as well as optimizing and interpreters add another source for possibly subtle bugs. This raises demand for versatile that help developers to detect errors at runtime under realistic conditions. However, most traditional approaches fail to support source-level inspection across language boundaries. As a result, developers often need to use multiple frontends to debug native extensions for programs in dynamic languages such as Ruby.

Sulong is an interpreter for LLVM IR, a common representation of code in various low-level programming languages, including C, C++ and Fortran. It is based on the Truffle framework for implementing high-performance, interoperable Abstract Syntax Tree (AST) interpreters. Sulong Introduction 8 is also part of the GraalVM project where its main use-case is the execution of native extensions for various other Truffle-based implementations of dynamic programming languages.

Truffle contains a framework for source-level program instrumentation and debugging. It re- quires individual language implementations to provide additional information in the AST and to define a language-specific representation of their runtime state. However, supporting it is optional. The goal of this thesis is to implement the necessary features in Sulong. Truffle generally supports source-level debugging across language boundaries in the same frontend. By implementing the corresponding API in Sulong we enable the important use-case of debugging native extensions for dynamic languages.

1.2 Goals and Scope

Enabling support for source-level debugging in Sulong requires applying debug information in LLVM IR to relate nodes in the Truffle AST to locations in the source code, reconstruct the original program state from the interpreter’s execution of compiled code and providing a language-specific representation of it to Truffle’s debugger backend. The concrete scope of this thesis consists of the following goals.

• Analyzing debug information in LLVM IR: This entails evaluating the information it contains as well as its encoding.

• Relating Truffle AST and source code: This includes marking AST nodes that correspond to statements or other programming constructs and relating them to their respective locations in the source code and retaining the source-level scope hierarchy.

• Supporting Truffle instrumentation: Extend Sulong’s implementation of AST nodes as required by Truffle’s framework for source-level program instrumentation. The debug- ging framework requires this to enable single-stepping and breakpoints.

• Source-level symbol inspection: This requires abstracting the interpreter’s runtime state to provide a language-specific representation of source-level scope entries and their values in a format suitable for the debugging framework. Introduction 9

1.3 Thesis Structure

This thesis starts with a general overview of the technologies it uses in Chapter2. Following this, Chapter3 introduces the basic concepts of LLVM IR and its different formats. Chapter4 continues with a more in-depth analysis of the content and encoding of debug information in LLVM IR files. Next, Chapter5 describes how Truffle implements program instrumentation and what this requires of Sulong. After this, the thesis describes in detail how the interpreter provides a language-specific view of the runtime state of programs it executes in Chapter6. Chapter7 then exercises a case study to demonstrate the previously discussed features. Follow- ing this, Chapter8 provides a short evaluation of the impact enabling debugging support has on execution performance. Subsequently, Chapters9 and 10 present future and related work. The thesis concludes with a summary of the presented accomplishments in Chapter 11. System Overview 10

Chapter 2

System Overview

This chapter provides an overview of the various projects and technologies used in the thesis. We start with a short overview of the LLVM project. Following this, we discuss the GraalVM project with a special focus on the Truffle framework and some of its features. The chapter will conclude with a short introduction of the LLVM IR interpreter Sulong.

2.1 The LLVM Compiler Infrastructure

The LLVM Compiler Infrastructure consists of a highly extensible collection of tools and frame- works facilitating program analysis and transformation. It is based around a common, language-

Figure 2.1: Concept of the LLVM compilation tool-chain System Overview 11 agnostic intermediate representation of source code called LLVM IR [1]. The project’s flexibility and extensive ecosystem has gained it a large following both in terms of commercial and open- source projects as well as academic research. The name LLVM is not an acronym, though many people mistakenly refer to the project as Low Level Virtual Machine.

Figure 2.1 shows the general structure of an LLVM compilation toolchain. Its entry point if a compiler frontend whose task it is to verify the syntactic correctness of the given source code and parse it into LLVM IR. This is the only language-specific part of the tool chain, though any particular LLVM frontend may support multiple programming languages. Perhaps the most popular project of this kind, aims to provide a frontend for the C/C++ family of programming languages [2]. It is developed as part of the LLVM project. DragonEgg[3], on the other hand, is a plug-in for the Gnu Compiler Collection (GCC) which enables it to generate LLVM IR from C and C++ as well as Ada and Fortran. Although the project is no longer under active development its source code is openly available and remains in use due to its ability to process Fortran.

LLVM can apply various transformation and analysis passes on the parsed IR. In the context of compilation the project uses this concept to implement and perform program optimization, but other uses-cases also include static analysis and program verification. While frontends, e.g. Clang, usually provide the option of applying selections of passes themselves, LLVM also contains the opt tool which gives users more fine-grained control over which transformations or analyses to perform. The project also provides developers with a powerful framework to implement custom passes. This extensibility has greatly aided LLVM in gaining popularity among compiler researchers.

To produce a final artifact LLVM provides multiple backends which are able to generate target- specific executable code for different platforms. For our use-case this is not needed though. Tools based on the LLVM framework can also export a binary or textual representation of their in-memory LLVM IR to a file for further processing. System Overview 12

Figure 2.2: Structure of the GraalVM

2.2 GraalVM

The GraalVM project provides efficient, interoperable implementations and tooling support for various popular programming languages such as JavaScript, Ruby, R and more[4] on top of the Java Virtual Machine. It leverages the power of the Truffle language implementation framework[5] and the Graal dynamic compiler. GraalVM also uses Sulong to execute LLVM- based languages and implement native extensions of the included dynamic languages. Figure 2.2 further illustrates the structure of the GraalVM and its components.

2.2.1 Truffle

Truffle is a framework for implementing Abstract Syntax Tree (AST) interpreters in Java. It provides built-in partial evaluation capabilities to facilitate program optimization. Truffle can also use Graal to dynamically compile parts of the AST to machine code based on profiling feedback collected at runtime in order to further improve execution performance. In addition to that, Truffle contains an interoperability API which enables the sharing of functions and values between different language implementations. Language implementers can also benefit from low-overhead tooling support by implementing the framework’s Instrumentation API. System Overview 13

i n t abs ( i n t a ) { i f ( a < 0) { return −a ; } else { return a ; } }

Figure 2.3: AST of the abs function

2.2.1.1 AST Implementation

To create a new Truffle language a developer needs only to implement a parser that translates functions of the source program into an AST representation that implements Truffle’s node interfaces. The framework provides a sophisticated DSL to greatly simplify this task. As an example, Figure 2.3 shows the C code and AST of a function that computes the absolute value of an integer.

A Truffle AST node must inherit from the Node class. It represents an operation in the source- level program and can have an arbitrary number of children. An implementation needs to define at least one execute method to retrieve the node’s value. This function receives the stack-frame for the source-level program as its first argument. Truffle uses it to store the current values for all necessary source-level local variables, including function arguments. When compiling the AST to machine code Graal includes the typed slots of the frame in register allocation.

The root of a Truffle AST is an instance of the RootNode class. Its commonly has only one direct child node which represents the function’s body. This node is typically responsible for copying the arguments of a call to this function to the frame. System Overview 14

A Truffle language implementation must also provide a subclass of the generic TruffleLanguage class. Upon launch, Truffle instantiates it as an entry point to request parsing of a supported file into its AST representation. This class also provides information about the guest language, e.g. its name and the MIME-types it supports. Instances of it also manage the language context, i.e. an object that holds the global scope and additional runtime state, for the current execution. The framework also uses this class to provide more information about guest language values.

2.2.1.2 Language interoperability

Truffle’s language interoperability is based around TruffleObject. This interface defines a language-agnostic abstraction of language-specific complex values that can be shared with other Truffle language implementations. The API also defines a message-based API to interact with these objects. Among other features, the set of predefined messages allows for invocation in case the object is callable, accessing named members if the object is structured or to indexed members if it is array-like, as well as testing for any of the aforementioned properties.

A Truffle based language interpreter parses the functions of a source-level program into an AST representation. The framework defines common interfaces for its elements. Coupled with the TruffleObject value abstraction, this generalization of execution code allows Truffle to combine the function ASTs generated by different interpreters. As a result, code in one guest language can call functions defined in another.

Truffle provides the Polyglot API to support language interoperability. That includes features to facilitate parsing of files by the appropriate interpreters as well as explicitly importing functions and values from their runtime contexts.

2.2.1.3 Debugging

Truffle provides an API for efficient source-level instrumentation of executed programs with low runtime overhead [6]. This forms the basis of tooling support for Truffle languages. While interpreters can choose whether or not to provide it at all, this feature requires language im- plementations to provide additional information about the programs they execute. The Truffle Debugging API uses these instrumentation capabilities to provide a language-agnostic debugger System Overview 15 backend. This framework also enables Truffle language implementations to provide access to source-level information to the frontend.

A compatible debugger frontend can use the debugging API to control execution in a guest language program. This enables it to dynamically suspend the debuggee once it reaches arbi- trary locations in its source code, i.e. setting breakpoints. The API also supports source-level stepping strategies which includes single-stepping as well as stepping into or out of function calls. The instrumentation framework avoids significant runtime overhead in terms of execu- tion performance by making heavy use of Truffle’s partial evaluation and the Graal compiler as JIT.

The debugging API provides compatible frontends with the means to display the source-level state of a running guest language program. This includes accessing the names and types of local and global variables as well as language-specific representations of their values at runtime. Additionally, the framework can associate these entities with their declaration sites in the original source files. The debugging API even provides abstractions for source-level scopes.

GraalVM contains the Chrome Inspector (CI), a tool which acts as a bridge between the Truffle debugging API and any debugger using the Chrome Devtools protocol [7]. It enables developers to debug programs running in a Truffle language interpreter using the debugger frontend of the Chrome Devtools [8]. This collection development tools is integrated into the Google Chrome [9] webbrowser and the open-source project it is based on, Chromium [10].

The maintainers of Truffle’s debugging API also provide a plug-in for the Netbeans IDE [11] which enables debugging support for Truffle languages. While it allows developers to benefit from the IDE’s integrated support for multiple programming languages, including syntax high- lighting, this debugger frontend has one major drawback. Since the plug-in is essentially an extension for Netbeans’ existing Java debugger the IDE does not associate it with any non-Java source files and makes it impossible to set breakpoints in guest language programs. However, Truffle also enables developers to wrap Java functions in an interop object so that interpreters can call them as part of guest language execution. Since the framework cannot instrument this code CI has no access to it. What is more, Netbeans enables developers to debug the interpreter itself while it executes a guest language program. Fortunately, CI does not prevent developers to attach any additional Java debuggers to the same process. In practice it is very valuable for Truffle language implementers to debug the guest language program in CI while at the same System Overview 16 time debugging the interpreter in the Java debugger included in their IDE of choice.

2.2.2 Sulong

Sulong is a Truffle language implementation for LLVM IR [12]. Though GraalVM includes it as a standalone interpreter as well, Sulong’s primary use case is the execution of native extensions for other Truffle languages. This approach opens up opportunities for runtime optimization, e.g. instruction inlining or partial evaluation, across language boundaries. Truffle also provides a Native Function Interface to directly interact with system libraries and other native code. However, language implementations still prefer to use Sulong due to its potential for superior execution performance. This interpreter mainly targets executing IR compiled from C, C++ and Fortran code, though multiple efforts exist to extend this support to other LLVM-based languages, e.g. Haskell and Rust.

Sulong translates LLVM IR’s unstructured control flow to an AST interpreter by dynamically dispatching basic blocks. That is, the single child node of a RootNode for a function in Sulong has itself an array of basic blocks in the form of nodes as children. When execution enters this dispatch node it executes the first block node to retrieve the index of its current successor block. It repeats this dispatch until either a block indicates that it has no successor and the function should return or it throws an exception.

Sulong’s integrated parser targets the binary encoding produced by LLVM in versions 3.8 to 5.0. Additionally, Sulong can parse and execute LLVM IR compiled by the DragonEgg GCC plug-in, specifically the version based on LLVM 3.2. This LLVM frontend remains among the few capable of processing Fortran code. Since many native modules in the R were implemented in Fortran and GraalVM also includes FastR, a Truffle language implementation of R, this support is still necessary. LLVM IR 17

Chapter 3

LLVM IR

This chapter will give readers a general overview of LLVM IR, its structure and elements as well as how different representations reflect it.

LLVM Intermediate Representation (LLVM IR) is the language-independent program repre- sentation used by all tools of the LLVM ecosystem. It features an assembly-like structure and syntax that lacks the syntactic conciseness of higher-order languages such as Ruby or Python, but is designed to be versatile enough to express any of their semantic constructs. As such, LLVM IR itself can be used as a general-purpose programming language. However, this is not its intended goal. The representation is not platform-agnostic as it includes target-specific memory alignment information. LLVM IR uses a RISC-like instruction set enriched with cer- tain higher-level information such as a type information and an explicit data and control flow graph to allow for effective analyses. [1]

3.1 General Structure

In the following we will use the common abs function, as already shown in Figure 2.3, to illustrate some important concepts of LLVM IR. We used the Clang frontend in version 5.0.1 to generate and export the IR from the given C code. Listing 3.1 shows the textual representation of it, though we omitted some less important details for brevity. LLVM IR 18

define i32 @abs( i32 ){ %2 = alloca i32 , a l i g n 4 %3 = alloca i32 , a l i g n 4 store i32 %0, i32∗ %3, a l i g n 4 %4 = load i32 , i32∗ %3, a l i g n 4 %5 = icmp slt i32 %4, 0 br i1 %5, label %6, label %9

;

;

;

Listing 3.1: abs function in LLVM IR

3.1.1 Scopes

LLVM IR distinguishes only between two scopes. The Module scope can contain global vari- ables, constants, definitions of structured types and declarations as well as definitions of func- tions. The Function scope, on the other hand, contains the local constants and variables of the function, but it cannot contain nested functions. In addition, both local and global scopes may optionally include metadata about the entities they contain. The function scope has access to everything contained in the module scope, but not vice versa.

In our example the global scope contains only a single entry, the abs function. LLVM IR prefixes the identifiers of global symbols with an @ and local symbols with a %. Definitions of structured types are the only exception to this rule. They can only occur in the global scope but use the same prefix as local identifiers. LLVM IR 19

3.1.2 Data flow

LLVM IR is in Static Single Assignment (SSA) form. In practice, this means that all instructions in LLVM IR that produce a value store it into a potentially infinite set of typed (virtual) registers, each of which can only be assigned once. In essence, this makes the instruction synonymous with the value it produces. This property, however, does not extend to memory as well. In general, each value needs to be defined before an instruction can use it. An exception to this are special Phi-instructions which determine their actual value by selecting from one of several possible values based on prior control flow. E.g. in the case of a loop variable this may require a forward reference.

In the textual representation most virtual registers have an explicit name merely to improve readability. Frontends can specify this name, but usually LLVM derives it implicitly from the defining instruction’s index in the function’s value list. This list also contains function parameters and labels. In our example its 0th entry is the input parameter to the @abs function while indices 1, 6, 9 and 11 correspond to labels. Instructions that do not produce a value, e.g. memory writes, branches or calls to functions without return value, do not have a name in the value list.

The example also shows a common paradigm for how LLVM frontends translate variables that are mutable at source-level to SSA form. They use the alloca instruction to allocate stack memory for the variable and the load and store instructions to change and retrieve the variable’s value where required. Since stack variables exhibit poor runtime performance compared to register allocated ones, LLVM provides the mem2reg optimization pass to lower the data flow to SSA instructions wherever possible. This paradigm greatly simplifies frontend implementation.

In our example, the program allocates two stack variables. The register %2 contains a pointer to the return value of the function while the allocation %3 contains the value of the source-level variable a. LLVM IR 20

3.1.3 Control flow

LLVM IR models the control flow graph of a function explicitly by using conditional and unconditional jumps between the basic blocks of a function. In this context, a basic block refers to a series of subsequent instructions that does not contain inner jumps. Branching instructions may only occur at the end of a basic block and may target only the beginning of one. Function invocations are the only exception to this rule. The instruction set only defines two higher- level control flow instructions, unwind and invoke, in order to represent exception handling paradigms. The IR does not, however, directly include information about loops, though LLVM is capable of loop detection since some passes require it.

The example in Listing 3.1 contains four basic blocks. Since the frontend does not specify their names, LLVM generates an implicit label for each of them. The entry block allocates space for the used variables on the stack and checks the condition of the if -statement. The second and third basic blocks, identified by the labels %6 and %9, then model its branches. Both read the value of a from %3 and write a result to %2 and then transfer control to the last block, %11, which retrieves the return value and terminates execution of the function with it as result.

3.1.4 Type system

LLVM IR includes a source-language-independent type system. As primitives it defines integer types of arbitrary width as well as floating point types of different precision. LLVM leaves the choice to treat integer values as signed or unsigned up to the instructions operating on them, the type does not have a notion of this distinction. LLVM IR’s type system defines only five derived types: functions, arrays, vectors, typed pointers and structs. The difference between arrays and vectors is only in their range of supported element type, neither can be of dynamic length. While only primitives and pointers can act as element type of a vector, any type can do so for an array. A struct can contain an arbitrary number and choice of member types. Members of a struct type do not have names, they are identified and accessed only by their offset and size.

Our example uses only two types. i32 denotes a 32bit integer type and i32* the type of a pointer to a value of it. The return type of any instruction producing a value also defines the LLVM IR 21 type of the virtual register used to store it.

3.2 Representations

LLVM defines 3 distinct but equivalent representations of LLVM IR and provides the tools necessary to transform between them.

• in-memory

All tools based on the compiler infrastructure operate on a highly-compact in-memory representation that is optimized for easy modifiability. LLVM provides a sophisticated API which allows tools to build and manipulate this representation which is a vital re- quirement to allow developers to implement new LLVM frontends and passes.

• binary file

The binary encoding is mainly optimized for space-efficiency but its structure also enables developers to implement fast parsers. This format is subject to frequent change and, while the LLVM developers do try to maintain a degree of backward compatibility [13] in the parser, this happens mostly on a best-effort basis. The LLVM project provides documen- tation for the binary encoding in [14]. We refer to LLVM IR in this format as LLVM bitcode. Though there is no official naming convention for the different representations of LLVM IR, it makes sense to define one for this thesis.

• textual file

The textual encoding of LLVM IR aims to be human readable. This format is mostly stable, but LLVM does not promise any kind of backward compatibility for parsing this encoding. Listing 3.1 shows the textual representation as used by LLVM in the 5.0.1 release. The LLVM project provides documentation for the binary encoding in [15].

Standalone LLVM-based tools usually accept LLVM IR files in either encoding as input and can also export into either. E.g. the Clang frontend can produce an LLVM IR file in binary encoding LLVM IR 22 by specifying the arguments -c -emit-, or in the textual encoding by using the arguments -S -emit-llvm. LLVM also provides the llvm-dis tool to disassemble a file in the binary encoding to the textual representation, and the llvm-as tool to do the reverse transformation.

3.3 Binary Encoding

We refer the binary encoding of LLVM IR as LLVM bitcode. This format encodes the IR as information entries in a structure of nested blocks. Though it is similar to the textual representation, there are key differences that make bitcode files much smaller and easier to parse. LLVM’s own parser implementation refers to information entries as Records. These are indexed sequences that can contain any combination of fixed as well as variable-width integers, 6-bit characters, a typed array or at most one blob, random binary data of any size.

On the most basic level, an LLVM bitcode file is just a sequence of 5 distinct primitive directives. These direct a parser how to build the higher-level block structure. We briefly describe them in the following.

• Enter subblock

This directive instructs the parser to begin parsing a subblock. It also provides an operand to indicate which kind of block to start. Coupled with another operand, the number of words in the bitcode file until the first directive after the subblock, this allows the parser to skip or defer parsing of certain blocks until their content is needed.

• Exit block

Quite self-explanatory, this directive instructs the parser to close the current block and continue in its parent block. This directive is zero-padded in the bitcode file to ensure that the first directive after the block begins on an addressable word.

• Define record

This directive gives the parser a sequence of data types to interpret as a parsing template LLVM IR 23

for future records. Record definitions are identified by the order in which the parser encounters them, they do not have an explicit ID. The first entry in a record is usually an integer as it, together with the kind of block that immediately contains the record, defines its semantics. This directive essentially allows a bitcode file to define its own encoding and is an important factor for the binary encoding’s space efficiency.

• Defined record

The argument to this directive identifies a previously defined record. The parser needs to parse it immediately after this directive.

• Undefined record

This directive instructs the parser to read first an integer denoting a record’s ID, followed by another integer containing the number of operands to parse and, in sequence, this number of operands encoded also as integers. This record encoding is highly inefficient compared to defined records. However, bitcode files still use it in some places.

An LLVM bitcode file usually uses the *.bc file extension. While its content is not intended to be human-readable, LLVM includes the llvm-bcanalyzer tool that, when given the -dump option, prints the file content in an XML-like syntax.

3.3.1 Symbol Table

LLVM implements the symbol table as a Value List. The parser enters all records denoting symbols or values into it in the order the records occur in the file. Local scopes append their values to the global list but pop them off again after the corresponding blocks have been parsed.

To reference another value or symbol a record states the offset of the corresponding entry’s index in the value list from the top of the list when the parser reaches the record. This concept has the distinct advantage of keeping the numbers in the records small. Paired with the binary format’s concept of variable-width integers it greatly reduces the size of bitcode files. In addition, negative numbers enable forward references. Even though LLVM avoids emitting LLVM IR 24 these whenever possible, they are necessary in some instances. Wherever a record contains a forward reference to a symbol whose type is unknown at the time of parsing the record, it also contains an explicit reference to the type. Older versions of LLVM bitcode instead explicitly stated the referenced value’s index in the value list.

3.3.2 High-Level File Structure

The following is a list of the most important blocks that occur in a bitcode file. We describe their overall purpose and the kind of records they contain. This list is not meant to be exhaustive. It is only intended to give the reader an idea of how the binary encoding represents the structure presented in Section 3.1.

• Module block

This block is never part of another block. Analogous to the structure outlined in Sec- tion 3.1, its records denote mostly global variables and headers for both only declared and fully defined functions, but also some target-specific information like data layout in memory or mangling.

• Type block

This block occurs towards the beginning of the Module block. Its records build up a type table that is used throughout the bitcode file. As such, the type block precedes any records that describe a value. Nested blocks do not contain additional Type blocks. Definitions of named as well as structured types are also part of this type table. This is contrary to the textual representation of LLVM IR, where they would rather appear as top-level entries of the global scope.

• Function block

The records in a Function block encode the instructions that form the body of a function. There is no explicit record to assign an instruction to a basic block. A new one starts implicitly at the first record and after each record that encodes a control flow instruction. LLVM IR 25

Since LLVM IR does not support nested functions, function blocks only occur as direct children of the Module block.

Function blocks do not contain an explicit reference to a function header. Instead, they occur only after the last record in the Module block and in the same order as their headers in the value list. To allow parsers to exclude headers of functions that are only declared in the module from this enumeration, function headers contain the information whether or not the module also has a defining block for the function.

• Constant block

These blocks populate the value list with typed constant values. By extending the value- numbering strategy to constants as well LLVM bitcode avoids the need to distinguish between constant and dynamic value references in records. This not only simplifies pars- ing, it also prevents LLVM from having to repeatedly store type information for constant values with multiple uses. Constant blocks precede any value-producing records in their parent block. They can occur multiple times in both Module and Function blocks. Their presence marks perhaps the most significant divergence between textual and binary en- codings as the former inlines constant values wherever they are used as operands.

• ValueSymTab & StrTab

The records in Value Symtab blocks assign explicit names to the symbols in the value list as well as labels to instruction blocks. This also includes the names of named globals and functions. Since LLVM uses these names for linking it seems counter-intuitive not to store or reference them in the record that defines the corresponding symbol. To address this issue LLVM 5.0 introduced the StrTab block. Its only record contains a sequence of strings used throughout the bitcode file as a binary blob that can easily be mapped to memory. Function and global records now point directly to the offset and length of their names in this string-table. Interestingly, this block occurs only as a sibling but not as a descendant to the Module block. Debug Information 26

Chapter 4

Debug Information

This chapter describes the extent and representation of debug information in LLVM IR. In it, we first introduce the general metadata format. Following this, we explain the concrete structures LLVM uses to describe the original program. Lastly, we state how LLVM links debug information to values and instructions to describe a programs source-level state at runtime.

4.1 LLVM IR Metadata

LLVM frontends can include metadata in LLVM IR to preserve information that can be used in further processing steps. While debug information remains the main use case for this feature, the encoding is flexible enough to allow storing arbitrary, even tool-specific data. In practice, LLVM frontends always identify themselves in metadata of the modules they produce.

LLVM transformation passes usually make an effort to preserve metadata. This is especially true in the case of debug information. While it is in the nature of program optimization to remove or replace symbols and instructions, transformations that do so attempt to counteract the information loss by adapting the available metadata to reflect their changes. As an example, loop unrolling may remove an explicit index variable. However, the optimization can still preserve its value as a constant in debug information. Debug Information 27

1 define i32 @abs( i32 ) ! dbg !7 { 2 ;... 3 r e t i32 %12, !dbg !25 4 } 5 6 !llvm.dbg.cu = !{!0} 7 !llvm.ident = !{!6} 8 9 !0 = distinct !DICompileUnit(language: DW_LANG_C99, file : !1, ... ) 10 !1 = !DIFile(filename:"abs.c", directory:"<...>") 11 ;... 12 !6 = ! { !"clang version 5.0.1 (tags/ RELEASE_501/final)"} 13 !7 = distinct !DISubprogram(name:"abs", file: !1, line: 1, ...) 14 ;... 15 !25 = !DILocation(line: 7, column: 1, scope: !7) Listing 4.1: Partial metadata for the LLVM IR in Listing 3.1

As an example, Listing 4.1 shows some parts of the metadata for the abs function in textual format as introduced in Listing 3.1. We can infer that we used Clang to compile the example since it included its version string as shown in line 7. In the following we will use this example to illustrate some important concepts of LLVM metadata.

4.1.1 Structure

LLVM generally represents metadata as entries in a metadata list. This is analogous to the approach for the symbol table which we described in Section 3.3.1. To keep in line with the LLVM language reference [15] we will refer to an entry in the list as a node. We diverge from this document, however, in that we treat metadata strings only as a special kind of a metadata node rather than as a distinct concept. This notion reflects their representation in the binary encoding. Only the textual representation inlines metadata strings as unnamed constants when it uses them as operands to a node.

LLVM IR provides multiple different means to relate metadata to other bitcode entities. To begin with, a metadata attachment can link a node to a symbol or instruction and even express the kind of the association in the form of a name. Our example in Listing 4.1 uses the dbg kind to state that the attached node contains debug information. Other kinds include the llvm.loop metadata. Attached to a branch instruction it identifies the back-edge of a loop to its header block. The language reference [15] defines additional metadata attachment kinds. Additionally, frontends are free to define custom their own. There is also no defined limit for the number of attachments any particular symbol or instruction may hold. Debug Information 28

The example in Listing 4.1 also shows named metadata. Similar to the value attachments we described earlier these nodes are implicitly attached to the module. They are always generic metadata nodes with usually at least one entry. Our example shows two instances of this. The llvm.ident metadata is always present in any bitcode file. It references a metadata string which contains the name and version number of the tool that originally produced the module. Other LLVM tools or transformation passes usually do not change it. The llvm.dbg.cu metadata, on the other hand, is part of debug information. We will discuss its semantics later on.

Lastly, certain intrinsic functions require metadata nodes as operands. For this purpose, LLVM IR’s type system includes the metadata type. In bitcode, a value of this type is always an index into the metadata list. This feature allows LLVM IR to dynamically relate metadata to runtime values. In practice, debug information uses it to track the current values of source- level symbols. While backends typically do not emit instructions with metadata operands to executable machine code directly, they may still include the information they contain into other sections of the object files they produce.

4.1.2 Encoding

In the binary encoding, LLVM stores metadata nodes as records in special Metadata blocks. These can occur as children of both Module and Function blocks. The separation into global and function-local metadata is not present in the textual representation. As Listing 4.1 reflects, it features only module-level metadata.

LLVM bitcode in version 3.2 defines only three kinds of metadata nodes. We refer to this version of metadata as the old format. The new format used by LLVM version 3.8 and newer features significant structural changes. This includes a clear focus on debug information as it introduces a great number of additional records particular to that use-case. LLVM itself does not provide an upgrade path to the new format. Its parser simply drops all metadata when it detects a record exclusive to the old format. In the following we will present the most important records used for general metadata. In the interest of structure, we defer introducing those specific to debug information to future sections. Debug Information 29

• MD_Old_Node

In the old format, this record describes an all-purpose node that may contain an arbitrary number of typed entries. It is encoded as pairs of integers. The first value of each pair is an index into the type table. If it refers to the metadata type, the second value is a 0-based index into the metadata list. The void type instead indicates a NULL reference, regardless of the second value. If the first value refers to any other type, the second one is an also 0-based index into the value list.

• MD_Old_Fn_Node

In the old format, this record contains only the 0-based index of a defined function in the value list. It is actively used in debug information, though its advantage over MD_Old_Node is unclear.

• MD_String

This record represents a Unicode string encoded as a byte array. In bitcode, metadata string values are a distinct kind of node. This is contrary to the textual representation of LLVM IR where they are instead inlined into node definitions as nameless constants.

• MD_Value

Similar to MD_Old_Fn_Node this record contains an index into the value list. However, unlike its predecessor, the targeted value is not limited to a specific type.

• MD_Node

In the new format, this record replaces MD_Old_Node. It also defines a generic node, but it only contains metadata references. This avoids the need to explicitly emit type information for any of the entries. To retain the ability to reference values, the new format wraps them into MD_Value records. Since in practice other metadata forms the clear majority of node entries, this strategy significantly reduces metadata’s overhead on file size. The new format also uses 1-based indices into the metadata list. This allows it to encode NULL references efficiently as index 0. Debug Information 30

• MD_Strings

Starting from LLVM 3.9 this record replaces the less memory-efficient MD_String. Sim- ilar to the single record in an StrTab block, it contains all strings used throughout the metadata block as a single binary blob. The two concepts differ in two key points though. First, the binary data’s layout is different. It starts with a variable-width integer denot- ing the number of strings the blob contains. Another variable-width integer denoting its byte-size then precedes each of the subsequent strings. As the second difference, other nodes still expect the parser to generate an entry in the metadata list for each string as it parses the record.

• MD_Name & MD_Named_Node

Both formats use the same records to relate metadata to other bitcode entities. The MD_Name record encodes only a name. However, contrary to MD_String, a bitcode parser must not generate a node for it. In bitcode, an MD_Named_Node record always follows directly after MD_Name. Like MD_Node it contains any number of indexes into the metadata list. As the only difference, these are 0-based also in the new format. The parser must remember the mapping between the name and the subsequent node.

A metadata attachment in the textual representation is stated together with the attached-to value. Listing 4.1 shows this with the dbg attachments to both the function and an instruc- tion. The binary format, however, encodes these attachments separate from any non-metadata records. The Metadata_Kind block appears only directly under the module block. Its MD_Kind records contain the attachment names used throughout the module. Parsers are not supposed to enter these into the metadata list. Instead, each record also contains an integer number to identify the kind.

The binary encoding then uses the MD_Attachment records in the Metadata_Attachment block to actually represent the attachment. This block appears in both Function and Module blocks. If an MD_Attachment record has an uneven number of entries, then the first one is the 0-based number of the targeted instruction. Separate from the value list, this numbering includes also instructions that do not produce a value. Otherwise, the target is implicitly the function whose body the parser is currently parsing. MD_Attachment can describe multiple attachments. It encodes each of them as a pair of integers. The first contains the ID of the attachment Debug Information 31 kind, the second the 0-based indexes of the attached node in the metadata list. Similarly, the MD_Global_Attachment defines metadata attachments for a global symbol in the value list.

4.2 LLVM IR Debug Information

LLVM IR encodes debug information in the form of metadata nodes. Most LLVM frontends do not generate it per default and instead provide a command-line option to trigger this explicitly. Both Clang and DragonEgg use -g for this purpose.

The new metadata format in LLVM 3.8 and later uses a number of specialized records to encode descriptors for various constructs such as types, scopes and symbols. The old representation used in LLVM 3.2 ultimately contained the same structure. However, it encoded it within generic MD_Old_Node records. The first entry of each specialized node would be a 32-bit integer which identified the kind of specialization with its lower half and an encoding version in its upper 16 bits. However, the debug information representation is far from stable even in the new metadata format. In practice, each LLVM release also introduces a revision to the debug information format. This can entail repurposing or deprecating individual fields to significant structural changes. A parser that wants to support multiple LLVM versions invariably ends up using very inefficient node representations. Sulong applies the visitor pattern to its parser AST to transform it into a common runtime representation tailored to its specific needs.

Listing 4.1 contains the llvm.dbg.cu named metadata. This node always links to at least one DI_Compileunit. As the name suggests, it contains information about an individual compi- lation unit. This includes references to certain entities, e.g. all globals and enums, as well as general information like imports and the code’s original programming language. Until LLVM 3.8 it used to reference all nodes describing source-level functions as well. As a result, this re- quired nearly all metadata nodes to be encoded in the Module block as the function descriptors would reference local variables and those, in turn, their scopes. Debug Information 32

4.2.1 Locations

LLVM IR encodes location information in the form of the DI_Location metadata node. Its format has remained the identical in both old and new metadata format. Besides a line and a column number the node also contains a reference to another node representing the location’s scope which itself can reference a parent scope. Regardless of its length, however, this chain always leads to an DI_Subprogram, a metadata node describing the function or method that contains the original source location. Additionally, the DI_Location record contains as a forth argument an optional reference to another node of the same kind. If present, it indicates that LLVM inlined the instruction at the described location. This behavior can be caused by compiler optimizations, C++’s inline keyword or preprocessor directives like macros.

The binary format contains DI_Location nodes not only as records in the corresponding func- tion’s Metadata block. It uses these only to store the targets for inlined instructions. Since debug information requires location nodes quite frequently, using the regular matadata attach- ment for this purpose, like the textual representation does, would be rather wasteful. To reduce the file-size overhead, the binary encoding uses the Debug_Loc record directly in the function block to contain the required data. A parser must not enter this node into the metadata list, but instead attach it to whatever instruction it parsed last. To further avoid unnecessary overhead, Function blocks use the Debug_Loc_Again record to indicate that the last parsed Debug_Loc applies to another record as well. This case is especially prominent with LLVM 3.2 since in that version, although an explicit field for a column number exists, its value is always set to 0.

4.2.2 Symbols

LLVM IR metadata contains information about source-level symbols. This includes the kind and structure of their types as well as the scope and location where they are declared. Meta- data encodes data about strictly function-local symbols in the DI_Local_Variable node. Its members include metadata references to the scope and type of the symbol as well as its dec- laration site in the form of a line-number and a link to a DI_File node. For local variables that represent implicit object pointers, e.g. the this argument to instance methods in C++, DI_Local_Variable includes a flag to indicate this. DI_Global_Variable extends this range Debug Information 33 of information with several optional attributes, e.g. whether this node represents a static type member and, if so, a link to corresponding type. The name of both nodes refers to whether the values of the symbols they describe need to exist outside of a particular function execution. As an example, DI_Global_Variable would describe a local variable in a C program if it is declared as static. The node contains a boolean field to distinguish whether it describes an actually global symbol in terms of scope.

DI_Subprogram describes a function or method. In addition to its type, this includes its source- level name for display as well as its linkage name. The node also references its lexical location in the form of a file and line-number.

4.2.3 Types

LLVM IR debug information mostly uses 3 particular nodes to encode source-level types. Com- mon information among them includes the type’s name, its bitsize and alignment, its offset in its parent type, an optional integer value containing bitflags as well as an equally noncom- pulsory file reference and line-number to describe the declaration sites of user-defined types. For custom types the nodes also include an additional entry to reference their scopes. LLVM IR debug information contains no specific node to encode a void type, it simply uses a null metadata reference for this purpose.

• DI_Basic_Type

This node extends the common members only with an encoding kind for its values. This information specifies whether a debugger should interpret the bitpattern of such a value as a signed or unsigned integer or character, a floating point number, a memory address or as a boolean value. In combination with the bitsize this information is sufficient to describe values of common elementary types, e.g. C’s float or Fortran’s integer.

• DI_Composite_Type

A composite type describes a value with possibly several fields or different views to it. Beside several optional entries this metadata node always contains an integer tag to describe the kind of composition as well as a link to a generic metadata node which holds Debug Information 34

descriptors for the member types. It also references an identifier, a string that contains the type’s linkage name. Until LLVM 3.8 other metadata nodes commonly used this name to reference the composite type rather than point to it directly.

– Arrays & Vectors

For this composition kind the member node references one or more DI_Subrange nodes whose single entries denote the number of elements in each array dimension. Interestingly, DI_Composite_Type contains a reference to the elementtype directly rather than in its membertype field. The corresponding field remains unused for other composition kinds.

– Classes, Structs & Unions

For these composition kinds the node references derived types which relate the in- dividual fields’ types to their names and declaration sites. This strategy also allows LLVM to encode the member’s offsets in the intermediary rather than in a new copy of the basetype with the same information. Functions, however, do not require an intermediary node as the corresponding DI_Subprogram already contains all required data. There are other non-member types in this aggregate too. LLVM uses specific kinds of derived types to describe inheritance from or special relations to another composite.

– Enumerations

The members of an enumeration type map the specific integer values LLVM uses on IR-level to describe the possible values to their string representation. LLVM encodes this as one DI_Enumerator node per value.

• DI_Derived_Type

This node derives a new type by decorating another with additional information. As such, its noncompulsory members include the metadata reference to its basetype and an integer id which describes the kind of derivative. The most common derived types are pointers and references. They, and neither constant or volatile value qualifications or Debug Information 35

inheritance links, do not require additional information. None of these derivation kinds provide specific keywords or other formatting instructions to apply to the basetype’s name. Since symbols of this type necessarily reference a DI_Compileunit, and in turn the programming language they were defined in, at least indirectly, a debug information consumer requires knowledge about it to derive an accurate name. The derivation kind specific to type aliases, e.g. C’s typedef, necessarily contains it explicitly. The same holds true for the composite member type we already mentioned above.

An additional field of bitflags in the node defines further attributes about the type. For a composite member this includes whether LLVM introduced it artificially or if it has a static value. In the latter case, the module also contains a DI_Global_Variable that links to this node. Another flag defines the member as a bitfield. In that construct, its bitsize overrides that of its basetype and no alignment applies. Lastly, the only important flag for pointer types identifies object pointers, e.g. the implicit this argument of C++ instance methods. Like values of reference type, a debugger can safely dereference them as the LLVM frontend ensures that they point to allocated memory. The DI_Local_Variable for an object pointer also contains such a flag.

LLVM uses the DI_Subroutine node to describe function types. It does not include the other type nodes’ common members, but like DI_Composite_Type it links to an array of other type nodes. Its first entry denotes the function’s return type. It is always present, even if it just a null reference to indicate that functions of this type do not return a value. Any following nodes specify the parameter types. A null reference at the end of this list indicates that the function type supports variadic arguments.

4.2.4 Scopes

LLVM defines several debug information nodes to describe source-level scopes. They generally contain a link to their parent scope. With C/C++ source programs, the scope of an instruction or a local variable can only be either the containing function directly or a series of nested lexical blocks. Debug Information 36

• Files

The DI_File node describes a file by two string entries which denote its name and its absolute directory path. For C/C++ its containing file is the default scope for a top-level subprogram.

• Blocks

LLVM IR uses two separate debug information nodes to describe lexical blocks. To begin with, DI_Lexical_Block_File references both a parent scope as well as the source code file that contains the block in the form of an DI_File node. The DI_Lexical_Block node contains additionally the line and column in the referenced file at which the block starts.

• Functions

The DI_Subprogram node describes a function. It references the file and both the starting line of its definition as well as its body.

• Compileunits

Global variables use their DI_Compileunit as a default scope. For this purpose, the node also links to a DI_File. Functions link to it too, separately from their parent scope.

• Named Scopes

The DI_Namespace node describes a named scope. Up until LLVM 5.0 it used to contain a file and line-number in addition to a name, but since namespaces are generally not restricted to specific files the removal seems sensible. It can reference a parent scope, but this is optional here. Similarly, DI_Module also describes a named scope. However, this node has several entries unrelated to that purpose.

• Types

DI_Composite_Type acts as a scope for its member types which also includes functions. The node links to both the file and the line-number to describe its declaration site. Debug Information 37

declare void @llvm.dbg.declare( metadata , metadata , metadata ) declare void @llvm.dbg.value( metadata , i64 , metadata , metadata ) Listing 4.2: Signatures of LLVM IR’s value tracking intrinsics

4.3 Value Mapping

LLVM relates source-level symbols to their runtime values in different ways. While it uses a static mapping for global symbols, it updates the values of local ones as they change at runtime.

4.3.1 Locals

When compiling a source-file to LLVM IR, LLVM frontends insert calls to the llvm.dbg.declare and llvm.dbg.value intrinsics in function bodies to associate source-level local variables with their runtime values. Further transformation passes recognize and adapt them as needed to retain their ability to describe the original program state at any statement a debugger may hold at as accurately as possible. However, as optimizations often remove redundant variables, this is not always possible.

As Listing 4.2 shows, both intrinsics have three common parameters. Always first among them is the IR-level value wrapped as metadata. The second argument is the DI_Local_Variable metadata node that describes the source-level symbol. We will explain the separate scheme to track the state of a DI_Global_Variable later. Lastly, the intrinsics receive further information about how to interpret the first argument in the form of an MD_Expression node.

The llvm.dbg.declare intrinsic describes a local variable that resides on the stack. Its first argument is the address of the corresponding allocation wrapped as metadata. LLVM guar- antees that this pointer is always safe to dereference and that the instruction that produces it dominates the call to the intrinsic. When optimizations lower a symbol from the stack whose address the unmodified program would later dereference, they often change this argument to an explicitly undefined constant in addition to adding a call to llvm.dbg.value. This is quite redundant as a debugger could also infer from the presence of the replacing definition that the value is no longer in memory. The third and last parameter of llvm.dbg.declare, the Debug Information 38 metadata expression, is likely a remnant of earlier versions of debug information. Using LLVM 3.2 and later releases to compile Sulong’s extensive testsuites at various optimization levels we could not identify a single case that caused it to not be empty.

LLVM uses the llvm.dbg.value intrinsic to track optimized local variables. As such, its value argument is not restricted to a specific type, any entry of the value list is valid here. This includes constants as well as dynamic SSA values, even global variables. The MD_Expression argument to llvm.dbg.value is often empty. In general, it represents a list of operands that push or modify values on an implicit stack. Expressions for the llvm.dbg.value intrinsic have only two common uses. To begin with, a special operand in combination with a pointer as value argument declares that the actual value lies at that location in memory. As the only difference to llvm.dbg.declare, this address is not restricted to a stack allocation. The second possible expression describes a bitoffset and length. It declares that the value argument defines only a part of the symbol. LLVM optimizations generally try to lower as many values from memory to SSA level as possible to gain significant runtime performance improvements. For composite symbols such as structures or fixed-length arrays this often entails splitting them into multiple primitive values that fit into registers. As a result, this partial declaration occurs quite frequently.

While frontends never emit more than a single call to llvm.dbg.declare per DI_Local_Variable, especially at higher optimization levels they often end up populating function bodies with more invocations of the llvm.dbg.value intrinsic than actual instructions. Common bugs in opti- mization passes further contribute to this problem as they often define multiple values for a symbol in between two subsequent instructions, even in the same basic block.

4.3.2 Globals

Perhaps the biggest change in the debug information format between LLVM 3.8 and 5.0 regards the association between an IR-level global symbol and the corresponding DI_Global_Variable. Originally, the DI_Compileunit used to reference a node linking to all DI_Global_Variable descriptors which in turn linked to the value’s entry in the value list. This was all but reversed in LLVM 4.0 with the introduction of the DI_Global_Variable_Expression. That node just comprises links to a traditional DI_Global_Variable without a value reference and an optional MD_Expression. This intermediary now occurs as a metadata attachment of the well-known Debug Information 39 dbg kind to the IR-level value. Its expression is still usually empty. However, in rare occasions LLVM actually uses it to define a value for the symbol. For this purpose, valid operands can include integer literals, up to 64-bit dynamic integer values as well as addition and subtraction. In general though, LLVM still prefers constants in the value list over this encoding. Stepping & Breakpoints 40

Chapter 5

Stepping & Breakpoints

In this chapter we will first describe how Truffle encodes source-level location infor- mation and the strategies Sulong uses to relate LLVM IR debug information to that format. Following this we explain Truffle’s approach to instrumentation and what it requires from language implementations, in specific Sulong. We end the chapter with an overview of how Truffle’s debugging API uses these concepts to implement conditional halting of guest language program execution, i.e. single-stepping and breakpoints. We also discuss what particular challenges this poses for an interpreter for compiled languages and how Sulong solves them.

5.1 Location Information

Truffle and LLVM IR use different formats for location information. Figure 5.1 shows its repre- sentation in Sulong’s Truffle AST at runtime. In the following we will describe the components of this structure, how LLVM bitcode encodes location information and why Truffle’s own data structures for this purpose are not sufficient for bytecode interpreters. Stepping & Breakpoints 41

Figure 5.1: Source-Level Location Information in Sulong

5.1.1 Representation in Truffle

To represent source-level location information Truffle provides standard abstractions of plain- text containers and subsections of their content. These are only intended to represent lexical scopes. As we will explain in the next chapter, the Debugging API provides language imple- menters with separate means of exposing information about semantic ones.

Truffle provides the Source class as an abstraction to plaintext. In addition to the source code itself, these immutable objects provide additional metadata which includes the contained code’s MIME-type as well as a name and URI for it. Interpreters cannot define their own implementations of this class. Instead, the framework enables developers to create instances of it from regular files, Java Strings and also URIs, using a builder pattern. While this limitation is sufficient for non-compiled languages, it poses unique problems for bytecode interpreters. On the one hand, binary files usually do not conform to any text encodings such as UTF-8. When working with sources, Truffle insists on loading and analyzing the text they contain and inevitably fails. To work around this problem, Truffle languages apply Base64 encoding to the file content before converting it to a String and subsequently to a Source object to pass to Sulong. On the other hand, debug information in bytecode files usually uses absolute paths Stepping & Breakpoints 42 and filenames. It cannot detect, let alone track, modification or relocation of the original source files. In consequence, these files may be inaccessible or of unexpected content when or where the interpreter executes the compiled code.

The Truffle framework uses its SourceSection class to describe lexical regions in an associated Source. Language implementations cannot create subclasses of it either. To obtain instances they can instead use several overloaded, dynamic factory methods that the corresponding Source objects provide. Internally the class uses the section’s starting index and length in terms of characters to represent it, but it also defines convenience methods to retrieve this as line and column numbers for easier interpretation. Like Source objects, SourceSection instances are immutable.

Each implementation of a Truffle AST node must be derived from the abstract Node class. Its interface contains the aptly named getSourceSection method whose default implementation sim- ply returns null. Node instances need to define a dedicated field to store the object. Developers should either declare that field as final or annotate it as @CompilationFinal. Coupled with the lack of mutable members in both Source and SourceSection this allows Graal to efficiently treat the values as constants in compiled code.

Truffle uses the location information optionally attached to AST nodes in several ways. To begin with, the presence of this data is a prerequisite to allow language implementations to provide meaningful stack-traces. It is also invaluable for debugging the interpreters themselves as it enables tools, e.g. the IGV Truffle AST visualizer, to relate source code to execution nodes. The framework’s instrumentation API, and in direct consequence the Debugging API, requires it as well.

5.1.2 Representation in Sulong

Debug metadata in LLVM IR uses a combination of absolute filepaths, line and column numbers to reference source code. At the time Sulong executes a bitcode file, the parser cannot assume that this information still leads to a valid target. The user may have moved the original source code or deleted it altogether. Similarly, projects may decide to ship precompiled bitcode files to their users separately from the corresponding source code. If a programmer forgets to recompile the source code after making changes, it may no longer contain certain lines or Stepping & Breakpoints 43 columns. In all these situations Sulong can still use the available debug information to generate accurate stack-traces in case the executed code triggers a user-visible exception or an error in the interpreter. Sanity checks in Truffle’s Source and SourceSection classes actively prevent them from supporting this use-case though. They require each file to be present and readable and each combination of line and column to be valid in their associated text. To work around this problem Sulong does not attach SourceSection objects to its Truffle AST nodes directly. As Figure 5.1 shows, it instead uses a proxy interface called LLVMSourceLocation. If it is at all possible to build a SourceSection object for a location, the parser will prefer this representation. The alternative proxy implementation for unavailable code positions instead stores filename, line and column explicitly. Retaining debug information even in the absence of the referenced source code is not the only advantage of the proxy class though. As we will describe in the next chapter, Sulong also uses it to describe source-level scopes. Though this strategy undeniably causes significant runtime overhead in terms of memory, the Instrumentation API’s dependency on the unmodifiable SourceSection class leaves no viable alternative.

Curiously, LLVM’s debug metadata describing source-level scopes, e.g. DI_Lexical_Block or DI_Subprogram nodes, only state where the corresponding lexical region starts, but never where it ends. One possible explanation for this deficiency may be a lack of interested consumers for this information. Common debug information encodings for executable files, e.g. Dwarf, do not require it. For Sulong, however, this makes it hard to accurately convert location information to Truffle’s SourceSection. To solve this problem the project’s parser assumes a default length of 0 characters. In practice, this does not impact the functionality of debugger frontends as they do not use this information in any case.

Truffle interpreters for uncompiled languages, e.g. TruffleRuby for the Ruby programming lan- guage, represent a source-level statement by a single node with possibly multiple descendants for its subexpressions. Sulong, on the other hand, executes LLVM IR. This representation looses the notion of source-level statements almost entirely. Here, subexpressions do not form a tree but rather a sequential list of instructions. LLVM debug information is not exhaustive enough to allow for easy reconstruction of the original program. In the interest of reducing runtime overhead, Sulong doesn’t even attempt to do so. As a result though, Sulong only supports stepping on expression-level rather than statement-level. LLVM optimization passes only add to this problem by deleting instruction or even just dropping debug information. This is especially problematic at higher optimization levels. Stepping & Breakpoints 44

5.2 Truffle Instrumentation

The Truffle framework provides an API for source-level instrumentation. It implements this feature by dynamically inserting guest-language defined wrappers between nodes that represent source-level statements and their parents in the AST at runtime [6]. Such an intermediate node provides the same interface as the instrumented one, however, it notifies an attached probe of each execution event it encounters. A Truffle instrument, e.g. a debugger, can observe these events and react to them. The Truffle Debugging API uses this functionality to suspend execution at certain program locations.

1 /∗ ... package declaration and imports... ∗/ 2 @NodeChildren ({ 3 @NodeChild(value ="childA" , type = LLVMExpressionNode. class ), 4 @NodeChild(value ="childB" , type = LLVMExpressionNode. class ) 5 }) 6 @Instrumentable( factory = NodeImplWrapper. class ) 7 public abstract class NodeImpl extends LLVMExpressionNode { 8 9 private final LLVMSourceLocation sourceLocation ; 10 11 public NodeImpl(LLVMSourceLocation sourceLocation) { 12 this .sourceLocation = sourceLocation; 13 } 14 15 public NodeImpl(NodeImpl delegate) { 16 this (delegate.getSourceLocation() ) ; 17 } 18 19 @Specialization 20 protected Object executeObject(VirtualFrame frame, Object a, Object b) { 21 Object result; 22 /∗ ... compute result... ∗/ 23 return r e s u l t ; 24 } 25 26 @Override 27 protected boolean isTaggedWith(Class tag) { 28 return tag == StandardTags.StatementTag. class | | super .isTaggedWith(tag) ; 29 } 30 31 @Override 32 public LLVMSourceLocation getSourceLocation() { 33 return sourceLocation ; 34 } 35 36 @Override 37 public SourceSection getSourceSection() { 38 return sourceLocation != null ? sourceLocation.getSourceSection() : null ; 39 } Stepping & Breakpoints 45

40 }

Listing 5.1: Example implementation of a Truffle AST node in Sulong

5.2.1 Node Implementation

The example in Figure 5.1 shows an instrumented node with two children. In order to further explain how wrappers function and what their implementation looks like we define a class for it, NodeImpl, in Listing 5.1. The code is specific to Sulong and thus reflects its programming conventions and the representation of location information we described in the previous section. As a first detail, with the exception of those related to control flow, nodes in Sulong generally subclass LLVMExpressionNode. In most cases they also expect their children in the AST to be of that type. LLVMExpressionNode itself inherits from Node, Truffle’s baseclass for AST nodes.

Truffle provides a sophisticated DSL to simplify defining new nodes. The various annotations the abstract NodeImpl class uses are a testament to that. The framework’s DSL processor uses them to create a concrete class whose code conforms to an executable tree model. This includes the required instructions to track and execute the child nodes specified by the @NodeChild and @NodeChildren annotations. It also entails efficiently choosing and invoking the correct one of possibly several functions annotated with @Specialization based on what type of values the children produced. Developers can, of course, specify all of this manually as well. The implementation of e.g. an if or switch statement even necessitate it as these constructs require to execute only a select subset of the available children.

For Truffle to consider instrumenting a node its getSourceSection method must return a non-null object. In the case of Sulong this requires an available LLVMSourceLocation. This is by far not the case for all nodes though. A typical Truffle interpreter, e.g. TruffleRuby or FastR, represents a source-level statement by a subtree of the AST. The root of it represents the statement itself, but its descendants do not only correspond to subexpressions. They can be auxiliary nodes without a direct lexical location in the source file as well. The additional transformation of the source to LLVM IR only amplifies this for Sulong. LLVM optimization passes, especially those only included at higher optimization levels, often drop location information. Sulong does not build an LLVMSourceLocation for instructions without it. Stepping & Breakpoints 46

Truffle’s Node class provides the isTaggedWith method as a means for guest languages to categorize nodes. A tag in this context is an arbitrary class rather than a primitive. This allows individual interpreters as well as instruments and APIs in Truffle to define their own categories independently of each other. A Truffle language can decide which tags it supports in its nodes, however, it must list all of them as arguments of the ProvidedTags annotation on its concrete subclass of TruffleLanguage. The baseclass for Truffle nodes, Node, declares isTaggedWith such that it never returns true. NodeImpl overrides it to mark itself as tagged by the StatementTag. Defined by the instrumentation API this category identifies a node that represents a source-level statement.

1 /∗ ... package declaration and imports... ∗/ 2 @GeneratedBy(NodeImpl . class ) 3 public final class NodeImplWrapper implements InstrumentableFactory { 4 5 @Override 6 public WrapperNode createWrapper(NodeImpl delegateNode , ProbeNode probeNode) { 7 return new NodeImplWrapper0(delegateNode , probeNode) ; 8 } 9 10 @GeneratedBy(NodeImpl . class ) 11 private static final class NodeImplWrapper0 extends NodeImpl implements WrapperNode { 12 13 @Child private NodeImpl delegateNode; 14 @Child private ProbeNode probeNode; 15 16 private NodeImplWrapper0(NodeImpl delegateNode , ProbeNode probeNode) { 17 this .delegateNode = delegateNode; 18 this .probeNode = probeNode; 19 } 20 21 @Override 22 public NodeImpl getDelegateNode() { 23 return delegateNode ; 24 } 25 26 @Override 27 public ProbeNode getProbeNode() { 28 return probeNode ; 29 } 30 31 /∗ ... overloads for additional execute∗ methods... ∗/ 32 33 @Override 34 protected Object executeObject(VirtualFrame frame, Object a, Object b) { 35 Object returnValue; 36 for (;;){ 37 boolean wasOnReturnExecuted = false ; 38 t r y { 39 probeNode.onEnter(frame) ; 40 returnValue = delegateNode.executeObject(frame, a, b); Stepping & Breakpoints 47

41 wasOnReturnExecuted = true ; 42 probeNode.onReturnValue(frame , returnValue) ; 43 break ; 44 } catch (Throwable t) { 45 Object result = probeNode.onReturnExceptionalOrUnwind(frame, t , wasOnReturnExecuted) ; 46 i f ( r e s u l t == ProbeNode .UNWIND_ACTION_REENTER) { 47 continue ; 48 } else if ( r e s u l t != null ){ 49 returnValue = result; 50 break ; 51 } 52 throw t ; 53 } 54 } 55 return returnValue; 56 } 57 58 } 59 }

Listing 5.2: DSL-generated wrapper for the NodeImpl class in Listing 5.1

5.2.2 Wrapper Implementation

For Truffle to instrument a node, the framework requires its class or any of its superclasses to bear the @Instrumentable annotation. It declares the class of an abstract factory which is able to provide appropriate wrappers. In order to transparently replace an instrumented node in its parent in the AST, the intermediate must be of the same dynamic type. As a result, the instrumentation framework cannot just use one generic wrapper class. However, the DSL is able to generate these factory classes and the according wrapper implementations automatically. It does so if the annotation’s single argument references a non-existing class. Though developers can implement these manually as well, the Truffle project discourages them from doing so. In practice, most Truffle languages that are part of GraalVM, including Sulong, follow this suggestion. Though the general structure for a wrapper rarely changes, this strategy also has the advantage of requiring less manual maintenance. Listing 5.2 shows the wrapper factory class the Truffle DSL generated for our NodeImpl example. Per @Instrumentable’s requirement it implements the generic InstrumentableFactory interface and overrides its single defined method to wrap a node and attach the specified probe to it. Stepping & Breakpoints 48

The instrumentation API defines the WrapperNode interface which all wrapper nodes must implement. It defines getters for the two child nodes each wrapper must have, the instrumented node, which we refer to as the delegate, and the attached probe, an instance of the ProbeNode class. Additionally, the wrapper’s class must extend that of the delegate to provide its interface. This poses some restrictions on the implementation of instrumentable nodes. To begin with, their classes need to define either a parameterless constructor or a copy constructor visible to subclasses. Also, this strategy only applies to classes that are not final. It is suitable for abstract ones though. If a node implementation contains several internal fields or multiple child nodes, the wrapper class inherits them as well, causing unnecessary memory overhead at runtime. To avoid this, language implementers can instead place the @Instrumentable annotation on an abstract class that defines the delegate’s interface, rather than on the delegate’s type itself. The DSL is in theory always able to implement abstract methods by chaining them to the delegate node. In practice, earlier versions of Truffle contained a bug that prevented them from doing so. For this reason, Sulong used to use manually implemented wrappers for its nodes related to control flow. However, after Truffle developers fixed this flaw, it returned to exclusively auto-generated wrappers.

Typically, an @InstrumentableFactory contains the wrapper implementation as a static inner class. As NodeImplWrapper0 in Listing 5.2 shows, this is the DSL’s strategy as well. The inner structure of a wrapper implementation is always the same. Besides implementing WrapperN- ode’s methods, it overrides each function whose name begins with execute or that is annotated with @Specialization. A node’s parent in the AST calls these methods to obtain a value from it. Such an overridden method invokes the probe’s onEnter method before handing control over to the delegate by calling the intercepted method on it. This is where a debugger has the opportunity to suspend execution before the source-level statement that the instrumented node represents can be executed. After the delegate returns with a value the method provides it to the probe by calling its onReturnValue function. The instrumentation framework also supports stack unwinding. It implements this feature by throwing an appropriate exception. Wrapped nodes propagate it up in the execution stack, popping off frames in the process, until a probe stops this. Due to this and to support exceptional return from a delegate in general the inter- cepted methods encapsulate the regular calls to the delegate and the probe in a try-block. The according catch-block relays the exception to the probe via its onReturnExceptionalOrUnwind function. Its result indicates whether stack unwinding has halted and execution should enter the delegate again, an exception should be propagated or a value returned instead. Stepping & Breakpoints 49

The ProbeNode class represents an event sink for execution events from an instrumented node. It relays them to its internal chain of registered callbacks. Truffle instruments, e.g. a debugger, can dynamically attach and detach them as needed. In uncompiled code this event propagation and evaluation causes dramatic runtime overhead as typically most nodes of a function will be instrumented. The situation is different in compiled code however. Truffle developers specifically design the default wrapper code, the ProbeNode class as well as any callbacks they may register to it for partial evaluation. In the case of debugging this can go so far that the Graal compiler is able to produce machine code for the instrumented AST that does not even contain the wrappers on the fast-path anymore. As a result, debuggers based on Truffle instrumentation can achieve a very low runtime overhead.

5.3 Dynamically Halting Execution in an Instrumented AST

The Truffle instrumentation API defines three distinct tags to identify certain constructs of the guest language program in the Truffle AST. Language implementations need to provide them to enable proper source-level instrumentation. These tags are defined as static inner classes of the StandardTags class. Since they all have private constructors, they are effectively uninstantiable.

StatementTag: This tag identifies a node that represents a statement in the source program. As of Truffle’s 0.32 release the framework does not support the finer expression-level granu- larity. For Sulong, all nodes currently provide this tag. This implementation strategy works correctly as Truffle only considers nodes for instrumentation that make a non-null SourceSec- tion available. Only nodes that represent top-level LLVM IR instructions with attached debug information can fulfill this requirement. On the downside, what Sulong declares to be state- ments are actually subexpressions. We already explained in Section 5.1.2 that this is a direct consequence of the translation to LLVM IR.

CallTag: This tag identifies a node which performs a source-level function call. In Sulong only the node representations of the call and invoke instructions provide it. Fortunately, Stepping & Breakpoints 50

LLVM optimization passes usually preserve debug information for those records and, in turn, Truffle’s ability to instrument the corresponding nodes.

RootTag: This tag marks nodes which represent the body of a function. The instrumentation API expects that only one node in a function AST provides it. This should be the same node that copies the arguments the function is called with to the frame. Commonly, it is the only child of the function’s RootNode. In Sulong, this is also the node that performs basic block dispatch. Since it does not have an equivalent in LLVM IR, the interpreter compensates for the lack of explicit debug information by simply copying the RootNode’s LLVMSourceLocation.

The presence of these three tags on the nodes in a Truffle AST provides the debugging frame- work with enough information about the guest language program’s original control flow to single-step through it. This includes the different stepping strategies. To step into a function call, the debugging framework suspends execution at the first statement it encounters after resuming from a suspended state. If the guest language program stopped immediately before a function call, i.e. at a node that provides the CallTag, this will be the first statement in the called function. Otherwise, the behavior is effectively the same as only stepping over the cur- rent statement. To explicitly do just that, the debugging framework only halts guest language execution again at statements that it encounters at the same or at a lower stack depth than where it resumes from. Limiting the selection criteria instead to only the function itself would not be sufficient as that strategy fails to support recursive calls. To step out of the current guest language function, the debugging framework halts execution of the guest language program at the first statement after a node with the RootTag at the same stack depth as the node from which it resumed. This requires the previously mentioned structure in which a node that a guest language tags with RootTag must be the only child of the function’s RootNode.

Truffle’s debugging API also defines a tag of its own. AlwaysHalt, an inner class of the aptly named DebuggerTags, describes a node at which a debugger should always suspend guest- language execution. One can think of this as a programmatically defined, unconditional, al- ways enabled breakpoint. Sulong uses this tag only on the node that implements the “int $3 ” assembly instruction. This software interrupt is specifically intended to be interpreted as a breakpoint by a debugger but not to cause any side-effects otherwise. Sulong’s implementa- tion exhibits exactly that behavior as the corresponding node’s execute method performs no operations. It only has a function when it is being instrumented. Stepping & Breakpoints 51

Truffle’s debugging framework supports setting breakpoints at arbitrary lines in a guest lan- guage program’s source files. Since Sulong considers, for reasons we already explained, each expression of a source program as a distinct statement, this causes breakpoints to usually trig- ger several times per line. Though the API also allows frontends to set breakpoints based on a SourceSection rather than a line number, they usually do not make use of this functionality, though it enables column-based breakpoints. This feature would require a debugger to know the exact character length of the designated subexpression as the Truffle AST contains it as well. Determining this in a language-agnostic way seems a daunting task. A debugger frontend specific to Sulong could make use of the fact that it is always 0 for this interpreter though.

In most Truffle language interpreters the RootNode each function AST provides a SourceSection that lexically contains all of those held by its statement nodes. For the debugging framework this assumption has even become a requirement. It silently refuses to set and correctly trigger breakpoints for nodes that do not fulfill this condition. As we already mentioned, LLVM debug information gives no direct information about where a lexical scope ends. For statements, Sulong can simply set the character length to 0 per default but the SourceSection for a RootNode requires a different strategy. Inspecting the contained nodes to compute a minimal lexical region would require additional parsing overhead. Since debuggers do only require the starting position to be semantically correct in any case, Sulong just assumes that each function’s lexical scope extends to the end of the source file. This works for statements defined within the function itself. However, many low-level languages, e.g. those of the C family and, in turn, LLVM IR, support preprocessor macros which allow developers to define them at arbitrary locations in a source file. What is more, LLVM frontends can apply inlining as part of optimization passes. To support such constructs, Sulong derives the corresponding nodes’ LLVMSourceLocation from where the instructions are inlined at rather than where they are defined. This mitigation strategy has the significant disadvantage of dropping column information for the individual expressions. On the upside, it keeps debuggers from halting at a location where they cannot set a breakpoint at. A better solution for this issue would require support from Truffle itself. Source-Level Symbol Inspection 52

Chapter 6

Source-Level Symbol Inspection

In this chapter we explain how Sulong makes the source-level program state available to debugger frontends. We begin by introducing Sulong’s representation of symbol information and follow this with an overview of how the Truffle debugging framework enables interpreters to provide a language-specific representation of their runtime state to a debugger frontend. Lastly, we go into detail as to how Sulong restores the source-level state of local and global variables from LLVM IRs runtime values.

6.1 Symbol Information

Sulong represents source-level symbol information at runtime in highly compact data structures. Their organization and extent differs from LLVM IR’s representation to better suite the specific needs of Truffle’s debugging framework. As Figure 6.1 shows, this leads to a composition that makes plentiful use of abstraction to avoid unnecessary fields in individual elements. Its main artifacts are LLVMSourceSymbol, LLVMSourceLocation and LLVMSourceType.

An LLVMSourceSymbol stores a source-level symbol as a name, type and location. It specializes into Static and Dynamic instances. While this distinction is not strictly required, it does enable Sulong to apply different visibility styles. As an example, the frontend can display the value of a function-local static symbol even at statements prior to its declaration to express its persistence beyond a particular method execution. Source-Level Symbol Inspection 53

Figure 6.1: Source-Level Symbol and Scope Information in Sulong

We already introduced the LLVMSourceLocation class in Chapter5. It represents a location in a source-file as either a Truffle SourceSection or, if the file is not accessible, a combination of filename and line/column number. However, as Figure 6.1 shows, there is a second level of distinction to it. The format we already introduced only applies to statements and symbols. A separate specialization pertains to named scopes. In contrast to LLVM metadata, this represen- tation references the contained symbols directly. However, each kind of LLVMSourceLocation still retains a direct link to the parent scope as well. In combination, these reference chains allow Sulong to quickly determine which source-level symbols are accessible at a particular statement. Function scopes bear the source-level name of the method they represent. In order to provide such unmangled names in stack-traces, Sulong returns these strings in the getName methods of its RootNode instances.

As we already described, LLVM tries to encode diverse kinds of source-level type information in only four different kinds of metadata nodes which ultimately leads to multiple optional entries in the according records. The frontend still needs to emit them to bitcode files, regardless of their actual use. Sulong avoids this memory overhead by defining multiple specializations, each with only the appropriate fields, for a common baseclass to represent source-level type Source-Level Symbol Inspection 54

LLVM IR type Tag LLVMSourceType DI_Basic_Type Primitive DI_Composite_Type Array Array DI_Composite_Type Vector Array DI_Composite_Type Class Struct DI_Composite_Type Struct Struct DI_Composite_Type Union Struct DI_Composite_Type Enumeration Enum DI_Derived_Type Pointer Pointer DI_Derived_Type Reference Pointer DI_Derived_Type Constant Decorator DI_Derived_Type Volatile Decorator DI_Derived_Type Inheritance Member DI_Derived_Type Alias Decorator DI_Derived_Type Member (dynamic) Member DI_Derived_Type Member (static) StaticMember DI_Derived_Type Others None (dropped) DI_Subroutine Decorator null Void

Table 6.1: Mapping LLVM source-level type nodes to LLVMSourceType information. LLVMSourceType itself contains the common attributes of any type, those being a name to describe it, its size and alignment in memory as well as its offset within an according value. The baseclass also defines methods to access them. While this structure is ultimately apt for efficient storage, it still requires interfaces that interact with these objects to manually distinguish between their concrete classes.

Table 6.1 defines a mapping between the LLVM IR source-level type nodes and Sulong’s LLVM- SourceType. Figure 6.1 lists all these specializations as well. As most of them do not encode any additional information compared to what we already explained in Section 4.2.3, we refrain from describing them any further. The Decorator subclass does not have an equivalent in LLVM IR though. Like some kinds of DI_Derived_Type it enriches a basetype with additional information. However, this type does not specify a kind of qualification. It instead stores an explicit name for the basetype. Sulong does not use it for named members though. Their cor- responding LLVMSourceType stores its declaration site and, to support bitfields, its actual size in memory. Also, Sulong uses an explicit singleton instance of LLVMSourceType, rather than a null value, to represent the Void type. Readers may notice the lack of a specific function type. Since values of this kind would not have any displayable members, Sulong uses a Decorator to apply the type’s name, including parameters and return type, to a Void type. Source-Level Symbol Inspection 55

Figure 6.2: Truffle debugging API access model

6.2 Symbol Inspection in Truffle

The Truffle debugging API defines an abstract representation of source-level program state which it provides to debugger frontends. It consists of classes for nested scopes and correspond- ing symbol entries. Once it has halted execution of a guest language function, the framework calls the Truffle language implementation that produced the corresponding AST to provide concrete values for this structure. Language implementers can overwrite the corresponding methods to return a proper view of source-level execution state rather than Truffle’s default strategy of presenting the entries in the stack-frame. Figure 6.2 reflects the general structure of this model.

When the debugging framework suspends guest language program execution at a node, it calls the findLocalScopes function on the TruffleLanguage instance that originally produced it to retrieve the scope hierarchy of the statement it represents. This includes values for all symbols actually visible at that location. The default implementation of this method returns the entries on the guest language function’s stack-frame as the only local scope. However, a Truffle interpreter can override it to provide a more detailed and accurate representation. On request, the debugging framework also calls findLocalScopes for each instrumented node with the CallTag on the execution stack. This allows a frontend to inspect all entries on the source- level call stack. As this function is never called on the fast-path, its implementation has no impact on execution performance. Source-Level Symbol Inspection 56

Truffle’s debugging framework defines the Scope class to represent the runtime state of a single source-level scope. While it also stores a name and optionally an associated node, it is ultimately a wrapper around a TruffleObject. These delegates need to support the KEYS message which the debugger frontend sends to retrieve the scope entries’ names. It then uses such a String identifier as argument to a READ message to the same object in order to obtain the symbol’s current value. Truffle does not provide a default implementation for these delegates. Individual languages need to instead define their own in order to build the sequence of Scope instances they return in findLocalScopes.

In addition to the statement node, findLocalScopes receives its corresponding language context and the materialized stack-frame of its execution as arguments. Implementations can use them to compute or access the scope entries’ runtime values. In principle, these can be of any type, even implicitly wrapped Java primitives. Debugger frontends display them as their self-defined toString representation. However, the debugging framework can only access and display the structure of complex objects if they implement the TruffleObject interface at all levels. As with the scope delegates we described earlier, Truffle accesses their fields via dynamic messages.

The debugging framework passes the runtime values of scope entries to the findMetaObject method to obtain a language-defined String-representation of its type. In Sulong, this is the name of the associated LLVMSourceType. If the method returns null, Truffle uses an empty String instead. Unfortunately, the debugging framework calls this function on the TruffleLan- guage instance of the currently inspected statement or call, rather than on that of the guest language that actually produced the value. Truffle does not provide a way to access the appro- priate target either. As a result, debugger frontends fail to show type information for interop values.

TruffleLanguage further defines the findSourceLocation method to retrieve the SourceSection of an arbitrary object. The debugging framework uses it to locate the declaration site of source- level values, including members of complex objects, as well as their types. This function may return null or a valid SourceSection. Source-Level Symbol Inspection 57

1 public interface LLVMDebugValue { 2 String describeValue( long b i t O f f s e t , i n t b i t S i z e ) ; 3 boolean canRead ( long b i t O f f s e t , i n t b i t s ) ; 4 Object readBoolean( long bitOffset); 5 Object readFloat( long bitOffset); 6 Object readDouble( long bitOffset); 7 Object read80BitFloat( long bitOffset); 8 Object readAddress( long bitOffset); 9 Object readUnknown( long b i t O f f s e t , i n t b i t S i z e ) ; 10 Object readBigInteger( long b i t O f f s e t , i n t b i t S i z e , boolean signed ) ; 11 LLVMDebugValue dereferencePointer ( long bitOffset); 12 boolean isInteropValue() ; 13 Object asInteropValue() ; 14 } Listing 6.1: Definition of Sulong’s LLVMDebugValue interface

6.3 Symbol Inspection in Sulong

When the debugging framework calls findLocalScopes as we discussed previously, Sulong tra- verses the LLVMSourceLocation scope hierarchy of the node under inspection and converts it into a list of Truffle Scope objects. However, this is not a bijective mapping. Sulong combines all function-local scopes into one. Looking at graphical debuggers in popular IDEs, e.g. Netbeans, this seems to be a popular strategy as it leads to a leaner display without loosing relevant in- formation. Any visibility problems would become obvious in the compiler frontend in any case. Sulong also retains only those non-static scope entries whose declaration lexically precedes the node’s location, i.e. those that are actually visible. If two function-local LLVMSourceLocation scopes contain symbols with the same name, the interpreter prefers the entry in the lower one unless the shadowing local variable is not declared at that location.

The values Sulong provides to the debugging framework as scope entries use two layers of abstraction. The lower one, in the following we refer to it as the value layer, hides their low-level runtime representation and storage location behind a common interface. The representation layer then applies Sulong’s runtime debug information to it to provide a source-level view on the value.

6.3.1 Value Layer

As we already mentioned in Section 4.3, source-level symbols can receive their value from di- verse locations. Some reside in memory, others in SSA-registers. Some exist only as constants Source-Level Symbol Inspection 58 in debug information while others frequently change their value at runtime. LLVM optimiza- tion passes can even split source-level symbols into multiple parts, each of which they track separately. What is more, Sulong adds another level of diversity as it supports values of other Truffle languages and generally represents its own IR-level ones with Java primitives or custom objects. To mitigate this issue, it defines a common abstraction for values in the form of the LLVMDebugValue interface like shown in Listing 6.1. Each implementation abstracts a specific value to a bitpattern and provides random access within it.

As Listing 6.1 shows, LLVMDebugValue defines methods to derive common primitive inter- pretations from any region within the bitpattern it wraps. This includes signed and unsigned integers and characters of arbitrary bitwidth, single- and double-precision floating point values as well as addresses. LLVM IR also supports 80-bit wide floating point values with its x86fp80 type. To allow it to display symbols for which compilers emit values of this type, LLVMDe- bugValue also provides an accessor for it. Furthermore, the interface provides a method to dereference a pointer within its bitpattern and return the result as another LLVMDebugValue. This is only possible in certain cases though and callers need to be certain the address is valid.

To illustrate the concept of LLVMDebugValue with an example, take the C language struc- tured type struct {int a; float b;}. At higher optimization levels LLVM often represents such a composite value as a single 64-bit integer. In turn, Sulong stores it as a Java long. The cor- responding LLVMSourceType describes how to interpret its bitpattern. The LLVMDebugValue implementation for long constants lets users conveniently retrieve member a in the form of a 32- bit signed integer in two’s complement encoding at offset 0 and member b as a single-precision floating point value at offset 32.

6.3.1.1 Representation Abstraction

Sulong implements LLVMDebugValue for every kind of value it uses at runtime. Besides Java primitives, these include classes for 80-bit floats, variable-width integers, function pointers, memory addresses and various kinds of vectors. Since LLVM IR debug information uses regular value list entries to encode constants, Sulong already has a scheme to convert them to these runtime representations and thus avoids requiring an additional abstraction for this case. Source-Level Symbol Inspection 59

Values from other languages in the form of TruffleObject instances usually do not provide a defined memory representation. As a result, LLVMDebugValue cannot apply its usual bitpat- tern interpretation. The representation layer accounts for this by explicitly displaying this case to the user in combination with the unindexed value. Some TruffleObject implementations can store their content into native memory and provide a pointer to it, but this often entails changing their internal state. As symbol inspection in a debugger should take care to avoid sideeffects, LLVMDebugValue does not use this feature.

As we mentioned previously, the llvm.dbg.value intrinsic supports tracking defined parts of local variables seperately. For this case, Sulong defines a specific LLVMDebugValue implemen- tation that can aggregate multiple instances of this interface. When a caller tries to access a value at a particular offset into it, this class determines which of its fields represents that location and relays the call to it with an adapted offset. The LLVMDebugValue specialization for Sulong’s internal vector classes follows the same approach.

6.3.1.2 Location Abstraction

Source-level symbols can reside in native memory. This includes global variables, object point- ers and local variables that llvm.dbg.declare describes. To support this case, Sulong defines an implementation of LLVMDebugValue that dereferences a pointer to locate and access the bitstream it wraps. This relies on Java’s Unsafe class and, thus, using it with bad pointers can result in a segmentation fault. Sulong requires LLVM debug information to be accurate to avoid these problems. Concretely, it assumes that all values that are of source-level refer- ence types or represent artificial object pointers are always safe to dereference unless they are null. Sulong relies on LLVM frontends to ensure that this assumption is accurate. Although at least in C++ it is possible to create invalid references, the corresponding language standard treats this as undefined behavior and compilers should detect it. We believe that the enhanced usability this assumption allows for justifies the risk. On the other hand, pointers to local vari- ables that the llvm.dbg.declare intrinsic describes are provably safe to dereference as frontends necessarily emit the corresponding stack allocation. Other debuggers, e.g. GDB or LLDB are usually implemented in low-level languages. They can use signal handlers to safely recover from segmentation faults. Sulong cannot use the same strategy though, as the JVM actively prevents it from handling the corresponding interrupt. Source-Level Symbol Inspection 60

LLVM treats IR-level global variables like pointers and assumes frontends will allocate native memory for each of them. Sulong, however, requires a more sophisticated storage scheme that also supports Truffle interop values without a defined native representation. As a solution, the interpreter stores global variables in the language context and uses special descriptors rather than pointers to identify them. However, this approach fails once a program needs to actually retrieve the address of the global variable. In this case, Sulong falls back to a different storage scheme that does allocate native memory and accepts the limitations this poses. The LLVMDebugValue implementation for global variables wraps both the language context as well as Sulong’s descriptor for the IR-level symbol. It determines the current storage strategy whenever one of its methods is called and builds the appropriate specialization for either the current value within the managed container or the allocated memory region.

6.3.2 Representation Layer

Sulong uses different implementations of its abstract LLVMDebugObject class to represent the values of source-level symbols. Each of them specializes to a certain kind of LLVMSourceType to provide a language-specific view of a generic LLVMDebugValue. This includes overrid- ing toString to return a source-level representation of the actual value. For the same reason LLVMDebugObject also implements the TruffleObject interface and supports the KEYS and READ messages to optionally provide access to any fields the corresponding type defines.

In the aforementioned findLocalScopes method the interpreter uses a factory method in this class to create appropriate instances for a specific type on demand. It then supplies them as entries to a Truffle Scope. Since the framework calls both findMetaObject and findSourceLocation only on a symbol’s value rather than on its name, LLVMDebugObject needs to store both its LLVMSourceLocation and its LLVMSourceType for access in these methods. This is due to the restriction that, in the debugging framework, names must be Java Strings rather than arbitrary objects with a toString representation. Sulong does not just store an LLVMSourceSymbol in LLVMDebugObject since it also instantiates this class for a symbol’s members. For the same reason it stores an offset into its LLVMDebugValue instance at which the field it represents begins. Conveniently, its type already encodes its length. Source-Level Symbol Inspection 61

Primitive: This implementation of LLVMDebugObject uses the functions of LLVMDebug- Value to retrieve the appropriate interpretation of its wrapped value based on the kind of primitive type. The factory method instantiates this subclass for the Primitive subclass of LLVMSourceType or Decorator instances that wrap it.

Structured: A Structured source-level object can provide named fields. Upon access to one of them this class instantiates another LLVMDebugObject with its offset. In its toString method, this subclass optionally provides the memory address of its value to the user.

Static Member: As Figure 6.1 shows, static members of source-level structured types also have their own subclass of LLVMSourceType. However, contrary to this implementation of LLVMDebugObject it only represents a single named member with an attached LLVMDebug- Value. Sulong uses the field’s static value and its type to instantiate another subclass of LLVMDebugObject. This one instead aggregates all static fields of a parent type into a group to separate them from dynamic ones and focus the user’s attention on the latter as they are typically more interesting.

Array: Similar to Structured, this class provides fields. While internally they are indexed numerically, this subclass simply wraps the corresponding integers into Java Strings to adhere to the debugging framework’s expectation of purely name-based access to members of scope objects. The toString method of this subclass expects an array to be allocated in memory and simply returns its address per default. However, Sulong also uses Array for LLVMDebugValue instances based on its internal classes for IR-level vectors. These do not have a memory address. For such a case, toString returns “” while the object still provides access to the values of its fields. The only other exception to the String-representation are character arrays. For the users’ convenience, toString returns not only the string’s address but also its content.

Enum: This subclass does not provide any members. Instead, it interprets its LLVMDebug- Value instance as an integer. It then retrieves the label corresponding to that number from its LLVMSourceType enumeration type and returns it in its toString representation. Source-Level Symbol Inspection 62

Pointer: This subclass represents all values of Pointer type. This also includes source-level reference types. In general, it does not provide members and simply interprets its LLVMDe- bugValue as a memory address to display to the user. However, for reference types and object pointers, this subclass actively dereferences the address to instead display the value it refers to as well as its members if it has any. This constitutes the only use for the dereference- Pointer function that we can see in Listing 6.1. Pointer also wraps the resulting value in a new LLVMDebugObject with its own pointer type’s basetype upon access.

Interop: This subclass of LLVMDebugObject wraps a value from another Truffle guest lan- guage. It does not attempt any interpretation of it, but merely displays it as the string “” to indicate the symbol’s state. We already explained in Section 6.3.1.1 why we cannot do better. However, this LLVMDebugObject provides the foreign value as its single member. Users can still inspect its internal structure since it is necessarily an instance of TruffleObject. For reasons we already stated previously in this chapter though, the debugger is unable to display its source-level type.

6.3.3 Value Tracking

Sulong tracks source-level global variables, as well as static local ones, in the language con- text. It maps their corresponding LLVMSourceSymbol to an LLVMDebugValue instance whose concrete class depends on the IR-level value. In most cases, this will be an LLVM IR global variable, however, constants are also possible. In any case, tracking source-level static variables never causes runtime overhead for Sulong. Even a change to an IR-level global variable would not invalidate Sulong’s descriptor for it. Therefore, the LLVMDebugValue implementation that wraps it is essentially a constant as well.

LLVM inserts a call to the llvm.dbg.declare intrinsic for each local variable of a function that spends its entire lifetime at a particular region in memory. Unless its first argument is explicitly undefined, it references the LLVM IR instruction that produced the pointer. Since Sulong creates a slot on the Truffle stack-frame for each SSA-value, it can determine already in the parser which of them will contain the pointer at runtime. It uses the language context to store this information and map the according LLVMSourceSymbol to it. When the debugging framework calls findLocalScopes as we discussed previously, Sulong traverses the LLVMSource- Source-Level Symbol Inspection 63

Location scope hierarchy of a suspended node and computes the current value of each symbol. For non-static ones it queries the context object whether it contains such a declaration. If so, Sulong obtains the actual memory address from the stack-frame and uses it to build the corresponding LLVMDebugValue.

LLVM inserts calls to the llvm.dbg.value intrinsic wherever the value of a local variable changes in a function. It tracks symbols as they switch between multiple SSA-registers, stack as well as heap memory and even constants. Unlike llvm.dbg.declare, the containing function’s control flow at runtime determines which call to llvm.dbg.value describes a local variable at a particular statement. This requires Sulong to implement the intrinsic and emit calls to it into the Truffle AST. The corresponding node stores the symbol’s new value, together with a factory object to wrap it into an LLVMDebugValue, onto its stack-frame. The implementation of this intrinsic also considers the special case where LLVM tracks individual parts of a local variable separately. In this case, Sulong reads the aggregated LLVMDebugValue which is already present on the stack-frame and relays the partial value as well as the factory object to it. For an identifier, the target slot references the LLVMSourceSymbol that describes the local variable. This allows Sulong to identify all tracked symbol in findLocalScopes by analyzing the provided stack-frame.

Tracking values of source-level local variables with llvm.dbg.value has a significant impact on runtime performance. At the very least it requires additional copy operations on the stack- frame. What is more, this tracking even includes symbols that LLVM had lowered to constants. Sulong tries to mitigate this issue by handling effectively final local variables, i.e. those for which only a single call to llvm.dbg.value exists, in a more efficient way. Depending on the corresponding IR-level value, it stores the symbol either as a constant to the language context, like it does for static symbols, or as a reference to a value in a specific frame-slot, similar to its implementation of llvm.dbg.declare. However, performance is still far from optimal. In consequence, Sulong does not enable value tracking per default. Instead, its users need to explicitly set the –llvm.enableLVI argument to true when launching the interpreter.

Without applying any LLVM transformation passes, both Clang and DragonEgg describe local variables exclusively with llvm.dbg.declare. What is more, frontends always emit source-level static or global variables as IR-level global variables. Since Sulong can effectively avoid dynami- cally tracking either kind of these symbols at runtime, debugging unoptimized bitcode files does not exhibit performance overhead beyond the low impact of Truffle’s AST instrumentation. Case Study 64

Chapter 7

Case Study

This case study demonstrates how debugging in Sulong works at the hand of a Ruby native extension implemented in C++. It computes the result of an arithmetic expression in postfix notation. The program uses Ruby code only to parse the input expression and to pass it to a C++ class that actually evaluates it. To execute and debug the case study we will use TruffleRuby and Sulong in GraalVM 0.32 and its integrated connector to the Chrome Devtools.

In this case study we demonstrate debugging C++ code that we implemented as a native extension for a Ruby program. It consists of a calculator for arithmetic expressions in postfix notation. The program calc.rb merely parses the input and invokes our actual demo code. We will inspect our calculator in the Chrome Devtools as it evaluates the expression “3 4 + =”.

To begin our case study we use the command “$GRAALVM/bin/ruby –llvm.enableLVI=true –inspect=true calc.rb 3 4 + =” to execute calc.rb on TruffleRuby in GraalVM. That Ruby interpreter automatically loads Sulong, the llvm Truffle language, to execute native extensions compiled to LLVM IR. To enable source-level symbol inspection, or more precisely value track- ing, we specify –llvm.enableLVI=true. The second option, –inspect=true, instructs GraalVM to enable the Chrome Inspector debugger tool. Before it begins executing the guest language program, this also forces Truffle to instrument all AST nodes for which this is possible.

Upon launch, Chrome Inspector prints a URL to stdout to launch the Chrome Devtools in a supported browser. It then blocks the interpreter until the user enters the link. Once that Case Study 65

happens, CI’s default behavior is to suspend guest language execution on the first source-level statement it encounters.

7.1 Program Description

The C++ file calc.cpp, as shown in Listing 7.1, defines a stack machine for simple integer arithmetic calculations in the form of the Calculator class. Its internal members include a single-linked list whose nodes are made of C-style structs to represent a number stack and a counter to track the number of elements on it. Calculator defines instance methods to push numbers onto and pop them off its stack. The class further supports binary addition, subtraction, multiplication and division as well as an auxiliary command to print the current content of its stack. These operations are encoded as an enum. Users can pass its values as argument to the doOp instance method which also receives a pointer to another method for printing stack entries. Lastly, the class defines the getResult method to pop off and return the top of the stack after the expression has been evaluated in its entirety.

The C++ file calcExt.cpp shown in Listing 7.2 wraps an instance of Calculator in a Ruby module with the name CalcExt. The Ruby program calc.rb, as shown in Listing 7.3 imports this native extension and uses it to compute the result of an arithmetic expression in postfix notation which it receives as arguments. It also defines a lambda for printing entries of the calculator’s number stack in doOp.

1 #include < s t d l i b . h> 2 #include < s t d i o . h> 3 4 /∗ Entry in the value stack ∗/ 5 struct StackEntry { 6 i n t num; 7 struct StackEntry ∗next ; 8 }; 9 10 /∗ Supported operations ∗/ 11 typedef enum Op { ADD = 0 , SUB = 1 , MUL = 2 , DIV = 3 , PRINT = 4 } Op; 12 13 class Calculator { 14 private : 15 i n t stackSize ; 16 struct StackEntry ∗top ; 17 18 /∗ Popa value from the stack ∗/ 19 i n t pop ( ) { Case Study 66

20 i f ( this −>stackSize < 1) { 21 p r i n t f ("Error: Popping from empty stack!\n"); 22 abort ( ) ; 23 } 24 struct StackEntry ∗ topEntry = this −>top ; 25 i n t val = topEntry −>num; 26 this −>top = topEntry −>next ; 27 free(topEntry); 28 this −>stackSize −= 1; 29 return v a l ; 30 } 31 32 public : 33 Calculator() : stackSize(0), top(0) { 34 }; 35 36 /∗ Pusha number onto the stack ∗/ 37 void push ( i n t num) { 38 struct StackEntry ∗newEntry ; 39 newEntry = ( struct StackEntry ∗) c a l l o c (1 , sizeof ( struct StackEntry)); 40 newEntry−>num = num; 41 newEntry−>next = this −>top ; 42 this −>top = newEntry; 43 this −>stackSize += 1; 44 }; 45 46 /∗ Applya binary operation to the top two stack elements and push the result, 47 ∗ or print the current content of the stack top−down ∗/ 48 void doOp(Op &op , void (∗ printStackEntry)( i n t )){ 49 i f ( op == PRINT ) { 50 for ( struct StackEntry ∗ e nt ry = this −>top; entry ; entry = entry −>next ) { 51 const int v a l = entry −>num; 52 printStackEntry(val);/ ∗ | Inspection Point| ∗/ 53 } 54 return ; 55 } 56 const int rhs = pop ( ) ; 57 const int l h s = pop ( ) ; 58 i n t r e s u l t = 0; 59 switch ( op ) { 60 case ADD: result = lhs + rhs; break ; 61 case SUB: result = lhs − rhs ; break ; 62 case MUL: result = lhs ∗ rhs ; break ; 63 case DIV: result = lhs / rhs; break ; 64 default : p r i n t f ("Unknown Op: %d\n", op); abort(); break ; 65 } 66 push(result); 67 }; 68 69 /∗ Return the only value on the stack as result ∗/ 70 i n t getResult() { 71 i f ( this −>stackSize != 1) { 72 p r i n t f ("Error: Stack contains %d elements!\n", this −>stackSize) ; 73 abort ( ) ; Case Study 67

74 } 75 return pop ( ) ; 76 }; 77 };

Listing 7.1: C++ code file (calc.cpp) that defines a stack-based calculator

1 #include "ruby.h" 2 #include "calc.cpp" 3 4 /∗ Backing Calculator ∗/ 5 C a l c u l a t o r ∗Calc = new Calculator() ; 6 7 /∗ Ruby method stubs ∗/ 8 extern "C" VALUE method_pushNumber(VALUE self , i n t num) { 9 Calc−>push(num) ; 10 return (VALUE) 0; 11 } 12 extern "C" VALUE method_doOp(VALUE self , Op op, void (∗ printStackEntry)( i n t )){ 13 Calc−>/ ∗ | Stepping Point1| ∗/ 14 doOp(op, printStackEntry);/ ∗ | Stepping Point2| ∗/ 15 return 0;/ ∗ | Stepping Point3| ∗/ 16 } 17 extern "C" VALUE method_getResult(VALUE self) { 18 i n t result = Calc−>getResult() ; 19 return INT2NUM( r e s u l t ) ; 20 } 21 22 /∗ setup Ruby module ∗/ 23 extern "C" void Init_CalcExt() { 24 VALUE CalcExt = rb_define_module("CalcExt"); 25 26 /∗ i n i t i a l i z e module methods ∗/ 27 rb_define_method(CalcExt ,"pushNumber" , (VALUE ( ∗ ) (...) ) method_pushNumber, 1); 28 rb_define_method(CalcExt ,"doOp" , (VALUE ( ∗ ) (...) ) method_doOp, 2); 29 rb_define_method(CalcExt ,"getResult" , (VALUE ( ∗ ) (...) ) method_getResult, 0); 30 31 /∗ i n i t i a l i z e module fields ∗/ 32 rb_define_const(CalcExt ,"OP_ADD" , INT2NUM(Op : : ADD) ) ; 33 rb_define_const(CalcExt ,"OP_SUB" , INT2NUM(Op : : SUB) ) ; 34 rb_define_const(CalcExt ,"OP_MUL" , INT2NUM(Op : : MUL) ) ; 35 rb_define_const(CalcExt ,"OP_DIV" , INT2NUM(Op : : DIV ) ) ; 36 rb_define_const(CalcExt ,"OP_PRINT" , INT2NUM(Op : : PRINT ) ) ; 37 }

Listing 7.2: Ruby C++ extension (calcExt.cpp).

1 r e q u i r e"./ CalcExt.su" 2 include CalcExt 3 4 stack_entry_printer = −> (num){ puts’ −> ’ + num.to_s } 5 for operand in ARGV 6 case operand Case Study 68

7 when ’+’ 8 CalcExt ::doOp(CalcExt ::OP_ADD, stack_entry_printer) 9 when ’− ’ 10 CalcExt ::doOp(CalcExt ::OP_SUB, stack_entry_printer) 11 when ’ ∗ ’ 12 CalcExt ::doOp(CalcExt ::OP_MUL, stack_entry_printer) 13 when ’/’ 14 CalcExt ::doOp(CalcExt ::OP_DIV, stack_entry_printer) 15 when ’=’ 16 puts’Stack Content (top −down):’ 17 CalcExt ::doOp(CalcExt ::OP_PRINT, stack_entry_printer)#| Point1| 18 else 19 num = Integer(operand) 20 CalcExt :: pushNumber(num) 21 end 22 end 23 result = CalcExt::getResult() 24 puts’The r e s u l t i s: ’ + result.to_s

Listing 7.3: Ruby program (calc.rb) to compute the value of an arithmetic expression in postfix notation

In order to execute the program of this case study one first needs to compile the C++ extension it uses. Listing 7.4 defines another Ruby program, extconf.rb, which generates a Unix Makefile for this purpose. When created by TruffleRuby that script invokes Clang to compile the C++ files into LLVM bitcode files that include debug information, calls LLVM’s opt tool to apply the mem2reg, always-inline and constprop optimization passes on them and finally assembles them to a single *.su file for Sulong to execute. As Listing 7.3 shows in line one, calc.rb loads this file prior to importing the CalcExt module. GraalVM’s ruby executable detects this filetype and uses Sulong to parse it.

1 r e q u i r e’mkmf’ 2 extension_name =’CalcExt’ 3 dir_config (extension_name) 4 $CPPFLAGS =’ −std=c++11’ 5 create_makefile(extension_name)

Listing 7.4: Ruby program (extconf.rb) to create a Makefile for the native extension CalcExt

This program is suitable for demonstrating Sulong’s debugging features as it makes use of core C++ language features. What is more, this case study demonstrates the possibility of debugging both a Ruby program as well as a native extension at source-level in the same debugger. Case Study 69

7.2 Analysis

In this section we analyze how Sulong enables various debugging features. First we describe Sulong’s Truffle AST for the method_doOp function and how it enables source-level stepping. Then we take a closer look at how the interpreter applies LLVM IR debug information to facilitate language-specific display of the guest language program’s runtime state.

After launching calc.rb as we described previously and connecting the Chrome Devtools debugger, the user can step through the Ruby program. The first statement in it (at line 1 in calc.rb) loads our C++ extension and executes its initialization function. As this constitutes a function call the user can step into it and inspect the execution of the Init_CalcExt function which is defined in calcExt.cpp. In traditional setups developers can debug either the Ruby code or the C/C++ code at source-level, but not both in the same frontend at the same time. Truffle debugging tools, however, do support this scheme.

7.2.1 Stepping

The function method_doOp acts as a proxy between the doOp functions on the CalcExt Ruby module and a Calculator instance. For the input expression we selected, “3 4 + =”, the program calls it twice, once to relay the addition and a second time to facilitate the stack inspection. In this Section we analyze the function’s representation at runtime and how this enables various stepping actions. For this purpose we provide Listing 7.5 to show the relevant parts of its representation in LLVM IR and Figure 7.1 to display the Truffle AST that Sulong produces for it. To keep the example concise we removed the calls to llvm.dbg.* intrinsics in both.

1 define i8 ∗ @method_doOp( i8 ∗ , i32 , void ( i32 ) ∗) ! dbg !119 { 2 %4 = alloca i32 , a l i g n 4 3 store i32 %1, i32∗ %4, a l i g n 4 4 %5 = load %class.Calculator ∗ , %class.Calculator ∗∗ @Calc, align 8, !dbg !128 5 c a l l void @_ZN10Calculator4doOpER2OpPFviE(%class.Calculator ∗ %5, i32∗ dereferenceable(4) %4, void ( i32 ) ∗ %2), !dbg !129 6 r e t i8 ∗ null , !dbg !130 7 } 8 9 define void @_ZN10Calculator4doOpER2OpPFviE(%class.Calculator ∗ , i32∗ dereferenceable(4) , void ( i32 ) ∗) ! dbg !131 { 10 %4 = load i32 , i32∗ %1, align 4, !dbg !138 11 ;... 12 } Case Study 70

13 14 ;... 15 !119 = !DISubprogram(name:"method_doOp", scope: !47, file: !47, line: 12, type: !120, scopeLine: 12, unit : ! 2 , . . . ) 16 ;... 17 !128 = !DILocation(line: 13, column: 5, scope: !119) 18 !129 = !DILocation(line: 14, column: 9, scope: !119) 19 !130 = !DILocation(line: 15, column: 5, scope: !119) 20 !131 = !DISubprogram(name:"doOp", scope: !49, file: !6, line: 48, type: !64, scopeLine: 48, unit: !2, ... ) 21 ;... 22 !138 = !DILocation(line: 49, column: 13, scope: !139) 23 !139 = !DILexicalBlock(scope: !131, file: !6, line: 49, column: 13)

Listing 7.5: Partial LLVM IR of the method_doOp function at calcExt.cpp:12

Listing 7.5 shows the LLVM IR that represents method_doOp. The function receives three arguments which LLVM implicitly names %0, %1 and %2. This corresponds to their index in the value list. The first argument is unused and only required by Ruby. The second one contains an integer primitive that refers to a value of the Op enum and the third a pointer to a function that consumes a number. method_doOp contains five instructions. First, the program allocates memory on the stack to hold an integer (line 2) and stores %1 into it (line 3). This is necessary since the doOp instance method of Calculator expects its Op argument as a reference rather than a value. The third instruction (line 4) then dereferences the memory location of the global variable Calc to retrieve the pointer to a Calculator instance that it stores. This is also the first instruction with available debug information. Like its successors its dbg attachment designates a DILocation descriptor that links to the DISubprogram of method_doOp as its parent. The fourth instruction (line 5) then performs the actual call to Calculator::doOp that we see in the function’s C++ code in Listing 7.2. In LLVM IR that function uses C++ linkage while method_doOp uses the simpler C linkage. Hence both functions feature vastly dissimilar naming conventions at IR-level. At last (line 6), the function returns a null pointer.

Sulong represents each basic block of an LLVM IR function as a node that sequentially executes its instructions and returns the block index of its successor at runtime. The single child of a RootNode, Figure 7.1 names it body, is the parent for these nodes in the AST and sees to their dynamic dispatch. It is also responsible for copying the arguments to the virtual stack-frame and thus the interpreter applies the RootTag to it. Sulong also links it to the root’s LLVM- SourceLocation to enable Truffle to instrument this node. Figure 7.1 shows that individual basic block nodes, on the other hand, remain neither tagged nor instrumentable. The same holds for the RootNode itself as well. For the purpose of instrumentation, providing a node Case Study 71

Figure 7.1: Sulong AST of the method_doOp function in Listing 7.5 Case Study 72

Figure 7.2: Call stack for method_doOp at Stepping Point 2 in Chrome Devtools with the RootTag is sufficient. Figure 7.1 further illustrates that only LLVM IR instructions with a dbg attachment are instrumented in the AST. Each of them also provides the State- mentTag and links to a LLVMSourceLocation that is within the scope of the one attached to the RootNode. Function calls additionally bear the CallTag. The RootNode also references the function’s source-level name to enable proper display of the call stack at runtime.

Stepping Point 1 When a Truffle debugger steps into a function call, the framework halts execution at the first instrumented node with a StatementTag it encounters. For method_doOp this is the load instruction that is shown on line 4 in Listing 7.5 and corresponds to Stepping Point 1 in Figure 7.1 and Listing 7.2. Stepping over or into at this point in the program both lead Truffle to suspend execution at the node marked as Stepping Point 2 since there is no function call in between. Though the attached LLVMSourceLocation puts the AST nodes of both points on different lines, they belong to the same C++ statement in calcExt.cpp. As we explained in Chapter5, a dbg attachment to a particular instruction in LLVM IR only indicates that it belongs to at least a sub-expression in the original program. LLVM debug information does not explicitly identify individual statements.

Stepping Point 2 Stepping Point 2 marks a node with a CallTag. Originally, the Truffle debugging framework treated such locations as the boundaries between source-level functions. While stepping into a function call only requires halting the debuggee at the next StatementTag on the execution path, stepping over one entails suspending this search until after the boundary node has finished executing. Ultimately, this semantic shifted from CallTag to the RootTag. However, Truffle still uses it to reconstruct the source-level call hierarchy. Figure 7.2 shows the call stack of method_doOp at Stepping Point 2 for this case study in the Chrome Devtools debugger. All entries besides method_doOp belong to the Ruby program. As it shows, Sulong Case Study 73

Sulong displays the source-level name of the of the executing method together with the filename and line-number of the currently executing statement.

Stepping Point 3 The debugging framework can step out of a function by resuming execution until it encounters a RootTag at a higher stack-depth than the last halted node and then suspending the guest language program at the first node with a StatementTag. The return statement at Stepping Point 3 concludes the body of method_doOp. Stepping into and over at this point has the same result as stepping out of the function.

Source-level breakpoints do not depend on tags. Truffle can suspend execution at any instru- mented node whose SourceSection matches the requested location. The framework only poses the restriction that the lexical scope of the corresponding RootNode must contain the state- ment’s declaration. To fulfill this condition despite LLVM debug information not describing the extent of scopes, Sulong declares, as Figure 7.1 also shows, the LLVMSourceLocation of each function to extend until the end of the file.

7.2.2 Symbol Inspection

Figure 7.3: Symbol Inspection in the Chrome Devtools at the Inspection Point in calc.cpp Case Study 74

The doOp function of the Calculator class performs either a binary arithmetic operation on the top two stack elements or prints the content of the stack. The instruction marked Inspection Point at line 52 in calc.cpp is part of the latter action. At that point in the program, the source-level scope hierarchy contains diverse kinds of symbols with different runtime representa- tions in Sulong. Figure 7.3 illustrates the language-specific view that Sulong provides on them in the Chrome Devtools. We demonstrate the concept of variable inspection in this interpreter by exercising the execution of findLocalScopes for the corresponding node. To support this, we provide an abstract view of the source-level scope hierarchy at Inspection Point in Figure 7.4 and the relevant parts of the function’s representation in LLVM IR in Listing 7.6.

1 @Calc = global %class.Calculator ∗ null, align 8, !dbg !0 2 3 define void @_ZN10Calculator4doOpER2OpPFviE(%class.Calculator ∗ , i32 ∗ , void ( i32 ) ∗){ 4 c a l l void @llvm.dbg.value( metadata %class.Calculator ∗ %0, i64 0 , metadata !132 , metadata ! 8 5 ) 5 c a l l void @llvm.dbg.value( metadata i32∗ %1, i64 0 , metadata !134 , metadata ! 8 5 ) 6 c a l l void @llvm.dbg.value( metadata void ( i32 ) ∗ %2, i64 0 , metadata !136 , metadata ! 8 5 ) 7 %4 = load i32 , i32∗ %1, align 4, !dbg !138 8 %5 = icmp eq i32 %4, 4, !dbg !140 9 br i1 %5, label %6, label %18, !dbg !141 10 11 ;

41 !132 = !DILocalVariable(name:"this",...) 42 !134 = !DILocalVariable(name:"op",...) 43 !136 = !DILocalVariable(name:"printStackEntry",...) 44 !145 = !DILocalVariable(name:"entry",...) 45 !153 = !DILocalVariable(name:"val",...)

Listing 7.6: Partial LLVM IR of the Calculator::doOp function at calc.cpp:48

In findLocalScopes, Sulong traverses the chain of LLVMSourceLocation instances attached to the node for which the debugging framework requested the scopes. In the process it resolves each LLVMSourceSymbol that is visible at the node’s location to an LLVMDebugObject that attempts to provide a language-specific view of the symbol’s current value for display in the debugger frontend. In the following we describe the abstractions sulong builds for each of the available scope members. Figure 7.5 shows a visual representation of this information.

• const int val

The instruction that marks our Inspection Point resides at line 52 in calc.cpp. Its immediate scope is the block that forms the body of the for loop in which the program prints the current content of the calculator’s number stack. The arithmetic expression we supplied as input for our program lets it reach Inspection Point exactly once. At that point, the number 7 is the first and only entry on its stack.

The local variable val is declared prior to the suspended statement in the loop body. Listing 7.6 shows that it corresponds to the SSA value %13 and is of the i32 type. The LLVM IR function contains only a single call to llvm.dbg.value to describe val (line 26 in Listing 7.6). Sulong detects situations like this during parsing and notes the SSA register in the language context. This tactic allows the interpreter to avoid dynamically tracking values that are effectively final on IR-level. In findLocalScopes it queries the provided language context for the symbol to detect the statically known location of its value. Subsequently Sulong obtains the content of the SSA register, in this case a Java int, and creates an LLVMDebugValue from it.

As Figure 7.4 shows, the LLVMSourceType of val is a Decorator that assigns the name const int to a dynamic, signed 32bit integer primitive type. We also see that descriptor in Figure 7.3. The LLVMDebugObject that represents val is a Primitive which, per the decorated type, interprets its internal value as a 32bit signed integer. Case Study 76

• struct StackEntry* entry

The for loop that prints the calculator’s stack content iterates through the nodes in that linked list and uses a pointer as loop variable. As we mentioned, the number stack at Inspection Point contains only a single entry. Figure 7.3 shows that the value of entry is the same memory address that also marks the top of the stack in the this pointer.

Unlike val, Sulong needs to track the value of entry since the LLVM IR function describes it in three separate calls to llvm.dbg.value (lines 14, 19 and 33 in Listing 7.6), one each for the initialization and increment in the loop header and the phi selection between both in the loop body. Barring a static alternative, Sulong resorts to dynamic value tracking and emits the intrinsic to the Truffle AST. In findLocalScopes the interpreter analyzes the provided stack-frame and detects the tracked symbol. The corresponding LLVMDebugValue wraps an instance of Sulong’s custom class for memory addresses which itself is only a wrapper around a Java long.

The LLVMSourceType of entry is a Pointer that is marked unsafe to dereference. That class derives its name by appending an asterisk to that of the pointed-to type. This notation is specific to the C family of programming languages. Support for other styles, e.g. the pointer keyword in Fortran, is part of future work. Similarly, the Structured basetype of entry implicitly prepends the keyword struct to the StackEntry name that the corresponding DI_Composite_Type with the structured tag provides. The LLVMDe- bugObject for this symbol is a specialization for pointers. As Figure 7.3 shows, it displays the IR-level value in hexadecimal notation. However, Sulong does not dereference the pointer to provide access to the members of the composite it points to.

• Calculator* this

The this pointer is an implicit argument to Calculator::doOp. As such it is declared di- rectly in the scope of the function body. Like val, the LLVM IR function describes this symbol only once (line 4 in Listing 7.6) and Sulong tracks the location of its value in the language context. At runtime, the corresponding SSA register contains an instance of Su- long’s internal class for memory addresses which Sulong resolves to an LLVMDebugValue in findLocalScopes. Case Study 77

LLVM debug information describes this as an artificial object pointer. For this reason the symbol’s LLVMSourceType, an instance of the Pointer subclass, declares that its target address is safe to dereference. While it still formats the value of the local variable as the pointer it is, the corresponding LLVMDebugObject also resolves the memory address to provide the internal fields of the Calculator class as its members. Figure 7.4 shows the first one to be a 32bit signed integer with the name stackSize at offset 0. The second field, like entry, is a pointer to a struct StackEntry with the name top located 64 bits after the first. The gap of 32 bits between the members is a result of memory alignment. While the this pointer is safe to dereference, the same is not true for top. As the constructor of Calculator shows, for a stackSize of 0 it is an explicit null-pointer.

• Op& op

The operation to perform is the first explicit argument of Calculator::doOp. Like this, op is a direct member of the function scope and Sulong statically remembers the SSA register that holds its value in the form of a memory address (the single declaration with llvm.dbg.value is on line 5 in Listing 7.6).

While the LLVMSourceType of this symbol is a Pointer, it is also marked as a reference type. In contrast to a regular pointer, these addresses are more of an implementation detail. Sulong assumes that references always point to valid memory locations and dis- plays the value at that region rather than its address. To indicate the pointer’s reference nature, Sulong appends the & operator rather than the usual asterisk to the type-name. In Figure 7.4 we can see that the basetype of op is a Decorator. This corresponds to the typedef of the Op enum defined in calc.cpp at line 11. Without it, the type would rather be displayed as enum Op, following the regular declaration style of such values in C.

On IR-level, op is a memory address in an SSA register. The corresponding LLVMDebu- gObject dereferences this pointer to another LLVMDebugValue, from which it then reads an integer value. It then uses the decorated Enum subclass of LLVMSourceType to resolve it to the corresponding label. At Inspection Point, this is the word PRINT. We also see this in Figure 7.3. Case Study 78

• void(*)(int) printStackEntry

The second explicit parameter to Calculator::doOp is a pointer to a function that receives each value on the number stack to print it. Like the other function arguments, this is a direct member of the function scope and the SSA register that holds the runtime value is statically known (the single declaration with llvm.dbg.value is on line 6 in Listing 7.6).

Since its formal parameter and return types are ultimately unimportant for its purposes, Sulong represents the source-level function type as a Decorator which assigns the appro- priate name to a Pointer to Void. Figure 7.4 also shows this composition.

How Sulong displays values of a source-level function pointer depends on their runtime representation. Memory addresses that point to native functions are formatted as such. However, the more typical case here would be Sulong’s internal representation of func- tion pointers. For them, the interpreter shows the name of the function instead. The Chrome Devtools in Figure 7.3 show a third case. Here, the value of printStackEntry is a Truffle interop object which corresponds to the lambda in calc.rb at line 4. Sulong uses a specialized subclass of LLVMDebugObject for cases where an LLVMDebugValue declares itself an interop object. It only provides the implicit member Unindexed Interop Value to indicate its inability to format the content at any offset within a foreign TruffleObject.

• Calculator* Calc

The only entry in the scope hierarchy at Inspection Point that is static and not local to the function is Calc. calcExt.cpp uses it to reference an instance of the Calculator class which it wraps as a Ruby module. Consequently, in Calculator::doOp the same value appears as the this pointer.

In LLVM IR, this pointer resides in a global variable called @Calc. We see this already in the first line of Listing 7.6. Similar to effectively final descriptions with llvm.dbg.declare Sulong stores this information in the language context and retrieves it again in findLo- calScopes. However, there is still a difference in Storage location as Sulong does not retrieve its value from the execution stack but rather from a static container within the language context. Barring this difference in storage though, the access model is nearly the same as for this. The content of the global variable is still a memory address which Case Study 79

an instance of the Pointer subclass of LLVMDebugObject displays. In contrast to the dynamic object pointer though, LLVM debug information does not identify Calc to be safe to dereference. Figure 7.4 also shows this difference in LLVMSourceType instances. Consequently, the Chrome Devtools debugger cannot access the fields of the pointed-to value in Figure 7.3.

In calc.cpp we can see that Calculator::doOp contains additional entries in the function scope. The integers rhs, lhs and result are all declared after the location of Inspection Point though. For this reason, Sulong does not include them in the list of available symbols that it computes in findLocalScopes. As an additional modification from the actual source-level scope hierarchy, the interpreter aggregates all function-local symbols into a single scope. This avoids adding unnecessary detail in the debugger frontend. Figure 7.4 shows that especially blocks often have only few scope members or none at all. The aggregation spares the user from having to manually expand several scopes after each stepping action to get a complete picture of all available symbols. Case Study 80

Figure 7.4: Scope hierarchy at Inspection Point in Calculator::doOp Case Study 81

Figure 7.5: Value abstraction for symbols at Inspection Point in Calculator::doOp Evaluation 82

Chapter 8

Evaluation

This chapter presents a short evaluation of the overhead in terms of execution time that instrumentation and value tracking in Sulong impose. For this purpose we com- pare the interpreter’s performance in several micro benchmarks which we compiled to LLVM IR with different levels of optimization.

We evaluated the runtime overhead of instrumentation and value tracking in Sulong using its proprietary benchmark suite. In the following we discuss the results of the most interesting cases. We used Clang in the released version 5.0.1 on a 64bit installation of Ubuntu 14.04 (Linux 4.4.0-116) running on an Intel Core i7 2670QM quad-core CPU with 8GB of memory to compile the benchmarks to LLVM IR twice. For the first set we chose the compiler’s O0 optimization level which does not apply any transformation passes. For the second one, however, we used LLVM’s opt tool to apply a predefined selection of optimizations in Sulong’s benchmark harness to the unoptimized bitcode files. We then executed the bitcode on the same platform with Sulong in revision 26254e71 together with the open-source Graal compiler and Truffle, both in revision ee8f1caae1. For each benchmark we only counted the results of ten subsequent executions after fifty warmup iterations. The evaluation uses the Oracle Hotspot JVM that is part of the JVMCI-enabled Oracle Labs JDK in version 0.40. [6] performs a more elaborate evaluation of the runtime overhead of instrumentation in a Truffle AST which finds that it only occurs for breakpoints set at locations that are actually reached in execution. As we would likely arrive at the same conclusion, we avoid doing the same experiment and restrict ourselves to analyzing the impact of value tracking since this poses the only significant difference between both debuggers and is independent of breakpoint activity. Evaluation 83

Figure 8.1: Runtime overhead of debugging unoptimized bitcode in Sulong

The results in Figure 8.1 show that at O0, i.e. for unoptimized bitcode files, value tracking and Truffle node instrumentation do not cause any significant impact on execution time. What is more, it even slightly improves performance in the case of the lnxaddseq and lnxdivseq bench- marks. As we already stated in Section 6.3.3, at O0 Clang produces code that tracks dynamic local variables only using llvm.dbg.declare. Since Sulong does not execute this intrinsic at runtime, value tracking cannot be the cause of this improvement. We believe it to be possible that the additional wrapper nodes for instrumentation coincidentally led the Graal compiler to make optimization decisions that are more beneficial to the benchmark’s AST. [6] notices a similar effect and also attributes it to non-determinism in the Graal compiler. However, we did not investigate this any further. Evaluation 84

Figure 8.2: Runtime overhead of debugging Optimized bitcode in Sulong

Other Truffle languages rarely use Sulong to execute unoptimized bitcode files. Instead, they typically apply several LLVM optimization passes to them in order to prevent situations where the interpreter needs to store interop values into native memory. Unfortunately, this also re- quires LLVM IR to use llvm.dbg.value to track local variables at runtime. In order to evaluate the impact of Sulong’s debugging support in a more common usage scenario, we performed a second benchmark run with optimized bitcode files. Concretely, we used LLVM’s opt tool with the -mem2reg -globalopt -simplifycfg -constprop -instcombine -dse -loop-simplify -reassociate - licm -gvn arguments to produce optimized LLVM IR. This choice follows a predefined selection in Sulong’s benchmark harness.

As Figure 8.2 shows, the runtime overhead of value tracking highly depends on the concrete benchmark. deltablue, fasta.cint and knucleotide.cint use a more functional programming style where most local variables are effectively final either on source-level or on IR-level. Sulong Evaluation 85 optimizes for this case and, as the results prove, can avoid significant runtime impact in these cases. lnxaddseq, lnxdivseq and lnxsubseq, on the other hand, frequently reassign local variables also as part of nested loops. Tracking these values at runtime incurs significant overhead, even decreasing performance below the level of O0. However, even in this situation Sulong is able to reconstruct the source-level state to a high degree. LLDB fails to provide a similar level of accuracy when debugging the same files compiled to native code. Enabling developers to conveniently debug the code that is actually being run in production environments, rather than an explicitly “debugified” version of it, allows them to better reproduce actual error conditions and exclude (or detect) compiler optimizations as the source of bugs. Especially since Sulong does not exhibit the performance impact of value tracking unless users explicitly enable it, its benefits greatly outweigh the drawbacks. Future Work 86

Chapter 9

Future Work

At the time of this thesis Sulong supports source-level debugging of C and C++ programs compiled to LLVM IR. This implementation has already gone beyond a mere proof-of-concept. However, various fields of improvement remain.

Source Languages: Sulong only supports displaying source-level symbols in a notation spe- cific to C and C++ programs. While this is a limited issue in terms of value display due to the little variance that different programming languages use in displaying primitive values, it is distinctly noticeable in the formatting of source-level type names. LLVM debug information contains a reference to the original programming language of the entities within a compile unit. Sulong implements its framework for source-level types already with extensibility in mind.

Linking: Sulong currently does not merge source-level scopes between multiple bitcode files. For scopes whose members are defined in multiple compile units, e.g. a C++ namespace, this may lead to an incomplete listing of members. Since the language context is available during parsing, storing these necessarily named scopes in it would enable merging them across files.

Performance: Sulong already employs different strategies to avoid tracking values of source- level symbols at runtime. However, there is still much room for improvement. Possible ap- proaches could, to name only an example, leverage partial evaluation in Truffle to further reduce the impact on peak performance. Future Work 87

Memory Safety: Sulong relies on certain assumptions based on available debug information to avoid accessing unallocated native memory. While reliable in most cases, they are susceptible to undefined behavior at the levels of both the source language as well as LLVM IR.

Safe Sulong: Safe Sulong [16] is an extension to Sulong that replaces the interpreter’s use of native memory with managed Java objects to enable memory-safe execution of LLVM IR. Supporting source-level debugging in that project requires implementing the LLVMDebugValue interface for its additional data structures.

State Manipulation: In principle, the Truffle debugging framework enables frontends to manipulate the runtime state they display by changing entire values or fields within them. Sulong does not support this capability yet. While program optimization performed by LLVM as well as the general abstraction to LLVM IR would make this a challenging task, the ability to change primitive values or fields when debugging unoptimized bitcode files is a realistic goal. Related Work 88

Chapter 10

Related Work

This chapter discusses other work related to source-level debugging in a multi- language environment.

Microsoft’s Common Language Runtime (CLR) [17] is the basis of the .NET framework. Similar to Sulong it is an interpreter for an intermediate representation that supports language inter- operability and source-level debugging. The main difference seems to be the used bytecode. Rather than LLVM IR, the CLR uses its own intermediate representation of source code.

The LLDB debugger [18] is part of the LLVM project and also uses LLVM IR internally. However, unlike Sulong it does not support interpreting the intermediate representation directly and instead requires it to be compiled to native code. The LLI tool is an LLVM IR interpreter integrated in LLVM [19]. However, it lacks debugging features.

Ryu and Ramsay [20] describe an approach for providing source-level debugging support for programming languages. They propose extending compilers to emit a specific representation of debug information which a preexisting debugger can use. This encoding contains executable code to print values in a language-specific way. By shifting this task from the debugger to the compiler, the former can be language-independent and, as a result, debug even programs implemented in multiple languages. Sulong, on the other hand, uses an already existing format of debug information and reconstructs a source-level representation of its runtime state from that. Related Work 89

[21] describes an approach to debugging that allows inspecting interpreted programs both at source-level as well as on the interpreter level. Truffle enables a similar strategy. Developers can attach a Java debugger to inspect the interpreter while debugging the guest language program in Chrome Inspector. In Sulong, this allows users to debug on the level of LLVM IR as well. LLVM used to be able to generate debug information for bitcode files that targets a file containing its textual representation rather than the original source code. If active plans for this [22] succeed, Sulong could support debugging on the IR-level even more explicitly.

Blink [23] is a debugger that also supports source-level inspection of multiple languages in the same frontend. It controls and coordinates other debuggers attached to the debuggee and aggregates their information in a single frontend. In contrast to GraalVM/Sulong it requires external debugging tools, like gdb for C code or jdb for Java, to perform the actual language- specific state reconstruction. Conclusion 90

Chapter 11

Conclusion

This thesis describes the realization of source-level debugging support in the LLVM IR inter- preter Sulong. It enriches the Truffle AST with a specialized representation of debug informa- tion which LLVM frontends can emit to bitcode files. Combined with additional modifications to Sulong’s various node implementations this data enables Truffle to instrument programs it executes at the language level. The interpreter also supports the framework’s debugging API for which multiple frontends exist. These support single-stepping through the program as well as setting breakpoints in its original code. By applying multiple levels of abstraction to its internal execution environment Sulong can further provide a source-level view on a debuggee’s state at runtime, including language-specific display of values, types and scopes. By tracking symbols as they transition between different representations the interpreter can debug even op- timized programs. Performance evaluation shows that while this imposes a significant impact on execution time for some programs, Sulong is successful in avoiding this overhead in most cases. What is more, the project seamlessly integrates with other Truffle-based interpreters, enabling users to debug even across language boundaries. The thesis also demonstrates this at the hand of a native Ruby extension implemented in C++. Acknowledgements 91

Acknowledgements

First and foremost, I would like to thank Matthias Grimmer for his constant and valuable feedback on my work in the Sulong project. His ideas and suggestions on any issues that I encountered in the development process never failed to be of use. I would also especially like to thank Manuel Rigger for his invaluable help and feedback in writing this thesis. Then I would like to thank my colleagues in the Oracle project, especially Roland Schatz, and my professor Hanspeter Mössenböck for their constant support of my work.

This work was performed in a research cooperation with, and supported by, Oracle.

Oracle and Java are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners. List of Figures 92

List of Figures

2.1 Concept of the LLVM compilation tool-chain ...... 10 2.2 Structure of the GraalVM ...... 12 2.3 AST of the abs function ...... 13

5.1 Source-Level Location Information in Sulong ...... 41

6.1 Source-Level Symbol and Scope Information in Sulong ...... 53 6.2 Truffle debugging API access model ...... 55

7.1 Sulong AST of the method_doOp function in Listing 7.5...... 71 7.2 Call stack for method_doOp at Stepping Point 2 in Chrome Devtools ...... 72 7.3 Symbol Inspection in the Chrome Devtools at the Inspection Point in calc.cpp 73 7.4 Scope hierarchy at Inspection Point in Calculator::doOp ...... 80 7.5 Value abstraction for symbols at Inspection Point in Calculator::doOp ...... 81

8.1 Runtime overhead of debugging unoptimized bitcode in Sulong ...... 83 8.2 Runtime overhead of debugging Optimized bitcode in Sulong ...... 84 List of Tables 93

List of Tables

6.1 Mapping LLVM source-level type nodes to LLVMSourceType ...... 54 Listings 94

Listings

3.1 abs function in LLVM IR ...... 18

4.1 Partial metadata for the LLVM IR in Listing 3.1...... 27 4.2 Signatures of LLVM IR’s value tracking intrinsics ...... 37

5.1 Example implementation of a Truffle AST node in Sulong ...... 44 5.2 DSL-generated wrapper for the NodeImpl class in Listing 5.1...... 46

6.1 Definition of Sulong’s LLVMDebugValue interface ...... 57

7.1 C++ code file (calc.cpp) that defines a stack-based calculator ...... 65 7.2 Ruby C++ extension (calcExt.cpp)...... 67 7.3 Ruby program (calc.rb) to compute the value of an arithmetic expression in postfix notation ...... 67 7.4 Ruby program (extconf.rb) to create a Makefile for the native extension CalcExt 68 7.5 Partial LLVM IR of the method_doOp function at calcExt.cpp:12 ...... 69 7.6 Partial LLVM IR of the Calculator::doOp function at calc.cpp:48 ...... 74 Bibliography 95

Bibliography

[1] Chris Lattner and Vikram Adve. Llvm: A compilation framework for lifelong program analysis & transformation. In Proceedings of the International Symposium on Code Gen- eration and Optimization: Feedback-directed and Runtime Optimization, CGO ’04, pages 75–, Washington, DC, USA, 2004. IEEE Computer Society.

[2] clang: a c language family frontend for llvm. https://clang.llvm.org/. Accessed: 2018- 01-30.

[3] DragonEgg - using llvm as a gcc backend. https://dragonegg.llvm.org/. Accessed: 2018-01-30.

[4] Thomas Würthinger, Christian Wimmer, Andreas Wöß, Lukas Stadler, Gilles Duboscq, Christian Humer, Gregor Richards, Doug Simon, and Mario Wolczko. One vm to rule them all. In Proceedings of the 2013 ACM International Symposium on New Ideas, New Paradigms, and Reflections on Programming & Software, Onward! 2013, pages 187–204, New York, NY, USA, 2013. ACM.

[5] Christian Wimmer and Thomas Würthinger. Truffle: A self-optimizing runtime system. In Proceedings of the 3rd Annual Conference on Systems, Programming, and Applications: Software for Humanity, SPLASH ’12, pages 13–14, New York, NY, USA, 2012. ACM.

[6] Chris Seaton, Michael L. Van De Vanter, and Michael Haupt. Debugging at full speed. In Proceedings of the Workshop on Dynamic Languages and Applications, Dyla’14, pages 2:1–2:13, New York, NY, USA, 2014. ACM.

[7] Chrome devtools protocol. https://chromedevtools.github.io/devtools-protocol/. Accessed: 2018-01-30. Bibliography 96

[8] Chrome devtools. https://developers.google.com/web/tools/chrome-devtools/. Accessed: 2018-01-30.

[9] Google chrome webbrowser. https://www.google.com/chrome/index.html. Accessed: 2018-01-30.

[10] Chromium project. https://www.chromium.org/. Accessed: 2018-01-30.

[11] Truffle debugging support for the netbeans ide. http://plugins.netbeans.org/plugin/ 68647/truffle-debugging-support. Accessed: 2018-01-30.

[12] Manuel Rigger, Matthias Grimmer, and Hanspeter Mössenböck. Sulong - execution of llvm-based languages on the jvm: Position paper. In Proceedings of the 11th Workshop on Implementation, Compilation, Optimization of Object-Oriented Languages, Programs and Systems, ICOOOLPS ’16, pages 7:1–7:4, New York, NY, USA, 2016. ACM.

[13] Llvm 5.0.1 developer policy regarding backwards compatibility of llvm ir. https://releases.llvm.org/5.0.1/docs/DeveloperPolicy.html# ir-backwards-compatibility. Accessed: 2018-01-30.

[14] Llvm 5.0.1 documentation for binary encoding of llvm ir. https://releases.llvm.org/ 5.0.1/docs/BitCodeFormat.html. Accessed: 2018-01-30.

[15] Llvm 5.0.1 language reference manual for textual encoding of llvm ir. https://releases. llvm.org/5.0.1/docs/LangRef.html. Accessed: 2018-01-30.

[16] Manuel Rigger, Roland Schatz, René Mayrhofer, Matthias Grimmer, and Hanspeter Mössenböck. Sulong, and thanks for all the bugs: Finding errors in c programs by abstract- ing from the native execution model. In Proceedings of the Twenty-Third International Conference on Architectural Support for Programming Languages and Operating Systems, ASPLOS ’18, pages 377–391, New York, NY, USA, 2018. ACM.

[17] Jennifer Hamilton. Language integration in the common language runtime. SIGPLAN Not., 38(2):19–28, February 2003. Bibliography 97

[18] The debugger. https://lldb.llvm.org/. Accessed: 2018-03-30.

[19] The llvm compiler infrastructure. https://www.llvm.org/. Accessed: 2018-01-30.

[20] Sukyoung Ryu and Norman Ramsey. Source-level debugging for multiple languages with modest programming effort. In Rastislav Bodik, editor, Compiler Construction, pages 10–26, Berlin, Heidelberg, 2005. Springer Berlin Heidelberg.

[21] Bastian Kruck, Tobias Pape, Tim Felgentreff, and Robert Hirschfeld. Crossing abstrac- tion barriers when debugging in dynamic languages. In Proceedings of the Symposium on Applied Computing, SAC ’17, pages 1498–1504, New York, NY, USA, 2017. ACM.

[22] Reviving the debugir pass. https://reviews.llvm.org/D40778. Accessed: 2018-04-06.

[23] Byeongcheol Lee, Martin Hirzel, Robert Grimm, and Kathryn S. McKinley. Debug all your code: Portable mixed-environment debugging. In Proceedings of the 24th ACM SIG- PLAN Conference on Object Oriented Programming Systems Languages and Applications, OOPSLA ’09, pages 207–226, New York, NY, USA, 2009. ACM. Curriculum Vitae 98

Curriculum Vitae

Personal Information

Name Kreindl, Jacob Address Zeppelinstraße 11a, 4030 Linz, Austria Telephone +43 664 3562712 Email [email protected]

Professional Experience

2016-present Student Researcher, Institute for System Software, Linz, Austria. 2016 Tutor in Practical Computer Science, Institute for System Software, Linz, Austria. 2015-2016 Tutor in Basics of Programming, Institute for System Software, Linz, Austria. 2015 Tutor in Software Development 2, Institute for System Software, Linz, Austria.

Education

2016-present MSc in Computer Science, Johannes Kepler University, Linz, Austria. 2013-2016 BSc in Computer Science, Johannes Kepler University, Linz, Austria. 2004-2012 Matura, Akademisches Gymnasium Linz, Austria.

Other Interests

Research Implementation and Tooling Support for Programming Languages Hobbies Science Fiction Eidesstattliche Erklärung 99

Eidesstattliche Erklärung

Ich erkläre an Eides statt, dass ich die vorliegende Masterarbeit selbstständig und ohne fremde Hilfe verfasst, andere als die angegebenen Quellen und Hilfsmittel nicht benutzt bzw. die wörtlich oder sinngemäß entnommenen Stellen als solche kenntlich gemacht habe. Die vor- liegende Masterarbeit ist mit dem elektronisch übermittelten Textdokument identisch.

Linz, am 10. April 2018

Jacob Kreindl, BSc.