RFC: Handle Execution Results in clang-repl

TL;DR: Synthesize automatic printf to print execution results in clang-repl and generalize the approach to use an object used to bridge compiled/interpreted code taking inspiration from what was done in Cling.

Introduction

The Cling interpreter is a unique interpretative technology for C++ based on Clang developed by high-energy physics (HEP). It is used to deliver reflection and type information for exabytes of scientific data and is heavily used during data analysis of particle physics data from the Large Hadron Collider (LHC) and other particle physics experiments.

In RFC Moving (parts of) the Cling REPL in Clang we discussed and shipped the initial incremental compilation facilities into LLVM mainline, called clang-repl.

In this RFC we propose two distinct features and their interaction: automatic printf and connecting compiled and interpreted C++ through a class called Value as an abstraction layer used to carry expression results and support value pretty printing in clang-repl.

Goals

Automatic printf

One of the key aspects of interactive C++ is exploratory programming which encourages showing execution results on screen easily. Typing every time printf or similar is too laborious and too annoying. Taking inspiration from Cling, we could achieve this effect by an extension that lives purely in libclangInterpreter. We propose to have a special mode to indicate when we want to do value pretty printing: A expression in the global scope (without the semicolon). Coincidently Rust takes a similar approach:

clang-repl> int x = 42;
clang-repl> x // equivalent to calling printf("(int &) %d\n", x);
(int &) 42

clang-repl> std::vector<int> v = {1,2,3};
clang-repl> v // This syntax is fine after [D127284](https://reviews.llvm.org/D127284)
(std::vector<int> &) {1,2,3}

clang-repl> "Hello, interactive C++!"
(const char [24]) "Hello, interactive C++!"

In the RFC below we discuss at length how to make this technique extensible, versatile and efficient by introducing simple concepts that live in libclangInterpreter only. For example, we demonstrate how a simple pair of (clang type and execution result) can be used to write custom pretty printers.

The implementation is uncomplicated since the Clang parser is responsible for parsing code in clang-repl, we need to teach it to recognize this pattern and propagate some flags that can be used later.

The implementation for this might be trivial after the patch addressing (RFC: Flexible Lexer Buffering for Handling Incomplete Input in Interactive C/C++)

Crossing the compiled/interpreted world

In some scenarios, we can embed a C++ interpreter in a C++ program. In the example below, we create an interpreter and define and increment a variable p.

#include "clang/Interpreter/Interpreter.h"

int main(int argc, char** argv) {
std::vector<const char *> ClangArgs = {};
auto CI = cantFail(clang::IncrementalCompilerBuilder::create(ClangArgs));
auto interp = return cantFail(clang::Interpreter::create(std::move(CI)));
interp.ParseAndExecute("int p=0; ++p;");
}

In many cases, it is useful to bring back the execution result to the compiled program. In particular, if we could instantiate a template with a user type on demand and use its value or call directly the symbol. This has been utilized by Cling for a decade now, allowing it to build patterns such as:

float Global = 3.141f;
float getGlobal() { return Global; }
void setGlobal(float val) { Global = val; }

void Demo(cling::Interpreter& interp) {
// We could use a header as well.
interp.declare("float getGlobal();\n"
"void setGlobal(float val);\n");

cling::Value res; // This will hold the result of the expression evaluation.
interp.process("getGlobal();", &res);
std::cout << "getGlobal() returned " << res.getAs<float>() << '\n';

setGlobal(1.); // We can modify the value in compiled code.
interp.process("getGlobal();", &res); // The interpreter can see it.
std::cout << "getAnotherGlobal() returned " << res.getAs<float>() << '\n';

// We modify using the interpreter, now the binary sees the new value.
interp.process("setGlobal(7.777);");
std::cout << "getGlobal() returned " << getGlobal() << '\n';
}

Here Cling introduces a concept called cling::Value to connect the compiled/interpreted worlds. The Value object carries the execution results and we can pass it around between two sides. Supporting this feature is essential for interoperability as it provides extended control over the object’s lifetime if requested.

Value Interface – An execution result – type pair

A value is a container that can carry the arbitrary result of an expression in an endian-independent way with small buffer optimization. Its design is driven by performance-critical use cases (sec Performance Considerations). In addition, the value container should support out-of-process/remote execution to support microcontrollers such as Arduino Due which cannot host the entire LLVM JIT infrastructure.

12 + 30 // This is a BinaryExpression, which yields a value whose type is int and the content is 42.

In general, we should implement the interface below for Value:

class Value {
public:
clang::QualType* getType(); // Obtain the type information of the expression.
template<typename T>
T castAs(); // Cast the value to corresponding type.

void printType(llvm::raw_ostream& OS);

void printData(llvm::raw_ostream& OS);

void print(llvm::raw_ostream& OS);
void dump() const; // Dump the value, called print(llvm::outs()) internally.
};

Note that the actual implementation is slightly more complex as there are several optimizations to avoid repetitive, expensive operations such as getType.

Implementation

Note this implementation we proposed is inspired by Cling.

Stealing an execution result

After capturing the desired expression, we need to create the Value object. Here we achieved this by doing code generation or synthesizing Clang AST. In general, first, we synthesize the wrapper function ValueGetter, which is used as a user interface for passing the Value object out. Then we generate the function body, which is another function call (SetValue) to construct the Value object. The function SetValue is declared the first time when we enter the REPL and defined somewhere else in the library (exported using LLVM_EXTERNAL_VISIBILITY).

Lifetime and temporaries

Let’s consider this example:

clang-repl> struct S {};
clang-repl> S foo() { return S{};}
clang-repl> foo()

foo() here returns an rvalue which means it will be destroyed immediately after being created. So we can’t simply pass that Expr to SetValue since it will cause dangling problems. Therefore, we need to make a copy of the original expression.

Thus, we need two branches to deal with objects:

  1. If the object is a lvalue – the Value will not get involved in the lifetime management of the object, but only stores its address, which can be used later.
  2. If the object is an rvalue, or temporary – the Value will allocate an internal buffer that is enough to contain the object, and use placement new to construct the object in the buffer. In this case, we manually extend the lifetime of the temporary object, so it is possible to use that later.

Overview of the implementation in clang-repl

In general, based on its type, we transform:

clang-repl> x

into

clang-repl> void ValueGetter(void* OpaqueValue) {
// 1. if x is a built-in type like int, float.
SetValueNoAlloc(OpaqueValue, xQualType, x);
// 2. if x is a struct, and a lvalue.
SetValueNoAlloc(OpaqueValue, xQualType, &x);
// 3. if x is a struct, but a rvalue.
new (SetValueWithAlloc(OpaqueValue, xQualType) (x);
}

Then in the interpreter, we can ask JIT for a function pointer to ValueGetter :

auto* F = (void(*)(void*))Interp.getSymbolAddr("ValueGetter");
Value V;
(*F)((void*)&V);
V.dump(); // Do pretty printing or return the value to the user.

After we have the Value object, the pretty print logic could be implemented in its Value::dump() method.

STL types and user-defined class

In the implementation of Value::dump(), it’s pretty straightforward to support printing built-in types, and we can always support any arbitrary user-defined struct/class by printing its address. However, we want to achieve more!

  1. Is it possible to obtain more useful information for types that almost everybody knows and uses like STL containers?
  2. How can the user customize the behavior for their own types?

To address the issues above, we propose adding a fallback in Value::dump() when clang-repl fails to handle all possible cases. We synthesize a call again to a function like PrintValueRuntime, which lives in a header that is processed by the JIT ahead of time. Any types that are not primitive types fall back to it, like STL components and user-defined types. They distinguish each other via overloads and SFINAE. In default, we provide a general implementation for standard library facilities and only print the address for unknown types. In this case, the equivalent code becomes:

clang-repl> std::vector<int> v {1,2,3};
clang-repl> #include "PrintValueRuntime.h"
clang-repl> PrintValueRuntime(v);

If users need to customize behavior for their own types like S, they only have to write a corresponding overload for PrintValueRuntime function in their code:

clang-repl> struct S { int i = 42; };
clang-repl> S s
(S&) 0x123
clang-repl> std::string PrintValueRuntime(S* s) {
return std::string("i = ") + std::to_string(s->i);
}

clang-repl> s // Picks up the custom pretty printer.
(S&) i = 42

Performance consideration

We want this facility to be as fast as possible. Note that we are actually “interpreting” the language, which means compiling the code when executing code. So the runtime performance will suffer if the compile time increases too much. This is likely to happen if there are too many overloads for PrintValueRuntime and heavy usage of templates in Value class design.

The implementation aims to be minimalistic because its header needs to be included during the interpreter’s runtime. These requirements prevent us from using basically any other concepts with standard implementations that are template heavy such as std::any or std::variant. Therefore, when representing the internal structure of the Value class, we intentionally choose the combination of a union and an enum class. Here is a rudimentary, illustrative implementation of the idea: Compiler Explorer

Except for the speed, the design can blow up the memory when using modules which would be called cross-module deserialization. However, this is a common problem for all overloaded functions across modules, and we would love to hear your feedback!

CC @vvassilev @sunho @weliveindetail @lhames @AaronBallman

Just to mention that I was involved in writing this RFC and it makes sense to me :wink:

I believe this is the last key component that the clang-repl infrastructure is missing to be useful on its own. I am really looking forward to more community input before moving forward with the proposed approach!

Thank you for the RFC – in general, I think this is a good idea (certainly quite useful for a REPL tool). I did have a few questions, though.

  • In terms of triggering the debug print, will this only work with a simple DeclRefExpr, or can you use arbitrary expressions and will print the resulting value so long as there’s no trailing semicolon? e.g.,
struct Obj {
  int x, y;
};

struct Other {
  Obj o;
};

Other get();

get().o.x

Will that print the int? Or does the user have to do:

auto temp = get().o.x;
temp

to print the value? I’m mostly trying to understand where the boundaries are for “oops, forgot a semi colon and now this behaves in an unexpected way.” e.g., consider:

Other obj;
obj.o.x = 12

is that going to perform the assignment and print (int)12 because that’s the result of the expression or will it do something else?

  • Is there some sort of cut-off for printing massive objects? If so, how does that work? e.g., what happens if the user asks you to print an array of 10k+ elements?

  • How do you intend to handle text encodings? If the text is being printed in the current locale, do you anticipate giving users a way to override that locale?

  • Speaking of text and massive objects, what is the recovery mechanism when the user invariably does something entertaining like tries to print an accidentally not-null-terminated string?

  • In general, are you planning to print the values before or after lvalue to rvalue conversion? e.g., if the object is of type unsigned char, will the value be printed after integer promotions are performed or before?

  • How do you plan to handle error situations where printing the value would result in an exception being thrown (perhaps even a hardware exception like a floating point signaling NaN)?

Hi @AaronBallman, thanks for your valuable input, and I’m sorry about the late reply.

In terms of triggering the debug print, will this only work with a simple DeclRefExpr, or can you use arbitrary expressions and will print the resulting value so long as there’s no trailing semicolon? e.g.,

struct Obj {
 int x, y;
};

struct Other {
 Obj o;
};

Other get();

get().o.x // Case 1

Will that print the int? Or does the user have to do:

auto temp = get().o.x;
temp

to print the value? I’m mostly trying to understand where the boundaries are for “oops, forgot a semicolon and now this behaves in an unexpected way.” e.g., consider:

Other obj;
obj.o.x = 12 // Case 2

Is that going to perform the assignment and print (int)12 because that’s the result of the expression or will it do something else?

That’s a good question. get().o.x will print the value of x and obj.o.x = 12 will print (int) 12 after the initialization.

Let’s consider Case 1 first. It’s actually about expression capturing. So we’ll capture the last expression in the input, which is MemberExpr .x here, and generated code looks like:

get().o.x;
void ValueGetter(void* OutValue) {
  SetValueNoAlloc(OutValue, xQualType, get().o.x);
}

Since it’s just an int, its data could be copied into a Value object efficiently using the small buffer optimization.

Another detail is what if x is not a simple int but a RecordType like:

struct S {
  // …
};
struct Obj {
  S x;
};

Notice that obj.o.x yields an rvalue and we can’t just hold a reference to it. This is discussed in Section Lifetime and temporaries, and the basic idea is maintaining an internal buffer as storage that is used to keep the temporary value, so its lifetime can be extended. The generated code looks like:

obj.o.x;
void ValueGetter(void* OutValue) {
  new (SetValueWithAlloc(OutValue, xQualType)) (obj.o.x);
}

When it comes to Case 2, we generally will transform the original code to

obj.o.x = 12;
void ValueGetter(void* OutValue) {
  SetValueNoAlloc(OutValue, xQualType, x);
}

So even if you omit the semicolon clang-repl will still assume there’s one and enter the value printing mode. x will be assigned to 12 first, then it gets printed.

Is there some sort of cut-off for printing massive objects? If so, how does that work? e.g., what happens if the user asks you to print an array of 10k+ elements?

So the conclusion is that we expect clang-repl is supposed to work in this case.
Let’s imagine we have the following code:

clang-repl> #include <vector>
clang-repl> std::vector<int> v(1 << 20, 42);
clang-repl> v

Because v is not a built-in type, it lives in header, so the way we used to print int or double > doesn’t work here. Instead, we have a special runtime header to handle these cases – it contains lots of overloads of PrintValueRuntime. so the original code becomes

clang-repl> #include “PrintValueRuntime.h”
clang-repl> PrintValueRuntime(v);

There’s no more JIT magic, just like you write some functions that are used for dumping and you call it.
Another advantage of this design is that the user could provide their own pretty printers, like:

clang-repl> struct S { int i = 42; };
clang-repl> S s
(S&) 0x123
clang-repl> std::string PrintValueRuntime(S* s) {
return std::string(“I don’t want to print anything!”);
}
clang-repl> s // Picks up the custom pretty printer.
(S&) I don’t want to print anything!

How do you intend to handle text encodings? If the text is being printed in the current locale, do you anticipate giving users a way to override that locale?

We plan to add overloads for std::wstring, std::u16string and etc in PrintValueRuntime.h but as far as I know, there’s no good solution to override the locale.

Speaking of text and massive objects, what is the recovery mechanism when the user invariably does something entertaining like trying to print an accidentally not-null-terminated string?

Pretty much the same as what we would get in a debugger. Users are taking their own risk to print something not-null-terminated strings.

In general, are you planning to print the values before or after lvalue to rvalue conversion? e.g., if the object is of type unsigned char, will the value be printed after integer promotions are performed or before?

The effect is very similar to:

int i = 45;
printf("(int) %d\n", i);

So we’re printing the values after lvalue to rvalue conversion.

Because we do code generation when parsing, we can make full use of clang AST, and always keep the QualType of the original expression. Then we dispatch the print logic depending on the QualType, so the unsigned char is always printed as unsigned char, and no type information is lost.

How do you plan to handle error situations where printing the value would result in an exception being thrown (perhaps even a hardware exception like a floating point signaling NaN)?

When an exception happens, we really wish that clang-repl should be able to detect it and report a message”. However, the problem is that LLVM disabled exceptions globally due to various reasons. As a compromise, we could only enable exceptions locally for clang-repl which is responsible for catching the exceptions from value printing. Since clang-repl is a separate command line tool and nobody’s dependent on it, it should be fine.

No worries at all!

I think that design is reasonable. I was a bit worried about how easy it is to forget a semi colon, but because we execute the statement anyway, that seems like about the best we can do (the action still happens AND the user gets some extra output from it).

It does make me wonder a bit about what would happen on code like:

struct S { int x; }
enum { One, Two }

where it’s a declaration of a type (not a value) but is still missing the semi colon.

Will the tool have a way for the user to recover from a runaway problem? e.g., they have a massive array, didn’t think about what would happen, and now printing the values of the array will take several minutes to complete.

CC @cor3ntin and @tahonermann to see if they have ideas. I think users are going to need some way to say “print using this encoding”, especially for std::string/const char *. It’s just too easy for the string to contain UTF-8 data but is printed using the system code page, etc and given the goal of printing useful information for the user, it seems like we’ll want to give them some way to override printing options. Text encodings is one obvious need, but even things like “print hex/binary literals with digit separators” seem like the sort of options users will want. Does clang-repl have some notion of user settings like this?

I think there needs to be some way to break out of a runaway print. For example, IDEs will sometimes let the user press the “Page Break” button to “break” out of a long-running task, etc.

It wouldn’t trigger value printing. We do not simply decide to do value printing if we find the last token is not a tok::semi but something more complicated. Currently my working-in-progress patch hacks handleExprStmt to teach Clang to understand the pattern. However, this is considered as a workaround and later we may rework it after flexible lexer buffering patches (⚙ D143148 [clang-repl] Add basic multiline input support) are landed.

1 Like

Excellent, that makes sense to me, thanks!

It likely would be useful for clang-repl users to be able to (dynamically) set a locale. However, it will be necessary to override the locale in some cases. For example, given:

clang-repl> std::vector<double> v = {1.0, 2.0, 3.0};
clang-repl> v

it would be quite wrong to print the expression such that , is substituted for .:

(std::vector<double> &) {1,0, 2,0, 3,0}

For char-based text, it is probably ok to just pass the text without any form of translation on POSIX systems. For Windows, where the console encoding is, well, not what any modern program wants, it is possible to do better since the literal/execution encoding is known; text can be written directly to the console thereby bypassing the console encoding. See P2093 (Formatted output) for details on how such improved support was specified and implemented for the C++23 std::print() function.

For text in wchar_t, char8_t, char16_t, or char32_t based types, transcoding to an appropriate encoding for presentation will be necessary.

For all text, I think it would be desirable to adopt the escaping rules used for “debug” modes of std::format() and std::print(). See [format.string.escaped]p2 and P2713 (Escaping improvements in std::format).

@cor3ntin might have additional thoughts or suggestions.

That’s a good question. The easiest is to set a default char limit which can be controlled. That limit can be the size of the screen and we can ask if the user wants to print more. It’s probably too early to start working on signal handlers (ctrl + c) at that stage of development.

I think that makes sense – basically, print one (screen-width) line of information, and then make the user interact with the tool to say “okay, give me the next line” or “stopppppp!”

1 Like

If actual calls to printf are synthesized, I think passing the string as is without trying to be clever about
encoding the result is probably the right thing to do.
the repl does not introduce a new challenge, we always were in a situation where runtime strings and string literals could have different encoding, and the repl is just a fancy runtime.
So i would not expect the repl runtime to try to do conversions, because it too may be dealing with mixed encodings.

We might want to explore being clever about string literals and converting them with c8rtomb in the repl environment if that first approach proves insufficient, but that would introduce disparity with how programs behave in a non repl environments, and has wild interactions with https://reviews.llvm.org/D93031 - ie it may force to take a dependency on iconv.

I would expect the generated printf calls to respect locale settings of the repl, and I would expect LC_ALL etc to be respected also.

Tom’s suggestion to force LC_NUMERIC such that . is used for floating point is worth exploring.
I agree that std::format may be a better interface to use, but I don’t know whether that’s realistic in non-c++ modes or because std::format was only very recently added to libc++.

I agree that other character types need transcoding but the best way to do that is probably to inject and evaluate calls to the appropriate c library functions in the library environment as we don’t have a better way to convert something to the execution encoding.

This is true with regard to code interpreted by the run-time, but the repl has an additional requirement to be able to display values in a manner similar to common debuggers like gdb. It might be worth investigating how various debuggers cope with these challenges (e.g., displaying values stored in wchar_t, char8_t, or char16_t based storage, locale behavior for the executed/interpreted code vs locale behavior for the debugger/repl, etc…).

The patch series:

  1. [clang] Add a new annotation token: annot_repl_input_end
  2. [clang-repl] Introduce Value to capture expression results
  3. [clang-repl] Implement Value pretty printing