diff --git a/cpp-generator/experimental/impl_header_parser.cpp b/cpp-generator/experimental/impl_header_parser.cpp index 9f961e9..49fded4 100644 --- a/cpp-generator/experimental/impl_header_parser.cpp +++ b/cpp-generator/experimental/impl_header_parser.cpp @@ -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 = + // 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 + // std::vector — 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 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> — 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") { - 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 — 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>> 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, map) 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(), {} }; } // --------------------------------------------------------------------------- diff --git a/cpp-generator/experimental/lidl_gen_cdylib.cpp b/cpp-generator/experimental/lidl_gen_cdylib.cpp index fe85431..c9316ed 100644 --- a/cpp-generator/experimental/lidl_gen_cdylib.cpp +++ b/cpp-generator/experimental/lidl_gen_cdylib.cpp @@ -1,6 +1,7 @@ #include "lidl_gen_cdylib.h" #include "lidl_emit_common.h" +#include #include QString lidlToPascalCase(const QString& name); @@ -27,51 +28,67 @@ bool typeSupported(const TypeExpr& te, bool isReturn) return true; return false; } - if (te.kind == TypeExpr::Array && te.elements.size() == 1) { - const TypeExpr& e = te.elements[0]; - // `bstr` elements are the one array kind that cannot ride nlohmann's - // blanket get<>(): each element is the tagged {"_bytes": base64url} - // OBJECT, not a number array, so it needs the emitted per-element - // codec (lidlBytesListFromJson / lidlBytesListToJson) below. - return e.kind == TypeExpr::Primitive - && (e.name == "tstr" || e.name == "int" || e.name == "uint" - || e.name == "float64" || e.name == "bool" || e.name == "any" - || e.name == "bstr"); + // Composition is GENERIC and recursive: [T] and {tstr: T} are supported for + // any supported T, at any depth. logos::toJson/fromJson (logos_codec.h) does + // the matching recursion at runtime, so [[bstr]], {tstr: [bstr]} and bytes + // nested inside a map all encode canonically without this function — or the + // emitter — enumerating the combinations. + if (te.kind == TypeExpr::Array && te.elements.size() == 1) + return typeSupported(te.elements[0], /*isReturn=*/false); + + // Only string-keyed maps are representable, matching {tstr: T}. + if (te.kind == TypeExpr::Map && te.elements.size() == 2) { + const TypeExpr& k = te.elements[0]; + if (!(k.kind == TypeExpr::Primitive && k.name == "tstr")) + return false; + return typeSupported(te.elements[1], /*isReturn=*/false); } - // Maps ({k: v}, i.e. LogosMap) round-trip through nlohmann too. - if (te.kind == TypeExpr::Map) - return true; return false; } -// `[bstr]` — an array of byte strings (std::vector>). -// Every place scalar `bstr` needs the tagged-bytes codec, this needs the list -// form of it, so the check lives in one place. -bool isBytesArray(const TypeExpr& te) +// The C++ spelling the parser could not map, for the rejection message. Empty +// for anything else. +QString unsupportedSpelling(const TypeExpr& te) { - return te.kind == TypeExpr::Array && te.elements.size() == 1 - && te.elements[0].kind == TypeExpr::Primitive - && te.elements[0].name == "bstr"; + if (te.kind == TypeExpr::Named) + return qs(te.name); + if (te.kind == TypeExpr::Array && te.elements.size() == 1) + return unsupportedSpelling(te.elements[0]); + if (te.kind == TypeExpr::Map && te.elements.size() == 2) { + const QString v = unsupportedSpelling(te.elements[1]); + return v.isEmpty() ? unsupportedSpelling(te.elements[0]) : v; + } + return QString(); } -// True when the module mentions `[bstr]` anywhere the emitted TU has to encode -// or decode it — method params, method returns, or event payloads. Gates the -// list codec so modules that never use it don't carry an unused static function -// (the same reason hasBytesEventParam() gates the scalar encoder). -bool usesBytesArray(const ModuleDecl& module) +// " (std::pair<...>)" when the parser could not map a spelling, else empty. +// The author needs the offending C++ type, not just the parameter name — and for +// a narrow numeric spelling, the fix, since that is the likeliest rejection and +// the answer is always the same: numbers in this contract are 64-bit. +QString spellingNote(const TypeExpr& te) { - for (const MethodDecl& md : module.methods) { - if (isBytesArray(md.returnType)) - return true; - for (const ParamDecl& pd : md.params) - if (isBytesArray(pd.type)) - return true; - } - for (const EventDecl& ed : module.events) - for (const ParamDecl& pd : ed.params) - if (isBytesArray(pd.type)) - return true; - return false; + const QString sp = unsupportedSpelling(te); + if (sp.isEmpty()) + return QString(); + + static const QSet kNarrowSigned = { + "int", "signed", "signed int", "short", "short int", "signed short", + "long", "long int", "long long", "long long int", "ssize_t", "ptrdiff_t", + "int8_t", "int16_t", "int32_t", "intmax_t", "intptr_t", + }; + static const QSet kNarrowUnsigned = { + "unsigned", "unsigned int", "unsigned short", "unsigned long", + "unsigned long long", "size_t", "uint8_t", "uint16_t", "uint32_t", + "uintmax_t", "uintptr_t", + }; + if (kNarrowSigned.contains(sp)) + return QString(" (%1 — numbers are 64-bit here: use int64_t)").arg(sp); + if (kNarrowUnsigned.contains(sp)) + return QString(" (%1 — numbers are 64-bit here: use uint64_t; uint8_t is " + "only meaningful as std::vector, i.e. bstr)").arg(sp); + if (sp == "float" || sp == "long double") + return QString(" (%1 — the only floating type is double)").arg(sp); + return QString(" (%1)").arg(sp); } // Qt-free spelling of a LIDL type (defined below). Forward-declared so the @@ -79,34 +96,6 @@ bool usesBytesArray(const ModuleDecl& module) // aliases instead of Qt containers in this Qt-free TU. QString lidlTypeToStdCdylib(const TypeExpr& te); -// json arg expression -> std-typed C++ expression -QString jsonArgToStd(const TypeExpr& te, const QString& expr) -{ - if (te.kind == TypeExpr::Primitive) { - if (te.name == "tstr") return expr + ".get()"; - if (te.name == "bstr") return "lidlBytesFromJson(" + expr + ")"; - if (te.name == "int") return expr + ".get()"; - if (te.name == "uint") return expr + ".get()"; - if (te.name == "float64") return expr + ".get()"; - if (te.name == "bool") return expr + ".get()"; - } - if (te.kind == TypeExpr::Array && te.elements.size() == 1) { - // `[bstr]` cannot go through get>>(): - // its elements arrive as tagged {"_bytes": …} objects, which nlohmann - // would refuse (and a raw number-array element would silently bypass - // the base64 decode). Route it through the per-element codec instead. - if (isBytesArray(te)) - return "lidlBytesListFromJson(" + expr + ")"; - // Qt-free spelling: `[any]` must decode as LogosList, NOT QVariantList - // (lidlTypeToStd's Qt fallback), which is undeclared in this TU. Typed - // scalar arrays ([tstr]/[int]/…) are unaffected — Cdylib defers to the - // same std spelling for them. - const QString inner = lidlTypeToStdCdylib(te); - return expr + ".get<" + inner + ">()"; - } - return expr; -} - // std-typed return variable -> json expression QString stdReturnToJson(const MethodDecl& md, const QString& var) { @@ -116,18 +105,14 @@ QString stdReturnToJson(const MethodDecl& md, const QString& var) // (same shape logos_json_convert emits for Qt LogosResult). return "lidlResultToJson(" + var + ")"; } - if (md.jsonReturn) { - return var; // LogosMap / LogosList are nlohmann::json already - } - if (te.kind == TypeExpr::Primitive) { - if (te.name == "bstr") return "lidlBytesToJson(" + var + ")"; - return "nlohmann::json(" + var + ")"; - } - // `[bstr]`: nlohmann::json(std::vector>) would emit - // nested number arrays, which no consumer decodes as bytes. Tag each element. - if (isBytesArray(te)) - return "lidlBytesListToJson(" + var + ")"; - return "nlohmann::json(" + var + ")"; + // No LogosMap/LogosList special case: logos::toJson of an nlohmann::json is + // the identity, and inferring "already json" from the LIDL kind is wrong now + // that a plain std::map is also Map-kind — it produced + // `result.dump()` on a std::map and failed to compile. + // Everything else — scalars, bytes, arrays, maps, any nesting — goes through + // the canonical encoder, which tags bytes wherever they occur. + (void)te; + return "logos::toJson(" + var + ")"; } // Qt-free spelling of a LIDL type. lidlTypeToStd() falls back to Qt containers @@ -149,31 +134,6 @@ QString lidlTypeToStdCdylib(const TypeExpr& te) return lidlTypeToStd(te); } -// True when the module declares at least one `bstr` event parameter — the only -// reason the events sidecar needs the bytes encoder. Emitting it unconditionally -// leaves an unused static function (a -Wunused-function warning) in every module -// whose events carry no binary data. -bool hasBytesEventParam(const ModuleDecl& module) -{ - for (const EventDecl& ed : module.events) - for (const ParamDecl& pd : ed.params) - if ((pd.type.kind == TypeExpr::Primitive && pd.type.name == "bstr") - || isBytesArray(pd.type)) - return true; - return false; -} - -// True when an EVENT carries `[bstr]`, so the sidecar needs the list encoder on -// top of the scalar one. Method params/returns are handled in the impl TU. -bool hasBytesArrayEventParam(const ModuleDecl& module) -{ - for (const EventDecl& ed : module.events) - for (const ParamDecl& pd : ed.params) - if (isBytesArray(pd.type)) - return true; - return false; -} - // True when any event parameter is spelled LogosMap / LogosList, so the sidecar // needs for those aliases. bool hasJsonEventParam(const ModuleDecl& module) @@ -187,40 +147,6 @@ bool hasJsonEventParam(const ModuleDecl& module) return false; } -// `withList` additionally emits lidlBytesListToJson for `[bstr]`. Off by -// default so a module that never carries a byte-string array does not gain an -// unused static function. -void emitBytesEncodeHelpers(QTextStream& s, bool withList = false) -{ - s << "// Canonical tagged bytes form {\"_bytes\": base64url} (see logos_protocol.h)\n"; - s << "std::string lidlB64UrlEncode(const std::vector& bytes)\n{\n"; - s << " static const char* alpha = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_\";\n"; - s << " std::string out;\n"; - s << " size_t i = 0;\n"; - s << " while (i + 3 <= bytes.size()) {\n"; - s << " uint32_t n = (uint32_t(bytes[i]) << 16) | (uint32_t(bytes[i+1]) << 8) | uint32_t(bytes[i+2]);\n"; - s << " out += alpha[(n >> 18) & 0x3f]; out += alpha[(n >> 12) & 0x3f];\n"; - s << " out += alpha[(n >> 6) & 0x3f]; out += alpha[n & 0x3f];\n"; - s << " i += 3;\n }\n"; - s << " if (i < bytes.size()) {\n"; - s << " uint32_t n = uint32_t(bytes[i]) << 16;\n"; - s << " if (i + 1 < bytes.size()) n |= uint32_t(bytes[i+1]) << 8;\n"; - s << " out += alpha[(n >> 18) & 0x3f]; out += alpha[(n >> 12) & 0x3f];\n"; - s << " if (i + 1 < bytes.size()) out += alpha[(n >> 6) & 0x3f];\n"; - s << " }\n return out;\n}\n\n"; - - s << "nlohmann::json lidlBytesToJson(const std::vector& bytes)\n{\n"; - s << " return nlohmann::json{{\"_bytes\", lidlB64UrlEncode(bytes)}};\n}\n\n"; - - if (withList) { - s << "nlohmann::json lidlBytesListToJson(const std::vector>& list)\n{\n"; - s << " nlohmann::json out = nlohmann::json::array();\n"; - s << " for (const std::vector& bytes : list)\n"; - s << " out.push_back(lidlBytesToJson(bytes));\n"; - s << " return out;\n}\n\n"; - } -} - void emitInterfaceJson(QTextStream& s, const ModuleDecl& module) { s << "static nlohmann::json lidlInterfaceJson()\n{\n"; @@ -290,8 +216,8 @@ bool lidlCdylibSupported(const ModuleDecl& module, QString* error) if (!typeSupported(pd.type, /*isReturn=*/false)) { if (error) *error = QString("method '%1': parameter '%2' has a type outside the " - "cdylib-supported (Qt-free) subset") - .arg(qs(md.name), qs(pd.name)); + "cdylib-supported (Qt-free) subset%3") + .arg(qs(md.name), qs(pd.name), spellingNote(pd.type)); return false; } } @@ -305,7 +231,8 @@ bool lidlCdylibSupported(const ModuleDecl& module, QString* error) && !typeSupported(md.returnType, /*isReturn=*/true)) { if (error) *error = QString("method '%1': return type outside the cdylib-supported " - "(Qt-free) subset").arg(qs(md.name)); + "(Qt-free) subset%2") + .arg(qs(md.name), spellingNote(md.returnType)); return false; } } @@ -314,8 +241,8 @@ bool lidlCdylibSupported(const ModuleDecl& module, QString* error) if (!typeSupported(pd.type, /*isReturn=*/false)) { if (error) *error = QString("event '%1': parameter '%2' has a type outside the " - "cdylib-supported (Qt-free) subset") - .arg(qs(ed.name), qs(pd.name)); + "cdylib-supported (Qt-free) subset%3") + .arg(qs(ed.name), qs(pd.name), spellingNote(pd.type)); return false; } } @@ -342,6 +269,9 @@ QString lidlMakeModuleImplExports(const ModuleDecl& module, s << "#include \"logos_module_context.h\"\n"; s << "#include \"logos_result.h\"\n"; s << "#include \n"; + // The canonical codec — one implementation of the LIDL <-> JSON mapping, + // replacing the base64/tagged-bytes copy this file used to emit per module. + s << "#include \n"; s << "#include \n"; s << "#include \n"; s << "#include \n"; @@ -373,78 +303,6 @@ QString lidlMakeModuleImplExports(const ModuleDecl& module, s << " if (out) std::memcpy(out, str.data(), str.size() + 1);\n"; s << " return out;\n}\n\n"; - const bool bytesList = usesBytesArray(module); - emitBytesEncodeHelpers(s, bytesList); - - s << "int lidlB64Idx(char ch)\n{\n"; - s << " if (ch >= 'A' && ch <= 'Z') return ch - 'A';\n"; - s << " if (ch >= 'a' && ch <= 'z') return ch - 'a' + 26;\n"; - s << " if (ch >= '0' && ch <= '9') return ch - '0' + 52;\n"; - s << " if (ch == '-') return 62;\n if (ch == '_') return 63;\n return -1;\n}\n\n"; - - s << "std::vector lidlBytesFromJson(const nlohmann::json& j)\n{\n"; - s << " std::vector out;\n"; - s << " // Lenient bytes decode (matches the std path, where a QString or\n"; - s << " // QByteArray arg both became bytes): a caller may send the tagged\n"; - s << " // {\"_bytes\": base64url} form, a plain string (raw UTF-8 bytes), or\n"; - s << " // an array of byte values. Only the tagged form needs base64.\n"; - s << " if (j.is_string()) {\n"; - s << " const std::string s = j.get();\n"; - s << " out.assign(s.begin(), s.end());\n"; - s << " return out;\n"; - s << " }\n"; - s << " if (j.is_number()) {\n"; - s << " // A number arg becomes its decimal text as bytes — matches\n"; - s << " // Qt's QVariant(int)->QByteArray, so a caller (or the\n"; - s << " // logoscore CLI's type auto-detection) passing a bare number\n"; - s << " // to a bytes param behaves the same as the Qt path.\n"; - s << " const std::string s = j.dump();\n"; - s << " out.assign(s.begin(), s.end());\n"; - s << " return out;\n"; - s << " }\n"; - s << " if (j.is_array()) {\n"; - s << " for (const auto& e : j)\n"; - s << " if (e.is_number_integer() || e.is_number_unsigned())\n"; - s << " out.push_back(static_cast(e.get() & 0xff));\n"; - s << " return out;\n"; - s << " }\n"; - s << " if (!j.is_object() || j.size() != 1 || !j.contains(\"_bytes\") || !j[\"_bytes\"].is_string())\n"; - s << " return out;\n"; - s << " const std::string s64 = j[\"_bytes\"].get();\n"; - s << " size_t i = 0;\n"; - s << " while (i + 4 <= s64.size()) {\n"; - s << " int a = lidlB64Idx(s64[i]), b = lidlB64Idx(s64[i+1]), c2 = lidlB64Idx(s64[i+2]), d = lidlB64Idx(s64[i+3]);\n"; - s << " if (a < 0 || b < 0 || c2 < 0 || d < 0) return {};\n"; - s << " uint32_t n = (uint32_t(a) << 18) | (uint32_t(b) << 12) | (uint32_t(c2) << 6) | uint32_t(d);\n"; - s << " out.push_back((n >> 16) & 0xff); out.push_back((n >> 8) & 0xff); out.push_back(n & 0xff);\n"; - s << " i += 4;\n }\n"; - s << " size_t rem = s64.size() - i;\n"; - s << " if (rem == 2 || rem == 3) {\n"; - s << " int a = lidlB64Idx(s64[i]), b = lidlB64Idx(s64[i+1]);\n"; - s << " if (a < 0 || b < 0) return {};\n"; - s << " uint32_t n = (uint32_t(a) << 18) | (uint32_t(b) << 12);\n"; - s << " out.push_back((n >> 16) & 0xff);\n"; - s << " if (rem == 3) {\n"; - s << " int c2 = lidlB64Idx(s64[i+2]);\n"; - s << " if (c2 < 0) return {};\n"; - s << " n |= uint32_t(c2) << 6;\n"; - s << " out.push_back((n >> 8) & 0xff);\n"; - s << " }\n }\n return out;\n}\n\n"; - - if (bytesList) { - s << "std::vector> lidlBytesListFromJson(const nlohmann::json& j)\n{\n"; - s << " std::vector> out;\n"; - s << " // Each element runs through the lenient scalar decode above, so a\n"; - s << " // caller may send tagged {\"_bytes\": base64url} objects, plain\n"; - s << " // strings or number arrays — element by element. A non-array arg\n"; - s << " // yields an empty list rather than throwing, matching the scalar\n"; - s << " // decoder's behaviour on an unexpected shape.\n"; - s << " if (!j.is_array()) return out;\n"; - s << " out.reserve(j.size());\n"; - s << " for (const auto& e : j)\n"; - s << " out.push_back(lidlBytesFromJson(e));\n"; - s << " return out;\n}\n\n"; - } s << "nlohmann::json lidlResultToJson(const StdLogosResult& r)\n{\n"; s << " nlohmann::json obj;\n"; @@ -533,8 +391,11 @@ QString lidlMakeModuleImplExports(const ModuleDecl& module, s << " if (args.size() < " << md.params.size() << ") return nullptr;\n"; QString call = "lidlImpl()." + qs(md.name) + "("; for (int i = 0; i < md.params.size(); ++i) { - call += jsonArgToStd(md.params[i].type, - QString("args.at(%1)").arg(i)); + // logos::JsonArg converts itself into whatever the impl's parameter + // type is, so the author's own spelling (uint32_t, a nested map, …) + // is what gets decoded — no type name is emitted, and there is no + // mapping table to keep in sync. logos_codec.h does the recursion. + call += QString("logos::JsonArg{args.at(%1), \"arg%1\"}").arg(i); if (i + 1 < md.params.size()) call += ", "; } call += ")"; @@ -618,7 +479,8 @@ QString lidlMakeEventsSourceCdylib(const ModuleDecl& module, s << "// nlohmann::json and route through LogosModuleContext::emitEventImpl_\n"; s << "// (the export wrapper forwards to the host's emit callback).\n"; s << "#include \"" << implHeader << "\"\n"; - s << "#include \n\n"; + s << "#include \n"; + s << "#include \n\n"; s << "#include \n"; s << "#include \n"; s << "#include \n"; @@ -628,14 +490,6 @@ QString lidlMakeEventsSourceCdylib(const ModuleDecl& module, s << "#include \n"; s << "\n"; - // Only the modules that actually emit binary event payloads need the bytes - // encoder; emitting it everywhere would leave it unused (and warned about). - if (hasBytesEventParam(module)) { - s << "namespace {\n\n"; - emitBytesEncodeHelpers(s, hasBytesArrayEventParam(module)); - s << "} // namespace\n\n"; - } - for (const EventDecl& ed : module.events) { s << "void " << implClass << "::" << ed.name << "("; for (int i = 0; i < ed.params.size(); ++i) { @@ -652,12 +506,7 @@ QString lidlMakeEventsSourceCdylib(const ModuleDecl& module, s << ")\n{\n"; s << " nlohmann::json args = nlohmann::json::array();\n"; for (const ParamDecl& pd : ed.params) { - if (pd.type.kind == TypeExpr::Primitive && pd.type.name == "bstr") - s << " args.push_back(lidlBytesToJson(" << pd.name << "));\n"; - else if (isBytesArray(pd.type)) - s << " args.push_back(lidlBytesListToJson(" << pd.name << "));\n"; - else - s << " args.push_back(" << pd.name << ");\n"; + s << " args.push_back(logos::toJson(" << pd.name << "));\n"; } s << " emitEventImpl_(\"" << ed.name << "\", &args);\n"; s << "}\n\n"; diff --git a/flake.lock b/flake.lock index 0c463d8..15fe339 100644 --- a/flake.lock +++ b/flake.lock @@ -55,11 +55,11 @@ ] }, "locked": { - "lastModified": 1785065460, - "narHash": "sha256-cp5Un0NBXwNoK/+atBLMl4NwCfDXzy0KMpyUOtK6pcc=", + "lastModified": 1785110737, + "narHash": "sha256-jM5RgMD3KrQKupw3BY/Y2qohmFL+gd+J1j/NjsmLWU8=", "owner": "logos-co", "repo": "logos-protocol", - "rev": "ae2f7e1b5842d62836091c6dc0c903508806456e", + "rev": "8b5b562e9accf3979e24728ecb7dde7dd363f9fb", "type": "github" }, "original": { diff --git a/tests/experimental/test_lidl_gen_cdylib.cpp b/tests/experimental/test_lidl_gen_cdylib.cpp index 07fa290..c86a6c3 100644 --- a/tests/experimental/test_lidl_gen_cdylib.cpp +++ b/tests/experimental/test_lidl_gen_cdylib.cpp @@ -82,26 +82,29 @@ TEST(LidlGenCdylib, BinaryEventPayloadUsesCanonicalBytesEncoding) const QString source = eventsSourceFor(m); - // The real argument is serialized, through the canonical encoder... - EXPECT_TRUE(source.contains("args.push_back(lidlBytesToJson(payload));")); - EXPECT_TRUE(source.contains("std::string lidlB64UrlEncode")); - EXPECT_TRUE(source.contains("nlohmann::json lidlBytesToJson")); + // The real argument is serialized through the canonical encoder, which now + // lives in logos-protocol instead of being emitted into every module. + EXPECT_TRUE(source.contains("args.push_back(logos::toJson(payload));")); + EXPECT_TRUE(source.contains("#include ")); + EXPECT_FALSE(source.contains("lidlB64UrlEncode")); + EXPECT_FALSE(source.contains("lidlBytesToJson")); // ...and the empty tagged value is gone. EXPECT_FALSE(source.contains("nlohmann::json{{\"_bytes\", \"\"}}")); - // The other parameters are still passed straight through. - EXPECT_TRUE(source.contains("args.push_back(messageHash);")); - EXPECT_TRUE(source.contains("args.push_back(timestamp);")); + // Every parameter goes through the same call — no per-type special cases. + EXPECT_TRUE(source.contains("args.push_back(logos::toJson(messageHash));")); + EXPECT_TRUE(source.contains("args.push_back(logos::toJson(timestamp));")); // Bytes are taken by const-ref, matching the author's logos_events: block. EXPECT_TRUE(source.contains("const std::vector& payload")); } -// The encoder is only needed by modules that actually emit binary payloads. -// Emitted unconditionally it is an unused static function in every other -// module's sidecar (-Wunused-function). -TEST(LidlGenCdylib, BytesEncoderOmittedWhenNoEventCarriesBytes) +// No module carries a codec copy any more: the base64/tagged-bytes +// implementation lives once in logos-protocol (logos_codec.h) and modules call +// it. This also retires the gating that existed only to avoid emitting an +// unused static function. +TEST(LidlGenCdylib, NoCodecIsEmittedIntoTheModule) { const ModuleDecl m = moduleWithEvent("fault", { param("code", prim("int")), @@ -113,7 +116,9 @@ TEST(LidlGenCdylib, BytesEncoderOmittedWhenNoEventCarriesBytes) EXPECT_FALSE(source.contains("lidlB64UrlEncode")); EXPECT_FALSE(source.contains("lidlBytesToJson")); - EXPECT_TRUE(source.contains("args.push_back(code);")); + EXPECT_FALSE(source.contains("lidlB64Idx")); + EXPECT_TRUE(source.contains("#include ")); + EXPECT_TRUE(source.contains("args.push_back(logos::toJson(code));")); } // The sidecar is compiled into the module's Qt-free cdylib, so a JSON payload @@ -158,9 +163,7 @@ TEST(LidlGenCdylib, ArrayOfBytesEventParamIsEligibleAndTagsEachElement) // Each element is tagged, not emitted as a nested number array — which is // what nlohmann::json(std::vector>) would have produced // and no consumer decodes as bytes. - EXPECT_TRUE(source.contains("args.push_back(lidlBytesListToJson(payloads));")); - EXPECT_TRUE(source.contains("nlohmann::json lidlBytesListToJson")); - EXPECT_TRUE(source.contains("out.push_back(lidlBytesToJson(bytes));")); + EXPECT_TRUE(source.contains("args.push_back(logos::toJson(payloads));")); // Qt-free, and taken by const-ref like the other composite payloads. EXPECT_TRUE(source.contains("const std::vector>& payloads")); @@ -183,13 +186,13 @@ TEST(LidlGenCdylib, ArrayOfBytesMethodParamDecodesPerElement) const QString source = implSourceFor(m); - EXPECT_TRUE(source.contains("lidlBytesListFromJson(")); - EXPECT_TRUE(source.contains("std::vector> lidlBytesListFromJson")); - EXPECT_TRUE(source.contains("out.push_back(lidlBytesFromJson(e));")); - // The scalar param still uses the scalar decoder. - EXPECT_TRUE(source.contains("lidlBytesFromJson(")); - // The blanket container decode must not be used for this type. - EXPECT_FALSE(source.contains(".get>>()")); + // Both params decode through the proxy, which converts itself into the + // author's own parameter type — so no type name is emitted at all and the + // scalar/array distinction needs no special case. + EXPECT_TRUE(source.contains("logos::JsonArg{args.at(0), \"arg0\"}")); + EXPECT_TRUE(source.contains("logos::JsonArg{args.at(1), \"arg1\"}")); + // The blanket container decode must not be used for any type. + EXPECT_FALSE(source.contains(".get>) would emit @@ -203,23 +206,9 @@ TEST(LidlGenCdylib, ArrayOfBytesReturnTagsEachElement) ASSERT_TRUE(lidlCdylibSupported(m, &error)) << error.toStdString(); const QString source = implSourceFor(m); - EXPECT_TRUE(source.contains("lidlBytesListToJson(")); - EXPECT_TRUE(source.contains("nlohmann::json lidlBytesListToJson")); + EXPECT_TRUE(source.contains("logos::toJson(result)")); } -// The list encoder is gated the same way the scalar one is: a module whose -// events carry only a single blob must not gain an unused static function. -TEST(LidlGenCdylib, BytesListEncoderOmittedWhenNoEventCarriesAnArray) -{ - const ModuleDecl m = moduleWithEvent("messageReceived", { - param("payload", prim("bstr")), - }); - - const QString source = eventsSourceFor(m); - - EXPECT_TRUE(source.contains("nlohmann::json lidlBytesToJson")); - EXPECT_FALSE(source.contains("lidlBytesListToJson")); -} // The supported scalar / bytes payloads stay eligible. TEST(LidlGenCdylib, SupportedEventParamsRemainEligible) @@ -233,3 +222,48 @@ TEST(LidlGenCdylib, SupportedEventParamsRemainEligible) QString error; EXPECT_TRUE(lidlCdylibSupported(m, &error)) << error.toStdString(); } + +// Numbers are 64-bit only. A narrower spelling is NOT auto-widened: widening +// would make the declared C++ type and the published LIDL contract disagree +// about range, so it is a build error that names the type and the fix. +TEST(LidlGenCdylib, NarrowNumericSpellingsAreRejectedWithTheFix) +{ + struct Case { const char* cpp; const char* hint; }; + const Case cases[] = { + {"uint32_t", "uint64_t"}, + {"int", "int64_t"}, + {"size_t", "uint64_t"}, + {"float", "double"}, + }; + + for (const Case& c : cases) { + const ModuleDecl m = moduleWithMethod(method("f", prim("tstr"), { + param("n", TypeExpr{TypeExpr::Named, c.cpp, {}}), + })); + QString error; + EXPECT_FALSE(lidlCdylibSupported(m, &error)) << c.cpp; + EXPECT_TRUE(error.contains(c.cpp)) << error.toStdString(); + EXPECT_TRUE(error.contains(c.hint)) << error.toStdString(); + } + + // uint8_t names its one legitimate use rather than just the width rule. + const ModuleDecl m = moduleWithMethod(method("f", prim("tstr"), { + param("b", TypeExpr{TypeExpr::Named, "uint8_t", {}}), + })); + QString error; + EXPECT_FALSE(lidlCdylibSupported(m, &error)); + EXPECT_TRUE(error.contains("std::vector")) << error.toStdString(); +} + +// A narrow spelling nested inside a supported container is rejected too, and the +// message still names it — the recursion must not lose the offender. +TEST(LidlGenCdylib, NarrowNumericInsideAContainerIsRejected) +{ + const ModuleDecl m = moduleWithMethod(method("f", prim("tstr"), { + param("counters", TypeExpr{TypeExpr::Array, "", {TypeExpr{TypeExpr::Named, "uint32_t", {}}}}), + })); + QString error; + EXPECT_FALSE(lidlCdylibSupported(m, &error)); + EXPECT_TRUE(error.contains("uint32_t")) << error.toStdString(); + EXPECT_TRUE(error.contains("counters")) << error.toStdString(); +}