Commit Graph
13 Commits
Author SHA1 Message Date
Dario Gabriel LipicarandClaude Opus 5 dfeef26697 fix(generator): one declaration, one binding — optional record fields on the legacy path
`? maybe: tstr` and `maybe: ?tstr` are the same declaration. logos-lidl's
docs/spec.md says so and requires them to produce byte-identical code, and
logos-lidl#7 added fieldIsOptional()/fieldValueType() precisely so no backend
re-derives the answer. The cdylib, client-stub and both Rust backends honour
that. The LEGACY consumer path — the one every real C++ module builds through,
`logos-cpp-generator --general-only --dep <name>=<lidl>` from
logos-plugin-qt's buildPlugin.nix — did not:

      ? maybe: tstr   ->  QString maybe{};   __m.value("maybe").toString()
        maybe: ?tstr  ->  QVariant maybe{};  __m.value("maybe")

and on `--api-style lp`, `std::string maybe{}` vs `LogosMap maybe{}` — neither
of them std::optional. The flag spelling is the one production contracts use:
logos-chat-module writes all five of its optionals that way, so every one of
them landed on the branch that silently defaults. A `QString` has no empty
inhabitant at all; an absent `nickname` and an empty-string one were the same
value by the time the consumer saw them.

ROOT CAUSE. legacy/main.cpp's moduleRecordsToJson / moduleMethodsToJson /
moduleEventsToJson flatten every TypeExpr into a single Qt TYPE-NAME STRING.
Answering "is this optional" from a name means answering it from the verbatim
spelling, which is the one thing the accessors exist to stop.

WHAT THIS CHANGES. The record-field half of that boundary, and only it. A field
object now carries `optional` alongside `type`, where `type` is the VALUE type
(fieldValueType) and `optional` is true for either spelling (fieldIsOptional).
Both spellings arrive at the emitter as the same object, so they leave as the
same code. Per surface, matching the sibling backend that already serves it:

  Qt  — QVariant, as lidl_gen_client.cpp already emits. Qt has no optional
        template; an invalid QVariant is its single empty inhabitant. Two-state,
        untyped.
  Lp  — std::optional<T>, as lidl_gen_cdylib.cpp already emits, encoded by
        logos-protocol's Codec<std::optional<T>>. Keeps the value type.
        `?any` / `?{K:V}` / `?[any]` collapse onto the bare LogosMap/LogosList:
        nlohmann::json already carries null, so wrapping it would give the slot
        two empty spellings — three states, which the two-state rule forbids.

Encode omits the key when empty (a record field is a NAMED slot); decode treats
an absent key and an explicit null as the same state, so neither turns empty
into "" or 0. The round trip is canonicalising, as the spec requires.

The three functions move to legacy/lidl_to_json.{h,cpp}. Not cosmetic: the rule
is a property of frontend -> JSON -> emitter, and while they sat inside a TU
with main() no test could observe it. tests/generator/test_optional_spellings.cpp
now runs that composition end to end.

WHAT THIS DOES NOT CHANGE, and why. POSITIONAL slots — method parameters,
return types, event parameters — are still flattened to QVariant (Qt) /
LogosMap (Lp). They have no name to hang a flag on, so they only ever had the
type-kind spelling and there is no divergence there to fix; what they lose is
the value type. Closing that changes generated method SIGNATURES, i.e. a source
break for every existing call site, for a defect this commit is not about.
`OptionalSpellings.PositionalSlotsAreStillFlattened` pins the current behaviour
so closing it later is deliberate, and the generator still prints a `Note:`
naming every slot it flattens. Nesting, map key types and descriptions still do
not cross the boundary either — the flag is per-field, not a general widening.

VERIFIED BY RUNNING, each check shown to fail when the property does not hold:

  - Two contracts identical but for the spelling, generated with `--dep` on both
    `--api-style qt` and `--api-style lp`: byte-identical. The SAME comparison on
    a generator built from this base without the fix reports a difference on
    both styles.
  - Harness sensitivity: two contracts differing only in `count: uint` ->
    `count: int` are reported as different, so the compare is not vacuous.
  - A contract with no optional field generates byte-identically to the
    unpatched generator, both styles — and the same compare reports a difference
    when given genuinely different output.
  - The 5 new assertions FAIL on this base with only the extraction grafted in
    (generator_lib.cpp pristine) and pass with the fix; the 3 control assertions
    pass on both, which is what makes them controls.
  - Generated wrappers compile on both surfaces, for the probe contracts and for
    the real logos-chat-module and test_fullapi_ext_rust contracts.
  - Emitted lp record codec exercised at runtime: empty omits the key, an
    explicit null decodes to nullopt rather than "", uint64 above 2^63 survives,
    bstr stays canonically tagged, and `{"maybe": null}` re-encodes omitted.
  - `nix build .#tests` green (107 tests), `nix build .#cpp-generator` green.

logos-qt-sdk's qt-generator has the same defect in lidl_gen_qt_consumer.cpp
(record fields read `f.type` directly); it is a separate repo and not touched
here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 18:35:09 -03:00
Dario LipicarandClaude Opus 5 5a809c17a6 docs(generator): record where a dependency entry is read (#126)
The generator's own description still had `dependencies[]` as a list of names,
and the shared-helper list under share/lidl-frontend still had the three files
it had before metadata_dependencies.h joined them — the one a reader consults
before adding a header that impl_header_parser.cpp includes, which is how the
qt-generator's build breaks from a change made here.

Documents both entry forms where the umbrella is described, and why the array
is read once rather than per emitter: the aggregate is emitted by several
passes, and passes that answer that question separately can answer it
differently, which is a member whose type was never included.

Also notes the umbrella emitters' new home in generator_lib, the tests that
cover them, and the fixture declaring both forms.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 08:39:13 -03:00
Dario LipicarandClaude Opus 5 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 bef3ef5 were no-ops.
    With only `--override-input logos-cpp-sdk <sha>`, that node's logos-lidl
    already resolves to 35f33d87 out of cpp-sdk's own lock — nix >= 2.26
    carries an overridden input's lock, and CI runs Determinate Nix. Verified
    by resolving the lock with and without them: byte-identical.
  * The `logos-module-client/...` overrides never matched anything. Nix says so
    out loud ("does not match any input"): logoscore-cli has no such root
    input; module-client only appears under logos-test-modules/, outside the
    runtime closure. The prose claiming it pins the SDK is corrected too.

cpp-sdk-concurrent-dispatch is fixed here as well — it failed the same way and
carried no lidl overrides at all.

VERIFIED locally against bef3ef5, the exact commit CI failed on:

  * accounts .lgx  -> exit 0, logos-accounts_module-module-lib.lgx (5,939,898 B)
  * logoscore CLI  -> exit 0, ./logos/bin/logoscore reports
                      "logos-cpp-sdk bef3ef57d3f489073672e70a786c550df7edd003"
  * negative control (same command minus the single qt-sdk lidl flag) fails
    with CI's exact derivation,
    /nix/store/pf96n2ldvhy6sq39ygkh5zdqx7dcn4df-logos-qt-generator-0.1.0.drv
  * no "does not match any input" warnings remain on any command

The durable fix is a one-line bump of logos-qt-sdk's own flake.lock logos-lidl
to master (logos-lidl#7 is purely additive: six new inline helpers, nothing
removed or renamed). Once qt-sdk carries it, every override added here can go.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 07:19:47 -03:00
Dario LipicarandClaude Opus 5 d11fbb2220 docs: drop the reference to the deleted lidlGenerateProviderGlue (#124)
logos-qt-sdk#23 deleted that function -- it was a second, parallel top-level
pipeline over the same emitters main.cpp already drives, with no callers
anywhere in the workspace. This doc bullet was its only surviving reference.

The .lidl sidecar the bullet described is emitted elsewhere and is unaffected:
main.cpp:92/:184 here, and module-builder's modulePreConfigure.nix:75.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 00:38:42 -03:00
Dario LipicarandClaude Opus 5 5d0a99a182 chore(generator): retire ApiStyle::Std (#122)
The Std surface — std-typed signatures over a QVariant + LogosAPIClient
body — no longer had a caller. `interface: "universal"` modules moved to
`lp` (std types over the Qt-free logos-protocol C ABI), and nothing else
ever selected it, so every Std branch was dead weight sitting in front of
the two live ones.

`--api-style=std` is now rejected with a message naming the retirement
rather than aliased to `qt`. A stale caller that still passes it wants
std signatures; handing it the Qt surface would fail later, further from
the cause.

The collapse is deliberate about the branches where Std was tested BEFORE
Qt, since a naive "delete the block containing ApiStyle::Std" changes Qt
output:

- makeHeader's include block tested Std first, so its `else` is the Qt
  include list — the Qt includes are kept and promoted, not deleted.
- recordToWireExpr / recordFromWireExpr returned the Qt map form from a
  guarded `if` and the Std form from the function's trailing `return`.
  The guard is dropped and the Qt form promoted to the tail; deleting
  only the trailing return would have left a path falling off the end.
- The private-member `else if (!events.isEmpty())` arm reads as an event
  test but was Std-only; the Qt arm (m_eventReplica + m_eventSource)
  survives, so setEventSource/trigger still have their storage.
- `if (apiStyle == Qt || !events.isEmpty())` is a disjunction, not an
  Std branch: it unwraps to an unconditional emit, keeping
  ensureReplica()'s declaration next to its definition.
- `isRec || style == Std` loses only the right disjunct — dropping
  `isRec ||` would double-wrap record fields in QVariant::fromValue.

mapParamTypeStd / mapReturnTypeStd / isStdRefType stay: they are the
shared std type table that ApiStyle::Lp reaches through the non-Qt arm of
paramTypeFor / returnTypeFor / byRefFor and directly from lpPushExpr /
lpFromJsonExpr.

Verified by output equivalence rather than by the build succeeding: the
generator was run over 13 fixture cases (the full_api contract as both a
bound interface and a baked dep, three record-bearing contracts incl.
map-of-record fields, the chat module's production contract, and a
no-events contract) for both qt and lp, before and after. `diff -r` over
the 194 resulting files reports no differences, and the experimental
--lidl backends are byte-identical too. Test suite: 180/180, unchanged.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 21:41:52 -03:00
Dario LipicarandClaude Opus 4.8 182d850e72 docs: update project.md for the logos-lidl frontend split (#90)
The embedded LIDL frontend (lidl_ast/lexer/parser/serializer/validator) was
deleted — it now lives in the logos-lidl repo, linked via find_package and
bridged onto the Qt backends by experimental/lidl_compat.h. Update the project
doc to match: describe the consumed logos-lidl frontend + the compat shim,
list the backends cpp-generator actually keeps (lidl_emit_common,
lidl_gen_client, lidl_gen_cdylib, impl_header_parser), drop the deleted
frontend files and their (removed) tests, and note the qt provider glue lives
in logos-qt-generator and the Rust backend in logos-rust-sdk.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 23:35:56 -03:00
Dario LipicarandClaude Opus 4.8 87abcd8044 Cdylib authoring: --backend cdylib emits the common C ABI wrapper + uniform Qt glue (#84)
* 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).

* Cdylib authoring backend: --backend cdylib emits the common C ABI wrapper + uniform Qt glue

From a module's LIDL contract the generator now emits:
- <name>_module_impl.cpp — the Qt-FREE logos_module_impl.h export
  wrapper (dispatch/get_methods/set_context/set_emit_callback/
  accept_token/get_protocol_version/string_free) around the universal
  impl class; compiled into the module's cdylib. Tagged {"_bytes"}
  bytes, StdLogosResult -> {success,value,error}, context via the
  existing _logos_codegen_::maybeSet* SFINAE helpers.
- <name>_events_cdylib.cpp — typed logos_events: bodies marshalling
  into nlohmann::json (the cdylib flavor of the events sidecar).
- <name>_cdylib_glue.{h,cpp} — the UNIFORM Qt-plugin glue forwarding
  LogosProviderObject to the C symbols; identical regardless of the
  module's source language (the Rust SDK emits the same exports).
- the .lidl sidecar.

Qt-container types are rejected at generation time (a cdylib impl is
Qt-free by definition). Verified end-to-end by dlopen smoke: typed
dispatch, typed events through the emit callback, result returns,
introspection, and the protocol-version handshake — and the SAME C
harness passes against a Rust cdylib generated by logos-lidl-gen
--provider.

* 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>

* 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>

* generator: glue-only cdylib mode for non-C++ impls

--lidl x.lidl --backend cdylib (no --impl-class) emits just the uniform
Qt-plugin glue; the C exports come from the module's own language
backend (e.g. the Rust SDK's lidl-gen --provider). This is the entry
point logos-module-builder's cdylib interface uses for Rust modules.

* cdylib glue: root plugin implements PluginInterface

logos_host's module_initializer hard-requires PluginInterface on the
root plugin before any provider detection — same bases as the qt glue
(QObject, PluginInterface, LogosProviderPlugin). Caught by the first
host-loaded run of a cdylib-authored module; the dlopen smoke harness
exercised only the C seam.

* cdylib glue: seed the cdylib's protocol stack with the host auth token

The glue's init() reads the authToken property module_initializer now
surfaces on the LogosAPI object and forwards it across
logos_module_accept_token under the initializer's own keys
(core / capability_module). Without it the cdylib's TokenManager (a
separate static copy of the singleton) is empty and every outbound
call — including the capability requestModule bootstrap — is rejected
as unauthorized.

* cdylib glue: provider derives LogosProviderBase; tokens land in BOTH stacks

informModuleToken = base save (host-stack TokenManager — what ModuleProxy
validates inbound calls against, incl. the grant the daemon pushes after
a capability requestModule) + C-ABI forward (the cdylib's own stack, for
outbound auth). init folds into onInit; the authToken property seeding
stays.

* lock: protocol integer-fidelity fix

* 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.

* fix: restore a clean flake.lock after the merge conflict (protocol 176fbc8)

* 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.

* generator: contract-first C++ cdylib modules from --lidl

--lidl x.lidl --backend cdylib with --impl-class/--impl-header now emits
the FULL set (C-ABI export wrapper + events + uniform glue) around the
named hand-written Qt-free impl class — the C++ mirror of declaring the
contract in .lidl and implementing the Rust trait. Without --impl-class
the glue-only mode is unchanged.

* 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.

* cdylib: fire the context-ready hook at module LOAD, not first dispatch

The impl-exports wrapper used to fire maybeSetContext (stamp + hook)
immediately inside logos_module_set_context — which the glue calls during
onInit, BEFORE the auth token is seeded and BEFORE ModuleProxy wires the
emit callback. A hook that made outbound calls or emitted events ran
half-wired; and the Rust scaffold deferred its hook to first dispatch
entirely, so a Rust module couldn't subscribe/emit/act until someone
called it — a capability gap vs the universal C++ path.

Two changes, applied uniformly:
- The glue's onInit now seeds the auth token FIRST and forwards the
  context LAST (the same context-last convention as the universal onInit
  ordering fix).
- The impl-exports gained a ready-latch (lidlTryFireContext): the context
  is stored on set_context and the hook fires ONCE as soon as both the
  context AND the emit callback have been delivered — during module
  registration, before the module is published for inbound calls. Hosts
  that never wire an emit callback still get the hook before the first
  dispatch (requireEmit=false fallback in logos_module_dispatch).

The Rust scaffold (logos-rust-sdk lidl-gen) implements the same latch, so
on_context_ready now matches C++ onContextReady semantics: subscriptions,
authenticated outbound calls and typed emission all work from the hook at
load time.

* 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.

* generator: --backend ui — universal authoring for UI plugin backends

New emitters (lidl_gen_ui.{h,cpp}) for type=ui_qml + interface=universal
modules: from the author's single clean impl class (optionally deriving
LogosModuleContext), generate
  <name>.rep            — the view contract, one SLOT per public method
                          (framework hooks like onContextReady excluded)
  <name>_ui_interface.h — PluginInterface subclass + IID
  <name>_ui_glue.{h,cpp}— plugin deriving <Cls>SimpleSource +
                          <Cls>Interface + <Cls>ViewPluginBase; slots
                          forward to the impl (std<->Qt at the boundary);
                          Q_INVOKABLE initLogos(LogosAPI*) builds
                          LogosModules, wires modules(), stamps context
                          (fires onContextReady — same order as the
                          universal core glue), then setBackend(this).

Typed dependency callers, typed event subscriptions and bind_<interface>
binders come from the existing umbrella pass (logos_sdk.h), which already
runs for UI modules. ui-host's reflection-based initLogos call site is
unchanged; legacy LogosAPI*-based UI plugins are untouched. v1 view API
types: void/int/uint/float64/bool/string; logos_events: rejected for ui
backends (views talk to QML via .rep, not module events).

* 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.

* generator: Qt glue emission removed — logos-qt-generator owns it

The deletion half of the generator split (counterpart: logos-qt-sdk
3b37474, builder d7a2272). This tool now emits ONLY Qt-free outputs:

  kept     std typed wrappers + logos_sdk umbrella (--general-only),
           cdylib C-ABI impl-exports + typed event emitters
           (--lidl/--from-header --backend cdylib --impl-class),
           LIDL derivation/serialization (--header-to-lidl), client stubs
  removed  universal Qt glue (--backend qt), the uniform cdylib Qt glue,
           the ui backend emitters — all relocated verbatim (byte-identical
           output verified) to logos-qt-generator; invocations here now
           fail with a pointer to the right tool

The provider-glue golden tests travel with the emitters (to be re-homed
in logos-qt-sdk's test suite); test_lidl_type_mapping stays — it covers
lidl_emit_common, which both generators compile.

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

* 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.

* docs: note the generator split (Qt glue emission lives in logos-qt-sdk)

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 22:10:16 -03:00
Dario LipicarandClaude Opus 4.8 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>
2026-06-04 17:06:12 -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 c71089abe9 remove legacy emitEvent (#69) 2026-06-02 15:54:54 -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 1468180b25 add support for new types (#50) 2026-04-13 13:29:26 -04: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