mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-09-02 10:41:15 +00:00
feat(cdylib): generic recursive type contract, no silent admissions
The parser mapped a hand-written list of C++ spellings and fell back to the
opaque primitive `any` for everything else; the gate admits `any`. So an
unrecognised spelling was silently accepted and then
- worked by luck through nlohmann's implicit conversions, or
- threw at call time (dispatch_failed on a tagged-bytes object where the
blanket get<>() wanted numbers), or worst
- emitted a NON-canonical wire value: a vector<vector<vector<uint8_t>>> return
went out as untagged nested number arrays that no consumer decodes as bytes.
Now there is a leaf set, and composition that is generic and recursive.
parser - numbers are 64-bit ONLY: int64_t, uint64_t, double. Narrower
spellings are NOT auto-widened — widening would let the declared C++
type and the published LIDL contract disagree about range. uint8_t
has one meaning: std::vector<uint8_t> = bstr.
- nlohmann::json is named explicitly. It only ever reached `any` via
the fallback, so making the fallback an error without this breaks
test_fullapi_cpp, test_fullapi_proxy and both full_api interface
headers — the cross-language conformance chain.
- std::vector<T> and std::map/unordered_map<std::string,T> recurse
through the same function, so nesting composes to any depth
- anything left becomes TypeExpr::Named carrying the C++ spelling
gate - typeSupported recurses; the message names the offending type and,
for a narrow numeric, the fix:
parameter 'depth' has a type outside the cdylib-supported
(Qt-free) subset (uint32_t — numbers are 64-bit here: use
uint64_t; uint8_t is only meaningful as std::vector<uint8_t>,
i.e. bstr)
emitter - params decode via logos::JsonArg, which converts itself into the
author's parameter type, so no type name is emitted and no
LIDL->C++ mapping table has to stay in sync
- returns and event payloads encode via logos::toJson
- the ~60-line base64/tagged-bytes codec emitted into EVERY module is
gone, as are #111's lidlBytesList* helpers and the gating that
existed only to avoid unused static functions. Modules include
logos-protocol's logos_codec.h instead.
- dropped the LogosMap/LogosList "already json" special case: toJson
of an nlohmann::json is the identity, and inferring it from the LIDL
kind is wrong now that a plain std::map is also Map-kind (it emitted
result.dump() on a std::map and failed to compile).
Compatibility, from a scan of every universal module. cdylib-interface modules
(all the Rust ones) and ui_qml backends never reach this parser. Two modules
need a source edit:
- logos-execution-zone-module: 3 slots spell uint32_t (one scalar, two
vector<uint32_t>) -> uint64_t / vector<uint64_t>. They silently worked as
`any` before.
- logos-libp2p-module: createXpr takes vector<pair<string,string>>, which has
no canonical JSON form. Wants map<string, vector<uint8_t>> ({tstr: bstr}) —
the pair's second element carries raw binary, so a string-pair widening
would be UTF-8-lossy.
Everything else builds unchanged.
Verified end to end, not just as emitted text — a module with bytes at three
nesting depths and a string-keyed map of bytes, driven through logoscore over
the real transport:
[[bstr]] param -> "2|2,2:0:255:255,0👎-1:0|0" (byte-exact, empties kept)
{tstr: bstr} -> "2|a=2:128:1:129|b=0👎-1:0"
[[bstr]] return -> [[{"_bytes":"AP8"},{"_bytes":""}],[]]
{tstr: bstr} ret-> {"a":{"_bytes":"gAE"},"b":{"_bytes":""}}
bad element -> {"code":"dispatch_failed","message":"expected integer at
arg1[0], got string"}
Generator probes on real headers: libp2p and lez_core are both rejected by name
with the fix in the message; test_fullapi_cpp keeps echoAny(v: any) -> any.
Tests: 172/172. Re-pin logos-protocol to master once its PR lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
02d64fbc0e
commit
4778ced7fa
@@ -44,53 +44,44 @@ static TypeExpr cppTypeToLidl(const QString& raw)
|
||||
|
||||
// Primitives
|
||||
if (t == "bool") return { TypeExpr::Primitive, "bool", {} };
|
||||
if (t == "void") return { TypeExpr::Primitive, "void", {} };
|
||||
|
||||
// Numbers are 64-bit ONLY: int64_t, uint64_t, double. Narrower spellings are
|
||||
// NOT auto-widened — a uint32_t parameter is a build error telling the author
|
||||
// to write uint64_t, rather than a silent widening that makes the declared
|
||||
// C++ type and the published LIDL contract disagree about range. uint8_t has
|
||||
// exactly one meaning in this contract, and it is std::vector<uint8_t> =
|
||||
// bstr, handled below.
|
||||
if (t == "int64_t") return { TypeExpr::Primitive, "int", {} };
|
||||
if (t == "uint64_t") return { TypeExpr::Primitive, "uint", {} };
|
||||
if (t == "double") return { TypeExpr::Primitive, "float64", {} };
|
||||
if (t == "void") return { TypeExpr::Primitive, "void", {} };
|
||||
|
||||
// std::string
|
||||
if (t == "std::string")
|
||||
return { TypeExpr::Primitive, "tstr", {} };
|
||||
|
||||
// std::vector<T>
|
||||
// std::vector<T> — recursive: the element is parsed by the same function, so
|
||||
// [T] composes to any depth ([[bstr]], [{tstr: [int]}], …) without this
|
||||
// table having to enumerate the combinations. std::vector<uint8_t> is the
|
||||
// one exception: it IS `bstr`, not an array of uint.
|
||||
static QRegularExpression vecRe("^std::vector\\s*<\\s*(.+)\\s*>$");
|
||||
QRegularExpressionMatch m = vecRe.match(t);
|
||||
if (m.hasMatch()) {
|
||||
QString inner = m.captured(1).trimmed();
|
||||
if (inner == "std::string") {
|
||||
TypeExpr elem = { TypeExpr::Primitive, "tstr", {} };
|
||||
return { TypeExpr::Array, "", { elem } };
|
||||
}
|
||||
if (inner == "uint8_t") {
|
||||
const QString inner = m.captured(1).trimmed();
|
||||
if (inner == "uint8_t")
|
||||
return { TypeExpr::Primitive, "bstr", {} };
|
||||
}
|
||||
// std::vector<std::vector<uint8_t>> — an array of byte strings. Spelled
|
||||
// out so it lands on `[bstr]` rather than the opaque `any` fallback
|
||||
// below, which would emit a bare QVariant into the Qt-free TU. As
|
||||
// `[bstr]` it goes through the cdylib list codec
|
||||
// (lidlBytesListFromJson / lidlBytesListToJson), so each element keeps
|
||||
// the canonical tagged {"_bytes": base64url} form on the wire.
|
||||
if (inner == "std::vector<uint8_t>") {
|
||||
TypeExpr elem = { TypeExpr::Primitive, "bstr", {} };
|
||||
return { TypeExpr::Array, "", { elem } };
|
||||
}
|
||||
if (inner == "int64_t") {
|
||||
TypeExpr elem = { TypeExpr::Primitive, "int", {} };
|
||||
return { TypeExpr::Array, "", { elem } };
|
||||
}
|
||||
if (inner == "uint64_t") {
|
||||
TypeExpr elem = { TypeExpr::Primitive, "uint", {} };
|
||||
return { TypeExpr::Array, "", { elem } };
|
||||
}
|
||||
if (inner == "double") {
|
||||
TypeExpr elem = { TypeExpr::Primitive, "float64", {} };
|
||||
return { TypeExpr::Array, "", { elem } };
|
||||
}
|
||||
if (inner == "bool") {
|
||||
TypeExpr elem = { TypeExpr::Primitive, "bool", {} };
|
||||
return { TypeExpr::Array, "", { elem } };
|
||||
}
|
||||
return { TypeExpr::Array, "", { cppTypeToLidl(inner) } };
|
||||
}
|
||||
|
||||
// std::map / std::unordered_map<std::string, T> — recursive on the value.
|
||||
// Only string-keyed maps are representable ({tstr: T}); any other key type
|
||||
// falls through to the unsupported marker below.
|
||||
static QRegularExpression mapRe(
|
||||
"^std::(?:unordered_)?map\\s*<\\s*std::string\\s*,\\s*(.+)\\s*>$");
|
||||
QRegularExpressionMatch mm = mapRe.match(t);
|
||||
if (mm.hasMatch()) {
|
||||
return { TypeExpr::Map, "",
|
||||
{ { TypeExpr::Primitive, "tstr", {} }, cppTypeToLidl(mm.captured(1).trimmed()) } };
|
||||
}
|
||||
|
||||
// Qt collection types — pass through directly (non-std-convertible)
|
||||
@@ -108,13 +99,34 @@ static TypeExpr cppTypeToLidl(const QString& raw)
|
||||
if (t == "LogosList")
|
||||
return { TypeExpr::Array, "", { {TypeExpr::Primitive, "any", {}} } };
|
||||
|
||||
// The alias spelled out. LogosMap/LogosList ARE nlohmann::json, and modules
|
||||
// do write the underlying name (test_fullapi_cpp's echoAny, the full_api
|
||||
// interface headers). It only survived via the opaque fallback below, so it
|
||||
// has to be named explicitly now that the fallback is an error.
|
||||
if (t == "nlohmann::json" || t == "json")
|
||||
return { TypeExpr::Primitive, "any", {} };
|
||||
|
||||
// StdLogosResult — pure C++ result type for universal impls. The generator
|
||||
// emits a StdLogosResult→Qt LogosResult conversion in the glue layer.
|
||||
if (t == "StdLogosResult")
|
||||
return { TypeExpr::Primitive, "result", {} };
|
||||
|
||||
// Fallback: treat as opaque
|
||||
return { TypeExpr::Primitive, "any", {} };
|
||||
// Anything else is UNSUPPORTED, and says so by name.
|
||||
//
|
||||
// This used to return the opaque primitive `any`, which the cdylib gate
|
||||
// admits — so an unrecognised spelling was silently accepted and then either
|
||||
// worked by luck through nlohmann's implicit conversions, threw at call time,
|
||||
// or (worst) emitted a non-canonical wire value: a
|
||||
// std::vector<std::vector<std::vector<uint8_t>>> return went out as untagged
|
||||
// nested number arrays that no consumer decodes as bytes.
|
||||
//
|
||||
// `Named` carries the offending C++ spelling, and no backend accepts a Named
|
||||
// type, so the module fails to BUILD with the parameter and the type in the
|
||||
// message. Compatible spellings are enumerated above; the composition rule
|
||||
// (vector<T>, map<string,T>) is recursive, so this fires only for types that
|
||||
// genuinely have no canonical JSON form (std::pair, std::set, std::optional,
|
||||
// custom structs, pointers, Qt types in a Qt-free module).
|
||||
return { TypeExpr::Named, t.toStdString(), {} };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user