<<

Compiling a Higher-Order Smart Contract Language to LLVM

Vaivaswatha Nagaraj Jacob Johannsen Anton Trunov Zilliqa Research Zilliqa Research Zilliqa Research [email protected] [email protected] [email protected] George Pîrlea Amrit Kumar Ilya Sergey Zilliqa Research Zilliqa Research Yale-NUS College [email protected] [email protected] National University of Singapore [email protected] Abstract +------+ Scilla is a higher-order polymorphic typed intermediate | Blockchain Smart | | Contract Module | level language for implementing smart contracts. In this talk, | in ++ (BC) | +------+ we describe a Scilla targeting LLVM, with a focus + state variable | + ^ on mapping Scilla types, values, and its functional language foo.scilla | | | & message | fetch| | constructs to LLVM-IR. | | |update v v | The compiled LLVM-IR, when executed with LLVM’s JIT +------+------+ framework, achieves a speedup of about 10x over the refer- | | | +------+ +------+ | ence on a typical Scilla contract. This reduced | +------> |JIT Driver | +--> | Scilla Run-time| | | | |in C++ (JITD)| | in C++ | | latency is crucial in the setting of blockchains, where smart | | +-+------+---+ | (SRTL) | | | | | ^ +------+ | contracts are executed as parts of transactions, to achieve | | | | | | | foo.scilla| | | peak transactions processed per second. Experiments on the | | | foo.ll| | | | | | | Ackermann function achieved a speedup of more than 45x. | | v | | | | +--+------+----+ | This talk is aimed at both researchers | | |Scilla Compiler| | | | |in OCaml (SC) | | looking to implement an LLVM based compiler for their func- | | +------+ | | | | tional language, as well as at LLVM practitioners. | | | | | | | | Scilla Virtual Machine | | v in OCaml & C++ (SVM) | | +-----+------+ | | | JIT Cache | | 1 Introduction | +------+ | | | Scilla (Smart Contract Intermediate Level Language) [16] +------+ is a functional programming language in the ML family, designed to enable writing safe smart contracts. Scilla is the Fig. 1. Design of the Scilla Virtual Machine main smart contract language of the Zilliqa blockchain [18]. Contracts written in Scilla are deployed as-is (i.e., as ) and are interpreted during transaction validation by vision on some desired LLVM features which would facilitate the nodes participating in the blockchain consensus (aka projects similar to our own (Sec. 5). miners). The project is under active development and is free and open-source [12–14]. 2 The Compiler Pipeline

arXiv:2008.05555v1 [cs.PL] 12 Aug 2020 The Scilla Virtual Machine (SVM) (Fig. 1) aims to eventu- Scilla’s reference interpreter and the accompanying type- ally replace the Scilla interpreter with faster , thus checker are written in OCaml. To take advantage of the achieving higher transaction throughput. This project com- developed infrastructure, we decided to write the compiler prises of the Scilla compiler (SC) which translates Scilla also in OCaml, thus enabling re-using much of the frontend to LLVM-IR, the Scilla runtime library (SRTL) and the JIT code and the type-checker. The compiler uses LLVM’s OCaml driver which JIT compiles the LLVM-IR and executes it. Ex- bindings to generate LLVM-IR. ecution of a Scilla contract may result in accessing the Serving as a background for the next section, we now blockchain state, with such interactions facilitated by the briefly discuss our compiler pipeline. While in this proposal runtime library. The runtime library also implements Scilla we use textual description, our talk will make use of examples built-in operations. to illustrate the transformations performed in each pass. In the course of this talk, we will briefly describe the phases of our compiler pipeline (Sec. 2), elaborate on mapping the 2.1 Parsing and Type Checking Scilla AST to LLVM-IR (Sec. 3), present characteristic bench- The parser and type checker reuse code from the reference marks used to evaluate our compiler (Sec. 4), and share our interpreter and the result is an AST with each identifier 1 Vaivaswatha Nagaraj, Jacob Johannsen, Anton Trunov, George Pîrlea, Amrit Kumar, and Ilya Sergey

1 fun (p: List(Option Int32)) ⇒ or monomorphization (C++, Rust). The former erases all 2 match p with type information in generic code after type-checking and 3 | Nil ⇒ z generates a single copy of the code. This requires that run- 4 | Cons(Somex) xs ⇒ x time values be boxed (i.e., values of all types represented 5 | Cons__ ⇒ z uniformly [8]) so that they can all be handled uniformly in 6 end the generic code. On the other hand monomorphization spe- cializes the code for each possible (at runtime) ground type Fig. 2. Nested pattern in a match-expression. (i.e., types that don’t have a type parameter) [17], creating annotated with its type. The type annotations are necessary copies of the code. While this results in code replication, for code generation later on. each copy can be efficiently compiled because the runtime values no longer need to be boxed [3]. 2.2 Dead Code Elimination A third approach to parametric polymorphism is to wait Scilla follows a whole-program compilation model. This until polymorphic code is used, and specialize it then (typi- means that along with the program currently being compiled, cally using JIT compilation). This approach is used in C# [5] we also compile imported libraries. While the DCE pass is and Ada [6]. useful in the traditional sense, our primary goal for it here Scilla is based on System F [4, 15]— an expressive type is to simply eliminate unused imported library functions, to system with explicit type bindings and instantiations. We im- improve . plement parametric polymorphism by specializing each poly- morphic Scilla expression with all possible ground types. 2.3 Pattern Match Flattening This uses a higher-order data-flow analysis to track flow of Scilla allows for nested patterns in a match expression. For types into polymorphic expressions [9]. As type instantiation example, the expression shown in Fig. 2 requires checking for may happen in stages for Scilla expressions with several a Some constructor inside an object created with the Cons type parameters, we choose to compile a polymorphic expres- constructor. To efficiently translate pattern match constructs sion as a dynamic dispatch table, associating ground types into an LLVM switch statement, match expressions with with the corresponding monomorphic version of the expres- nested patterns are flattened into nested match expressions sion. This provides for a simpler compilation that saves us with flat patterns [10]. from replicating code outside of the polymorphic expression, but at the cost of a runtime indirection indexing into the (pos- 2.4 Uncurrying sibly nested) dynamic dispatch tables. While this is a mix of As is common in functional programming languages, Scilla existing strategies to implement parametric polymorphism, functions and their applications (calls) follow curried seman- we are not aware of one that uses this exact combination. tics [2]. This means that a function taking n arguments is actually a function that takes in one argument and returns 2.6 Closure Conversion a function to process the remaining n − 1 arguments and As is typical of higher-order languages, Scilla allows one produce the result. The uncurrying pass transforms the AST to pass functions as arguments to other functions and re- to have regular C-like (and LLVM-IR) function call seman- turn them, as well as to define functions nested inside other tics, i.e., functions may take n ≥ 0 arguments, and their calls functions. This means a function may have not only its argu- will provide exactly n arguments. The change in semantics ments available for use inside, but also a set of “free variables” is accompanied by an optimization pass that analyzes all that are defined outside its syntactic definition. calls of a function, and when all of them provide the same n The closure conversion pass [1] computes the set of free number of arguments, the function is translated to a function variables for each function, constructs an aggregate type to taking n arguments. If it cannot prove so, then the function hold all the free variables of the function (conventionally is translated to take just one argument, and a nested function called the function’s environment) and lifts the function handling the rest. An example: If all calls of the function f definition to the top / global level, as is required for LLVM-IR : T1 -> T2 -> T3 provide two arguments (of type T1 and generation (where all functions are defined at the top level). T2), then f is transformed to have the type f : (T1,T2) -> The lifted function takes the environment as an argument. T3. When functions are used as values in the program, each At the time of writing this proposal, our uncurrying pass such variable is now a pair of (1) pointer to the function defi- changes the semantics of the AST to use uncurried semantics, nition and (2) the environment. Function applications (calls) but we do not have the optimization implemented yet. are translated into to calling the function pointer, with the paired environment being passed as an additional argument. 2.5 Monomorphize In our implementation, this pass also flattens the AST by Parametric polymorphism (also called generic programming) transforming nested let-in expressions into a sequence of is typically implemented using type-erasure (Java, ML, etc) assignment statements. 2 Compiling a Higher-Order Smart Contract Language to LLVM

3 Mapping Scilla to LLVM-IR 1 type MyList= The final stage of the compiler pipeline translates the closure 2 | Nil converted AST to LLVM-IR. 3 | Cons of Int32 MyList

3.1 Type Descriptors Fig. 3. Defining an integer list in Scilla. Although Scilla code is monomorphized by the time we For an ADT tname with constructors cname1, cname2, generate LLVM-IR for it (which means we know the ground ...cnameN, we define LLVM types %tname = type { i8, type of code and values that we’re generating code for), %cname1*, %cname2*, ... %cnameN*}, %cname1 = type at times it is necessary for code in the runtime library to <{ i8, [types in cname1’s tuple]}>, %cname2 = ... operate on Scilla values. For example, many Scilla contract etc. The LLVM type %tname acts as the placeholder for all executions end with sending out a message (which for the values of this ADT, and a pointer to this type is used to rep- purposes of this discussion is just a composite Scilla value). resent the boxed ADT values. It is always only dereferenced The message needs to be serialized into a JSON. It is much for its first i8 field, which is the tag representing the spe- simpler to do the JSON serialization outside in the runtime cific constructor used to build this value. The other fields library rather than generate code to do that. are defined only for type completeness, with the idea that, Such a scenario gives rise to the requirement that we com- in the future, we can type-check the LLVM-IR at the level of municate the type of a Scilla value to the runtime library. Scilla types. The tag field is dereferenced at pattern matches, To this end we define type descriptor structs, defined tobe and based on which branch is taken, the pointer is cast to a identical in both the runtime library and in the generated %cnameI pointer and used. code. Every type in the program being compiled will have We use packed LLVM structs to represent ADT values a type descriptor built for it. A global type descriptor table so that, when we build or deconstruct ADT values in the is added into the compiled LLVM module, and interactions runtime library, we do not have to bother with architecture with the runtime library that require the type of a value will specific struct packing / padding. use the type’s index in this table. 3.4 Maps 3.2 Primitive Types Maps are boxed and hence handled using an opaque pointer. • Integer types: Scilla has signed and unsigned integer We rely on the runtime library to create a map and perform types with widths 32, 64, 128 and 256. These types are operations on it. In the runtime library, maps are defined mapped to LLVM integer types, but with a wrapper struct as unordered_map. This map’s value type type. The wrapper struct enables having a 1-1 relation b/w is std::any because it needs to be able to represent any Scilla types and their translated LLVM types (because Scilla type, including nested maps and program specific LLVM does not distinguish b/w signed and unsigned in- user-defined types. tegers). For example, Int32 translates to the LLVM type %Int32 = type { i32 } 3.5 Messages • String types: String and ByStr (byte strings of arbitrary Message types in Scilla are used to create events, exceptions size) in Scilla translate to LLVM structs containing a and outgoing messages. Message objects are created similar pointer to their contents and an integer field with the size. to tuples in other languages, but with each component being • ByStrX : Fixed-sized byte strings (i.e., X is known at com- given a name. In other words, they do not have an explicit pile time) translate to LLVM array type [X × i8]. predefined type. To enable the runtime library to work with Message objects, we encode them as a sequence of triples, 3.3 ADTs each consisting of a name, type descriptor, and the value Algebraic Data Types (also called variants or tagged unions) itself. The sequence is headed by an integer indicating the are composite types that have one or more “constructors” number of fields. (sum type), each of which allow defining the value to bea tuple (product type). Figure3 shows an integer list defined 3.6 Closures 1 in Scilla. It has two constructors Nil and Cons . All Scilla functions are represented as closures, irrespective Because different constructors may be used to construct a of whether they have free variables or not. In the generated value of a specific ADT, ADTs cannot easily be represented LLVM-IR, a closure is represented by an anonymous struct unboxed. We represent ADT values via a pointer to a packed type { fundef_sig*, void* } where fundef_sig is the struct containing the actual ADT value. signature of the LLVM function definition. The void * rep- resents the environment pointer. A stronger type isn’t used 1At the time of writing this, Scilla does not support user-defined self- for the environment pointer because we want to represent referencing ADTs. List is a builtin type. different Scilla functions with the same type, but whose 3 Vaivaswatha Nagaraj, Jacob Johannsen, Anton Trunov, George Pîrlea, Amrit Kumar, and Ilya Sergey

environments may be different, in a uniform way. By conven- been removed from the tree. Considering that a previous tion, all generated LLVM functions will take an environment attempt at reviving this wasn’t successful, we decided to pointer as the first argument. If the function’s return type adapt the code into a standalone tool [11]. cannot be “by value”, then the second argument will be a stack pointer (“sret”) where the return value must be stored. 5.2 Shared Definitions with Pre-compiled Code To avoid ABI complexities, generated LLVM functions and Many Scilla operations are implemented in the Scilla run- the hand written functions in the runtime library that they time library (SRTL). This requires invoking functions in SRTL may call, all follow the simple rule that if the value size is from the dynamically (JIT) compiled Scilla code and pass- larger than two eightbytes [7], we define that parameter (or ing Scilla values. We currently specify common type and return value) to be passed by reference (stack pointer). function definitions both when generating LLVM-IR (in the compiler) as well as in .h files in SRTL. This requires both 4 Evaluation definitions to be kept in sync. While this can be automated The Scilla compiler and the runtime library are still under by compiling the .h file to LLVM-IR and using that during active development and are not feature complete. We do compilation, a more systematic (LLVM provided) method for however have interesting Scilla programs and synthetic doing this may lead to a cleaner approach. tests that we can compile and execute end-to-end. 1. Simple-Map: This is a simple map access contract that References is representative of common constructs used in a typical [1] Andrew W. Appel. 1992. Compiling with Continuations. Cambridge University Press. Scilla contract. It fetches a map entry, does a simple [2] Richard S. Bird and Philip Wadler. 1988. Introduction to functional computation on it and stores back the result. While the programming. In Prentice Hall International series in computer science. interpreted execution takes about 5ms to execute this, the [3] Richard Eisenberg and Simon Peyton Jones. 2017. Levity polymor- compiled code runs in about 0.5ms. phism. (June 2017), 525–539. https://www.microsoft.com/en-us/ 2. Ackermann: The Ackermann function, implemented in research/publication/levity-polymorphism/ [4] Jean-Yves Girard. 1972. Interprétation fonctionnelle et élimination des Scilla takes about 2.2s to compute ackermann(3,7). The coupures de l’arithmétique ’ordre supérieur. Thèse d’État. Université compiled code computes the same result in about 46ms. de Paris VII, Paris, France. 3. Church Encoding: This code computes Church-encoded [5] Anders Hejlsberg. 2004. Generics in C#, Java, and C++. A Conversation numerals with the base type equal to Uint32 and two with Anders Hejlsberg, Part VII by Bill Venners with Bruce Eckel. operations on them: addition and multiplication. We com- https://www.artima.com/intv/generics.html#part2. [6] Xavier Leroy. 1992. Unboxed Objects and Polymorphic Typing. In pute a term that evaluates to Church-encoded 131099 and POPL. ACM Press, 177–188. then we convert it to the native Uint32. The interpreted [7] H.J. Lu, Michael Matz, Milind Girkar, Jan HubiÄŊka, Andreas Jaeger, execution takes about 0.7s while the compiled execution and Mark Mitchell. 2018. System V Application Binary Interface. (2018). completes with the result in about 4ms. https://github.com/hjl-tools/x86-psABI/wiki/x86-64-psABI-1.0.pdf [8] Ronald Morrison, Alan Dearle, Richard C. H. Connor, and Alfred L. These experiments do not include the time taken by the Brown. 1991. An Ad Hoc Approach to the Implementation of Poly- Scilla LLVM-IR compiler or the LLVM JIT compiler, but morphism. ACM Trans. Program. Lang. Syst. 13, 3 (1991), 342–371. just the time spent in the actual execution. (1) We haven’t [9] Flemming Nielson, Hanne Riis Nielson, and Chris Hankin. 1999. Prin- prioritized compile time improvements yet, so we expect ciples of program analysis. Springer. [10] Simon L. Peyton Jones. 1987. The Implementation of Functional Pro- it to be high and not worthy of benchmarking. (2) In the gramming Languages. Prentice-Hall. production environment, we cache compiled code on disk, [11] Zilliqa Research. 2020. DebugIR: Debugging LLVM-IR Files. https: and the most frequently used ones in memory. So we expect //github.com/vaivaswatha/debugir the impact of compile times to be low. [12] Zilliqa Research. 2020. Scilla - A Smart Contract Intermediate Level Language. https://github.com/Zilliqa/scilla. [13] Zilliqa Research. 2020. Scilla Compiler. https://github.com/Zilliqa/ 5 LLVM Feature Requests scilla-compiler. LLVM is a mature framework for building . While [14] Zilliqa Research. 2020. Scilla Virtual Machine. https://github.com/ it has largely simplified writing compiler frontends for new Zilliqa/scilla-vm. languages, in the course of writing the Scilla compiler we [15] John C. Reynolds. 1974. Towards a theory of type structure. In Pro- gramming Symposium (LNCS), Vol. 19. Springer, 408–423. found the following features, which, if implemented in LLVM, [16] Ilya Sergey, Vaivaswatha Nagaraj, Jacob Johannsen, Amrit Kumar, would further simplify and ease projects such as ours. Anton Trunov, and Ken Chan Guan Hao. 2019. Safer smart contract programming with Scilla. PACMPL 3, OOPSLA (2019), 185:1–185:30. 5.1 DebugIR [17] Andrew Tolmach and Dino P. Oliva. 1998. From ML to Ada: Strongly- Typed Language Interoperability via Source Translation. J. Funct. While attaching source debug information to LLVM-IR is Program. 8, 4 (July 1998), 367âĂŞ412. useful for debugging, at times, for a compiler developer, being [18] Zilliqa Team. 2017. The Zilliqa Technical Whitepaper. https://docs. able to debug the LLVM-IR itself can save time. LLVM’s zilliqa.com/whitepaper.pdf Version 0.1. -debug-ir pass served exactly this purpose, but has long 4