Files
logos-test-modules/test-fullapi-ext-module-rust/rust-lib/test_fullapi_ext_rust.lidl
Dario Gabriel LipicarandClaude Opus 5 f03a5d8bf6 fix(ext): the two providers agree on echoOptional again, at -> ?tstr
One contract had two providers answering different SHAPES.
test_fullapi_ext_rust.lidl declared `echoOptional(v: ?tstr) -> result`;
test_fullapi_ext_cpp is header-first and kept deriving `-> ?tstr` from
`std::optional<std::string> echoOptional(...)`. Carried as known-ext.json's
`ext-optional-return-changed-on-one-provider`, and visible on a second surface
as `ext-optional-return-shape-through-the-qt-proxy`.

THE WORKAROUND OUTLIVED ITS CAUSE. `-> result` was chosen because
`logos-qt-generator --backend consumer` REFUSED `-> ?T` -- null was also how a
failed call reported itself on that path, so an empty ?T and a failure were one
wire value. That gate is retired: `lidlTypeToQt` maps `?T` to
std::optional<T>, so the empty answer is std::nullopt, an inhabitant of its
own, and failure travels on logos::CallError, decided by the C ABI return code
and never by the value's null-ness.

So `-> ?tstr` is emittable today, it is what the C++ provider already derives,
and it is what the case table already expects. Reverting the Rust side is what
makes the two agree; the alternative (moving C++ to `-> result`) would have
meant changing the expectations to match a workaround for a constraint that no
longer exists.

Changed together, because a proxy that forwards a shape must declare the same
one: the Rust LIDL and its impl (`Option<String>` in and out, no success flag
standing in for presence), the qtproxy's own interfaces/full_api_ext.lidl, and
the qtproxy impl's echoOptional -- which now forwards through sOptStr rather
than sResult, and renders with rOpt rather than rResult in the exercise-all and
async blocks.

Both registry entries are retired. The proxy one had a SECOND, independent half
-- a Qt wrapper that could not decode a reply returned a default-constructed
LogosResult, and that default is byte-for-byte a provider refusal -- which is
fixed in logos-qt-sdk#44 and logos-cpp-sdk#150 and stands whichever shape the
contract had chosen. Recorded in the `fixed` entry, because retiring it here
without naming that half would credit the shape change with more than it did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 14:33:47 -03:00

90 lines
4.0 KiB
Plaintext

module test_fullapi_ext_rust {
version "1.0.0"
description "The composite tail of the conformance matrix: records, bytes at depth, typed maps and nested composites. Deliberately SEPARATE from full_api — several of these types are not yet expressible on the C++ cdylib provider (its typeSupported() gate rejects records and [bstr] by name), and adding them to full_api would break test_fullapi_cpp's build rather than add a test."
category "testing"
depends []
type Blob {
id: tstr
n: uint
payload: bstr
}
type Wrapper {
inner: Blob
tags: [tstr]
blobs: [Blob]
}
; Optionality. `?T` is TWO-state — a value of T, or empty — never three-state:
; one LIDL type maps to one type per language, and every target has exactly
; one empty inhabitant (None / nullopt / invalid QVariant / undefined).
;
; BOTH SPELLINGS ON PURPOSE. `? maybe: tstr` (the field flag) and `?tstr` (the
; type kind, in echoOptional below) are bound to the same meaning by
; logos-lidl/docs/spec.md:179,184-185 — they must emit byte-identical code,
; and nothing tested that until these cells existed. They did NOT agree when
; these cells were written: the Rust dispatch descriptor typed a flag-optional
; field by its BASE type (so an absent `maybe` was rejected as `expected string
; at arg0.maybe, got null`) while `?tstr` fell to the catch-all `Ty::Any` (so
; anything at all was accepted). Same declared meaning, opposite behaviour —
; which is what these cells were written to catch. They agree now; the cells
; stay as the guard.
;
; `required` is here to hold the other half of the contract down: leniency
; applies in an OPTIONAL slot only. A required field must still reject both
; absence and null.
type Opt {
required: tstr
? maybe: tstr
? count: uint
? blob: bstr
}
method whoAmI() -> tstr
method echoBlob(v: Blob) -> Blob
method echoWrapper(v: Wrapper) -> Wrapper
method echoBlobList(v: [Blob]) -> [Blob]
method echoBlobMap(v: {tstr: Blob}) -> {tstr: Blob}
method echoBytesList(v: [bstr]) -> [bstr]
method echoBytesMap(v: {tstr: bstr}) -> {tstr: bstr}
method echoIntMap(v: {tstr: int}) -> {tstr: int}
method echoStringMap(v: {tstr: tstr}) -> {tstr: tstr}
method echoNestedInts(v: [[int]]) -> [[int]]
method echoMapOfBytesLists(v: {tstr: [bstr]}) -> {tstr: [bstr]}
; Optionality in a record, in a LIST of records (per-element presence must be
; independent), and as a bare positional slot. The last one is where the wire
; rule bites: a record field is NAMED, so empty is spelled by omitting the
; key; a method ARG is POSITIONAL and has no key to omit, so empty is spelled
; null and the arity never changes.
;
; A ?T RETURN was spelled `-> result` here for a while, and this comment used
; to explain why: `logos-qt-generator --backend consumer` REFUSED `-> ?T`,
; because null was also how a failed call reported itself on that path, so an
; empty ?T and a failure were one wire value for every non-Rust caller.
;
; THAT GATE IS RETIRED and the reason no longer holds. `lidlTypeToQt` maps
; `?T` to std::optional<T>, so the empty answer is std::nullopt — a distinct
; inhabitant, not a null that collides with anything — and the Qt consumer
; headers this module could not emit before now emit. Failure travels on
; `logos::CallError`, decided by the C ABI return code and never by the
; value's null-ness.
;
; The workaround outlived its cause, and while it did it cost more than it
; bought: test_fullapi_ext_cpp is header-first and kept deriving `-> ?tstr`,
; so ONE contract had two providers answering different SHAPES, which
; known-ext.json carried as `ext-optional-return-changed-on-one-provider`.
; `?tstr` is what the other provider already sends and what the case table
; already expects, so it is the shape both agree on.
method echoOpt(v: Opt) -> Opt
method echoOptList(v: [Opt]) -> [Opt]
method echoOptional(v: ?tstr) -> ?tstr
method fireBlobEvent(v: Blob) -> bool
event blobEvent(v: Blob)
}