ascii-chat 0.8.38
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
defer/tool.cpp
Go to the documentation of this file.
1#include <cstddef>
2#include <filesystem>
3#include <fstream>
4#include <mutex>
5#include <optional>
6#include <set>
7#include <string>
8#include <unordered_map>
9#include <unordered_set>
10#include <vector>
11
12#include "clang/AST/ASTConsumer.h"
13#include "clang/AST/ASTContext.h"
14#include "clang/AST/RecursiveASTVisitor.h"
15#include "clang/Frontend/CompilerInstance.h"
16#include "clang/Lex/Lexer.h"
17#include "clang/Lex/Preprocessor.h"
18#include "clang/Rewrite/Core/Rewriter.h"
19#include "clang/Driver/Driver.h"
20#include "clang/Tooling/ArgumentsAdjusters.h"
21#include "clang/Tooling/CommonOptionsParser.h"
22#include "clang/Tooling/Tooling.h"
23#include "llvm/Support/CommandLine.h"
24#include "llvm/Support/FileSystem.h"
25#include "llvm/Support/InitLLVM.h"
26#include "llvm/Support/Path.h"
27#include "llvm/Support/raw_ostream.h"
28
29using namespace llvm;
30using namespace clang;
31
32namespace fs = std::filesystem;
33
34std::mutex &outputRegistryMutex() {
35 static std::mutex mutex;
36 return mutex;
37}
38
39std::unordered_set<std::string> &outputRegistry() {
40 static std::unordered_set<std::string> registry;
41 return registry;
42}
43
44bool registerOutputPath(const std::string &path) {
45 std::lock_guard<std::mutex> guard(outputRegistryMutex());
46 auto &registry = outputRegistry();
47 bool inserted = registry.insert(path).second;
48 return inserted;
49}
50
51void unregisterOutputPath(const std::string &path) {
52 std::lock_guard<std::mutex> guard(outputRegistryMutex());
53 outputRegistry().erase(path);
54}
55
56// Command line options
57static cl::OptionCategory ToolCategory("ascii-defer transformation options");
58static cl::extrahelp CommonHelp(tooling::CommonOptionsParser::HelpMessage);
59static cl::extrahelp MoreHelp("\nDefer transformation tool for ascii-chat\n");
60
61static cl::opt<std::string> OutputDirectoryOption("output-dir",
62 cl::desc("Directory where transformed sources will be written"),
63 cl::value_desc("path"), cl::Required, cl::cat(ToolCategory));
64
65static cl::opt<std::string>
66 InputRootOption("input-root", cl::desc("Root directory of original sources (used to compute relative paths)"),
67 cl::value_desc("path"), cl::init(""), cl::cat(ToolCategory));
68
69static cl::opt<std::string> BuildPath("p", cl::desc("Build path (directory containing compile_commands.json)"),
70 cl::Optional, cl::cat(ToolCategory));
71
72static cl::opt<bool> QuietMode("quiet", cl::desc("Suppress verbose diagnostic output"), cl::init(false),
73 cl::cat(ToolCategory));
74
75static cl::list<std::string> SourcePaths(cl::Positional, cl::desc("<source0> [... <sourceN>]"), cl::cat(ToolCategory));
76
77namespace {
78
79// Structure to track a block (compound statement) that contains defers
80struct BlockScope {
81 CompoundStmt *stmt = nullptr;
82 unsigned scopeId = 0;
83 unsigned depth = 0; // Nesting depth (0 = function body)
84 bool hasDefers = false;
85 bool endsWithReturn = false; // True if block's last statement is a return
86 SourceLocation startLoc; // After opening brace
87 SourceLocation endLoc; // Before closing brace
88};
89
90// Structure to track defer calls within a function
91struct DeferCall {
92 SourceLocation location;
93 SourceLocation endLocation; // End of the defer statement (after semicolon)
94 unsigned fileOffset; // File offset of the defer statement
95 std::string expression; // The code to execute (e.g., "fclose(f)" or "{ cleanup(); }")
96 unsigned scopeId; // Which scope this defer belongs to
97};
98
99// Structure to track return statements and their active scopes
100struct ReturnInfo {
101 SourceLocation location;
102 unsigned fileOffset; // File offset of the return statement
103 std::vector<unsigned> activeScopeIds; // Scopes that are active at this return
104};
105
106// Structure to track function transformation state
107struct FunctionTransformState {
108 FunctionDecl *funcDecl = nullptr;
109 std::vector<DeferCall> deferCalls;
110 std::vector<ReturnInfo> returnInfos;
111 std::map<unsigned, BlockScope> blockScopes; // scopeId -> BlockScope
112 std::vector<unsigned> currentScopeStack; // Stack of active scope IDs during traversal
113 bool needsTransformation = false;
114 unsigned nextScopeId = 0;
115};
116
117class DeferVisitor : public RecursiveASTVisitor<DeferVisitor> {
118public:
119 DeferVisitor(ASTContext &context, Rewriter &rewriter, const fs::path &outputDir, const fs::path &inputRoot)
120 : context_(context), rewriter_(rewriter), outputDir_(outputDir), inputRoot_(inputRoot) {}
121
122 bool TraverseFunctionDecl(FunctionDecl *funcDecl) {
123 if (!funcDecl || funcDecl->isImplicit()) {
124 return RecursiveASTVisitor<DeferVisitor>::TraverseFunctionDecl(funcDecl);
125 }
126
127 SourceManager &sourceManager = context_.getSourceManager();
128 SourceLocation location = funcDecl->getLocation();
129 location = sourceManager.getExpansionLoc(location);
130 if (!location.isValid() || !sourceManager.isWrittenInMainFile(location)) {
131 return RecursiveASTVisitor<DeferVisitor>::TraverseFunctionDecl(funcDecl);
132 }
133
134 // Start tracking this function
135 currentFunction_ = FunctionTransformState();
136 currentFunction_.funcDecl = funcDecl;
137
138 bool result = RecursiveASTVisitor<DeferVisitor>::TraverseFunctionDecl(funcDecl);
139
140 // Transform function if it contains defer calls
141 if (currentFunction_.needsTransformation && !currentFunction_.deferCalls.empty()) {
142 transformFunction(currentFunction_);
143 }
144
145 currentFunction_ = FunctionTransformState();
146 return result;
147 }
148
149 bool TraverseCompoundStmt(CompoundStmt *compoundStmt) {
150 if (!compoundStmt || !currentFunction_.funcDecl) {
151 return RecursiveASTVisitor<DeferVisitor>::TraverseCompoundStmt(compoundStmt);
152 }
153
154 SourceManager &sourceManager = context_.getSourceManager();
155 SourceLocation lbracLoc = compoundStmt->getLBracLoc();
156 if (!lbracLoc.isValid() || !sourceManager.isWrittenInMainFile(lbracLoc)) {
157 return RecursiveASTVisitor<DeferVisitor>::TraverseCompoundStmt(compoundStmt);
158 }
159
160 // Create a new scope for this block
161 unsigned scopeId = currentFunction_.nextScopeId++;
162 unsigned depth = currentFunction_.currentScopeStack.size();
163
164 BlockScope blockScope;
165 blockScope.stmt = compoundStmt;
166 blockScope.scopeId = scopeId;
167 blockScope.depth = depth;
168 blockScope.hasDefers = false;
169 blockScope.endsWithReturn = false;
170 blockScope.startLoc = compoundStmt->getLBracLoc().getLocWithOffset(1);
171 blockScope.endLoc = compoundStmt->getRBracLoc();
172
173 // Check if block ends with a return statement
174 if (!compoundStmt->body_empty()) {
175 Stmt *lastStmt = compoundStmt->body_back();
176 if (isa<ReturnStmt>(lastStmt)) {
177 blockScope.endsWithReturn = true;
178 }
179 }
180
181 currentFunction_.blockScopes[scopeId] = blockScope;
182 currentFunction_.currentScopeStack.push_back(scopeId);
183
184 // Traverse children
185 bool result = RecursiveASTVisitor<DeferVisitor>::TraverseCompoundStmt(compoundStmt);
186
187 // Pop the scope
188 currentFunction_.currentScopeStack.pop_back();
189
190 return result;
191 }
192
193 bool TraverseStmt(Stmt *stmt) {
194 if (!stmt || !currentFunction_.funcDecl) {
195 return RecursiveASTVisitor<DeferVisitor>::TraverseStmt(stmt);
196 }
197
198 // Skip container statements that may contain defers in nested blocks.
199 // We DON'T skip DoStmt because defer() macro expands to do { ... } while(0).
200 // We DO skip IfStmt/ForStmt/WhileStmt/SwitchStmt because their source text
201 // includes child statements with defers that should be tracked at inner scopes.
202
203 if (isa<CompoundStmt>(stmt) || isa<IfStmt>(stmt) || isa<ForStmt>(stmt) || isa<WhileStmt>(stmt) ||
204 isa<SwitchStmt>(stmt)) {
205 return RecursiveASTVisitor<DeferVisitor>::TraverseStmt(stmt);
206 }
207
208 SourceManager &sourceManager = context_.getSourceManager();
209 SourceLocation stmtLoc = stmt->getBeginLoc();
210
211 // For macro-expanded statements, check the expansion location
212 SourceLocation checkLoc = stmtLoc;
213 if (stmtLoc.isMacroID()) {
214 checkLoc = sourceManager.getExpansionLoc(stmtLoc);
215 }
216
217 if (checkLoc.isValid() && sourceManager.isWrittenInMainFile(checkLoc)) {
218 CharSourceRange range;
219 bool isMacro = stmtLoc.isMacroID();
220
221 if (isMacro) {
222 // For macro-expanded statements, get the full macro call range
223 // This includes the macro name AND arguments
224 CharSourceRange macroRange = sourceManager.getImmediateExpansionRange(stmtLoc);
225 range = macroRange;
226 } else {
227 SourceLocation begin = stmt->getBeginLoc();
228 SourceLocation end = stmt->getEndLoc();
229 if (!begin.isValid() || !end.isValid()) {
230 return RecursiveASTVisitor<DeferVisitor>::TraverseStmt(stmt);
231 }
232 range = CharSourceRange::getTokenRange(begin, end);
233 }
234
235 bool invalid = false;
236 StringRef stmtText = Lexer::getSourceText(range, sourceManager, context_.getLangOpts(), &invalid);
237
238 // Get begin location for defer location calculation
239 SourceLocation begin = isMacro ? range.getBegin() : stmt->getBeginLoc();
240
241 // Only process defer() for DoStmt (or non-macro statements)
242 // This avoids processing the same defer multiple times for child nodes
243 bool shouldProcess = !isMacro || isa<DoStmt>(stmt);
244
245 if (shouldProcess && !invalid && containsDeferCall(stmtText)) {
246 // Found a defer() call - extract it
247 size_t deferPos = findDeferCall(stmtText);
248 if (deferPos != StringRef::npos) {
249 // Find matching closing parenthesis
250 size_t openParen = deferPos + 5; // after "defer"
251 size_t closeParen = findMatchingParen(stmtText, openParen);
252
253 if (closeParen != StringRef::npos) {
254 // Extract the expression inside defer(...)
255 StringRef expression = stmtText.substr(openParen + 1, closeParen - openParen - 1);
256
257 // Calculate the actual source location of "defer" in the file
258 SourceLocation deferLoc = begin.getLocWithOffset(deferPos);
259
260 // Calculate the end location (after the closing paren, we'll find semicolon later)
261 SourceLocation deferEndLoc = begin.getLocWithOffset(closeParen + 1);
262
263 // Get the current scope ID (innermost scope)
264 unsigned currentScopeId = 0;
265 if (!currentFunction_.currentScopeStack.empty()) {
266 currentScopeId = currentFunction_.currentScopeStack.back();
267 }
268
269 // Store the expression as-is - we'll inline it directly at exit points
270 std::string exprStr = expression.str();
271
272 // Trim leading/trailing whitespace
273 size_t firstNonSpace = exprStr.find_first_not_of(" \t\n\r");
274 size_t lastNonSpace = exprStr.find_last_not_of(" \t\n\r");
275 if (firstNonSpace != std::string::npos && lastNonSpace != std::string::npos) {
276 exprStr = exprStr.substr(firstNonSpace, lastNonSpace - firstNonSpace + 1);
277 }
278
279 DeferCall deferCall;
280 deferCall.location = deferLoc;
281 deferCall.endLocation = deferEndLoc;
282 deferCall.fileOffset = sourceManager.getFileOffset(deferLoc);
283 deferCall.expression = exprStr;
284 deferCall.scopeId = currentScopeId;
285
286 // Mark the scope as having defers
287 if (currentFunction_.blockScopes.count(currentScopeId)) {
288 currentFunction_.blockScopes[currentScopeId].hasDefers = true;
289 }
290
291 currentFunction_.deferCalls.push_back(deferCall);
292 currentFunction_.needsTransformation = true;
293 }
294 }
295 }
296 }
297
298 return RecursiveASTVisitor<DeferVisitor>::TraverseStmt(stmt);
299 }
300
301 bool TraverseReturnStmt(ReturnStmt *returnStmt) {
302 if (returnStmt && currentFunction_.funcDecl) {
303 SourceManager &sourceManager = context_.getSourceManager();
304 SourceLocation location = returnStmt->getReturnLoc();
305 if (location.isValid()) {
306 SourceLocation expansionLocation = sourceManager.getExpansionLoc(location);
307 if (expansionLocation.isValid() && sourceManager.isWrittenInMainFile(expansionLocation)) {
308 // Record return with its active scopes (copy the current scope stack)
309 ReturnInfo returnInfo;
310 returnInfo.location = expansionLocation;
311 returnInfo.fileOffset = sourceManager.getFileOffset(expansionLocation);
312 returnInfo.activeScopeIds = currentFunction_.currentScopeStack;
313 currentFunction_.returnInfos.push_back(returnInfo);
314 }
315 }
316 }
317 return RecursiveASTVisitor<DeferVisitor>::TraverseReturnStmt(returnStmt);
318 }
319
320 std::string makeRelativePath(const fs::path &absolutePath) const {
321 if (inputRoot_.empty()) {
322 return absolutePath.generic_string();
323 }
324
325 std::error_code errorCode;
326 fs::path relative = fs::relative(absolutePath, inputRoot_, errorCode);
327 if (errorCode) {
328 return absolutePath.generic_string();
329 }
330 return relative.generic_string();
331 }
332
333private:
334 size_t findMatchingParen(StringRef text, size_t openPos) const {
335 if (openPos >= text.size() || text[openPos] != '(') {
336 return StringRef::npos;
337 }
338
339 int depth = 1;
340 for (size_t i = openPos + 1; i < text.size(); i++) {
341 if (text[i] == '(') {
342 depth++;
343 } else if (text[i] == ')') {
344 depth--;
345 if (depth == 0) {
346 return i;
347 }
348 }
349 }
350
351 return StringRef::npos;
352 }
353
354 // Find "defer(" with word boundary checking - must not be preceded by alphanumeric or underscore
355 // Returns position of 'd' in "defer(" or StringRef::npos if not found
356 size_t findDeferCall(StringRef text, size_t startPos = 0) const {
357 size_t pos = startPos;
358 while (pos < text.size()) {
359 size_t found = text.find("defer(", pos);
360 if (found == StringRef::npos) {
361 return StringRef::npos;
362 }
363 // Check if preceded by word boundary (not alphanumeric or underscore)
364 if (found == 0 || (!std::isalnum(text[found - 1]) && text[found - 1] != '_')) {
365 return found;
366 }
367 // Move past this match and keep searching
368 pos = found + 1;
369 }
370 return StringRef::npos;
371 }
372
373 // Check if text contains a standalone "defer(" call (not part of another identifier)
374 bool containsDeferCall(StringRef text) const {
375 return findDeferCall(text) != StringRef::npos;
376 }
377
378 // Get defers for a specific scope in LIFO order (last registered first)
379 std::vector<const DeferCall *> getDefersForScope(unsigned scopeId, const std::vector<DeferCall> &deferCalls) const {
380 std::vector<const DeferCall *> result;
381 for (const auto &dc : deferCalls) {
382 if (dc.scopeId == scopeId) {
383 result.push_back(&dc);
384 }
385 }
386 // Reverse for LIFO order
387 std::reverse(result.begin(), result.end());
388 return result;
389 }
390
391 // Format a defer expression for inline insertion
392 std::string formatDeferExpression(const std::string &expr) const {
393 // Check if it's a block-style defer (starts with '{')
394 if (!expr.empty() && expr[0] == '{') {
395 // Block defer - execute the block directly
396 return "do " + expr + " while(0); ";
397 } else {
398 // Function call defer - just add semicolon if needed
399 std::string result = expr;
400 // Trim trailing semicolons/whitespace
401 while (!result.empty() && (result.back() == ';' || result.back() == ' ' || result.back() == '\t' ||
402 result.back() == '\n' || result.back() == '\r')) {
403 result.pop_back();
404 }
405 return result + "; ";
406 }
407 }
408
409 // Generate inline cleanup code for all active scopes at a return statement (LIFO order)
410 // Only includes defers that were declared BEFORE the return statement
411 std::string generateInlineCleanupForReturn(const ReturnInfo &returnInfo, const FunctionTransformState &state) const {
412 std::string code;
413 // Process scopes from innermost to outermost
414 for (auto scopeIt = returnInfo.activeScopeIds.rbegin(); scopeIt != returnInfo.activeScopeIds.rend(); ++scopeIt) {
415 unsigned scopeId = *scopeIt;
416 auto blockIt = state.blockScopes.find(scopeId);
417 if (blockIt == state.blockScopes.end() || !blockIt->second.hasDefers) {
418 continue;
419 }
420 // Get defers for this scope in LIFO order, but only those declared before this return
421 auto defers = getDefersForScopeBeforeOffset(scopeId, state.deferCalls, returnInfo.fileOffset);
422 for (const auto *dc : defers) {
423 code += formatDeferExpression(dc->expression);
424 }
425 }
426 return code;
427 }
428
429 // Get defers for a specific scope that were declared before a given file offset (in LIFO order)
430 std::vector<const DeferCall *>
431 getDefersForScopeBeforeOffset(unsigned scopeId, const std::vector<DeferCall> &deferCalls, unsigned maxOffset) const {
432 std::vector<const DeferCall *> result;
433 for (const auto &dc : deferCalls) {
434 if (dc.scopeId == scopeId && dc.fileOffset < maxOffset) {
435 result.push_back(&dc);
436 }
437 }
438 // Reverse for LIFO order
439 std::reverse(result.begin(), result.end());
440 return result;
441 }
442
443 // Generate inline cleanup code for end of a block (LIFO order)
444 std::string generateInlineCleanupAtBlockEnd(unsigned scopeId, const FunctionTransformState &state) const {
445 std::string code;
446 auto defers = getDefersForScope(scopeId, state.deferCalls);
447 for (const auto *dc : defers) {
448 code += " " + formatDeferExpression(dc->expression) + "\n";
449 }
450 return code;
451 }
452
453 void transformFunction(FunctionTransformState &state) {
454 if (!state.funcDecl || state.deferCalls.empty()) {
455 return;
456 }
457
458 Stmt *body = state.funcDecl->getBody();
459 if (!body) {
460 return;
461 }
462
463 CompoundStmt *compoundBody = dyn_cast<CompoundStmt>(body);
464 if (!compoundBody) {
465 return;
466 }
467
468 // Step 1: Remove all defer() statements
469 for (const DeferCall &deferCall : state.deferCalls) {
470 removeDeferStatement(deferCall);
471 }
472
473 // Step 2: Insert cleanup before each return statement (inline the deferred code)
474 for (const ReturnInfo &returnInfo : state.returnInfos) {
475 std::string cleanup = generateInlineCleanupForReturn(returnInfo, state);
476 if (!cleanup.empty()) {
477 rewriter_.InsertText(returnInfo.location, cleanup, true, true);
478 }
479 }
480
481 // Step 3: Insert cleanup at the end of each block that has defers
482 // Skip blocks that end with a return statement (cleanup already inserted before the return)
483 for (const auto &pair : state.blockScopes) {
484 const BlockScope &blockScope = pair.second;
485 if (blockScope.hasDefers && blockScope.endLoc.isValid() && !blockScope.endsWithReturn) {
486 std::string cleanup = generateInlineCleanupAtBlockEnd(blockScope.scopeId, state);
487 if (!cleanup.empty()) {
488 rewriter_.InsertText(blockScope.endLoc, cleanup, true, true);
489 }
490 }
491 }
492 }
493
494 void removeDeferStatement(const DeferCall &deferCall) {
495 SourceManager &sourceManager = context_.getSourceManager();
496
497 SourceLocation macroLoc = deferCall.location;
498 if (!macroLoc.isValid()) {
499 return;
500 }
501
502 // Get the range covering "defer(expression);"
503 FileID fileId = sourceManager.getFileID(macroLoc);
504 bool invalid = false;
505 StringRef fileData = sourceManager.getBufferData(fileId, &invalid);
506 if (invalid) {
507 return;
508 }
509
510 unsigned offset = sourceManager.getFileOffset(macroLoc);
511
512 // Find "defer(" starting at offset, with word boundary check
513 size_t deferStart = findDeferCall(fileData, offset);
514 if (deferStart == StringRef::npos || deferStart != offset) {
515 return; // Not at the expected position or not a standalone defer call
516 }
517
518 // Find matching closing paren for defer(...)
519 size_t openParen = deferStart + 5; // Position of '(' in defer(
520 size_t closeParen = findMatchingParenInFile(fileData, openParen);
521 if (closeParen == StringRef::npos) {
522 return;
523 }
524
525 // Find the semicolon AFTER the closing paren
526 size_t semicolonPos = closeParen + 1;
527 while (semicolonPos < fileData.size() && (fileData[semicolonPos] == ' ' || fileData[semicolonPos] == '\t' ||
528 fileData[semicolonPos] == '\n' || fileData[semicolonPos] == '\r')) {
529 semicolonPos++;
530 }
531 if (semicolonPos >= fileData.size() || fileData[semicolonPos] != ';') {
532 return; // No semicolon found after closing paren
533 }
534
535 SourceLocation semicolonLoc = macroLoc.getLocWithOffset(semicolonPos - offset);
536 CharSourceRange deferRange = CharSourceRange::getCharRange(macroLoc, semicolonLoc.getLocWithOffset(1));
537
538 // Replace with a comment noting the defer was moved
539 // For block defers, just note it's a block defer to avoid multiline comment issues
540 std::string exprSummary = deferCall.expression;
541 bool isBlockDefer = !exprSummary.empty() && exprSummary[0] == '{';
542 if (isBlockDefer) {
543 exprSummary = "{...}"; // Summarize block defers
544 }
545 std::string comment = "/* defer: " + exprSummary + " (moved to scope exit) */";
546 rewriter_.ReplaceText(deferRange, comment);
547 }
548
549 size_t findMatchingParenInFile(StringRef fileData, size_t openPos) const {
550 if (openPos >= fileData.size() || fileData[openPos] != '(') {
551 return StringRef::npos;
552 }
553
554 int depth = 1;
555 size_t i = openPos + 1;
556 while (i < fileData.size()) {
557 char c = fileData[i];
558 if (c == '(') {
559 depth++;
560 i++;
561 } else if (c == ')') {
562 depth--;
563 if (depth == 0) {
564 return i;
565 }
566 i++;
567 } else if (c == '"') {
568 // Skip string literals to avoid counting parens inside strings
569 i++;
570 while (i < fileData.size() && fileData[i] != '"') {
571 if (fileData[i] == '\\' && i + 1 < fileData.size()) {
572 i++; // Skip escaped character
573 }
574 i++;
575 }
576 if (i < fileData.size()) {
577 i++; // Skip closing quote
578 }
579 } else if (c == '\'') {
580 // Skip character literals
581 i++;
582 while (i < fileData.size() && fileData[i] != '\'') {
583 if (fileData[i] == '\\' && i + 1 < fileData.size()) {
584 i++; // Skip escaped character
585 }
586 i++;
587 }
588 if (i < fileData.size()) {
589 i++; // Skip closing quote
590 }
591 } else {
592 i++;
593 }
594 }
595
596 return StringRef::npos;
597 }
598
599 ASTContext &context_;
600 Rewriter &rewriter_;
601 fs::path outputDir_;
602 fs::path inputRoot_;
603 FunctionTransformState currentFunction_;
604};
605
606class DeferASTConsumer : public ASTConsumer {
607public:
608 explicit DeferASTConsumer(DeferVisitor &visitor) : visitor_(visitor) {}
609
610 void HandleTranslationUnit(ASTContext &context) override {
611 visitor_.TraverseDecl(context.getTranslationUnitDecl());
612 }
613
614private:
615 DeferVisitor &visitor_;
616};
617
618class DeferFrontendAction : public ASTFrontendAction {
619public:
620 explicit DeferFrontendAction(const fs::path &outputDir, const fs::path &inputRoot)
621 : outputDir_(outputDir), inputRoot_(inputRoot) {
622 initializeProtectedDirectories();
623 }
624
625 void EndSourceFileAction() override {
626 SourceManager &sourceManager = rewriter_.getSourceMgr();
627 const FileEntry *fileEntry = sourceManager.getFileEntryForID(sourceManager.getMainFileID());
628 if (!fileEntry) {
629 return;
630 }
631
632 if (!visitor_) {
633 llvm::errs() << "Defer visitor not initialized; skipping file output\n";
634 hadWriteError_ = true;
635 return;
636 }
637
638 const StringRef filePathRef = fileEntry->tryGetRealPathName();
639 if (filePathRef.empty()) {
640 llvm::errs() << "Unable to resolve file path for transformed output\n";
641 return;
642 }
643
644 const fs::path originalPath = fs::path(filePathRef.str());
645 const std::string relativePath = visitor_->makeRelativePath(originalPath);
646 fs::path destinationPath = outputDir_ / relativePath;
647
648 // SAFETY CHECK: Never overwrite source files
649 std::error_code ec;
650 fs::path canonicalOriginal = fs::canonical(originalPath, ec);
651 if (!ec) {
652 fs::path canonicalDest = fs::weakly_canonical(destinationPath, ec);
653 if (!ec && canonicalOriginal == canonicalDest) {
654 llvm::errs() << "ERROR: Output path is the same as source file! Refusing to overwrite source.\n";
655 llvm::errs() << " Source: " << canonicalOriginal.string() << "\n";
656 llvm::errs() << " Output: " << canonicalDest.string() << "\n";
657 hadWriteError_ = true;
658 return;
659 }
660 }
661
662 // outputDir_ is already absolute (made absolute in main()), so destinationPath should be too
663
664 // Use generic_string() for forward slashes on all platforms
665 const std::string destinationString = destinationPath.generic_string();
666
667 if (!registerOutputPath(destinationString)) {
668 return;
669 }
670
671 // Check file existence using LLVM's exists() inline function
672 bool fileExists = llvm::sys::fs::exists(llvm::Twine(destinationString));
673
674 if (fileExists && isInProtectedSourceTree(destinationPath)) {
675 llvm::errs() << "Refusing to overwrite existing file in protected source tree: " << destinationString << "\n";
676 unregisterOutputPath(destinationString);
677 hadWriteError_ = true;
678 return;
679 }
680
681 const fs::path parent = destinationPath.parent_path();
682 std::error_code directoryError;
683 fs::create_directories(parent, directoryError);
684 if (directoryError) {
685 llvm::errs() << "Failed to create output directory: " << parent.string() << " - " << directoryError.message()
686 << "\n";
687 unregisterOutputPath(destinationString);
688 hadWriteError_ = true;
689 return;
690 }
691
692 std::string rewrittenContents;
693 if (const RewriteBuffer *buffer = rewriter_.getRewriteBufferFor(sourceManager.getMainFileID())) {
694 rewrittenContents.assign(buffer->begin(), buffer->end());
695
696 } else {
697 rewrittenContents = sourceManager.getBufferData(sourceManager.getMainFileID()).str();
698 }
699
700 std::error_code fileError;
701 llvm::raw_fd_ostream outputStream(destinationPath.string(), fileError, llvm::sys::fs::OF_Text);
702 if (fileError) {
703 llvm::errs() << "Failed to open output file: " << destinationString << " - " << fileError.message() << "\n";
704 unregisterOutputPath(destinationString);
705 hadWriteError_ = true;
706 return;
707 }
708
709 outputStream << rewrittenContents;
710 outputStream.close();
711 if (outputStream.has_error()) {
712 llvm::errs() << "Error while writing transformed file: " << destinationString << "\n";
713 unregisterOutputPath(destinationString);
714 hadWriteError_ = true;
715 }
716 }
717
718 bool hadWriteError() const {
719 return hadWriteError_;
720 }
721
722protected:
723 std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &compiler, StringRef) override {
724 rewriter_.setSourceMgr(compiler.getSourceManager(), compiler.getLangOpts());
725 visitor_ = std::make_unique<DeferVisitor>(compiler.getASTContext(), rewriter_, outputDir_, inputRoot_);
726 return std::make_unique<DeferASTConsumer>(*visitor_);
727 }
728
729private:
730 Rewriter rewriter_;
731 fs::path outputDir_;
732 fs::path inputRoot_;
733 fs::path inputRootCanonical_;
734 fs::path protectedSrcDir_;
735 fs::path protectedLibDir_;
736 std::unique_ptr<DeferVisitor> visitor_;
737 bool hadWriteError_ = false;
738
739 void initializeProtectedDirectories() {
740 std::error_code ec;
741 fs::path normalizedRoot = inputRoot_;
742
743 if (normalizedRoot.empty()) {
744 normalizedRoot = fs::current_path(ec);
745 ec.clear();
746 }
747
748 if (!normalizedRoot.is_absolute()) {
749 normalizedRoot = fs::absolute(normalizedRoot, ec);
750 ec.clear();
751 }
752
753 inputRootCanonical_ = fs::weakly_canonical(normalizedRoot, ec);
754 if (ec) {
755 inputRootCanonical_.clear();
756 return;
757 }
758
759 protectedSrcDir_ = inputRootCanonical_ / "src";
760 protectedLibDir_ = inputRootCanonical_ / "lib";
761 }
762
763 static bool pathStartsWith(const fs::path &path, const fs::path &prefix) {
764 if (prefix.empty()) {
765 return false;
766 }
767
768 auto pathIter = path.begin();
769 for (auto prefixIter = prefix.begin(); prefixIter != prefix.end(); ++prefixIter) {
770 if (pathIter == path.end() || *pathIter != *prefixIter) {
771 return false;
772 }
773 ++pathIter;
774 }
775
776 return true;
777 }
778
779 bool isInProtectedSourceTree(const fs::path &path) const {
780 if (inputRootCanonical_.empty()) {
781 return false;
782 }
783
784 std::error_code ec;
785 fs::path canonicalPath = fs::weakly_canonical(path, ec);
786 if (ec) {
787 return false;
788 }
789
790 return pathStartsWith(canonicalPath, protectedSrcDir_) || pathStartsWith(canonicalPath, protectedLibDir_);
791 }
792};
793
794class DeferActionFactory : public tooling::FrontendActionFactory {
795public:
796 DeferActionFactory(const fs::path &outputDir, const fs::path &inputRoot)
797 : outputDir_(outputDir), inputRoot_(inputRoot) {}
798
799 std::unique_ptr<FrontendAction> create() {
800 return std::make_unique<DeferFrontendAction>(outputDir_, inputRoot_);
801 }
802
803private:
804 fs::path outputDir_;
805 fs::path inputRoot_;
806};
807
808} // namespace
809
810// Store the original CWD before any tool changes it
811static fs::path g_originalCwd;
812
813int main(int argc, const char **argv) {
814 InitLLVM InitLLVM(argc, argv);
815
816 // Capture CWD before anything else can change it
817 g_originalCwd = fs::current_path();
818
819 // Hide all the LLVM internal options that aren't relevant to our tool
820 cl::HideUnrelatedOptions(ToolCategory);
821
822 cl::ParseCommandLineOptions(argc, argv, "ascii-defer transformation tool\n");
823
824 fs::path outputDir = fs::path(OutputDirectoryOption.getValue());
825 // Make output directory absolute relative to original CWD
826 if (!outputDir.is_absolute()) {
827 outputDir = g_originalCwd / outputDir;
828 }
829 fs::path inputRoot;
830 if (!InputRootOption.getValue().empty()) {
831 inputRoot = fs::path(InputRootOption.getValue());
832 } else {
833 inputRoot = fs::current_path();
834 }
835
836 // Make input root absolute for reliable path computation
837 if (!inputRoot.is_absolute()) {
838 std::error_code ec;
839 inputRoot = fs::absolute(inputRoot, ec);
840 if (ec) {
841 llvm::errs() << "Failed to resolve input root path: " << ec.message() << "\n";
842 return 1;
843 }
844 }
845
846 std::vector<std::string> sourcePaths;
847 for (const auto &path : SourcePaths) {
848 if (!path.empty()) {
849 sourcePaths.push_back(path);
850 }
851 }
852
853 if (sourcePaths.empty()) {
854 llvm::errs() << "No translation units specified for transformation. Provide positional source paths.\n";
855 return 1;
856 }
857
858 if (fs::exists(outputDir)) {
859 if (!fs::is_directory(outputDir)) {
860 llvm::errs() << "Output path exists and is not a directory: " << outputDir.c_str() << "\n";
861 return 1;
862 }
863 } else {
864 std::error_code errorCode;
865 fs::create_directories(outputDir, errorCode);
866 if (errorCode) {
867 llvm::errs() << "Failed to create output directory: " << outputDir.c_str() << " - " << errorCode.message()
868 << "\n";
869 return 1;
870 }
871 }
872
873 // Load compilation database
874 std::string buildPath = BuildPath.getValue();
875 if (buildPath.empty()) {
876 buildPath = ".";
877 }
878 std::string errorMessage;
879 std::unique_ptr<tooling::CompilationDatabase> compilations =
880 tooling::CompilationDatabase::loadFromDirectory(buildPath, errorMessage);
881 if (!compilations) {
882 llvm::errs() << "Error loading compilation database from '" << buildPath << "': " << errorMessage << "\n";
883 return 1;
884 }
885
886 tooling::ClangTool tool(*compilations, sourcePaths);
887
888 // Build the list of arguments to prepend for system header resolution
889 // LibTooling uses CC1 mode internally which has different include path handling than
890 // the clang driver. We use -Xclang to pass CC1-level flags that properly configure
891 // system include paths for LibTooling's CompilerInvocation.
892 std::vector<std::string> prependArgs;
893
894 // Try to find clang resource directory at runtime
895 // Priority: 1) CLANG_RESOURCE_DIR compile-time path, 2) Runtime detection via common paths
896 std::string resourceDir;
897
898#ifdef CLANG_RESOURCE_DIR
899 if (llvm::sys::fs::exists(CLANG_RESOURCE_DIR)) {
900 resourceDir = CLANG_RESOURCE_DIR;
901 // llvm::errs() << "Using embedded clang resource directory: " << resourceDir << "\n";
902 } else {
903 if (!QuietMode) {
904 llvm::errs() << "Embedded clang resource directory not found: " << CLANG_RESOURCE_DIR << "\n";
905 }
906 }
907#endif
908
909 // Runtime detection if embedded path doesn't work
910 if (resourceDir.empty()) {
911 // Common locations for clang resource directories
912 std::vector<std::string> searchPaths;
913
914#ifdef __APPLE__
915 // Homebrew LLVM on Apple Silicon
916 searchPaths.push_back("/opt/homebrew/opt/llvm/lib/clang");
917 // Homebrew LLVM on Intel Mac
918 searchPaths.push_back("/usr/local/opt/llvm/lib/clang");
919 // Xcode's clang
920 searchPaths.push_back(
921 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang");
922 // CommandLineTools clang
923 searchPaths.push_back("/Library/Developer/CommandLineTools/usr/lib/clang");
924#endif
925#ifdef __linux__
926 // System LLVM installations
927 searchPaths.push_back("/usr/lib/llvm-22/lib/clang");
928 searchPaths.push_back("/usr/lib/llvm-21/lib/clang");
929 searchPaths.push_back("/usr/lib/llvm-20/lib/clang");
930 searchPaths.push_back("/usr/lib/clang");
931 searchPaths.push_back("/usr/local/lib/clang");
932#endif
933 // Universal fallback
934 searchPaths.push_back("/usr/local/lib/clang");
935
936 for (const auto &basePath : searchPaths) {
937 if (!llvm::sys::fs::exists(basePath)) {
938 continue;
939 }
940
941 // Find the highest version subdirectory
942 std::error_code ec;
943 std::string bestVersion;
944 int bestMajor = 0;
945
946 for (llvm::sys::fs::directory_iterator dir(basePath, ec), dirEnd; !ec && dir != dirEnd; dir.increment(ec)) {
947 std::string name = llvm::sys::path::filename(dir->path()).str();
948 // Parse version number (e.g., "22", "21.1.0", "21")
949 int major = 0;
950 if (sscanf(name.c_str(), "%d", &major) == 1 && major > bestMajor) {
951 bestMajor = major;
952 bestVersion = dir->path();
953 }
954 }
955
956 if (!bestVersion.empty()) {
957 resourceDir = bestVersion;
958 if (!QuietMode) {
959 llvm::errs() << "Found clang resource directory at runtime: " << resourceDir << "\n";
960 }
961 break;
962 }
963 }
964
965 if (resourceDir.empty() && !QuietMode) {
966 llvm::errs() << "Warning: Could not find clang resource directory\n";
967 }
968 }
969
970 if (!resourceDir.empty()) {
971 prependArgs.push_back(std::string("-resource-dir=") + resourceDir);
972 }
973
974 // Add target triple - LibTooling needs this to validate architecture-specific flags
975 // Without a target, flags like -mavx2 cause "unsupported option for target ''" errors
976#ifdef __APPLE__
977#ifdef __arm64__
978 prependArgs.push_back("-target");
979 prependArgs.push_back("arm64-apple-darwin");
980 // llvm::errs() << "Using target: arm64-apple-darwin\n";
981#else
982 prependArgs.push_back("-target");
983 prependArgs.push_back("x86_64-apple-darwin");
984 // llvm::errs() << "Using target: x86_64-apple-darwin\n";
985#endif
986#elif defined(__linux__)
987#ifdef __aarch64__
988 prependArgs.push_back("-target");
989 prependArgs.push_back("aarch64-linux-gnu");
990 // llvm::errs() << "Using target: aarch64-linux-gnu\n";
991#else
992 prependArgs.push_back("-target");
993 prependArgs.push_back("x86_64-linux-gnu");
994 // llvm::errs() << "Using target: x86_64-linux-gnu\n";
995#endif
996#endif
997
998 // Override the sysroot for macOS. Homebrew's LLVM config file sets -isysroot
999 // to CommandLineTools SDK, but we strip that from compile_commands.json and
1000 // set our own explicitly to ensure consistent behavior.
1001 std::string selectedSDK;
1002#ifdef __APPLE__
1003 {
1004 const char *sdkPaths[] = {
1005 // Xcode SDK (preferred - most complete headers and frameworks)
1006 "/Applications/Xcode.app/Contents/Developer/Platforms/"
1007 "MacOSX.platform/Developer/SDKs/MacOSX.sdk",
1008 // CommandLineTools SDK (fallback for users without Xcode)
1009 "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk",
1010 };
1011
1012 for (const char *sdk : sdkPaths) {
1013 if (llvm::sys::fs::exists(sdk)) {
1014 selectedSDK = sdk;
1015 break;
1016 }
1017 }
1018
1019 if (!selectedSDK.empty()) {
1020 prependArgs.push_back("-isysroot");
1021 prependArgs.push_back(selectedSDK);
1022 // llvm::errs() << "Using macOS SDK: " << selectedSDK << "\n";
1023 } else {
1024 llvm::errs() << "Warning: No macOS SDK found, system headers may not be available\n";
1025 }
1026 }
1027#endif
1028
1029 // Build list of system include paths to add as -isystem paths.
1030 // LibTooling (cc1 mode) doesn't automatically add system include paths like
1031 // the clang driver does, so we add them explicitly:
1032 // 1. Clang's builtin headers (stdbool.h, stddef.h, etc.) - FIRST so they shadow SDK builtins
1033 // 2. SDK's usr/include (stdio.h, stdlib.h, etc.) - for system headers
1034 std::vector<std::string> appendArgs;
1035 if (!resourceDir.empty()) {
1036 std::string builtinInclude = resourceDir + "/include";
1037 if (llvm::sys::fs::exists(builtinInclude)) {
1038 appendArgs.push_back("-isystem");
1039 appendArgs.push_back(builtinInclude);
1040 // llvm::errs() << "Added clang builtin -isystem: " << builtinInclude << "\n";
1041 }
1042 }
1043 // NOTE: We intentionally do NOT add SDK's usr/include to the -isystem path.
1044 // LibTooling on LLVM 21 has a bug where __has_include_next() evaluates even
1045 // in non-taken preprocessor branches and creates VFS entries that can't be opened.
1046 // The clang builtin stdbool.h has:
1047 // #if defined(__MVS__) && __has_include_next(<stdbool.h>)
1048 // Even though __MVS__ is not defined on macOS, LLVM 21's LibTooling still tries
1049 // to resolve __has_include_next(<stdbool.h>), looks in SDK/usr/include, and fails
1050 // because stdbool.h doesn't exist there (it's a builtin header).
1051 //
1052 // By not adding SDK/usr/include, the __has_include_next won't find any entry
1053 // and will return false without trying to open a non-existent file.
1054 // The -isysroot flag still provides access to SDK headers through framework paths.
1055
1056 // Single consolidated argument adjuster that:
1057 // 1. Preserves compiler path (first arg)
1058 // 2. Inserts prepend args immediately after compiler (-nostdlibinc, -resource-dir, -isysroot)
1059 // 3. Strips unnecessary flags from remaining args
1060 // 4. Adds system include paths as -isystem at the end (after project -I paths)
1061 // 5. Adds defer tool parsing define BEFORE the "--" separator
1062 std::string inputRootStr = inputRoot.string();
1063 auto consolidatedAdjuster = [prependArgs, appendArgs, inputRootStr](const tooling::CommandLineArguments &args,
1064 StringRef) {
1065 // Helper to check if a path is under the project root
1066 auto isProjectPath = [&inputRootStr](const std::string &path) -> bool {
1067 if (inputRootStr.empty())
1068 return false;
1069 // Normalize both paths for comparison
1070 std::error_code ec;
1071 auto normalizedPath = fs::canonical(path, ec);
1072 if (ec)
1073 return false; // Path doesn't exist or can't be resolved
1074 auto normalizedRoot = fs::canonical(inputRootStr, ec);
1075 if (ec)
1076 return false;
1077 // Check if the path starts with the root
1078 std::string pathStr = normalizedPath.string();
1079 std::string rootStr = normalizedRoot.string();
1080 if (pathStr.find(rootStr) != 0)
1081 return false;
1082 // Exclude .deps-cache directory - these are cached dependencies that use
1083 // angled includes (<header.h>) and need -isystem, not -iquote
1084 if (pathStr.find("/.deps-cache/") != std::string::npos)
1085 return false;
1086 return true;
1087 };
1088 tooling::CommandLineArguments result;
1089
1090 if (args.empty()) {
1091 return result;
1092 }
1093
1094 // First: preserve the compiler path (first argument)
1095 result.push_back(args[0]);
1096
1097 // Second: add the prepend args right after the compiler
1098 for (const auto &arg : prependArgs) {
1099 result.push_back(arg);
1100 }
1101
1102 // Third: process remaining arguments, stripping unnecessary flags
1103 // IMPORTANT: Convert -I to -iquote for project include paths.
1104 // Clang include search order for <header.h>: -I paths -> -isystem paths -> system paths
1105 // Clang include search order for "header.h": -iquote paths -> -I paths -> -isystem paths -> system paths
1106 //
1107 // The problem: Project -I paths are searched BEFORE -isystem paths for <stdio.h>.
1108 // This causes LibTooling to look for <stdio.h> in the project's lib/ directory first.
1109 //
1110 // The fix: Convert project -I to -iquote. Then <stdio.h> skips project paths entirely
1111 // and finds system headers in -isystem paths instead.
1112 //
1113 // ALSO: Collect all -isystem paths and reorder them so clang builtins come FIRST.
1114 // LibTooling on LLVM 21 has a VFS bug where it creates phantom entries for headers
1115 // in -isystem directories even when the header doesn't exist there.
1116 std::vector<std::string> collectedIsystemPaths;
1117 bool foundSeparator = false;
1118 size_t separatorIndex = 0;
1119 for (size_t i = 1; i < args.size(); ++i) {
1120 const std::string &arg = args[i];
1121
1122 // When we hit "--", note its position and break
1123 if (arg == "--") {
1124 foundSeparator = true;
1125 separatorIndex = i;
1126 break;
1127 }
1128
1129 // Skip sanitizer flags
1130 if (arg.find("-fsanitize") != std::string::npos)
1131 continue;
1132 if (arg.find("-fno-sanitize") != std::string::npos)
1133 continue;
1134 // Skip debug info flags (not needed for AST parsing)
1135 if (arg == "-g" || arg == "-g2" || arg == "-g3")
1136 continue;
1137 if (arg == "-fno-eliminate-unused-debug-types")
1138 continue;
1139 if (arg == "-fno-inline")
1140 continue;
1141 // Strip -resource-dir flags and their arguments - we added our embedded path
1142 if (arg == "-resource-dir") {
1143 ++i;
1144 continue;
1145 }
1146 if (arg.find("-resource-dir=") == 0)
1147 continue;
1148 // Strip -isysroot flags and their arguments - we added our embedded SDK path
1149 if (arg == "-isysroot") {
1150 ++i;
1151 continue;
1152 }
1153 if (arg.find("-isysroot=") == 0 || (arg.find("-isysroot") == 0 && arg.length() > 9))
1154 continue;
1155
1156 // Collect -isystem paths instead of passing them through
1157 // We'll add them at the end in the correct order (clang builtins first)
1158 if (arg == "-isystem" && i + 1 < args.size()) {
1159 collectedIsystemPaths.push_back(args[++i]);
1160 continue;
1161 }
1162 if (arg.find("-isystem") == 0 && arg.length() > 8) {
1163 collectedIsystemPaths.push_back(arg.substr(8));
1164 continue;
1165 }
1166
1167 // Convert -I to -iquote for project include paths
1168 // Convert -I to -isystem for dependency paths (so they come after our clang builtins)
1169 // This prevents <stdbool.h> from being searched in dependency directories
1170 // before our clang builtin path
1171 // EXCEPTION: Keep -I for the public "include/" directory since it contains
1172 // public API headers meant to be included with angle brackets like <ascii-chat/header.h>
1173 if (arg == "-I" && i + 1 < args.size()) {
1174 // -I /path/to/dir (separate argument)
1175 const std::string &includePath = args[++i];
1176 if (isProjectPath(includePath) && includePath.find("/include") == std::string::npos) {
1177 // Project path but NOT the public include directory - convert to -iquote
1178 result.push_back("-iquote");
1179 result.push_back(includePath);
1180 } else if (!isProjectPath(includePath)) {
1181 // Collect dependency -I paths to add as -isystem after our builtins
1182 collectedIsystemPaths.push_back(includePath);
1183 } else {
1184 // Public include directory - keep as -I for angle-bracket includes
1185 result.push_back("-I");
1186 result.push_back(includePath);
1187 }
1188 continue;
1189 }
1190 if (arg.find("-I") == 0 && arg.length() > 2) {
1191 // -I/path/to/dir (combined form)
1192 std::string includePath = arg.substr(2);
1193 if (isProjectPath(includePath) && includePath.find("/include") == std::string::npos) {
1194 // Project path but NOT the public include directory - convert to -iquote
1195 result.push_back("-iquote");
1196 result.push_back(includePath);
1197 } else if (!isProjectPath(includePath)) {
1198 // Collect dependency -I paths to add as -isystem after our builtins
1199 collectedIsystemPaths.push_back(includePath);
1200 } else {
1201 // Public include directory - keep as -I for angle-bracket includes
1202 result.push_back("-I");
1203 result.push_back(includePath);
1204 }
1205 continue;
1206 }
1207
1208 result.push_back(arg);
1209 }
1210
1211 // Fourth: add system include paths in the correct order
1212 // Order matters for LibTooling on LLVM 21 - clang builtins MUST come first
1213 // to shadow any phantom VFS entries that might be created for other paths
1214 for (const auto &arg : appendArgs) {
1215 result.push_back(arg);
1216 }
1217 // Then add the collected -isystem paths from the compilation database
1218 for (const auto &path : collectedIsystemPaths) {
1219 result.push_back("-isystem");
1220 result.push_back(path);
1221 }
1222
1223 // Fifth: add the defer tool define and separator
1224 result.push_back("-DASCIICHAT_DEFER_TOOL_PARSING");
1225 if (foundSeparator) {
1226 result.push_back("--");
1227 // Copy any remaining args after "--"
1228 for (size_t i = separatorIndex + 1; i < args.size(); ++i) {
1229 result.push_back(args[i]);
1230 }
1231 }
1232
1233 return result;
1234 };
1235 tool.appendArgumentsAdjuster(consolidatedAdjuster);
1236
1237 DeferActionFactory actionFactory(outputDir, inputRoot);
1238 const int executionResult = tool.run(&actionFactory);
1239 if (executionResult != 0) {
1240 llvm::errs() << "Defer transformation failed with code " << executionResult << "\n";
1241 }
1242 return executionResult;
1243}
void unregisterOutputPath(const std::string &path)
std::mutex & outputRegistryMutex()
int main(int argc, const char **argv)
std::unordered_set< std::string > & outputRegistry()
bool registerOutputPath(const std::string &path)
action_args_t args