Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
111 changes: 110 additions & 1 deletion src/server/master_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
#include "syntax/dependency_graph.h"
#include "syntax/scan.h"

#include "llvm/Support/xxhash.h"

namespace clice {

namespace protocol = eventide::ipc::protocol;
Expand Down Expand Up @@ -181,6 +183,14 @@ et::task<> MasterServer::run_build_drain(std::uint32_t path_id, std::string uri)
}
}

// Build or reuse PCH for preamble acceleration.
co_await ensure_pch(path_id, params.path, params.text, params.directory, params.arguments);

// Populate PCH info if available.
if(auto pch_it = pch_paths.find(path_id); pch_it != pch_paths.end()) {
params.pch = {pch_it->second, pch_bounds[path_id]};
}

LOG_DEBUG("Sending compile: path={}, args={}, gen={}",
params.path,
params.arguments.size(),
Expand Down Expand Up @@ -379,6 +389,81 @@ bool MasterServer::fill_compile_args(llvm::StringRef path,
return true;
}

et::task<bool> MasterServer::ensure_pch(std::uint32_t path_id,
llvm::StringRef path,
const std::string& text,
const std::string& directory,
const std::vector<std::string>& arguments) {
auto bound = compute_preamble_bound(text);
if(bound == 0) {
// No preamble directives — PCH would be empty. Clear any stale entry.
if(auto old_it = pch_paths.find(path_id); old_it != pch_paths.end()) {
fs::remove(old_it->second);
}
pch_paths.erase(path_id);
pch_bounds.erase(path_id);
pch_hashes.erase(path_id);
co_return true;
}

auto preamble_hash = llvm::xxh3_64bits(llvm::StringRef(text).substr(0, bound));

// Reuse existing PCH if preamble content hasn't changed.
if(auto it = pch_hashes.find(path_id); it != pch_hashes.end()) {
if(it->second == preamble_hash && pch_paths.contains(path_id)) {
pch_bounds[path_id] = bound;
co_return true;
}
}

// If another coroutine is already building PCH for this file, wait for it.
if(auto it = pch_building.find(path_id); it != pch_building.end()) {
co_await it->second->wait();
co_return pch_paths.contains(path_id);
}

// Register in-flight build so concurrent requests wait on us.
auto completion = std::make_shared<et::event>();
pch_building[path_id] = completion;

// Build a new PCH via stateless worker.
worker::BuildPCHParams pch_params;
pch_params.file = std::string(path);
pch_params.directory = directory;
pch_params.arguments = arguments;
pch_params.content = text;
pch_params.preamble_bound = bound;

LOG_DEBUG("Building PCH for {}, bound={}", path, bound);

auto result = co_await pool.send_stateless(pch_params);

if(!result.has_value() || !result.value().success) {
LOG_WARN("PCH build failed for {}: {}",
path,
result.has_value() ? result.value().error : result.error().message);
pch_building.erase(path_id);
completion->set();
co_return false;
}

// Delete old PCH temp file before replacing.
if(auto old_it = pch_paths.find(path_id); old_it != pch_paths.end()) {
fs::remove(old_it->second);
}

pch_paths[path_id] = result.value().pch_path;
pch_bounds[path_id] = bound;
pch_hashes[path_id] = preamble_hash;

LOG_INFO("PCH built for {}: {}", path, result.value().pch_path);

// Signal waiters after state is fully updated, then remove in-flight entry.
pch_building.erase(path_id);
completion->set();
Comment thread
16bit-ykiko marked this conversation as resolved.
co_return true;
Comment thread
16bit-ykiko marked this conversation as resolved.
}

et::task<bool> MasterServer::ensure_compiled(std::uint32_t path_id, const std::string& uri) {
auto doc_it = documents.find(path_id);
if(doc_it == documents.end())
Expand Down Expand Up @@ -459,7 +544,24 @@ MasterServer::RawResult MasterServer::forward_stateless(const std::string& uri,
if(!fill_compile_args(path, wp.directory, wp.arguments))
co_return serde_raw{};

lsp::PositionMapper mapper(doc.text, lsp::PositionEncoding::UTF16);
// Ensure PCH is available for stateless compilation (completion/signatureHelp).
co_await ensure_pch(path_id, path, wp.text, wp.directory, wp.arguments);
if(auto pch_it = pch_paths.find(path_id); pch_it != pch_paths.end()) {
wp.pch = {pch_it->second, pch_bounds[path_id]};
}

// Fill available PCM paths for module-aware completion.
// Skip the file's own PCM to avoid "multiple module declarations" errors.
for(auto& [pid, pcm_path]: pcm_paths) {
if(pid == path_id)
continue;
auto mod_it = path_to_module.find(pid);
if(mod_it != path_to_module.end()) {
wp.pcms[mod_it->second] = pcm_path;
}
}

lsp::PositionMapper mapper(wp.text, lsp::PositionEncoding::UTF16);
auto offset = mapper.to_offset(position);
if(!offset)
co_return serde_raw{"null"};
Expand Down Expand Up @@ -675,6 +777,9 @@ void MasterServer::register_handlers() {

documents.erase(path_id);
debounce_timers.erase(path_id);
pch_paths.erase(path_id);
pch_bounds.erase(path_id);
pch_hashes.erase(path_id);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Clear diagnostics for closed file
clear_diagnostics(params.text_document.uri);
Expand Down Expand Up @@ -711,6 +816,10 @@ void MasterServer::register_handlers() {
}
}

// Invalidate all cached PCH hashes — the saved file may be a header
// included by other TUs, so we must force rebuild for all open documents.
pch_hashes.clear();

LOG_DEBUG("didSave: {}", params.text_document.uri);
});

Expand Down
19 changes: 19 additions & 0 deletions src/server/master_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ class MasterServer {
// path_id -> module name (for files that provide a module interface).
llvm::DenseMap<std::uint32_t, std::string> path_to_module;

// path_id -> built PCH file path.
llvm::DenseMap<std::uint32_t, std::string> pch_paths;

// path_id -> preamble bound (byte offset) used when building the PCH.
llvm::DenseMap<std::uint32_t, std::uint32_t> pch_bounds;

// path_id -> hash of preamble content at PCH build time (for staleness detection).
llvm::DenseMap<std::uint32_t, std::uint64_t> pch_hashes;

// path_id -> in-flight PCH build event (later arrivals co_await the same build).
llvm::DenseMap<std::uint32_t, std::shared_ptr<et::event>> pch_building;

// Document state: path_id -> DocumentState
llvm::DenseMap<std::uint32_t, DocumentState> documents;

Expand Down Expand Up @@ -104,6 +116,13 @@ class MasterServer {
std::string& directory,
std::vector<std::string>& arguments);

// 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,
const std::string& text,
const std::string& directory,
const std::vector<std::string>& arguments);

// Forwarding helpers for feature requests (RawValue passthrough)
using RawResult = et::task<et::serde::RawValue, et::ipc::Error>;

Expand Down
2 changes: 2 additions & 0 deletions src/server/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@ struct BuildPCHParams {
std::string directory;
std::vector<std::string> arguments;
std::string content;
std::uint32_t preamble_bound = UINT32_MAX;
};

struct BuildPCHResult {
bool success;
std::string error;
std::string pch_path;
};

struct BuildPCMParams {
Expand Down
14 changes: 9 additions & 5 deletions src/server/stateless_worker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,28 @@ int run_stateless_worker_mode() {
CompilationParams cp;
cp.kind = CompilationKind::Preamble;
fill_args(cp, params.directory, params.arguments);
cp.add_remapped_file(params.file, params.content);
cp.add_remapped_file(params.file, params.content, params.preamble_bound);
Comment thread
16bit-ykiko marked this conversation as resolved.

auto tmp = fs::createTemporaryFile("clice-pch", "pch");
if(!tmp) {
LOG_ERROR("BuildPCH: failed to create temp file");
return {false, "Failed to create temporary PCH file"};
return {false, "Failed to create temporary PCH file", ""};
}
cp.output_file = *tmp;

PCHInfo pch_info;
auto unit = compile(cp, pch_info);

if(unit.completed()) {
LOG_INFO("BuildPCH done: file={}, {}ms", params.file, timer.ms());
return {true, ""};
LOG_INFO("BuildPCH done: file={}, output={}, {}ms",
params.file,
cp.output_file,
timer.ms());
return {true, "", std::string(cp.output_file)};
} else {
LOG_WARN("BuildPCH failed: file={}, {}ms", params.file, timer.ms());
return {false, "PCH compilation failed"};
fs::remove(cp.output_file);
return {false, "PCH compilation failed", ""};
}
});
co_return result.value();
Expand Down
8 changes: 8 additions & 0 deletions tests/data/pch_test/common.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#pragma once

struct Point {
int x;
int y;
};

int add(int a, int b);
11 changes: 11 additions & 0 deletions tests/data/pch_test/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#include "common.h"

int add(int a, int b) {
return a + b;
}

int main() {
Point p{1, 2};
int result = add(p.x, p.y);
return result;
}
7 changes: 7 additions & 0 deletions tests/data/pch_test/no_includes.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
int square(int x) {
return x * x;
}

int main() {
return square(3);
}
104 changes: 104 additions & 0 deletions tests/integration/test_pch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Integration tests for PCH (precompiled header) functionality in MasterServer."""

import asyncio

import pytest
from lsprotocol.types import (
CompletionParams,
DidChangeTextDocumentParams,
DidCloseTextDocumentParams,
HoverParams,
Position,
TextDocumentContentChangeWholeDocument,
TextDocumentIdentifier,
VersionedTextDocumentIdentifier,
)


def _doc(uri: str) -> TextDocumentIdentifier:
return TextDocumentIdentifier(uri=uri)


@pytest.mark.workspace("pch_test")
Comment thread
16bit-ykiko marked this conversation as resolved.
async def test_pch_diagnostics_on_open(client, workspace):
"""Opening a file with #include should trigger PCH build and return clean diagnostics."""
uri, _ = await client.open_and_wait(workspace / "main.cpp")
assert uri in client.diagnostics
# main.cpp is well-formed, so diagnostics list should be empty (no errors).
diags = client.diagnostics[uri]
assert len(diags) == 0, f"Expected no diagnostics, got: {diags}"
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))


@pytest.mark.workspace("pch_test")
async def test_pch_body_edit_triggers_recompile(client, workspace):
"""Editing only the body (not the preamble) should trigger recompilation."""
uri, content = await client.open_and_wait(workspace / "main.cpp")

# Edit only the function body — preamble (#include "common.h") unchanged.
new_content = content.replace("return result;", "return result + 1;")
event = client.wait_for_diagnostics(uri)
client.text_document_did_change(
DidChangeTextDocumentParams(
text_document=VersionedTextDocumentIdentifier(uri=uri, version=1),
content_changes=[TextDocumentContentChangeWholeDocument(text=new_content)],
)
)
# The key assertion: recompilation completes (diagnostics event fires).
await asyncio.wait_for(event.wait(), timeout=30.0)
assert uri in client.diagnostics
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))


@pytest.mark.workspace("pch_test")
async def test_no_pch_for_no_includes(client, workspace):
"""A file with no #include directives should compile without PCH."""
uri, _ = await client.open_and_wait(workspace / "no_includes.cpp")
assert uri in client.diagnostics
diags = client.diagnostics[uri]
assert len(diags) == 0, f"Expected no diagnostics, got: {diags}"
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))


@pytest.mark.workspace("pch_test")
async def test_hover_on_local_symbol(client, workspace):
"""Hover on a locally defined symbol should work when PCH is active."""
uri, _ = await client.open_and_wait(workspace / "main.cpp")

# Hover over "add" on line 2 (0-indexed): "int add(int a, int b) {"
result = await client.text_document_hover_async(
HoverParams(text_document=_doc(uri), position=Position(line=2, character=4))
)
assert result is not None
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))


@pytest.mark.workspace("pch_test")
async def test_completion_with_pch(client, workspace):
"""Completion should see symbols from PCH headers."""
uri, content = await client.open_and_wait(workspace / "main.cpp")

# Add a line that starts typing "Poi" to trigger completion for Point.
new_content = content + "\nPoi"
lines = new_content.split("\n")
last_line = len(lines) - 1

event = client.wait_for_diagnostics(uri)
client.text_document_did_change(
DidChangeTextDocumentParams(
text_document=VersionedTextDocumentIdentifier(uri=uri, version=1),
content_changes=[TextDocumentContentChangeWholeDocument(text=new_content)],
)
)
# Brief wait for the change to be processed.
await asyncio.sleep(1.0)

result = await client.text_document_completion_async(
CompletionParams(
text_document=_doc(uri),
position=Position(line=last_line, character=3),
)
)
# Completion should return results.
assert result is not None
client.text_document_did_close(DidCloseTextDocumentParams(text_document=_doc(uri)))
Loading
Loading