# Meta-compilation of Baseline JIT Compilers with Druid

Nahuel Palumbo, Guillermo Polito, Stéphane Ducasse, Pablo Tesone

► To cite this version:

Nahuel Palumbo, Guillermo Polito, Stéphane Ducasse, Pablo Tesone. Meta-compilation of Baseline JIT Compilers with Druid. The Art, Science, and Engineering of Programming, 2025. hal-05306190

HAL Id: hal-05306190

https://hal.science/hal-05306190v1

Submitted on 9 Oct 2025

HAL is a multi-disciplinary open access
archive for the deposit and dissemination of sci-
entific research documents, whether they are pub-
lished or not. The documents may come from
teaching and research institutions in France or
abroad, or from public or private research centers.

L'archive ouverte pluridisciplinaire HAL, est
destinée au dépôt et à la diffusion de documents
scientifiques de niveau recherche, publiés ou non,
émanant des établissements d'enseignement et de
recherche français ou étrangers, des laboratoires
publics ou privés.

<figure><img src="image_1.png" /></figure>
# Meta-compilation of Baseline JIT Compilers with Druid

Nahuel Palumbo<sup>a ID</sup>, Guillermo Polito<sup>a ID</sup>, Stéphane Ducasse<sup>a ID</sup>, and Pablo Tesone<sup>a ID</sup>

a Université de Lille, Inria, CNRS, Centrale Lille, UMR 9189 CRISTAL, Lille, France

**Abstract** Virtual Machines (VMs) combine interpreters and just-in-time (JIT) compiled code to achieve good performance. However, implementing different execution engines increases the cost of developing and maintaining such solutions. JIT compilers based on meta-compilation cope with these issues by automatically generating optimizing JIT compilers. This leaves open the question of how meta-compilation applies to baseline JIT compilers, which improve warmup times by trading off optimizations.

In this paper, we present Druid, an ahead-of-time automatic approach to generate baseline JIT compiler frontends from interpreters. Language developers guide meta-compilation by annotating interpreter code and using Druid's intrinsics. Druid targets the meta-compilation to an existing JIT compiler infrastructure to achieve good warm-up performance.

We applied Druid in the context of the Pharo programming language and evaluated it by comparing an autogenerated JIT compiler frontend against the one in production for more than 10 years. Our generated JIT compiler frontend is 2x faster on average than the interpreter and achieves on average 0.7x the performance of the handwritten JIT compiler. Our experiment only required changes in 60 call sites in the interpreter, showing that our solution makes language VMs **easier to maintain and evolve in the long run**.

ACM CCS 2012

* Software and its engineering → Just-in-time compilers;

**Keywords** JIT compiler, interpreter, meta-compilation, code generation, virtual machine

# The Art, Science, and Engineering of Programming

**Submitted** September 30, 2024

**Published** February 15, 2025

**DOI** 10.22152/programming-journal.org/2025/10/9

<figure><img src="image_2.png" /></figure>
1

**Introduction**

Language Virtual Machines (VMs) use multiple execution tiers to optimize programs at run time [4, 2]. They are often structured around an interpreter and one or more just-in-time (JIT) compilers to achieve a good balance between runtime, program analysis, and compilation time [17, 37, 12, 52].

In such architectures, programming language features must be implemented more than once, often in different ways, making these machines difficult to build, understand, and extend [21]. Reimplementing the same features at different levels of abstraction increases the risk of introducing bugs due to differences in the execution [42]. These facts increase the cost of language implementations and disrupt the tasks between language and VM developers (c.f., Section 2).

Meta-compilation approaches [52, 10, 53, 46] propose to reduce such complexity and risks by automatically generating compiled code from interpreters. These solutions are based on the first Futamura projection, applying partial evaluation to the interpreter for a given program to generate optimized machine code [23]. Language developers focus on developing a language interpreter, which is simpler to implement and debug than a compiler [21]. In these approaches, meta-compilation happens at run time, making them sensitive to higher compilation and warmup times.

In this article, we present Druid, an ahead-of-time meta-compiler that generates baseline JIT compiler code from interpreters, inspired by the second Futamura projection [23]. Our approach is not based on partial evaluation, but a hand-written meta-compiler (*Cogen*) [9] (c.f., Section 3). More concretely, we propose to target the code generation to an existing JIT compiler *backend* (c.f., Section 4), thus generating a compiler *frontend* [48]. Our solution introduces interpreter annotations and intrinsic functions language that developers use to guide meta-compilation. Moreover, Druid stages computations to be executed by the JIT compiler instead of run time.

This architecture allows the separation of concerns and maintenance of VM components. Language interpreters are defined by language implementors based on the interpreter framework while JIT compiler front ends are automatically generated ahead of time by the Druid meta-compiler.

In this paper, we study the following research questions:

**RQ1:** Does our solution generate JIT compilers that improve the performance of the interpreter?

**RQ2:** Is the JIT compiler generated by Druid competitive in performance against the handwritten version?

**RQ3:** Does our generated approach increase the overhead at compile time?

**RQ4:** How much development effort is affected by our automatic approach?

**Results** We evaluated our approach using the Pharo VM in production for over 20 years (c.f., Section 5). The Pharo VM is based on an interpreter and a JIT compiler handwritten by experts that we used as comparison baseline [37, 36].

Our results show that the JIT compiler generated by Druid is 2x faster than the interpreter on average, and 5x in our best scenario. Moreover, our prototype achieves
up to 0.7× the performance of the handwritten JIT compiler front end engineered over 10 years. Finally, our autogenerated front end shows JIT compile times similar to the handwritten one (difference of 0.18% on average).

## Contributions

* A description of Druid, an ahead of time meta-compiler generating baseline JIT compiler frontends from interpreters.

* The changes introduced in the interpreter and JIT compiler architecture to implement our solution using the production Pharo VM.

* A comparison of runtime performance, development effort, and JIT compilation overhead between the autogenerated front end and the handwritten one.

# 2 Background: Translating Interpreters to Baseline JITs

This section introduces the basic concepts required throughout the paper.

## 2.1 Interpreters, Compilers and JIT Compilers

The goal of a compiler is to translate a program written in a source language to an equivalent program written in a target language. Usually, when comparing interpreted vs. compiled language implementations, such translation has the goal of reducing execution time, assuming that a program will be compiled once and executed many times. In other words, the goal is to satisfy the following equation.

$$ target = compiler(source) \qquad (1) $$

$$ t_{interpreter}(source, input) > t_{target}(input) \qquad (2) $$

Where $t_{interpreter}(source, input)$ represents the time interpreting *source* with input *input*, and $t_{target}(input)$ represents the time executing the compiled *source* in *target* language with input *input*.

JIT compilers take such constraints to the extreme because compilation happens at run time. The following equation introduces $t_{compiler}(source)$, which represents the time compiling *source* to a *target* language. When compilation time is considered at run time, the sum of the compilation time and the execution time should not be greater than the interpreter time for compilation to be worth.

$$ t_{interpreter}(source, input) > t_{compiler}(source) + t_{target}(input) \qquad (3) $$

JIT compilers are built around this compilation time vs. run time tradeoffs. Optimizing JIT compilers minimize $t_{target}$ at the cost of higher $t_{compiler}$ times. Baseline JIT compilers minimize $t_{compiler}$ at the cost of the optimizations applied. Most of the time, baseline JIT compilation removes interpretation overhead.

Optimizing and baseline JIT compilers are combined in practice to achieve the best of both worlds: lots of methods that are rarely executed are compiled with baseline JIT compilers, and few methods that are commonly executed are compiled with optimizing JIT compilers [27, 25].
## 2.2 VMs as Multi-staged Machines

A virtual machine can be seen as a multi-staged engine because it usually contains more than one execution engine with different properties [25, 27].

**Definition 1 (Interpreter).** An interpreter is a one-stage engine: it directly executes source code to get the result. It easily interacts with other runtime components, such as the memory manager and the garbage collector, because they are defined at the same level of abstraction.

**Definition 2 (JIT compiler).** A JIT compiler is a two-stage engine: instead of executing source code, it generates residual (machine) code with the same behavior as the source code, that will later be executed at run time. Such translation requires, for example, understanding the transformation of code between two different representations/abstraction levels (e.g., stack bytecode to register machine code), the difference between what can be executed at each stage (compile time vs. run time), and deciding the scope of the compilation i.e., when should the compiler stop and emit a runtime function call to e.g., run the GC.

When in multi-staged machines, only one tier (typically the interpreter) must support the entire language implementation. Other tiers usually trade-off space by execution time, compiling only common paths and falling back to the interpreter when the rare paths are hit.

## 2.3 Meta-compilation Targetting Baseline JIT Compilers

In this paper, we study the meta-compilation of baseline JITs. As stated before, the goal of the baseline JIT compiler is to compile fast. To generate a baseline JIT compiler from an interpreter, meta-compilation can be engineered as a **one-stage to two-stage execution engine transformation**.

Interpreter >> bytecodeDup
self push: self stackTop

JITFrontend >> gen_bytecodeDup
self MoveMw: o r: SPReg R: TempReg.
self PushR: TempReg

<caption>■ **Figure 1** Interpreter vs. Baseline JIT frontend for bytecode duplicate top.</caption>

To illustrate this transformation, Figure 1 shows an example of the bytecode that duplicates the top of the stack as defined in the Pharo virtual machine [29, 37]. The bytecode is defined both in the interpreter (left) and in the compiler frontend (right) using a dynamic machine code generator named Cogit [36]. Our solution, Druid, is a source-to-source meta-compiler for the Pharo VM that takes as input the Pharo interpreter (left of Figure 1) and outputs the domain-specific language used to implement the JIT compiler frontends for the Pharo VM (right of Figure 1).

At JIT compile time (i.e., during program execution), if the bytecode *dup* is found in a stream of bytecodes, the Cogit JIT compiler executes the method *gen_bytecodeDup*
to generate the intermediate representation of the method under compilation. Then, it generates machine code and executes it at run time to obtain a program's result. The details of the solution are found in Section 3.

## 2.4 Challenges of Baseline JIT Generation from Interpreter Definition

Our meta-compilation approach translates interpreter code into JIT compiler code that implements the same semantics. However, JIT compilation can take advantage of the translation step to weave decisions into the generated code for optimization purposes. For example, not interpreter features are worth being JIT-compiled: compiling slow paths in the handler definition may occupy additional space and execute infrequently, thus the JIT compiler could emit a call to the interpreter instead. Also, the JIT compiler can take advantage of compilation invariants such as values that are JIT compile-time constants, and fold them.

The challenge is thus to embed such decisions into the generated code, without runtime information during meta-compilation. This presents the following challenges:

**Ahead-of-time compilation scoping.** The baseline JIT meta-compiler needs to statically decide what language handler paths should be compiled. A VM containing many execution engines must have at least one supporting the entire language specification. This is typically the role of an interpreter. On top of that, JIT compilers can decide to compile, for an instruction handler, a subset of all paths and fall back on the slower engines for other cases. This allows JIT compilers to focus on common or important language features. Although traditional optimizing JIT compilers drive those decisions using profiling information obtained at run time, an ahead-of-time meta-compiler should provide mechanisms to statically determine the slow paths inside the handler definition to avoid their (meta-)compilation.

**Switching between generated code and the runtime system.** The baseline JIT meta-compiler needs to statically decide when to introduce runtime calls. On the one hand, interpreters are typically implemented in the same programming language as the rest of the runtime (e.g., the memory manager, the JIT compiler), and thus they have a seamless integration. On the other hand, JIT compilers need to explicitly manage the interactions between JIT-compiled code and the runtime environment e.g., if JIT-compiled code invokes the GC, values stored in registers must be spilled in the stack if the GC traverses the stack to find roots.

**Different staging levels.** The baseline JIT meta-compiler needs to statically decide whether operations should be executed ahead of time, JIT-compile time, or run time, producing a residual program that will execute code at different stages. As said in Section 2.2, interpreters are one-stage machines that directly execute code. Compilers are two-stage machines where compilation decisions are taken statically and ahead of time. JIT compilers, instead, have runtime information e.g., some properties and values are known constant at JIT compile time and can be used to drive optimizations.
## 2.5 Vocabulary: Bytecode and Primitive Methods

For clarity of the presentation, this subsection presents some vocabulary points. As a Smalltalk descendant, the Pharo programming language is implemented in terms of *bytecode* and *primitive instructions* [26].

**Bytecode instructions.** Bytecode instructions are used as a VM intermediate language. Pharo source code is compiled to a sequence of stack-based bytecode instructions e.g., push instance variable, duplicate the top of the stack.

**Primitive Methods.** Primitive methods are normal Pharo methods that are annotated to call a primitive instruction (see below).

**Primitive instructions.** Primitive instructions are native operations exposed by the virtual machine, equivalent to native methods in other language implementations [2]. Primitive instructions implement basic functionality such as arithmetics and object allocation. By design, the primitive instructions can *fail* and activate a fall-back bytecode method. Otherwise, when a primitive instruction succeeds, the execution engine returns control to the caller with the result.

From the point of view of this paper, we will consider both bytecode and primitive instructions as VM instructions. Some functionality in our VM implementation is provided as both bytecode and primitive instructions, duplicated for performance reasons e.g., integer addition. Moreover, primitive instructions tend to be larger and more complex than bytecode instructions [42].

**Instruction Handlers** In the PharoVM, both the interpreter and the JIT compiler front end are implemented using a table dispatch approach. Bytecode opcodes are mapped to functions called *handlers*. Handlers in the interpreter execute the instruction, while handlers in the JIT compiler frontend generate nodes in the JIT compiler intermediate representation (IR). In the rest of this paper, as a shortcut, we refer to the handlers in the JIT compiler frontend as *IR generators*. Figure 2 illustrates the handlers on the JIT compiler front end.

<figure><img src="image_3.png" /><figcaption>■ **Figure 2** The JIT compiler front end uses a table dispatch approach, mapping bytecode opcodes to handler functions generating intermediate representation nodes.</figcaption></figure>
## 3 Druid: AoT Meta-compilation of a Baseline JIT Compiler

We built Druid,<fnref n="1" /> an ahead-of-time meta-compiler that generates JIT compiler frontend code from a language interpreter. Conceptually, Druid works as a compiler generator program (Cogen) [9]. We opted for simplicity and control to implement our meta-compiler as a hand-written compiler instead of using a partial evaluator [23, 25]. To prototype these ideas, we extended the Pharo VM interpreter framework [29, 37] to solve the challenges expressed in Section 2.4:

**Intrinsics and annotations drive scoping and runtime interactions.** The interpreter code is annotated and extended with intrinsics to guide the JIT frontend generation.

**Staging separates JIT compile time vs run time.** A staging pass at the end of the compilation pipeline statically determines code that is executable at JIT-compile time.

### 3.1 Druid Meta-compilation Overview

<figure><img src="image_4.png" /><figcaption>■ **Figure 3** Druid's meta-compiler architecture. Druid generates ahead of time a baseline JIT compiler frontend from an interpreter, translating interpreter bytecode handlers into IR generator handlers. Druid generates an SSA-form control flow graph IR and optimizes it using traditional algorithms. Finally, it translates this IR to a JIT compiler front end deployed along the rest of the VM source code.</figcaption></figure>

As explained briefly in the previous section, Druid is a source-to-source ahead-of-time meta-compiler for the Pharo VM. We illustrate Druid in Figure 3. Druid's input is an interpreter and its output is the source code of its corresponding JIT compiler frontend. The Druid meta-compiler is engineered as a traditional multi-pass compiler with additional custom passes designed for meta-compilation.

<footnote n="1">¹ https://github.com/Alamvic/druid visited on 2024-02-04.</footnote>
Meta-compilation of Baseline JIT Compilers with Druid

**Druid's front end** (Figure 3, step ①) Druid's front end is a meta-interpreter (an interpreter's interpreter) that builds Druid's IR from the language interpreter. The meta-interpreter works as a message-directed translation, coping with Smalltalk's traditional *all computations are done through messages* (see Section 3.2). The unit of translation is a method defining a VM instruction (i.e., bytecode and primitive instructions), and we perform aggressive inlinings. This is possible because the VM interpreter framework ensures by construction that all calls are monomorphic.

**Druid's mid end** (Figure 3, step ②) At its core, Druid is a method-based compiler using an intermediate representation with a control flow graph in SSA form, maintaining use-def chains. Druid's mid-end optimizes its IR using traditional optimizations such as inlinings, constant folding, dead code elimination, and global value numbering [41].

**Druid's back end** (Figure 3, step ③) Druid's target is Cogit [36], a dynamic machine code generator that uses abstract interpretation to implement peephole optimizations [47]. We kept interpreter changes to the minimum, limiting ourselves to behavior-preserving refactorings. Druid's back end first lowers the IR to accommodate it to the target, in our case, an x64-86-inspired register transfer language (see Section 4). Then, it stages computations that can be executed at JIT compile time (see Section 3.4). Finally, it generates source code that is deployed as a JIT compiler front end.

## 3.2 Intrinsics and Message-Directed IR Generation

Druid's front end generates the compiler intermediate representation using a message-directed translation, instead of a typical syntax-directed translation. That is, translation rules are based on messages and their selectors instead of syntactic elements. On the one hand, most messages are translated as normal calls. On the other hand, Druid specifies special translation rules for a subset of selectors called *intrinsics*.

**Definition 3 (Intrinsic).** An intrinsic is a message-send that has a specific IR translation.

The main reason for this decision is that the Pharo Virtual Machine is written in Pharo itself, a Smalltalk object-oriented dynamically-typed programming language, and follows the Smalltalk tradition: the language has a small syntax, and thus all operations including arithmetics e.g., +, -, *, / and control flow are expressed as late-bound message sends. Thus, IR generation rules are written depending on the *message* found at call sites. This is possible because the VM is written in a style that assumes that all call sites have a single call target, allowing an efficient translation to C by the Slang VM generator [29, 37].

Our meta-compiler design further separates intrinsics into groups. Appendix A lists all our intrinsics by category. Guiding intrinsics that are important in our design are described separately in the sections that follow.

**Essential Intrinsics.** Intrinsics for standard Pharo code, implementing the semantics of standard library (e.g., arithmetics) and control flow operations (e.g., `ifTrue:`).
**VM-specific Intrinsics.** Intrinsics for VM semantics, such as stack access (e.g., push and pop operations), memory access, and type coercions. These have the same semantics as the Pharo-to-C translation done by Slang.

**Guiding Intrinsics.** Intrinsics to guide meta-compilation decisions. These intrinsics work mostly as interpreter no-ops except if specified differently.

Figure 4 shows an excerpt of code using `sumSmallInteger:withSmallInteger:ifOverflow:`, a VM-specific intrinsic that performs checked-arithmetic detecting overflow. Such an intrinsic is inlined when translating the interpreter to C, yielding no performance overhead, and is translated to a *jump if overflow* instruction in our JIT compiler backend.

Interpreter >> primitiveAdd

...  
result := self  
sumSmallInteger: smi1  
withSmallInteger: smi2  
ifOverflow: [ ^ self primitiveFail ].  
...

JITFrontend >> gen primitiveAdd

...  
self AddCq: -1 R: SendNumArgsReg.  
self AddR: ClassReg R: SendNumArgsReg.  
jump2 := self JumpOverflow: target.  
...

<caption>■ **Figure 4** Meta-compilation of a primitive using an intrinsic performing SmallInteger addition with checked overflow.</caption>

## 3.3 Guiding Intrinsics: Compiler and Interpreter Specific Code

The `druidIgnore:` and `interpreterIgnore:` intrinsics allow fine-tuning the compilation scope to ignore expensive compilation cases or to rewrite parts of the code that must be performed differently in the interpreter or the compiler. They allow us to control code that will be ignored by the JIT compiler or the interpreter, respectively. Our most common usage for such intrinsics is omitting compilation of interpreter-specific optimizations such as static type predictions [27], which are subsumed by polymorphic inline caches in the JIT compiled code [28].

Figure 5 illustrates the usage of `druidIgnore:` to meta-compile closure activation. During interpretation, a static-type prediction avoids expensive lookups. In JIT-compiled code, such optimization becomes useless in the presence of PICs.

## 3.4 Druid Staging Pass

Druid implements a staging pass that runs at the end of the pipeline, after all optimizations but just before register allocation and code generation. The staging pass is designed as a forward sparse data-flow problem following SSA use-def chains, similar to a constant propagation analysis [1], identifying JIT-constant expressions. After the analysis, all expressions identified as JIT-constant are marked for staging during the code generation pass.
Meta-compilation of Baseline JIT Compilers with Druid

Interpreter >> bytecodePrimValue

...
self druidIgnore: [
| rcvr isBlock |
rcvr := self stackTop.
isBlock := self isInstanceOfClassBlockClosure: rcvr.
isBlock ifTrue: [ ^ self primitiveFullClosureValue ] ].

self normalSendSpecialSelector: 25 argumentCount: 0

■ **Figure 5** Excerpt of the interpreter using the druidIgnore: intrinsic to avoid the compilation of static type prediction during closure application. This optimization is subsumed by message-send optimizations such as polymorphic inline caches.

To support expressions other than constants for staging, we extended the interpreter with intrinsic functions to explicitly annotate expressions for staging, which is equivalent to a *lift* operation in other meta-compilers [44, 25]. Figure 6 presents an excerpt of code that illustrates how we have used this intrinsic to stage, for example, the load of method literals, which remain constant during the life-cycle of a method.

Interpreter >> literal: offset ofMethod: methodPointer

^ self druidStageable: [
  objectMemory
  fetchPointer: offset + LiteralStart
  ofObject: methodPointer ]

■ **Figure 6** Excerpt of the interpreter using the druidStageable: intrinsic to lift literal load operations as compile time constants.

## 3.5 Compilation Exit Points

*Compilation exit points* are annotations (*pragmas* in Pharo parlance [19]) marking interpreter methods that must avoid JIT compilation. Instead, those methods must output code performing a runtime call. Within bytecode instructions, exit points deoptimize the current stack frame spilling all registers into the stack, and perform a runtime call through a trampoline. Within primitive instructions, exit points skip to the end of the primitive instruction, compiling a fall-through so the compiler framework generates a call to the interpreter.

*Compilation exit points* are useful to scope the work of the JIT compiler, and the usage in our experiments was two-fold. First, from a JIT compiler design standpoint, it allows us to leave slow paths outside of the JIT compiler scope. Second, from an iterative development standpoint, it allowed us to incrementally extend the JIT compiler and have early, yet incomplete, working versions to experiment.

Figure 7 shows the annotation druidExitPoint that indicates to Druid to ignore the compilation on the method contextSize and all paths calling it.
Interpreter >> contextSize
"Special version of primitiveSize for accessing contexts."
<druidExitPoint>
....

■ **Figure 7** The exitPoint annotation allows Druid to skip complex operations (e.g., access to context information) that cause long compilation times, ignoring them by the JIT compiler and deoptimizing to the interpreter instead.

## 3.6 Compilation Metadata

We added metadata as method annotations to each instruction. Metadata declares the number of arguments of primitive functions, the delta left in the stack [14, 22], if the instruction defines a suspension point, and if the instruction requires a stack-frame for its execution. This metadata is required mainly by the target JIT compiler backend to decide frame-ful/less compilation and machine-code mappings for deoptimization. Moreover, our meta-compiler uses this metadata to transform the stack-based calling convention in the interpreter to the register-based calling convention in the target JIT compiler. Appendix A lists all druid annotations. We plan in the future to extract such annotations automatically using static analysis.

Figure 8 shows two annotated methods defining respectively a primitive and a bytecode instruction in the Pharo VM. The first method, defining the primitive that adds two small integers, declares the number of arguments to properly map the stack calling convention to the register-based one. The second method, defining the fast-path add bytecode, declares that the instruction contains a message-send and that it's program counter should be mapped because it contains a deoptimization point.

Interpreter >> primitiveAdd
<numberOfArguments: 1>
....

Interpreter >> bytecodePrimAdd
<compilationInfo: #isMapped>
<druidInfo: #hasSend>
....

■ **Figure 8** Metadata of language instructions as annotations in the interpreter definitions.

## 4 Targeting an Existing Compiler Framework: Cogit

For completeness, this section presents several implementation details that were important during the development of our prototype. Although important, we believe many of these points are implementation-specific and will be relevant for similar implementations only.
## 4.1 Targeting the Cogit Dynamic Machine Code Generator

Druid compilation targets Cogit, a JIT compiler backend with support for different architectures [36], developed originally for the OpenSmalltalk-VM [37, 29]. Cogit is a method-based dynamic machine code generator that takes as input a Pharo method. Cogit frontends must define an IR generator for each bytecode/primitive instruction. Our solution meta-compiles each bytecode and primitive handler defined in the interpreter to generate the IR generators for Cogit, as illustrated in Figure 3.

IR generators are defined using a register-transfer language, namely CogRTL, to build its intermediate representation (IR) from the source bytecode. CogRTL is designed as an intel-inspired 2 address code IR (2AC). Cogit uses a fixed list of registers with virtual names (e.g., TempReg, ReceiverReg, ClassReg) that are manually mapped ahead of time to machine registers by JIT-compiler developers.

**Cogit register allocation** Primitive method IR generators are defined as code templates in CogRTL. In this case, Cogit works mostly as an assembler: JIT frontend developers implement the instruction entirely in CogRTL and perform register allocation manually. Instead, bytecode compilation uses an abstract interpreter to optimize across the boundaries of each instruction [47]. The main optimization done by this abstract interpreter is register allocation as a peephole optimization: registers are tracked on an abstract stack, assigned dynamically, and spilled when needed. The current register allocator uses only the value stack, reducing the scope of register allocation to single statements.

**Cogit runtime interactions** As explained above, Cogit transforms stack-based bytecode to register-based machine code and tries to avoid stack accesses across bytecode boundaries. Cross-runtime calls are done through trampolines, small machine-code routines that implement the system calling convention when jumping from generated machine code to runtime, and vice-versa. Register values must be spilled to the stack when performing machine code to runtime calls. This is because runtime functions assume that all data is on the stack, e.g., the GC traverses the stack to find graph roots. Stack spilling needs to be manually managed by the JIT front end developer and is not automatically performed by trampolines.

**Cogit polymorphic inline caches** The Cogit JIT compiler infrastructure has built-in support for dynamically-bound message-sends using (polymorphic) inline caches (ICs) [28]. ICs are implemented by machine code patching. They support unbound call sites linked to a *miss* trampoline, monomorphic call sites linked directly to methods, and polymorphic and megamorphic call sites linked to machine code stubs.

## 4.2 Accommodating Druid to the Cogit JIT Compiler

As mentioned in Section 3, the Druid compilation scope is one interpreter handler. From this, it generates a control flow graph intermediate representation (IR) in
3 address code (3AC) SSA form [16]. After applying all optimizations, Druid lowers the IR to accommodate CogRTL.

**Accommodating to Cogit register allocation** Druid performs a register allocation pass that performs 3AC to 2AC translation and targets Cogit's registers using the constraint approach found in [38]. When meta-compiling primitive instructions, Druid's register allocator assigns Cogit's fixed virtual registers. When meta-compiling bytecode instructions, we output instructions that will dynamically –at JIT compile time– allocate registers following the values in the abstract stack. Examples of both meta-compiling instructions are present in Appendix B.

**Accommodating to Cogit runtime interactions** When Druid translates runtime interactions, it must take care of spilling registers to the stack and respect the trampoline calling convention. Our current implementation supports only pre-existing trampolines.

**Accommodating to Cogit polymorphic inline caches** We decided in our current implementation to reuse existing support of ICs and call site linking. Message-sends are expressed in the interpreter through intrinsic functions generating a DRSend instruction. Druid translates DRSend instructions during code generation into a patchable call-site RTL sequence.

## 4.3 Changes to the Compiler Architecture

Besides introducing intrinsic functions and annotations in the interpreter, we performed several changes to the Cogit architecture. First, the existing frontend was hand-tailored to compiling Pharo code and thus it implemented workarounds to support advanced features instead of exposing well-defined APIs. Moreover, the compiler architecture was originally engineered to compile the entire language, preventing us from incrementally developing the compiler and experimenting with intermediate results.

**Supporting incremental compiler development** We extended the runtime to avoid the unnecessary re-compilation of methods that previously failed compilation. The Cogit JIT compiler originally worked as a *compile all or nothing* compiler, failing compilation and falling back to the interpreter as soon as a compilation error happened e.g., because register allocation did not satisfy some particular constraints, or a bytecode did not have a defined IR generator. This architecture worked well for the hand-written frontends where all the language was specified. However, this incurred a large JIT compilation overhead for our first experiments when working with partial translations.

**Partial compilation support** Following the previous point, we also extended the compiler with support for arbitrary *deoptimization points*. Deoptimization points allowed us to focus on fast execution paths and jump to the interpreter in the case
# Meta-compilation of Baseline JIT Compilers with Druid

of less common slow paths. Deoptimization points spill the value of registers on the stack, transfer the control to the runtime, deoptimize the stack frame, and transfer control to the interpreter.

## Improving the target compiler API
We extended the compiler API, particularly the abstract stack API, to simplify meta-compilation. The existing front end was hand-tailored and depended on undocumented workarounds on the compiler backend. Moreover, it used non-homogeneous calling conventions when activating methods, block closures, and even different trampolines. Instead of pushing such complexity to our meta-compiler, we improved the exposed APIs and simplified calling conventions.

## 5 Evaluation

To answer the research questions in Section 1, we evaluate how our prototype performs in comparison with the interpreter and the hand-written baseline JIT compiler of the Pharo VM.

Both JIT compilers—one using the Druid-generated frontend and the other using the hand-written version—use Cogit as the backend, thus they just diverge in the IR generators in the frontend.

### 5.1 Setup and Methodology

We built a set of four different variations of the Pharo VM. Each variation has a different behavior regarding interpretation vs. compilation, the amount of code that is compiled and supported optimizations:

* **InterpreterOnly.** Pharo VM without JIT compiler, only an interpreter implementing indirect threading. It implements the entire language encompassing 240 bytecodes and 263 primitive instructions. It is used as the baseline for speed-up measurements.

* **FullManualJITVM.** Pharo VM extending InterpreterOnly with a handwritten compiler frontend, in production for at least 10 years. It supports the compilation of 240 bytecodes and 130 primitive instructions. This production VM includes other optimizations such as *static type prediction* and *dynamic super-instructions*.

* **DruidJITVM.** Pharo VM extending InterpreterOnly with a Druid autogenerated compiler front end. It supports the compilation of 236 bytecodes and 52 primitive instructions. The four bytecodes missing from the above implement unused/experimental instructions from the Sista bytecode set [8] and the stack frame reification instruction (i.e., `pushThisContext`) that we considered part of the *slow paths* of a program (e.g., exceptions, debugging).

* **MirrorJITVM.** Pharo VM using a subset of FullManualJITVM compiling exclusively the same bytecodes and primitive instructions as *DruidJITVM*. We use this subset to fairly compare our solution's quality against the handwritten version in FullManualJITVM.
We run a set of classical benchmark programs written in Pharo on each VM. We use the Rebench framework [34, 35] to run the benchmarks. We run all benchmarks using 30 VM iterations. Micro benchmarks use 10 in-process with two (ignored) warmup iterations. This number of iterations showed enough to measure the behavior of the non-optimizing baseline JIT compilers. Macro benchmarks, consisting of running the test suite of several packages, use a single in-process iteration. For all the results below, we report the average of the execution time of each in-process iterations.

We build each VM and run the benchmarks using a Linux Debian 5.10.140-1 distribution on a AMD64 CPU E5620 @ 2.40GHz processor with 16 GB DDR3 of RAM.

## 5.2 Benchmarks

<table><caption>**Table 1** Relative speed-up between the VMs per benchmark. Higher is better. (D = *DruidJITVM*; I=*InterpreterOnly*; FM=FullManualJITVM; M=MirrorJITVM)</caption><thead><tr><th rowspan="2">Benchmark</th><th colspan="7">Speed-up</th></tr><tr><th>D/I</th><th colspan="3">Optimized</th><th colspan="3">Unoptimized</th></tr><tr><th></th><th></th><th>D/FM</th><th>D/M</th><th>M/FM</th><th>D/FM</th><th>D/M</th><th>M/FM</th></tr></thead><tbody><tr><td>BinaryTrees</td><td>3.08</td><td>0.66</td><td>0.66</td><td>1.00</td><td>0.69</td><td>0.69</td><td>1.00</td></tr><tr><td>Chameleons</td><td>1.50</td><td>0.95</td><td>0.97</td><td>0.98</td><td>0.96</td><td>0.97</td><td>0.99</td></tr><tr><td>ChameneosRedux</td><td>2.96</td><td>0.80</td><td>0.78</td><td>1.03</td><td>0.82</td><td>0.84</td><td>0.98</td></tr><tr><td>DeltaBlue</td><td>2.90</td><td>0.64</td><td>0.65</td><td>0.97</td><td>0.44</td><td>0.87</td><td>0.51</td></tr><tr><td>Fasta</td><td>0.92</td><td>0.39</td><td>0.65</td><td>0.60</td><td>0.45</td><td>0.69</td><td>0.65</td></tr><tr><td>KNucleotide</td><td>2.20</td><td>0.89</td><td>0.93</td><td>0.95</td><td>0.79</td><td>0.96</td><td>0.82</td></tr><tr><td>Mandelbrot</td><td>2.08</td><td>0.56</td><td>0.56</td><td>0.99</td><td>0.60</td><td>0.59</td><td>1.01</td></tr><tr><td>Meteor</td><td>2.32</td><td>0.51</td><td>0.52</td><td>0.98</td><td>0.67</td><td>0.69</td><td>0.97</td></tr><tr><td>NBody</td><td>2.28</td><td>0.61</td><td>0.63</td><td>0.97</td><td>0.62</td><td>0.69</td><td>0.89</td></tr><tr><td>PiDigits</td><td>1.08</td><td>0.97</td><td>0.98</td><td>0.99</td><td>0.99</td><td>0.98</td><td>1.01</td></tr><tr><td>RegexDNA</td><td>2.86</td><td>0.59</td><td>0.81</td><td>0.72</td><td>0.45</td><td>0.86</td><td>0.52</td></tr><tr><td>ReverseComplement</td><td>1.68</td><td>0.88</td><td>0.70</td><td>1.26</td><td>0.75</td><td>0.72</td><td>1.04</td></tr><tr><td>Richards</td><td>5.09</td><td>0.69</td><td>0.72</td><td>0.96</td><td>0.52</td><td>0.78</td><td>0.67</td></tr><tr><td>Slopstone</td><td>1.27</td><td>0.42</td><td>0.78</td><td>0.54</td><td>0.54</td><td>0.89</td><td>0.61</td></tr><tr><td>SpectralNorm</td><td>1.42</td><td>0.61</td><td>0.60</td><td>1.01</td><td>0.64</td><td>0.64</td><td>1.00</td></tr><tr><td>ThreadRing</td><td>0.95</td><td>0.91</td><td>0.98</td><td>0.93</td><td>0.99</td><td>1.05</td><td>0.94</td></tr><tr><td>Compiler</td><td>1.52</td><td>0.89</td><td>0.89</td><td>1.00</td><td>0.89</td><td>0.93</td><td>0.96</td></tr><tr><td>Microdown</td><td>1.09</td><td>0.59</td><td>0.61</td><td>0.98</td><td>0.68</td><td>0.72</td><td>0.95</td></tr><tr><td>Network</td><td>1.29</td><td>0.80</td><td>0.81</td><td>0.99</td><td>0.84</td><td>0.94</td><td>0.90</td></tr><tr><td>Startup</td><td>1.40</td><td>0.74</td><td>0.76</td><td>0.98</td><td>0.81</td><td>0.89</td><td>0.91</td></tr><tr><td>Best</td><td>5.09</td><td>0.97</td><td>0.98</td><td>1.26</td><td>0.99</td><td>1.05</td><td>1.04</td></tr><tr><td>Worst</td><td>0.92</td><td>0.39</td><td>0.52</td><td>0.54</td><td>0.44</td><td>0.59</td><td>0.51</td></tr><tr><td>Average</td><td>1.99</td><td>0.70</td><td>0.75</td><td>0.94</td><td>0.71</td><td>0.82</td><td>0.87</td></tr></tbody></table>
Meta-compilation of Baseline JIT Compilers with Druid

Our set of classical **micro benchmarks**<fnref n="2" /> is as follows:

**BinaryTrees.** An adaptation of *Hans Boehm's GCBench* for Garbage Collection.

**Chameleons.** Creates many threads to wait for mutex semaphores.

**ChameneosRedux.** Small problem size for *Chameleons* benchmark.

**DeltaBlue.** Classic object-oriented *constraint solver*.

**Fasta.** DNA sequence generation algorithm based on weighted random selection.

**KNucleotide.** Map the DNA letters into a hash table to accumulate count values.

**Mandelbrot.** Plot the Mandelbrot set on an N-by-N bitmap.

**Meteor.** Uses *ByteStrings* to solve a puzzle.

**NBody.** Classic n-body simulation of the solar system.

**PiDigits.** Generates and prints the first N digits of Pi.

**RegexDNA.** A simple regex pattern and actions to manipulate FASTA format data.

**ReverseComplement.** computes the reverse complement of a DNA sequence.

**Richards.** Simulates a *task dispatcher* on a multitasking operating system.

**Slopstones.** *Smalltalk Low-level Operation Stones*, a series of low-level operations.

**SpectralNorm.** Calculate the spectral norm of a N-by-N matrix.

**ThreadRing.** A token ring algorithm using threads and mutex semaphores.

In addition, we run some applications as **macro benchmarks**:

**Compiler.** Tests for source-to-bytecode Pharo compiler.

**Microdown.** Tests for markup documentation library.

**Network.** Tests for network communication library.

**Startup.** Just evaluate 1 + 1, measure the time for starting up the system.

**5.3 RQ1: Does our solution generates JIT compilers that improve the performance of the interpreter?**

This research question evaluates whether a Druid autogenerated baseline-JIT compiler frontend performs better than a pure interpreter. Figure 9 shows, for each benchmark, the execution time relative to *InterpreterOnly*. *InterpreterOnly* remains in 1× as it is our baseline. These same results are shown in the first column of Table 1.

Our results show that the generated JIT compiler frontend presents an average improvement of 2×. In the best scenario, it shows improvements up to 5.09× (Richards). From our macro benchmarks, we see a speedup between 1.09× (Microdown) and 1.52× (Compiler).

Only three negative results can be observed in the table. First, we can also observe that *PiDigits* and *ThreadRing* are not very sensitive to any JIT compilation, showing similar improvements for all compilers. Moreover, we observe that our solution negatively impacts the performance of *Fasta*, by 8%, when compared to the interpreter. From the results in the following Section 5.4, we see that the results of *Fasta* are

<footnote n="2">² Implementations are in https://github.com/guillep/SMark visited on 2024-02-04.</footnote>
<figure><img src="image_5.png" /><figcaption>■ **Figure 9** Speed up of the Pharo VM using the JIT compiler generated automatically by our approach (*DruidJITVM*) compared to *InterpreterOnly* (baseline). Higher is better.</figcaption></figure>
probably caused by low-quality generated machine code in a common case of that
benchmark.

**RQ1 Conclusion.** Our solution generates JIT compilers that improve the performance of just interpreted VMs by an **average of 2×, up to 5× faster**.

5.4 RQ2: Is the JIT compiler generated by Druid competitive in performance against the handwritten version?

This research question evaluates how well Druid performs in comparison with a 10-year-old matured JIT compiler front end. For this purpose, we compare *Druid-JITVM* with *FullManualJITVM* and *MirrorJITVM*. We use *FullManualJITVM* as an upper bound and main goal: our objective is that *DruidJITVM* approaches the performance of *FullManualJITVM*. However, such a comparison does not yield fair results as *FullManualJITVM* translates more code than our prototype.

To better understand the quality of our generated code against the handwritten
version, we compare *DruidJITVM* to *MirrorJITVM*. The translations of *MirrorJITVM*
and *DruidJITVM* define an isomorphism: all translations defined in *DruidJITVM* are
defined in *MirrorJITVM* and vice-versa. However, the translations of *MirrorJITVM*
were extracted from *FullManualJITVM*.

Figure 10 shows a boxplot of our benchmarks, with all results plotted relative to the InterpreterOnly. Table I (Optimized) shows the ratio of improvement between all three solutions. Results should be interpreted as follows:

- When *DruidJITVM* and *MirrorJITVM* are close, our generated JIT compiler is as good as the baseline. Conversely, if they are distant, this indicates potential improvements in generated front end.

- When *MirrorJITVM* and *FullManualJITVM* are close, the set of translations (byte-codes and primitives) properly represents the benchmark working set. Conversely, if they are distant, this indicates missing translations in our prototype.

From our results, *DruidJITVM* achieves on average 0.70× the performance of *Full-ManualJITVM*, and particularly 0.59× (*Microdown*) and 0.89× (*Compiler*) for our macro-benchmarks. *MirrorJITVM*, on the other side, is almost as fast as the handwritten one, 0.94× on average, except for benchmarks *Slopstone*, *Fasta* and *RegexDNA* where it is slower. *DruidJITVM* achieves on average 0.75× the performance of *Mirror-JITVM*.

Using these results, we conclude that *DruidJITVM* supports a good set of language
instructions (the same as the *MirrorJITVM*), but the quality of the JIT compiler
produced by our meta-compiler is not yet as good as the one found in *FullManualJITVM*
that has been matured for 10 years. For example, optimizations missing in *DruidJITVM*
that are present in *FullManualJITVM* are:

1. *static type prediction* for arithmetics and bit-wise manipulation [27].

2. *dynamic super-instructions* for comparisons and jumps [15].
<figure><img src="image_6.png" /><figcaption>■ **Figure 10** Speed up of VM using different versions of the JIT compiler: *DruidJITVM*, *MirrorJITVM* and *FullManualJITVM*. *InterpreterOnly* is used as the baseline.
Higher is better.</figcaption></figure>
To measure the impact of those optimizations in our benchmarks, we re-compiled all of them using unoptimized bytecode (avoiding the optimization cases in the VM). The results of re-run the benchmarks using the unoptimized bytecode are present in Table I (Unoptimized). We compare the results between optimized and unoptimized in Figure 11.

<figure><img src="image_7.png" /><figcaption>■ **Figure 11** Estimating the impact of generated code quality, missing optimizations, and unimplemented instructions between *DruidJITVM* and *FullManualJITVM*.</figcaption></figure>

The experiments using the unoptimized bytecode suggest that the 30% of the performance gap between *DruidJITVM* and *FullManualJITVM* is composed by:

* 6% of unimplemented VM instructions in the translation set.

* 7% of missing optimizations (i.e., *static type prediction* and *dynamic super-instructions*).

* The remaining 16% comes from the quality of the JIT-compiled code.

**RQ2 Conclusion.** Our solution generates a JIT compiler that achieves the 0.70× of the speed of the handwritten version matured for 10 years. Improving the set of translated instructions shows a potential performance improvement of 6%. Other 7% comes from missing optimizations in our meta-compiler such as static type predictions. The rest of the difference is due to the quality of the JIT-compiled code.

## 5.5 RQ3: Does our generated approach increase the overhead at compile time?

To assess that our approach does not generate JIT compiler front ends that introduce significant compilation overhead, we measure the compilation time per benchmark and compute what percentage of the total execution time it represents. Then we compare those values with the obtained for the *FullManualJITVM*. Results are presented in Table 2.

On average, the JIT compile time represents ~6% of the total run time for both VMs. There are no significant differences between *DruidJITVM* and *FullManualJITVM*. The biggest difference between both VMs is, for the micro-benchmarks, *PiDigits*
Runtime Classification of DruidJITVM

<figure><img src="image_8.png" /><figcaption>■ **Figure 12** Compile and execution times for *DruidJITVM*.</figcaption></figure>

and *ThreadRing*, already explained in Section 5.3. Moreover, the *Microdown* macro-benchmark presents a compilation overhead 2.3× bigger than *FullManualJITVM*. The authors believe that it represents a *compilation pathological case* in *DruidJITVM* due to the big JIT compilation overhead presented in this benchmark in comparison with the others. Figure 12 shows the absolute execution and compilation time for all benchmarks running the *DruidJITVM*.

Our benchmark results show, on average, a tiny overhead increment of 0.18% between *DruidJITVM* and *FullManualJITVM*. This means that our solution does not increase the overhead at JIT compile time.

**RQ3 Conclusion.** Druid's generated JIT compiler adds only 0.18% more overhead, on average, compared to the manually developed one, at JIT compile time. Both represent nearly the same portion of the total execution time for most benchmarks. This means that **our solution does not introduce significant additional overhead to the production VM at JIT compile time.**

## 5.6 RQ4: How much development effort is affected by our automatic approach?

In addition to the achieved performance and relative speed regarding a handwritten JIT compiler, we are interested in evaluating the impact of our solution on the development effort. So far, language developers have required compiler knowledge to extend the Pharo Virtual Machine.
Meta-compilation of Baseline JIT Compilers with Druid

<table><caption>**Table 2** Compile time relative to total runtime. The last column shows the difference between VMs. Lower is better.</caption><thead><tr><th colspan="4">Compile Time relative to total run time</th></tr><tr><th>Benchmark</th><th>DruidJITVM</th><th>FullManualJITVM</th><th>difference</th></tr></thead><tbody><tr><td>BinaryTrees</td><td>1.92%</td><td>2.50%</td><td>-0.58</td></tr><tr><td>Chameleons</td><td>5.22%</td><td>4.07%</td><td>+1.15</td></tr><tr><td>ChameneosRedux</td><td>8.96%</td><td>7.06%</td><td>+1.90</td></tr><tr><td>DeltaBlue</td><td>4.72%</td><td>5.41%</td><td>-0.69</td></tr><tr><td>Fasta</td><td>1.59%</td><td>2.67%</td><td>-1.08</td></tr><tr><td>KNucleotide</td><td>1.99%</td><td>1.99%</td><td>0.00</td></tr><tr><td>Mandelbrot</td><td>0.75%</td><td>0.94%</td><td>-0.19</td></tr><tr><td>Meteor</td><td>2.97%</td><td>5.08%</td><td>-2.11</td></tr><tr><td>NBody</td><td>1.46%</td><td>1.57%</td><td>-0.11</td></tr><tr><td>PiDigits</td><td>13.33%</td><td>8.78%</td><td>+4.55</td></tr><tr><td>RegexDNA</td><td>6.75%</td><td>9.56%</td><td>-2.81</td></tr><tr><td>ReverseComplement</td><td>4.73%</td><td>7.17%</td><td>-2.44</td></tr><tr><td>Richards</td><td>6.24%</td><td>5.77%</td><td>+0.47</td></tr><tr><td>Slopstone</td><td>4.64%</td><td>8.56%</td><td>-3.92</td></tr><tr><td>SpectralNorm</td><td>0.52%</td><td>0.60%</td><td>-0.08</td></tr><tr><td>ThreadRing</td><td>29.44%</td><td>25.34%</td><td>+4.10</td></tr><tr><td>Compiler</td><td>3.83%</td><td>4.48%</td><td>-0.65</td></tr><tr><td>Microdown</td><td>12.77%</td><td>5.57%</td><td>+7.20</td></tr><tr><td>Network</td><td>4.35%</td><td>4.57%</td><td>-0.22</td></tr><tr><td>Startup</td><td>5.86%</td><td>6.63%</td><td>-0.77</td></tr><tr><td>Best</td><td>4.64%</td><td>8.56%</td><td>-3.92</td></tr><tr><td>Worst</td><td>12.77%</td><td>5.57%</td><td>+7.20</td></tr><tr><td>Average</td><td>6.10%</td><td>5.92%</td><td>+0.18</td></tr></tbody></table>

This section estimates the development efforts by considering the amount of generated code (in KLOCs), the amount of handwritten code (in KLOCs) in the existing front end, and the number of modified interpreter call sites. We measured lines of code by selecting the involved Pharo methods and counting the lines of the source code, including comments and white lines. Figure 13 show our measurements for the different components, with and without applying Druid. Druid being a general compiler infrastructure, we considered the common parts (the IR, the optimizations) separately from the meta-compilation specific parts. We considered:

**Handwritten compiler frontend.** All IR generator methods (with prefix gen in Stack-ToRegisterMappingCogit hierarchy, for bytecodes, and CogObjectRepresentation-For64BitSpur hierarchy, for primitives; except the ones in Cogit superclass, they are part of the backend).
**Generated frontend.** All methods generated by the meta-compiler (with prefix `gen_` in DruidJIT class).

**Druid compiler infrastructure.** All methods in the Druid package, excluding tests, experimental features, and code inside the Druid-Cogit subpackage.

**Druid cogit extensions.** All methods in the Druid-Cogit and Druid-Staging subpackage.

<figure><img src="image_9.png" /><figcaption>■ **Figure 13** Lines of code for the different parts of our solution. Druid is a handwritten compiler comprising 8KLOCS of infrastructure plus 5KLOC of Cogit extension. It generates 13KLOC for the JIT compiler frontend, equivalent to the handwritten 3.3KLOC in the production VM.</figcaption></figure>

In each of the discussions below, we put in perspective such numbers, as manually written code tends to contain abstractions and avoid code repetitions for maintenance e.g., common code extracted in methods.

**Separation of concerns** The JIT compiler is one of the most complex components to maintain inside a virtual machine [31], by avoiding handwriting it we are lowering the barrier for many developers, non-experts in VMs, who want to contribute to the language. In the context of an open-source project, as in the case of Pharo, the *democratization* of the VM code is an important goal. Thanks to Druid’s architecture, language developers concentrate on an annotated interpreter and do not need compiler expertise to obtain a JIT compiler, thus reducing the maintenance of a specific language implementation. In our experiments, the Druid-generated JIT front end encompasses a total of 13.4KLOC equivalent to 3.3KLOC handwritten and maintained by VM-compiler developers over the last 10 years. Such a difference between our generated frontend and the manually maintained (~4x) is generated by Druid’s aggressive inlinings.

**Interpreter and runtime maintenance** Our solution has minimal impact on language developers who need only to use intrinsics and annotations to guide meta-compilation. In our experiments, we did our best to minimize modifications to the interpreter, ensuring it remained backward compatible. To evaluate the cost of interpreter modification, we collected all interpreter call sites using our intrinsics (described in Section 3). Such interpreter modifications represent only **60 modified call sites**. However, it
is important to notice that experience in language implementation, profiling, and
optimization is required to achieve decent performance.

**Trading-off meta-compiler maintenance** The main trade-off of our architecture is that language maintenance comes at the cost of higher VM infrastructure maintenance. However, Druid is a compiler infrastructure with a modular architecture comprising 13KLOC of Pharo code. 8KLOC out of 13KLOC make up Druid's common compilation infrastructure: IR instructions and optimization passes. This subset of Druid works as a standard optimizing compiler only. The additional 5KLOC implement passes required for meta-compilation: meta-interpretation of VM semantics, intrinsic implementations, and code generation targeting Cogit. These passes, used for JIT-compiler frontend generation, are in the same order of magnitude as the manually maintained front end.

This trade-off is softened because of many reasons. First, the maintenance of Druid and the Pharo language can be assured by developers with different skill sets. Moreover, the Pharo VM code, written in Slang [37], limits the libraries, idioms, and abstractions that can be used in VM code. However, Druid code is not limited by those constraints: developers can write idiomatic Pharo code. Second, compiler generation can easily accommodate tasks such as helping evolve an existing compiler e.g., by evolving the compiler IR and autogenerating the front ends targeting it. Finally, as we said above, Druid implements a traditional multi-pass compiler infrastructure, which can be leveraged in other contexts.

**RQ4 Conclusion.** Druid is an optimizing meta-compiler comprising 13K lines of pure Pharo code. It generates a total of 13.4KLOC of JIT-compiler front end, equivalent to handwritten 3.3KLOC. Modifying and maintaining those lines of code requires a completely different skill set than maintaining a language interpreter, which only required changes in 60 call sites. The size in KLOC of custom extensions are in the same order of magnitude as the manually written frontend, while separating the concerns of language semantics and its compilation. Moreover, Druid is implemented as a traditional multi-pass compiler infrastructure, which can be leveraged in other contexts. All in all, we believe that such an architecture turns language VMs **easier to maintain and evolve in the long run.**

5.7 Discussion and Lessons Learned

This work presents the viability of meta-compilation in translating interpreters defined
by instruction handlers into baseline JIT compiler generators using a traditional multi-
pass compiler enhanced with specialized frontend and backend extensions. Targeting
an existing JIT compiler architecture allow us to focus on the meta-compilation of
the language's features, avoiding JIT compilation details like specific architecture
instructions encoding.
Our approach confirms that meta-compilation can be achieved as an ahead-of-time solution, without being present during runtime execution and enabling development in a language different from the host VM. It also presents the necessity of including specific annotations or intrinsics within the interpreter code to guide meta-compilation effectively, as well as similar approaches.

While the performance achieved is a significant improvement over interpreters, the results also show space for further optimizations and quality improvements of the jitted code to close the gap with handwritten JIT compiler performance.

# 6 Related Work

In the last two decades, some projects based on (JIT) compiler generation and VM frameworks appeared.

## 6.1 JIT Compiler Generation

**Pypy and Truffle** The PyPy project generates a JIT Compiler from an interpreter written in RPython and based on meta-tracing [10, 43, 3]. It is used not only to develop a Python VM but also for other languages [11, 13, 35] Truffle is a language implementation framework written in Java on top of Graal Compiler [50, 52, 18]. It is based on a self-optimizing AST interpreter and JIT compilation via partial evaluation. There are implementations of many languages implemented using this approach [40, 35, 51, 45].

Both Truffle and Pypy are engineered to generate optimizing JIT compilers. In contrast, our goal is to generate a baseline JIT compiler to reduce warm-up time. Our meta-compiler knows which expressions in the interpreter code are computable at JIT compile time, thus generating staged optimizations. The code generated by our meta-compiler is installed with the remaining VM code, allowing developers to read, modify, or mix generated and handwritten code before the VM deployment.

Another difference between these approaches is the compilation and meta-compilation granularity. Truffle performs meta-compilation at runtime on a method level. Pypy performs meta-compilation ahead of time to generate a tracing interpreter. This tracing interpreter is executed at runtime to perform JIT compilation at a trace granularity. Druid performs meta-compilation ahead of time with an instruction handler granularity. Implementation-wise, since handlers are methods, Druid is a source-to-source method-level meta-compiler, translating interpreter bytecode handlers into JIT compiler bytecode handlers.

**Ahead of time meta-compilation** Latifi et al., propose to accelerate the IR generation of Truffle-based JIT compilers using the Graal Compiler ahead of time to pre-compute IR generators [32]. Their approach is based on the second Futamura projection [23].

This work is the closest to ours that we can identify. They generate a JIT compiler ahead of time without runtime information, like us. The fact that they generate IR to
Meta-compilation of Baseline JIT Compilers with Druid

accelerate the optimizing JIT compiler, while we generate a baseline one, makes both
approaches complementary in a VM with multiple execution tiers.

Another difference between the approaches is that we developed directly the meta-compiler without the need for partial evaluation. Pros and cons between these approaches are studied in [9].

**Targetting compiler backends** Different authors propose to generate code targetting dynamic low-level optimizing engines like VCODE [20], Dynamo [5], or SPUR [6]. This is similar to our approach based on targeting Cogit. We target Cogit not only because of its machine code generation but also for the JIT compiler infrastructure support (i.e., hot-spot detection, deoptimization, PICs).

6.2 Compiler Generation

In 1971, Futamura proposed to use partial evaluation to generate compilers via self-application [23]. Since then, many authors have followed that approach for various programming languages [7, 30]. A recent example is LuaAOT, a compiler for Lua derived from the interpreter [39]. Other authors propose a direct approach to generate compilers by writing program generator generators (Cogen) [9, 33].

These solutions generate a compiler from a language definition (e.g., an interpreter) and compilation hints (e.g., annotations), like our approach using intrinsics in the interpreter to guide meta-compilation. The key distinction lies in the kind of the generated compiler: our JIT compiler must interact with an additional runtime tier (i.e., hot-spot detection, deoptimizations), meanwhile, a standard compiler does not.

6.3 VM Frameworks

There are related works on how to build Virtual Machines efficiently. Most of them,
including ours, share the idea of decoupling the language definition from VM opti-
mizations.

**Efficient interpreters generation** Ertl, Gregg *et al.*, are responsible for the Vmgen project, a framework to generate efficient interpreters in C from a declarative definition [22, 14]. They have built a DSL to define the language semantics and apply staged optimizations to the generated interpreter (e.g., super-instructions).

While the goal of both projects is indeed different, (they generate interpreters and
we generate baseline JIT compilers from interpreters), their syntax to define optimiza-
tions is analogous to our guiding intrinsics. Both approaches are complementary and
help VM developers build efficient execution engines in a declarative way.

**Other approaches** Yermolovich *et al.*, have built a VM for LUA based on a hierarchical layering architecture of a host VM running a guest VM [53]. They optimize the guest VM using a tracing JIT compiler based on interpreter hints in the host VM. Geoffray, Thomas *et al.*, developed VMKit to have a common substrate that eases the development of high-level VMs [24].
These approaches are architectural and based on component reuse, ours is a gener-
ative approach. Druid meta-compilation happens ahead of time, targeting Cogit [36],
the JIT compiler backend defined on Slang.

7 Conclusion

In this paper, we presented Druid, a meta-compiler generating baseline JITs from interpreters. Druid introduces interpreter annotations and intrinsics that guide meta-compilation. Druid targets Cogit, a JIT compiler backend used in the Pharo VM. Our solution stages computations to be executed at JIT compile time, and finally lowers code to Cogit's register transfer language, generating a JIT compiler frontend.

Our generated front end improves interpreter performance by 2x on average and achieves 0.70x the performance of the existing JIT compiler frontend that has been manually hand-tuned for 10 years, without introducing significative JIT compile time overheads. We show that meta-compiling the Pharo interpreter required only 60 semantic-preserving changes in the interpreter, mostly refactorings and code annotations, and VM maintenance is know decoupled between the implementors of a particular language and the implementors of the compiler infrastructure. In the future, we plan to explore the ahead-of-time meta-compilation of peephole optimizations, *static type prediction* and *dynamic super-instructions* for comparisons and jumps.

**Acknowledgements** This work was funded by Inria's Action Exploratoire AlaMVic.

A Instrinsics and Annotations for Meta-compilation

This appendix presents the intrinsic functions and annotations supported by Druid
for the baseline JIT compiler generation in the Pharo VM.

A.1 Instrinsics

We perform inlinings of invoked functions during the abstract interpretation of the
VM instructions defined in the interpreter (i.e., meta-interpretation). However, there
are functions in the interpreter in which our meta-compiler has defined functions for
their interpretation, these are *instrinsic* (c.f., Section 3.2).

Tables 3 - 7 describe all the intrinsics used in Druid for the baseline JIT compiler generation in the Pharo VM. Intrinsic's implementations are defined in the meta-compiler to directly manipulate the Druid IR (control flow graph).

Figure 14 shows the implementation of ifTrue: intrinsic, where a conditional branch is inserted in the IR.
Meta-compilation of Baseline JIT Compilers with Druid

<table><caption>Table 3 Guiding Intrinsics used during meta-compilation.</caption><thead><tr><th>Function</th><th>Description</th></tr></thead><tbody><tr><td></td><td><strong>Druid</strong></td></tr><tr><td>druidIgnore:</td><td>Ignore code during meta-interpretation.</td></tr><tr><td>interpreterIgnore:</td><td>Ignore code by the interpreter but meta-interpreted.</td></tr><tr><td>druidStageable:</td><td>Mark code as stageable for meta-compilation.</td></tr><tr><td>druidForceInterpretation</td><td>Jump the execution to the interpreter (deoptimization).</td></tr></tbody></table>

<table><caption>Table 4 Essential Intrinsics used during meta-compilation.</caption><thead><tr><th>Function(s)</th><th>Description</th></tr></thead><tbody><tr><td></td><td><strong>Control flow</strong></td></tr><tr><td>value value: cull:<br>ifTrue: caseOf:otherwise:<br>whileTrue whileTrue:</td><td>Block closure activation.<br>Conditional control flow.<br>Condition control flow for loops.</td></tr><tr><td></td><td><strong>Comparisons</strong></td></tr><tr><td>= ~ = &lt; = &gt; &gt;= | &amp;</td><td>Operators for equality, inequality, less, less or equal, greater, greater or equals, or and and.</td></tr><tr><td></td><td><strong>Arithmetics</strong></td></tr><tr><td>+ - * / // \ negated</td><td>Operators for addition, subtraction, multiplication, division, integer division, mod and negation.</td></tr><tr><td></td><td><strong>Bit Manipulation</strong></td></tr><tr><td>&lt;&lt;&gt;&gt;&gt;&gt;</td><td>Operators and functions for left shift, right shift, arithmetic shift, and, or, xor, mask comparison and rotation.</td></tr><tr><td>bitAnd: bitOr: bitXor:<br>anyMask: rotateLeft:by:</td><td></td></tr></tbody></table>
<table><caption>**Table 5** VM-specific Intrinsics used during meta-compilation.</caption><thead><tr><th>Function(s)</th><th>Description</th></tr></thead><tbody><tr><td colspan="2"><strong>SmallIntegers</strong></td></tr><tr><td>sumSmallInteger:with:</td><td>SmallInteger object operations for addition, subtraction</td></tr><tr><td>ifOverflow:</td><td>and multiplication with custom overflow check.</td></tr><tr><td>subSmallInteger:with:</td><td></td></tr><tr><td>ifOverflow:</td><td></td></tr><tr><td>multiplySmallInteger:with:</td><td></td></tr><tr><td>ifOverflow:</td><td></td></tr><tr><td colspan="2"><strong>Stack access</strong></td></tr><tr><td>stackTop stackValue:</td><td>Read values from the stack.</td></tr><tr><td>pop: push: pop:thenPush:</td><td>Pop or push values from the stack.</td></tr><tr><td colspan="2"><strong>Memory access</strong></td></tr><tr><td>byteAt:put:</td><td>Load and write signed integers in 8, 32 and 64 bits.</td></tr><tr><td>longAt: longAt:put:</td><td></td></tr><tr><td>uint8At: uint8At:put:</td><td>Load and write unsigned integers in 8, 16, 32 and 64 bits.</td></tr><tr><td>uint16At: uint16At:put:</td><td></td></tr><tr><td>uint32At: uint32At:put:</td><td></td></tr><tr><td>uint64At: uint64At:put:</td><td></td></tr></tbody></table>
Meta-compilation of Baseline JIT Compilers with Druid

<table><caption>**Table 6** VM-specific Intrinsics used during meta-compilation (continuation).</caption><thead><tr><th>Function</th><th>Description</th></tr></thead><tbody><tr><td colspan="2"><strong>Message send</strong></td></tr><tr><td>normalLiteralSelectorAt:</td><td>Message send of a literal selector.</td></tr><tr><td>argumentCount:</td><td></td></tr><tr><td>normalSendSpISelector:</td><td>Message send of a special selector.</td></tr><tr><td>argumentCount:</td><td></td></tr><tr><td>sendSuper:numArgs:</td><td>Super message sends.</td></tr><tr><td>sendDirectSuper:numArgs:</td><td></td></tr><tr><td colspan="2"><strong>Method invocation</strong></td></tr><tr><td>iframeNumArgs:</td><td>Access to the number of arguments.</td></tr><tr><td>iframeMethod:</td><td>Access to the method object.</td></tr><tr><td>itememporary:in:</td><td>Load or store a value from a temporary variable.</td></tr><tr><td>itememporary:in:put:</td><td></td></tr><tr><td>fetchNextBytecode</td><td>Load the next bytecode to execute.</td></tr><tr><td>fetchByte</td><td>Load the next byte in the bytecode sequence.</td></tr><tr><td>commonReturn:</td><td>Return from a method or closure activation.</td></tr><tr><td>commonCallerReturn:</td><td></td></tr><tr><td>internalMustBeBoolean:</td><td>Throws a <em>MustBeBoolean</em> exception.</td></tr><tr><td>checkEventsCnxtSwitch:</td><td>Execution interruptions (GC and Threads management).</td></tr><tr><td colspan="2"><strong>Pharo features</strong></td></tr><tr><td>error: assert: deny:</td><td>VM errors and assertions.</td></tr><tr><td>createFullClosureInIndex:</td><td>Instantiate a Block closure object.</td></tr><tr><td>numCopied:ignoreContext:</td><td></td></tr><tr><td>executeFullCogBlock:</td><td>Activate a Block closure compiled to machine code.</td></tr><tr><td>closure:mayContextSwitch:</td><td></td></tr><tr><td>remember:</td><td>Adds an object in the Remembered Set [49].</td></tr><tr><td>newHashBitsOf:</td><td>Calls a function to create a hash number for an object.</td></tr></tbody></table>
<table><caption>**Table 7** Intrinsic variables in the meta-compiler.</caption><thead><tr><th>Variable</th><th>Description</th></tr></thead><tbody><tr><td colspan="2"><strong>Execution</strong></td></tr><tr><td>instructionPointer</td><td>Stores the current instruction pointer.</td></tr><tr><td>framePointer</td><td>Stores the current frame pointer.</td></tr><tr><td>stackPointer</td><td>Stores the current stack pointer.</td></tr><tr><td>stackLimit</td><td>Stores the limit of the stack.</td></tr><tr><td>argumentCount</td><td>Stores the argument count for the current method.</td></tr><tr><td>primFailCode</td><td>Stores the current fail code of a primitive execution.</td></tr><tr><td>classTableFirstPage</td><td>Pointer to the first page of the class table.</td></tr><tr><td colspan="2"><strong>Well known objects</strong></td></tr><tr><td>nilObj trueObj falseObj</td><td>Pointers to nil, true and false objects.</td></tr><tr><td>hiddenRootsObj</td><td>Pointer to the root objects table.</td></tr><tr><td colspan="2"><strong>Garbage collection</strong></td></tr><tr><td>scavengeThreshold</td><td>Pointer to the threshold for Scavenger.</td></tr><tr><td>freeStart</td><td>Pointer to the beginning of a free bunch of memory.</td></tr><tr><td>newSpaceStart</td><td>Pointer to the beginning of the New Space.</td></tr><tr><td>spaceMaskToUse</td><td>Stores a mask to know if an object is in the New, Old or Permanent Space.</td></tr><tr><td>newSpaceMask</td><td></td></tr><tr><td>oldSpaceMask</td><td></td></tr><tr><td>permSpaceMask</td><td></td></tr><tr><td colspan="2"><strong>Extension bytecodes</strong></td></tr><tr><td>extA extB</td><td>Stores values for extended bytecodes.</td></tr><tr><td>numExtA numExtB</td><td>Stores the number of extended bytecodes executed consecutively.</td></tr></tbody></table>

## A.2 Annotations

Druid uses *annotations* to declare meta-data for the VM instructions in the interpreter. The meta-data are analyzed during meta-compilation to guide the meta-interpretation and to have extra information required by the JIT compiler framework.

Table 8 describes all the annotations supported by Druid. Figure 15 presents an extract of the IR generators bytecode table for the JIT compiler generated by Druid using extra meta-data from annotations. The JIT compiler framework uses this information during the JIT compilation of methods on runtime.

## B Examples of Meta-compilation Results

In this section, we present some examples of VM instructions in the Interpreter and the version for a baseline JIT compiler meta-compiled by Druid as it is explained in
Meta-compilation of Baseline JIT Compilers with Druid

interpretIfTrueWith: aRBMessageNode

| conditionalJump trueBranchBasicBlockOut startingBasicBlock executionStateBeforeBranch |
"Condition"
aRBMessageNode receiver acceptVisitor: self.
conditionalJump := self
    instantiateNoResultInstruction: DRBranchIfCondition
    operands: { DREqualsThanComparison new. self popOperand. true asDRValue }.
self currentBasicBlock endInstruction: conditionalJump.

"Branch"
executionStateBeforeBranch := executionState copy.
trueBranchBasicBlockOut := self
    branchFrom: self currentBasicBlock
    onEdge: [ :branchEntryBlock | conditionalJump trueBranch: branchEntryBlock ]
    doing: [ self interpretBlockBody: aRBMessageNode arguments first ].

"Merge point"
startingBasicBlock := self currentBasicBlock.
self newBasicBlock.
self currentBasicBlock addPredecessor: startingBasicBlock.
conditionalJump falseBranch: self currentBasicBlock.

"Could happen that the evaluated block had a non local return.
In that case, this block should not arrive to this merge point
=> do not add the jump"

trueBranchBasicBlockOut hasFinalInstruction
ifTrue: [ executionState := executionStateBeforeBranch ]
ifFalse: [ executionState := self
    onBlock: currentBasicBlock
    mergeAll: { executionStateBeforeBranch. executionState copy }.
trueBranchBasicBlockOut jumpTo: self currentBasicBlock ]

<caption>Figure 14 Meta-interpretation of ifTrue: intrinsic.</caption>

Section 3 and using the intrinsics and metadata listed in Appendix A. Since the VM
instructions in the PharoVM are too complex, we present simplified versions of them
to show how Druid meta-compiles the main language features.

B.1 Meta-compilation of Primitives

Figure 16 presents the primitive to make the addition between two SmallInteger
objects (receiver and argument). At left is the definition in the interpreter and at right
is the meta-compiled version of that definition for the JIT compiler frontend.

The annotation numberOfArguments: defines which values are accessed by stackValue:
intrinsic. It is required because, by the calling convention in the machine code, argu-
ments (and the receiver) are passed by registers instead of the stack. Thus, values
accessed by the stack are replaced by expected registers (i.e., ReceiverResultReg,
ArgoReg) during meta-compilation.
<table><caption>**Table 8** Annotations with meta-compilation options.</caption><thead><tr><th>Annotation</th><th>Description</th></tr></thead><tbody><tr><td colspan="2" style="text-align: center; font-weight: bold;">For primitives</td></tr><tr><td> numberOfArguments: </td><td> One or many expected numbers of arguments for the primitive method, failing JIT compilation in other cases. </td></tr><tr><td> customisedReceiverFor: </td><td> Conditional JIT compilation based on primitive method class. </td></tr><tr><td> druidExitPoint </td><td> Primitive fails at this point on runtime. </td></tr><tr><td colspan="2" style="text-align: center; font-weight: bold;">For bytecodes</td></tr><tr><td> compilationInfo: </td><td> Adds extra information on bytecode declaration for the JIT compiler framework (i.e., mappings between bytecodes and JITted code, jumps, etc). </td></tr><tr><td> druidInfo: </td><td> Computes extra information during bytecode meta-compilation (i.e., message send invocations). </td></tr><tr><td> needsFrameNever: </td><td> Says if the bytecode has access to the frame (if none, the frame is not created in JITted code). </td></tr><tr><td> needsFrameIfInBlock: </td><td> </td></tr><tr><td> needsFrameIfImmutability: </td><td> </td></tr></tbody></table>

The annotation `customisedReceiverFor:` indicates that this primitive only works if the receiver is an instance of a special class, `SmallInteger` in this case. The meta-compiler optimizes it by assuming that the primitive method must be installed in the expected class and makes that check at JIT compile time (i.e., `mclassIsSmallInteger`) to avoid compilation in other cases. Several compiler optimizations remove the check of the receiver type at runtime, only the check for the argument is performed in JIT-compiled code (by inlining the `isIntegerObject:` function).

Finally, the intrinsic `sumSmallInteger:` with `ifOverflow`: is meta-compiled to deal with tag, perform the addition and check overflows. Meta-compilation of the intrinsic `pop:thenPush`: moves the result to the expected register (i.e., `ReceiverResultReg`), following the register-based calling convention. In cases where the primitive fails (i.e., `primitiveFail`), the flow jumps to the end of the generated machine code, where a fallback method will be executed.

## B.2 Meta-compilation of Bytecodes

Figure 17 shows the input (left) and output (right) of the meta-compilation of a bytecode responsive to push a receiver instance variable value to the stack.

The interpreter's definition uses the `currentBytecode` variable to compute the instance variable index. This variable contains the executed bytecode index in the bytecode table, thus the same definition is used in many entries for accessing different indexes. We meta-compile different definitions for each entry by staging the `currentBytecode` to the current number and constant-fold it, if possible. The example
Meta-compilation of Baseline JIT Compilers with Druid

JITFrontend >> bytecodeTable

```json
^ {
  ...
  { 1. 64. 64. #gen_PushTemporaryVariableBytecode0 }.
  ...
  { 1. 88. 88. #gen ReturnReceiver. #return. #isMappedInBlock. #needsFramedInBlock: 0 }.
  ...
  { 1. 92. 92. #gen_Returnd TopFromMethod. #return. #isMappedInBlock. #needsFramedInBlock:.
    → -1 }.
  ...
  { 1. 95. 95. #gen_ExtNopBytecode. #needsFrameNever: 0 }.
  { 1. 96. 96. #gen_BytecodePrimAdd. #isMapped }.
  ...
  { 1. 128. 128. #gen SendLiteralSelectoroArgsBytecode0. #isMapped }.
  ...
  { 1. 176. 176. #gen_ShortUnconditionalJumpo. #branch. #v3:ShortForward:Branch:Distance: }.
  ...
  { 1. 184. 184. #gen_ShortConditionalJumpTrueo. #branch. #isBranchTrue. #ismapped. #v3:
    → ShortForward:Branch:Distance: }.
  ...
  { 1. 200. 200. #gen_StoreAndPopReceiverVariableBytecode0. #isInstVarRef.
    → #ismappedIfImmutability. #needsFramedIfImmutability: -1 }.
  ...
  { 3. 255. 255. #unknownBytecode } }
```

<caption>■ **Figure 15** Extract of the bytecode table generated by Druid using meta-data.</caption>

in Figure 17 was meta-compiled for index = 1 in the bytecode table, so it accesses the
second instance variable (o-based). This value was constant folding, after the inline
of fetchPointer:ofObject: function, to offset = 16 (bytes) in the meta-compiled code
(8 bytes of the object header + 8 bytes of the first instance variable).

The way of accessing the receiver object from the frame in the interpreter is meta-
compiled by using the register-based calling convention (i.e., using ReceiverResultReg).
Since previous bytecodes in the compiling method could override the register value,
the definition requires calling ensureReceiverResultRegContainsSelf compiler function.

Bytecode meta-compilation implies dynamic register allocation at JIT-compile time
by using allocateRegNotConflictingWith:ifNone: compiler function. Each meta-compiled
bytecode definition keeps a variable (live) with the status of already used registers to
avoid conflicts.

To push the value, Druid uses the ssPushBase:offset: function exposed by the compiler
backend to track the values in the stack and perform peep-hole optimizations during
JIT compilation.

In the end, the fetchNextBytecode intrinsic, present in every bytecode definition in
the interpreter, is ignored during meta-compilation due to the JIT compiler framework
resolves the compilation of a sequence of bytecodes inside the method.
Interpreter >> primitiveAdd

<numberOfArguments: 1>
<customisedReceiverFor: #smallInteger>

| rcvr arg rawInt1 rawInt2 rawResult result |
"Get the values from the stack"
rcvr := self stackValue: 0.
arg := self stackValue: 1.
"Check small integer objects"
(memory isIntegerObject: rcvr)
ifFalse: [ ^ self primitiveFail ].
(memory isIntegerObject: arg)
ifFalse: [ ^ self primitiveFail ].
"Perform the addition, checking overflow"
result := self
sumSmallInteger: rcvr
with: arg
ifOverflow: [ ^ self primitiveFail ].
"Pop values and push the result"
self pop: 2 thenPush: result

JITFrontend >> gen_PrimitiveAdd
"AutoGenerated by Druid"

| jump1 jump2 currentBlock |
self mclassIsSmallInteger ifFalse: [ ^
  `-> UnimplementedPrimitive ].
self TstCq: 1 R: ArgoReg.
jump1 := self JumpZero: 0.
self MoveR: ArgoReg R: TempReg.
self AddCq: -1 R: TempReg.
self MoveR: ReceiverResultReg R: ClassReg.
self AddR: ClassReg R: TempReg.
jump2 := self JumpOverflow: 0.
self MoveR: TempReg R: ReceiverResultReg.
self genPrimReturn.
currentBlock := self Label.
jump1 jmpTarget: currentBlock.
jump2 jmpTarget: currentBlock.
^ CompletePrimitive

<caption>■ **Figure 16** Meta-compilation result of addition primitive for SmallInteger objects.</caption>

Interpreter >> bytecodePushReceiverVariable

| fieldIndex receiver |
fieldIndex := currentBytecode bitAnd: 16rF.
receiver := stackPages longAt: framePointer +
  `-> FoxIFReceiver.
self push: (objectMemory fetchPointer:
  `-> fieldIndex ofObject: receiver).
self fetchNextBytecode

JITFrontend >>
`-> gen_bytecodePushReceiverVariable_1
"AutoGenerated by Druid"

| live currentBlock to |
live := 0.
self ensureReceiverResultRegContainsSelf.
to := self
  allocateRegNotConflictingWith: live
  ifNone: [ ^ self unknownBytecode ].
live := live bitOr: (self registerMaskFor: to).
self MoveR: ReceiverResultReg R: to.
self ssPushBase: to offset: 16.
^ 0

<caption>■ **Figure 17** Push receiver variable bytecode meta-compilation with index = 1.</caption>

## B.3 Using the JIT Compiler Framework

Figure 18 introduces a conditional jump bytecode for short distances. The offset on the interpreter is computed based on the currentBytecode (the bytecode index in the table). Similar to the previously presented case, we meta-compile a version for each entry in the table (with offset = 1 in the given example).
Meta-compilation of Baseline JIT Compilers with Druid

Interpreter >> shortConditionalJumpTrue
<compilationInfo: #isMapped>
<compilationInfo: #branch>
<compilationInfo: #isBranchTrue>

| boolean offset |
offset := (currentBytecode bitAnd: 7) + 1.
boolean := self stackTop.
self pop: 1.
boolean = objectMemory trueObject
ifTrue: [ self jump: offset ]
ifFalse: [
  boolean = objectMemory falseObject
  self internalMustBeBoolean: boolean ].
  self fetchNextBytecode ]

JITFrontend >>

gen_ShortConditionalJumpTrue0
"AutoGenerated by Druid"

| jump1 s5 s2 currentBlock s12 to jump2 s9 |
self annotateBytecode: self Label.
to := ... "Allocate register"
(self ssValue: 0) copyToReg: to.
self ssPop: 1.
s5 := objectMemory trueObject.
self ssFlushStack.
self CmpCq: s5 R: to.
jump1 := self JumpNonZero: 0.
s9 := bytecodePC + 2.
self Jump: (self ensureFixupAt: s9).
jump2 := self Jump: 0.
currentBlock := self Label.
jump1jmpTarget: currentBlock.
s12 := objectMemory falseObject.
self CmpCq: s12 R: to.
jump1 := self JumpZero: 0.
self MoveR: to R: TempReg.
self CallRT:
  ceSendMustBeBooleanTrampoline.
currentBlock := self Label.
jump2jmpTarget: currentBlock.
jump1jmpTarget: currentBlock.
^ 0

<caption>Figure 18 Conditional true jump bytecode meta-compilation with offset = 1.</caption>

The annotation with the compilation info isMapped in the interpreter definition indicates that this bytecode needs to save the link between the bytecode index and the address of the JIT-compiled code because of deoptimization possibilities. This link is created using the annotateBytecode: function, exposed by the JIT compiler framework, at the beginning of the JITFrontend definition. The deoptimization case occurs when the internalMustBeBoolean: intrinsic is executed.<fnref n="3" /> As this case produces an exception and escapes from the fast path that we want to JIT compile, this intrinsic is meta-compiled as a call to the trampoline ceSendMustBeBooleanTrampoline to manage the case by the runtime system. The trampolines are generated by the JIT compiler framework at the beginning of execution and deal with the required transformation to continue the execution in the runtime (e.g., deoptimize the stack frame).

<footnote n="3">³ In Pharo, conditionals are written as messages to boolean objects (e.g., boolean ifTrue: [...] ).
The message is compiled to bytecode as a conditional jump. The bytecode must support
the case of a non-boolean object as the receiver (error case).</footnote>
The meta-compiled generator handler definition in the JIT frontend uses functions exposed by the JIT backend to perform optimizations at JIT compile time. On one side, the functions to access and modify the stack, `ssValue:` and `ssPop:`, use an abstract stack to track operations and perform peep-hole optimizations. As a result, we must call `ssFlushStack` before any potential deoptimization to ensure that values in registers are spilled to the stack in case of continuing the execution in the runtime. Conversely, we use the `ensureFixupAt:` function to track jumps between bytecodes and perform dead code elimination.

The JITFrontend version in Figure 18 also presents staged operations at JIT compile time. In the expression where the bytecode to jump is computed, `s9 := bytecodePC + 2`, our meta-compiler identifies which values can be constant folded at meta-compile time—the final value 2 is produced by `1 (offset) + 1` (next bytecode is always avoided)—and which values must be staged for JIT compile time—the addition `bytecodePC + 2` is evaluated by the JIT compiler since it does not rely on runtime information.

A similar case occurs on comparing boolean objects—the expressions `objectMemory trueObject` and `objectMemory falseObject` return a pointer to each object. Since they are special objects, they are fixed in memory and are not moved during Garbage Collection. Thus, we cannot get the value at meta-compile time but at JIT compile time and inline the value in the JIT-compiled code.

## B.4 Message Send

Figure 19 shows both definitions for a message send bytecode. The presented bytecode is a special case for `=` message, used for equality comparison in Pharo.

Interpreter >> bytecodeSendEquals
<compilationInfo: #isMapped>
<druidInfo: #hasSend>

self druidIgnore: [
  | rcvr arg |
  rcvr := self stackValue: 1.
  arg := self stackValue: 0.
  (objectMemory areIntegers: rcvr and: arg)
    --> ifTrue: [
      ^ self booleanCheat: rcvr = arg ]
    ]

self normalSendSpecialSelector: 6
  --> argumentCount: 1

JITFrontend >> gen_bytecodeSendEquals
"AutoGenerated by Druid"

self marshallSendArguments: 1.
self
  genMarshalledSend: -7
  numArgs: 1
  sendTable: ordinarySendTrampolines.
  ^ 0

<caption>■ Figure 19 Special message send bytecode meta-compilation for `=` message.</caption>

The interpreter definition of the bytecode includes the two optimizations commented in Section 5.4. A *static type prediction* is implemented for the case of Small-Integer objects, performing the comparison of the tagged values. Also, *dynamic super-instructions* is implemented inside the `booleanCheat:` function, where a look-ahead
Meta-compilation of Baseline JIT Compilers with Druid

of the next bytecode is performed to check if it is a conditional jump and directly
execute it with the computed boolean value. Our meta-compiler does not support
these optimizations yet, so they are ignored using the intrinsic druidIgnore:. As we say
in Section 7, we are still working to support them.

The targeting JIT compiler backend Cogit already has support for message sends.
We meta-compile the bytecodes with message sends by using marshallSendArguments:
and genMarshalledSend:numArgs:sendTable: JIT compiler backend functions. These
functions generate calls to trampolines to perform the lookup and patch the code
with PICs. They also create the mapping between bytecode and compiled code, so the
annotateBytecode: function is not invoked by the frontend.

B.5 Staging Conditional Branches

Figure 20 presents a version of the push new array bytecode, i.e., it instantiates a new
array object and fills all slots with references to nil object. The Interpreter version
was simplified to focus on the staging features of the meta-compiler. The JITFrontend
version only shows the important parts of this section.

Interpreter >> bytecodePushNewArray

| size array |
size := self fetchByte.
size < 0 ifTrue: [
    self druidForceInterpretation.
    "..."
].
size := size bitAnd: 127.
self fetchNextBytecode.
array := ... "Instantiate new array"
o to: size - 1 do: [ :i |
    objectMemory
        storePointerunchecked: i
        ofObject: array
        withValue: objectMemory nilObject ].
self push: array

JITFrontend >> gen_bytecodePushNewArray

"AutoGenerated by Druid"

| ... |
s2 := byte1.
self ssFlushStack.
s2 < 0 ifTrue: [
    self deoptimize.
    ^ 0 ].
s7 := s2 bitAnd: 127.
t0 := ... "Allocate register"
t1 := ... "Allocate register"
    ... "Store new array in memory, saved in t1"
s28 := s7 - 1.
s29 := 0.
t2 := ... "Allocate register"
    [ ((s29<=s28)) ] whileTrue: [
        self genMoveConstant: objectMemory
        <> nilObject R: to.
        s34 := s29 << 3.
        self MoveR: t1 R: t2.
        self AddCq: s34 R: t2.
        self MoveR: to M64: 8 r: t2.
        s38 := s29 + 1.
        s29 := s38 ].
        self ssPushRegister: t1.
        ^ 0

<caption>Figure 20 Push new array bytecode meta-compilation (some parts of the code).</caption>
In previous examples, we presented that our meta-compiler stages expressions to be executed at JIT compile time. For the cases where these expressions are the condition of a conditional branch, the meta-compiler stages the branches too. We can see the staged branches by using `ifTrue:` and `whileTrue:` functions in the JITFrontend version in Figure 20.

The first case is where the condition size < 0 `ifTrue:` in the Interpreter is meta-compiled as `s2 < 0 ifTrue:` in the JITFrontend. Since the value of size is encoded in the 2 bytes instruction, the condition does not depend on runtime information and can be resolved at JIT compile time. In this case, the JIT compiler backend already performs the fetch of the next bytes storing them in variables for the frontend, so the `fetchByte` intrinsic is meta-compiled as an access to the expected variable `byte1`. Inside the branch, the `druidForceInterpretation` intrinsic is meta-compiled by calling `deoptimize` JIT compiler backend function. In that case, a trampoline is called to continue the execution on the Interpreter (slow path).

The second case of staged branches occurs during the meta-compilation of the `to:do:` function, defined based on `whileTrue:` intrinsic. Here also, the condition depends only on the size variable, thus the loop is staged. In this case, the meta-compiler must determine which expressions belong to the pre-header, the conditional header, and the loop body.

## References

[1] Alfred V. Aho, Monica S. Lam, Ravi Sethi, and Jeffrey D. Ullman. *Compilers: Principles, Techniques, and Tools (2nd Edition)*. Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, 2006.

[2] Bowen Alpern, Clement R. Attanasio, John J. Barton, Michael G. Burke, Perry Cheng, Jong-Deok Choi, Anthony Cocchi, Stephen J. Fink, David Grove, Michael Hind, Susan F. Hummel, Derek Lieber, V. Litvinov, Mark F. Mergen, Ton Ngo, James R. Russell, Vivek Sarkar, Maurício J. Serrano, Janice C. Shepherd, Stephen E. Smith, Vugranam C. Sreedhar, Harini Srinivasan, and John Whaley. The Jalapeño virtual machine. *IBM Systems Journal*, 39(1):211–238, 2000. doi:10.1147/sj.391.0211.

[3] Davide Ancona, Massimo Ancona, Antonio Cuni, and Nicholas D. Matsakis. `RPython`: a step towards reconciling dynamically and statically typed OO languages. In *Proceedings of the 2007 Symposium on Dynamic Languages*, DLS ’07, pages 53–64, New York, NY, USA, 2007. Association for Computing Machinery. doi:10.1145/1297081.1297091.

[4] Matthew Arnold, Stephen J. Fink, David Grove, Michael Hind, and Peter F. Sweeney. A Survey of Adaptive Optimization in Virtual Machines. *Proceedings of the IEEE*, 93(2):449–466, 2005. doi:10.1109/JPROC.2004.840305.

[5] Vasanth Bala, Evelyn Duesterwald, and Sanjeev Banerjia. *Dynamo: A Transparent Dynamic Optimization System*. In *Proceedings of the ACM SIGPLAN 2000 Conference on Programming Language Design and Implementation*, PLDI ’00,
pages 1–12, New York, NY, USA, 2000. Association for Computing Machinery.
doi:10.1145/349299.349303.

[6] Michael Bebenita, Florian Brandner, Manuel Fahndrich, Francesco Logozzo, Wolfram Schulte, Nikolai Tillmann, and Herman Venter. SPUR: a trace-based JIT compiler for CIL. In *Proceedings of the ACM International Conference on Object Oriented Programming Systems Languages and Applications*, OOPSLA '10, pages 708–725, New York, NY, USA, 2010. ACM. doi:10.1145/1869459.1869517.

[7] Lennart Beckman, Anders Haraldson, Östen Oskarsson, and Erik Sandewall. A partial evaluator, and its use as a programming tool. *Artificial Intelligence*, 7(4):319–357, 1976. doi:10.1016/0004-3702(76)90011-4.

[8] Clément Béra and Eliot Miranda. A bytecode set for adaptive optimizations. In *International Workshop on Smalltalk Technologies (IWST)*, August 2014. URL: http://hal.inria.fr/hal-01088801.

[9] Lars Birkedal and Morten Welinder. Hand-Writing Program Generator Generators. In *Proceedings of the 6th International Symposium on Programming Language Implementation and Logic Programming*, PLILP '94, pages 198–214, Berlin, Heidelberg, 1994. Springer-Verlag. doi:10.1007/3-540-58402-1_15.

[10] Carl Friedrich Bolz, Antonio Cuni, Maciej Fijalkowski, and Armin Rigo. Tracing the meta-level: PyPy's tracing JIT compiler. In *ICOOLPS '09: Proceedings of the 4th workshop on the Implementation, Compilation, Optimization of Object-Oriented Languages and Programming Systems*, pages 18–25, New York, NY, USA, 2009. ACM. doi:10.1145/1565824.1565827.

[11] Carl Friedrich Bolz, Adrian Lienhard, Nicholas D. Matsakis, Oscar Nierstrasz, Lukas Renggli, Armin Rigo, and Toon Verwaest. Back to the future in one week — implementing a Smalltalk VM in PyPy. In *Self-Sustaining Systems*, volume 5142 of LNCS, pages 123–139. Springer, 2008. doi:10.1007/978-3-540-89275-5_7.

[12] Carl Friedrich Bolz and Laurence Tratt. The impact of meta-tracing on VM design and implementation. *Science of Computer Programming*, pages 408–421, February 2015. doi:10.1016/j.scico.2013.02.001.

[13] Camillo Bruni and Toon Verwaest. PyGirl: Generating Whole-System VMs from High-Level Prototypes using PyPy. In *Objects, Components, Models and Patterns, Proceedings of TOOLS Europe 2009*, volume 33 of LNBIP, pages 328–347. Springer-Verlag, 2009. doi:10.1007/978-3-642-02571-6_19.

[14] Kevin Casey, David Gregg, and M. Anton Ertl. Tiger – An Interpreter Generation Tool. In Rastislav Bodik, editor, *Compiler Construction*, pages 246–249, Berlin, Heidelberg, 2005. Springer Berlin Heidelberg. doi:10.1007/978-3-540-31985-6_18.

[15] Kevin Casey, David Gregg, M. Anton Ertl, and Andy Nisbet. Towards superinstructions for Java interpreters. *Lecture Notes in Computer Science*, jan 2003. doi:10.1007/978-3-540-39920-9_23.

[16] Ron Cytron, Jeanne Ferrante, Barry K. Rosen, Mark N. Wegman, and F. Kenneth Zadeck. Efficiently computing static single assignment form and the control dependence graph. *ACM Trans. Program. Lang. Syst.*, 13(4):451–490, 1991. doi: 10.1145/115372.115320.
[17] L. Peter Deutsch and Allan M. Schiffman. Efficient Implementation of the Smalltalk-80 system. In *Proceedings of the 11th ACM SIGACT-SIGPLAN Symposium on Principles of Programming Languages*, Salt Lake City, Utah, January 1984. doi:10.1145/800017.800542.

[18] Gilles Duboscq, Lukas Stadler, Thomas Würthinger, Doug Simon, Christian Wimmer, and Hanspeter Mössenböck. Graal IR: An Extensible Declarative Intermediate Representation. In *Asia-Pacific Programming Languages and Compilers Workshop*, 2013. URL: https://www.academia.edu/download/76699729/APPLC-2013-paper_12.pdf.

[19] Stéphane Ducasse, Eliot Miranda, and Alain Plantec. Pragmas: Literal Messages as Powerful Method Annotations. In *International Workshop on Smalltalk Technologies IWST'16*, Prague, Czech Republic, August 2016. doi:10.1145/2991041.2991050.

[20] Dawson R. Engler. VCODE: a retargetable, extensible, very fast dynamic code generation system. In *Proceedings of the ACM SIGPLAN 1996 Conference on Programming Language Design and Implementation*, PLDI '96, pages 160–170, New York, NY, USA, 1996. ACM. doi:10.1145/231379.231411.

[21] M. Anton Ertl and David Gregg. The Structure and Performance of Efficient Interpreters. *Journal of Instruction-Level Parallelism*, 5, November 2003. URL: https://jilp.org/vol5/v5paper12.pdf.

[22] M. Anton Ertl, David Gregg, Andreas Krall, and Bernd Paysan. Vmgen-a generator of efficient virtual machine interpreters. *Software: Practice and Experience*, 32(3):265–294, 2002. doi:https://doi.org/10.1002/spe.434.

[23] Yoshihiko Futamura. Partial Evaluation of Computation Process: An Approach to a Compiler-Compiler. *Higher Order Symbol. Comput.*, 12(4):381–391, 1999. doi:10.1023/A:1010095604496.

[24] Nicolas Geoffray, Gaël Thomas, Julia Lawall, Gilles Muller, and Bertil Folliot. VMKit: a substrate for managed runtime environments. In *Proceedings of the 6th ACM SIGPLAN/SIGOPS International Conference on Virtual Execution Environments*, VEE '10, pages 51–62, New York, NY, USA, 2010. ACM. doi:10.1145/1735997.1736006.

[25] Robert Glück and Jesper Jørgensen. An automatic program generator for multi-level specialization. *Lisp and Symbolic Computation*, 10:113–158, 1997. doi: 10.1023/A:1007763000430.

[26] Adele Goldberg and David Robson. *Smalltalk 80: the Language and its Implementation*. Addison Wesley, Reading, Mass., May 1983. URL: http://stephane.ducasse.free.fr/FreeBooks/BlueBook/Bluebook.pdf.

[27] Urs Hölzle. *Adaptive Optimization for Self: Reconciling High Performance with Exploratory Programming*. PhD thesis, Stanford University, Stanford, CA, USA, 1994. URL: http://infolab.stanford.edu/pub/cstr/reports/cs/tr/94/1520/CS-TR-94-1520.pdf.

[28] Urs Hölzle, Craig Chambers, and David Ungar. Optimizing Dynamically-Typed Object-Oriented Languages With Polymorphic Inline Caches. In *European Conference on Object-Oriented Programming (ECOOP'91)*, 1991. doi:10.1007/BFb0057013.
[29] Dan Ingalls, Ted Kaehler, John Maloney, Scott Wallace, and Alan Kay. Back to the Future: The Story of Squeak, a Practical Smalltalk Written in Itself. In *Proceedings of Object-Oriented Programming, Systems, Languages, and Applications conference (OOPSLA'97)*, pages 318–326. ACM Press, November 1997. doi:10.1145/263700.263754.

[30] Neil J. Jones, Carsten K. Gomard, and Peter Sestoft. *Partial Evaluation and Automatic Program Generation*. Prentice-Hall, 1993.

[31] Ken Kennedy and John R. Allen. *Optimizing Compilers for Modern Architectures: A Dependence-Based Approach*. Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, 2001.

[32] Florian Latifi, David Leopoldseder, Christian Wimmer, and Hanspeter Mössenböck. CompGen: generation of fast JIT compilers in a multi-language VM. In *Proceedings of the 17th ACM SIGPLAN International Symposium on Dynamic Languages*, pages 35–47, 2021. doi:10.1145/3486602.3486930.

[33] Mark Leone and Peter Lee. Deferred Compilation: The Automation of Run-Time Code Generation. Technical Report CMU-CS-93-225, Carnegie Mellon University, USA, 1993. URL: https://www.cs.cmu.edu/afs/cs/project/fox/mosaic/papers/mleone-rtcg.html.

[34] Stefan Marr. ReBench: Execute and Document Benchmarks Reproducibly, 2018. Version 1.0. doi:10.5281/zenodo.1311762.

[35] Stefan Marr, Benoit Daloze, and Hanspeter Mössenböck. Cross-Language Compiler Benchmarking: Are We Fast Yet? In *Proceedings of the 12th Symposium on Dynamic Languages*, DLS 2016, pages 120–131, New York, NY, USA, 2016. Association for Computing Machinery. doi:10.1145/2989225.2989232.

[36] Eliot Miranda. The Cog Smalltalk Virtual Machine — writing a JIT in a high-level dynamic language. In *Proceedings of 2011 Workshop on Virtual Machines and Language Implementations*, 2011. URL: https://web.archive.org/web/20130516153602/http://design.cs.iastate.edu/vmil/2011/papers/p03-miranda.pdf.

[37] Eliot Miranda, Clément Béra, Elisa Gonzalez Boix, and Dan Ingalls. Two decades of smalltalk VM development: live VM development through simulation tools. In *Proceedings of International Workshop on Virtual Machines and Intermediate Languages (VMIL'18)*, pages 57–66. ACM, 2018. doi:10.1145/3281287.3281295.

[38] Hanspeter Mössenböck and Michael Pfeiffer. Linear Scan Register Allocation in the Context of SSA Form and Register Constraints. In R. Nigel Horspool, editor, *Compiler Construction*, pages 229–246, Berlin, Heidelberg, 2002. Springer Berlin Heidelberg. doi:10.1007/3-540-45937-5_17.

[39] Hugo Musso Gualandi and Roberto Ierusalimschy. A Surprisingly Simple Lua Compiler. In *Proceedings of the 25th Brazilian Symposium on Programming Languages*, pages 1–8, 2021. doi:10.1145/3475061.3475077.

[40] Fabio Niephaus, Tim Felgentreff, and Robert Hirschfeld. GraalSqueak: Toward a Smalltalk-Based Tooling Platform for Polyglot Programming. In *Proceedings of the 16th ACM SIGPLAN International Conference on Managed Programming Languages and Runtimes (MPLR'19)*, pages 14–26, New York, NY, USA, 2019. ACM. doi:10.1145/3357390.3361024.
[41] Nahuel Palumbo, Guillermo Polito, Pablo Tesone, and Stéphane Ducasse. Ordering Optimisations in Meta-Compilation of Primitive Methods. In *FAST Workshop 2022 on Smalltalk Related Technologies*, 2022. URL: https://openreview.net/forum?id=jYsMG5sjQy.

[42] Guillermo Polito, Pablo Tesone, and Stéphane Ducasse. Interpreter-guided Differential JIT Compiler Unit Testing. In *Programming Language Design and Implementation (PLDI’22)*, 2022. doi:10.1145/3519939.3523457.

[43] Armin Rigo and Samuele Pedroni. PyPy’s approach to virtual machine construction. In *Proceedings of the 2006 Conference on Dynamic Languages Symposium*, pages 944–953, New York, NY, USA, 2006. ACM. doi:10.1145/1176617.1176753.

[44] Tiark Rompf, Arvind K. Sujeeth, Kevin J. Brown, HyoukJoong Lee, Hassan Chafi, and Kunle Olukotun. Surgical Precision JIT Compilers. In *Programming Language Design and Implementation*, PLDI ’14, 2014. doi:10.1145/2594291.2594316.

[45] Chris Seaton, Michael L. Van De Vanter, and Michael Haupt. Debugging at full speed. In *Proceedings of the Workshop on Dynamic Languages and Applications*, pages 1–13, 2014. doi:10.1145/2617548.2617550.

[46] Manas Thakur and V. Krishna Nandivada. PYE: A Framework for Precise-Yet-Efficient Just-In-Time Analyses for Java Programs. *ACM Transactions on Programming Languages and Systems*, 41(3), 2019. doi:10.1145/3337794.

[47] Ben L. Titzer. Whose Baseline Compiler is it Anyway? In *2024 IEEE/ACM International Symposium on Code Generation and Optimization (CGO)*, pages 207–220, Los Alamitos, CA, USA, mar 2024. IEEE Computer Society. doi:10.1109/CGO57630.2024.10444855.

[48] Linda Torczon and Keith Cooper. *Engineering a Compiler*. Morgan Kaufmann Publishers Inc., San Francisco, CA, USA, 2nd edition, 2007.

[49] David Ungar. *The design and evaluation of a high performance Smalltalk system*. MIT Press, Cambridge, MA, USA, 1987.

[50] 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*, pages 13–14, 2012. doi:10.1145/2384716.2384723.

[51] Andreas Wöß, Christian Wirth, Daniele Bonetta, Chris Seaton, Christian Humer, and Hanspeter Mössenböck. An Object Storage Model for the Truffle Language Implementation Framework. In *Proceedings of the 2014 International Conference on Principles and Practices of Programming on the Java Platform: Virtual Machines, Languages, and Tools*, pages 133–144. ACM, 2014. doi:10.1145/2647508.2647517.

[52] 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 *International Symposium on New Ideas, New Paradigms, and Reflections on Programming & Software (ONWARD’13)*, 2013. doi:10.1145/2509578.2509581.

[53] Alexander Yermolovich, Christian Wimmer, and Michael Franz. Optimization of Dynamic Languages Using Hierarchical Layering of Virtual Machines. 44(12):79–88, 2009. doi:10.1145/1837513.1640147.
## About the authors

**Nahuel Palumbo** nahuel.palumbo@inria.fr

I am a PhD student at INRIA - EVREF team working on meta-compilation and virtual machines for object-oriented programming.

ID https://orcid.org/0009-0001-5004-5632

**Guillermo Polito** guillermo.polito@inria.fr

I am an Inria Researcher at the CRISTAL laboratory, where I am expert in software engineering and language implementations, with a particular focus on test generation. I work in the Evref team from Inria Lille-Nord Europe.

ID https://orcid.org/0000-0003-0813-8584

**Stéphane Ducasse** stephane.ducasse@inria.fr

I'm an expert in object design, object language design, reflective programming, and the maintenance and evolution of large applications (visualization, metrics, meta-modeling). My work on traits has been introduced in AmbientTalk, Slate, Pharo, Perl-6, PHP 5.4 and Squeak. They have been ported to JavaScript. It has influenced the Scala and Fortress languages. I'm one of the founders of Pharo, a new pure open-source object language inspired by Smalltalk. I head its industry consortium. I'm one of the designers of Moose, an analysis platform. I was one of the founders of Synectique, a company offering dedicated analysis tools.

ID https://orcid.org/0000-0001-6070-6599

**Pablo Tesone** pablo.tesone@inria.fr

Pablo Tesone is Ingénieur de recherche of the Pharo Consortium at INRIA University of Lille, within the Evref team, with more than 10 years of experience in industrial projects. He has done a PhD on Dynamic Software Update applied to Live programming environments, distributed systems and robotic applications. He is interested in improving development tools and the daily development process. He is an enthusiast of the object oriented programming and their tools. He collaborates with different open source projects like the ones in the Pharo Community and the Uqbar Foundation.

ID https://orcid.org/0000-0002-5615-6691