Sprints 273-276: Polyglot FFI Glue, Symbol Index, and LSP Proxy (steps 1893-1912)
Sprint 273 — PolyglotFFIGlueGenerator (steps 1893-1897): ABIBoundaryExtractor, CHeaderEmitter, RustPythonBindingEmitter, GoCppBindingEmitter. Extracts exported symbols from toComponent provider and emits C ABI + language-specific bindings. Sprint 274 — More Binding Pairs + DWARF (steps 1898-1902): CppJSBindingEmitter (N-API), RustGoBindingEmitter (#[no_mangle]), DWARFBoundaryAnnotator (DW_TAG_* + crossLangBoundary), FFIGlueDispatcher (routes by fromLang→toLang pair), MCP tool whetstone_generate_ffi_glue (#92). Sprint 275 — Cross-Language Symbol Index (steps 1903-1907): SCIPEmitter (SCIP JSON, one doc/language, caller+provider roles), CrossLanguageSymbolTable (byLangName_ + byScip_ dual index), SymbolIndexUpdater (nodeKey diff, clear-and-rebuild), SymbolIndexOrchestrator (orchestrate + update), MCP tool whetstone_emit_symbol_index (#93). Sprint 276 — LSP Proxy Core (steps 1908-1912): LSPProxyServer (JSON-RPC 2.0 dispatcher), LanguageServerRouter (routeByUri/routeByLanguageId), CrossLanguageBoundaryDetector (detect/detectWithUri/detectInLanguage), CrossLanguageDefinitionResolver (resolve/resolveFromUri → DefinitionLocation). End-to-end: Python call site goto-def resolves to Rust sort-core provider. 100/100 tests passing across all four sprints. Architecture gate clean. LoRA sessions recorded for all sprints (~14k tokens captured). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
81
editor/src/ABIBoundaryExtractor.h
Normal file
81
editor/src/ABIBoundaryExtractor.h
Normal file
@@ -0,0 +1,81 @@
|
||||
#pragma once
|
||||
#include "PolyglotProjectSpec.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct ABIBoundaryNode {
|
||||
std::string name; // exported symbol name
|
||||
std::string kind; // "function" | "struct"
|
||||
std::string fromComponent; // component that exports this node
|
||||
std::string toComponent; // component that imports it
|
||||
std::string fromLanguage; // assignedLanguage of fromComponent
|
||||
std::string toLanguage; // assignedLanguage of toComponent
|
||||
nlohmann::json signature; // raw AST node JSON (pass-through)
|
||||
};
|
||||
|
||||
class ABIBoundaryExtractor {
|
||||
public:
|
||||
// Extract boundary nodes from all interfaces in the spec.
|
||||
// ast format: {"components": {"comp-name": {"nodes": [{name, kind, exported, ...}]}}}
|
||||
// For each interface (fromComponent → toComponent), collect exported nodes from
|
||||
// the from-side component. Non-exported nodes and missing components are skipped.
|
||||
static std::vector<ABIBoundaryNode> extract(
|
||||
const PolyglotProjectSpec& spec,
|
||||
const nlohmann::json& ast)
|
||||
{
|
||||
std::vector<ABIBoundaryNode> result;
|
||||
|
||||
if (!ast.contains("components") || !ast["components"].is_object())
|
||||
return result;
|
||||
|
||||
const auto& components = ast["components"];
|
||||
|
||||
for (const auto& iface : spec.interfaces) {
|
||||
const std::string& from = iface.fromComponent;
|
||||
const std::string& to = iface.toComponent;
|
||||
|
||||
// The provider (toComponent) exports the symbols the caller (fromComponent) needs.
|
||||
if (!components.contains(to)) continue;
|
||||
const auto& toEntry = components[to];
|
||||
if (!toEntry.contains("nodes") || !toEntry["nodes"].is_array()) continue;
|
||||
|
||||
std::string fromLang = findLanguage(spec, from);
|
||||
std::string toLang = findLanguage(spec, to);
|
||||
|
||||
for (const auto& node : toEntry["nodes"]) {
|
||||
if (!node.contains("exported")) continue;
|
||||
if (!node["exported"].get<bool>()) continue;
|
||||
|
||||
ABIBoundaryNode bn;
|
||||
bn.name = node.contains("name") ? node["name"].get<std::string>() : "";
|
||||
bn.kind = node.contains("kind") ? node["kind"].get<std::string>() : "function";
|
||||
bn.fromComponent = from;
|
||||
bn.toComponent = to;
|
||||
bn.fromLanguage = fromLang;
|
||||
bn.toLanguage = toLang;
|
||||
bn.signature = node;
|
||||
|
||||
if (!bn.name.empty())
|
||||
result.push_back(std::move(bn));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string findLanguage(const PolyglotProjectSpec& spec,
|
||||
const std::string& componentName)
|
||||
{
|
||||
for (const auto& section : spec.sections) {
|
||||
if (section.componentName == componentName)
|
||||
return section.assignedLanguage;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
110
editor/src/CHeaderEmitter.h
Normal file
110
editor/src/CHeaderEmitter.h
Normal file
@@ -0,0 +1,110 @@
|
||||
#pragma once
|
||||
#include "ABIBoundaryExtractor.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
class CHeaderEmitter {
|
||||
public:
|
||||
// Emit a complete C ABI header for the given boundary nodes.
|
||||
// headerName is used to form the include guard (e.g., "sort_array" → SORT_ARRAY_ABI_H).
|
||||
// Returns the full header as a std::string ending with '\n'.
|
||||
static std::string emit(const std::vector<ABIBoundaryNode>& nodes,
|
||||
const std::string& headerName)
|
||||
{
|
||||
std::string guard = toUpperGuard(headerName);
|
||||
std::ostringstream out;
|
||||
|
||||
out << "#ifndef " << guard << "\n";
|
||||
out << "#define " << guard << "\n\n";
|
||||
out << "#include <stdint.h>\n";
|
||||
out << "#include <stddef.h>\n\n";
|
||||
out << "#ifdef __cplusplus\n";
|
||||
out << "extern \"C\" {\n";
|
||||
out << "#endif\n\n";
|
||||
|
||||
for (const auto& node : nodes) {
|
||||
out << "/* boundary: " << node.fromComponent
|
||||
<< " -> " << node.toComponent << " */\n";
|
||||
if (node.kind == "struct") {
|
||||
out << emitStruct(node);
|
||||
} else {
|
||||
out << emitFunction(node);
|
||||
}
|
||||
out << "\n";
|
||||
}
|
||||
|
||||
out << "#ifdef __cplusplus\n";
|
||||
out << "}\n";
|
||||
out << "#endif\n\n";
|
||||
out << "#endif /* " << guard << " */\n";
|
||||
|
||||
return out.str();
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string emitFunction(const ABIBoundaryNode& node) {
|
||||
const auto& sig = node.signature;
|
||||
|
||||
std::string retType = "void";
|
||||
if (sig.contains("returnType") && sig["returnType"].is_string())
|
||||
retType = sig["returnType"].get<std::string>();
|
||||
|
||||
std::ostringstream out;
|
||||
out << retType << " " << node.name << "(";
|
||||
|
||||
bool hasParams = false;
|
||||
if (sig.contains("params") && sig["params"].is_array() && !sig["params"].empty()) {
|
||||
bool first = true;
|
||||
for (const auto& param : sig["params"]) {
|
||||
if (!first) out << ", ";
|
||||
first = false;
|
||||
std::string ptype = param.contains("type") ? param["type"].get<std::string>() : "void*";
|
||||
std::string pname = param.contains("name") ? param["name"].get<std::string>() : "";
|
||||
out << ptype;
|
||||
if (!pname.empty()) out << " " << pname;
|
||||
hasParams = true;
|
||||
}
|
||||
}
|
||||
if (!hasParams) out << "void";
|
||||
|
||||
out << ");";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
static std::string emitStruct(const ABIBoundaryNode& node) {
|
||||
const auto& sig = node.signature;
|
||||
std::ostringstream out;
|
||||
|
||||
out << "typedef struct {\n";
|
||||
|
||||
if (sig.contains("fields") && sig["fields"].is_array()) {
|
||||
for (const auto& field : sig["fields"]) {
|
||||
std::string ftype = field.contains("type") ? field["type"].get<std::string>() : "void*";
|
||||
std::string fname = field.contains("name") ? field["name"].get<std::string>() : "field";
|
||||
out << " " << ftype << " " << fname << ";\n";
|
||||
}
|
||||
}
|
||||
|
||||
out << "} " << node.name << ";";
|
||||
return out.str();
|
||||
}
|
||||
|
||||
static std::string toUpperGuard(const std::string& name) {
|
||||
std::string result;
|
||||
result.reserve(name.size() + 8);
|
||||
for (char c : name) {
|
||||
if (std::isalnum(static_cast<unsigned char>(c)))
|
||||
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
else
|
||||
result += '_';
|
||||
}
|
||||
result += "_ABI_H";
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
213
editor/src/CppJSBindingEmitter.h
Normal file
213
editor/src/CppJSBindingEmitter.h
Normal file
@@ -0,0 +1,213 @@
|
||||
#pragma once
|
||||
#include "ABIBoundaryExtractor.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct CppJSBinding {
|
||||
std::string cppCode; // C++ N-API source snippet
|
||||
std::string jsCode; // JavaScript source snippet
|
||||
};
|
||||
|
||||
class CppJSBindingEmitter {
|
||||
public:
|
||||
static CppJSBinding emit(const ABIBoundaryNode& node) {
|
||||
if (node.kind == "struct") return emitStruct(node);
|
||||
return emitFunction(node);
|
||||
}
|
||||
|
||||
private:
|
||||
static CppJSBinding emitFunction(const ABIBoundaryNode& node) {
|
||||
const auto& sig = node.signature;
|
||||
std::string retRaw;
|
||||
if (sig.contains("returnType") && sig["returnType"].is_string())
|
||||
retRaw = sig["returnType"].get<std::string>();
|
||||
bool voidRet = retRaw.empty() || retRaw == "void";
|
||||
|
||||
// Collect params
|
||||
std::vector<std::pair<std::string,std::string>> params; // name, type
|
||||
if (sig.contains("params") && sig["params"].is_array()) {
|
||||
for (const auto& p : sig["params"]) {
|
||||
std::string pn = p.contains("name") ? p["name"].get<std::string>() : "arg";
|
||||
std::string pt = p.contains("type") ? p["type"].get<std::string>() : "int";
|
||||
params.push_back({pn, pt});
|
||||
}
|
||||
}
|
||||
|
||||
std::string camel = toCamelCase(node.name);
|
||||
std::string addonName = node.toComponent;
|
||||
// replace '-' with '_' for valid C identifiers
|
||||
for (char& c : addonName) if (c == '-') c = '_';
|
||||
|
||||
// --- C++ side ---
|
||||
std::ostringstream cpp;
|
||||
cpp << "// N-API binding: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
cpp << "#include <node_api.h>\n";
|
||||
cpp << "napi_value js_" << node.name << "(napi_env env, napi_callback_info info) {\n";
|
||||
|
||||
if (!params.empty()) {
|
||||
cpp << " size_t argc = " << params.size() << ";\n";
|
||||
cpp << " napi_value args[" << params.size() << "];\n";
|
||||
cpp << " napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);\n";
|
||||
for (int i = 0; i < (int)params.size(); ++i) {
|
||||
cpp << " " << napiGetValue(params[i].second, params[i].first, i);
|
||||
}
|
||||
} else {
|
||||
cpp << " napi_get_cb_info(env, info, nullptr, nullptr, nullptr, nullptr);\n";
|
||||
}
|
||||
|
||||
// Call native
|
||||
if (!voidRet) {
|
||||
cpp << " " << toCppRetType(retRaw) << " result = " << node.name << "(";
|
||||
} else {
|
||||
cpp << " " << node.name << "(";
|
||||
}
|
||||
for (int i = 0; i < (int)params.size(); ++i) {
|
||||
if (i > 0) cpp << ", ";
|
||||
cpp << params[i].first;
|
||||
}
|
||||
cpp << ");\n";
|
||||
|
||||
// Return value
|
||||
cpp << " napi_value ret;\n";
|
||||
if (!voidRet) {
|
||||
cpp << " " << napiSetValue(retRaw, "result");
|
||||
} else {
|
||||
cpp << " napi_get_undefined(env, &ret);\n";
|
||||
}
|
||||
cpp << " return ret;\n}\n";
|
||||
|
||||
// NAPI_MODULE_INIT
|
||||
cpp << "NAPI_MODULE_INIT() {\n";
|
||||
cpp << " napi_value fn;\n";
|
||||
cpp << " napi_create_function(env, \"" << camel
|
||||
<< "\", NAPI_AUTO_LENGTH, js_" << node.name << ", nullptr, &fn);\n";
|
||||
cpp << " napi_set_named_property(exports, \"" << camel << "\", fn);\n";
|
||||
cpp << " return exports;\n}\n";
|
||||
|
||||
// --- JS side ---
|
||||
std::ostringstream js;
|
||||
js << "// JS binding: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
js << "const addon = require('./" << addonName << "_addon');\n";
|
||||
js << "// " << camel << "(";
|
||||
for (int i = 0; i < (int)params.size(); ++i) {
|
||||
if (i > 0) js << ", ";
|
||||
js << params[i].first << ": " << toJSType(params[i].second);
|
||||
}
|
||||
js << "): " << (voidRet ? "void" : toJSType(retRaw)) << "\n";
|
||||
js << "const " << camel << " = addon." << camel << ";\n";
|
||||
js << "module.exports = { " << camel << " };\n";
|
||||
|
||||
return {cpp.str(), js.str()};
|
||||
}
|
||||
|
||||
static CppJSBinding emitStruct(const ABIBoundaryNode& node) {
|
||||
const auto& sig = node.signature;
|
||||
std::vector<std::pair<std::string,std::string>> fields;
|
||||
if (sig.contains("fields") && sig["fields"].is_array()) {
|
||||
for (const auto& f : sig["fields"]) {
|
||||
std::string fn = f.contains("name") ? f["name"].get<std::string>() : "field";
|
||||
std::string ft = f.contains("type") ? f["type"].get<std::string>() : "int";
|
||||
fields.push_back({fn, ft});
|
||||
}
|
||||
}
|
||||
|
||||
std::string addonName = node.toComponent;
|
||||
for (char& c : addonName) if (c == '-') c = '_';
|
||||
|
||||
// --- C++ side ---
|
||||
std::ostringstream cpp;
|
||||
cpp << "// N-API struct: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
cpp << "#include <node_api.h>\n";
|
||||
cpp << "napi_value " << node.name << "_to_js(napi_env env, const " << node.name << "& s) {\n";
|
||||
cpp << " napi_value obj;\n";
|
||||
cpp << " napi_create_object(env, &obj);\n";
|
||||
for (auto& [fn, ft] : fields) {
|
||||
cpp << " { napi_value v; " << napiSetFieldValue(ft, "s." + fn)
|
||||
<< " napi_set_named_property(env, obj, \"" << fn << "\", v); }\n";
|
||||
}
|
||||
cpp << " return obj;\n}\n";
|
||||
|
||||
// --- JS side ---
|
||||
std::ostringstream js;
|
||||
js << "// JS struct: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
js << "// " << node.name << ": { ";
|
||||
for (int i = 0; i < (int)fields.size(); ++i) {
|
||||
if (i > 0) js << ", ";
|
||||
js << fields[i].first << ": " << toJSType(fields[i].second);
|
||||
}
|
||||
js << " }\n";
|
||||
js << "const addon = require('./" << addonName << "_addon');\n";
|
||||
js << "module.exports = { " << node.name << ": addon." << node.name << " };\n";
|
||||
|
||||
return {cpp.str(), js.str()};
|
||||
}
|
||||
|
||||
static std::string napiGetValue(const std::string& ctype,
|
||||
const std::string& varName, int idx) {
|
||||
std::string s = " std::string idx" + std::to_string(idx) + " = std::to_string(" + std::to_string(idx) + ");\n";
|
||||
std::ostringstream o;
|
||||
if (ctype == "char*" || ctype == "char *") {
|
||||
o << "size_t " << varName << "_len; "
|
||||
<< "napi_get_value_string_utf8(env, args[" << idx << "], nullptr, 0, &" << varName << "_len);\n";
|
||||
o << " char* " << varName << " = nullptr; /* allocate " << varName << "_len+1 */\n";
|
||||
} else if (ctype == "double" || ctype == "f64") {
|
||||
o << "double " << varName << "; "
|
||||
<< "napi_get_value_double(env, args[" << idx << "], &" << varName << ");\n";
|
||||
} else if (ctype == "float" || ctype == "f32") {
|
||||
o << "double " << varName << "_d; "
|
||||
<< "napi_get_value_double(env, args[" << idx << "], &" << varName << "_d); "
|
||||
<< "float " << varName << " = (float)" << varName << "_d;\n";
|
||||
} else {
|
||||
// default: int32
|
||||
o << "int32_t " << varName << "; "
|
||||
<< "napi_get_value_int32(env, args[" << idx << "], &" << varName << ");\n";
|
||||
}
|
||||
return " " + o.str();
|
||||
}
|
||||
|
||||
static std::string napiSetValue(const std::string& ctype, const std::string& varName) {
|
||||
if (ctype == "double" || ctype == "f64")
|
||||
return "napi_create_double(env, " + varName + ", &ret);\n";
|
||||
return "napi_create_int32(env, (int32_t)" + varName + ", &ret);\n";
|
||||
}
|
||||
|
||||
static std::string napiSetFieldValue(const std::string& ctype, const std::string& expr) {
|
||||
if (ctype == "double" || ctype == "f64")
|
||||
return "napi_create_double(env, " + expr + ", &v);";
|
||||
return "napi_create_int32(env, (int32_t)(" + expr + "), &v);";
|
||||
}
|
||||
|
||||
static std::string toCppRetType(const std::string& t) {
|
||||
if (t == "double" || t == "f64") return "double";
|
||||
if (t == "float" || t == "f32") return "float";
|
||||
return "int32_t";
|
||||
}
|
||||
|
||||
static std::string toJSType(const std::string& t) {
|
||||
if (t == "double" || t == "f64" || t == "float" || t == "f32") return "number";
|
||||
if (t == "char*" || t == "char *" || t == "string") return "string";
|
||||
if (t == "void") return "void";
|
||||
return "number";
|
||||
}
|
||||
|
||||
// process_request → processRequest
|
||||
static std::string toCamelCase(const std::string& name) {
|
||||
std::string result;
|
||||
bool capitalizeNext = false;
|
||||
bool first = true;
|
||||
for (char c : name) {
|
||||
if (c == '_') { capitalizeNext = true; }
|
||||
else if (first) { result += static_cast<char>(std::tolower(static_cast<unsigned char>(c))); first = false; capitalizeNext = false; }
|
||||
else if (capitalizeNext) { result += static_cast<char>(std::toupper(static_cast<unsigned char>(c))); capitalizeNext = false; }
|
||||
else { result += c; }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
61
editor/src/CrossLanguageBoundaryDetector.h
Normal file
61
editor/src/CrossLanguageBoundaryDetector.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
#include "CrossLanguageSymbolTable.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct DetectionResult {
|
||||
bool isBoundary = false;
|
||||
SymbolRecord record;
|
||||
};
|
||||
|
||||
class CrossLanguageBoundaryDetector {
|
||||
public:
|
||||
explicit CrossLanguageBoundaryDetector(const CrossLanguageSymbolTable& table)
|
||||
: table_(table) {}
|
||||
|
||||
DetectionResult detect(const std::string& symbolName) const {
|
||||
static const std::vector<std::string> langs =
|
||||
{"Python","Rust","Go","TypeScript","C++"};
|
||||
for (const auto& lang : langs) {
|
||||
auto r = table_.lookup(lang, symbolName);
|
||||
if (r.found) return {true, r};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
DetectionResult detectWithUri(const std::string& uri,
|
||||
const std::string& symbolName) const {
|
||||
std::string lang = langFromUri(uri);
|
||||
if (!lang.empty()) {
|
||||
auto r = detectInLanguage(lang, symbolName);
|
||||
if (r.isBoundary) return r;
|
||||
}
|
||||
return detect(symbolName);
|
||||
}
|
||||
|
||||
DetectionResult detectInLanguage(const std::string& language,
|
||||
const std::string& symbolName) const {
|
||||
auto r = table_.lookup(language, symbolName);
|
||||
if (r.found) return {true, r};
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
const CrossLanguageSymbolTable& table_;
|
||||
|
||||
static std::string langFromUri(const std::string& uri) {
|
||||
auto dot = uri.rfind('.');
|
||||
if (dot == std::string::npos) return "";
|
||||
std::string ext = uri.substr(dot);
|
||||
if (ext == ".py") return "Python";
|
||||
if (ext == ".rs") return "Rust";
|
||||
if (ext == ".go") return "Go";
|
||||
if (ext == ".ts" || ext == ".js") return "TypeScript";
|
||||
if (ext == ".cpp" || ext == ".cc" || ext == ".h" || ext == ".hpp") return "C++";
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
63
editor/src/CrossLanguageDefinitionResolver.h
Normal file
63
editor/src/CrossLanguageDefinitionResolver.h
Normal file
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
#include "CrossLanguageBoundaryDetector.h"
|
||||
#include "CrossLanguageSymbolTable.h"
|
||||
#include <string>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct DefinitionLocation {
|
||||
bool found = false;
|
||||
std::string providerComponent;
|
||||
std::string providerLanguage;
|
||||
std::string callerLanguage;
|
||||
std::string scipSymbol;
|
||||
};
|
||||
|
||||
class CrossLanguageDefinitionResolver {
|
||||
public:
|
||||
explicit CrossLanguageDefinitionResolver(const CrossLanguageSymbolTable& table)
|
||||
: table_(table), detector_(table) {}
|
||||
|
||||
DefinitionLocation resolve(const std::string& symbolName) const {
|
||||
auto det = detector_.detect(symbolName);
|
||||
if (!det.isBoundary) return {};
|
||||
return makeLocation(det.record, "");
|
||||
}
|
||||
|
||||
DefinitionLocation resolveFromUri(const std::string& callerUri,
|
||||
const std::string& symbolName) const {
|
||||
auto det = detector_.detectWithUri(callerUri, symbolName);
|
||||
if (!det.isBoundary) return {};
|
||||
std::string callerLang = langFromUri(callerUri);
|
||||
return makeLocation(det.record, callerLang);
|
||||
}
|
||||
|
||||
private:
|
||||
const CrossLanguageSymbolTable& table_;
|
||||
CrossLanguageBoundaryDetector detector_;
|
||||
|
||||
static DefinitionLocation makeLocation(const SymbolRecord& r,
|
||||
const std::string& callerLang) {
|
||||
DefinitionLocation loc;
|
||||
loc.found = true;
|
||||
loc.providerComponent = r.toComponent;
|
||||
loc.providerLanguage = r.toLanguage;
|
||||
loc.callerLanguage = callerLang.empty() ? r.fromLanguage : callerLang;
|
||||
loc.scipSymbol = r.toComponent + "/" + r.name + ".";
|
||||
return loc;
|
||||
}
|
||||
|
||||
static std::string langFromUri(const std::string& uri) {
|
||||
auto dot = uri.rfind('.');
|
||||
if (dot == std::string::npos) return "";
|
||||
std::string ext = uri.substr(dot);
|
||||
if (ext == ".py") return "Python";
|
||||
if (ext == ".rs") return "Rust";
|
||||
if (ext == ".go") return "Go";
|
||||
if (ext == ".ts" || ext == ".js") return "TypeScript";
|
||||
if (ext == ".cpp" || ext == ".cc" || ext == ".h" || ext == ".hpp") return "C++";
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
74
editor/src/CrossLanguageSymbolTable.h
Normal file
74
editor/src/CrossLanguageSymbolTable.h
Normal file
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
#include "ABIBoundaryExtractor.h"
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct SymbolRecord {
|
||||
bool found = false;
|
||||
std::string name;
|
||||
std::string kind;
|
||||
std::string fromComponent;
|
||||
std::string toComponent;
|
||||
std::string fromLanguage;
|
||||
std::string toLanguage;
|
||||
std::string scipSymbol; // "<toComponent>/<name>."
|
||||
};
|
||||
|
||||
struct SymbolAppearance {
|
||||
std::string language; // "Python" | "Rust" | etc.
|
||||
std::string role; // "caller" | "provider"
|
||||
std::string scipSymbol;
|
||||
};
|
||||
|
||||
class CrossLanguageSymbolTable {
|
||||
public:
|
||||
void insert(const ABIBoundaryNode& node) {
|
||||
std::string scip = makeScipSymbol(node);
|
||||
|
||||
SymbolRecord rec;
|
||||
rec.found = true;
|
||||
rec.name = node.name;
|
||||
rec.kind = node.kind;
|
||||
rec.fromComponent = node.fromComponent;
|
||||
rec.toComponent = node.toComponent;
|
||||
rec.fromLanguage = node.fromLanguage;
|
||||
rec.toLanguage = node.toLanguage;
|
||||
rec.scipSymbol = scip;
|
||||
|
||||
byLangName_[{node.toLanguage, node.name}] = rec;
|
||||
byLangName_[{node.fromLanguage, node.name}] = rec;
|
||||
|
||||
byScip_[scip].push_back({node.toLanguage, "provider", scip});
|
||||
byScip_[scip].push_back({node.fromLanguage, "caller", scip});
|
||||
}
|
||||
|
||||
SymbolRecord lookup(const std::string& language, const std::string& name) const {
|
||||
auto it = byLangName_.find({language, name});
|
||||
if (it == byLangName_.end()) return {};
|
||||
return it->second;
|
||||
}
|
||||
|
||||
bool contains(const std::string& language, const std::string& name) const {
|
||||
return byLangName_.count({language, name}) > 0;
|
||||
}
|
||||
|
||||
std::vector<SymbolAppearance> lookupByScip(const std::string& scipSymbol) const {
|
||||
auto it = byScip_.find(scipSymbol);
|
||||
if (it == byScip_.end()) return {};
|
||||
return it->second;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::pair<std::string,std::string>, SymbolRecord> byLangName_;
|
||||
std::map<std::string, std::vector<SymbolAppearance>> byScip_;
|
||||
|
||||
static std::string makeScipSymbol(const ABIBoundaryNode& node) {
|
||||
return node.toComponent + "/" + node.name + ".";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
43
editor/src/DWARFBoundaryAnnotator.h
Normal file
43
editor/src/DWARFBoundaryAnnotator.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include "ABIBoundaryExtractor.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
class DWARFBoundaryAnnotator {
|
||||
public:
|
||||
// Produce one JSON annotation object per ABIBoundaryNode.
|
||||
static nlohmann::json annotate(const std::vector<ABIBoundaryNode>& nodes) {
|
||||
nlohmann::json result = nlohmann::json::array();
|
||||
for (const auto& node : nodes) {
|
||||
nlohmann::json ann;
|
||||
ann["astNodeId"] = makeAstNodeId(node);
|
||||
ann["name"] = node.name;
|
||||
ann["kind"] = node.kind;
|
||||
ann["fromComponent"] = node.fromComponent;
|
||||
ann["toComponent"] = node.toComponent;
|
||||
ann["fromLanguage"] = node.fromLanguage;
|
||||
ann["toLanguage"] = node.toLanguage;
|
||||
ann["dwarfTag"] = dwarfTag(node.kind);
|
||||
ann["crossLangBoundary"] = true;
|
||||
result.push_back(std::move(ann));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
// Deterministic: "<fromComponent>:<toComponent>:<name>:<kind>"
|
||||
static std::string makeAstNodeId(const ABIBoundaryNode& node) {
|
||||
return node.fromComponent + ":" + node.toComponent + ":" + node.name + ":" + node.kind;
|
||||
}
|
||||
|
||||
static std::string dwarfTag(const std::string& kind) {
|
||||
if (kind == "function") return "DW_TAG_subprogram";
|
||||
if (kind == "struct") return "DW_TAG_structure_type";
|
||||
return "DW_TAG_variable";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
74
editor/src/FFIGlueDispatcher.h
Normal file
74
editor/src/FFIGlueDispatcher.h
Normal file
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
#include "ABIBoundaryExtractor.h"
|
||||
#include "CHeaderEmitter.h"
|
||||
#include "RustPythonBindingEmitter.h"
|
||||
#include "GoCppBindingEmitter.h"
|
||||
#include "RustGoBindingEmitter.h"
|
||||
#include "CppJSBindingEmitter.h"
|
||||
#include "DWARFBoundaryAnnotator.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
class FFIGlueDispatcher {
|
||||
public:
|
||||
// Dispatch a single node to the correct emitter.
|
||||
// Returns: {rustCode, secondaryCode, emitter}
|
||||
static nlohmann::json dispatch(const ABIBoundaryNode& node) {
|
||||
std::string from = lower(node.fromLanguage);
|
||||
std::string to = lower(node.toLanguage);
|
||||
|
||||
if (from == "python" && to == "rust") {
|
||||
auto b = RustPythonBindingEmitter::emit(node);
|
||||
return {{"rustCode", b.rustCode}, {"secondaryCode", b.pythonCode},
|
||||
{"emitter", "RustPython"}};
|
||||
}
|
||||
if (from == "go" && to == "c++") {
|
||||
auto b = GoCppBindingEmitter::emit(node);
|
||||
return {{"rustCode", b.cppCode}, {"secondaryCode", b.goCode},
|
||||
{"emitter", "GoCpp"}};
|
||||
}
|
||||
if (from == "go" && to == "rust") {
|
||||
auto b = RustGoBindingEmitter::emit(node);
|
||||
return {{"rustCode", b.rustCode}, {"secondaryCode", b.goCode},
|
||||
{"emitter", "RustGo"}};
|
||||
}
|
||||
if ((from == "typescript" || from == "javascript") && to == "c++") {
|
||||
auto b = CppJSBindingEmitter::emit(node);
|
||||
return {{"rustCode", b.cppCode}, {"secondaryCode", b.jsCode},
|
||||
{"emitter", "CppJS"}};
|
||||
}
|
||||
// fallback
|
||||
return {{"rustCode", ""}, {"secondaryCode", ""}, {"emitter", "unknown"}};
|
||||
}
|
||||
|
||||
// Orchestrate all nodes.
|
||||
// Returns: {bindings: [...], c_header: "...", dwarf_annotations: [...]}
|
||||
static nlohmann::json generateAll(const std::vector<ABIBoundaryNode>& nodes,
|
||||
const std::string& projectName)
|
||||
{
|
||||
nlohmann::json bindings = nlohmann::json::array();
|
||||
for (const auto& node : nodes) {
|
||||
bindings.push_back(dispatch(node));
|
||||
}
|
||||
|
||||
std::string header = CHeaderEmitter::emit(nodes, projectName);
|
||||
nlohmann::json dwarf = DWARFBoundaryAnnotator::annotate(nodes);
|
||||
|
||||
return {{"bindings", bindings},
|
||||
{"c_header", header},
|
||||
{"dwarf_annotations", dwarf}};
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string lower(std::string s) {
|
||||
std::transform(s.begin(), s.end(), s.begin(),
|
||||
[](unsigned char c){ return std::tolower(c); });
|
||||
return s;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
181
editor/src/GoCppBindingEmitter.h
Normal file
181
editor/src/GoCppBindingEmitter.h
Normal file
@@ -0,0 +1,181 @@
|
||||
#pragma once
|
||||
#include "ABIBoundaryExtractor.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct GoCppBinding {
|
||||
std::string cppCode; // C++ source snippet
|
||||
std::string goCode; // Go source snippet
|
||||
};
|
||||
|
||||
class GoCppBindingEmitter {
|
||||
public:
|
||||
static GoCppBinding emit(const ABIBoundaryNode& node) {
|
||||
if (node.kind == "struct") return emitStruct(node);
|
||||
return emitFunction(node);
|
||||
}
|
||||
|
||||
private:
|
||||
static GoCppBinding emitFunction(const ABIBoundaryNode& node) {
|
||||
const auto& sig = node.signature;
|
||||
std::string retRaw;
|
||||
if (sig.contains("returnType") && sig["returnType"].is_string())
|
||||
retRaw = sig["returnType"].get<std::string>();
|
||||
|
||||
// --- C++ side ---
|
||||
std::ostringstream cpp;
|
||||
cpp << "// FFI: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
cpp << "extern \"C\" {\n";
|
||||
cpp << toCppType(retRaw) << " " << node.name << "(";
|
||||
|
||||
bool hasParams = false;
|
||||
if (sig.contains("params") && sig["params"].is_array()) {
|
||||
bool first = true;
|
||||
for (const auto& param : sig["params"]) {
|
||||
if (!first) cpp << ", ";
|
||||
first = false;
|
||||
std::string pt = param.contains("type") ? param["type"].get<std::string>() : "void*";
|
||||
std::string pn = param.contains("name") ? param["name"].get<std::string>() : "arg";
|
||||
cpp << toCppType(pt) << " " << pn;
|
||||
hasParams = true;
|
||||
}
|
||||
}
|
||||
cpp << ");\n}\n";
|
||||
|
||||
// --- Go side ---
|
||||
std::ostringstream go;
|
||||
go << "// FFI: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
go << "/*\n#include \"" << node.toComponent << "_abi.h\"\n*/\n";
|
||||
go << "import \"C\"\n";
|
||||
go << "import \"unsafe\"\n\n";
|
||||
|
||||
bool voidRet = retRaw.empty() || retRaw == "void";
|
||||
std::string goRetType = voidRet ? "" : toGoType(retRaw);
|
||||
std::string goFuncName = toPascalCase(node.name);
|
||||
|
||||
go << "func " << goFuncName << "(";
|
||||
bool firstGo = true;
|
||||
std::vector<std::pair<std::string,std::string>> goParams;
|
||||
if (sig.contains("params") && sig["params"].is_array()) {
|
||||
for (const auto& param : sig["params"]) {
|
||||
std::string pn = param.contains("name") ? param["name"].get<std::string>() : "arg";
|
||||
std::string pt = param.contains("type") ? param["type"].get<std::string>() : "void*";
|
||||
goParams.push_back({pn, pt});
|
||||
if (!firstGo) go << ", ";
|
||||
firstGo = false;
|
||||
go << pn << " " << toGoType(pt);
|
||||
}
|
||||
}
|
||||
if (!goRetType.empty()) go << ") " << goRetType << " {\n";
|
||||
else go << ") {\n";
|
||||
|
||||
// body: call C function
|
||||
if (!voidRet) go << " return " << goRetType << "(C." << node.name << "(";
|
||||
else go << " C." << node.name << "(";
|
||||
|
||||
bool firstCall = true;
|
||||
for (auto& [pn, pt] : goParams) {
|
||||
if (!firstCall) go << ", ";
|
||||
firstCall = false;
|
||||
go << toCGoType(pt) << "(" << pn << ")";
|
||||
}
|
||||
if (!voidRet) go << "))\n";
|
||||
else go << ")\n";
|
||||
|
||||
go << "}\n";
|
||||
|
||||
// suppress unused import warning
|
||||
go << "var _ = unsafe.Pointer(nil)\n";
|
||||
|
||||
return {cpp.str(), go.str()};
|
||||
}
|
||||
|
||||
static GoCppBinding emitStruct(const ABIBoundaryNode& node) {
|
||||
const auto& sig = node.signature;
|
||||
|
||||
// --- C++ side ---
|
||||
std::ostringstream cpp;
|
||||
cpp << "// FFI struct: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
cpp << "extern \"C\" {\n";
|
||||
cpp << "struct " << node.name << " {\n";
|
||||
if (sig.contains("fields") && sig["fields"].is_array()) {
|
||||
for (const auto& field : sig["fields"]) {
|
||||
std::string fn = field.contains("name") ? field["name"].get<std::string>() : "field";
|
||||
std::string ft = field.contains("type") ? field["type"].get<std::string>() : "int";
|
||||
cpp << " " << toCppType(ft) << " " << fn << ";\n";
|
||||
}
|
||||
}
|
||||
cpp << "};\n}\n";
|
||||
|
||||
// --- Go side ---
|
||||
std::ostringstream go;
|
||||
go << "// FFI struct: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
go << "/*\n#include \"" << node.toComponent << "_abi.h\"\n*/\n";
|
||||
go << "import \"C\"\n\n";
|
||||
go << "type " << node.name << " = C.struct_" << node.name << "\n";
|
||||
|
||||
return {cpp.str(), go.str()};
|
||||
}
|
||||
|
||||
static std::string toCppType(const std::string& t) {
|
||||
if (t == "void") return "void";
|
||||
if (t == "i32" || t == "int32_t" || t == "int") return "int";
|
||||
if (t == "u32" || t == "uint32_t") return "unsigned int";
|
||||
if (t == "i64" || t == "int64_t") return "int64_t";
|
||||
if (t == "u64" || t == "uint64_t") return "uint64_t";
|
||||
if (t == "usize" || t == "size_t") return "size_t";
|
||||
if (t == "f32" || t == "float") return "float";
|
||||
if (t == "f64" || t == "double") return "double";
|
||||
if (t.find('*') != std::string::npos) return t;
|
||||
return "void*";
|
||||
}
|
||||
|
||||
static std::string toGoType(const std::string& t) {
|
||||
if (t == "void") return "";
|
||||
if (t == "i32" || t == "int32_t" || t == "int") return "int32";
|
||||
if (t == "u32" || t == "uint32_t") return "uint32";
|
||||
if (t == "i64" || t == "int64_t") return "int64";
|
||||
if (t == "u64" || t == "uint64_t") return "uint64";
|
||||
if (t == "usize" || t == "size_t") return "uint";
|
||||
if (t == "f32" || t == "float") return "float32";
|
||||
if (t == "f64" || t == "double") return "float64";
|
||||
if (t.find('*') != std::string::npos) return "unsafe.Pointer";
|
||||
return "unsafe.Pointer";
|
||||
}
|
||||
|
||||
static std::string toCGoType(const std::string& t) {
|
||||
if (t == "i32" || t == "int32_t" || t == "int") return "C.int";
|
||||
if (t == "u32" || t == "uint32_t") return "C.uint";
|
||||
if (t == "i64" || t == "int64_t") return "C.longlong";
|
||||
if (t == "u64" || t == "uint64_t") return "C.ulonglong";
|
||||
if (t == "usize" || t == "size_t") return "C.size_t";
|
||||
if (t == "f32" || t == "float") return "C.float";
|
||||
if (t == "f64" || t == "double") return "C.double";
|
||||
if (t.find('*') != std::string::npos) return "unsafe.Pointer";
|
||||
return "unsafe.Pointer";
|
||||
}
|
||||
|
||||
// "handle_request" → "HandleRequest"
|
||||
static std::string toPascalCase(const std::string& name) {
|
||||
std::string result;
|
||||
bool capitalizeNext = true;
|
||||
for (char c : name) {
|
||||
if (c == '_') {
|
||||
capitalizeNext = true;
|
||||
} else if (capitalizeNext) {
|
||||
result += static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||
capitalizeNext = false;
|
||||
} else {
|
||||
result += c;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
82
editor/src/LSPProxyServer.h
Normal file
82
editor/src/LSPProxyServer.h
Normal file
@@ -0,0 +1,82 @@
|
||||
#pragma once
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
class LSPProxyServer {
|
||||
public:
|
||||
using Handler = std::function<nlohmann::json(const nlohmann::json& params)>;
|
||||
|
||||
LSPProxyServer() {
|
||||
handlers_["initialize"] = [](const nlohmann::json&) -> nlohmann::json {
|
||||
return {{"capabilities", {
|
||||
{"textDocumentSync", 1},
|
||||
{"definitionProvider", true},
|
||||
{"hoverProvider", true},
|
||||
{"referencesProvider", true}
|
||||
}}};
|
||||
};
|
||||
auto nullStub = [](const nlohmann::json&) -> nlohmann::json { return nullptr; };
|
||||
auto emptyStub = [](const nlohmann::json&) -> nlohmann::json { return nlohmann::json::array(); };
|
||||
handlers_["textDocument/definition"] = nullStub;
|
||||
handlers_["textDocument/hover"] = nullStub;
|
||||
handlers_["textDocument/references"] = emptyStub;
|
||||
handlers_["shutdown"] = nullStub;
|
||||
// Notifications (no-op)
|
||||
handlers_["textDocument/didOpen"] = [](const nlohmann::json&){ return nlohmann::json{}; };
|
||||
handlers_["textDocument/didChange"] = [](const nlohmann::json&){ return nlohmann::json{}; };
|
||||
handlers_["textDocument/didClose"] = [](const nlohmann::json&){ return nlohmann::json{}; };
|
||||
handlers_["exit"] = [](const nlohmann::json&){ return nlohmann::json{}; };
|
||||
}
|
||||
|
||||
nlohmann::json handle(const nlohmann::json& message) {
|
||||
if (!message.contains("method"))
|
||||
return makeError(idOf(message), -32600, "Invalid Request");
|
||||
|
||||
std::string method = message["method"].get<std::string>();
|
||||
bool isNotification = !message.contains("id");
|
||||
|
||||
nlohmann::json params = message.contains("params") ? message["params"]
|
||||
: nlohmann::json::object();
|
||||
if (isNotification) {
|
||||
auto it = handlers_.find(method);
|
||||
if (it != handlers_.end()) it->second(params);
|
||||
return nlohmann::json{}; // null — no response for notifications
|
||||
}
|
||||
|
||||
nlohmann::json id = message["id"];
|
||||
auto it = handlers_.find(method);
|
||||
if (it == handlers_.end())
|
||||
return makeError(id, -32601, "Method not found");
|
||||
|
||||
nlohmann::json result = it->second(params);
|
||||
return makeResponse(id, result);
|
||||
}
|
||||
|
||||
void registerHandler(const std::string& method, Handler handler) {
|
||||
handlers_[method] = std::move(handler);
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, Handler> handlers_;
|
||||
|
||||
static nlohmann::json idOf(const nlohmann::json& msg) {
|
||||
return msg.contains("id") ? msg["id"] : nullptr;
|
||||
}
|
||||
|
||||
nlohmann::json makeResponse(const nlohmann::json& id,
|
||||
const nlohmann::json& result) const {
|
||||
return {{"jsonrpc","2.0"}, {"id",id}, {"result",result}};
|
||||
}
|
||||
|
||||
nlohmann::json makeError(const nlohmann::json& id, int code,
|
||||
const std::string& msg) const {
|
||||
return {{"jsonrpc","2.0"}, {"id",id},
|
||||
{"error", {{"code",code}, {"message",msg}}}};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
52
editor/src/LanguageServerRouter.h
Normal file
52
editor/src/LanguageServerRouter.h
Normal file
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
class LanguageServerRouter {
|
||||
public:
|
||||
LanguageServerRouter() {
|
||||
extMap_[".py"] = "Python";
|
||||
extMap_[".rs"] = "Rust";
|
||||
extMap_[".go"] = "Go";
|
||||
extMap_[".ts"] = "TypeScript";
|
||||
extMap_[".js"] = "TypeScript";
|
||||
extMap_[".cpp"] = "C++";
|
||||
extMap_[".cc"] = "C++";
|
||||
extMap_[".cxx"] = "C++";
|
||||
extMap_[".h"] = "C++";
|
||||
extMap_[".hpp"] = "C++";
|
||||
|
||||
langMap_["python"] = "Python";
|
||||
langMap_["rust"] = "Rust";
|
||||
langMap_["go"] = "Go";
|
||||
langMap_["typescript"] = "TypeScript";
|
||||
langMap_["javascript"] = "TypeScript";
|
||||
langMap_["cpp"] = "C++";
|
||||
langMap_["c++"] = "C++";
|
||||
}
|
||||
|
||||
std::string routeByUri(const std::string& uri) const {
|
||||
auto dot = uri.rfind('.');
|
||||
if (dot == std::string::npos) return "unknown";
|
||||
std::string ext = uri.substr(dot);
|
||||
auto it = extMap_.find(ext);
|
||||
if (it == extMap_.end()) return "unknown";
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::string routeByLanguageId(const std::string& languageId) const {
|
||||
std::string lower = languageId;
|
||||
for (auto& c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
auto it = langMap_.find(lower);
|
||||
if (it == langMap_.end()) return "unknown";
|
||||
return it->second;
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, std::string> extMap_;
|
||||
std::map<std::string, std::string> langMap_;
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
@@ -1166,6 +1166,8 @@ private:
|
||||
#include "mcp/RegisterValidationTools.h"
|
||||
#include "mcp/RegisterMetricsTools.h"
|
||||
#include "mcp/RegisterLanguageFitnessTools.h"
|
||||
#include "mcp/RegisterFFIGlueTools.h"
|
||||
#include "mcp/RegisterSymbolIndexTools.h"
|
||||
#include "mcp/RegisterPortingFoundationTools.h"
|
||||
#include "mcp/RegisterRustSemanticTools.h"
|
||||
#include "mcp/RegisterCppRaisingTools.h"
|
||||
|
||||
182
editor/src/RustGoBindingEmitter.h
Normal file
182
editor/src/RustGoBindingEmitter.h
Normal file
@@ -0,0 +1,182 @@
|
||||
#pragma once
|
||||
#include "ABIBoundaryExtractor.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <cctype>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct RustGoBinding {
|
||||
std::string rustCode; // Rust source snippet
|
||||
std::string goCode; // Go source snippet
|
||||
};
|
||||
|
||||
class RustGoBindingEmitter {
|
||||
public:
|
||||
static RustGoBinding emit(const ABIBoundaryNode& node) {
|
||||
if (node.kind == "struct") return emitStruct(node);
|
||||
return emitFunction(node);
|
||||
}
|
||||
|
||||
private:
|
||||
static RustGoBinding emitFunction(const ABIBoundaryNode& node) {
|
||||
const auto& sig = node.signature;
|
||||
std::string retRaw;
|
||||
if (sig.contains("returnType") && sig["returnType"].is_string())
|
||||
retRaw = sig["returnType"].get<std::string>();
|
||||
bool voidRet = retRaw.empty() || retRaw == "void";
|
||||
|
||||
std::vector<std::pair<std::string,std::string>> params;
|
||||
if (sig.contains("params") && sig["params"].is_array()) {
|
||||
for (const auto& p : sig["params"]) {
|
||||
std::string pn = p.contains("name") ? p["name"].get<std::string>() : "arg";
|
||||
std::string pt = p.contains("type") ? p["type"].get<std::string>() : "i32";
|
||||
params.push_back({pn, pt});
|
||||
}
|
||||
}
|
||||
|
||||
std::string libName = node.toComponent;
|
||||
for (char& c : libName) if (c == '-') c = '_';
|
||||
|
||||
// --- Rust side ---
|
||||
std::ostringstream rust;
|
||||
rust << "// Rust FFI export: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
rust << "use std::os::raw::c_void;\n";
|
||||
rust << "#[no_mangle]\n";
|
||||
rust << "pub extern \"C\" fn " << node.name << "(";
|
||||
|
||||
bool first = true;
|
||||
for (auto& [pn, pt] : params) {
|
||||
if (!first) rust << ", ";
|
||||
first = false;
|
||||
rust << pn << ": " << toRustType(pt);
|
||||
}
|
||||
|
||||
if (!voidRet) rust << ") -> " << toRustType(retRaw) << " {\n";
|
||||
else rust << ") {\n";
|
||||
rust << " todo!()\n}\n";
|
||||
|
||||
// --- Go side ---
|
||||
std::ostringstream go;
|
||||
go << "// Go CGo caller: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
go << "/*\n";
|
||||
go << "#cgo LDFLAGS: -L. -l" << libName << "\n";
|
||||
go << "#include \"" << libName << "_abi.h\"\n";
|
||||
go << "*/\n";
|
||||
go << "import \"C\"\n";
|
||||
go << "import \"unsafe\"\n\n";
|
||||
|
||||
std::string goName = toPascalCase(node.name);
|
||||
std::string goRet = voidRet ? "" : toGoType(retRaw);
|
||||
|
||||
go << "func " << goName << "(";
|
||||
bool firstGo = true;
|
||||
std::vector<std::pair<std::string,std::string>> goParams;
|
||||
for (auto& [pn, pt] : params) {
|
||||
if (!firstGo) go << ", ";
|
||||
firstGo = false;
|
||||
go << pn << " " << toGoType(pt);
|
||||
goParams.push_back({pn, pt});
|
||||
}
|
||||
if (!goRet.empty()) go << ") " << goRet << " {\n";
|
||||
else go << ") {\n";
|
||||
|
||||
if (!voidRet) go << " return " << goRet << "(C." << node.name << "(";
|
||||
else go << " C." << node.name << "(";
|
||||
|
||||
bool firstCall = true;
|
||||
for (auto& [pn, pt] : goParams) {
|
||||
if (!firstCall) go << ", ";
|
||||
firstCall = false;
|
||||
go << toCGoType(pt) << "(" << pn << ")";
|
||||
}
|
||||
if (!voidRet) go << "))\n";
|
||||
else go << ")\n";
|
||||
go << "}\n";
|
||||
go << "var _ = unsafe.Pointer(nil)\n";
|
||||
|
||||
return {rust.str(), go.str()};
|
||||
}
|
||||
|
||||
static RustGoBinding emitStruct(const ABIBoundaryNode& node) {
|
||||
const auto& sig = node.signature;
|
||||
std::string libName = node.toComponent;
|
||||
for (char& c : libName) if (c == '-') c = '_';
|
||||
|
||||
// --- Rust side ---
|
||||
std::ostringstream rust;
|
||||
rust << "// Rust FFI struct: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
rust << "#[repr(C)]\n";
|
||||
rust << "pub struct " << node.name << " {\n";
|
||||
if (sig.contains("fields") && sig["fields"].is_array()) {
|
||||
for (const auto& field : sig["fields"]) {
|
||||
std::string fn = field.contains("name") ? field["name"].get<std::string>() : "field";
|
||||
std::string ft = field.contains("type") ? field["type"].get<std::string>() : "i32";
|
||||
rust << " pub " << fn << ": " << toRustType(ft) << ",\n";
|
||||
}
|
||||
}
|
||||
rust << "}\n";
|
||||
|
||||
// --- Go side ---
|
||||
std::ostringstream go;
|
||||
go << "// Go CGo struct: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
go << "/*\n#include \"" << libName << "_abi.h\"\n*/\n";
|
||||
go << "import \"C\"\n\n";
|
||||
go << "type " << node.name << " = C.struct_" << node.name << "\n";
|
||||
|
||||
return {rust.str(), go.str()};
|
||||
}
|
||||
|
||||
static std::string toRustType(const std::string& t) {
|
||||
if (t == "void") return "()";
|
||||
if (t == "i32" || t == "int32_t" || t == "int") return "i32";
|
||||
if (t == "u32" || t == "uint32_t") return "u32";
|
||||
if (t == "i64" || t == "int64_t") return "i64";
|
||||
if (t == "u64" || t == "uint64_t") return "u64";
|
||||
if (t == "usize" || t == "size_t") return "usize";
|
||||
if (t == "f32" || t == "float") return "f32";
|
||||
if (t == "f64" || t == "double") return "f64";
|
||||
if (t.find('*') != std::string::npos) return "*mut c_void";
|
||||
return "c_void";
|
||||
}
|
||||
|
||||
static std::string toGoType(const std::string& t) {
|
||||
if (t == "void") return "";
|
||||
if (t == "i32" || t == "int32_t" || t == "int") return "int32";
|
||||
if (t == "u32" || t == "uint32_t") return "uint32";
|
||||
if (t == "i64" || t == "int64_t") return "int64";
|
||||
if (t == "u64" || t == "uint64_t") return "uint64";
|
||||
if (t == "usize" || t == "size_t") return "uint";
|
||||
if (t == "f32" || t == "float") return "float32";
|
||||
if (t == "f64" || t == "double") return "float64";
|
||||
if (t.find('*') != std::string::npos) return "unsafe.Pointer";
|
||||
return "unsafe.Pointer";
|
||||
}
|
||||
|
||||
static std::string toCGoType(const std::string& t) {
|
||||
if (t == "i32" || t == "int32_t" || t == "int") return "C.int";
|
||||
if (t == "u32" || t == "uint32_t") return "C.uint";
|
||||
if (t == "i64" || t == "int64_t") return "C.longlong";
|
||||
if (t == "u64" || t == "uint64_t") return "C.ulonglong";
|
||||
if (t == "usize" || t == "size_t") return "C.size_t";
|
||||
if (t == "f32" || t == "float") return "C.float";
|
||||
if (t == "f64" || t == "double") return "C.double";
|
||||
if (t.find('*') != std::string::npos) return "unsafe.Pointer";
|
||||
return "unsafe.Pointer";
|
||||
}
|
||||
|
||||
static std::string toPascalCase(const std::string& name) {
|
||||
std::string result;
|
||||
bool cap = true;
|
||||
for (char c : name) {
|
||||
if (c == '_') { cap = true; }
|
||||
else if (cap) { result += static_cast<char>(std::toupper(static_cast<unsigned char>(c))); cap = false; }
|
||||
else { result += c; }
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
142
editor/src/RustPythonBindingEmitter.h
Normal file
142
editor/src/RustPythonBindingEmitter.h
Normal file
@@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
#include "ABIBoundaryExtractor.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct RustPythonBinding {
|
||||
std::string rustCode; // Rust source snippet
|
||||
std::string pythonCode; // Python source snippet
|
||||
};
|
||||
|
||||
class RustPythonBindingEmitter {
|
||||
public:
|
||||
static RustPythonBinding emit(const ABIBoundaryNode& node) {
|
||||
if (node.kind == "struct") return emitStruct(node);
|
||||
return emitFunction(node);
|
||||
}
|
||||
|
||||
private:
|
||||
static RustPythonBinding emitFunction(const ABIBoundaryNode& node) {
|
||||
const auto& sig = node.signature;
|
||||
std::string retTypeRaw;
|
||||
if (sig.contains("returnType") && sig["returnType"].is_string())
|
||||
retTypeRaw = sig["returnType"].get<std::string>();
|
||||
|
||||
// --- Rust ---
|
||||
std::ostringstream rust;
|
||||
rust << "// FFI binding: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
rust << "mod ffi {\n";
|
||||
rust << " use std::os::raw::c_void;\n";
|
||||
rust << " extern \"C\" {\n";
|
||||
rust << " pub fn " << node.name << "(";
|
||||
|
||||
bool hasParams = false;
|
||||
if (sig.contains("params") && sig["params"].is_array()) {
|
||||
bool first = true;
|
||||
for (const auto& param : sig["params"]) {
|
||||
if (!first) rust << ", ";
|
||||
first = false;
|
||||
std::string pname = param.contains("name") ? param["name"].get<std::string>() : "arg";
|
||||
std::string ptype = param.contains("type") ? param["type"].get<std::string>() : "i32";
|
||||
rust << pname << ": " << toRustType(ptype);
|
||||
hasParams = true;
|
||||
}
|
||||
}
|
||||
if (!hasParams) rust << ""; // empty params OK in Rust extern
|
||||
|
||||
std::string rustRet = retTypeRaw.empty() || retTypeRaw == "void" ? "" : " -> " + toRustType(retTypeRaw);
|
||||
rust << ")" << rustRet << ";\n";
|
||||
rust << " }\n";
|
||||
rust << "}\n";
|
||||
|
||||
// --- Python ---
|
||||
std::ostringstream py;
|
||||
py << "import ctypes\n";
|
||||
py << "# lib = ctypes.CDLL(\"./lib" << node.toComponent << ".so\")\n";
|
||||
py << "lib." << node.name << ".argtypes = [";
|
||||
|
||||
bool firstPy = true;
|
||||
if (sig.contains("params") && sig["params"].is_array()) {
|
||||
for (const auto& param : sig["params"]) {
|
||||
if (!firstPy) py << ", ";
|
||||
firstPy = false;
|
||||
std::string ptype = param.contains("type") ? param["type"].get<std::string>() : "i32";
|
||||
py << toCType(ptype);
|
||||
}
|
||||
}
|
||||
py << "]\n";
|
||||
|
||||
std::string pyRet = (retTypeRaw.empty() || retTypeRaw == "void") ? "None" : toCType(retTypeRaw);
|
||||
py << "lib." << node.name << ".restype = " << pyRet << "\n";
|
||||
|
||||
return {rust.str(), py.str()};
|
||||
}
|
||||
|
||||
static RustPythonBinding emitStruct(const ABIBoundaryNode& node) {
|
||||
const auto& sig = node.signature;
|
||||
|
||||
// --- Rust ---
|
||||
std::ostringstream rust;
|
||||
rust << "// FFI struct: " << node.fromComponent << " -> " << node.toComponent << "\n";
|
||||
rust << "#[repr(C)]\n";
|
||||
rust << "pub struct " << node.name << " {\n";
|
||||
|
||||
if (sig.contains("fields") && sig["fields"].is_array()) {
|
||||
for (const auto& field : sig["fields"]) {
|
||||
std::string fname = field.contains("name") ? field["name"].get<std::string>() : "field";
|
||||
std::string ftype = field.contains("type") ? field["type"].get<std::string>() : "i32";
|
||||
rust << " pub " << fname << ": " << toRustType(ftype) << ",\n";
|
||||
}
|
||||
}
|
||||
rust << "}\n";
|
||||
|
||||
// --- Python ---
|
||||
std::ostringstream py;
|
||||
py << "import ctypes\n";
|
||||
py << "class " << node.name << "(ctypes.Structure):\n";
|
||||
py << " _fields_ = [\n";
|
||||
|
||||
if (sig.contains("fields") && sig["fields"].is_array()) {
|
||||
for (const auto& field : sig["fields"]) {
|
||||
std::string fname = field.contains("name") ? field["name"].get<std::string>() : "field";
|
||||
std::string ftype = field.contains("type") ? field["type"].get<std::string>() : "i32";
|
||||
py << " (\"" << fname << "\", " << toCType(ftype) << "),\n";
|
||||
}
|
||||
}
|
||||
py << " ]\n";
|
||||
|
||||
return {rust.str(), py.str()};
|
||||
}
|
||||
|
||||
static std::string toRustType(const std::string& t) {
|
||||
if (t == "void") return "()";
|
||||
if (t == "i32" || t == "int32_t" || t == "int") return "i32";
|
||||
if (t == "u32" || t == "uint32_t") return "u32";
|
||||
if (t == "i64" || t == "int64_t") return "i64";
|
||||
if (t == "u64" || t == "uint64_t") return "u64";
|
||||
if (t == "usize" || t == "size_t") return "usize";
|
||||
if (t == "f32" || t == "float") return "f32";
|
||||
if (t == "f64" || t == "double") return "f64";
|
||||
if (t.find('*') != std::string::npos) return "*mut c_void";
|
||||
return "c_void";
|
||||
}
|
||||
|
||||
static std::string toCType(const std::string& t) {
|
||||
if (t == "void") return "None";
|
||||
if (t == "i32" || t == "int32_t" || t == "int") return "ctypes.c_int32";
|
||||
if (t == "u32" || t == "uint32_t") return "ctypes.c_uint32";
|
||||
if (t == "i64" || t == "int64_t") return "ctypes.c_int64";
|
||||
if (t == "u64" || t == "uint64_t") return "ctypes.c_uint64";
|
||||
if (t == "usize" || t == "size_t") return "ctypes.c_size_t";
|
||||
if (t == "f32" || t == "float") return "ctypes.c_float";
|
||||
if (t == "f64" || t == "double") return "ctypes.c_double";
|
||||
if (t.find('*') != std::string::npos) return "ctypes.c_void_p";
|
||||
return "ctypes.c_void_p";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
61
editor/src/SCIPEmitter.h
Normal file
61
editor/src/SCIPEmitter.h
Normal file
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
#include "ABIBoundaryExtractor.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
class SCIPEmitter {
|
||||
public:
|
||||
// Emit a SCIP-format index for the given boundary nodes.
|
||||
// Returns: {"schemaVersion":"0.1", "documents":[{language, uri, symbols:[...]}, ...]}
|
||||
// Each node appears in both its fromLanguage and toLanguage documents.
|
||||
static nlohmann::json emit(const std::vector<ABIBoundaryNode>& nodes) {
|
||||
// language → document stub
|
||||
struct DocStub {
|
||||
std::string uri;
|
||||
nlohmann::json symbols = nlohmann::json::array();
|
||||
};
|
||||
std::map<std::string, DocStub> docs;
|
||||
|
||||
for (const auto& node : nodes) {
|
||||
// Provider side (toLanguage document)
|
||||
auto& toDoc = docs[node.toLanguage];
|
||||
if (toDoc.uri.empty()) toDoc.uri = "scip://" + node.toComponent;
|
||||
toDoc.symbols.push_back(makeSymbol(node, "provider"));
|
||||
|
||||
// Caller side (fromLanguage document)
|
||||
auto& fromDoc = docs[node.fromLanguage];
|
||||
if (fromDoc.uri.empty()) fromDoc.uri = "scip://" + node.fromComponent;
|
||||
fromDoc.symbols.push_back(makeSymbol(node, "caller"));
|
||||
}
|
||||
|
||||
nlohmann::json docArray = nlohmann::json::array();
|
||||
for (auto& [lang, stub] : docs) {
|
||||
docArray.push_back({
|
||||
{"language", lang},
|
||||
{"uri", stub.uri},
|
||||
{"symbols", stub.symbols}
|
||||
});
|
||||
}
|
||||
|
||||
return {{"schemaVersion", "0.1"}, {"documents", docArray}};
|
||||
}
|
||||
|
||||
private:
|
||||
static nlohmann::json makeSymbol(const ABIBoundaryNode& node,
|
||||
const std::string& role) {
|
||||
return {
|
||||
{"name", node.name},
|
||||
{"kind", node.kind},
|
||||
{"fromComponent", node.fromComponent},
|
||||
{"toComponent", node.toComponent},
|
||||
{"role", role},
|
||||
{"scipSymbol", node.toComponent + "/" + node.name + "."}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
16
editor/src/Sprint273IntegrationSummary.h
Normal file
16
editor/src/Sprint273IntegrationSummary.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct Sprint273IntegrationSummary {
|
||||
int stepsCompleted = 5;
|
||||
int bindingPairsImplemented = 2; // RustPython + GoCpp
|
||||
bool success = true;
|
||||
|
||||
std::string sprintName() const {
|
||||
return "Sprint 273: PolyglotFFIGlueGenerator";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
17
editor/src/Sprint274IntegrationSummary.h
Normal file
17
editor/src/Sprint274IntegrationSummary.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct Sprint274IntegrationSummary {
|
||||
int stepsCompleted = 5;
|
||||
int bindingPairsImplemented = 4; // RustPython, GoCpp, RustGo, CppJS
|
||||
int mcpToolsAdded = 1; // whetstone_generate_ffi_glue
|
||||
bool success = true;
|
||||
|
||||
std::string sprintName() const {
|
||||
return "Sprint 274: More Binding Pairs + DWARF Annotations";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
16
editor/src/Sprint275IntegrationSummary.h
Normal file
16
editor/src/Sprint275IntegrationSummary.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct Sprint275IntegrationSummary {
|
||||
int stepsCompleted = 5;
|
||||
int mcpToolsAdded = 1; // whetstone_emit_symbol_index
|
||||
bool success = true;
|
||||
|
||||
std::string sprintName() const {
|
||||
return "Sprint 275: Cross-Language Symbol Index";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
15
editor/src/Sprint276IntegrationSummary.h
Normal file
15
editor/src/Sprint276IntegrationSummary.h
Normal file
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct Sprint276IntegrationSummary {
|
||||
int stepsCompleted = 5;
|
||||
bool success = true;
|
||||
|
||||
std::string sprintName() const {
|
||||
return "Sprint 276: LSP Proxy Core";
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
33
editor/src/SymbolIndexOrchestrator.h
Normal file
33
editor/src/SymbolIndexOrchestrator.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include "SCIPEmitter.h"
|
||||
#include "CrossLanguageSymbolTable.h"
|
||||
#include "SymbolIndexUpdater.h"
|
||||
#include "ABIBoundaryExtractor.h"
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
class SymbolIndexOrchestrator {
|
||||
public:
|
||||
// Build index from scratch: insert all nodes into table, emit SCIP index.
|
||||
static nlohmann::json orchestrate(const std::vector<ABIBoundaryNode>& nodes,
|
||||
CrossLanguageSymbolTable& table)
|
||||
{
|
||||
for (const auto& node : nodes) table.insert(node);
|
||||
auto scipIndex = SCIPEmitter::emit(nodes);
|
||||
return {{"success", true},
|
||||
{"node_count", (int)nodes.size()},
|
||||
{"scip_index", scipIndex}};
|
||||
}
|
||||
|
||||
// Incremental update: delegate to SymbolIndexUpdater.
|
||||
static UpdateResult update(CrossLanguageSymbolTable& table,
|
||||
const std::vector<ABIBoundaryNode>& oldNodes,
|
||||
const std::vector<ABIBoundaryNode>& newNodes)
|
||||
{
|
||||
return SymbolIndexUpdater::update(table, oldNodes, newNodes);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
50
editor/src/SymbolIndexUpdater.h
Normal file
50
editor/src/SymbolIndexUpdater.h
Normal file
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
#include "CrossLanguageSymbolTable.h"
|
||||
#include "ABIBoundaryExtractor.h"
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace whetstone {
|
||||
|
||||
struct UpdateResult {
|
||||
int added = 0;
|
||||
int removed = 0;
|
||||
int unchanged = 0;
|
||||
};
|
||||
|
||||
class SymbolIndexUpdater {
|
||||
public:
|
||||
// Diff oldNodes vs newNodes; update table in place.
|
||||
// Node identity key: toComponent/name.fromLanguage:toLanguage
|
||||
static UpdateResult update(CrossLanguageSymbolTable& table,
|
||||
const std::vector<ABIBoundaryNode>& oldNodes,
|
||||
const std::vector<ABIBoundaryNode>& newNodes)
|
||||
{
|
||||
std::set<std::string> oldKeys, newKeys;
|
||||
for (auto& n : oldNodes) oldKeys.insert(nodeKey(n));
|
||||
for (auto& n : newNodes) newKeys.insert(nodeKey(n));
|
||||
|
||||
UpdateResult result;
|
||||
for (auto& n : newNodes) {
|
||||
if (oldKeys.count(nodeKey(n))) ++result.unchanged;
|
||||
else ++result.added;
|
||||
}
|
||||
for (auto& n : oldNodes) {
|
||||
if (!newKeys.count(nodeKey(n))) ++result.removed;
|
||||
}
|
||||
|
||||
// Rebuild table from newNodes (clean remove-and-reinsert)
|
||||
table = CrossLanguageSymbolTable{};
|
||||
for (auto& n : newNodes) table.insert(n);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string nodeKey(const ABIBoundaryNode& n) {
|
||||
return n.toComponent + "/" + n.name + "." + n.fromLanguage + ":" + n.toLanguage;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace whetstone
|
||||
75
editor/src/mcp/RegisterFFIGlueTools.h
Normal file
75
editor/src/mcp/RegisterFFIGlueTools.h
Normal file
@@ -0,0 +1,75 @@
|
||||
// Step 1901: MCP wiring for whetstone_generate_ffi_glue
|
||||
//
|
||||
// Registers:
|
||||
// whetstone_generate_ffi_glue
|
||||
//
|
||||
// This file is #included inside the MCPServer class body.
|
||||
|
||||
void registerFFIGlueTools() {
|
||||
tools_.push_back({"whetstone_generate_ffi_glue",
|
||||
"Generate FFI glue code for all cross-language boundaries in a polyglot project. "
|
||||
"Extracts boundary nodes from the AST, dispatches to the correct binding emitter "
|
||||
"(RustPython, GoCpp, RustGo, CppJS) based on language pair, emits a C ABI header, "
|
||||
"and produces DWARF annotations for the DAP orchestrator.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"spec", {{"type", "object"},
|
||||
{"description", "PolyglotProjectSpec as JSON: {projectName, sections:[{componentName,assignedLanguage}], interfaces:[{fromComponent,toComponent}]}"}}},
|
||||
{"ast", {{"type", "object"},
|
||||
{"description", "Component AST map: {components: {compName: {nodes:[{name,kind,exported,...}]}}}"}}},
|
||||
{"project_name", {{"type", "string"},
|
||||
{"description", "Project name used for the C header guard (default: project)"}}}
|
||||
}}, {"required", json::array({"spec", "ast"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_generate_ffi_glue"] =
|
||||
[this](const json& args) -> json {
|
||||
return runGenerateFFIGlue(args);
|
||||
};
|
||||
}
|
||||
|
||||
json runGenerateFFIGlue(const json& args) {
|
||||
if (!args.contains("spec") || !args.contains("ast"))
|
||||
return {{"success", false}, {"error", "spec and ast are required"}};
|
||||
|
||||
// Reconstruct PolyglotProjectSpec from JSON
|
||||
whetstone::PolyglotProjectSpec spec;
|
||||
const auto& specJ = args["spec"];
|
||||
if (specJ.contains("projectName") && specJ["projectName"].is_string())
|
||||
spec.projectName = specJ["projectName"].get<std::string>();
|
||||
|
||||
if (specJ.contains("sections") && specJ["sections"].is_array()) {
|
||||
for (const auto& s : specJ["sections"]) {
|
||||
whetstone::PolyglotSection sec;
|
||||
if (s.contains("componentName")) sec.componentName = s["componentName"].get<std::string>();
|
||||
if (s.contains("assignedLanguage")) sec.assignedLanguage = s["assignedLanguage"].get<std::string>();
|
||||
if (s.contains("explicitLanguage")) sec.explicitLanguage = s["explicitLanguage"].get<std::string>();
|
||||
spec.sections.push_back(sec);
|
||||
}
|
||||
}
|
||||
|
||||
if (specJ.contains("interfaces") && specJ["interfaces"].is_array()) {
|
||||
for (const auto& i : specJ["interfaces"]) {
|
||||
whetstone::PolyglotInterface iface;
|
||||
if (i.contains("fromComponent")) iface.fromComponent = i["fromComponent"].get<std::string>();
|
||||
if (i.contains("toComponent")) iface.toComponent = i["toComponent"].get<std::string>();
|
||||
if (i.contains("description")) iface.description = i["description"].get<std::string>();
|
||||
spec.interfaces.push_back(iface);
|
||||
}
|
||||
}
|
||||
|
||||
const auto& ast = args["ast"];
|
||||
std::string projectName = "project";
|
||||
if (args.contains("project_name") && args["project_name"].is_string())
|
||||
projectName = args["project_name"].get<std::string>();
|
||||
|
||||
// Extract boundary nodes
|
||||
auto nodes = whetstone::ABIBoundaryExtractor::extract(spec, ast);
|
||||
|
||||
// Generate all bindings
|
||||
auto result = whetstone::FFIGlueDispatcher::generateAll(nodes, projectName);
|
||||
|
||||
return {{"success", true},
|
||||
{"node_count", (int)nodes.size()},
|
||||
{"bindings", result["bindings"]},
|
||||
{"c_header", result["c_header"]},
|
||||
{"dwarf_annotations", result["dwarf_annotations"]}};
|
||||
}
|
||||
@@ -155,5 +155,7 @@
|
||||
registerSprint155Tools();
|
||||
registerOnboardingTools();
|
||||
registerLanguageFitnessTools();
|
||||
registerFFIGlueTools();
|
||||
registerSymbolIndexTools();
|
||||
}
|
||||
};
|
||||
|
||||
71
editor/src/mcp/RegisterSymbolIndexTools.h
Normal file
71
editor/src/mcp/RegisterSymbolIndexTools.h
Normal file
@@ -0,0 +1,71 @@
|
||||
// Step 1906: MCP wiring for whetstone_emit_symbol_index
|
||||
//
|
||||
// Registers:
|
||||
// whetstone_emit_symbol_index
|
||||
//
|
||||
// This file is #included inside the MCPServer class body.
|
||||
|
||||
void registerSymbolIndexTools() {
|
||||
tools_.push_back({"whetstone_emit_symbol_index",
|
||||
"Emit a SCIP-format cross-language symbol index from a set of polyglot boundary nodes. "
|
||||
"Inserts nodes into an in-memory CrossLanguageSymbolTable and returns the SCIP index "
|
||||
"with one document per language. Supports incremental update via old/new node diff.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"nodes", {{"type", "array"},
|
||||
{"description", "Array of ABIBoundaryNode JSON objects (from whetstone_generate_ffi_glue output)."}}},
|
||||
{"old_nodes", {{"type", "array"},
|
||||
{"description", "Previous node array for incremental diff (omit for full rebuild)."}}}
|
||||
}}, {"required", json::array({"nodes"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_emit_symbol_index"] =
|
||||
[this](const json& args) -> json {
|
||||
return runEmitSymbolIndex(args);
|
||||
};
|
||||
}
|
||||
|
||||
json runEmitSymbolIndex(const json& args) {
|
||||
if (!args.contains("nodes") || !args["nodes"].is_array())
|
||||
return {{"success", false}, {"error", "nodes array is required"}};
|
||||
|
||||
// Deserialise nodes
|
||||
std::vector<whetstone::ABIBoundaryNode> nodes;
|
||||
for (const auto& j : args["nodes"]) {
|
||||
whetstone::ABIBoundaryNode n;
|
||||
if (j.contains("name")) n.name = j["name"].get<std::string>();
|
||||
if (j.contains("kind")) n.kind = j["kind"].get<std::string>();
|
||||
if (j.contains("fromComponent")) n.fromComponent = j["fromComponent"].get<std::string>();
|
||||
if (j.contains("toComponent")) n.toComponent = j["toComponent"].get<std::string>();
|
||||
if (j.contains("fromLanguage")) n.fromLanguage = j["fromLanguage"].get<std::string>();
|
||||
if (j.contains("toLanguage")) n.toLanguage = j["toLanguage"].get<std::string>();
|
||||
if (j.contains("signature")) n.signature = j["signature"];
|
||||
nodes.push_back(n);
|
||||
}
|
||||
|
||||
// Incremental path
|
||||
if (args.contains("old_nodes") && args["old_nodes"].is_array()) {
|
||||
std::vector<whetstone::ABIBoundaryNode> oldNodes;
|
||||
for (const auto& j : args["old_nodes"]) {
|
||||
whetstone::ABIBoundaryNode n;
|
||||
if (j.contains("name")) n.name = j["name"].get<std::string>();
|
||||
if (j.contains("kind")) n.kind = j["kind"].get<std::string>();
|
||||
if (j.contains("fromComponent")) n.fromComponent = j["fromComponent"].get<std::string>();
|
||||
if (j.contains("toComponent")) n.toComponent = j["toComponent"].get<std::string>();
|
||||
if (j.contains("fromLanguage")) n.fromLanguage = j["fromLanguage"].get<std::string>();
|
||||
if (j.contains("toLanguage")) n.toLanguage = j["toLanguage"].get<std::string>();
|
||||
oldNodes.push_back(n);
|
||||
}
|
||||
whetstone::CrossLanguageSymbolTable table;
|
||||
for (auto& n : oldNodes) table.insert(n);
|
||||
auto diff = whetstone::SymbolIndexOrchestrator::update(table, oldNodes, nodes);
|
||||
auto scip = whetstone::SCIPEmitter::emit(nodes);
|
||||
return {{"success", true}, {"mode", "incremental"},
|
||||
{"added", diff.added}, {"removed", diff.removed},
|
||||
{"unchanged", diff.unchanged}, {"scip_index", scip}};
|
||||
}
|
||||
|
||||
// Full rebuild
|
||||
whetstone::CrossLanguageSymbolTable table;
|
||||
auto result = whetstone::SymbolIndexOrchestrator::orchestrate(nodes, table);
|
||||
result["mode"] = "full";
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user