* 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>
19 KiB
Logos Code Generator — Project Description
Project Structure
cpp-generator/
├── main.cpp # Entry point — dispatches to legacy or experimental
├── CMakeLists.txt # Build config
├── compile.sh # Standalone build script
├── legacy/ # Original generator (unchanged from master)
│ ├── main.cpp # legacy_main() — plugin/metadata/provider-header modes
│ ├── generator_lib.h/cpp # Shared utilities, type mapping, header parser
│ └── legacy_main.h # Forward declaration
├── experimental/ # C++/Qt-specific generator backends
│ ├── lidl_compat.h # Bridges the backends onto logos-lidl's std AST
│ ├── lidl_emit_common.h/cpp # LIDL type → Qt/std type-name mapping
│ ├── lidl_gen_client.h/cpp # Typed client stub generation (+ Doxygen /// docs)
│ ├── lidl_gen_cdylib.h/cpp # cdylib module-impl C-ABI export generation
│ └── impl_header_parser.h/cpp # C++ impl header → lidl::ModuleDecl
│ # The lexer/parser/AST/serializer/validator now live in the standalone
│ # logos-lidl repo (linked via find_package(logos-lidl)); the Qt glue
│ # emitters live in logos-qt-sdk's logos-qt-generator.
└── docs/ # This documentation
Components
Entry Point (main.cpp)
Checks for --from-header or --lidl flags before creating QCoreApplication. If neither is present, falls through to legacy_main().
LIDL frontend — logos-lidl (consumed as a library)
The lexer, parser, AST, serializer, and validator are no longer embedded here — they live in the standalone logos-lidl repo, the language-neutral (Qt-free) common frontend every Logos SDK shares (C++ here, Rust in logos-rust-sdk, …). cpp-generator links it via find_package(logos-lidl) and reaches it through experimental/lidl_compat.h.
logos-lidl exposes (namespace lidl):
lidl::parse(std::string) → ParseResult(ModuleDecl+ error/line/column)lidl::serialize(ModuleDecl) → std::stringlidl::validate(ModuleDecl) → ValidationResult- the AST:
TypeExpr(Kind: Primitive/Array/Map/Optional/Named,name,elements),ParamDecl,FieldDecl,MethodDecl(name, params, returnType,description,jsonReturn,resultReturn),EventDecl(name, params,description),TypeDecl,ModuleDecl. (logos-lidl also exposes an AST↔JSON bridge and a C ABI that the Rust SDK consumes over FFI — not used by this generator.)
The .lidl grammar (defined in logos-lidl):
module = "module" IDENT "{" body "}"
body = (metadata | type_def | method_def | event_def)*
metadata = "version" STRING | "description" STRING | "category" STRING
| "depends" "[" (IDENT ("," IDENT)*)? "]"
type_def = "type" IDENT "{" field* "}"
field = "?"? IDENT ":" type_expr
method_def = "method" IDENT "(" params ")" "->" type_expr ("description" STRING)?
event_def = "event" IDENT "(" params ")" ("description" STRING)?
params = (IDENT ":" type_expr ("," IDENT ":" type_expr)*)?
type_expr = IDENT | "[" type_expr "]" | "{" type_expr ":" type_expr "}"
| "?" type_expr
Validation (in logos-lidl) checks: empty module name, duplicate type/method/event names, builtin type shadowing, unknown named type references, duplicate parameter names. Serialization round-trips ModuleDecl back to .lidl text (incl. the trailing description "…" clause).
Compat shim (lidl_compat.h)
Bridges the existing Qt-flavored backends onto logos-lidl's std AST so they compile unchanged:
- brings the
lidl::AST types into the global scope the backends use (viausing) qs(std::string) → QStringplus aQTextStream << std::stringoverload, so emission of AST string fields just works- name-compatible shims
lidlParse/lidlSerialize/lidlValidateoverlidl::parse/serialize/validate
Type Mapping (lidl_emit_common.h/cpp)
lidlTypeToQt(TypeExpr)/lidlTypeToStd(TypeExpr)— LIDL type → Qt / std type-name stringslidlIsStdConvertible(TypeExpr)— whether a type has a pure-C++ (Qt-free) representationlidlToPascalCase(name)— convertssnake_casetoPascalCase
Optionality
?T is two-state: a value of T, or empty. Never three-state — "one LIDL type ↔ one
type per language" leaves nowhere for a third state, because every target has exactly one
empty inhabitant.
Two spellings, one meaning. A record field may be written ? name: T (the flag) or
name: ?T (the type kind); the spec binds them to the same declaration, so they MUST emit
identical code. Backends never answer this themselves — logos-lidl's fieldIsOptional(f) /
fieldValueType(f) (re-exported by lidl_compat.h) are the one place the two are
reconciled. Reading f.optional or f.type.kind == Optional on its own is a bug.
Wire rule. Absent and explicit null are the same state on decode and different on encode:
| empty is spelled | |
|---|---|
| decode, optional slot | absent or null → empty |
| decode, required slot | absent and null are both still errors |
| encode, named slot (a record field) | the key is omitted |
| encode, positional slot (argument, return, event param) | null — there is no key to omit, and arity must never change |
A round trip therefore canonicalises: a peer that sent "f": null gets the key back
omitted. A present-but-wrong-typed value is still an error — optional widens the domain by
exactly one inhabitant, it does not switch type checking off.
Per surface:
| Surface | ?T |
Notes |
|---|---|---|
cdylib / std (lidlTypeToStd, lidl_gen_cdylib) |
std::optional<T> |
encoded by logos-protocol's Codec<std::optional<T>>; key omission is the record emitter's job (a codec never sees the slot) |
?any / ?{tstr: any} / ?[any] |
LogosMap / LogosList |
collapses: nlohmann::json already carries null, so wrapping it would make the slot three-state |
Qt (lidlTypeToQt) |
QVariant |
two-state (an invalid QVariant is Qt's empty inhabitant) but untyped — see Known Limitations |
header-first (impl_header_parser) |
std::optional<T> ↔ ?T |
std::optional<std::optional<T>> has no LIDL type; it collapses to ?T and is reported on stderr |
Client stubs (lidl_gen_client.h/cpp)
lidlMakeHeader(ModuleDecl)/lidlMakeSource(ModuleDecl)— typed<Module>client wrapper; each method (and its…Asynctwin) carries a Doxygen///comment generated from the method'sdescriptionlidlGenerateMetadataJson(ModuleDecl)— generates metadata.json content
cdylib backend (lidl_gen_cdylib.h/cpp)
Emits the Qt-free half of a universal C++ cdylib module:
lidlCdylibSupported(ModuleDecl)— gate to the std-convertible (Qt-free) type subsetlidlMakeModuleImplExports(...)— thelogos_module_impl.hC-ABI export wrapper around the universal impl class (compiled into the module's cdylib; dispatches via nlohmann::json)lidlMakeEventsSourceCdylib(...)— typedlogos_events:bodies marshalling into nlohmann::json
Per-build API-style choice (legacy/generator_lib.{h,cpp})
The codegen exposes one wrapper class per module — <Module> — with signatures that match the API style picked at the consumer's build time. The two styles are mutually exclusive (no composite output):
--api-style |
Wrapper signatures |
|---|---|
qt (default) |
QString / QStringList / QVariantList / QVariantMap / int / LogosResult |
lp |
std::string / std::vector<std::string> / LogosMap / LogosList / int64_t / StdLogosResult, over the Qt-free logos-protocol C ABI |
(A third value, std — std signatures over a QVariant / LogosAPIClient body — was retired; the generator now rejects --api-style=std instead of aliasing it.)
Both styles emit:
- A
<Module>client class with sync method shapes + matching<method>Async(...)overloads. - The std variant additionally inlines Qt↔std conversion in its
.cppso the caller's translation unit needs zero Qt headers.
The umbrella logos_sdk.h is also generated per-build and aggregates every dep into a flat LogosModules struct — no nested view:
struct LogosModules {
LogosAPI* api;
SomeDep some_dep; // one accessor per `metadata.json#dependencies` entry
// ...
};
Only the modules explicitly listed as dependencies are exposed. The runtime's core_manager is intentionally NOT in LogosModules — apps that need to manage the core do so via liblogos' C API, not via a typed RPC wrapper.
ApiStyle enum + new helpers in generator_lib:
enum class ApiStyle { Qt, Lp }— passed to every wrapper-emitting function.- File-local
mapParamTypeStd/mapReturnTypeStd— the std-side type-mapping table thelpsurface exposes. Hidden fromgenerator_lib.h(not part of the public surface). makeHeader(moduleName, className, methods, apiStyle, events)/makeSource(moduleName, className, headerBaseName, methods, apiStyle, events)— single entry points that branch onapiStyleinternally to emit the right include block, signature shape, and conversion bridges.eventsis loaded from a<name>.lidlsidecar via--events-from; when non-empty, the wrapper also gets one typedon<EventName>(callback)adapter per declared event (callback arg types followapiStyle).
Flag plumbing:
metadata.json#interface == "universal"(or"cdylib") →mkLogosModule.nixadds-DLOGOS_API_STYLE=lptoextraCmakeFlags. Anything else ("legacy","provider", absent) leaves the defaultqt.LogosModule.cmakereads${LOGOS_API_STYLE}(defaultqt) and forwards--api-style=${LOGOS_API_STYLE}to thelogos-cpp-generator --general-onlyinvocation that writes the umbrella. Each module's Nix build emits two header derivations (<name>.headers-qtand<name>.headers-lp) viabuildHeaders.nix— onelogos-cpp-generator --api-style=…run per style, at the dep's build time. A consumer'sbuildPlugin.nixpicksdep.headers-${apiStyle}and copies itsinclude/straight into the build sandbox; no codegen runs at consume time. Nix's laziness means only the variant a downstream actually depends on is realised.legacy/main.cppparses--api-styleonce (rejecting the retiredstd) and threads the resultingApiStylethroughgenerateFromPlugin,writeUmbrellaHeader{,FromDeps}. No per-style filenames are ever emitted; each module gets a single<name>_api.h+<name>_api.cpppair regardless of style.
Provider Generation (logos-qt-generator)
The Qt provider glue (
lidl_gen_provider.{h,cpp}) is emitted by logos-qt-sdk'slogos-qt-generator, not this binary — it consumes the samelogos-lidlfrontend (+ the sharedlidl_emit_common/impl_header_parser/lidl_compat.hhelpers, distributed undershare/lidl-frontend). Documented here for reference.
lidlMakeProviderHeader(ModuleDecl, implClass, implHeader)— generates Qt glue header- Emits
nlohmannToQVariant()helper when any method hasjsonReturn = true - Always emits an
onInit(LogosAPI*) overridethat, via SFINAE'd helpers inlogos_module_context.h, (a) copies the three runtime-injected properties (modulePath,instanceId,instancePersistencePath) into the impl, (b) constructs a per-moduleLogosModulesaggregate and threads its pointer through the same base, and (c) installs the typed-event callback (maybeSetEmitEvent) consumed by<name>_events.cppmethod bodies. Impls that don't inheritLogosModuleContextcompile unchanged — the helper overloads collapse to no-ops. The fullLogosAPIis never exposed past the provider boundary. - Always emits
#include "logos_sdk.h"and astd::unique_ptr<LogosModules> m_logosModulesmember; ownership lives on the provider, the context base sees only a non-owningvoid*reinterpreted inLogosModuleContext::modules()(which depends on the impl's TU having includedlogos_sdk.h).
- Emits
lidlMakeProviderDispatch(ModuleDecl)— generates callMethod/getMethods dispatch.getMethods()emits the full interface: each method taggedtype: "method", then eachmodule.eventsentry taggedtype: "event"(name, signature, parameters, escapeddescription; no returnType/isInvokable). There is no separategetEvents()— folding events intogetMethods()keeps the provider vtable ABI-stable.lidlMakeEventsSource(ModuleDecl, implClass, implHeader)— generates<name>_events.cpp: Qt-MOC-style method bodies for prototypes declared in the impl'slogos_events:block. Each body marshals typed args into aQVariantListand callsthis->emitEventImpl_("<name>", &args)on the LogosModuleContext base.
Impl Header Parser (impl_header_parser.h/cpp)
parseImplHeader(headerPath, className, metadataPath, err)— parses C++ header + metadata.json into ModuleDecl- State machine:
LookingForClass→InClass→InPublic/InPrivate/InLogosEvents - The literal
logos_events:token (defined inlogos_module_context.has#define logos_events public) opens an events section; bare prototypes inside becomeEventDecl{name, params, description}entries appended toModuleDecl.events(thedescriptionis the doc comment immediately above the declaration, captured viajoinDocLinesexactly as for methods) - Skips: constructors, destructors, typedefs, using, friend, enum, struct,
std::functiondeclarations - Recognizes
LogosMapandLogosListreturn types (nlohmann::json aliases) and setsMethodDecl.jsonReturn = true - Recognizes
std::optional<T>→?T(see Optionality). Anything it does not recognize still falls back to the opaqueany, silently — that fallback is why an optional was unexpressible header-first until it was named explicitly - Template-aware parameter splitting (handles
std::vector<std::string>correctly)
CLI Usage
From C++ impl header (primary use case for universal modules)
logos-cpp-generator --from-header src/my_module_impl.h \
--backend qt \
--impl-class MyModuleImpl \
--impl-header my_module_impl.h \
--metadata metadata.json \
--output-dir ./generated_code
Generates: my_module_qt_glue.h, my_module_dispatch.cpp
From LIDL file — provider glue
logos-cpp-generator --lidl my_module.lidl \
--backend qt \
--impl-class MyModuleImpl \
--impl-header my_module_impl.h \
--output-dir ./generated_code
From LIDL file — client stubs
logos-cpp-generator --lidl my_module.lidl \
--output-dir ./generated_code \
--module-only
Legacy modes (unchanged)
logos-cpp-generator /path/to/plugin.so --output-dir ./generated
logos-cpp-generator --metadata metadata.json --general-only --output-dir ./generated
logos-cpp-generator --provider-header src/provider.h --output-dir ./generated
Consumer wrapper with typed event accessors
The --events-from <path> flag points the legacy <plugin>.dylib --module-only codegen at a LIDL sidecar shipped alongside the dep's pre-built headers. When set, the generated <name>_api.{h,cpp} gains one typed on<EventName>(callback) accessor per declared event (callback arg types match --api-style):
logos-cpp-generator /path/to/plugin.dylib \
--module-only --api-style lp \
--events-from /path/to/dep/share/logos/my_module.lidl \
--output-dir ./generated
In Nix builds this is wired automatically: buildHeaders.nix looks for <pluginLib>/share/logos/<name>.lidl (which buildPlugin.nix's installPhase placed there) and threads it through.
Building
The generator is built as part of logos-cpp-sdk:
ws build logos-cpp-sdk # builds everything including the generator
The generator binary is available as logos-cpp-generator in module build environments (provided by logos-module-builder's nativeBuildInputs).
Testing
Tests are in tests/experimental/:
ws test logos-cpp-sdk # runs all tests including experimental
The frontend tests (lexer/parser/validator/serializer) moved to the logos-lidl repo along with the code; only the C++/Qt-specific backends are tested here:
| Test file | What it tests |
|---|---|
test_lidl_type_mapping.cpp |
lidlTypeToQt, lidlTypeToStd, lidlIsStdConvertible, lidlToPascalCase, optionality on both surfaces |
test_lidl_gen_client.cpp |
Client stub generation: sync/async methods, events, metadata JSON, edge cases, both optional spellings agreeing |
test_lidl_gen_cdylib.cpp |
cdylib eligibility + emission: bytes at depth, records, typed maps, optionality (key omission, arity, ?any collapse) |
test_impl_header_parser.cpp |
Header parsing: type mapping, access specifiers, skipping private/protected, error cases, std::optional<T> |
(The lexer/parser/AST/serializer/validator round-trip + description tests live in logos-lidl's own tests/test_lidl.cpp.)
Fixture files in tests/experimental/fixtures/:
sample_impl.h— module with all supported type variationssample_metadata.json— metadata with dependenciescomplex_impl.h— module with multiple access specifier sectionsempty_class_impl.h— class with no public methodsempty_metadata.json— minimal metadataoptional_impl.h/optional_metadata.json—std::optional<T>header-first, incl. an optional over a declared record
Known Limitations
- The impl header parser is lightweight (regex + state machine). It does not handle:
- Multi-line method declarations
- Default parameter values
- Method definitions in the header (only declarations ending with
;) - Nested classes
- Template methods
std::functionmembers are silently skipped (never treated as methods)
- LIDL does not support generic/parameterized types or inheritance
--from-headeremits the cdylib backend here (theqtglue backend moved to logos-qt-generator); the Rust backend lives in logos-rust-sdk'slidl-gen, generating over logos-lidl's C ABI- Client stub generation (
lidlMakeHeader/lidlMakeSource) is only available from LIDL files, not from--from-header - Optionality is untyped on the Qt/Lp consumer surface. The consumer wrappers real
modules get come from
legacy/main.cpp→generateInterfaceWrappers→generator_lib, and the AST is flattened to a single Qt type-name string per slot atmoduleMethodsToJson/moduleRecordsToJson/moduleEventsToJson— a boundary that optionality (like nesting, map key types and descriptions) cannot cross.?Ttherefore arrives asQVariant: the right shape (an invalid QVariant is Qt's empty inhabitant, and the wire'snullbecomes exactly that) with no type, so a consumer gets no compile-time check and cannot tell?tstrfrom?uintor recover a?Record's struct. Carrying it further means widening that JSON surface with a per-slot optional flag and teaching both the Qt and Lp emitters to honour it. Until then the generator prints aNote:naming every flattened slot, so an affected build is never silent. lidlRecordCollidesWithBytesTagreads through an optional (viafieldValueType), so a single-_bytes-field record is refused under both spellings. It used to readf.type, which refused? _bytes: tstrand let_bytes: ?tstrthrough — the same declaration, two answers.?bstris unaffected either way: the tag lives in the value, not the slot.