diff --git a/cpp-generator/experimental/impl_header_parser.cpp b/cpp-generator/experimental/impl_header_parser.cpp index 49fded4..414597f 100644 --- a/cpp-generator/experimental/impl_header_parser.cpp +++ b/cpp-generator/experimental/impl_header_parser.cpp @@ -365,6 +365,10 @@ ImplParseResult parseImplHeader(const QString& headerPath, QStringList pendingDoc; bool inBlockComment = false; + // Record being accumulated (see LookingForClass below). + TypeDecl record; + bool inRecord = false; + QRegularExpression classRe("\\bclass\\s+" + QRegularExpression::escape(className) + "\\b"); QRegularExpression accessRe("^\\s*(public|private|protected)\\s*:"); QRegularExpression eventsRe("^\\s*logos_events\\s*:"); @@ -375,6 +379,41 @@ ImplParseResult parseImplHeader(const QString& headerPath, switch (state) { case LookingForClass: + // A plain `struct Foo { T a; U b; };` ahead of the impl class is a + // RECORD: a named wire shape the module's methods can take and + // return. LIDL has carried TypeDecl/FieldDecl all along (chat_module + // declares records in a hand-written .lidl) — this is the + // impl-header path finally producing them, so an author writes a + // struct instead of an untyped LogosMap. + if (inRecord) { + if (line.startsWith("}")) { + if (!record.fields.empty()) + result.module.types.push_back(record); + inRecord = false; + } else { + static QRegularExpression fieldRe( + "^([A-Za-z_][A-Za-z0-9_:<>,\\s\\*&]*?)\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*;$"); + const QRegularExpressionMatch fm = fieldRe.match(line); + if (fm.hasMatch()) { + FieldDecl f; + f.name = fm.captured(2).toStdString(); + f.type = cppTypeToLidl(fm.captured(1).trimmed()); + record.fields.push_back(f); + } + } + break; + } + { + static QRegularExpression recordRe( + "^struct\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\{\\s*$"); + const QRegularExpressionMatch rm = recordRe.match(line); + if (rm.hasMatch()) { + record = TypeDecl(); + record.name = rm.captured(1).toStdString(); + inRecord = true; + break; + } + } if (classRe.match(line).hasMatch()) { state = InClass; for (QChar c : line) { diff --git a/cpp-generator/experimental/lidl_gen_cdylib.cpp b/cpp-generator/experimental/lidl_gen_cdylib.cpp index b320a78..3ca2b9d 100644 --- a/cpp-generator/experimental/lidl_gen_cdylib.cpp +++ b/cpp-generator/experimental/lidl_gen_cdylib.cpp @@ -13,8 +13,18 @@ namespace { // The cdylib-supported subset: std-convertible LIDL types only — the same // Qt-free set the std apiStyle handled, so any universal module that built // under std also builds as a header-first cdylib. +// Records declared by the module, so a Named type can be resolved. Set for the +// duration of lidlCdylibSupported / the emitters — the alternative is threading +// the module through typeSupported's several recursive call sites. +QSet g_declaredRecords; + bool typeSupported(const TypeExpr& te, bool isReturn) { + // A record the module declares is a first-class type: the generated codec + // specialisation below encodes it, and because that plugs into + // logos::detail::Codec, records compose inside [T] and {tstr: T} for free. + if (te.kind == TypeExpr::Named && g_declaredRecords.contains(qs(te.name))) + return true; if (te.kind == TypeExpr::Primitive) { if (te.name == "tstr" || te.name == "bstr" || te.name == "int" || te.name == "uint" || te.name == "float64" || te.name == "bool") @@ -211,6 +221,9 @@ void emitInterfaceJson(QTextStream& s, const ModuleDecl& module) bool lidlCdylibSupported(const ModuleDecl& module, QString* error) { + g_declaredRecords.clear(); + for (const TypeDecl& t : module.types) + g_declaredRecords.insert(qs(t.name)); for (const MethodDecl& md : module.methods) { for (const ParamDecl& pd : md.params) { if (!typeSupported(pd.type, /*isReturn=*/false)) { @@ -311,6 +324,41 @@ QString lidlMakeModuleImplExports(const ModuleDecl& module, s << " obj[\"error\"] = r.error.empty() ? nlohmann::json() : nlohmann::json(r.error);\n"; s << " return obj;\n}\n\n"; + // One Codec specialisation per declared record. Fields are addressed through + // decltype, so no C++ type name has to be spelled — the same trick JsonArg + // uses — and because these plug into logos::detail::Codec, a record nested in + // [T] or {tstr: T} is handled by the existing recursion with nothing further + // emitted. A missing field decodes as null, which the leaf codec then rejects + // with the field's path. + if (!module.types.empty()) { + s << "} // namespace\n\n"; + s << "namespace logos { namespace detail {\n\n"; + for (const TypeDecl& t : module.types) { + const QString n = qs(t.name); + s << "template <> struct Codec<" << n << ", void> {\n"; + s << " static nlohmann::json to(const " << n << "& v)\n {\n"; + s << " nlohmann::json o = nlohmann::json::object();\n"; + for (const FieldDecl& f : t.fields) { + const QString fn = qs(f.name); + s << " o[\"" << fn << "\"] = Codec>::to(v." << fn << ");\n"; + } + s << " return o;\n }\n"; + s << " static " << n << " from(const nlohmann::json& j, const std::string& path)\n {\n"; + s << " if (!j.is_object()) typeError(path, \"object\", j);\n"; + s << " " << n << " out;\n"; + for (const FieldDecl& f : t.fields) { + const QString fn = qs(f.name); + s << " out." << fn << " = Codec>::from(j.contains(\"" << fn << "\") ? j.at(\"" << fn + << "\") : nlohmann::json(), joinPath(path, \"." << fn << "\"));\n"; + } + s << " return out;\n }\n};\n\n"; + } + s << "}} // namespace logos::detail\n\n"; + s << "namespace {\n\n"; + } + emitInterfaceJson(s, module); s << "} // namespace\n\n"; diff --git a/tests/experimental/test_lidl_gen_cdylib.cpp b/tests/experimental/test_lidl_gen_cdylib.cpp index af54dd7..2785f1e 100644 --- a/tests/experimental/test_lidl_gen_cdylib.cpp +++ b/tests/experimental/test_lidl_gen_cdylib.cpp @@ -286,3 +286,54 @@ TEST(LidlGenCdylib, TooFewArgumentsReportsInvalidArgs) EXPECT_TRUE(source.contains("expected 2 arguments, got")); EXPECT_FALSE(source.contains("if (args.size() < 2) return nullptr;")); } + +// Records: a module declares a struct in its impl header and uses it like any +// other type. LIDL has carried TypeDecl all along; this is the impl-header path +// producing one, so an author writes a real shape instead of an untyped LogosMap. +TEST(LidlGenCdylib, DeclaredRecordIsSupportedAndGetsACodec) +{ + ModuleDecl m; + m.name = "info_module"; + + TypeDecl rec; + rec.name = "Status"; + FieldDecl a; a.name = "port"; a.type = prim("uint"); + FieldDecl b; b.name = "blob"; b.type = prim("bstr"); + rec.fields = {a, b}; + m.types.push_back(rec); + + // Used as a param, as a return, and nested in a container. + m.methods.push_back(method("setStatus", prim("bool"), { + param("s", TypeExpr{TypeExpr::Named, "Status", {}}), + })); + m.methods.push_back(method("all", TypeExpr{TypeExpr::Array, "", {TypeExpr{TypeExpr::Named, "Status", {}}}}, {})); + + QString error; + ASSERT_TRUE(lidlCdylibSupported(m, &error)) << error.toStdString(); + + const QString source = lidlMakeModuleImplExports(m, "InfoImpl", "info_impl.h"); + + // A codec specialisation, addressing fields through decltype rather than + // spelling their C++ types. + EXPECT_TRUE(source.contains("struct Codec")); + EXPECT_TRUE(source.contains("decltype(v.port)")); + EXPECT_TRUE(source.contains("decltype(out.blob)")); + // A missing field decodes as null so the leaf codec reports the field path. + EXPECT_TRUE(source.contains("j.contains(\"port\")")); + EXPECT_TRUE(source.contains(".port")); + // Nothing extra is emitted for the [Status] return — the existing recursion + // in logos_codec.h handles it. + EXPECT_TRUE(source.contains("logos::toJson(result)")); +} + +// An UNDECLARED name is still a build error naming the type: records are opt-in, +// not a reopening of the old opaque fallback. +TEST(LidlGenCdylib, UndeclaredNamedTypeIsStillRejected) +{ + const ModuleDecl m = moduleWithMethod(method("f", prim("tstr"), { + param("s", TypeExpr{TypeExpr::Named, "NotDeclared", {}}), + })); + QString error; + EXPECT_FALSE(lidlCdylibSupported(m, &error)); + EXPECT_TRUE(error.contains("NotDeclared")) << error.toStdString(); +}