使用clang的libTooling重写嵌套三元表达式

问题描述 投票:0回答:1

以下包含嵌套三元语句的“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>
clang abstract-syntax-tree libtooling
1个回答
0
投票

修复bug

您的尝试即将成功。该错误位于这一行

gStmtToString

return clang::Lexer::getSourceText(charRange, SM, LO).str();
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^

这将检索指定范围的“原始”源文本。 当此代码用于获取具有以下内容的子表达式的文本时: 已经被重写,它有效地丢弃了这些重写。 解决方法是询问

Rewriter

对于修改后的代码:
return mRewriter.getRewrittenText(charRange); ^^^^^^^^^^^^^^^^^^^^^^^^^^

修复后,您的程序应该按预期运行。

概念背景

为了清楚起见,一些相关的部分是:

问题中的代码不足以制作一个工作程序, 所以我最终填写了一些细节并删除了其他细节,但是 本质还在。

// 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 ----

© www.soinside.com 2019 - 2024. All rights reserved.