Skip to content

Conversation

@anutosh491
Copy link
Member

Fixes #166120

I could think of two approaches here

  1. As per the comment [clang-repl] Inconsitent flushing between out of process and in-process mode #166120 (comment) : Saying we could flush after each input line.

Yes this would make sense and work too

Here's an unpolished diff where

  1. Flush after every input is parsed (through an IsFlushed var that is toggled for each input)
  2. Once the flush is performed, we don't care about storing it in the list of PTUs or tracking it so we hit undo.
diff --git a/clang/include/clang/Interpreter/Interpreter.h b/clang/include/clang/Interpreter/Interpreter.h
index 078d70b3b174..d07992a4ad02 100644
--- a/clang/include/clang/Interpreter/Interpreter.h
+++ b/clang/include/clang/Interpreter/Interpreter.h
@@ -234,6 +234,9 @@ private:
   // This function forces emission of the needed dtor.
   llvm::Expected<llvm::orc::ExecutorAddr>
   CompileDtorCall(CXXRecordDecl *CXXRD) const;
+
+  bool IsOutOfProcess = false;
+  bool IsFlushing = false;
 };
 } // namespace clang
 
diff --git a/clang/lib/Interpreter/Interpreter.cpp b/clang/lib/Interpreter/Interpreter.cpp
index a071d0312f76..9a178487dec7 100644
--- a/clang/lib/Interpreter/Interpreter.cpp
+++ b/clang/lib/Interpreter/Interpreter.cpp
@@ -347,6 +347,8 @@ const char *const Runtimes = R"(
       memcpy(Placement, Src, Size);
     }
 #endif // __cplusplus
+  #include <iostream>
+  #include <cstdio>
   EXTERN_C void *__clang_Interpreter_SetValueWithAlloc(void*, void*, void*);
   EXTERN_C void __clang_Interpreter_SetValueNoAlloc(void *This, void *OutVal, void *OpaqueType, ...);
 )";
@@ -648,6 +650,7 @@ llvm::Error Interpreter::CreateExecutor(JITConfig Config) {
   bool IsWindowsTarget = TargetTriple.isOSWindows();
 
   if (!IsWindowsTarget && Config.IsOutOfProcess) {
+    IsOutOfProcess = true;
     if (!JITBuilder) {
       auto ResOrErr = outOfProcessJITBuilder(Config);
       if (!ResOrErr)
@@ -732,6 +735,25 @@ llvm::Error Interpreter::ParseAndExecute(llvm::StringRef Code, Value *V) {
     } else
       *V = std::move(LastValue);
   }
+
+  if (IsOutOfProcess && !IsFlushing) {
+    IsFlushing = true; // prevent recursion
+
+    static const char FlushSnippet[] = R"(
+      std::cout.flush();
+      std::cerr.flush();
+      std::fflush(stdout);
+      std::fflush(stderr);
+    )";
+
+    if (auto FlushErr = ParseAndExecute(FlushSnippet))
+      consumeError(std::move(FlushErr));
+
+    if (auto E = Undo())
+      consumeError(std::move(E));
+    IsFlushing = false;
+  }
+
   return llvm::Error::success();
 }
 

Downsides of this approach is we end up parsing and executing the above block for each input and then undoing the execution too everytime. Which just looks heavy !

@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Nov 4, 2025
@llvmbot
Copy link
Member

llvmbot commented Nov 4, 2025

@llvm/pr-subscribers-clang

Author: Anutosh Bhat (anutosh491)

Changes

Fixes #166120

I could think of two approaches here

  1. As per the comment [clang-repl] Inconsitent flushing between out of process and in-process mode #166120 (comment) : Saying we could flush after each input line.

Yes this would make sense and work too

Here's an unpolished diff where

  1. Flush after every input is parsed (through an IsFlushed var that is toggled for each input)
  2. Once the flush is performed, we don't care about storing it in the list of PTUs or tracking it so we hit undo.
diff --git a/clang/include/clang/Interpreter/Interpreter.h b/clang/include/clang/Interpreter/Interpreter.h
index 078d70b3b174..d07992a4ad02 100644
--- a/clang/include/clang/Interpreter/Interpreter.h
+++ b/clang/include/clang/Interpreter/Interpreter.h
@@ -234,6 +234,9 @@ private:
   // This function forces emission of the needed dtor.
   llvm::Expected&lt;llvm::orc::ExecutorAddr&gt;
   CompileDtorCall(CXXRecordDecl *CXXRD) const;
+
+  bool IsOutOfProcess = false;
+  bool IsFlushing = false;
 };
 } // namespace clang
 
diff --git a/clang/lib/Interpreter/Interpreter.cpp b/clang/lib/Interpreter/Interpreter.cpp
index a071d0312f76..9a178487dec7 100644
--- a/clang/lib/Interpreter/Interpreter.cpp
+++ b/clang/lib/Interpreter/Interpreter.cpp
@@ -347,6 +347,8 @@ const char *const Runtimes = R"(
       memcpy(Placement, Src, Size);
     }
 #endif // __cplusplus
+  #include &lt;iostream&gt;
+  #include &lt;cstdio&gt;
   EXTERN_C void *__clang_Interpreter_SetValueWithAlloc(void*, void*, void*);
   EXTERN_C void __clang_Interpreter_SetValueNoAlloc(void *This, void *OutVal, void *OpaqueType, ...);
 )";
@@ -648,6 +650,7 @@ llvm::Error Interpreter::CreateExecutor(JITConfig Config) {
   bool IsWindowsTarget = TargetTriple.isOSWindows();
 
   if (!IsWindowsTarget &amp;&amp; Config.IsOutOfProcess) {
+    IsOutOfProcess = true;
     if (!JITBuilder) {
       auto ResOrErr = outOfProcessJITBuilder(Config);
       if (!ResOrErr)
@@ -732,6 +735,25 @@ llvm::Error Interpreter::ParseAndExecute(llvm::StringRef Code, Value *V) {
     } else
       *V = std::move(LastValue);
   }
+
+  if (IsOutOfProcess &amp;&amp; !IsFlushing) {
+    IsFlushing = true; // prevent recursion
+
+    static const char FlushSnippet[] = R"(
+      std::cout.flush();
+      std::cerr.flush();
+      std::fflush(stdout);
+      std::fflush(stderr);
+    )";
+
+    if (auto FlushErr = ParseAndExecute(FlushSnippet))
+      consumeError(std::move(FlushErr));
+
+    if (auto E = Undo())
+      consumeError(std::move(E));
+    IsFlushing = false;
+  }
+
   return llvm::Error::success();
 }
 

Downsides of this approach is we end up parsing and executing the above block for each input and then undoing the execution too everytime. Which just looks heavy !


Full diff: https://github.com/llvm/llvm-project/pull/166368.diff

1 Files Affected:

  • (modified) clang/lib/Interpreter/Interpreter.cpp (+14)
diff --git a/clang/lib/Interpreter/Interpreter.cpp b/clang/lib/Interpreter/Interpreter.cpp
index cde354c9cd8d1..f3927fc341341 100644
--- a/clang/lib/Interpreter/Interpreter.cpp
+++ b/clang/lib/Interpreter/Interpreter.cpp
@@ -351,6 +351,15 @@ const char *const Runtimes = R"(
   EXTERN_C void __clang_Interpreter_SetValueNoAlloc(void *This, void *OutVal, void *OpaqueType, ...);
 )";
 
+const char *const OOPRuntimes = R"(
+  #include <stdio.h>
+  __attribute__((constructor))
+  static void __clang_repl_ioinit(void) {
+    setvbuf(stdout, NULL, _IONBF, 0);
+    setvbuf(stderr, NULL, _IONBF, 0);
+  }
+)";
+
 llvm::Expected<std::pair<std::unique_ptr<llvm::orc::LLJITBuilder>, uint32_t>>
 Interpreter::outOfProcessJITBuilder(JITConfig Config) {
   std::unique_ptr<llvm::orc::ExecutorProcessControl> EPC;
@@ -463,6 +472,11 @@ Interpreter::create(std::unique_ptr<CompilerInstance> CI, JITConfig Config) {
   if (auto E = Interp->ParseAndExecute(Runtimes))
     return std::move(E);
 
+  if (Config.IsOutOfProcess) {
+    if (auto E = Interp->ParseAndExecute(OOPRuntimes))
+      return std::move(E);
+  }
+
   Interp->markUserCodeStart();
 
   return std::move(Interp);

@anutosh491
Copy link
Member Author

Approach 2: As what the PR has, configures the executor’s output streams to be unbuffered once at startup, ensuring immediate flushing for all subsequent inputs.
This is cleaner and faster than approach 1, which redundantly parsed and executed a flush command after every input line followed by an undo.

@anutosh491
Copy link
Member Author

Some outputs

// out of process

(base) anutosh491@vv-nuc:/build/anutosh491/llvm-project/build/bin$ ./clang-repl   --oop-executor=$PWD/llvm-jitlink-executor
clang version 22.0.0git (https://github.com/anutosh491/llvm-project.git 81ad8fbc2bb09bae61ed59316468011e4a42cf47)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /build/anutosh491/llvm-project/build/bin
Build config: +unoptimized, +assertions
clang-repl> #include <iostream>
clang-repl> std::cout << "first";
firstclang-repl> std::cout << " second";
 secondclang-repl> std::cout << std::endl;

clang-repl> std::string myname;
clang-repl> std::cin >> myname;
ANUTOSH
clang-repl> std::cout << "Your name is " << myname;
Your name is ANUTOSHclang-repl> std::cout << "hello" << std::endl;
hello
clang-repl> #include <unistd.h>
clang-repl> for (int i = 0; i < 3; ++i) { std::cout << i << std::endl; sleep(1); }
0
1
2

// in process
anutosh491@vv-nuc:/build/anutosh491/llvm-project/build/bin$ ./clang-repl
clang-repl> #include <iostream>
clang-repl> std::cout << "first";
firstclang-repl> std::cout << " second";
 secondclang-repl> std::cout << std::endl;

clang-repl> std::string myname;
clang-repl> std::cin >> myname;
ANUTOSH
clang-repl> clang-repl> std::cout << "Your name is " << myname;
Your name is ANUTOSHclang-repl> std::cout << "hello" << std::endl;
hello
clang-repl> #include <unistd.h>
clang-repl> for (int i = 0; i < 3; ++i) { std::cout << i << std::endl; sleep(1); }
0
1
2

The only difference here is the cin for in-process

clang-repl> std::cin >> myname;
ANUTOSH
clang-repl> clang-repl> std::cout << "Your name is " << myname;
Your name is ANUTOSHclang-repl>

You can see 2 "clang-repl>"'s here. This is a separate in-process thing in itself (which needs to be looked into in the future)

@anutosh491
Copy link
Member Author

anutosh491 commented Nov 6, 2025

Hey @vgvassilev @Vipul-Cariappa ,

I guess we can start with something like this on the tool side.

Added simple test logs at the start and the end of the file to confirm consistent flushing.

So now we have the following (which is what in-process has too)

anutosh491@vv-nuc:/build/anutosh491/llvm-project/build/bin$ ./clang-repl   --oop-executor=$PWD/llvm-jitlink-executor
clang-repl> extern "C" int printf(const char *, ...);
clang-repl> printf("FLUSH_START");
FLUSH_STARTclang-repl> printf("\nFLUSH_OK\n");

FLUSH_OK

instead of the following on master

anutosh491@vv-nuc:/build/anutosh491/llvm-project/build/bin$ ./clang-repl   --oop-executor=$PWD/llvm-jitlink-executor
clang-repl> extern "C" int printf(const char *, ...);
clang-repl> printf("FLUSH_START");
clang-repl> printf("\nFLUSH_OK\n");
FLUSH_START
FLUSH_OK

The test passes as expected

anutosh491@vv-nuc:/build/anutosh491/llvm-project/build$ ./bin/llvm-lit -v ../clang/test/Interpreter/out-of-process.cpp
llvm-lit: /build/anutosh491/llvm-project/llvm/utils/lit/lit/llvm/config.py:531: note: using clang: /build/anutosh491/llvm-project/build/bin/clang
llvm-lit: /build/anutosh491/llvm-project/llvm/utils/lit/lit/llvm/subst.py:126: note: Did not find cir-opt in /build/anutosh491/llvm-project/build/bin:/build/anutosh491/llvm-project/build/bin
-- Testing: 1 tests, 1 workers --
PASS: Clang :: Interpreter/out-of-process.cpp (1 of 1)

Testing Time: 1.31s

Total Discovered Tests: 1
  Passed: 1 (100.00%)

Interp = ExitOnErr(clang::Interpreter::create(std::move(CI), Config));
}

if (Config.IsOutOfProcess) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we condition this on out-of-process?
I am not sure, asking for an opinion.

I guess I am fine with everything else.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, removing the condition would introduce some consistency (otherwise oop is maintaining 1 more PTU .... PTU#2 after the inital runtime).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[clang-repl] Inconsitent flushing between out of process and in-process mode

4 participants