From b72d9bb6b14e70103cd6c8b180eb4bc57de9112d Mon Sep 17 00:00:00 2001 From: Dario Gabriel Lipicar Date: Fri, 21 Aug 2026 20:32:15 -0300 Subject: [PATCH] test(doctests): the generator round-trip pins the lossless Qt spellings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `cpp-sdk-generator-roundtrip.test.yaml` is a CI gate (.github/workflows/doctests.yml), and two of its `expect_contains` were pinned to the type names the Qt consumer produced BEFORE the lossless mapping: QStringList labels(const QVariantList& ids QVariant nearest(const Point& p, QVariant limit The generator now emits `QList` and `std::optional` / `std::optional` for those slots, so both assertions failed. The `nearest` step's `run` grep was pinned the same way (`QVariant nearest`), so the line it was supposed to assert on was not even in the output being searched. Verified by running the spec's own steps against the generator built from this commit: 10 run-steps, 0 failures. The `[uint]` -> QList and `?T` -> std::optional lines were read out of the real `consumer/sensor_module_api.h` and `geometry/geometry_module_api.h`, not written from the mapping table. Prose too, in three places that described the old table: the Flow-3 type mapping ("other arrays -> QVariantList"), the composite-types intro ("optionals ... stay QVariantMap / QVariant"), and the composite-signature step. They now say what the mapping actually is — one LIDL type, one C++ spelling, with `any` the single deliberate exception — and `nearest` is called out as the one signature carrying both halves of the optional mapping. `doctests/outputs/cpp-sdk-generator-roundtrip.md` carries the same prose corrections. That tree is hand-pinned and CI never diffs it, which is exactly why it must be corrected by hand. Co-Authored-By: Claude Opus 5 --- .../cpp-sdk-generator-roundtrip.test.yaml | 39 ++++++++++++------- .../outputs/cpp-sdk-generator-roundtrip.md | 34 +++++++++++----- 2 files changed, 50 insertions(+), 23 deletions(-) diff --git a/doctests/cpp-sdk-generator-roundtrip.test.yaml b/doctests/cpp-sdk-generator-roundtrip.test.yaml index 7611e5d..9b6c4be 100644 --- a/doctests/cpp-sdk-generator-roundtrip.test.yaml +++ b/doctests/cpp-sdk-generator-roundtrip.test.yaml @@ -274,10 +274,14 @@ sections: variant that hands it a `logos::AsyncResult` (`{value, error}`) so a failed call is distinguishable from a legitimately default-valued one — and each `event` an `on(...)` subscription. The type mapping is the Qt - caller style: + caller style, and it is **lossless** — one LIDL type, one C++ spelling: `float64`→`double`, `tstr`→`QString`, `int`→`qlonglong`, `uint`→ - `qulonglong`, `bstr`→`QByteArray`, `[tstr]`→`QStringList`, other - arrays→`QVariantList`, and `result`→`LogosResult`. + `qulonglong`, `bstr`→`QByteArray`, `result`→`LogosResult`, and each + container carries its element type through — `[tstr]`→`QStringList`, + every other `[T]`→`QList` (so `[uint]` is `QList`). + Only `any` stays `QVariant`: it is the one Qt type that holds bytes *and* + an exact `uint64` *and* arbitrary nesting, so narrowing it would lose what + it was chosen to carry. The integer spellings are 64-bit, and unsigned stays unsigned: LIDL `int`/`uint` are `int64_t`/`uint64_t` in every other binding, so spelling @@ -302,7 +306,7 @@ sections: - "double temperature(" - "qlonglong record(qulonglong id, double value, const QString& note, bool valid" - "QByteArray firmware(const QByteArray& image" - - "QStringList labels(const QVariantList& ids" + - "QStringList labels(const QList& ids" - "LogosResult reset(const QString& id" - title: "The three call surfaces per method" text: | @@ -334,10 +338,14 @@ sections: generates `struct Point` plus the conversions, so a caller writes `Point p = client.translate(q, 1, 2)` instead of digging fields out of a `QVariantMap`. `[Point]` is a `QList` and `{tstr: Point}` a - `QMap`. Maps of `any`, optionals (`?T`) and `any` itself - still cross as untyped JSON and stay `QVariantMap` / `QVariant` — a record - has a declared shape, those do not. Here is a contract that uses all of - them, taken straight to a consumer header. + `QMap`. An **optional** `?T` is a + `std::optional` — two-state in the type system, so an absent value is + not spelled the same way as a present default, and `?uint` is + distinguishable from `?tstr`. Only `any` — and any container whose element + type bottoms out at `any`, such as `{tstr: any}` — stays untyped, as + `QVariant` / `QVariantMap`: a record has a declared shape and an `any` does + not. Here is a contract that uses all of them, taken straight to a consumer + header. steps: - title: "geometry_module.lidl" file: @@ -374,10 +382,15 @@ sections: text: | `Point` is generated as a struct, so a record parameter is taken by const-ref and a record return comes back typed. An array-of-records is a - `QList`. A map of `any`, an optional and a bare `any` stay - `QVariantMap` / `QVariant` — the untyped carriers for JSON whose shape - the contract does not declare. - run: "grep -E 'class GeometryModule|struct Point|Point translate|QList|QVariantMap attributes|QVariant nearest' geometry/geometry_module_api.h" + `QList`. `nearest` shows both halves of the optional mapping in + one signature — `?uint` in, `?Point` out — as + `std::optional` and `std::optional`: the caller can + ask `limit.has_value()` and the return can be *nothing* without + colliding with a legitimate `Point{0, 0}`. Only `attributes` and + `describe` stay `QVariantMap` / `QVariant`: `{tstr: any}` and `any` are + the untyped carriers for JSON whose shape the contract does not + declare. + run: "grep -E 'class GeometryModule|struct Point|Point translate|QList|QVariantMap attributes|std::optional nearest' geometry/geometry_module_api.h" code_block: | grep -E 'class|translate|bounds|attributes|nearest' geometry/geometry_module_api.h expect_contains: @@ -386,4 +399,4 @@ sections: - "Point translate(const Point& p, double dx, double dy" - "Point bounds(const QList& points" - "QVariantMap attributes(const QVariantMap& tags" - - "QVariant nearest(const Point& p, QVariant limit" + - "std::optional nearest(const Point& p, const std::optional& limit" diff --git a/doctests/outputs/cpp-sdk-generator-roundtrip.md b/doctests/outputs/cpp-sdk-generator-roundtrip.md index 937d780..5583947 100644 --- a/doctests/outputs/cpp-sdk-generator-roundtrip.md +++ b/doctests/outputs/cpp-sdk-generator-roundtrip.md @@ -258,10 +258,15 @@ cat provider/sensor_module_events_cdylib.cpp The consumer side. From the same contract, `--module-only` emits the typed wrapper a *consumer* compiles against to call `sensor_module`. Each LIDL `method` becomes a synchronous caller plus an `…Async` variant, and each -`event` an `on(...)` subscription. The type mapping is the Qt caller style: +`event` an `on(...)` subscription. The type mapping is the Qt caller style, +and it is **lossless** — one LIDL type, one C++ spelling: `float64`→`double`, `tstr`→`QString`, `int`→`qlonglong`, `uint`→ -`qulonglong`, `bstr`→`QByteArray`, `[tstr]`→`QStringList`, other -arrays→`QVariantList`, and `result`→`LogosResult`. +`qulonglong`, `bstr`→`QByteArray`, `result`→`LogosResult`, and each +container carries its element type through — `[tstr]`→`QStringList`, +every other `[T]`→`QList` (so `[uint]` is `QList`). +Only `any` stays `QVariant`: it is the one Qt type that holds bytes *and* +an exact `uint64` *and* arbitrary nesting, so narrowing it would lose what +it was chosen to carry. The integer spellings are 64-bit, and unsigned stays unsigned: LIDL `int`/`uint` are `int64_t`/`uint64_t` in every other binding, so spelling @@ -293,10 +298,14 @@ A **record becomes a real C++ struct**: a `type Point { … }` in the contract generates `struct Point` plus the conversions, so a caller writes `Point p = client.translate(q, 1, 2)` instead of digging fields out of a `QVariantMap`. `[Point]` is a `QList` and `{tstr: Point}` a -`QMap`. Maps of `any`, optionals (`?T`) and `any` itself -still cross as untyped JSON and stay `QVariantMap` / `QVariant` — a record -has a declared shape, those do not. Here is a contract that uses all of -them, taken straight to a consumer header. +`QMap`. An **optional** `?T` is a +`std::optional` — two-state in the type system, so an absent value is +not spelled the same way as a present default, and `?uint` is +distinguishable from `?tstr`. Only `any` — and any container whose element +type bottoms out at `any`, such as `{tstr: any}` — stays untyped, as +`QVariant` / `QVariantMap`: a record has a declared shape and an `any` does +not. Here is a contract that uses all of them, taken straight to a consumer +header. ### 6.1 geometry_module.lidl @@ -331,9 +340,14 @@ logos-cpp-generator --lidl geometry_module.lidl \ `Point` is generated as a struct, so a record parameter is taken by const-ref and a record return comes back typed. An array-of-records is a -`QList`. A map of `any`, an optional and a bare `any` stay -`QVariantMap` / `QVariant` — the untyped carriers for JSON whose shape -the contract does not declare. +`QList`. `nearest` shows both halves of the optional mapping in +one signature — `?uint` in, `?Point` out — as +`std::optional` and `std::optional`: the caller can +ask `limit.has_value()` and the return can be *nothing* without +colliding with a legitimate `Point{0, 0}`. Only `attributes` and +`describe` stay `QVariantMap` / `QVariant`: `{tstr: any}` and `any` are +the untyped carriers for JSON whose shape the contract does not +declare. ```bash grep -E 'class|translate|bounds|attributes|nearest' geometry/geometry_module_api.h