★ Lab 24.2 · pebble-jit: an ORC session and a REPL for Pebble¶
Chapter: 24 · The Complete Pebble Compiler — and Beyond · Lesson: 24.3 (also 10.8, 11.9) · Time: 4–6 hours · Tests: ./course test 24 (labels ch24 and star)
1. Goal¶
Run Pebble in process. pebble-jit program.pbl compiles the program with the same front end, code generator and pipeline as pebblec and executes pebble_main through ORC instead of writing an object file; pebble-jit --repl keeps a session open and evaluates lines as they come. You write the session: an eager configuration on LLJIT (Algorithm 24.3.3) and a lazy one on LLLazyJIT (Algorithm 24.3.5), behind one interface, with a count of compiled function bodies that makes the difference measurable (Proposition 24.3.8: called iff compiled).
2. The contract and the command line¶
include/pebblejit/JIT.h; your code in src/ (any files; Stub.cpp stops with TODO(ch24)):
namespace pebblejit {
struct Options { bool Lazy = false; bool Trace = false; };
class Session {
public:
static std::expected<std::unique_ptr<Session>, pebble::Error> create(const Options &);
virtual std::expected<int64_t, pebble::Error>
run(std::unique_ptr<llvm::Module> M, std::unique_ptr<llvm::LLVMContext> Ctx, std::string_view Entry) = 0;
virtual unsigned compiledFunctions() const = 0;
};
}
The provided driver (tools/pebble-jit.cpp):
pebble-jit [--lazy] [-O0|-O1|-O2] [--jit-trace] [--jit-stats] program.pbl
pebble-jit [--lazy] [-O0|-O1|-O2] [--jit-trace] --repl
It compiles in process (front end → PIR → codegen::lowerPIRToLLVM → driver::optimizeModule at the level) and calls run(M, Ctx, codegen::EntryPointSymbol). The exit status is the program's result modulo 256, or 1 on a compile error; a trap ends the process with 101 as for native code. --jit-stats prints pebble-jit: compiled N function(s) on stderr at exit. In --repl, a line starting with fn, struct or extern begins a declaration that ends when its braces balance, and declarations that compile are kept; any other line is evaluated in a fresh main: a line ending in ; is a statement, anything else an expression whose value is printed; a line that fails to compile is reported and skipped. Prompts go to stderr.
3. Requirements¶
- R1 (runtime symbols). Every function of
pebble/runtimethat generated code may call (pebble_print_int,pebble_print_float,pebble_print_bool,pebble_print_str,pebble_print_newline,pebble_trap, ..., and the C library'sfmod, which float%lowers to) resolves in every module. Publish them withabsoluteSymbolsinto the mainJITDylib: the runtime is statically linked into the driver and not exported (-rdynamicis not portable to every platform this course supports), soDynamicLibrarySearchGenerator::GetForCurrentProcessdoes not findpebble_*(fmodlives in the shared C library and would be found, but publishing it too keeps the session independent of how the driver was linked). - R2 (many modules). Every
runcreates a freshJITDylib(pebble.N) whose link order includes the main dylib, adds the module to it and looks upEntrythere. Modules that all definepebble_main(the REPL's<repl:N>modules,ManyModulesInOneSession) coexist. The module's own functions may beinternal. - R3 (eager vs lazy). With
Lazy == false,runadds the module withaddIRModuleand the lookup ofEntrycompiles the whole module:compiledFunctions()counts every defined function. WithLazy == true(LLLazyJIT,addLazyIRModule), only the functions that are actually called are compiled: on the test program (used,unused,also_unused,main) eager gives 4 and lazy gives 2. - R4 (counting and tracing). Count function bodies in a transform installed with
IRTransformLayer::setTransformon the JIT's transform layer (with the lazy JIT each materialization unit holds one function, so the count is exact);Traceprintspebble-jit: compiled <function>per body in compilation order. - R5 (errors and traps). A missing
Entryis a returnedpebble::Error, not a crash (MissingEntryIsAnError); a trap in JITed code calls the runtime'spebble_trap, which printspebble: trap: <kind> at <file>:<line>:<col>and exits 101 exactly as native code (Inputs/trap.pbl); the module'sLLVMContextis kept alive by the session (or by ORC'sThreadSafeContext) as long as the module may still be compiled lazily.
4. What the tests check¶
| Test | What |
|---|---|
ch24.JIT.EagerRunsAndCompilesEverything |
result 42, compiledFunctions() == 4 (R3) |
ch24.JIT.LazyCompilesOnlyWhatIsCalled |
result 42, compiledFunctions() == 2 (R3, R4) |
ch24.JIT.ManyModulesInOneSession |
three modules defining pebble_main, results 10, 20, 30 (R2) |
ch24.JIT.MissingEntryIsAnError |
run(..., "no_such_function") returns an error (R5) |
tests/ch24/lit/jit.pbl |
output 42 and 3.5 at -O0, -O1, -O2; --jit-stats reports 4 eager and 2 lazy; --jit-trace names pebble_main; trap.pbl traps with the native message and column (R1, R3–R5) |
tests/ch24/lit/repl.test |
the session Inputs/session.txt: a declaration, an expression (144), a statement, a float, an erroneous line (skipped), a recursive fn, a struct; eager and lazy (R2) |
5. Milestones¶
- Eager.
LLJITBuilder().create(),absoluteSymbolsfor the runtime,addIRModuleinto a fresh dylib perrun,lookup(Entry), call it asint64_t (*)().jit.pbl's first RUN line and three unit tests pass. - Counting. The transform layer;
--jit-statsprints 4. - Lazy.
LLLazyJITBuilder,addLazyIRModule; the count drops to 2;--jit-traceshowspebble_mainfirst, thenused. - REPL. Nothing to write: the driver's REPL only needs R2. Run
Inputs/session.txtand then try your own:fndeclarations, expressions, statements.
6. Hints¶
- The
LLJITowns aExecutionSession;getMainJITDylib()is where the runtime symbols go;createJITDylib("pebble." + std::to_string(N))per run, thenJD.addToLinkOrder(Main). absoluteSymbols({{ES.intern("pebble_print_int"), {ExecutorAddr::fromPtr(&pebble_print_int), JITSymbolFlags::Exported | JITSymbolFlags::Callable}}, ...}); takefmod's address asstatic_cast<double (*)(double, double)>(&std::fmod)to pick the overload.J.getIRTransformLayer().setTransform([&](ThreadSafeModule TSM, MaterializationResponsibility &) { TSM.withModuleDo([&](Module &M) { for (auto &F : M) if (!F.isDeclaration()) ++Count; }); return std::move(TSM); }). In the lazy JIT this runs per extracted function.LLLazyJITneeds aLazyCallThroughManager, whichLLLazyJITBuildersets up for the host; on a platform it does not support,create()returns an error, which the tests report. Both the eager and the lazy configuration must runEntrythroughlookup, not through a raw pointer taken before materialization.- The result of a lookup is an
ExecutorSymbolDef;toPtr<int64_t (*)()>()gives the function pointer. - Keep the
LLVMContextin aThreadSafeContextthat the module'sThreadSafeModuleshares; the session, notrun, owns the modules' lifetimes.
7. Stretch goals¶
- A third tier: wrap the compile layer in a
ReOptimizeLayer(Lesson 24.3, Algorithm 24.3.6) that recompiles a function at-O2after 10 calls (ReOptimizeLayer::CallCountThreshold), and measurebench-fibat-O0, with re-optimization, and at-O2. - Remove a run's dylib when the REPL redefines a function (
ES.removeJITDylib), so old definitions do not accumulate. - Speculative compilation: with
--lazy, start compiling every function of a module on a background thread afterpebble_mainreturns for the first time (ORC'sSpeculator).