mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 09:41:06 +00:00
`lidlTypeToQt` answered four different LIDL types with one Qt name. `[uint]`,
`[bstr]`, `[[uint]]` and `[any]` were all QVariantList; `{tstr: uint}` and
`{tstr: any}` were both QVariantMap; every `?T` was a bare QVariant. A Qt
consumer therefore lost, on the SAME contract, types that the std consumer next
door kept — it could not tell `?tstr` from `?uint`, and got no compile-time
check on any element.
The table is now recursive:
[T] QList<qtOf(T)> ([tstr] stays QStringList)
{tstr: V} QMap<QString, qtOf(V)>
?T std::optional<qtOf(T)> (through optionalValueType,
so ??T stays two-state)
any QVariant — KEPT, deliberately
`any` is the one row that must not widen: QVariant is the only Qt type that
holds bytes AND an exact uint64 AND arbitrary nesting at once, so every
narrower spelling would lose what it was chosen to carry. The rule is applied
at the LEAF, so anything whose element type bottoms out at `any` keeps the
QVariant-family spelling at every depth — `[any]` is QVariantList, `[[any]]`
still is, `{tstr: [any]}` is QVariantMap, `?any` is QVariant.
THE TRAP, and why this is not just a rename. A widened name must never reach
QVariant::fromValue / qvariant_cast / logos::qt::toWire as a WHOLE value.
logos-protocol's qvariantToNlohmann matches a CLOSED userType() set:
QList<qulonglong> is in none of it, so it serialises to JSON null. The decode
fails just as quietly — qvariant_cast<QList<qulonglong>> of a QVariantList
yields an EMPTY list. Neither direction warns. So every widened slot is encoded
and decoded by a generator-emitted ELEMENT LOOP, the shape the record cases
already used, and `lidlQtNeedsElementLoop` is the single predicate that decides
which slots need one.
The emitted loops take their source as a lambda PARAMETER, not a body-local
binding. They nest (`[[uint]]`), every level wants the same short names, and a
local — or a range-for over a name the loop itself declares — is then
self-referential: it compiles and reads uninitialised memory. Measured: three
round-trip tests died on SIGTRAP before the argument form.
THE STRING-KEYED EMITTER IS FROZEN, ON PURPOSE. generator_lib is keyed on flat
type NAMES (lidl_to_json flattens the contract before it gets there, because
that emitter also serves the metaobject-introspection path), so it cannot
derive the levels an element loop needs without parsing C++ type names back
into a tree. Every widened spelling is folded back to the name it produced
before (legacyQtBase), which keeps BOTH surfaces it feeds byte-for-byte
unchanged: the legacy Qt consumer, and the Qt-free lp one whose table is
DERIVED from it through mapParamTypeStd. Verified by generating a
28-method contract through both before and after: the diff is empty. The
widened types are spent in the TypeExpr-driven emitters instead
(lidl_gen_client.cpp here, lidl_gen_qt_consumer.cpp in logos-qt-sdk).
Also here, because both are consequences of the table becoming recursive:
* lidlTypeToQt gained a record-name HOOK. A wrapper nests its record structs
in the wrapper class, so a type written outside that scope must qualify
them — and the emitters used to do that by matching the three shapes that
could mention a record on the finished string. `?Point` and
`QList<QList<Point>>` are now spellable, so the qualification happens
during the walk, at the one place that knows a name is a record.
* lidlTypeToLidlText — the LIDL contract spelling of a type. Unused here; the
commit that follows puts getMethods() on it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
151 lines
6.4 KiB
C++
151 lines
6.4 KiB
C++
#include "lidl_to_json.h"
|
|
|
|
#include <QJsonObject>
|
|
#include <QStringList>
|
|
|
|
#include "experimental/lidl_emit_common.h" // lidlTypeToQt — the one Qt type mapper
|
|
|
|
// Convert a TypeExpr → Qt-typed string name (same surface the
|
|
// metaobject-introspection path produces for methods, so generator_lib
|
|
// can consume both via one code path).
|
|
//
|
|
// ONE Qt type mapper. This used to be a near-duplicate of `lidlTypeToQt`
|
|
// (experimental/lidl_emit_common.cpp) and the two disagreed: this copy had no
|
|
// `void` case, so a `-> void` method reaching it as Primitive("void") from the
|
|
// impl-header parser fell through to QVariant and generated
|
|
// `QVariant doVoid(...)`. (The .lidl parser spells the same thing
|
|
// Named("void"), which survived only by accident — mapReturnType's
|
|
// `base == "void"` early-out.) The lp/std tables are DERIVED from this name, so
|
|
// the same bug produced `LogosMap doVoid(...)` on the Qt-free surface: not a
|
|
// Qt-only defect, a front-end one. It is now a delegation, so there is one
|
|
// table to disagree with.
|
|
QString lidlTypeExprToQtTypeName(const TypeExpr& te)
|
|
{
|
|
return lidlTypeToQt(te);
|
|
}
|
|
|
|
// Report every optional slot this path still flattens into a bare type name.
|
|
//
|
|
// A record FIELD no longer does: moduleRecordsToJson below carries `optional`
|
|
// alongside the value type, and the emitter reconstitutes it (QVariant on the
|
|
// Qt surface, std::optional<T> on the Lp one). What is still flattened is every
|
|
// POSITIONAL slot — a method parameter, a return type, an event parameter.
|
|
// Those have no name to hang a flag on, so they only ever had the type-kind
|
|
// spelling and there is no spelling divergence to fix.
|
|
//
|
|
// `lidlTypeToQt` DOES now answer `?T` with std::optional<T>. This path cannot
|
|
// keep it: generator_lib is keyed on flat type NAMES and folds every widened
|
|
// spelling back (legacyQtBase), because encoding one correctly needs an element
|
|
// loop it has no tree to derive. So the loss is this emitter's, not the
|
|
// mapping's — the TypeExpr-driven Qt consumer emitters keep the value type —
|
|
// and the note says which surface is affected rather than claiming the table
|
|
// still flattens.
|
|
void noteOptionalPositionalSlots(const ModuleDecl& mod, const QString& where,
|
|
QTextStream& err)
|
|
{
|
|
QStringList optSlots;
|
|
for (const MethodDecl& md : mod.methods) {
|
|
for (const ParamDecl& pd : md.params)
|
|
if (paramIsOptional(pd))
|
|
optSlots << (qs(md.name) + "(" + qs(pd.name) + ")");
|
|
if (typeIsOptional(md.returnType))
|
|
optSlots << (qs(md.name) + "() return");
|
|
}
|
|
for (const EventDecl& ed : mod.events)
|
|
for (const ParamDecl& pd : ed.params)
|
|
if (paramIsOptional(pd))
|
|
optSlots << (qs(ed.name) + "(" + qs(pd.name) + ")");
|
|
if (optSlots.isEmpty()) return;
|
|
err << "Note: " << where << ": optional positional slot(s) ["
|
|
<< optSlots.join(", ")
|
|
<< "] are generated as untyped QVariant (LogosMap on the lp surface) by "
|
|
"THIS emitter, which is keyed on flat type names and folds "
|
|
"std::optional<T> back to QVariant. `?T` keeps its two states (an "
|
|
"invalid QVariant / a JSON null is the empty one) but loses T here. "
|
|
"The TypeExpr-driven Qt consumer emitters keep it as "
|
|
"std::optional<T>; record fields are unaffected on every surface — "
|
|
"they carry optionality through.\n";
|
|
}
|
|
|
|
// Build a getMethods()-shaped QJsonArray (the surface makeHeader/makeSource
|
|
// consume) from a parsed ModuleDecl. Every interface method is invokable.
|
|
QJsonArray moduleMethodsToJson(const ModuleDecl& mod)
|
|
{
|
|
QJsonArray arr;
|
|
for (const MethodDecl& m : mod.methods) {
|
|
QJsonObject o;
|
|
o["name"] = qs(m.name);
|
|
o["returnType"] = lidlTypeExprToQtTypeName(m.returnType);
|
|
o["isInvokable"] = true;
|
|
QJsonArray params;
|
|
for (const ParamDecl& p : m.params) {
|
|
QJsonObject po;
|
|
po["type"] = lidlTypeExprToQtTypeName(p.type);
|
|
po["name"] = qs(p.name);
|
|
params.append(po);
|
|
}
|
|
o["parameters"] = params;
|
|
arr.append(o);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// Build the records QJsonArray ({ name, fields:[{name,type,optional}] }) from a
|
|
// parsed ModuleDecl — the contract's `type Foo { ... }` declarations, which
|
|
// generator_lib turns into structs nested in the wrapper class.
|
|
//
|
|
// OPTIONALITY SURVIVES HERE, and it is the whole reason this object has three
|
|
// keys instead of two. A field has two equivalent spellings — the flag
|
|
// (`? name: T`) and the type kind (`name: ?T`) — which logos-lidl's docs/spec.md
|
|
// binds to ONE meaning and requires to produce byte-identical code. Flattening
|
|
// `fd.type` into a name answered that question two different ways from one
|
|
// contract: the flag spelling kept T (so `? maybe: tstr` became a bare `QString`
|
|
// that cannot be empty at all, silently defaulting), the type spelling collapsed
|
|
// to QVariant. Both answers came from reading the verbatim spelling instead of
|
|
// asking.
|
|
//
|
|
// So: `type` is the value type with optionality stripped (fieldValueType), and
|
|
// `optional` is true for either spelling (fieldIsOptional). Those accessors are
|
|
// the frontend's, and are the ONLY correct source — `fd.optional` alone and
|
|
// `fd.type.kind == Optional` alone are the same bug from opposite sides.
|
|
QJsonArray moduleRecordsToJson(const ModuleDecl& mod)
|
|
{
|
|
QJsonArray arr;
|
|
for (const TypeDecl& td : mod.types) {
|
|
QJsonObject o;
|
|
o["name"] = qs(td.name);
|
|
QJsonArray fields;
|
|
for (const FieldDecl& fd : td.fields) {
|
|
QJsonObject f;
|
|
f["name"] = qs(fd.name);
|
|
f["type"] = lidlTypeExprToQtTypeName(fieldValueType(fd));
|
|
f["optional"] = fieldIsOptional(fd);
|
|
fields.append(f);
|
|
}
|
|
o["fields"] = fields;
|
|
arr.append(o);
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
// Build the events QJsonArray ({ name, params:[{name,type}] }) — same shape
|
|
// loadEventsFromLidl produces — from a parsed ModuleDecl.
|
|
QJsonArray moduleEventsToJson(const ModuleDecl& mod)
|
|
{
|
|
QJsonArray arr;
|
|
for (const EventDecl& ed : mod.events) {
|
|
QJsonObject o;
|
|
o["name"] = qs(ed.name);
|
|
QJsonArray params;
|
|
for (const ParamDecl& pd : ed.params) {
|
|
QJsonObject p;
|
|
p["name"] = qs(pd.name);
|
|
p["type"] = lidlTypeExprToQtTypeName(pd.type);
|
|
params.append(p);
|
|
}
|
|
o["params"] = params;
|
|
arr.append(o);
|
|
}
|
|
return arr;
|
|
}
|