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:
Bill
2026-03-01 18:41:32 -07:00
parent 1f1234b28b
commit 94f61dc97c
47 changed files with 4577 additions and 6 deletions

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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"

View 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

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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"]}};
}

View File

@@ -155,5 +155,7 @@
registerSprint155Tools();
registerOnboardingTools();
registerLanguageFitnessTools();
registerFFIGlueTools();
registerSymbolIndexTools();
}
};

View 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;
}