Files
clice/src/compile/compilation_unit.cpp
ykiko e1202d2fa5 fix: prevent worker crashes from null ASTConsumer, invalid FileID, and missing PCH cache dir (#435)
## Summary

Three pre-existing bugs cause worker processes to crash with SEGV or
SIGABRT. On the main branch these crashes are silent (workers die,
requests fail fast with "transport closed", tests still pass because
null responses are accepted). However when combined with #432's worker
respawn mechanism, the crash-respawn-crash cycle on low-core CI machines
causes request timeouts and smoke test hangs.

### Fixes

- **compilation.cpp**: `ProxyAction::CreateASTConsumer` now checks for
null before passing to `MultiplexConsumer`. When the wrapped action's
`CreateASTConsumer` fails (e.g. missing system headers during PCH
generation), this previously caused a null pointer dereference, SEGV,
ASAN kills the stateless worker.
- **compilation_unit.cpp**: `file_path()` returns empty `StringRef` on
invalid `FileID` instead of asserting. The assert fired when
`IncludeGraph::from()` called `file_path(interested_file())` on an AST
compiled with synthesized default commands (no compile_commands.json,
clang++ -std=c++20 fallback, no system headers, invalid main file ID),
SIGABRT, stateful worker crash.
- **compiler.cpp**: `ensure_pch` now creates the PCH cache directory
before sending the build request. Previously, when `load_workspace()`
exited early (no compile_commands.json), the cache subdirectories were
never created, causing every PCH write to fail with "No such file or
directory".
- **master_server.cpp/h**: `load_workspace()` changed from
`kota::task<>` to plain `void` -- it contains only synchronous
filesystem operations and no co_await, so the coroutine wrapper was
unnecessary. Called directly instead of via `loop.schedule()`.

## Test plan

- [x] Verified zero SEGV/SIGABRT/assertion crashes in worker stderr
after fix
- [x] rapid_edit.jsonl smoke test passes 3/3 runs consistently (34s
each)
- [x] Behavior matches main branch (both return 134 responses, 0
pending)
- [x] Debug build with ASAN (detect_leaks=0) -- clean run, no sanitizer
reports

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/clice-io/codesmith/clice/pr/435"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-light.svg"><img
alt="View in Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-in-codesmith-dark.svg"></picture></a>
<sup>Codesmith can help with this PR — just tag <code>@codesmith</code>
or enable autofix.</sup>

- [ ] Autofix CI and bot reviews
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved error handling for AST consumer creation with null checks and
a clear failure path.
* Safer file-path access that returns empty for invalid identifiers
instead of asserting.
* PCH cache handling now validates cache configuration, attempts
directory creation, logs warnings, and aborts PCH builds on failure.

* **Refactor**
* Workspace loading changed from asynchronous to synchronous execution.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-23 10:36:03 +08:00

344 lines
10 KiB
C++

#include "compile/implement.h"
#include "index/usr.h"
#include "semantic/ast_utility.h"
namespace clice {
CompilationKind CompilationUnitRef::kind() {
return self->kind;
}
CompilationStatus CompilationUnitRef::status() {
return self->status;
}
auto CompilationUnitRef::file_id(clang::FileEntryRef entry) -> clang::FileID {
return self->SM().translateFile(entry);
}
auto CompilationUnitRef::file_id(llvm::StringRef file) -> clang::FileID {
auto entry = self->SM().getFileManager().getFileRef(file);
if(entry) {
return file_id(*entry);
}
return clang::FileID();
}
auto CompilationUnitRef::decompose_location(clang::SourceLocation location)
-> std::pair<clang::FileID, std::uint32_t> {
assert(location.isFileID() && "Decompose macro location is meaningless!");
return self->SM().getDecomposedLoc(location);
}
auto CompilationUnitRef::decompose_range(clang::SourceRange range)
-> std::pair<clang::FileID, LocalSourceRange> {
auto [begin, end] = range;
assert(begin.isValid() && end.isValid() && "Invalid source range");
assert(begin.isFileID() && end.isValid() && "Input source range should be a file range");
if(begin == end) {
auto [fid, offset] = decompose_location(begin);
return {
fid,
{offset, offset + token_length(end)}
};
} else {
auto [begin_fid, begin_offset] = decompose_location(begin);
auto [end_fid, end_offset] = decompose_location(end);
if(begin_fid == end_fid) {
end_offset += token_length(end);
} else {
auto content = file_content(begin_fid);
end_offset = content.size();
}
return {
begin_fid,
{begin_offset, end_offset}
};
}
}
auto CompilationUnitRef::decompose_expansion_range(clang::SourceRange range)
-> std::pair<clang::FileID, LocalSourceRange> {
auto [begin, end] = range;
if(begin == end) {
return decompose_range(expansion_location(begin));
} else {
return decompose_range(
clang::SourceRange(expansion_location(begin), expansion_location(end)));
}
}
auto CompilationUnitRef::file_id(clang::SourceLocation location) -> clang::FileID {
return self->SM().getFileID(location);
}
auto CompilationUnitRef::file_offset(clang::SourceLocation location) -> std::uint32_t {
return self->SM().getFileOffset(location);
}
auto CompilationUnitRef::file_path(clang::FileID fid) -> llvm::StringRef {
if(!fid.isValid())
return {};
if(auto it = self->path_cache.find(fid); it != self->path_cache.end()) {
return it->second;
}
auto entry = self->SM().getFileEntryRefForID(fid);
if(!entry) {
return {};
}
llvm::SmallString<128> path;
/// Try to get the real path of the file.
auto name = entry->getName();
if(auto error = llvm::sys::fs::real_path(name, path)) {
/// If failed, use the virtual path.
path = name;
}
assert(!path.empty() && "Invalid file path");
/// Allocate the path in the storage.
auto size = path.size();
auto data = self->path_storage.Allocate<char>(size + 1);
memcpy(data, path.data(), size);
data[size] = '\0';
auto [it, inserted] = self->path_cache.try_emplace(fid, llvm::StringRef(data, size));
assert(inserted && "File path already exists");
return it->second;
}
auto CompilationUnitRef::file_content(clang::FileID fid) -> llvm::StringRef {
return self->SM().getBufferData(fid);
}
auto CompilationUnitRef::interested_file() -> clang::FileID {
return self->SM().getMainFileID();
}
auto CompilationUnitRef::interested_content() -> llvm::StringRef {
return file_content(interested_file());
}
bool CompilationUnitRef::is_builtin_file(clang::FileID fid) {
// No FileEntryRef => built-in/command line/scratch.
if(!self->SM().getFileEntryRefForID(fid)) {
if(auto buffer = self->SM().getBufferOrNone(fid)) {
auto name = buffer->getBufferIdentifier();
return name == "<built-in>" || name == "<command line>" || name == "<scratch space>";
}
}
return false;
}
auto CompilationUnitRef::start_location(clang::FileID fid) -> clang::SourceLocation {
return self->SM().getLocForStartOfFile(fid);
}
auto CompilationUnitRef::end_location(clang::FileID fid) -> clang::SourceLocation {
return self->SM().getLocForEndOfFile(fid);
}
auto CompilationUnitRef::spelling_location(clang::SourceLocation loc) -> clang::SourceLocation {
return self->SM().getSpellingLoc(loc);
}
auto CompilationUnitRef::expansion_location(clang::SourceLocation location)
-> clang::SourceLocation {
return self->SM().getExpansionLoc(location);
}
auto CompilationUnitRef::file_location(clang::SourceLocation location) -> clang::SourceLocation {
return self->SM().getFileLoc(location);
}
auto CompilationUnitRef::include_location(clang::FileID fid) -> clang::SourceLocation {
return self->SM().getIncludeLoc(fid);
}
auto CompilationUnitRef::presumed_location(clang::SourceLocation location) -> clang::PresumedLoc {
return self->SM().getPresumedLoc(location, false);
}
auto CompilationUnitRef::create_location(clang::FileID fid, std::uint32_t offset)
-> clang::SourceLocation {
return self->SM().getComposedLoc(fid, offset);
}
auto CompilationUnitRef::spelled_tokens(clang::FileID fid) -> TokenRange {
return self->buffer->spelledTokens(fid);
}
auto CompilationUnitRef::spelled_tokens(clang::SourceRange range) -> TokenRange {
auto tokens = self->buffer->spelledForExpanded(self->buffer->expandedTokens(range));
if(!tokens) {
return {};
}
return *tokens;
}
auto CompilationUnitRef::spelled_tokens_touch(clang::SourceLocation location) -> TokenRange {
return clang::syntax::spelledTokensTouching(location, *self->buffer);
}
auto CompilationUnitRef::expanded_tokens() -> TokenRange {
return self->buffer->expandedTokens();
}
auto CompilationUnitRef::expanded_tokens(clang::SourceRange range) -> TokenRange {
return self->buffer->expandedTokens(range);
}
auto CompilationUnitRef::expansions_overlapping(TokenRange spelled_tokens)
-> std::vector<clang::syntax::TokenBuffer::Expansion> {
return self->buffer->expansionsOverlapping(spelled_tokens);
}
auto CompilationUnitRef::token_length(clang::SourceLocation location) -> std::uint32_t {
return clang::Lexer::MeasureTokenLength(location, self->SM(), self->instance->getLangOpts());
}
auto CompilationUnitRef::token_spelling(clang::SourceLocation location) -> llvm::StringRef {
return llvm::StringRef(self->SM().getCharacterData(location), token_length(location));
}
auto CompilationUnitRef::module_name() -> llvm::StringRef {
return self->instance->getPreprocessor().getNamedModuleName();
}
bool CompilationUnitRef::is_module_interface_unit() {
return self->instance->getPreprocessor().isInNamedInterfaceUnit();
}
auto CompilationUnitRef::diagnostics() -> std::vector<Diagnostic>& {
return self->diagnostics;
}
auto CompilationUnitRef::top_level_decls() -> llvm::ArrayRef<clang::Decl*> {
return self->top_level_decls;
}
std::chrono::milliseconds CompilationUnitRef::build_at() {
return self->build_at;
}
std::chrono::milliseconds CompilationUnitRef::build_duration() {
return self->build_duration;
}
clang::LangOptions& CompilationUnitRef::lang_options() {
return self->instance->getLangOpts();
}
std::vector<std::string> CompilationUnitRef::deps() {
llvm::StringSet<> deps;
/// FIXME: consider `#embed` and `__has_embed`.
for(auto& [fid, directive]: directives()) {
for(auto& include: directive.includes) {
if(!include.skipped) {
auto path = file_path(include.fid);
if(!path.empty()) {
deps.try_emplace(path);
}
}
}
for(auto& has_include: directive.has_includes) {
if(has_include.fid.isValid()) {
auto path = file_path(has_include.fid);
if(!path.empty()) {
deps.try_emplace(path);
}
}
}
}
std::vector<std::string> result;
for(auto& deps: deps) {
result.emplace_back(deps.getKey().str());
}
return result;
}
index::SymbolID CompilationUnitRef::getSymbolID(const clang::NamedDecl* decl) {
uint64_t hash;
auto iter = self->symbol_hash_cache.find(decl);
if(iter != self->symbol_hash_cache.end()) {
hash = iter->second;
} else {
llvm::SmallString<128> usr;
index::generateUSRForDecl(decl, usr);
hash = llvm::xxh3_64bits(usr);
self->symbol_hash_cache.try_emplace(decl, hash);
}
return index::SymbolID{hash, ast::name_of(decl)};
}
index::SymbolID CompilationUnitRef::getSymbolID(const clang::MacroInfo* macro) {
std::uint64_t hash;
auto name = token_spelling(macro->getDefinitionLoc());
auto iter = self->symbol_hash_cache.find(macro);
if(iter != self->symbol_hash_cache.end()) {
hash = iter->second;
} else {
llvm::SmallString<128> usr;
index::generateUSRForMacro(name, macro->getDefinitionLoc(), self->SM(), usr);
hash = llvm::xxh3_64bits(usr);
self->symbol_hash_cache.try_emplace(macro, hash);
}
return index::SymbolID{hash, name.str()};
}
const llvm::DenseSet<clang::FileID>& CompilationUnitRef::files() {
if(self->all_files.empty()) {
/// FIXME: handle preamble and embed file id.
for(auto& [fid, directive]: directives()) {
for(auto& include: directive.includes) {
if(!include.skipped && include.fid.isValid()) {
self->all_files.insert(include.fid);
}
}
}
self->all_files.insert(self->SM().getMainFileID());
}
return self->all_files;
}
clang::TranslationUnitDecl* CompilationUnitRef::tu() {
return self->instance->getASTContext().getTranslationUnitDecl();
}
llvm::DenseMap<clang::FileID, Directive>& CompilationUnitRef::directives() {
return self->directives;
}
TemplateResolver& CompilationUnitRef::resolver() {
assert(self->resolver && "Template resolver is not available");
return *self->resolver;
}
clang::ASTContext& CompilationUnitRef::context() {
return self->instance->getASTContext();
}
clang::syntax::TokenBuffer& CompilationUnitRef::token_buffer() {
return *self->buffer;
}
CompilationUnit::~CompilationUnit() {
delete self;
}
} // namespace clice