Files
clice/src/compile/compilation_unit.cpp
ykiko e239b0d32c feat: smart PCH rebuild, #include/import completion, rapid-edit robustness (#394)
## Summary

### Preamble completeness check
- `is_preamble_complete()` in `scan.cpp`: checks whether
`#include`/`import`/`export module` directives in the preamble region
are syntactically complete (have closing `>`/`"`/`;`)
- `ensure_pch` defers PCH rebuild when preamble is incomplete (user
still typing), reuses old PCH instead of failing

### #include / import completion
- Master intercepts completion requests in `#include "..."` / `#include
<...>` / `import ...` contexts before forwarding to worker
- `complete_include()`: searches include paths (from compile args via
`SearchConfig`) using `DirListingCache`, supports
quoted/angled/multi-level paths
- `complete_import()`: filters `path_to_module` map by prefix
- Word boundary checks prevent false matches (e.g. `important` not
treated as `import`)

### Detached compile task (rapid-edit fix)
- Compile operations (`ensure_deps` + `send_stateful` +
`publish_diagnostics`) run as detached tasks via `loop.schedule()`,
independent of the LSP request coroutine chain
- LSP `$/cancelRequest` can no longer kill in-flight compilations —
previously, cancellation would destroy the `ensure_compiled` coroutine
frame, leaving `doc.compiling` permanently set and hanging all
subsequent requests
- `CompileGuard` RAII ensures `doc.compiling` is always cleaned up even
if the detached task fails
- Stale feature requests (where `ast_dirty` became true after compile
finished) are dropped before forwarding to worker

### Other fixes
- `signal(SIGPIPE, SIG_IGN)` on POSIX: prevents server crash when LSP
client disconnects mid-write
- `CompilationUnitRef::file_path()` / `deps()`: null-check
`FileEntryRef` to prevent segfault on invalid FileID
- `stateless_worker.cpp`: log BuildPCH diagnostic errors for
debuggability
- Default worker counts changed to 2 stateful + 3 stateless
- `logging_dir` default changed to `.clice/logs` in config

### Tests
- 19 unit tests for `is_preamble_complete` (incomplete `#include`,
`import`, `export module`, mixed cases)
- Integration tests: `test_include_completion.py` (5 tests),
`test_import_completion.py` (4 tests), `test_rapid_edit.py` (2 tests),
`test_pch.py` (4 new tests)
- Smoke test: `rapid_edit.jsonl` — recorded VSCode session with 40 rapid
edits + 61 cancel requests

## Test plan
- [x] Unit tests: 463 passed
- [x] Integration tests: 104 passed
- [x] Smoke test (rapid_edit.jsonl): PASS
- [x] Manual VSCode testing with `#include <iostream>` project

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 14:49:09 +08:00

343 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 {
assert(fid.isValid() && "Invalid fid");
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