## 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>
343 lines
10 KiB
C++
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
|