mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-30 09:11:13 +00:00
master
19
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
667990f28c |
feat(sdk): aboutToUnload — a module's chance to finish before teardown (#143)
* feat(sdk): aboutToUnload — a module's chance to finish before teardown
A module could not flush state or close handles on the way out: the host
stopped it, its destructors ran, and anything mid-flight was gone. This gives
LogosModuleContext the Qt Creator contract for exactly that problem.
enum class LogosShutdown { Synchronous, Asynchronous };
virtual LogosShutdown aboutToUnload() { return LogosShutdown::Synchronous; }
void unloadFinished() const; // any thread
Default Synchronous, so no existing module changes behaviour. A module with work
to finish returns Asynchronous and calls unloadFinished() when done; the host
waits, but only for a bounded grace period.
unloadFinished() is a NO-OP outside a framework context, and after the deadline
has passed. That matters more than it reads: a module needs no special case for
being torn down under a deadline it already missed.
Two SFINAE pairs mirror the existing maybeSet* helpers. maybeAboutToUnload
reports Synchronous for an impl that never inherited LogosModuleContext, which
is exactly right -- it has no hook, so there is nothing to wait for.
Both names join the reserved set beside onContextReady: an impl overriding
aboutToUnload is talking to the framework, not publishing API, and leaking
either would generate a consumer wrapper for a lifecycle hook (LogosShutdown
has no LIDL type to return anyway).
The cdylib backend emits the two optional C ABI exports. The completion
callback is installed BEFORE the impl is asked to unload, and that ordering is
the correctness of the whole async path: an impl that finishes INLINE would
otherwise signal into a slot that is still empty, and the host would wait out
its entire grace period for a module already done. There is a test for it,
because nothing about reading the code makes that failure visible.
294/294, 4 new. Requires logos-protocol#62; flake.lock pins that branch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): track logos-protocol master now that the teardown ABI has landed
logos-co/logos-protocol#62 merged as 9664ae2. The lock pointed at the PR branch
while it was open.
The narHash is unchanged across the move (sha256-JTREoJn2kjQmYyYHg2RQb4...), so
the merged tree is byte-identical to the branch this was built and tested
against.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(generator): guard the teardown emission on protocol 0.5
The emitted exports named logos_module_unload_done_cb unconditionally, so a
module built with this generator against an older logos-protocol failed to
compile on a typedef it never asked for:
error: 'logos_module_unload_done_cb' was not declared in this scope
in generated code the author never wrote and cannot see. That is what this PR's
doc-tests hit -- new generator, older protocol pin.
logos-protocol#63 gives the surface a MINOR (0.5) so it is detectable, and both
the statics and the exports now sit behind
#if defined(LOGOS_PROTOCOL_VERSION_MINOR) && LOGOS_PROTOCOL_VERSION_MINOR >= 5
the same way the 0.3 trust-root surface is guarded a few lines below. A module
built against 0.4 simply has no teardown entry point, which is the same state as
a module that never overrode the hook -- and the glue that would call it is
generated alongside, so nothing goes looking for the missing symbol.
Both halves need the guard, not just the exports: the typedef is what an older
header lacks, and it is the statics that name it. The test asserts both.
295/295. flake.lock tracks protocol master (0d2a3c0), where 0.5 landed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
d972fe207a |
fix(generator): no silent drops — unread STRUCTURE in a record is a build error (#131)
#127 made an unknown C++ TYPE a build error instead of a silent `any`. This is the same hole one level down, in the record scanner: a line inside a `struct` body that the scanner could not read as a field was `continue`d, and the struct was published anyway — MINUS that field. A field list is not a detail of a record, it IS the record: the promise every other language binds to. A contract with two fields is exactly as well-formed as one with three, so nothing downstream could tell. Measured on the generator built from master: struct SplitRecord { -> type SplitRecord { exit 0, no diagnostic std::string id; id: tstr std::vector<std::string> n: uint tags; } uint64_t n; }; `tags` is simply GONE struct Outer { -> type Outer { a: int } exit 0 struct Inner { the record is made of the INNER type's int64_t a; field and has none of its own }; Inner inner; std::string label; }; struct Pair { -> method echoPair(v: Pair) with NO `type Pair` std::string first; std::string second; emitted — a contract naming a }; type it never declares struct Defaulted { -> the brace-initialised field is dropped std::string id{"none"}; (only the `= v` form was recognised) int64_t n = 0; }; struct Allman -> not a record AT ALL, so every mention of it { falls to the `any` fallback — since #127 an std::string id; error whose hint says "declare a struct", }; which is the thing the author declared The scanner now reads a body as DECLARATIONS rather than lines. Physical lines are joined until the declaration is whole — a `;` at the struct's own brace depth, or a `}` there, which is how a member function defined inline ends — exactly as the caller already joins a method signature until its parentheses balance. Where a brace sits, and where a line wraps, cannot decide what a header means. Allman and base-clause openings are recognised for the same reason. Whatever is left after that, and after the constructs that definitively are NOT fields (member functions, `using`/`typedef`/`friend`/`static`, access specifiers), is reported instead of skipped, naming the struct, the declaration and the fix. Same withdrawal discipline as #127: the diagnostic is keyed on the struct and tested against the API-REFERENCED set, so a helper struct the module never publishes may be as unreadable as it likes — every real module carries one (openmetrics' `ModuleSource`, the package manager's `PendingAction`). Comments come off with a literal-aware strip. The old bare `indexOf("//")` truncated `std::string url = "http://x";` inside the literal, which merely lost the field before and would now reject valid code. Blast radius, measured: all 27 universal-interface impl headers in the workspace emit BYTE-IDENTICAL .lidl and stderr, with identical exit codes. All 6 structs those headers declare are K&R with no unparsed body line, so nothing in tree changes. logos-qt-generator, which compiles this same file, builds; and test_fullapi_ext_cpp — the records-heavy module — builds end to end. 14 new tests. 10 of them fail on the unpatched parser; the rest are the "must still work" controls, including one for an inline member-function body that an earlier cut of this change would have broken. |
||
|
|
44b92b0480 |
feat(generator): no silent admissions — an unknown C++ spelling is a build error (salvage of #112) (#127)
* feat(parser): name nlohmann::json explicitly, ahead of killing the fallback `nlohmann::json` (and the `json` alias) has never had a branch in cppTypeToLidl. It reaches the opaque `any` the same way every unrecognised spelling does: the fallback at the bottom of the function. That is fine while the fallback is silent and wrong the moment it becomes an error, because `any` is the RIGHT answer here. test_fullapi_cpp declares `nlohmann::json echoAny(const nlohmann::json&)`, `bool fireAnyEvent(const nlohmann::json&)` and `logos_events: void anyEvent(const nlohmann::json&)`, and those three are the cross-language conformance chain's `any` cells — they must keep publishing `any`. So this lands first and on its own. It maps to the bare `any` primitive, which is exactly what the fallback already produced, making the change output-neutral: generating over every impl header and every .lidl in the workspace produces 764 byte-identical artifacts. That is what lets the later commit treat everything still reaching the fallback as a silent admission rather than a legitimate `any`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(cdylib): a typed map decodes into the author's own container `{tstr: T}` has two C++ spellings — std::map<std::string, T> and std::unordered_map<std::string, T> — and logos_codec.h specializes Codec for both, because they are the same wire shape. The generated dispatch named one: lidlImpl().echoIntMap(logos::fromJson<std::map<std::string, int64_t>>(...)) fromJson returns a std::map, and a std::map does not convert to an unordered_map parameter. So the container the author picked decided whether generated code they never wrote compiles — with the diagnostic pointing at that generated line, not at their declaration. logos::JsonArg (logos-protocol, already on master) exists for exactly this: it instantiates its conversion operator with the parameter's own type, so the author's declaration drives the decode. The return side is the same problem mirrored, and `logos::toJson(result)` deduces instead of asserting. Restricted to Map because every other LIDL type has one C++ spelling here, and because JsonArg documents one target it cannot serve — std::optional<X>, whose converting constructor out-ranks the proxy's conversion operator. The Optional branch returns before reaching this code. For a std::map author nothing changes: JsonArg instantiates the same Codec<std::map<...>>::from at the same path, and toJson deduces the same T. Compile-checked against both spellings, including a bad element still failing with `expected string at arg0.k, got number`. Across the whole workspace the emitted delta is 5 methods, all in test_fullapi_ext. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(parser): an unknown C++ spelling is a build error, not a silent `any` cppTypeToLidl ended with `// Fallback: treat as opaque` -> `any`, and `any` is ADMITTED by every backend gate. So a spelling nobody had written a branch for was accepted in silence, published as `any`, and dispatched as a bare `lidlImpl().f(args.at(0))` — no logos::fromJson<>, no check. That is the one hole #113-#122 closed for every typed slot and left open for anything that reached `any`. An 11-method hostile probe was admitted wholesale: uint32_t, size_t, float and uint8_t all typed as `any`; std::set, std::vector<std::pair<...>> and a non-string-keyed map as `any`; std::vector<uint32_t> as `[any]`. Now every spelling with no LIDL type is collected with the declaration that carries it, and parseImplHeader turns the list into a parse error naming the offending type and the fix: method 'send_generic_public_transaction': parameter 'instruction' declared `const std::vector<uint32_t>&`, whose element `uint32_t` has no LIDL type. LIDL numbers are 64-bit only. Declare it `uint64_t` (LIDL `uint`). Widening is source-compatible for every caller; a narrow type on the wire is not, which is why LIDL has none. Numbers get that tailored hint (uint8_t its own — it means bytes here, and only as std::vector<uint8_t>); sets, pairs/tuples, non-string map keys, list/deque/array, Qt types and pointers each get theirs; anything else gets the full table of recognised spellings. A hint that does not name a replacement just moves the guesswork, so all of them do. std::unordered_map<std::string, T> joins std::map as a spelling of `{tstr: T}` — the codec has always handled both, and the previous commit made the generated dispatch bind whichever the author declared. Two slots are the exception and say so: a record FIELD and an event PARAMETER, where the generator writes the spelling out into code the author's own declaration has to match and can only pick one name. Three properties keep this from breaking things it should not: * The MAPPING is unchanged. cppTypeToLidl still returns `any` for an unsupported spelling; only a diagnostic is recorded. A diagnostic that is later withdrawn therefore leaves output byte-identical. * Diagnostics are withdrawn for declarations that never reach the contract — a helper struct dropped by keepOnlyReferencedRecords, a reserved lifecycle hook (onContextReady and friends), a struct with no parsed fields. Publishing is what makes a type a promise. * An empty spelling is not a C++ type, it is this line-based parser failing to find one. It keeps the old behaviour rather than reporting `''`. Also fixes a latent ordering bug found while threading the context through: metadata.json's event parameters were typed BEFORE the header was read, i.e. against whatever g_recordNames the previous module's parse had left behind. They are now read there and typed after scanForRecords, in the same position in module.events as before. Verified by generating over every impl header and every .lidl in the workspace: 31 derived contracts byte-identical, 368 consumer-umbrella files byte-identical under both --api-style qt and --api-style lp, 46 generated types headers byte-identical. Exactly two modules now fail, at exactly the four slots a prior scan identified as silent admissions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(cdylib): a wrong argument count reports invalid_args The arity gate was `if (args.size() < N) return nullptr;`. The Qt glue turns a NULL reply into an empty QVariant, so "you passed 2 of 4 arguments" was indistinguishable from a method that legitimately returned nothing. logos-rust-sdk already ships the other half. src/args.rs::invalid_args is documented "Same code and message as the C++ generated glue" and pinned by a test named invalid_args_shape_matches_cpp — both of which were false: Rust answered a structured object and C++ answered NULL. Checked against the JSON that crate actually emits rather than against its comment, the two are now byte-identical: {"code":"invalid_args","message":"expected 4 arguments, got 2","origin":"my_module"} `expected` counts REQUIRED parameters in both, so a trailing optional does not change it. The guard is emitted only when the method has at least one required parameter. args.size() is unsigned, so `< 0` never fires: a zero-argument method carried a dead branch. The Rust generator skips it for the same reason, so the two now agree on when a check exists at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(cdylib): delete the dead emitted base64 codec #117 replaced the codec the generator emitted into every module with logos-protocol's logos_codec.h, but left the base64 pair it had grown around: * lidlB64Idx + lidlBytesFromJson — 55 emitted lines with NO call site at all. Every byte parameter had already moved to logos::bytesFromJsonLenient. Confirmed across the whole workspace: in 48 generated export TUs, all 48 mentions of lidlBytesFromJson are its own definition line. * lidlB64UrlEncode + lidlBytesToJson — 34 emitted lines that are logos::bytesToJson rewritten, in a translation unit that already includes it through "<module>_types.h". Scalar bstr slots now call logos::bytesToJson. Composite ones ([bstr], {tstr: bstr}, records) have gone through logos::Codec since #117, so this removes the last place a module carried its own copy of an encoder — the arrangement that once let the emitted and canonical halves disagree about padded base64, and that #117's own comment set out to end. hasBytesEventParam goes with it: it existed only to keep the emitted copy from sitting unused in the events sidecar of modules whose events carry no binary data, and the `namespace { }` block it gated is gone too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(doctest): the bstr event marshal is logos::bytesToJson now Commit (D) deleted the dead emitted base64 codec, so the cdylib events sidecar calls logos::bytesToJson -- the one in logos_codec.h, included in the same translation unit -- instead of emitting its own lidlBytesToJson. The doctest still pinned the old spelling and failed on the new output: expected 'args.push_back(lidlBytesToJson(frame));' not found in output The prose around it is unchanged and still correct: the payload is still the canonical {"_bytes": "<base64url>"} form, which is the property that assertion exists to guard (#99). Only the symbol moved. Verified against real generator output rather than by search-and-replace: built the branch generator, ran --backend cdylib over a sensor_module contract with a bstr event param, and read the emitted line: args.push_back(logos::bytesToJson(frame)); Checked the rest of the specs for other stale symbols (lidlStrdup, b64Url, hasBytesEventParam, the old 'return nullptr' arity gate) -- this was the only one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
198f0317ca |
feat(optional): ?T is two-state, and the generators finally read it (#125)
* feat(optional): ?T is two-state, and the generators finally read it
No generator in any language read the optional flag — it had never been
implemented. `?T` was a HARD REJECT on the cdylib backend ("module not
cdylib-eligible"), `std::optional<T>` in an impl header fell through to the
opaque `any` with no diagnostic, and a `? name: T` field was emitted as a
required `T`. Three real contracts in the workspace already declare optionals
and were silently getting one of those three answers.
`?T` is TWO-state: a value of T, or empty. Never three — "one LIDL type <-> one
type per language" leaves nowhere for a third state, because every target has
exactly one empty inhabitant.
ONE MEANING, TWO SPELLINGS. `? name: T` (the field flag) and `name: ?T` (the
type kind) are the same declaration. Backends no longer answer that themselves:
logos-lidl's fieldIsOptional/fieldValueType are re-exported from lidl_compat.h
and every site THIS COMMIT TOUCHES reads them, so the two spellings emit
byte-identical code on the cdylib and client backends. That
caught a live drift on the way in — lidlRecordCollidesWithBytesTag read `f.type`
and so refused `? _bytes: tstr` while letting `_bytes: ?tstr` straight through,
one declaration with two answers.
THE WIRE RULE DEPENDS ON THE SLOT. Absent and explicit null are the SAME state
on decode and DIFFERENT on encode:
- decode is liberal, by exactly one inhabitant: in an optional slot absent and
null both mean empty; in a required slot both stay errors. A present value
goes through the decoder a required T would get, so a wrong type still fails
at the same path — optional widens the domain, it does not switch checking
off. `?bstr` therefore keeps the LENIENT bytes decode a bare `bstr` gets,
rather than silently becoming stricter in the optional slot.
- encode has one canonical form: empty OMITS the key where the slot is NAMED
(a record field) and is spelled null where it is POSITIONAL (an argument, a
return, an event parameter — no key to omit, and arity must not change). Key
omission lives in the record emitter because a Codec only ever sees a value,
never the slot it sits in. A round trip therefore canonicalises.
- `?any` collapses onto `any`: nlohmann::json already carries null, so
std::optional<LogosMap> would give the slot two spellings of empty.
The dispatch gate now admits a missing trailing optional argument and
materialises it as null, exactly the way a missing record field already was. A
method with no optional parameter emits the byte-identical gate it always did.
Header-first: `std::optional<T>` <-> `?T`, composing with records and
containers. `std::optional<std::optional<T>>` has NO LIDL type (three C++ states
over a two-state wire), so it maps down to `?T` — which makes the author's own
declaration stop compiling against the generated codec, deliberately — and says
so at derivation time instead of leaving a conversion error in generated code.
The Qt/Lp consumer surface is NOT fixed and does not pretend to be. The wrappers
real modules get come from legacy/main.cpp, where the AST is flattened to a
single Qt type-name string per slot before optionality could be seen; Qt has no
optional metatype, so `?T` lands on QVariant — the right shape (an invalid
QVariant is Qt's empty inhabitant) with no type. The generator now prints a Note
naming every flattened slot so an affected build is never silent, and
docs/project.md records exactly what a Qt consumer will still do with an
optional field.
Verified by output equivalence, not by a green build: the generator was built
before and after and run over every .lidl in the workspace plus the impl-header
fixtures, in cdylib, consumer-qt, consumer-lp, client and header-first modes.
428 of 465 artefacts are byte-identical; all 37 that differ belong to one of the
four contracts that declare an optional (the 38th path is the manifest). The
harness's sensitivity is pinned by a negative control: qt vs lp output differs
in 45 files. The emitted codec was additionally compiled under -Wall -Wextra and
run against the rules above — omission, absent==null, required-still-rejects,
present-but-wrong-still-fails, and canonicalising round trip.
Tests: 199 pass, 0 fail (180 before, 19 new).
Requires logos-lidl's optionality accessors and logos-protocol's
Codec<std::optional<T>>.
NOT FIXED, AND IT IS THE PATH THAT MATTERS MOST. The legacy interface-wrapper
path is untouched, and it is the one every real module builds through
(buildPlugin.nix:145 -> logos-cpp-generator --general-only). There the two
spellings still diverge:
? maybe: tstr -> QString maybe{}; __m.value("maybe").toString()
maybe: ?tstr -> QVariant maybe{}; __m.value("maybe")
and --api-style lp diverges too, neither side being std::optional. So R3 holds
on the backends below and NOT on the Qt consumer a shipping module actually
gets. logos-chat-module -- the contract that prompted this work -- uses the
field-flag spelling, so it lands on the branch that silently defaults.
The cause is upstream of codegen: legacy/main.cpp's moduleRecordsToJson and
moduleMethodsToJson flatten every TypeExpr to a single Qt TYPE-NAME STRING, so
optionality (along with nesting, map key types and descriptions) is gone before
generator_lib.cpp sees it. Widening that interface is a larger change and is
deliberately not attempted here. The only R3 test on a Qt surface covers
lidl_gen_client.cpp, which is on no live build path.
* chore: re-pin logos-lidl to master for the optionality accessors
lidl_compat.h re-exports typeIsOptional / optionalValueType / fieldIsOptional /
fieldValueType / paramIsOptional / paramValueType, which landed in
logos-lidl#7. The pinned lidl predated it, so CI failed to compile.
logos-lidl 8c95d4f -> 35f33d8. Tests: 199 pass, 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: re-pin logos-protocol to master for Codec<std::optional<T>>
The generated record codecs emit Codec<std::optional<T>> for an optional
field; that specialisation landed in logos-protocol#37 and the pinned
protocol predated it.
Note this repo's own tests would NOT have caught the omission -- the
generator tests string-assert emitted text rather than compiling it, so a
missing codec specialisation only surfaces when a real module compiles
generated optional code (logos-test-modules' ext provider).
logos-protocol 4359557 -> 72754ab. Tests: 199 pass, 0 fail.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(doctests): override logos-lidl alongside every logos-cpp-sdk override
The doc-tests build downstream repos (logoscore-cli, capability_module,
accounts_module) with --override-input logos-cpp-sdk. Nix does not carry the
overridden input's OWN lock, so those builds got this branch's cpp-sdk source
while still resolving logos-lidl from their own, older locks. The shipped
share/lidl-frontend/lidl_compat.h then calls accessors that lidl does not
have:
lidl_compat.h:46: error: 'paramValueType' has not been declared in 'lidl'
lidl_compat.h:92: error: 'fieldValueType' was not declared in this scope
Every --override-input logos-cpp-sdk now has a matching
--override-input <same-path>/logos-cpp-sdk/logos-lidl.
This is specific to the override path. A normal consumer running
'nix flake update logos-cpp-sdk' inherits cpp-sdk's own lock, which pins the
lidl carrying these accessors, and is unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(doctests): move logos-lidl at the qt-sdk nodes, not under logos-cpp-sdk
The doc-tests failed to build logos-qt-generator:
share/lidl-frontend/lidl_compat.h:46: error: 'paramValueType' has not been
declared in 'lidl'
MECHANISM. This SDK installs cpp-generator/experimental/lidl_compat.h into
$out/share/lidl-frontend/, and logos-qt-sdk's logos-qt-generator *compiles*
that installed header against qt-sdk's OWN logos-lidl input. Under
logos-qt-sdk, logos-lidl is a SIBLING of logos-cpp-sdk, not a descendant:
logos-qt-sdk
|-- logos-cpp-sdk <- --override-input moves this to the commit under test
`-- logos-lidl <- stays on qt-sdk's lock (8c95d4f), lacks the accessors
logos-logoscore-cli and logos-module-builder both declare
`logos-qt-sdk.inputs.logos-cpp-sdk.follows = "logos-cpp-sdk"` but no lidl
follows, so overriding the SDK hands qt-sdk a new lidl_compat.h next to its
old lidl. The failing derivation is logos-qt-generator — not anything in
logos-cpp-sdk, which is why the previous attempt aimed at the wrong node.
THE FIX is one `<path-to-logos-qt-sdk>/logos-lidl` override per qt-sdk node
that ends up on the SDK under test. A tree-walk over the resolved lock found
four in logoscore-cli's closure and two per module build; with the overrides
applied the walk reports zero remaining.
WHAT WAS REMOVED, and why it was doing nothing:
* The `.../logos-cpp-sdk/logos-lidl` overrides added in
|
||
|
|
fde0f6fccb |
fix: read dependency entries declared in object form (#123)
* fix: read dependency entries declared in object form The manifest schema lets a dependency entry be an object carrying the name alongside the constraints an installer resolves it by, but every reader took the element as a plain string and skipped what came back empty, so an object entry disappeared: the module it names was left out of the generated LogosModules aggregate, and every call through it failed to compile. The rule lives in one place now, since the copies of it are how the gap spread. It ships in share/lidl-frontend alongside the parser that includes it, which consumers compile from there. * fix: read every dependency entry through one pass over the array The object form reached the umbrella's members and constructor but not its includes: that emitter still read each element as a plain string, so a module declared in object form came out as a member whose type was never included, and the aggregate no longer compiled. It is the Qt-free umbrella, which is what every universal core module and every cdylib module generates, so the form the previous commit set out to support failed there in a new way rather than working. Reading the array element by element is what let one pass disagree with the next, so no reader does that any more: dependencyNames() answers what an array declares, once, and the emitters walk names. That leaves the entry form knowable in exactly one place, and the includes and members of an aggregate can no longer be built from different answers. The umbrella emission moves to generator_lib alongside the per-module wrapper emitters it mirrors, returning the text instead of writing it, so what it generates can be asserted on directly; main.cpp writes what it returns. Output for string-form dependencies is byte-identical in both API styles, with and without interface dependencies. The listing mode (`--metadata` with no `--module-dir`) went the same way — it was the last reader still deciding on its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Dario Gabriel Lipicar <dario@status.im> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3d322bd315 |
fix(codegen): LIDL int/uint are 64-bit in the Qt spelling too (#113)
* fix(codegen): LIDL int/uint are 64-bit in the Qt spelling too lidlTypeToQt mapped BOTH `int` and `uint` to plain `int`. Everywhere else in the stack a LIDL int/uint is 64-bit — int64_t/uint64_t in C++ impls, i64/u64 in the Rust SDK — so the Qt spelling broke the one-type-per-LIDL-type rule and lost data: a Qt consumer reading a `uint` return got a SIGNED 32-bit value, so anything above 2^31 came back wrong and anything above 2^63 was never expressible. int -> qlonglong, uint -> qulonglong, and returnConversion() gains the matching accessors (toLongLong / toULongLong instead of toInt). qlonglong/qulonglong rather than qint64/quint64 so the generated introspection JSON uses the same names Qt's own metaobject normalisation produces — otherwise a cdylib module's generated `signature` and a legacy module's QMetaObject-derived one would disagree for the same LIDL type. Nothing looks these strings up: the only QMetaType::fromName call in the stack is for "LogosResult". This changes two generated surfaces: the Qt consumer wrapper signatures and the introspection JSON. Passing an int argument still converts implicitly, so callers keep compiling; code that assigns a wrapper's return into an `int` narrows and may warn, which is the bug being surfaced rather than a regression. Tests: 168/168, with the type-mapping and client-emitter expectations updated to the 64-bit spelling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(records): typed C++ structs for Qt consumers Completes the Qt half of the type mapping this branch started. A `type Foo { … }` in a contract now generates a real struct in the client header, so a consumer writes `Status s = client.makeStatus();` instead of digging fields out of a QVariantMap. One LIDL type, one type per language. lidlTypeToQt - Named -> the record's struct (was QVariant) - [Record] -> QList<Record>, {tstr: Record} -> QMap<QString, Record>. QVariantList CANNOT hold a record without Q_DECLARE_METATYPE, and a typed list is the point. client emitter - struct + inline ToVariant/FromVariant per record, emitted before the class; conversions come after all structs so records may reference each other. Recursive, so a field may itself be [Status] or {tstr: bstr}. - records pass by const&, decode on return, and convert at the call site (sync and async) bstr fields are QByteArray on purpose: logos-protocol's QVariant<->JSON conversion already materialises the canonical {"_bytes": base64url} form as a QByteArray and back, so the record conversions stay pure field mapping and binary survives at any depth with no record-specific bytes handling. Verified by COMPILING and RUNNING the generated code, not just asserting on text — the string tests would not have caught either bug this found: [Record] first mapped to QVariantList (appending a Status to it does not compile) and the decode lambdas shadowed their accumulator. Extracted the emitted record block for a contract with a nested record and a bytes field, compiled it against Qt6Core, and round-tripped Batch -> QVariant -> Batch asserting items[0].port, the QByteArray blob and the label all survive. Exit 0. The LidlTypeToQt.NamedType expectation flips from "QVariant" to the struct name, which is the behaviour change. Tests: 169/169. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * 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> * 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> * fix(records): async record returns decoded a default-constructed struct Two correctness holes in the record work, both silent, found while scoping the cdylib provider side. 1. The async consumer overload emitted `qvariant_cast<Status>(v)` while the sync one emitted `StatusFromVariant(_result)`. The wire delivers a QVariantMap and no Q_DECLARE_METATYPE is emitted for the struct, so the cast does not fail — it returns a DEFAULT-CONSTRUCTED Status and the caller sees empty fields with no diagnostic. The sync path being correct is what makes it bad: the same call is right or wrong depending only on which overload the caller reached for. Async now decodes field by field through the same conversion. (The legacy dependency-wrapper generator already did this correctly — this was the experimental client-stub backend only.) 2. A record whose ONLY field is a tstr named `_bytes` is wire-identical to a canonical tagged byte string: `isTaggedBytes()` is checked before `is_object()` in both logos_codec.h and logos_json_convert.cpp, so such a record decodes as bytes and the struct silently disappears. The ambiguity is inherent to the tagged form — the codec's own comment says not to name a map key `_bytes` — but the generator can refuse to emit the one shape guaranteed to misdecode instead of leaving it to be found at runtime. Both front doors (the .lidl client-stub path and the --dep/--interface path) now reject it with a message naming the type and the fix. A second field disambiguates it (isTaggedBytes requires exactly one key), so that shape still generates — verified, not assumed. 181 tests, +2. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(cdylib): records, [bstr], typed maps and nested composites on the C++ provider The C++ cdylib backend could express scalars, `[scalar]`, `any` and an untyped map. Everything else it rejected BY NAME — `[bstr]`, `[[int]]`, `{tstr: int}`, and records, which the impl-header parser could not even declare because it skipped `struct` outright. That is why the ext contract had to be Rust-only. Four changes, in the order they matter: typeSupported() recurses instead of whitelisting element names, which admits [bstr], [[int]], [Record] and [{tstr: T}] in one rule; a declared record is admitted; a map now REQUIRES a tstr key (it used to `return true` for any map and then silently flatten {int: tstr} to an untyped LogosMap, losing the key type). lidlTypeToStdCdylib() became total. Its `lidlTypeToStd` fallback answers QVariantList / QVariantMap — Qt names in a Qt-FREE translation unit — and only failed to appear because the gate rejected everything that reached it. Widening the gate made that fallback a live leak, so composites now recurse and never reach it. <name>_types.h new: the generated codec, recursive, with a FULL specialization for std::vector<uint8_t> that wins over the generic vector rule — which is what keeps a bstr tagged at any depth instead of becoming a plain array of numbers. One Codec specialization per declared record, field by field, with the field path in the error. impl_header_parser learned `struct` (two passes, because a record field may name another record and the type mapper only answers Named() for an already-registered name — one pass silently typed `Blob inner;` as `any`), std::map<std::string, T>, and recursion into vector elements so std::vector<Blob> is [Blob] rather than falling through to `any`. Records are only names the contract DECLARES: `void` is not a LIDL builtin, so `-> void` arrives as Named("void"), and treating every Named as a record is the exact trap that made the Rust generator emit `-> Void`. Two things the interface JSON got wrong, both found by running it: - it spelled a record `Blob` and a `[Record]` `QList<Blob>`. Those are the CONSUMER's names, correct in a generated wrapper where the struct exists — but this JSON is the module's getMethods(), read by the host to marshal a QVariant, and there is no metatype called `Blob`. The host SIGSEGV'd on the first call to any record method. A record IS a variant map at that boundary; lidlTypeToQtWire() says so. - the types header emitted the structs. Header-first, the author owns them and the contract was derived from those very declarations, so it was a redefinition. It emits forward declarations and the codec. Also: `jsonReturn` is set by the front end for any map return, which no longer implies the C++ type IS nlohmann::json now that a typed map is std::map<std::string, T> — checking the flag before the spelling emitted `result.dump()` on a std::map. The spelling decides. Scalars keep their nlohmann accessor verbatim rather than routing through the codec: `.get<int64_t>()` TRUNCATES a float instead of throwing, and the conformance matrix pins that leniency (hostile/int/fractional expects 3 from 3.7). Changing it would silently move behaviour something depends on. The pinned-rejection test for [bstr] is INVERTED rather than deleted — the cell it pinned still matters, only its answer changed — plus new tests for the non-tstr map key rejection and for declared-vs-undeclared records. 183 tests. Every existing module still builds; test_fullapi_cpp is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(records): only structs the API mentions become contract types Teaching the impl-header parser to read `struct` (previous commit) published EVERY struct in a header as a contract `type`. Two production modules already carry private helpers: openmetrics-module struct ModuleSource (namespace scope, internal) logos-package-manager struct PendingAction (PRIVATE, inside the class) Both were being published — a module's interface changing as a side effect of an internal refactor, which is not something deriving a contract from a header may do. PendingAction was published WRONG as well: its fields carry trailing `// comments`, the field regex requires a line ending in ';', and the unmatched fields were silently dropped. A record with a partial field list is worse than no record, because it looks like a contract. A struct now earns its place by appearing in a method or event signature — transitively, since a published record's own fields may name others. Verified on the real headers: package-manager and openmetrics publish zero types again, while the ext provider keeps both Blob and Wrapper (Wrapper is reachable only through Blob's use in a signature). Trailing comments are stripped before the field match, so no field is dropped. Two tests over a fixture carrying both an internal namespace-scope struct and a private in-class one; 185 tests. test-modules and openmetrics both rebuild. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(doctests): the generator round-trip now shows 64-bit ints and typed records The two failing assertions in cpp-sdk-generator-roundtrip were documentation asserting the OLD behaviour, and both changes are the point of this branch: int record(int id, …) -> qlonglong record(qulonglong id, …) QVariant translate(QVariant p) -> Point translate(const Point& p, …) `record`'s `id` is a `uint64_t` in the impl header, so the old signature was handing a caller a SIGNED 32-bit value for a `uint` — the doc showed the bug. The surrounding prose was wrong too, not just the expectations, so both blocks are rewritten rather than patched: * Flow 3 now states the mapping as int->qlonglong / uint->qulonglong and says why (LIDL int/uint are int64_t/uint64_t in every other binding), pointing at `record` as the worked example. * The composite section claimed "records and optionals surface as QVariant". Records now generate a struct, `[Point]` a QList<Point> and `{tstr: Point}` a QMap<QString, Point>; maps of `any`, optionals and bare `any` still cross untyped and stay QVariant/QVariantMap — a record has a declared shape, those do not. The new text draws that line explicitly. Expectations added for `struct Point` and `Point bounds(const QList<Point>&…)` so the record path is pinned in the doc, not just described. Verified the way CI runs it — `--release-for logos-cpp-sdk=feat/qt-64bit-numerics`, which is what makes `{release}` resolve to this branch instead of master: 10 passed, 0 failed. (A plain local run builds master and is not representative — that is why it still showed the old signatures.) outputs/ regenerated; the diff also picks up unrelated pre-existing drift where the committed Markdown had fallen behind the spec. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8a40a98322 |
feat(cdylib): support [bstr] — arrays of byte strings (#111)
A universal module could take or return a single blob (`bstr`) but not a list
of them: `std::vector<std::vector<uint8_t>>` parsed to `[bstr]`, which the
cdylib gate rejected by name. Authors had to flatten to hex or base64 strings
by hand. logos-execution-zone-module hit this on
send_generic_private_transaction(..., program_dependencies), whose dependency
ELFs are exactly a list of blobs.
The gate excluded `[bstr]` because the array path decodes with a blanket
`expr.get<inner>()`, and `[bstr]` elements arrive as the canonical tagged
{"_bytes": base64url} OBJECT — nlohmann refuses that, and a number-array
element would silently bypass the base64 decode. So admitting the type needed
a per-element codec, not just a whitelist entry.
Adds one, on top of the scalar codecs already emitted:
- lidlBytesListFromJson: element-wise lidlBytesFromJson, so each element may
independently be tagged, a plain string or a number array; a non-array arg
yields an empty list instead of throwing, matching the scalar decoder.
- lidlBytesListToJson: element-wise lidlBytesToJson, so a returned or emitted
list carries the tagged form per element instead of nested number arrays
that no consumer decodes as bytes.
Wired into all three places the type can appear — method params, method
returns, event payloads — and both helpers are gated (usesBytesArray /
hasBytesArrayEventParam) so a module that never carries a byte-string array
gains no unused static function, matching how the scalar encoder is gated.
Nothing outside this generator needed changing: lidlTypeToStd already spelled
`[bstr]` as std::vector<std::vector<uint8_t>>, the wire form is protocol's
existing tagged-bytes encoding, and consumers see `[bstr]` as QVariantList
exactly like `[int]` — with nested QByteArray preserved through
qvariantToNlohmann since logos-protocol#23.
Tests: 171/171. New coverage for the param decode, the return encode, the event
encode, and the unused-helper gating; the test that enshrined the rejection is
now an eligibility + tagging assertion.
Verified end to end, not just as generated text — a module with `[bstr]` as
param, return and event payload, driven through logoscore over the real
transport:
param : json:[{"_bytes":"AH-A_w"},{"_bytes":""},{"_bytes":"3q2-7w"}]
-> "3|4:0:255:510|0👎-1:0|4:222:239:824" (byte-exact; the 0x80
and 0xff bytes survive, and the empty element stays an element)
return : [{"_bytes":"AH-A_w"},{"_bytes":""},{"_bytes":"3q2-7w"}]
event : {"arg0":[{"_bytes":"AH-A_w"},{"_bytes":""},{"_bytes":"3q2-7w"}]}
And lez_core's real header now generates, decoding program_elf with the scalar
codec and program_dependencies with the list one.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
f032cf291e |
fix(header-parser): join multi-line method declarations before parsing (#91)
* fix(header-parser): join multi-line method declarations before parsing * strip qualifiers/attributes before return-type matching (#92) --------- Co-authored-by: Álex <alex93cabeza@gmail.com> |
||
|
|
1bc101df1f |
feat: cpp-generator consumes logos-lidl; delete embedded frontend (#89)
* feat: cpp-generator consumes logos-lidl; delete embedded frontend The canonical LIDL frontend now lives in logos-lidl. cpp-generator links it and keeps only the C++/Qt-specific parts (impl-header parsing, the gen_client/ gen_cdylib backends, the Qt type-name mapping). - Delete the embedded lidl_lexer/parser/serializer/validator/ast. - Add experimental/lidl_compat.h: brings logos-lidl's std AST into the global scope the backends use (via `using`), a qs() std::string→QString helper, a QTextStream<<std::string overload, and name-compatible shims (lidlParse/ lidlSerialize/lidlValidate) so the emission code keeps compiling. - Re-point impl_header_parser, lidl_gen_client (+ Doxygen /// docs on the generated client methods), lidl_gen_cdylib, lidl_emit_common, and legacy/ main at lidl::ModuleDecl. - CMake: C++17 + find_package(logos-lidl) + link logos-lidl::logos_lidl. - bin.nix: distribute only the shared C++/Qt backend helpers (compat + impl_header_parser + emit_common) under share/lidl-frontend, not the frontend. - tests: drop the 4 frontend test files (covered by logos-lidl now); the backend tests link logos-lidl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: pin logos-lidl to the C-ABI commit + lock it The logos-lidl input was declared in flake.nix but missing from flake.lock, so override chains that don't reach the nested input (the doctest harness building a scaffolded module) couldn't resolve it. Pin the branch rev and lock it so the component is self-contained. Re-point at master once logos-lidl lands. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: re-point logos-lidl to merged master (#5) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
bb6d87b6ec |
fix: parse declarations on same line as logos_events: / access specifier (#76) (#81)
* fix: parse declarations on same line as logos_events/access specifier (#76) The impl header parser updated its section state and immediately broke out of line processing when it matched `logos_events:` (or `public:`/`private:`), discarding any declaration on the same physical line. This meant clang-format / prettier output like logos_events : void versionReady(const std::string &version); silently dropped the event, while the newline-separated form parsed fine — the same valid C++ was handled differently based on formatting. Strip any leading section specifiers in a loop, updating the section state, then let the remainder of the line fall through to the declaration parser. Brace counting still happens once per physical line and blank-line doc-comment reset is preserved. Adds a regression test (SameLineSectionSpecifiers) with a fixture covering the exact prettier form from the issue, a follow-on same-line event, the newline form alongside it, and the symmetric inline `public:` method case. Fixes #76 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: attach doc comments to same-line logos_events/access-specifier decls Address review feedback: the first pass cleared pendingDoc on every specifier match, so a `///` comment above a collapsed `logos_events : void foo();` did not attach to the event. In the collapsed form there is nowhere else to put the doc comment, so this left documentation formatting-dependent — the same bug class as #76, one level up. Only clear pendingDoc for a *bare* specifier (a section boundary, matching Qt `signals:` semantics); when a declaration shares the line, keep the pending doc so the declaration parser attaches it. Extend the fixture with a `///`-documented same-line event and assert the description is captured. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7b62ac2017 |
Per-event documentation + getPluginEvents introspection (#71)
* Add per-event documentation + getPluginEvents introspection Mirror the per-method documentation pipeline for events. Events (declared in a universal module's logos_events: section) now carry a description parsed from their /// doc comments, and are introspectable at runtime via a new getPluginEvents framework call. - lidl_ast: EventDecl gains a description field. - impl_header_parser: capture the event's doc comment (previously discarded) and an optional metadata.json events[].description. - lidl_gen_provider: generated universal provider emits getEvents() override, mirroring getMethods() (name/signature/ parameters/description; no returnType/isInvokable — events are void). - logos_provider_object: default-empty virtual getEvents() so the legacy provider path and QtProviderObject inherit empty. - module_proxy / qt_provider_object: intercept getPluginEvents next to the getPluginMethods special-case. - docs: spec + README event-documentation notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Add unit tests for event documentation + getEvents generation Address review feedback (#71): cover the event-introspection paths that previously only had method-side tests. - impl_header_parser test: assert metadata.json events[].description is parsed; new documented_events fixture asserts `///` doc-comment capture on a logos_events: block (multi-line joined with \n, adjacent-only, plain // ignored). - lidl_gen_provider test: assert the generated dispatch contains getEvents() emitting each event's name/signature/parameters and an escaped description, and that events carry no returnType/isInvokable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fold event introspection into getMethods() to keep the provider ABI stable The previous approach added a getEvents() virtual to LogosProviderObject, which inserted a new vtable slot and shifted every later slot — an ABI break that would misdispatch virtual calls whenever an old and new host/module were mixed across the in-process plugin boundary. Instead, report events INSIDE the existing getMethods() call: it now returns the module's whole interface, with each entry tagged type "method" or "event" (events omit returnType/isInvokable). The provider vtable is therefore byte-for-byte unchanged, so old/new hosts and modules stay binary-compatible — a new host reading an old module sees no event entries (zero events), and an old host reading a new module just ignores the "type" field (cosmetic). An entry with no "type" is treated as a method. - logos_provider_object.h: remove the getEvents() virtual; document that getMethods() carries both, and why. - generator (lidl_gen_provider): emit events as type "event" entries inside getMethods(); tag methods type "method"; no getEvents() output. - module_proxy / qt_provider_object: getPluginMethods()/getPluginEvents() are now type-filtered views of getMethods(), plus a new getPluginInterface() returning the whole list. (These are name- dispatched Q_INVOKABLEs, not vtable surface — adding them is safe.) - tests: generator asserts events fold into getMethods() tagged "event"; ModuleProxy asserts the three filtered views; parser tests unchanged. - docs: spec/project/docs/README updated, incl. an ABI rationale note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
c71089abe9 | remove legacy emitEvent (#69) | ||
|
|
8bdbd13848 |
Extend universal modules with module context (#61)
* extend universal modules with module context * implement module calls and events for universal modules * pr comments |
||
|
|
f7c855b110 | add logos result type (#55) | ||
|
|
1468180b25 | add support for new types (#50) | ||
|
|
d633575677 |
add IDL parser & generator (wip) (#33)
* add IDL parser & generator (wip) * fix fixtures issues affecting tests * fix fixtures issues affecting tests |