mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 17:51:07 +00:00
* ci: drop doctest chain pins — the qt-split chain is fully merged logoscore-cli and module-builder masters now contain the chain; the temporary --release-for pins (added so stacked-branch CI could resolve compatible cross-repo revs) default back to latest releases. * codegen: Qt-free outbound — ApiStyle::Lp typed wrappers over the lp_* C ABI Adds a third generator flavor (ApiStyle::Lp, --api-style lp) whose typed dependency wrappers + LogosModules umbrella call the logos-protocol C ABI directly via a new header-only logos::LpClient, instead of LogosAPIClient. This lets a module make outbound typed calls and event subscriptions with NO Qt in its translation units — Qt stays confined to the QRO transport (inside logos-protocol) and the generated plugin glue. - cpp/logos_lp_client.h: header-only logos::LpClient (lazy lp_client_create on a baked origin; invoke / invokeAsync / subscribe; std<->nlohmann JSON; CallError out-param) + RAII logos::LpSubscription (unsubscribes on drop) + json<->std helpers. The C++ analog of rust-sdk PluginProxy. - generator: makeHeaderLp/makeSourceLp emit the Lp wrappers; the Lp umbrella drops the LogosAPI ctor and bakes this module name as the lp_client origin (LogosModules() default-constructible). Qt/Std emission is byte-unchanged (dispatch added at the top of makeHeader/makeSource). Verified: generator builds; generated wrappers + umbrella compile to .o with ONLY cpp-sdk + logos-protocol headers + nlohmann (no Qt); cpp-sdk tests pass. * cdylib: wire the Qt-free typed dependency surface (modules()) into the impl When a cdylib module declares dependencies, the generated exports now include the Lp umbrella (logos_sdk.h) and construct LogosModules() + maybeSetLogosModules on the impl just before onContextReady — so the author can call modules().<dep>... and subscribe to dep events from a Qt-free cdylib. Guarded on module.depends so dependency-less cdylib modules are byte-unchanged. The umbrella + dep wrappers themselves are produced by the --general-only --api-style lp generation; feeding the dep .lidl files into that during the module build is the remaining build-system wiring (module-builder + plugin-qt dep resolution). * cdylib: wire modules() unconditionally (deps come from metadata, not the .lidl) The umbrella wiring was guarded on the .lidl module.depends, but a cdylib module declares its dependencies in metadata.json#dependencies — the .lidl contract.depends is typically empty — so modules() was left null and a typed outbound call segfaulted. Always include the generated logos_sdk.h umbrella and maybeSetLogosModules(impl, new LogosModules()) before onContextReady; the overload is a no-op for context-less impls and the umbrella codegen emits an (empty) logos_sdk.h for every cdylib, so this is safe in all cases. * fix(headers): ship logos_lp_client.h in the include/cpp source-export root A cdylib module's generated dep wrapper includes "logos_lp_client.h" and, transitively, "logos_result.h". The wrapper is compiled with the cpp-sdk source-export include root (include/cpp), so logos_lp_client.h must sit beside logos_result.h there — a quoted include resolves siblings relative to the including file's directory. Previously logos_lp_client.h shipped only at the top-level include/ (the CMake-export layout via cpp/ CMakeLists.txt), so it pulled in include/logos_result.h while the impl's logos_module_context.h pulled include/cpp/logos_result.h. Those are two distinct realpaths under the symlinkJoin, so #pragma once could not dedup them and StdLogosResult was redefined. Install every std header into both roots so a single TU only ever sees one logos_result.h. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cdylib): route logos_module_accept_token into the protocol TokenManager The generated module-impl export stored accepted tokens in a process-local std::map (g_tokens) that nothing ever read, so a cdylib module's OUTBOUND lp_client (modules().<dep>...) never saw the capability_module bootstrap token the host delivers at load. The automatic requestModule flow then ran unauthenticated: capability_module rejected requestModule, no per-target token was issued, and the cross-module call was rejected (returning a default-constructed result, e.g. 0). Forward the token into lp_token_save, which writes the same TokenManager::instance() singleton the cdylib's lp_client reads. The capability/token handshake now completes and typed Qt-free outbound calls return real results. Drop the dead g_tokens map + mutex. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lidl): exclude LogosModuleContext hooks from header-derived contracts --header-to-lidl parses an impl class's public methods. An impl commonly overrides onContextReady() (and could redeclare a context accessor) in its own public section, so the derived LIDL would include onContextReady / modules / modulePath / instanceId / instancePersistencePath. Those are framework plumbing, not API methods — and feeding them to the cdylib backend breaks cdylib-eligibility (e.g. the inherited accessors' Qt-free return-type check), which is exactly what header-first universal modules now hit. Skip the reserved LogosModuleContext names in the parser so both the Qt --from-header path and the cdylib --header-to-lidl path emit clean, API-only contracts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cdylib): support the full std type set in header-derived contracts Routing core universal modules through the cdylib backend surfaced gaps between the cdylib subset and what the std apiStyle handled — a universal module that built under std must also build as a header-first cdylib. - lidl parser: restore the return-shape flags (resultReturn / jsonReturn) from the parsed return TypeExpr, so a header -> .lidl -> cdylib round-trip (the universal path, needed to feed the Qt glue) preserves the semantics the impl-header parser sets from C++ types (StdLogosResult -> result; LogosMap/LogosList -> json). Without this the cdylib codegen/eligibility mis-handled result / map / list returns. - cdylib eligibility + dispatch: `void` is not a lidlBuiltinType, so the parser yields it as a Named "void" (header path uses empty name) — treat both as void in the eligibility check and the dispatch (was relying on lidlTypeToQt=="void", which didn't match Named "void" -> generated an `auto result = <void call>`). - typeSupported: accept `any` (both directions), `void`/`result` (returns), arrays-of-any, and Map ({k:v}/LogosMap) — the Qt-free-via-nlohmann set. Verified: a probe with void / LogosMap / LogosList / StdLogosResult / const returns is cdylib-eligible and dispatches correctly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lidl): carry method/event descriptions across the .lidl round-trip Header-first universal modules go header -> .lidl -> cdylib backend. The impl-header parser captures /// and /** */ doc comments into method/event descriptions, but the .lidl serializer emitted only the signature, so the descriptions were dropped — introspection (lm methods / --json, getMethods) then showed no docs (regressing the wrap-external-lib + tutorial doctests). Serialize each method/event's description as a trailing `description "..."` clause (escaped for the string literal; the lexer already decodes \\ \" \n \t) and parse it back in parseMethodDef/parseEventDef. Module description now escaped too. Verified: /// docs survive header -> .lidl -> getMethods. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lp): bound-interface wrappers are handles over umbrella-owned state The Lp interface (bind_<iface>) wrapper owned its LpClient + RAII subscriptions BY VALUE, so the idiomatic transient handle — modules().bind_calculator(p).fibonacciAsync(...) modules().bind_calculator(p).onVersionReady(...) — tore the client/subscription down when the temporary died, cancelling the async callback and the event subscription. (Sync calls completed before the temporary's destruction, so they worked; the Qt/std flavor works because its handle is thin over a LogosAPI-owned persistent client.) Make the Lp Bound wrapper a THIN, copyable handle over `State { LpClient client; vector<LpSubscription> subs; }` that the LogosModules umbrella OWNS per provider (std::map<provider, unique_ptr<State>>) for the module's lifetime. bind_<iface>(p) creates/looks up the State and returns a handle to it, so a transient handle's async/event registrations outlive it. Concrete (Static) dep wrappers are unchanged — they're already persistent umbrella members, so by-value ownership is fine there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cdylib): lenient bytes-param decode (string / array / tagged) A universal module's bstr (std::vector<uint8_t>) PARAM arrived empty when the caller sent a plain string rather than the tagged {"_bytes": base64url} form — lidlBytesFromJson only accepted the tagged object, so byteArraySize("12345") and byteArraySize(b"\x01..") both saw 0 bytes (the return direction already worked). The std path was lenient (a QString or QByteArray arg both became bytes). Accept all three forms: a plain JSON string (raw UTF-8 bytes), an array of byte values, and the tagged {"_bytes"} form (base64url). Return direction unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cdylib): a number arg to a bytes param decodes as its decimal text byteArraySize("12345") arrives as a JSON number (the logoscore CLI's type auto-detection turns the string "12345" into int 12345), and the Qt path gives QVariant(int)->QByteArray "12345" (5 bytes). The cdylib bstr decode returned 0 for a number. Treat a JSON number as its decimal text bytes (j.dump()), matching the Qt behaviour, so a bare-number arg to a bytes param round-trips identically. Verified: byteArraySize 12345 -> 5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
237 lines
12 KiB
C++
237 lines
12 KiB
C++
#include "lidl_parser.h"
|
|
#include "lidl_lexer.h"
|
|
|
|
#include <QSet>
|
|
|
|
static const QSet<QString>& lidlBuiltinTypes()
|
|
{
|
|
static const QSet<QString> bt = {
|
|
"tstr", "bstr", "int", "uint", "float64", "bool", "result", "any"
|
|
};
|
|
return bt;
|
|
}
|
|
|
|
class Parser {
|
|
public:
|
|
explicit Parser(const QVector<LidlToken>& tokens) : m_tokens(tokens) {}
|
|
|
|
LidlParseResult parse() {
|
|
LidlParseResult result;
|
|
if (!parseModule(result.module)) {
|
|
result.error = m_error; result.errorLine = m_errorLine; result.errorColumn = m_errorColumn;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
const QVector<LidlToken>& m_tokens;
|
|
int m_pos = 0;
|
|
QString m_error;
|
|
int m_errorLine = 0, m_errorColumn = 0;
|
|
|
|
const LidlToken& current() const { return m_tokens[m_pos < m_tokens.size() ? m_pos : m_tokens.size() - 1]; }
|
|
bool at(LidlToken::Type type) const { return current().type == type; }
|
|
bool consume(LidlToken::Type type) { if (!at(type)) return false; ++m_pos; return true; }
|
|
|
|
// The LIDL keywords (module/type/method/event/version/description/
|
|
// category/depends) are reserved only *structurally* — at the start of
|
|
// a declaration. In a *name* position (a module/type/field/method/
|
|
// event name, a parameter name, or a named-type reference) they are
|
|
// ordinary identifiers. A module with, say, an event parameter named
|
|
// `version` serializes to `event versionReady(version: tstr)`, and the
|
|
// consumer must be able to read that back. So treat any keyword token
|
|
// as an identifier wherever a bare name is expected. The grammar stays
|
|
// unambiguous because every name position is reached only after a
|
|
// structural token has already been consumed.
|
|
bool atName() const {
|
|
switch (current().type) {
|
|
case LidlToken::Ident:
|
|
case LidlToken::Module: case LidlToken::TypeKw: case LidlToken::Method:
|
|
case LidlToken::Event: case LidlToken::Version: case LidlToken::Description:
|
|
case LidlToken::Category: case LidlToken::Depends:
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bool expect(LidlToken::Type type, const QString& context) {
|
|
if (consume(type)) return true;
|
|
error(QString("Expected %1 in %2, got '%3'").arg(tokenTypeName(type), context, current().text));
|
|
return false;
|
|
}
|
|
|
|
void error(const QString& msg) {
|
|
if (!m_error.isEmpty()) return;
|
|
m_error = msg; m_errorLine = current().line; m_errorColumn = current().column;
|
|
}
|
|
|
|
static QString tokenTypeName(LidlToken::Type t) {
|
|
switch (t) {
|
|
case LidlToken::Module: return "'module'"; case LidlToken::TypeKw: return "'type'";
|
|
case LidlToken::Method: return "'method'"; case LidlToken::Event: return "'event'";
|
|
case LidlToken::Version: return "'version'"; case LidlToken::Description: return "'description'";
|
|
case LidlToken::Category: return "'category'"; case LidlToken::Depends: return "'depends'";
|
|
case LidlToken::Ident: return "identifier"; case LidlToken::StringLit: return "string literal";
|
|
case LidlToken::LBrace: return "'{'"; case LidlToken::RBrace: return "'}'";
|
|
case LidlToken::LParen: return "'('"; case LidlToken::RParen: return "')'";
|
|
case LidlToken::LBracket: return "'['"; case LidlToken::RBracket: return "']'";
|
|
case LidlToken::Colon: return "':'"; case LidlToken::Comma: return "','";
|
|
case LidlToken::Arrow: return "'->'"; case LidlToken::Question: return "'?'";
|
|
case LidlToken::Eof: return "end of input"; case LidlToken::Error: return "error";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
bool parseModule(ModuleDecl& mod) {
|
|
if (!expect(LidlToken::Module, "module declaration")) return false;
|
|
if (!atName()) { error("Expected module name"); return false; }
|
|
mod.name = current().text; ++m_pos;
|
|
if (!expect(LidlToken::LBrace, "module declaration")) return false;
|
|
if (!parseModuleBody(mod)) return false;
|
|
if (!expect(LidlToken::RBrace, "module declaration")) return false;
|
|
if (!at(LidlToken::Eof)) { error("Unexpected content after module closing '}'"); return false; }
|
|
return true;
|
|
}
|
|
|
|
bool parseModuleBody(ModuleDecl& mod) {
|
|
while (!at(LidlToken::RBrace) && !at(LidlToken::Eof)) {
|
|
switch (current().type) {
|
|
case LidlToken::Version: case LidlToken::Description: case LidlToken::Category: case LidlToken::Depends:
|
|
if (!parseMetadata(mod)) return false; break;
|
|
case LidlToken::TypeKw: if (!parseTypeDef(mod)) return false; break;
|
|
case LidlToken::Method: if (!parseMethodDef(mod)) return false; break;
|
|
case LidlToken::Event: if (!parseEventDef(mod)) return false; break;
|
|
default: error(QString("Unexpected token '%1' in module body").arg(current().text)); return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool parseMetadata(ModuleDecl& mod) {
|
|
if (at(LidlToken::Version)) { ++m_pos; if (!at(LidlToken::StringLit)) { error("Expected string after 'version'"); return false; } mod.version = current().text; ++m_pos; return true; }
|
|
if (at(LidlToken::Description)) { ++m_pos; if (!at(LidlToken::StringLit)) { error("Expected string after 'description'"); return false; } mod.description = current().text; ++m_pos; return true; }
|
|
if (at(LidlToken::Category)) { ++m_pos; if (!at(LidlToken::StringLit)) { error("Expected string after 'category'"); return false; } mod.category = current().text; ++m_pos; return true; }
|
|
if (at(LidlToken::Depends)) {
|
|
++m_pos;
|
|
if (!expect(LidlToken::LBracket, "depends list")) return false;
|
|
if (!at(LidlToken::RBracket)) {
|
|
if (!atName()) { error("Expected identifier in depends list"); return false; }
|
|
mod.depends.append(current().text); ++m_pos;
|
|
while (consume(LidlToken::Comma)) {
|
|
if (!atName()) { error("Expected identifier after ',' in depends list"); return false; }
|
|
mod.depends.append(current().text); ++m_pos;
|
|
}
|
|
}
|
|
if (!expect(LidlToken::RBracket, "depends list")) return false;
|
|
return true;
|
|
}
|
|
error("Expected metadata keyword"); return false;
|
|
}
|
|
|
|
bool parseTypeDef(ModuleDecl& mod) {
|
|
if (!expect(LidlToken::TypeKw, "type definition")) return false;
|
|
TypeDecl td;
|
|
if (!atName()) { error("Expected type name"); return false; }
|
|
td.name = current().text; ++m_pos;
|
|
if (!expect(LidlToken::LBrace, "type definition")) return false;
|
|
while (!at(LidlToken::RBrace) && !at(LidlToken::Eof)) { FieldDecl fd; if (!parseFieldDef(fd)) return false; td.fields.append(fd); }
|
|
if (!expect(LidlToken::RBrace, "type definition")) return false;
|
|
mod.types.append(td); return true;
|
|
}
|
|
|
|
bool parseFieldDef(FieldDecl& fd) {
|
|
fd.optional = consume(LidlToken::Question);
|
|
if (!atName()) { error("Expected field name"); return false; }
|
|
fd.name = current().text; ++m_pos;
|
|
if (!expect(LidlToken::Colon, "field definition")) return false;
|
|
return parseTypeExpr(fd.type);
|
|
}
|
|
|
|
bool parseMethodDef(ModuleDecl& mod) {
|
|
if (!expect(LidlToken::Method, "method definition")) return false;
|
|
MethodDecl md;
|
|
if (!atName()) { error("Expected method name"); return false; }
|
|
md.name = current().text; ++m_pos;
|
|
if (!expect(LidlToken::LParen, "method parameters")) return false;
|
|
if (!parseParams(md.params)) return false;
|
|
if (!expect(LidlToken::RParen, "method parameters")) return false;
|
|
if (!expect(LidlToken::Arrow, "method return type")) return false;
|
|
if (!parseTypeExpr(md.returnType)) return false;
|
|
// Optional trailing doc: `-> ret description "..."`. Carries the
|
|
// method's doc comment across a .lidl round-trip so introspection
|
|
// (lm / getMethods) still surfaces it.
|
|
if (at(LidlToken::Description)) {
|
|
++m_pos;
|
|
if (!at(LidlToken::StringLit)) { error("Expected string after method 'description'"); return false; }
|
|
md.description = current().text; ++m_pos;
|
|
}
|
|
// Restore the return-shape flags from the parsed type so a .lidl
|
|
// round-trip carries the same semantics the impl-header parser sets
|
|
// (it derives them from C++ types: StdLogosResult -> result,
|
|
// LogosMap/LogosList -> json). Without this, a header-first universal
|
|
// module (header -> .lidl -> cdylib backend) loses the flags and the
|
|
// cdylib codegen/eligibility mis-handles result / map / list returns.
|
|
{
|
|
const TypeExpr& rt = md.returnType;
|
|
md.resultReturn = (rt.kind == TypeExpr::Primitive && rt.name == "result");
|
|
md.jsonReturn =
|
|
rt.kind == TypeExpr::Map
|
|
|| (rt.kind == TypeExpr::Primitive && rt.name == "any")
|
|
|| (rt.kind == TypeExpr::Array && rt.elements.size() == 1
|
|
&& rt.elements[0].kind == TypeExpr::Primitive
|
|
&& rt.elements[0].name == "any");
|
|
}
|
|
mod.methods.append(md); return true;
|
|
}
|
|
|
|
bool parseEventDef(ModuleDecl& mod) {
|
|
if (!expect(LidlToken::Event, "event definition")) return false;
|
|
EventDecl ed;
|
|
if (!atName()) { error("Expected event name"); return false; }
|
|
ed.name = current().text; ++m_pos;
|
|
if (!expect(LidlToken::LParen, "event parameters")) return false;
|
|
if (!parseParams(ed.params)) return false;
|
|
if (!expect(LidlToken::RParen, "event parameters")) return false;
|
|
if (at(LidlToken::Description)) {
|
|
++m_pos;
|
|
if (!at(LidlToken::StringLit)) { error("Expected string after event 'description'"); return false; }
|
|
ed.description = current().text; ++m_pos;
|
|
}
|
|
mod.events.append(ed); return true;
|
|
}
|
|
|
|
bool parseParams(QVector<ParamDecl>& params) {
|
|
if (at(LidlToken::RParen)) return true;
|
|
ParamDecl p; if (!parseParam(p)) return false; params.append(p);
|
|
while (consume(LidlToken::Comma)) { ParamDecl p2; if (!parseParam(p2)) return false; params.append(p2); }
|
|
return true;
|
|
}
|
|
|
|
bool parseParam(ParamDecl& p) {
|
|
if (!atName()) { error("Expected parameter name"); return false; }
|
|
p.name = current().text; ++m_pos;
|
|
if (!expect(LidlToken::Colon, "parameter")) return false;
|
|
return parseTypeExpr(p.type);
|
|
}
|
|
|
|
bool parseTypeExpr(TypeExpr& te) {
|
|
if (consume(LidlToken::Question)) { te.kind = TypeExpr::Optional; te.elements.resize(1); return parseTypeExpr(te.elements[0]); }
|
|
if (consume(LidlToken::LBracket)) { te.kind = TypeExpr::Array; te.elements.resize(1); if (!parseTypeExpr(te.elements[0])) return false; return expect(LidlToken::RBracket, "array type"); }
|
|
if (consume(LidlToken::LBrace)) { te.kind = TypeExpr::Map; te.elements.resize(2); if (!parseTypeExpr(te.elements[0])) return false; if (!expect(LidlToken::Colon, "map type")) return false; if (!parseTypeExpr(te.elements[1])) return false; return expect(LidlToken::RBrace, "map type"); }
|
|
if (atName()) {
|
|
const QString& name = current().text;
|
|
te.kind = lidlBuiltinTypes().contains(name) ? TypeExpr::Primitive : TypeExpr::Named;
|
|
te.name = name; ++m_pos; return true;
|
|
}
|
|
error(QString("Expected type expression, got '%1'").arg(current().text)); return false;
|
|
}
|
|
};
|
|
|
|
LidlParseResult lidlParse(const QString& source) {
|
|
LidlLexResult lexResult = lidlTokenize(source);
|
|
if (lexResult.hasError()) { LidlParseResult pr; pr.error = lexResult.error; pr.errorLine = lexResult.errorLine; pr.errorColumn = lexResult.errorColumn; return pr; }
|
|
Parser parser(lexResult.tokens);
|
|
return parser.parse();
|
|
}
|