Skip to content

add IDL parser & generator (wip)#33

Merged
iurimatias merged 3 commits intomasterfrom
update_generator_wip_1
Mar 31, 2026
Merged

add IDL parser & generator (wip)#33
iurimatias merged 3 commits intomasterfrom
update_generator_wip_1

Conversation

@iurimatias
Copy link
Copy Markdown
Member

No description provided.

Copilot AI review requested due to automatic review settings March 30, 2026 20:40
@iurimatias iurimatias review requested due to automatic review settings March 30, 2026 20:41
Copilot AI review requested due to automatic review settings March 31, 2026 17:12
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an experimental “LIDL” (Logos Interface Definition Language) pipeline (lexer/parser/validator/serializer) plus new code generators (client stubs + provider glue/dispatch), and adds a --from-header mode to infer interfaces from pure C++ impl headers. It also refactors the existing generator into a legacy/ submodule and wires up new experimental tests in CMake/Nix/CI.

Changes:

  • Refactor: move the existing generator implementation into cpp-generator/legacy/ and route legacy CLI behavior via legacy_main().
  • Add experimental LIDL toolchain (AST, lexer, parser, validator, serializer) and code generation (client stubs, provider glue, dispatch).
  • Add extensive experimental test suite + fixtures, and run it in CI/Nix.

Reviewed changes

Copilot reviewed 40 out of 42 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tests/generator/CMakeLists.txt Point generator tests at the refactored legacy generator sources/includes.
tests/experimental/CMakeLists.txt Add experimental_tests target covering LIDL + generators + header parser.
tests/experimental/test_lidl_lexer.cpp New unit tests for tokenization, escapes, comments, and error locations.
tests/experimental/test_lidl_parser.cpp New unit tests for module parsing, metadata, types, methods/events, and errors.
tests/experimental/test_lidl_validator.cpp New tests for semantic validation rules (duplicates, shadowing, unknown types).
tests/experimental/test_lidl_serializer.cpp New tests for serialization + parse/serialize/parse roundtrip.
tests/experimental/test_lidl_type_mapping.cpp New tests for LIDL→Qt and LIDL→std type mapping helpers.
tests/experimental/test_lidl_gen_provider.cpp New tests for provider glue/dispatch generation output.
tests/experimental/test_lidl_gen_client.cpp New tests for client stub generation output + metadata defaults.
tests/experimental/test_impl_header_parser.cpp New tests for parsing impl headers + metadata.json into ModuleDecl.
tests/experimental/fixtures/sample_metadata.json Fixture metadata for impl-header parsing tests.
tests/experimental/fixtures/sample_impl.h Fixture C++ impl header containing diverse method signatures.
tests/experimental/fixtures/empty_metadata.json Fixture minimal metadata for impl-header parsing tests.
tests/experimental/fixtures/empty_class_impl.h Fixture C++ header with no public methods for warning-path tests.
tests/experimental/fixtures/complex_impl.h Fixture C++ header with multiple access sections and edge cases.
tests/CMakeLists.txt Register the new experimental tests subdirectory.
nix/tests.nix Install experimental test binary and copy runtime fixtures into $out/fixtures.
cpp-generator/main.cpp New entrypoint routing between experimental modes and legacy_main().
cpp-generator/legacy/main.cpp Move legacy CLI implementation into legacy/ (exposes legacy_main).
cpp-generator/legacy/legacy_main.h New header declaring legacy_main().
cpp-generator/legacy/generator_lib.h Move legacy generator utilities into legacy/.
cpp-generator/legacy/generator_lib.cpp Move legacy generator utilities into legacy/.
cpp-generator/experimental/lidl_ast.h New AST types for LIDL (ModuleDecl, TypeExpr, etc.).
cpp-generator/experimental/lidl_lexer.h New lexer interface and token/result structs.
cpp-generator/experimental/lidl_lexer.cpp New lexer implementation with line/column tracking.
cpp-generator/experimental/lidl_parser.h New parser interface and result struct.
cpp-generator/experimental/lidl_parser.cpp New recursive-descent parser producing ModuleDecl.
cpp-generator/experimental/lidl_validator.h New validator interface/result struct.
cpp-generator/experimental/lidl_validator.cpp New semantic validator (duplicates, unknown types, shadowing, etc.).
cpp-generator/experimental/lidl_serializer.h New serializer interface.
cpp-generator/experimental/lidl_serializer.cpp New serializer implementation for ModuleDecl → LIDL text.
cpp-generator/experimental/lidl_gen_client.h New client stub generator API + type mapping helpers.
cpp-generator/experimental/lidl_gen_client.cpp New client stub generator implementation + metadata generator.
cpp-generator/experimental/lidl_gen_provider.h New provider glue/dispatch generator API + std type mapping helpers.
cpp-generator/experimental/lidl_gen_provider.cpp New provider glue header + dispatch source generators + CLI pipeline helper.
cpp-generator/experimental/impl_header_parser.h New API for parsing a C++ header into a ModuleDecl.
cpp-generator/experimental/impl_header_parser.cpp New state-machine-based header parser + metadata.json ingestion.
cpp-generator/docs/spec.md New high-level specification for LIDL, header parsing, and generated outputs.
cpp-generator/docs/project.md New project-structure + CLI usage + testing documentation.
cpp-generator/docs/index.md New documentation index.
cpp-generator/CMakeLists.txt Build changes: compile legacy + experimental sources into generator binary.
.github/workflows/ci.yml Run the new experimental_tests in CI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

static const QSet<QString>& lidlBuiltinTypes()
{
static const QSet<QString> bt = {
"tstr", "bstr", "int", "uint", "float64", "bool", "result", "any"
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

void isn't included in lidlBuiltinTypes(), so parsing -> void (or field type void) will be treated as a Named type and then fail validation unless the user declares a custom type void { ... }. If void is intended to be a builtin primitive (as the generator/type mapping suggests), add it to the builtin set here (and in the validator's builtin set) so void round-trips correctly from LIDL source.

Suggested change
"tstr", "bstr", "int", "uint", "float64", "bool", "result", "any"
"tstr", "bstr", "int", "uint", "float64", "bool", "result", "any", "void"

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +57
case TypeExpr::Primitive: break;
case TypeExpr::Named: if (!m_declaredTypes.contains(te.name)) result.errors.append(QString("Unknown type '%1'").arg(te.name)); break;
case TypeExpr::Array: validateTypeExpr(te.elements[0], result); break;
case TypeExpr::Map: validateTypeExpr(te.elements[0], result); validateTypeExpr(te.elements[1], result); break;
case TypeExpr::Optional: validateTypeExpr(te.elements[0], result); break;
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validateTypeExpr() indexes te.elements[0] / [1] for Array/Optional/Map without checking elements.size(). A malformed ModuleDecl (e.g., produced by other tooling or partially constructed in code) will cause out-of-bounds access/UB during validation. Consider validating the expected arity per kind and emitting a clear error instead of indexing blindly.

Suggested change
case TypeExpr::Primitive: break;
case TypeExpr::Named: if (!m_declaredTypes.contains(te.name)) result.errors.append(QString("Unknown type '%1'").arg(te.name)); break;
case TypeExpr::Array: validateTypeExpr(te.elements[0], result); break;
case TypeExpr::Map: validateTypeExpr(te.elements[0], result); validateTypeExpr(te.elements[1], result); break;
case TypeExpr::Optional: validateTypeExpr(te.elements[0], result); break;
case TypeExpr::Primitive:
break;
case TypeExpr::Named:
if (!m_declaredTypes.contains(te.name))
result.errors.append(QString("Unknown type '%1'").arg(te.name));
break;
case TypeExpr::Array:
if (te.elements.size() != 1) {
result.errors.append("Array type expects exactly 1 element");
break;
}
validateTypeExpr(te.elements[0], result);
break;
case TypeExpr::Map:
if (te.elements.size() != 2) {
result.errors.append("Map type expects exactly 2 elements");
break;
}
validateTypeExpr(te.elements[0], result);
validateTypeExpr(te.elements[1], result);
break;
case TypeExpr::Optional:
if (te.elements.size() != 1) {
result.errors.append("Optional type expects exactly 1 element");
break;
}
validateTypeExpr(te.elements[0], result);
break;

Copilot uses AI. Check for mistakes.
#include <QSet>

static const QSet<QString>& lidlBuiltinTypes() {
static const QSet<QString> bt = { "tstr", "bstr", "int", "uint", "float64", "bool", "result", "any" };
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lidlBuiltinTypes() doesn't include void. Even if you rely on Primitive being accepted by the validator, the builtin set is used for builtin-shadowing checks; currently a user-defined type void { ... } would not be flagged as shadowing a builtin. If void is meant to be reserved/builtin, include it here too (and align with the parser's builtin set).

Suggested change
static const QSet<QString> bt = { "tstr", "bstr", "int", "uint", "float64", "bool", "result", "any" };
static const QSet<QString> bt = { "tstr", "bstr", "int", "uint", "float64", "bool", "result", "any", "void" };

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +43
case TypeExpr::Primitive:
if (te.name == "void") return "void";
if (te.name == "tstr") return "QString";
if (te.name == "bstr") return "QByteArray";
if (te.name == "int") return "int";
if (te.name == "uint") return "int";
if (te.name == "float64") return "double";
if (te.name == "bool") return "bool";
if (te.name == "result") return "LogosResult";
if (te.name == "any") return "QVariant";
return "QVariant";
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uint is described elsewhere in this PR (docs/spec.md) as an unsigned 64-bit type, but here it is mapped to int (and later converted via QVariant::toInt() in generated code). That will truncate values outside 32-bit signed range and loses unsigned semantics. Consider mapping uint to qulonglong/uint64_t on the Qt side and using toULongLong() (and corresponding QVariant::fromValue(qulonglong)), or explicitly document/rename the type if the intent is actually a 32-bit signed int.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +101
if (te.name == "int") return "static_cast<int>(" + varName + ")";
if (te.name == "uint") return "static_cast<int>(" + varName + ")";
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stdReturnToQt() converts uint64_t results via static_cast<int>(...), which can silently overflow/truncate and also changes signedness. If uint is intended to be 64-bit, the provider glue should return a Qt type capable of holding it (e.g., qulonglong) and wrap it into QVariant without narrowing.

Suggested change
if (te.name == "int") return "static_cast<int>(" + varName + ")";
if (te.name == "uint") return "static_cast<int>(" + varName + ")";
if (te.name == "int") return "static_cast<qint64>(" + varName + ")";
if (te.name == "uint") return "static_cast<qulonglong>(" + varName + ")";

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +109
if (te.kind == TypeExpr::Array && te.elements.size() == 1) {
const TypeExpr& elem = te.elements[0];
if (elem.kind == TypeExpr::Primitive && elem.name == "tstr")
return "lidlToStdStringVector(" + paramName + ")";
return "lidlToStdVector_" + elem.name + "(" + paramName + ")";
}
return paramName;
}

static QString stdReturnToQt(const TypeExpr& te, const QString& varName)
{
if (!lidlIsStdConvertible(te))
return varName;

if (te.kind == TypeExpr::Primitive) {
if (te.name == "tstr") return "QString::fromStdString(" + varName + ")";
if (te.name == "bstr") return "QByteArray(reinterpret_cast<const char*>(" + varName + ".data()), static_cast<int>(" + varName + ".size()))";
if (te.name == "int") return "static_cast<int>(" + varName + ")";
if (te.name == "uint") return "static_cast<int>(" + varName + ")";
return varName;
}
if (te.kind == TypeExpr::Array && te.elements.size() == 1) {
const TypeExpr& elem = te.elements[0];
if (elem.kind == TypeExpr::Primitive && elem.name == "tstr")
return "lidlToQStringList(" + varName + ")";
return "lidlToQVariantList_" + elem.name + "(" + varName + ")";
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For array types other than [tstr], the generated conversions reference helpers like lidlToStdVector_<elem> / lidlToQVariantList_<elem>, but this file only conditionally emits helpers for string vectors. As-is, generating provider glue for e.g. [int], [uint], [bool], [float64], or [bstr] will produce uncompilable code unless those helper functions exist elsewhere. Either emit the full set of helpers when needed, or change qtParamToStd/stdReturnToQt to use inline conversions for the supported primitive element types, or treat these arrays as non-std-convertible so they stay as QVariantList.

Copilot uses AI. Check for mistakes.
Comment on lines +313 to +336
s << "QVariant " << providerObjectClass
<< "::callMethod(const QString& methodName, const QVariantList& args)\n{\n";

for (const MethodDecl& md : module.methods) {
QString qtRet = lidlTypeToQt(md.returnType);
s << " if (methodName == \"" << md.name << "\") {\n";

if (qtRet == "void") {
s << " " << md.name << "(";
for (int i = 0; i < md.params.size(); ++i) {
s << variantToQtArg(md.params[i].type, i);
if (i + 1 < md.params.size()) s << ", ";
}
s << ");\n";
s << " return QVariant(true);\n";
} else {
s << " return QVariant::fromValue(" << md.name << "(";
for (int i = 0; i < md.params.size(); ++i) {
s << variantToQtArg(md.params[i].type, i);
if (i + 1 < md.params.size()) s << ", ";
}
s << "));\n";
}
s << " }\n";
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated callMethod() dispatch extracts arguments via args.at(i) without checking args.size() against the method's parameter count. If the caller supplies too few arguments, this will hit an out-of-range access (assert/crash depending on build). Consider emitting an explicit if (args.size() != N) guard per method (and returning an invalid QVariant + warning) before reading args.

Copilot uses AI. Check for mistakes.
Comment on lines +197 to +231
// State machine: find "class <className>", then collect public methods
enum State { LookingForClass, InClass, InPublic, InPrivate };
State state = LookingForClass;
int braceDepth = 0;

QRegularExpression classRe("\\bclass\\s+" + QRegularExpression::escape(className) + "\\b");
QRegularExpression accessRe("^\\s*(public|private|protected)\\s*:");
QRegularExpression ctorDtorRe("^\\s*~?" + QRegularExpression::escape(className) + "\\s*\\(");

for (const QString& rawLine : lines) {
QString line = rawLine.trimmed();

switch (state) {
case LookingForClass:
if (classRe.match(line).hasMatch()) {
state = InClass;
for (QChar c : line) {
if (c == '{') braceDepth++;
else if (c == '}') braceDepth--;
}
}
break;

case InClass:
case InPublic:
case InPrivate:
for (QChar c : line) {
if (c == '{') braceDepth++;
else if (c == '}') braceDepth--;
}

if (braceDepth <= 0) {
state = LookingForClass;
goto done;
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class-parsing state machine exits the class whenever braceDepth <= 0. If the opening { for class <name> is on the next line (common style), braceDepth will still be 0 when processing the first line after the class declaration (e.g. public:), causing an immediate goto done and no methods will be parsed. Consider tracking whether the opening brace has been seen yet (or initializing braceDepth to 1 only once { is encountered) so classes with brace-on-next-line are handled.

Copilot uses AI. Check for mistakes.
Comment on lines +30 to 58
QString outputDir;
const int outDirIdx = args.indexOf("--output-dir");
if (outDirIdx != -1 && outDirIdx + 1 < args.size()) {
outputDir = args.at(outDirIdx + 1);
}

QJsonObject methodObj;
methodObj["signature"] = QString::fromUtf8(method.methodSignature());
methodObj["name"] = QString::fromUtf8(method.name());
methodObj["returnType"] = QString::fromUtf8(method.typeName());
bool isInvokable = method.isValid() && (method.methodType() == QMetaMethod::Method || method.methodType() == QMetaMethod::Slot);
methodObj["isInvokable"] = isInvokable;

if (method.parameterCount() > 0) {
QJsonArray params;
for (int p = 0; p < method.parameterCount(); ++p) {
QJsonObject paramObj;
paramObj["type"] = QString::fromUtf8(method.parameterTypeName(p));
QByteArrayList paramNames = method.parameterNames();
if (p < paramNames.size() && !paramNames.at(p).isEmpty()) {
paramObj["name"] = QString::fromUtf8(paramNames.at(p));
} else {
paramObj["name"] = QString("param%1").arg(p);
}
params.append(paramObj);
// --from-header mode: parse C++ impl header directly (no .lidl needed)
if (hasFromHeader) {
const int fromHeaderIdx = args.indexOf("--from-header");
if (fromHeaderIdx + 1 >= args.size()) {
err << "Error: --from-header requires a path to the impl header\n";
return 1;
}
methodObj["parameters"] = params;
}

methodsArray.append(methodObj);
}

return methodsArray;
}

// toPascalCase, normalizeType, mapParamType, mapReturnType -> generator_lib.h/cpp

// makeHeader -> generator_lib.h/cpp

// makeSource -> generator_lib.h/cpp
QString headerPath = args.at(fromHeaderIdx + 1);

static QString makeCoreManagerHeader()
{
QString h;
QTextStream s(&h);
s << "#pragma once\n";
s << "#include <QString>\n";
s << "#include <QVariant>\n";
s << "#include <QStringList>\n";
s << "#include <QJsonArray>\n";
s << "#include <functional>\n";
s << "#include <utility>\n";
s << "#include \"logos_api.h\"\n";
s << "#include \"logos_api_client.h\"\n";
s << "#include \"logos_object.h\"\n\n";
s << "class CoreManager {\n";
s << "public:\n";
s << " explicit CoreManager(LogosAPI* api);\n\n";
s << " using RawEventCallback = std::function<void(const QString&, const QVariantList&)>;\n";
s << " using EventCallback = std::function<void(const QVariantList&)>;\n\n";
s << " bool on(const QString& eventName, RawEventCallback callback);\n";
s << " bool on(const QString& eventName, EventCallback callback);\n";
s << " void setEventSource(LogosObject* source);\n";
s << " LogosObject* eventSource() const;\n";
s << " void trigger(const QString& eventName);\n";
s << " void trigger(const QString& eventName, const QVariantList& data);\n";
s << " template<typename... Args>\n";
s << " void trigger(const QString& eventName, Args&&... args) {\n";
s << " trigger(eventName, packVariantList(std::forward<Args>(args)...));\n";
s << " }\n";
s << " void trigger(const QString& eventName, LogosObject* source, const QVariantList& data);\n";
s << " template<typename... Args>\n";
s << " void trigger(const QString& eventName, LogosObject* source, Args&&... args) {\n";
s << " trigger(eventName, source, packVariantList(std::forward<Args>(args)...));\n";
s << " }\n\n";
s << " void initialize(int argc, char* argv[]);\n";
s << " void setPluginsDirectory(const QString& directory);\n";
s << " void start();\n";
s << " void cleanup();\n";
s << " QStringList getLoadedPlugins();\n";
s << " QJsonArray getKnownPlugins();\n";
s << " QJsonArray getPluginMethods(const QString& pluginName);\n";
s << " void helloWorld();\n";
s << " bool loadPlugin(const QString& pluginName);\n";
s << " bool unloadPlugin(const QString& pluginName);\n";
s << " QString processPlugin(const QString& filePath);\n\n";
s << "private:\n";
s << " LogosObject* ensureReplica();\n";
s << " template<typename... Args>\n";
s << " static QVariantList packVariantList(Args&&... args) {\n";
s << " QVariantList list;\n";
s << " list.reserve(sizeof...(Args));\n";
s << " using Expander = int[];\n";
s << " (void)Expander{0, (list.append(QVariant::fromValue(std::forward<Args>(args))), 0)...};\n";
s << " return list;\n";
s << " }\n";
s << " LogosAPI* m_api;\n";
s << " LogosAPIClient* m_client;\n";
s << " QString m_moduleName;\n";
s << " LogosObject* m_eventReplica = nullptr;\n";
s << " LogosObject* m_eventSource = nullptr;\n";
s << "};\n";
return h;
}

static QString makeCoreManagerSource(const QString& headerBaseName)
{
QString c;
QTextStream s(&c);
s << "#include \"" << headerBaseName << "\"\n\n";
s << "#include <QDebug>\n";
s << "#include <QStringList>\n\n";
s << "CoreManager::CoreManager(LogosAPI* api) : m_api(api), m_client(api->getClient(\"core_manager\")), m_moduleName(QStringLiteral(\"core_manager\")) {}\n\n";
s << "LogosObject* CoreManager::ensureReplica() {\n";
s << " if (!m_eventReplica) {\n";
s << " LogosObject* replica = m_client->requestObject(m_moduleName);\n";
s << " if (!replica) {\n";
s << " qWarning() << \"CoreManager: failed to acquire remote object for events on\" << m_moduleName;\n";
s << " return nullptr;\n";
s << " }\n";
s << " m_eventReplica = replica;\n";
s << " }\n";
s << " return m_eventReplica;\n";
s << "}\n\n";
s << "bool CoreManager::on(const QString& eventName, RawEventCallback callback) {\n";
s << " if (!callback) {\n";
s << " qWarning() << \"CoreManager: ignoring empty event callback for\" << eventName;\n";
s << " return false;\n";
s << " }\n";
s << " LogosObject* origin = ensureReplica();\n";
s << " if (!origin) {\n";
s << " return false;\n";
s << " }\n";
s << " m_client->onEvent(origin, eventName, callback);\n";
s << " return true;\n";
s << "}\n\n";
s << "bool CoreManager::on(const QString& eventName, EventCallback callback) {\n";
s << " if (!callback) {\n";
s << " qWarning() << \"CoreManager: ignoring empty event callback for\" << eventName;\n";
s << " return false;\n";
s << " }\n";
s << " return on(eventName, [callback](const QString&, const QVariantList& data) {\n";
s << " callback(data);\n";
s << " });\n";
s << "}\n\n";
s << "void CoreManager::setEventSource(LogosObject* source) {\n";
s << " m_eventSource = source;\n";
s << "}\n\n";
s << "LogosObject* CoreManager::eventSource() const {\n";
s << " return m_eventSource;\n";
s << "}\n\n";
s << "void CoreManager::trigger(const QString& eventName) {\n";
s << " trigger(eventName, QVariantList{});\n";
s << "}\n\n";
s << "void CoreManager::trigger(const QString& eventName, const QVariantList& data) {\n";
s << " if (!m_eventSource) {\n";
s << " qWarning() << \"CoreManager: no event source set for trigger\" << eventName;\n";
s << " return;\n";
s << " }\n";
s << " m_client->onEventResponse(m_eventSource, eventName, data);\n";
s << "}\n\n";
s << "void CoreManager::trigger(const QString& eventName, LogosObject* source, const QVariantList& data) {\n";
s << " if (!source) {\n";
s << " qWarning() << \"CoreManager: cannot trigger\" << eventName << \"with null source\";\n";
s << " return;\n";
s << " }\n";
s << " m_client->onEventResponse(source, eventName, data);\n";
s << "}\n\n";
s << "void CoreManager::initialize(int argc, char* argv[]) {\n";
s << " QStringList args;\n";
s << " if (argv) {\n";
s << " for (int i = 0; i < argc; ++i) {\n";
s << " args << QString::fromUtf8(argv[i] ? argv[i] : \"\");\n";
s << " }\n";
s << " }\n";
s << " m_client->invokeRemoteMethod(\"core_manager\", \"initialize\", argc, args);\n";
s << "}\n\n";
s << "void CoreManager::setPluginsDirectory(const QString& directory) {\n";
s << " m_client->invokeRemoteMethod(\"core_manager\", \"setPluginsDirectory\", directory);\n";
s << "}\n\n";
s << "void CoreManager::start() {\n";
s << " m_client->invokeRemoteMethod(\"core_manager\", \"start\");\n";
s << "}\n\n";
s << "void CoreManager::cleanup() {\n";
s << " m_client->invokeRemoteMethod(\"core_manager\", \"cleanup\");\n";
s << "}\n\n";
s << "QStringList CoreManager::getLoadedPlugins() {\n";
s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"getLoadedPlugins\");\n";
s << " return _result.toStringList();\n";
s << "}\n\n";
s << "QJsonArray CoreManager::getKnownPlugins() {\n";
s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"getKnownPlugins\");\n";
s << " return qvariant_cast<QJsonArray>(_result);\n";
s << "}\n\n";
s << "QJsonArray CoreManager::getPluginMethods(const QString& pluginName) {\n";
s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"getPluginMethods\", pluginName);\n";
s << " return qvariant_cast<QJsonArray>(_result);\n";
s << "}\n\n";
s << "void CoreManager::helloWorld() {\n";
s << " m_client->invokeRemoteMethod(\"core_manager\", \"helloWorld\");\n";
s << "}\n\n";
s << "bool CoreManager::loadPlugin(const QString& pluginName) {\n";
s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"loadPlugin\", pluginName);\n";
s << " return _result.toBool();\n";
s << "}\n\n";
s << "bool CoreManager::unloadPlugin(const QString& pluginName) {\n";
s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"unloadPlugin\", pluginName);\n";
s << " return _result.toBool();\n";
s << "}\n\n";
s << "QString CoreManager::processPlugin(const QString& filePath) {\n";
s << " QVariant _result = m_client->invokeRemoteMethod(\"core_manager\", \"processPlugin\", filePath);\n";
s << " return _result.toString();\n";
s << "}\n\n";
return c;
}

static bool ensureCoreManagerWrapper(const QString& genDirPath, QTextStream& err)
{
const QString headerRel = QStringLiteral("core_manager_api.h");
const QString sourceRel = QStringLiteral("core_manager_api.cpp");
const QString headerAbs = QDir(genDirPath).filePath(headerRel);
const QString sourceAbs = QDir(genDirPath).filePath(sourceRel);

QString header = makeCoreManagerHeader();
QString source = makeCoreManagerSource(headerRel);

QFile headerFile(headerAbs);
if (!headerFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
err << "Failed to write core manager header: " << headerAbs << "\n";
return false;
}
headerFile.write(header.toUtf8());
headerFile.close();

QFile sourceFile(sourceAbs);
if (!sourceFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
err << "Failed to write core manager source: " << sourceAbs << "\n";
return false;
}
sourceFile.write(source.toUtf8());
sourceFile.close();

return true;
}

static bool writeUmbrellaHeader(const QString& genDirPath, QTextStream& err)
{
// Generate logos-cpp-sdk/cpp/generated/logos_sdk.h that includes all *_api.h in this dir
QDir genDir(genDirPath);
QStringList headers = genDir.entryList(QStringList() << "*_api.h", QDir::Files | QDir::Readable);
QString content;
QTextStream s(&content);
s << "#pragma once\n";
s << "#include \"logos_api.h\"\n";
s << "#include \"logos_api_client.h\"\n\n";
// Includes
for (const QString& h : headers) {
s << "#include \"" << h << "\"\n";
}
s << "\n";
// Convenience aggregator exposing module wrappers
s << "struct LogosModules {\n";
s << " explicit LogosModules(LogosAPI* api) : api(api)";
for (const QString& h : headers) {
QString base = h;
base.chop(QString("_api.h").size());
QString className = toPascalCase(base);
s << ", \n " << base << "(api)";
}
s << " {}\n";
s << " LogosAPI* api;\n";
for (const QString& h : headers) {
QString base = h;
base.chop(QString("_api.h").size());
QString className = toPascalCase(base);
s << " " << className << " " << base << ";\n";
}
s << "};\n";

QFile outFile(genDir.filePath("logos_sdk.h"));
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
err << "Failed to write umbrella header: " << outFile.fileName() << "\n";
return false;
}
outFile.write(content.toUtf8());
outFile.close();
return true;
}

static bool writeUmbrellaHeaderFromDeps(const QString& genDirPath, const QJsonArray& deps, QTextStream& err)
{
// Generate logos-cpp-sdk/cpp/generated/logos_sdk.h based on dependencies list
QDir genDir(genDirPath);
QString content;
QTextStream s(&content);
s << "#pragma once\n";
s << "#include \"logos_api.h\"\n";
s << "#include \"logos_api_client.h\"\n\n";
// Includes
s << "#include \"core_manager_api.h\"\n";
for (const QJsonValue& v : deps) {
if (!v.isString()) continue;
QString depName = v.toString();
s << "#include \"" << depName << "_api.h\"\n";
}
s << "\n";
// Convenience aggregator exposing module wrappers
s << "struct LogosModules {\n";
s << " explicit LogosModules(LogosAPI* api) : api(api), \n core_manager(api)";
for (const QJsonValue& v : deps) {
if (!v.isString()) continue;
QString depName = v.toString();
s << ", \n " << depName << "(api)";
}
s << " {}\n";
s << " LogosAPI* api;\n";
s << " CoreManager core_manager;\n";
for (const QJsonValue& v : deps) {
if (!v.isString()) continue;
QString depName = v.toString();
QString className = toPascalCase(depName);
s << " " << className << " " << depName << ";\n";
}
s << "};\n";

QFile outFile(genDir.filePath("logos_sdk.h"));
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
err << "Failed to write umbrella header: " << outFile.fileName() << "\n";
return false;
}
outFile.write(content.toUtf8());
outFile.close();
return true;
}

static bool writeUmbrellaSource(const QString& genDirPath, QTextStream& err)
{
// Generate logos-cpp-sdk/cpp/generated/logos_sdk.cpp that includes all *_api.cpp in this dir
QDir genDir(genDirPath);
QStringList sources = genDir.entryList(QStringList() << "*_api.cpp", QDir::Files | QDir::Readable);
QString content;
QTextStream s(&content);
s << "#include \"logos_sdk.h\"\n\n";
for (const QString& c : sources) {
s << "#include \"" << c << "\"\n";
}
s << "\n";

QFile outFile(genDir.filePath("logos_sdk.cpp"));
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
err << "Failed to write umbrella source: " << outFile.fileName() << "\n";
return false;
}
outFile.write(content.toUtf8());
outFile.close();
return true;
}

static bool writeUmbrellaSourceFromDeps(const QString& genDirPath, const QJsonArray& deps, QTextStream& err)
{
// Generate logos-cpp-sdk/cpp/generated/logos_sdk.cpp based on dependencies list
QDir genDir(genDirPath);
QString content;
QTextStream s(&content);
s << "#include \"logos_sdk.h\"\n\n";
s << "#include \"core_manager_api.cpp\"\n";
for (const QJsonValue& v : deps) {
if (!v.isString()) continue;
QString depName = v.toString();
s << "#include \"" << depName << "_api.cpp\"\n";
}
s << "\n";

QFile outFile(genDir.filePath("logos_sdk.cpp"));
if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) {
err << "Failed to write umbrella source: " << outFile.fileName() << "\n";
return false;
}
outFile.write(content.toUtf8());
outFile.close();
return true;
}

// ── Provider-header mode: scan LOGOS_METHOD markers and generate dispatch ────
// ParsedMethod, parseProviderHeader, toQVariantConversion -> generator_lib.h/cpp

static int generateProviderDispatch(const QString& headerPath, const QString& outputDir, QTextStream& out, QTextStream& err)
{
QFileInfo fi(headerPath);
if (!fi.exists()) {
err << "Header file does not exist: " << headerPath << "\n";
return 2;
}

QVector<ParsedMethod> methods = parseProviderHeader(headerPath, err);
if (methods.isEmpty()) {
err << "No LOGOS_METHOD markers found in: " << headerPath << "\n";
return 3;
}

// Derive the class name from the header: parse for ": public LogosProviderBase"
QString className;
{
QFile f(headerPath);
f.open(QIODevice::ReadOnly | QIODevice::Text);
QTextStream ts(&f);
QRegularExpression classRe(R"(class\s+(\w+)\s*:\s*public\s+LogosProviderBase)");
while (!ts.atEnd()) {
QString line = ts.readLine();
auto m = classRe.match(line);
if (m.hasMatch()) {
className = m.captured(1);
break;
const int implClassIdx = args.indexOf("--impl-class");
if (implClassIdx == -1 || implClassIdx + 1 >= args.size()) {
err << "Error: --from-header requires --impl-class <ClassName>\n";
return 1;
}
}
f.close();
}

if (className.isEmpty()) {
err << "Could not find class inheriting LogosProviderBase in: " << headerPath << "\n";
return 4;
}

QString headerBaseName = fi.fileName();
QString implClass = args.at(implClassIdx + 1);

QString genDirPath = outputDir.isEmpty() ? fi.absolutePath() : outputDir;
QDir().mkpath(genDirPath);

// Generate logos_provider_dispatch.cpp
QString content;
QTextStream s(&content);

s << "// AUTO-GENERATED by logos-cpp-generator -- do not edit\n";
s << "#include \"" << headerBaseName << "\"\n";
s << "#include <QJsonArray>\n";
s << "#include <QJsonObject>\n";
s << "#include <QVariant>\n";
s << "#include <QString>\n";
s << "#include \"logos_types.h\"\n\n";

// callMethod() — group by name to support overloaded methods
QMap<QString, QVector<const ParsedMethod*>> methodsByName;
for (const ParsedMethod& m : methods) {
methodsByName[m.name].append(&m);
}

s << "QVariant " << className << "::callMethod(const QString& methodName, const QVariantList& args)\n";
s << "{\n";
for (auto it = methodsByName.constBegin(); it != methodsByName.constEnd(); ++it) {
const QString& name = it.key();
const QVector<const ParsedMethod*>& overloads = it.value();
s << " if (methodName == \"" << name << "\") {\n";
bool needArgsSizeCheck = overloads.size() > 1;
for (const ParsedMethod* m : overloads) {
if (needArgsSizeCheck) {
s << " if (args.size() == " << m->params.size() << ") {\n";
s << " ";
}
if (m->returnType == "void" || m->returnType.isEmpty()) {
s << " " << m->name << "(";
for (int i = 0; i < m->params.size(); ++i) {
s << toQVariantConversion(m->params[i].first, QString("args.at(%1)").arg(i));
if (i + 1 < m->params.size()) s << ", ";
}
s << ");\n";
if (needArgsSizeCheck) s << " ";
s << " return QVariant(true);\n";
} else {
s << " return QVariant::fromValue(" << m->name << "(";
for (int i = 0; i < m->params.size(); ++i) {
s << toQVariantConversion(m->params[i].first, QString("args.at(%1)").arg(i));
if (i + 1 < m->params.size()) s << ", ";
}
s << "));\n";
}
if (needArgsSizeCheck) {
s << " }\n";
const int metadataIdx = args.indexOf("--metadata");
if (metadataIdx == -1 || metadataIdx + 1 >= args.size()) {
err << "Error: --from-header requires --metadata <metadata.json>\n";
return 1;
}
}
s << " }\n";
}
s << " qWarning() << \"" << className << "::callMethod: unknown method:\" << methodName;\n";
s << " return QVariant();\n";
s << "}\n\n";
QString metadataPath = args.at(metadataIdx + 1);

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legacy mode strips a leading '@' from paths passed to --output-dir (and some other args), but the new --lidl / --from-header modes do not. If your build tooling relies on that convention (e.g., passing @/path), these new modes will write into the wrong directory / fail to locate inputs. Consider applying the same @-prefix normalization here for outputDir, lidlPath, headerPath, and metadataPath for consistency with legacy_main.

Copilot uses AI. Check for mistakes.
Comment on lines +257 to +262
if (line.endsWith(';')) {
QString decl = line.left(line.size() - 1).trimmed();
MethodDecl md;
if (parseMethodLine(decl, md)) {
result.module.methods.append(md);
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Method detection requires the trimmed line to literally endsWith(';'). A common header style is ReturnType foo(...) ; // comment (or ...); // comment), which would not end with ; after trimming and will be skipped. Consider stripping inline //... (and possibly trailing /*...*/) comments before the endsWith(';') check so public methods aren't missed.

Suggested change
if (line.endsWith(';')) {
QString decl = line.left(line.size() - 1).trimmed();
MethodDecl md;
if (parseMethodLine(decl, md)) {
result.module.methods.append(md);
}
{
// Strip inline comments before checking for method declarations
QString codeLine = line;
int slCommentPos = codeLine.indexOf("//");
if (slCommentPos != -1) {
codeLine = codeLine.left(slCommentPos);
}
int blockCommentPos = codeLine.indexOf("/*");
if (blockCommentPos != -1) {
codeLine = codeLine.left(blockCommentPos);
}
codeLine = codeLine.trimmed();
if (codeLine.endsWith(';')) {
QString decl = codeLine.left(codeLine.size() - 1).trimmed();
MethodDecl md;
if (parseMethodLine(decl, md)) {
result.module.methods.append(md);
}
}

Copilot uses AI. Check for mistakes.
@iurimatias iurimatias merged commit d633575 into master Mar 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants