以下包含嵌套三元语句的“C”源代码无法使用 libTooling 的 clang::Rewriter 使用我的 RecursiveASTVisitor 正确重写。
我无法弄清楚为什么会发生这种情况,但我怀疑它与重叠写入有关,其中外部 ReWriter:ReplaceText 不考虑嵌套 ReplaceText 的效果。
考虑到语句是嵌套的,我想开始以递归方式重写从最里面的 AST 节点到父节点的三元源。当遇到父 clang::ConditionalOperator 节点时,每个节点都会被重写 - 保留已访问的子节点的重写)。
下面的原始“C”代码突出显示了递归
ConditionalOperator
:
void nestedTernaryDeclStmt() {
int ii, jj, kk, ll, mm;
int foo = ii > jj ? ( kk <= ll ) ? mm : 4123 : 5321 ;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ (outer ternary)
^~~~~~~~~~~~~~~~~~~~~~~^ (nested ternary)
}
我添加了诊断代码来检查每次回调后的编辑缓冲区和位置。不幸的是,重写最外面的
ConditionalOperator
会丢弃子“ConditionalOperator”中先前替换的替换文本。
在第一个
bool VisitConditionalOperator(clang::ConditionalOperator *CO) const
回调结束时,文件编辑缓冲区的内容(参见 str
变量)被正确重写(忽略函数注释)为:
// Rewrite buffer after nested rewrite
// this replaced
// '( kk <= ll ) ? mm : 4123'
// with
// '(/*COND0*/( kk <= ll )) ? (/*LHS0*/mm) : (/*RHS0*/4123)'
void nestedTernaryDeclStmt() {
int ii, jj, kk, ll, mm;
int foo = ii > jj ? (/*COND0*/( kk <= ll )) ? (/*LHS0*/mm) : (/*RHS0*/4123) : 5321 ;
}
这正是我所期望的,因为最里面的三元
( kk <= ll ) ? mm : 4123
被完全重写并放置在重写缓冲区中预期的位置。我不必使用调整后的结束位置来工作(通常,sourceRanges 转到最后一个标记的开头),因为重写器会调整重写长度以适应最后一个标记末尾的额外长度(AFAIK)。
注意,为了从最里面的三元表达式开始重写,我需要从覆盖的
true
返回 bool shouldTraversePostOrder() const
。如果没有这个,clang::Rewriter 就会开始从最外层的三元组重写到子节点(这不是我们想要的处理嵌套的方式,而且当我尝试它时,效果甚至更糟)。
当第二次调用
bool VisitConditionalOperator(clang::ConditionalOperator *CO) const
时,这次在外部三元表达式上,转储重写的重写缓冲区具有以下损坏的内容(再次忽略函数注释以及突出显示的范围 ^~~ ... ~ 〜^)。
// Rewrite buffer after outer (final) rewrite
// (the range below should have been replaced with the above replaced text
void nestedTernaryDeclStmt() {
int ii, jj, kk, ll, mm;
int foo = (/*COND1*/ii > jj) ? (/*LHS1*/( kk <= ll ) ? mm : 4123) : (/*RHS1*/5321) ;
^~~~~~~~~~~~~~~~~~~~~~~^
}
上面突出显示的范围应该已通过第一次重写进行修改,但事实并非如此。我期望最终的编辑缓冲区如下(忽略函数注释):
// This is what I expected as the final rewritten buffer.
void nestedTernaryDeclStmt() {
int ii, jj, kk, ll, mm;
int foo = (/*COND1*/ii > jj) ? (/*LHS1*/(/*COND0*/( kk <= ll )) ? (/*LHS0*/mm) : (/*RHS0*/4123)) : (/*RHS1*/5321) ;
}
我尝试了许多其他标记类型和非标记字符范围的组合,但我尝试的似乎都不起作用。
这是有问题的访问者,它带有一个辅助 lambda,用于将 clang::Stmt 转换为 std::string。
// Experimental recursive visitor class.
class MyVisitor : public clang::RecursiveASTVisitor<MyVisitor> {
public:
explicit MyVisitor(
clang::ASTContext& rContext,
clang::Rewriter& rRewriter)
: mContext{rContext}
, mRewriter{rRewriter}
{}
// default behavior is to traverse the AST in pre-order (override to true to force post-order).
// @JC note that since this uses CRTP pattern (i.e. class Derived : public Base<Derived>),
// the method is not virtual & bypasses the need for a VTable - very clever!
bool shouldTraversePostOrder() const {
return true;
}
//! Visitor pattern callback for 'ConditionalOperator'.
bool VisitConditionalOperator(clang::ConditionalOperator *CO) const {
// This method is called for every 'ConditionalOperator' in the code.
// You can examine 'CO' to extract information about it.
const auto& SM = mContext.getSourceManager();
const auto& LO = mContext.getLangOpts();
const auto sourceRange = CO->getSourceRange();
// Assume SM is a clang::SourceManager object and Range is a clang::SourceRange for the range
const auto BLoc = sourceRange.getBegin();
const auto ELoc = sourceRange.getEnd();
// Adjust the end location to the end of the last token
const auto AdjustedELoc = clang::Lexer::getLocForEndOfToken(
ELoc, 0, SM, LO);
// Create adjusted range that includes the length of the last token
clang::SourceRange AdjustedRange(BLoc, AdjustedELoc);
auto CSR1 = clang::CharSourceRange::getCharRange(BLoc, AdjustedELoc);
//CSR1.setTokenRange(true);
unsigned BLine = SM.getSpellingLineNumber(BLoc);
unsigned BCol = SM.getSpellingColumnNumber(BLoc);
unsigned ELine = SM.getSpellingLineNumber(ELoc);
unsigned ECol = SM.getSpellingColumnNumber(ELoc);
unsigned adjustedELine = SM.getSpellingLineNumber(AdjustedELoc);
unsigned adjustedECol = SM.getSpellingColumnNumber(AdjustedELoc);
auto cond = gStmtToString(&mContext, CO->getCond());
auto lhs = gStmtToString(&mContext, CO->getLHS());
auto rhs = gStmtToString(&mContext, CO->getRHS());
// Instrument as follows:
// add comments to each part of the ConditionalOperator.
const auto probeText = std::format(
"(/*COND{0}*/{1}) ? (/*LHS{0}*/{2}) : (/*RHS{0}*/{3})"
, gProbeIndex++
, cond
, lhs
, rhs);
mRewriter.ReplaceText(/*AdjustedRange*/sourceRange/*CSR1*/, probeText);
// Get the RewriteBuffer for the main file.
std::string str;
llvm::raw_string_ostream rso(str);
clang::RewriteBuffer &RB = mRewriter.getEditBuffer(SM.getMainFileID());
RB.write(rso);
rso.flush();
// returning false aborts the traversal
return true;
}
private:
clang::ASTContext& mContext;
clang::Rewriter& mRewriter;
};
这里是将 clang::Stmt (以及 clang::Expn)转换为 std::string 的辅助 lambda。
// SYSTEM INCLUDES
#include <string>
#pragma warning(push)
#pragma warning(disable : 4146 4267 4291 4100 4244 4624)
#include <clang/Tooling/Tooling.h>
#include <clang/AST/ASTConsumer.h>
#include <clang/Tooling/CommonOptionsParser.h>
#include <clang/AST/RecursiveASTVisitor.h>
#include <clang/ASTMatchers/ASTMatchers.h>
#include <clang/ASTMatchers/ASTMatchFinder.h>
#include <clang/Frontend/CompilerInstance.h>
#include <clang/Rewrite/Core/Rewriter.h>
#pragma warning(pop)
// STATIC VARIABLE INITIALIZATIONS
namespace {
//! The CProbe tool category.
cl::OptionCategory gToolCategory("CProbe Tool Category");
//! The probe index
int gProbeIndex = 0;
// Convert clang::Stmt or clang::Expr (subclass of Stmt) to std::string.
auto gStmtToString = [](
const clang::ASTContext* context,
const clang::Stmt* stmt) -> std::string {
const auto& SM = context->getSourceManager();
const auto& LO = context->getLangOpts();
const auto startLoc = stmt->getBeginLoc();
const auto endLoc = clang::Lexer::getLocForEndOfToken(
stmt->getEndLoc(), 0, SM, LO);
if (SM.isWrittenInSameFile(startLoc, endLoc)) {
const auto charRange = clang::CharSourceRange::getCharRange(startLoc, endLoc);
return clang::Lexer::getSourceText(charRange, SM, LO).str();
}
return {};
};
}
使其作为独立的 libTooling 实用程序工作的其余代码如下:
// ASTConsumer implementation reads AST produced by the Clang parser.
class MyASTConsumer : public clang::ASTConsumer {
public:
MyASTConsumer(clang::ASTContext& ctx, clang::Rewriter& r)
: mVisitor(ctx, r) {
// Add handler for translation unit.
mMatcher.addMatcher(traverse(clang::TK_IgnoreUnlessSpelledInSource,
translationUnitDecl().
bind("translationUnitDecl")), &mTranslationUnitHandler);
}
//! Insert the macro definitions at the beginning of the translation unit.
void HandleTranslationUnit(clang::ASTContext& context) override {
// first expand all the ConditionalOperator nodes
mVisitor.TraverseDecl(context.getTranslationUnitDecl());
}
private:
TranslationUnitHandler mTranslationUnitHandler;
MyVisitor mVisitor;
};
// For each source file provided to the tool, a new FrontendAction is created.
class CProbeFrontEndAction : public clang::ASTFrontendAction {
public:
//! explicit Constructor - pass the output path to write the instrumented source.
explicit CProbeFrontEndAction(fs::path rOutputPath)
: mOutputPath(std::move(rOutputPath))
{}
//! This function is called after parsing the source file.
void EndSourceFileAction() override {
const auto& SM = mRewriter.getSourceMgr();
std::error_code error_code;
raw_fd_ostream outFile(
mOutputPath.generic_string(),
error_code, sys::fs::OF_None);
// write the result to outFile
mRewriter.getEditBuffer(SM.getMainFileID()).write(outFile);
outFile.close();
}
std::unique_ptr<clang::ASTConsumer> CreateASTConsumer(clang::CompilerInstance& CI, StringRef file) override {
mRewriter.setSourceMgr(CI.getSourceManager(), CI.getLangOpts());
return std::make_unique<MyASTConsumer>(CI.getASTContext(), mRewriter);
}
private:
fs::path mOutputPath;
clang::Rewriter mRewriter;
};
//! Factory function for creating a new FrontendAction that takes a user parameter.
std::unique_ptr<FrontendActionFactory> myNewFrontendActionFactory(const fs::path& rOutputPath) {
class SimpleFrontendActionFactory : public FrontendActionFactory {
public:
explicit SimpleFrontendActionFactory(fs::path aOutputPath)
: mOutputPath(std::move(aOutputPath))
{}
std::unique_ptr<clang::FrontendAction> create() override {
return std::make_unique<CProbeFrontEndAction>(mOutputPath);
}
private:
fs::path mOutputPath;
};
return std::make_unique<SimpleFrontendActionFactory>(rOutputPath);
}
// Instrument the code - rewriting it to rOutputPath.
void instrumentSource(const fs::path& rInputPath, const fs::path& rOutputPath) {
std::string program = "foo.exe";
std::string inputPath = rInputPath.generic_string();
std::string outputPath = rOutputPath.generic_string();
const char* argv[] = { program.data(), inputPath.data(), "--", nullptr };
int argc = 3;
auto expectedParser = CommonOptionsParser::create(
argc, argv, gToolCategory);
if (expectedParser) {
CommonOptionsParser& optionsParser = expectedParser.get();
ClangTool tool(optionsParser.getCompilations(), optionsParser.getSourcePathList());
tool.run(myNewFrontendActionFactory(rOutputPath.generic_string()).get());
}
}
对于其他背景,这里是从 clang-query 报告的简化 AST 树(启用了设置 Traversal IgnoreUnlessSpelledInSource 选项)
clang-query> m conditionalOperator().bind("ternary")
Match #1:
Binding for "ternary":
ConditionalOperator 0x558606d904e0 </mnt/c/temp/trivial/src/NanoFile.c:3:16, col:54> 'int'
|-BinaryOperator 0x558606d90368 <col:16, col:21> 'int' '>'
| |-DeclRefExpr 0x558606d902f8 <col:16> 'int' lvalue Var 0x558606d8ffc8 'ii' 'int'
| `-DeclRefExpr 0x558606d90318 <col:21> 'int' lvalue Var 0x558606d90048 'jj' 'int'
|-ConditionalOperator 0x558606d90490 <col:27, col:47> 'int'
| |-BinaryOperator 0x558606d903f8 <col:29, col:35> 'int' '<='
| | |-DeclRefExpr 0x558606d90388 <col:29> 'int' lvalue Var 0x558606d900c8 'kk' 'int'
| | `-DeclRefExpr 0x558606d903a8 <col:35> 'int' lvalue Var 0x558606d90148 'll' 'int'
| |-DeclRefExpr 0x558606d90438 <col:42> 'int' lvalue Var 0x558606d901c8 'mm' 'int'
| `-IntegerLiteral 0x558606d90458 <col:47> 'int' 4123
`-IntegerLiteral 0x558606d904c0 <col:54> 'int' 5321
Match #2:
Binding for "ternary":
ConditionalOperator 0x558606d90490 </mnt/c/temp/trivial/src/NanoFile.c:3:27, col:47> 'int'
|-BinaryOperator 0x558606d903f8 <col:29, col:35> 'int' '<='
| |-DeclRefExpr 0x558606d90388 <col:29> 'int' lvalue Var 0x558606d900c8 'kk' 'int'
| `-DeclRefExpr 0x558606d903a8 <col:35> 'int' lvalue Var 0x558606d90148 'll' 'int'
|-DeclRefExpr 0x558606d90438 <col:42> 'int' lvalue Var 0x558606d901c8 'mm' 'int'
`-IntegerLiteral 0x558606d90458 <col:47> 'int' 4123
2 matches.
以下是从 clang-query 报告的三元数:
clang-query> m conditionalOperator().bind("ternary")
Match #1:
/mnt/c/temp/trivial/src/NanoFile.c:3:16: note: "ternary" binds here
3 | int foo = ii > jj ? ( kk <= ll ) ? mm : 4123 : 5321 ;
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Match #2:
/mnt/c/temp/trivial/src/NanoFile.c:3:27: note: "ternary" binds here
3 | int foo = ii > jj ? ( kk <= ll ) ? mm : 4123 : 5321 ;
| ^~~~~~~~~~~~~~~~~~~~~~~~
2 matches.
clang-query>
您的尝试即将成功。该错误位于这一行
gStmtToString
:
return clang::Lexer::getSourceText(charRange, SM, LO).str();
^^^^^^^^^^^^^^^^^^^^^^^^^^^
这将检索指定范围的“原始”源文本。 当此代码用于获取具有以下内容的子表达式的文本时: 已经被重写,它有效地丢弃了这些重写。 解决方法是询问
对于修改后的代码:
return mRewriter.getRewrittenText(charRange);
^^^^^^^^^^^^^^^^^^^^^^^^^^
修复后,您的程序应该按预期运行。
概念背景
, 它提供对原始源代码的访问,并维护 数据结构允许
SourceLocation
被紧凑地编码为单个 32 位整数。
, 它提供了
SourceManager
所具有的基于标记的文本视图,
并且还能够与
Preprocessor
进行宏扩展等
, 它维护要执行的重写操作的有序序列。 您可以要求它合成部分或全部生成的文本 从应用这些重写,但它实际上并没有改变 任何在它自己的对象之外的东西,直到你调用
overwriteChangedFiles
.
// rewrite-ternary.cc
// Attempt to rewrite ternary expressions.
#include "clang/AST/ASTContext.h" // clang::ASTContext
#include "clang/AST/RecursiveASTVisitor.h" // clang::RecursiveASTVisitor
#include "clang/Basic/Diagnostic.h" // clang::DiagnosticsEngine
#include "clang/Basic/DiagnosticOptions.h" // clang::DiagnosticOptions
#include "clang/Basic/SourceLocation.h" // clang::SourceLocation
#include "clang/Basic/SourceManager.h" // clang::SourceManager
#include "clang/Frontend/ASTUnit.h" // clang::ASTUnit
#include "clang/Frontend/CompilerInstance.h" // clang::CompilerInstance
#include "clang/Rewrite/Core/Rewriter.h" // clang::Rewriter
#include "clang/Serialization/PCHContainerOperations.h" // clang::PCHContainerOperations
#include <iostream> // std::cout
#include <sstream> // std::ostringstream
#include <string> // std::string
using std::cout;
int gProbeIndex = 0;
// Experimental recursive visitor class.
class MyVisitor : public clang::RecursiveASTVisitor<MyVisitor> {
public:
explicit MyVisitor(
clang::ASTContext& rContext,
clang::Rewriter& rRewriter)
: mContext{rContext}
, mRewriter{rRewriter}
{}
// default behavior is to traverse the AST in pre-order (override to true to force post-order).
// @JC note that since this uses CRTP pattern (i.e. class Derived : public Base<Derived>),
// the method is not virtual & bypasses the need for a VTable - very clever!
bool shouldTraversePostOrder() const {
return true;
}
std::string gStmtToString(const clang::Stmt* stmt) const {
const auto& SM = mContext.getSourceManager();
const auto& LO = mContext.getLangOpts();
const auto startLoc = stmt->getBeginLoc();
const auto endLoc = clang::Lexer::getLocForEndOfToken(
stmt->getEndLoc(), 0, SM, LO);
if (SM.isWrittenInSameFile(startLoc, endLoc)) {
const auto charRange = clang::CharSourceRange::getCharRange(startLoc, endLoc);
// This is the key fix: get the text *after* taking prior
// rewrites into account.
return mRewriter.getRewrittenText(charRange);
// This was the old code. It simply reads the original
// text, and consequently, any rewrites that had been
// performed within the indicated range were lost.
//return clang::Lexer::getSourceText(charRange, SM, LO).str();
}
return {};
};
//! Visitor pattern callback for 'ConditionalOperator'.
bool VisitConditionalOperator(clang::ConditionalOperator *CO) const {
// This method is called for every 'ConditionalOperator' in the code.
// You can examine 'CO' to extract information about it.
const auto& SM = mContext.getSourceManager();
const auto sourceRange = CO->getSourceRange();
// Diagnostics.
cout << "ternary range: " << sourceRange.printToString(SM) << "\n";
cout << "gStmtToString: " << gStmtToString(CO) << "\n";
// Get the text of the sub-expressions, taking into account the
// rewrites already performed on any of *their* sub-expressions.
auto cond = gStmtToString(CO->getCond());
auto lhs = gStmtToString(CO->getLHS());
auto rhs = gStmtToString(CO->getRHS());
// Construct the instrumented code that will replace the
// original expression 'CO'.
std::string probeText;
{
// I'm not set up for c++20 at the moment, so use
// std::ostringstream as an alternative to std::format.
std::ostringstream oss;
oss << "(/*COND" << gProbeIndex << "*/" << cond
<< ") ? (/*LHS" << gProbeIndex << "*/" << lhs
<< ") : (/*RHS" << gProbeIndex << "*/" << rhs
<< ")";
probeText = oss.str();
++gProbeIndex;
}
mRewriter.ReplaceText(sourceRange, probeText);
// Get the entire current rewrite buffer.
std::string str;
{
llvm::raw_string_ostream rso(str);
clang::RewriteBuffer &RB = mRewriter.getEditBuffer(SM.getMainFileID());
RB.write(rso);
rso.flush();
}
// Print it for diagnostic purposes.
cout << "---- BEGIN RB ----\n";
cout << str;
cout << "---- END RB ----\n";
// returning false aborts the traversal
return true;
}
private:
clang::ASTContext& mContext;
clang::Rewriter& mRewriter;
};
// This is all boilerplate for a program using the Clang C++ API
// ("libtooling") but not using the "tooling" part specifically.
int main(int argc, char const **argv)
{
// Copy the arguments into a vector of char pointers since that is
// what 'createInvocationFromCommandLine' wants.
std::vector<char const *> commandLine;
{
// Path to the 'clang' binary that I am behaving like. This path is
// used to compute the location of compiler headers like stddef.h.
// The Makefile sets 'CLANG_LLVM_INSTALL_DIR' on the compilation
// command line.
commandLine.push_back(CLANG_LLVM_INSTALL_DIR "/bin/clang");
for (int i = 1; i < argc; ++i) {
commandLine.push_back(argv[i]);
}
}
// Parse the command line options.
std::shared_ptr<clang::CompilerInvocation> compilerInvocation(
clang::createInvocation(llvm::ArrayRef(commandLine)));
if (!compilerInvocation) {
// Command line parsing errors have already been printed.
return 2;
}
// Boilerplate setup for 'LoadFromCompilerInvocationAction'.
std::shared_ptr<clang::PCHContainerOperations> pchContainerOps(
new clang::PCHContainerOperations());
clang::IntrusiveRefCntPtr<clang::DiagnosticsEngine> diagnosticsEngine(
clang::CompilerInstance::createDiagnostics(
new clang::DiagnosticOptions));
// Run the Clang parser to produce an AST.
std::unique_ptr<clang::ASTUnit> ast(
clang::ASTUnit::LoadFromCompilerInvocationAction(
compilerInvocation,
pchContainerOps,
diagnosticsEngine));
if (ast == nullptr ||
diagnosticsEngine->getNumErrors() > 0) {
// Error messages have already been printed.
return 2;
}
clang::ASTContext &astContext = ast->getASTContext();
clang::Rewriter rewriter(astContext.getSourceManager(),
astContext.getLangOpts());
MyVisitor visitor(astContext, rewriter);
visitor.TraverseDecl(astContext.getTranslationUnitDecl());
return 0;
}
// EOF
# Makefile
# Default target.
all:
.PHONY: all
# ---- Configuration ----
# Installation directory from a binary distribution.
# Has five subdirectories: bin include lib libexec share.
CLANG_LLVM_INSTALL_DIR = $(HOME)/opt/clang+llvm-16.0.0-x86_64-linux-gnu-ubuntu-18.04
# ---- llvm-config query results ----
# Program to query the various LLVM configuration options.
LLVM_CONFIG := $(CLANG_LLVM_INSTALL_DIR)/bin/llvm-config
# C++ compiler options to ensure ABI compatibility.
LLVM_CXXFLAGS := $(shell $(LLVM_CONFIG) --cxxflags)
# Directory containing the clang library files, both static and dynamic.
LLVM_LIBDIR := $(shell $(LLVM_CONFIG) --libdir)
# Other flags needed for linking, whether statically or dynamically.
LLVM_LDFLAGS_AND_SYSTEM_LIBS := $(shell $(LLVM_CONFIG) --ldflags --system-libs)
# ---- Compiler options ----
# C++ compiler.
CXX := $(CLANG_LLVM_INSTALL_DIR)/bin/clang++
# Compiler options, including preprocessor options.
CXXFLAGS =
CXXFLAGS += -g
CXXFLAGS += -Wall
CXXFLAGS += -Werror
# Get llvm compilation flags.
CXXFLAGS += $(LLVM_CXXFLAGS)
# Tell the source code where the clang installation directory is.
CXXFLAGS += -DCLANG_LLVM_INSTALL_DIR='"$(CLANG_LLVM_INSTALL_DIR)"'
# Linker options.
LDFLAGS =
LDFLAGS += -g -Wall
# Pull in clang+llvm via libclang-cpp.so, which has everything, but is
# only available as a dynamic library.
LDFLAGS += -lclang-cpp
# Arrange for the compiled binary to search the libdir for that library.
# Otherwise, one can set the LD_LIBRARY_PATH envvar before running it.
# Note: the -rpath switch does not work on Windows.
LDFLAGS += -Wl,-rpath=$(LLVM_LIBDIR)
# Get the needed -L search path, plus things like -ldl.
LDFLAGS += $(LLVM_LDFLAGS_AND_SYSTEM_LIBS)
# ---- Recipes ----
# Compile a C++ source file.
%.o: %.cc
$(CXX) -c -o $@ $(CXXFLAGS) $<
# Executable.
all: rewrite-ternary.exe
rewrite-ternary.exe: rewrite-ternary.o
$(CXX) -o $@ $^ $(LDFLAGS)
# Test.
.PHONY: check
check: rewrite-ternary.exe
./rewrite-ternary.exe -w test.cc
.PHONY: clean
clean:
$(RM) *.o *.exe
# EOF
测试输入:
void f()
{
1? 2? 3 : 4: 5;
}
输出:
ternary range: <test.cc:3:6, col:13>
gStmtToString: 2? 3 : 4
---- BEGIN RB ----
void f()
{
1? (/*COND0*/2) ? (/*LHS0*/3) : (/*RHS0*/4): 5;
}
---- END RB ----
ternary range: <test.cc:3:3, col:16>
gStmtToString: 1? (/*COND0*/2) ? (/*LHS0*/3) : (/*RHS0*/4): 5
---- BEGIN RB ----
void f()
{
(/*COND1*/1) ? (/*LHS1*/(/*COND0*/2) ? (/*LHS0*/3) : (/*RHS0*/4)) : (/*RHS1*/5);
}
---- END RB ----