Commit Graph
11 Commits
Author SHA1 Message Date
Dario Gabriel LipicarandClaude Opus 5 c598f569fd feat(records): typed structs for C++ dependency wrappers
Closes the second record gap. The wrapper every C++ module actually gets for
its dependencies comes from the LEGACY generator (`--dep <name>=<lidl>`), not
the client-stub backend the previous commit taught about records — and there a
contract's `type Status { ... }` reached the consumer as an untyped bag:
QVariant on the Qt surface, LogosMap on the std/lp one. Worse than
inconvenient for a `bstr` field: the caller received the canonical
`{"_bytes": "..."}` envelope and had to know to unwrap it, while Rust and the
stub backend handed back real bytes.

Now, on all three api styles:

    struct Status { uint64_t port{}; std::vector<uint8_t> blob{}; };
    Status getStatus(logos::CallError* err = nullptr);
    std::string describeStatus(const Status& s, ...);
    std::vector<Status> listStatuses(...);

The struct is NESTED in the wrapper class (`InfoModule::Status`) because a
module consuming two deps that each declare a `Status` includes both wrappers
into one translation unit. Conversions are file-local statics in the generated
.cpp, so a Qt-free module's own TUs still never see QVariant or nlohmann.
Records reach parameters, returns, event callbacks, `[Record]` and
`{tstr: Record}` — at any depth, with bytes tagged throughout.

Same commit, the legacy path's half of the 64-bit fix: `lidlTypeExprToQtTypeName`
mapped BOTH int and uint to `int` ("wire-as-int for now"), so a `uint` method on
a dep reached a Qt consumer as a signed 32-bit value and a std/lp consumer as a
signed int64_t. Now qlonglong/qulonglong, matching the spelling the other half
of this PR gave the stub backend. `lpFromJsonExpr` grew the uint64_t branch it
needed — without it a mistyped payload THREW out of nlohmann's implicit
conversion instead of defaulting like every other scalar.

Verified by generating a contract with a record, a record-of-records, a `uint`
above 2^32 and a high-byte `bstr`, then COMPILING the output for qt/std/lp and
round-tripping the emitted conversions:
  - `{"_bytes":"gAH_"}` at every depth, decoding back to the same bytes
  - 4294967296 intact through both directions
  - garbage/missing fields default rather than throw
That compile is what caught the one real bug here: the container decode lambdas
declared `__m`/`__j`, shadowing the record decoder's own locals, so a
map-of-records field read from its own uninitialized local — it compiled with
nothing but a -Wuninitialized warning. Locals are `__acc`/`__src` now, pinned
by a test.

Also: 8 generator tests (one asserting an empty record set leaves every byte
of the output as it was), 179 total green; logos-test-modules builds and tests
green against this generator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 16:42:43 -03:00
Dario Gabriel LipicarandClaude Opus 5 32ab1c6504 fix(codegen): teach the lp/std consumer wrappers the 64-bit spellings
Caught while checking whether the SDKs are ready for the `any` migration, and it
is a regression THIS BRANCH would otherwise have shipped.

legacy/generator_lib.cpp generates the lp/std consumer wrappers a universal
module uses to call its dependencies. It matches type names against an
allow-list and falls back to QVariant for anything else:

    static const QSet<QString> known = {
        "void","bool","int","double","float","QString", … };
    if (known.contains(base)) return base;
    return QString("QVariant");

Once lidlTypeToQt reports `qlonglong`/`qulonglong`, every LIDL int/uint method
misses that list — so a typed `int` parameter would have silently become an
opaque QVariant in those wrappers. Worse than the truncation this branch set out
to fix, and invisible until someone read the generated header.

Adds the two spellings to both allow-lists, plus the conversions they imply:
QVariant->Qt (toLongLong / toULongLong), the std spellings (int64_t / uint64_t),
the QVariant->std return path, the Qt-style return, and the default-value case.
The existing `int` entries stay for legacy Qt plugins, whose QMetaObject still
reports `int` for a 32-bit parameter.

Tests: 171/171, with the allow-list pinned in both mapping test files —
including that an unknown spelling still falls back to QVariant, so the fallback
itself is not what regressed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 15:57:45 -03:00
Dario Lipicar e8966bf7a9 fix(codegen): correct [T]-array arg packing and any-value return in the Qt client (#105)
* fix(codegen): pack Qt client args as one element each, not a spread list

The generated Qt client wrapper packed a method's arguments with
`QVariantList{a, b, ...}` (sync) and `QVariantList{...}` / `QVariantList() << a`
(async). For a QVariantList-typed argument -- every `[T]` list type (`[any]`,
`[int]`, `[uint]`, `[float64]`, `[bool]`) -- a braced `QVariantList{v}` and
`<< v` both CONCATENATE the list's elements into the args list, so
`echoList([1,2,3])` went out as three positional args instead of one array arg.
The receiver saw an arg-count mismatch and the list round-tripped empty; through
a UI->proxy->provider 2-hop it hung the call outright. This is the long-standing
"typed arrays empty over the Qt path" bug.

Fix both generators that emit the Qt client:
- legacy `generator_lib.cpp` (the production `logos-cpp-generator`): wrap each
  arg in `QVariant::fromValue(...)` in the sync and async call sites.
- experimental `lidl_gen_client.cpp`: route both paths through the existing
  `packVariantList` helper (which already wraps with `QVariant::fromValue`), the
  same helper the event `trigger` path uses.

`QVariant::fromValue` does not double-wrap an already-QVariant (`any`) arg, and
scalars/QString/QVariantMap/QByteArray were never affected (they don't
concatenate). Empirically: `QVariantList{v}` / `<< v` give size 3 for a 3-element
list; the wrapped forms give size 1.

Tests: legacy generator_tests gain ListArgWrappedAsOneElement and update the
param-packing assertions to the wrapped form; experimental gains
ListArgIsPackedAsOneElement. 167/167 green.

* fix(codegen): pass `any` (QVariant) return through raw in the lp wrapper

The Qt-free (lp) client generator maps both `any` (QVariant) and the `{tstr:any}`
map (QVariantMap) to the same `LogosMap` std type, and decoded a `LogosMap`
return as `jv.is_object() ? jv : LogosMap::object()`. For a genuine map that is
a no-op, but for `any` it collapsed every NON-object value (a string, a number,
an array) to an empty object `{}`. A universal proxy forwarding `echoAny("x")`
through this wrapper therefore returned `{}` instead of `"x"` — the concrete
cross-version blocker (the UI's runMethods verified echoAny and got FAIL:echoAny
through the 2-hop).

`lpFromJsonExpr` still receives the original Qt type, so it can tell `any`
(mapReturnType == "QVariant") from the map (QVariantMap): pass `any` through
unchanged, keep the object coercion only for the map.

Test: MakeSourceTest.LpAnyReturnPassesThroughButMapForcesObject.

With this + the arg-spread fix, a UI drives the full method surface (incl.
echoAny and every array type) through a universal proxy 2-hop end to end.
2026-07-19 01:03:30 -03:00
Dario LipicarandClaude Opus 4.8 2f34804948 fix(codegen): handle bstr + composite-any types in cdylib + Qt wrappers (#103)
Three type-handling gaps surfaced by a module that exercises the full type
surface (every method param/return + event param type):

1. cdylib method-param decode used lidlTypeToStd() for array params, which
   falls back to Qt containers (QVariantList) for [any] — undeclared in the
   Qt-free cdylib TU. Use the Qt-free lidlTypeToStdCdylib() so [any] decodes as
   LogosList. (lidl_gen_cdylib.cpp)
2. The Qt sync wrapper had no QByteArray return case, emitting a bare
   'return _result;' (QVariant) for a bstr return — no implicit QVariant ->
   QByteArray conversion. Add toByteArray(). (generator_lib.cpp)
3. The Qt event-callback arg converter had no QByteArray case, so a bstr event
   param was delivered via .toString() to a std::function<void(QByteArray)>.
   Add toByteArray(). (generator_lib.cpp)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 16:27:29 -03:00
Dario LipicarandClaude Opus 4.8 c7444bc29a Follow-ups to #100: lp-consumer bstr decode, Qt-free cdylib event types, binary-event coverage (#102)
* cdylib events: Qt-free types, and drop the unused bytes encoder

Three follow-ups to the bstr event fix, all in the cdylib events sidecar --
a Qt-FREE translation unit:

- An `any`/map event parameter was emitted as a bare QVariant/QVariantMap,
  which does not compile there. Spell those as their nlohmann aliases
  (LogosMap / LogosList) and pull in <logos_json.h> when they appear.
- std::vector<std::vector<uint8_t>> fell through the impl-header parser's
  unknown-type fallback to `any`, so the cdylib gate admitted it and the
  generator then emitted QVariant. Parse it as `[bstr]` so the gate rejects it
  with a message naming the offending parameter.
- The bytes encoder was emitted into every module's sidecar, leaving an unused
  static function (-Wunused-function) wherever no event carries binary data.
  Emit it only when a bstr event parameter exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* lp consumer: decode bstr into std::vector<uint8_t>

The Qt-free (`lp`) consumer wrappers -- what every universal C++ module gets for
its dependencies -- had no QByteArray in their type tables, so a `bstr` event
parameter, method argument, or return degraded to QVariant and then to LogosMap.
A consumer subscribing to a binary event was handed the raw tagged JSON object
{"_bytes": "<base64url>"} instead of the bytes, with no generated decode.

Teach the tables about QByteArray (-> std::vector<uint8_t>) and marshal it
through the canonical tagged form in both directions: logos::bytesToJson on the
way out, logos::jsonToBytes on the way in. Those live in logos_json.h -- Qt-free
and protocol-free, so the generated wrappers and module code can share them.
The Qt apiStyle already did this via QByteArray::toBase64/fromBase64.

Without this, a subscriber written the obvious way --

    onBinaryReady([](const std::string&, const std::vector<uint8_t>& payload) {...})

-- compiles (nlohmann::json has an implicit conversion operator) and then throws
at runtime on every event, so the callback body silently never runs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* tests: cover binary event payloads by value, not just by source text

The regression test for #99 asserts on generated source text, so it stays green
against an encoder that emits the wrong bytes. Add the value-level half:

- tests/sdk/test_logos_json_bytes.cpp exercises the canonical tagged-bytes codec
  against the RFC 4648 vectors, the URL-safe alphabet, every len%3 tail group,
  embedded NULs and high bytes, a 109,447-byte payload (the size from #99), and
  the lenient/padded decode paths.
- tests/experimental/test_lidl_gen_cdylib.cpp additionally pins the Qt-free
  spelling of JSON event payloads, the rejection of [bstr], and the omission of
  the bytes encoder from modules whose events carry no binary data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* doctests: prove a binary event payload survives the round trip

Neither doc-test covered bytes-in-an-event -- the gap #99 fell through. The
generator round-trip carried `bstr` only as a method argument and return, and
the composition doc-test, which is the one that actually runs two modules under
logoscore and subscribes to an event, carried only a string. So a generator that
dropped every bstr event argument kept both of them green.

- cpp-sdk-module-composition: greeter_module gains a `blobReady(label, payload)`
  event and an `emitBlob(size)` method; orchestrator_module subscribes and
  reports the length AND a checksum of what it received. Length alone would not
  catch a corrupted payload -- a wrong alphabet round-trips to the same size.
- cpp-sdk-generator-roundtrip: sensor_module gains a `capture(id, frame: bstr)`
  event, and a new step shows the generated event body encoding it through
  lidlBytesToJson rather than pushing it raw.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* logos_json.h: include <cstddef> for size_t

The tagged-bytes codec uses size_t but relied on it arriving transitively
through the other includes. Include <cstddef> directly so the header is
self-contained. (Copilot review, PR #102.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 18:50:40 -03:00
Dario LipicarandClaude Opus 4.8 676154070c codegen: Qt-free outbound — ApiStyle::Lp typed wrappers over the lp_* C ABI (#88)
* 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>
2026-06-14 00:47:09 -03:00
Dario LipicarandClaude Opus 4.8 f0fe8cbfeb Make the base SDK Qt-free: Qt developer layer moves to logos-qt-sdk (#83)
* Extract the protocol layer into logos-protocol; consume it as a flake input

The transport/token/IPC layer (transports incl. QRO + plain TCP/TLS,
consumer core LogosAPIClient/LogosAPIConsumer with the capability
auto-requestModule flow, ModuleProxy, token manager, QVariant<->JSON
conversion, the abstract LogosProviderObject interface) now lives in the
logos-protocol repo behind the versioned lp_* C ABI.

This SDK keeps the typed C++ developer layer (LogosAPI, provider base
classes + Qt provider glue, module context, code generator) and still
compiles the protocol sources INTO liblogos_sdk.a from the flake input,
so the installed artifact (archive symbols, include/ + include/cpp
layouts, cmake config) stays byte-compatible: existing consumers need
no changes. Public headers are unchanged; logos_provider_object.h keeps
its name and now re-exports the abstract interface from
logos_provider_interface.h.

Transport/protocol component tests moved to logos-protocol with the
code; the remaining sdk/generator/experimental suites are unchanged
(432/432 green against the local protocol checkout).

* lock: add logos-protocol input

* Make the base SDK Qt-free: move the Qt developer layer to logos-qt-sdk

LogosAPI, LogosAPIProvider, LogosProviderBase/LOGOS_PROVIDER macros, the
QObject provider glue (QtProviderObject) and the legacy PluginInterface
(core/interface.h) move to the new logos-qt-sdk repo. The protocol
sources are no longer compiled into a monolithic archive — consumers
link logos-qt-sdk (which layers on logos-protocol) instead.

What remains here is header-only std C++: logos_module_context.h,
logos_result.h (StdLogosResult), logos_json.h — exported as the CMake
INTERFACE target logos-cpp-sdk::logos_headers — plus the code generator
(a build-time tool; its introspection mode now includes
logos_provider_interface.h from logos-protocol, where
LogosProviderPlugin moved).

Mechanically verified Qt-free: the logos-cpp-lib / logos-cpp-include
closures contain only nlohmann_json. Tests: 245/245 (module-context std
suite + generator + experimental).

* fix: accept the installed source-export layout in the protocol-root check

The fail-fast only tested <root>/cpp/logos_protocol.h, but the LP_SRC
selection right below (and the error message itself) support the
installed export layout <root>/include/cpp as well. Pointing
LOGOS_PROTOCOL_ROOT at an installed export tripped the FATAL_ERROR
before that fallback could apply.

Caught by Copilot review on #82.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* lock: pin logos-protocol to the qt-free-split branch head

The Qt-free SDK (and the cdylib backend stacked on it) reference
LogosProviderPlugin from protocol's logos_provider_interface.h, which
lands on feat/qt-free-split — the P1-branch pin no longer compiles
standalone. Temporary — drop when the chain PRs merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* doctest: pin the logoscore runtime via its {release} placeholder

The spec built logoscore-cli at bare master with only the cpp-sdk inputs
overridden — master's stack cannot compile against the qt-free SDK, so
the suite failed on the chain branches. With the placeholder, CI's
--release-for pins expand it to the workspace's logoscore commit (and
local runs without a pin still fall back to master, unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* doctest: override the nested module builders to {release} too

capability_module (via logoscore's lock) and the cloned accounts module
resolve module-builder from their own locks — pre-split revs whose
LogosModule.cmake still detects the SDK by logos_api.h, which the
qt-free SDK no longer ships ('logos-cpp-sdk not found'). Overriding the
builder itself to the workspace-pinned chain rev (keeping the nested
cpp-sdk override) builds both modules with the split-aware builder.
Verified end-to-end locally with the exact doctest command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* doctest: apply the {release} + nested-builder overrides to all three specs

The runtime spec got the treatment in 210eea1; the composition and
worker-thread specs have the same logoscore/module build commands and
failed identically (pre-split builders from the modules' own locks).
All executed run: blocks now pin logoscore-cli{release} and override
the nested module builders to logos-module-builder{release}; the
displayed code_block: variants stay in their generic master form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* codegen: typed wrappers throw on call failure; dispatch catches escapes

Generated sync client wrappers call the new err-out invokeRemoteMethod
overload and throw logos::LogosCallError when the call fails (e.g. the
bound module is missing) — previously the empty QVariant silently
degraded to the return type's default and a caller could not tell
failure from a legitimate 0 / "". Both generators (legacy + LIDL),
both API styles. Async paths unchanged.

Generated provider dispatch (universal qt glue + LOGOS_PROVIDER) wraps
the method body in a catch-all that logs and returns an invalid QVariant
— an escaped exception becomes an ordinary METHOD_FAILED instead of
unwinding through Qt event dispatch and killing the module process.

* codegen: CallError out-param instead of throwing wrappers

Per review, the generated sync wrappers expose the error channel as an
optional trailing parameter — add(a, b, &err) — rather than throwing:
explicit, stateless, works on temporaries, and existing call sites
compile unchanged (they keep default-on-failure, now with a qWarning so
failures are visible in the module log). The dispatch catch-all from the
previous commit stays: it contains author exceptions, it doesn't
introduce any.

* glue: fire onContextReady AFTER modules()/event wiring

The generated onInit set the context (which fires the impl's
onContextReady hook) before constructing the LogosModules aggregate and
wiring typed event emission — so an impl doing its documented one-time
setup there (typed dependency calls, event subscriptions) dereferenced
a null aggregate and crashed the module process (signal 11). Found by
the first module to subscribe to a dependency's typed event from
onContextReady. Context now goes last.

* ci: run workflows on stacked PRs + workflow_dispatch

Both workflows filtered pull_request to master-based PRs, so stacked PRs
(feat/qt-free-sdk -> feat/extract-logos-protocol, feat/cdylib-authoring
-> feat/qt-free-sdk) ran NO checks at all. Drop the base-branch filter
for pull_request and add workflow_dispatch for manual runs. Same fix as
logos-module-builder 232b8a2.

* lock: protocol at the typed-requestModule port (3de5398)

* ci: chain pins for the doc-tests (drop at merge)

In repo CI only cpp-sdk's {release} is the commit under test —
logoscore-cli and module-builder expanded to master, which doesn't link
against the chain SDK the specs override in ('Build the CLI with the SDK
override' failed on every run since the stacked-PR triggers were
enabled). Pin both to the extraction-chain heads; the workspace pipeline
is unaffected (it pins every repo itself).

* generator: distribute the LIDL frontend for external generators

First step of moving ALL Qt glue emission out of this repo into
logos-qt-sdk's logos-qt-generator (cpp-sdk's generator keeps only the
Qt-free outputs: std typed wrappers, logos_sdk umbrella, cdylib
impl-exports, LIDL derivation).

- Shared emit helpers (lidlToPascalCase, lidlTypeToQt, lidlTypeToStd,
  lidlIsStdConvertible) move to a new lidl_emit_common.{h,cpp} unit, used
  by both generators.
- The frontend set (AST, lexer, parser, serializer, validator,
  impl-header parser, emit-common) is installed under
  share/lidl-frontend/ — the qt generator compiles these sources in
  directly, so the two tools share one frontend without a binary ABI.

* lock: protocol#3 merged — pin advances to protocol master

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 19:56:19 -03:00
Dario LipicarandClaude Opus 4.7 eb71a1aa90 feat: dependency interfaces — SDK code generator (bound wrappers) (#74)
* feat: dependency interfaces — runtime-bound typed wrappers

Turn a declared "interface" (a .lidl file or a pure-C++ header with a
logos_events: block) into a BOUND client wrapper: the target module name
is a constructor argument instead of a baked-in literal, so one interface
can be bound to any satisfying module at runtime.

- generator_lib: new BindMode { Static, Bound }. In Bound mode the ctor
  takes (LogosAPI*, const QString& moduleName) and every invokeRemoteMethod*
  / ensureReplica routes through m_moduleName. Default Static leaves existing
  name-baked output byte-for-byte unchanged.
- legacy/main.cpp: repeatable --interface <name>=<path>[=<impl_class>] flag,
  consumed in --general-only. Parses .lidl via lidlParse and .h via
  parseImplHeader, emits the bound <name>_api.{h,cpp}, and adds
  bind_<name>(moduleName) factories (QString + std::string) to the
  LogosModules umbrella. Also self-resolves local interface_dependencies
  from metadata.json for non-nix builds.
- experimental/lidl_gen_client: same BindMode parity for the --lidl path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* address review: dedup and validate --interface specs

A repeated --interface <name>=... would emit duplicate #include and
bind_<name>(...) into logos_sdk.h and fail to compile; empty name/path were
silently accepted. Dedup the flag-derived specs by name and drop malformed
ones with an explanatory message on stderr. (Copilot review, PR #74.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 20:44:43 -03:00
Dario LipicarandClaude Opus 4.8 760916e97b Parse method doc comments into per-method description (#70)
* parse adjacent method comments to populate description

* preserve line breaks in method descriptions

Join doc-comment lines with newlines instead of spaces (markers stripped,
leading/trailing blank lines dropped, interior blanks kept), and escape \n
when emitting the description into the generated getMethods(). Both codegen
paths updated; docs corrected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* don't count braces inside comment lines (impl-header parser)

A brace in a doc/line comment (e.g. `/// returns { ... }`) no longer affects
class-scope tracking, which previously could make the parser think the class
ended early and drop later declarations. Addresses review feedback on #70.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 11:38:27 -03:00
Dario Lipicar 8bdbd13848 Extend universal modules with module context (#61)
* extend universal modules with module context

* implement module calls and events for universal modules

* pr comments
2026-05-19 12:49:15 -03:00
Iuri Matias d633575677 add IDL parser & generator (wip) (#33)
* add IDL parser & generator (wip)

* fix fixtures issues affecting tests

* fix fixtures issues affecting tests
2026-03-31 16:29:35 -04:00