Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 197 additions & 7 deletions src/server/master_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,9 @@ et::task<> MasterServer::load_workspace() {

auto report = scan_dependency_graph(cdb, path_pool, dependency_graph);

// Build reverse include map so headers can find their host source files.
dependency_graph.build_reverse_map();

auto unresolved = report.includes_found - report.includes_resolved;
double accuracy =
report.includes_found > 0
Expand Down Expand Up @@ -562,20 +565,191 @@ et::task<> MasterServer::load_workspace() {
LOG_INFO("CompileGraph initialized with {} module(s)", path_to_module.size());
}

std::optional<HeaderFileContext>
MasterServer::resolve_header_context(std::uint32_t header_path_id) {
// Find source files that transitively include this header.
auto hosts = dependency_graph.find_host_sources(header_path_id);
if(hosts.empty()) {
LOG_DEBUG("resolve_header_context: no host sources for path_id={}", header_path_id);
return std::nullopt;
}

// Pick the first available host that has a CDB entry.
std::uint32_t host_path_id = 0;
std::vector<std::uint32_t> chain;
for(auto candidate: hosts) {
auto candidate_path = path_pool.resolve(candidate);
auto results = cdb.lookup(candidate_path, {.suppress_logging = true});
if(results.empty())
continue;
auto c = dependency_graph.find_include_chain(candidate, header_path_id);
if(c.empty())
continue;
host_path_id = candidate;
chain = std::move(c);
break;
Comment thread
16bit-ykiko marked this conversation as resolved.
Outdated
}

if(chain.empty()) {
LOG_DEBUG("resolve_header_context: no usable host with include chain for path_id={}",
header_path_id);
return std::nullopt;
}

// Build preamble text: for each file in the chain except the last (target),
// append all content up to (but not including) the line that includes the
// next file in the chain.
std::string preamble;
for(std::size_t i = 0; i + 1 < chain.size(); ++i) {
auto cur_id = chain[i];
auto next_id = chain[i + 1];

auto cur_path = path_pool.resolve(cur_id);
auto next_path = path_pool.resolve(next_id);
auto next_filename = llvm::sys::path::filename(next_path);

// Prefer in-memory document text over disk content.
std::string content;
if(auto doc_it = documents.find(cur_id); doc_it != documents.end()) {
content = doc_it->second.text;
} else {
auto buf = llvm::MemoryBuffer::getFile(cur_path);
if(!buf) {
LOG_WARN("resolve_header_context: cannot read {}", cur_path);
return std::nullopt;
}
content = (*buf)->getBuffer().str();
}

// Scan line by line for the #include that brings in next_filename.
llvm::StringRef content_ref(content);
std::size_t line_start = 0;
std::size_t include_line_start = std::string::npos;
while(line_start <= content_ref.size()) {
auto newline_pos = content_ref.find('\n', line_start);
auto line_end =
(newline_pos == llvm::StringRef::npos) ? content_ref.size() : newline_pos;
auto line = content_ref.slice(line_start, line_end).trim();

if(line.starts_with("#include") || line.starts_with("# include")) {
// Check if this line references the next file in the chain.
if(line.contains(next_filename)) {
include_line_start = line_start;
break;
}
}

line_start =
(newline_pos == llvm::StringRef::npos) ? content_ref.size() + 1 : newline_pos + 1;
}

// Emit a #line marker then all content before the include line.
preamble += std::format("#line 1 \"{}\"\n", cur_path.str());
if(include_line_start != std::string::npos) {
preamble += content_ref.substr(0, include_line_start).str();
} else {
// No matching include line found — emit the whole file to be safe.
LOG_DEBUG("resolve_header_context: include line for {} not found in {}, emitting full",
next_filename,
cur_path);
preamble += content;
}
Comment thread
16bit-ykiko marked this conversation as resolved.
}

// Hash the preamble and write to cache directory.
auto preamble_hash = llvm::xxh3_64bits(llvm::StringRef(preamble));
auto preamble_filename = std::format("{:016x}.h", preamble_hash);
auto preamble_dir = path::join(config.cache_dir, "header_context");
auto preamble_path = path::join(preamble_dir, preamble_filename);

if(!llvm::sys::fs::exists(preamble_path)) {
auto ec = llvm::sys::fs::create_directories(preamble_dir);
if(ec) {
LOG_WARN("resolve_header_context: cannot create dir {}: {}",
preamble_dir,
ec.message());
return std::nullopt;
}
if(auto result = fs::write(preamble_path, preamble); !result) {
LOG_WARN("resolve_header_context: cannot write preamble {}: {}",
preamble_path,
result.error().message());
return std::nullopt;
}
LOG_INFO("resolve_header_context: wrote preamble {} for header path_id={}",
preamble_path,
header_path_id);
}

return HeaderFileContext{host_path_id, preamble_path, preamble_hash};
}

bool MasterServer::fill_compile_args(llvm::StringRef path,
std::string& directory,
std::vector<std::string>& arguments) {
auto results = cdb.lookup(path, {.query_toolchain = true});
if(results.empty()) {
LOG_WARN("No CDB entry for {}", path);
if(!results.empty()) {
auto& ctx = results.front();
directory = ctx.directory.str();
arguments.clear();
for(auto* arg: ctx.arguments) {
arguments.emplace_back(arg);
}
return true;
}

// No direct CDB entry — try to compile the header in context of a host source.
auto path_id = path_pool.intern(path);

// Use cached context if available; otherwise resolve.
const HeaderFileContext* ctx_ptr = nullptr;
auto ctx_it = header_file_contexts.find(path_id);
if(ctx_it != header_file_contexts.end()) {
ctx_ptr = &ctx_it->second;
} else {
auto resolved = resolve_header_context(path_id);
if(!resolved) {
LOG_WARN("No CDB entry and no header context for {}", path);
return false;
}
header_file_contexts[path_id] = std::move(*resolved);
ctx_ptr = &header_file_contexts[path_id];
}

auto host_path = path_pool.resolve(ctx_ptr->host_path_id);
auto host_results = cdb.lookup(host_path, {.query_toolchain = true});
if(host_results.empty()) {
LOG_WARN("fill_compile_args: host {} has no CDB entry", host_path);
return false;
}
auto& ctx = results.front();
directory = ctx.directory.str();

auto& host_ctx = host_results.front();
directory = host_ctx.directory.str();
arguments.clear();
for(auto* arg: ctx.arguments) {
arguments.emplace_back(arg);
}

// Copy host arguments, skipping the last element if it looks like a source
// file path (i.e. not a flag). The header path is used as the main file by
// the caller via CompileParams::path.
auto num_args = host_ctx.arguments.size();
std::size_t copy_count = num_args;
if(copy_count > 0) {
llvm::StringRef last(host_ctx.arguments[copy_count - 1]);
if(!last.starts_with("-"))
copy_count -= 1;
}
for(std::size_t i = 0; i < copy_count; ++i) {
arguments.emplace_back(host_ctx.arguments[i]);
}
Comment thread
16bit-ykiko marked this conversation as resolved.
Outdated

// Inject the preamble before the header so the compiler sees all context
// code that normally precedes this header in the host translation unit.
arguments.insert(arguments.begin() + 1, ctx_ptr->preamble_path);
arguments.insert(arguments.begin() + 1, "-include");

LOG_INFO("fill_compile_args: using header context for {} (host={}, preamble={})",
path,
host_path,
ctx_ptr->preamble_path);
return true;
}

Expand Down Expand Up @@ -2018,6 +2192,22 @@ void MasterServer::register_handlers() {
}
}

// Invalidate header contexts whose host is the saved file.
// Collect entries to erase to avoid modifying the map while iterating.
llvm::SmallVector<std::uint32_t, 4> stale_headers;
for(auto& [hdr_id, hdr_ctx]: header_file_contexts) {
if(hdr_ctx.host_path_id == path_id)
stale_headers.push_back(hdr_id);
}
for(auto hdr_id: stale_headers) {
header_file_contexts.erase(hdr_id);
auto doc_it = documents.find(hdr_id);
if(doc_it != documents.end()) {
doc_it->second.ast_dirty = true;
LOG_DEBUG("didSave: invalidated header context for path_id={}", hdr_id);
}
}
Comment thread
16bit-ykiko marked this conversation as resolved.

// Trigger background indexing after save.
schedule_indexing();

Expand Down
15 changes: 15 additions & 0 deletions src/server/master_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ struct DocumentState {
std::shared_ptr<PendingCompile> compiling;
};

/// Context for compiling a header file that lacks its own CDB entry.
struct HeaderFileContext {
std::uint32_t host_path_id; // Source file acting as host
std::string preamble_path; // Path to generated preamble file on disk
std::uint64_t preamble_hash; // Hash of preamble content for staleness
};

/// Two-layer staleness snapshot for compilation artifacts (PCH, AST, etc.).
///
/// Layer 1 (fast): compare each file's current mtime against build_at.
Expand Down Expand Up @@ -216,6 +223,9 @@ class MasterServer {
/// Per-file dependency snapshots from last successful AST compilation.
llvm::DenseMap<std::uint32_t, DepsSnapshot> ast_deps;

/// Header context cache: header_path_id -> context
llvm::DenseMap<std::uint32_t, HeaderFileContext> header_file_contexts;

// === Helpers ===

/// Convert a file:// URI to a local file path.
Expand All @@ -240,6 +250,11 @@ class MasterServer {
std::string& directory,
std::vector<std::string>& arguments);

/// Generate a preamble file for compiling a header in context.
/// The preamble contains all code from the host source (and intermediate
/// headers) that comes BEFORE the #include of the target header.
std::optional<HeaderFileContext> resolve_header_context(std::uint32_t header_path_id);

/// Build or reuse PCH for a source file. Returns true if PCH is available.
et::task<bool> ensure_pch(std::uint32_t path_id,
llvm::StringRef path,
Expand Down
104 changes: 104 additions & 0 deletions src/syntax/dependency_graph.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "syntax/dependency_graph.h"

#include <algorithm>
#include <chrono>

#include "command/toolchain.h"
Expand Down Expand Up @@ -101,6 +102,109 @@ std::size_t DependencyGraph::edge_count() const {
return count;
}

void DependencyGraph::build_reverse_map() {
reverse_includes_.clear();
for(auto& [key, ids]: includes) {
for(auto flagged_id: ids) {
auto included_id = flagged_id & PATH_ID_MASK;
auto& vec = reverse_includes_[included_id];
if(llvm::find(vec, key.path_id) == vec.end()) {
vec.push_back(key.path_id);
}
}
}
}

llvm::ArrayRef<std::uint32_t> DependencyGraph::get_includers(std::uint32_t path_id) const {
auto it = reverse_includes_.find(path_id);
if(it != reverse_includes_.end()) {
return it->second;
}
return {};
}

llvm::SmallVector<std::uint32_t, 4>
DependencyGraph::find_host_sources(std::uint32_t header_path_id) const {
llvm::SmallVector<std::uint32_t, 4> result;
llvm::DenseSet<std::uint32_t> visited;
llvm::SmallVector<std::uint32_t, 16> queue;

queue.push_back(header_path_id);
visited.insert(header_path_id);

while(!queue.empty()) {
auto current = queue.pop_back_val();
auto includers = get_includers(current);
if(includers.empty()) {
// No includers: this is a root (source file).
// Exclude the starting header itself.
if(current != header_path_id) {
result.push_back(current);
}
continue;
}
for(auto includer: includers) {
if(visited.insert(includer).second) {
queue.push_back(includer);
}
}
}

return result;
}

std::vector<std::uint32_t> DependencyGraph::find_include_chain(std::uint32_t host_path_id,
std::uint32_t target_path_id) const {
if(host_path_id == target_path_id) {
return {host_path_id};
}

// BFS: predecessor map for path reconstruction.
llvm::DenseMap<std::uint32_t, std::uint32_t> prev;
llvm::SmallVector<std::uint32_t, 16> queue;

prev[host_path_id] = host_path_id;
queue.push_back(host_path_id);

bool found = false;
while(!queue.empty() && !found) {
llvm::SmallVector<std::uint32_t, 16> next_queue;
for(auto current: queue) {
auto includes_union = get_all_includes(current);
for(auto flagged_id: includes_union) {
auto child = flagged_id & PATH_ID_MASK;
if(prev.find(child) == prev.end()) {
prev[child] = current;
if(child == target_path_id) {
found = true;
break;
}
next_queue.push_back(child);
}
}
if(found) {
break;
}
}
queue = std::move(next_queue);
}

if(!found) {
return {};
}

// Reconstruct path from target back to host.
std::vector<std::uint32_t> chain;
auto node = target_path_id;
while(node != host_path_id) {
chain.push_back(node);
node = prev[node];
}
chain.push_back(host_path_id);
std::reverse(chain.begin(), chain.end());
return chain;
}

// ============================================================================
// Wavefront BFS scanner — async implementation
// ============================================================================
Expand Down
Loading
Loading