OMG DDS-XTypes 1.3 — the negative list

Spec: OMG DDS-XTypes 1.3, formal/2020-02-04, 340 pages. The PDF sits in the local cache at internal/standards/cache/omg/dds-xtypes-1.3.pdf (gitignored).

Tool: tools/xtypes-1.3-gap-scan/scan.sh. Denominator: tools/xtypes-1.3-gap-scan/roster.py and the roster.tsv it writes, plus anchors.tsv. Evidence: tools/xtypes-1.3-gap-scan/cases.tsv (199 probes) and expected.tsv (the recorded state).

The other pages under docs/spec-coverage/ are positive lists: they name what was looked at and how far it got. What was never looked at cannot appear in them, because their content is derived from the tree and not from the specification. This page is the other direction.

Every line here has an attempt behind it: the octets or the IDL that were fed in, and what came back. No line rests on a guess.

The denominator, and why it costs more here

OMG IDL 4.2 has Annex A: a consolidated, numbered grammar from which a complete inventory falls out with a hole check (range(1, 228) against the parsed numbers). XTypes 1.3 has no grammar. Its inventory is a set of normative tables and IDL blocks, and each needs its own extractor and its own completeness argument. roster.py pulls seven axes out of the PDF and prints the check for each:

Axis Denominator Completeness check
typekind 28 Annex B declares the TypeKind constants twice — once as const octet TK_x, once as const TypeKind TK_x. Both listings must agree on every name and every value, and no value may appear twice.
typeid 31 Every TI_* constant Annex B declares must be a case of union TypeIdentifier, and every case label a declared constant. Plus the rows of Table 29.
annotation 24 §7.3.1.2.1 has sixteen numbered subclauses; hole check range(1, 17). Plus the 19 annotations whose applicability Table 21 restricts.
assign 55 §7.2.4.4 has eight subclauses (hole check range(1, 9)), Tables 14 through 19 carry the definitions. The 55 rules live in anchors.tsv, each with a literal fragment of the PDF, without which roster.py fails.
typeobject 13 MinimalTypeObject and CompleteTypeObject are two unions over the same discriminator; the check is that their case sets are equal. TypeObject itself carries exactly EK_COMPLETE and EK_MINIMAL.
typelookup 16 §7.6.3.3 has four subclauses (hole check range(1, 5)), §7.6.3.3.4 exactly two operations, §7.6.3.3.3 ten TypeLookup_* types.
datarep 16 Table 59 carries four verdicts, Table 60 twelve rows of representation, extensibility, encoding version, endianness and identifier value. All twelve must parse.

183 items over seven axes, 19 checks, all holding.

What that proves and what it does not: that every rule listed here is in the specification is checked. That these are all the rules of the named clauses is the claim of the person who read them, not a check. For typekind, typeid, typeobject and typelookup the denominator falls out of the IDL and is therefore machine-complete; for assign, annotation and datarep it is assembled by hand and only backed item by item.

How it was measured

The probes here are octets, not text. Three measurements per probe; what they mean depends on the op, and the table is in the header of tools/xtypes-1.3-gap-scan/probe/src/main.rs:

Op decode encode rule
ti the octets read as a TypeIdentifier re-encoding them gives the same octets back §7.2.4.1: the identifier is assignable from itself
to the octets read as a TypeObject the same §7.6.3: the object becomes a model
todh the type builds from IDL the encoded TypeObject opens with a DHEADER covering the rest (§7.4.2 for @extensibility(APPENDABLE)) —
idl the IDL parses, lowers and resolves the MinimalTypeObject survives encode, decode and encode byte for byte the CompleteTypeObject builds
assign both types build from one IDL text — §7.2.4.4: is_assignable(T2, T1) said yes
datarep — — §7.6.3.1 Table 59: the negotiation accepted
encaps — — §7.6.3.1.2 Table 60: the pairing is in the matrix
tl the octets read as the named TypeLookup type re-encoding gives the same octets back —

Plus one measurement that is not about the code: corpus counts how many of the tree’s 10,722 committed sources contain the probe’s string at all (the witness column of cases.tsv). *.md is deliberately not in the corpus — a page that names a construct is not evidence that anything exercises it, and this page would otherwise be counting itself. A zero is a fact: nothing in the tree names the construct, so nothing in the tree can have exercised it. A - means no string isolates the construct.

The number

The survey found 199 probes, 166 with no finding. The remaining 33 fell out like this:

Category Probes of those closed
A not recognised — the tree refuses what the specification allows 17 16
B accepted although the specification says otherwise 9 7
C recognised, but not emitted that way 6 5
D emitted, and no source in the tree names it 1 1
E deliberate, with a reason 3 —

Category B is the one the predecessor added for OMG IDL 4.2, and it is the important one here too: there is no error message, only a model or a verdict with the wrong content. Six of the nine B lines are assignability rules that pass silently — two endpoints match that §7.2.4.4 says must not.

What came of it

Thirty of the 33 are closed. Three stay open, because they are not gaps in this tree.

Every closed line below carries a **Closed:** paragraph naming the test that holds it. The three others — A.8, B.8/B.9 and C.4 — are places where the specification contradicts itself: the question there is not whether the tree is wrong but which of the two sentences it follows, and that is a decision rather than a repair. They are written up as B7, B8 and B9 in docs/omg-xtypes-issues.md, in the shape the OMG issue form takes.

The probe count went from 199 to 201: two cases were added that tell the new framing from the old (tl-getTypes_in-extra-member, tl-call-unknown-operation). Eleven probes got a new arg — the tl-* and to-* octets had been written against the tree’s form rather than the clause’s, and the head of cases.tsv says such a probe measures nothing.

The vendor comparison this page recorded as missing has been run. On 2026-09-06 on the Linux test host, with the toolchains installed there: idlc (CycloneDDS) and rtiddsgen + DDS_TypeObjectV2_create_from_typecode (RTI Connext 7.7.0) over the same IDL. For Plain COMPLETE both report the hash 7c0c6fab7d066a0c56b8e93c0ea7; Cyclone’s stream is 80 octets and opens 4c000000 — a DHEADER of 76 — and RTI’s blob is 104 octets and holds exactly those 80 at offset 24. ZeroDDS produces them byte for byte. crates/types/tests/typeobject_vendor_dheader.rs holds all four cases in both forms; tools/typeobject-bytes/record.sh re-derives them from the vendors.


A — not recognised

The specification allows or requires it; the tree refuses it.

A.1 TK_ANNOTATION has no TypeObject in this tree

Spec: Annex B, union MinimalTypeObject switch (octet) — case TK_ANNOTATION: MinimalAnnotationType annotation_type;. Table 29 additionally lists TK_ANNOTATION as a TypeIdentifier discriminator.

Fed: @annotation A { long v; };

Result: no type named `A` — the declaration parses and lowers, but the model then carries no type A, so from_ast can never build a MinimalAnnotationType. The variant exists (crates/types/src/type_object/minimal/annotation_type.rs); it is never produced from IDL.

Corpus: 4 of 10,722.

Probe: tk-annotation

Closed. from_ast::annotation_type_object builds both forms, and Ast::annotation_defs is the counterpart to Ast::types for the other declaration kind that has a TypeObject. No TypeNode is created for it: @Name is meant to resolve to the declaration and not to a type. Test: crates/types/tests/type_object_annotations.rs::an_annotation_declaration_has_a_type_object.

A.2 Six of the ten TypeLookup types of §7.6.3.3.3 do not exist

Spec: §7.6.3.3.3 declares ten TypeLookup_* types: the four _In/_Out structures, the two _Result unions, TypeLookup_Call, TypeLookup_Return, and the two RPC envelopes TypeLookup_Request (@RPCRequestType) and TypeLookup_Reply (@RPCReplyType).

Result: crates/types/src/type_lookup.rs has four counterparts — GetTypesRequest, GetTypesReply, GetTypeDependenciesRequest, GetTypeDependenciesReply. The other six are missing. TypeLookup_Call, _Return, _Request and _Reply are the messages in which the four travel over the built-in endpoints of Table 61; without them there is no call, only the payload of one.

Corpus: 0 of 10,722 for each of the six names.

Closed. All ten types are in crates/types/src/type_lookup.rs, each with the framing its extensibility gives it: the four parameter types MUTABLE, the other six appendable, because §7.3.1.2.1.8 says a type whose representation does not indicate its extensibility kind is appendable. RequestHeader is declared there rather than taken from zerodds-rpc, because that crate depends on this one. Tests: type_lookup_call_roundtrips_both_operations, type_lookup_return_roundtrips_both_operations, type_lookup_request_and_reply_roundtrip, get_types_result_roundtrips_both_arms, get_type_dependencies_result_roundtrips_both_arms.

Probes: tl-gettypes_result, tl-gettypedependencies_result, tl-call, tl-return, tl-request, tl-reply

A.3 The default: arm of MinimalTypeObject is refused

Spec: Annex B — default: MinimalExtendedType extended_type;, under the comment // =========== Future extensibility ============.

Fed: F1 C0 00000000 — EK_MINIMAL, then a TypeKind this revision does not know.

Result: UnknownTypeKind { kind: 192 }.

Probe: to-extended-arm

Closed. MinimalTypeObject and CompleteTypeObject have an Extended variant holding the TypeKind and the member list verbatim — the DHEADER excluded, because that is a length the encoder writes and a byte order the value does not carry. Tests: a_type_kind_this_revision_does_not_define_survives_the_round_trip, an_extended_type_kind_keeps_the_member_list_it_arrived_with.

A.4 @non_serialized members count in the compatibility check

Spec: §7.2.4.4.2 — “Members that are marked as non-serialized, see Sub Clause 7.3.1.2.1.14, shall be ignored during type compatibility checking.”

Fed: reader @final struct R { long a; };, writer @final struct W { long a; @non_serialized long b; };

Result: No("final struct member count mismatch").

Corpus: 8 of 10,722.

Probes: as-nonser, an-non-serialized-effect

Closed. The member is no longer in the TypeObject at all, which is both halves at once: it is not on the wire, and the rules that read the TypeObject and nothing else cannot see it. The member index still advances, because a @non_serialized declaration occupies a position. Test: crates/types/tests/table19_key_members.rs::a_non_serialized_member_does_not_enter_the_comparison.

A.5 @ignore_literal_names has no effect

Spec: Table 18, row ENUMERATION_TYPE — “Any literals that have the same name in T1 and T2 also have the same value, and any literals that have the same value in T1 and T2 also have the same name. This behavior may be modified with the @ignore_literal_names annotation, see 7.3.1.2.1.11.”

Fed: @ignore_literal_names @appendable enum R { A, B }; against @ignore_literal_names @appendable enum W { X, Y }; — same values, different names.

Result: No("an enum literal name and its value do not agree across the two types"). The front end takes the annotation and it changes nothing about the verdict.

Corpus: 4 of 10,722.

Probes: as-enum-ignore-names, an-ignore-literal-names-effect

Closed, with a named side effect. The annotation now reaches EnumTypeFlag::IGNORE_LITERAL_NAMES, and the comparison requires it on both sides, as §7.3.1.2.1.11 says (“in both enumerated types”); it read ||, which let one side alone decide about the other’s names. That writes one bit of a flag word Annex B calls // Unused — the extensibility bits of the same word are still not written, see B.8/B.9 and omg-xtypes-issues.md §B8. Test: crates/types/tests/table19_key_members.rs::ignore_literal_names_needs_the_annotation_on_both_types.

A.6 Table 18 puts no condition on the bitmask flags; the tree does

Spec: Table 18 — “BITMASK_TYPE if and only if T1.bound == T2.bound”; the “Object construction” column says “Preserve bit values by index for all bits identified in both T1 and T2”, so it expressly allows for bits only one side knows.

Fed: two bitmasks with @bit_bound(8) whose flags sit at different positions; and one where the writer has an extra flag.

Result: both times No("bitmask writer flag unknown in reader").

Probes: as-bm-same-bound, as-bm-writer-extra-flag

Closed. The flag check is gone; the bound stays, because it is the wire width. The “Object construction” column is the argument for it and not merely the absence of a condition: “for all bits identified in both T1 and T2” expects bits only one side names, and says what happens to them. Test: crates/types/tests/table19_key_members.rs::two_bitmasks_of_the_same_bound_are_assignable_whatever_their_flags.

A.7 The empty DataRepresentation list

Spec: §7.6.3.1 — “The default value of the DataRepresentationQosPolicy shall be an empty list of preferences. An empty list of preferences shall be taken to be equivalent to a list containing the single element XCDR.”

Fed: once an empty offer against [XCDR], once [XCDR] against an empty request.

Result: both times NoOverlap. negotiate_representation does not apply the default; two endpoints with default QoS do not meet through this function.

Probes: dr-empty-offer, dr-empty-request

Closed. negotiate_representation substitutes the single-element list [XCDR] for an empty one, on both sides. The substitution is XCDR and not “anything”: an empty offer against a reader that takes only XCDR2 still finds nothing. Test: crates/types/src/qos.rs::an_empty_list_of_preferences_is_the_single_element_xcdr.

A.8 XCDR1 with APPENDABLE is refused, and Table 60 carries it

Spec: Table 60, third and fourth rows — XCDR | APPENDABLE | 1 | Big Endian | CDR_BE | {0x00, 0x00} and XCDR | APPENDABLE | 1 | Little Endian | CDR_LE | {0x00, 0x01}.

Fed: check_data_repr_extensibility(Xcdr1, Appendable).

Result: Err("Tab.59 §7.6.3.1: XCDR1 does not support APPENDABLE directly — the encoder must choose FINAL"). The refusal is deliberate and carries its reason in the source; the place it cites is Table 59, which says nothing about this. Table 60 says the opposite.

Probe: enc-xcdr-appendable

Open, and not as a gap in this tree. The contradiction is written up as B7 in docs/omg-xtypes-issues.md. The refusal stays as it is: changing it would be to pick one of the two tables, and picking is the RTF’s to do.


B — accepted although the specification says otherwise

Here there is no error message, only a model or a verdict with the wrong content.

B.1 TK_NONE is a valid type and is assignable from itself

Spec: Table 29, first row — TK_NONE | N/A | Invalid identifier.

Fed: the octet 00.

Result: decode ok, encode ok, and is_assignable(None, None) is Yes — the identity short-circuit of §7.2.4.1 fires before anything checks whether the identifier is valid at all. Two endpoints both carrying the invalid identifier match.

Corpus: 58 of 10,722.

Probe: ti-prim-none

Closed in the rule column, and only there. is_assignable refuses TK_NONE on either side, ahead of the identity shortcut. Decoding and encoding stay allowed: TypeInformation writes TK_NONE for an equivalence form the sender left out (§7.6.3.2.1), and a stream carrying that is not malformed. Test: crates/types/tests/table19_key_members.rs::tk_none_is_not_assignable_from_itself.

B.2 EK_BOTH passes as a TypeIdentifier discriminator

Spec: Annex B — union TypeIdentifier has case EK_COMPLETE: and case EK_MINIMAL: and no case for EK_BOTH = 0xF3. EK_BOTH belongs in the PlainCollectionHeader, not here.

Fed: F3 and fourteen octets of hash.

Result: accepted as Unknown(0xF3); re-encoding leaves one octet, the fourteen hash octets are gone.

Corpus: 5 of 10,722.

Probe: ti-ek-both

Closed. EK_BOTH at the TypeIdentifier discriminator is refused. In a PlainCollectionHeader, where it belongs, nothing changes — that is an octet field and not a union discriminator. Test: ek_both_is_not_a_type_identifier_discriminator.

B.3 The tree’s SCC form leaves out two fields, and does not read them either

Spec: Annex B —

@extensibility(APPENDABLE) @nested
struct StronglyConnectedComponentId {
    TypeObjectHashId sc_component_id;
    long             scc_length;
    long             scc_index;
};

TypeObjectHashId is itself a union with the discriminator EK_COMPLETE/EK_MINIMAL in front of the hash, and the file’s own header says “The types in this file shall be serialized with XCDR encoding version 2” — an APPENDABLE object carries a DHEADER under version 2 (§7.4.2).

Fed: the tree’s form, B0 followed directly by fourteen hash octets, with no DHEADER and no TypeObjectHashId discriminator.

Result: decode ok, encode ok — the stream no conforming reader should accept goes through and comes back unchanged. Consequence: StronglyConnectedComponentId in this tree has no field for the equivalence kind, and §7.3.4.6 distinguishes on exactly that (“those with discriminator TI_STRONG_COMPONENT where the contained TypeObjectHashId has discriminator EK_MINIMAL”). Minimal and complete SCC identifiers are indistinguishable here.

Corpus: 5 of 10,722.

Probe: ti-scc-tree-form; the other direction is C.5.

Closed. StronglyConnectedComponentId now has the field equivalence_kind and the appendable framing; the tree’s 24-octet form is refused. Tests: a_component_identifier_is_the_thirty_two_octets_of_annex_b, a_minimal_and_a_complete_component_identifier_differ, a_component_identifier_takes_only_the_two_equivalence_kinds.

B.4 The @key on a union discriminator never reaches the TypeObject

Spec: Table 19, row UNION_TYPE, third bullet — “Either the discriminators of both T1 and T2 are keys or neither are keys.”

Fed: @final union R switch (@key long) { case 1: long a; }; against @final union W switch (long) { case 1: long a; };

Result: Yes. crates/types/src/from_ast.rs sets discriminator_flags to a constant UnionDiscriminatorFlag::IS_MUST_UNDERSTAND | try_construct_bits(…); IS_KEY never enters it, so the rule has nothing to compare.

Corpus: 192 of 10,722.

Closed. The front end carries @key on the discriminator into XTypesFacet::discriminator_is_key, from_ast into UnionDiscriminatorFlag::IS_KEY, and check_unions compares the two. Which reading applies is an axis rather than this front end’s decision: KeyOnUnionDiscriminator::Refused is the default and reports the annotation — the measured refusal by Cyclone, RTI and Fast DDS (2026-08-10, /root/n11-keyunion/) is the reason — and Carried is what asks for it, all the way to a KeyHash over the discriminator alone (keyhash::union_key_discriminator). Tests: crates/types/tests/table19_key_members.rs::one_keyed_union_discriminator_and_one_unkeyed_are_not_assignable, crates/idl-lower/tests/annotations.rs::a_key_on_a_union_discriminator_is_refused_with_the_clause_that_permits_it and …::a_key_on_a_union_discriminator_reaches_the_facet_when_carried.

Probe: as-union-disc-key; the counter-probe with @key on both sides (as-union-disc-key-both) also yields Yes, as it should.

B.5–B.7 Three of Table 19’s key rules do not fire

Spec: Table 19, row STRUCTURE_TYPE. The rules are there verbatim, and the table closes with “Note: The rules regarding key members ensure that the key of T2 can be transformed faithfully into the key of T1 without aliasing or loss of information.”

Rule (quoted) Fed (reader against writer) Result Probe
“For any string key member m2 in T2, the m1 member of T1 with the same member ID verifies m1.type.length >= m2.type.length.” @key string<8> against @key string<16> Yes as-struct-key-string
“For any enumerated key member m2 in T2, the m1 member of T1 with the same member ID verifies that all literals in m2.type appear as literals in m1.type.” @key E1 with { A } against @key E2 with { A, B } Yes as-struct-key-enum
“For any sequence or map key member m2 in T2, the m1 member of T1 with the same member ID verifies m1.type.length >= m2.type.length.” @key sequence<long,4> against @key sequence<long,8> Yes as-struct-key-seqmap

All three are cases where the writer’s key does not fit into the reader’s key and the endpoints match anyway.

Closed. key_members_fit in assignability.rs checks the three rules, and only for members carrying IS_KEY: a string<8> reader is assignable from a string<16> writer as long as the member is not a key — then @try_construct decides, and it is being a key that makes the loss inadmissible. The other direction is checked too. Tests in crates/types/tests/table19_key_members.rs: a_shorter_string_key_in_the_reader_is_not_assignable, a_writer_enum_key_literal_the_reader_lacks_is_not_assignable, a_shorter_sequence_key_in_the_reader_is_not_assignable, a_shorter_string_member_that_is_not_a_key_is_still_assignable.

B.8/B.9 The extensibility of an enumeration is not compared

Spec: Table 18, row ENUMERATION_TYPE, first bullet — “T1.extensibility == T2.extensibility”, and third bullet — “If extensibility is final T1 and T2 have the same literals.”

Fed: @final enum R { A, B }; against @appendable enum W { A, B };, and @final enum R { A, B }; against @final enum W { A, B, C };

Result: both times Yes when the survey was taken; since the vendor measurement in E.3 both times No.

Why: the same specification declares in Annex B typedef TypeFlag EnumTypeFlag; // Unused. No flags apply, and from_ast.rs accordingly wrote EnumTypeFlag(0). RTI, OpenDDS and Cyclone write the TypeFlag extensibility position into the word, and RTI recomputes the hash of a received enumeration from its own rendering; from_ast.rs now writes the bits the same way and check_enumerations compares them once both sides carry them. See E.3.

Probes: as-enum-ext, as-enum-final

Open, and not as a gap in this tree. The contradiction is written up as B8 in docs/omg-xtypes-issues.md. One half of it has since been decided and is named there: the one bit §7.3.1.2.1.11 needs for @ignore_literal_names is written (A.5); the extensibility bits are not.


C — recognised, but not emitted that way

The tree reads the value and writes something else back.

C.1/C.2 The outer TypeObject gets no DHEADER

Spec: Annex B —

@extensibility(APPENDABLE) @nested
union TypeObject switch (octet) { // EquivalenceKind
case EK_COMPLETE: CompleteTypeObject complete;
case EK_MINIMAL:  MinimalTypeObject  minimal;
};

and, at the head of the same file, “The types in this file shall be serialized with XCDR encoding version 2”. §7.4.2 gives an APPENDABLE object under encoding version 2 a DHEADER — a UInt32 holding the length of everything after it.

Fed: @final struct S { long a; }; and @mutable struct S { @id(1) long a; };, each built as a MinimalTypeObject and encoded.

Result: 35 octets, whose first four read 86,513 and 283,121 respectively, with 31 octets after them. There is no DHEADER; the first octet is the EquivalenceKind. The inner APPENDABLE structures do get theirs — framing::encode_appendable has 19 call sites in crates/types/src/type_object/ — the outer union does not.

Not measured: whether a vendor writes a DHEADER in that position. This tree exchanges TypeObject octets with no vendor (discovery matches on type_name, see the “Wiring status” section of crates/types/src/lib.rs), so there is no recorded vendor byte in the tree to compare against. The next step for this finding is a PID_TYPE_OBJECT from Fast DDS or Cyclone on the Linux test host.

Probes: to-dheader-minimal-struct, to-dheader-complete-struct

Closed, and measured. TypeObject::encode_into writes the DHEADER; hash::minimal_hash_input calls that very encoder and adds nothing. One path, not two — a second DHEADER would move every octet of every hash, and a peer that disagrees about a hash looks like a type mismatch rather than a framing defect.

The vendor comparison this section named as the next step has been run: see “What came of it” above. CycloneDDS’s idlc and RTI Connext 7.7.0 on the test host, the same octets, the same hash.

The counter-check for the discovery rejection this finding carries with it: the announced size is the length of the stream the hash is taken over, and that is the number RTI counts for the same object — the rejection was a serializedSize mismatch of four octets. Tests: crates/types/tests/typeobject_vendor_dheader.rs (four cases, both forms, plus the hash and size equality) and crates/discovery/src/type_lookup/server.rs::a_dependency_is_announced_at_its_hashed_length.

C.3 TI_EXTENDED swallows its body

Spec: Annex B — default: ExtendedTypeDefn extended_defn;, and ExtendedTypeDefn is @extensibility(MUTABLE), so under encoding version 2 it carries a framing of its own that a reader can skip.

Fed: C0 and eight zero octets.

Result: decodes to Unknown(0xC0), re-encodes as one octet. The reader read not a single octet of the body. In a nested position — the element type of a plain sequence, say — that shifts every following field of the stream, and no error appears anywhere.

Corpus: 3 of 10,722.

Probe: ti-extended

Closed. TypeIdentifier::Extended holds the discriminator and the member list; the framing is read and written. Tests: the_extended_arm_carries_its_body, an_extended_arm_with_a_member_list_round_trips_verbatim, the_extended_arm_encodes_its_dheader_too.

C.4 TK_ANNOTATION as a TypeIdentifier loses its hash

Spec: Table 29, second to last row — TK_ANNOTATION | Annotation Declaration | Not plain type. Has TypeObject. Uses Hash of the TypeObject representation of the Annotation declaration. The union TypeIdentifier of Annex B has no case for TK_ANNOTATION; roster.py holds that contradiction as a check so that it cannot quietly disappear.

Fed: 50 and fourteen octets of hash.

Result: decodes to Unknown(0x50), re-encodes as one octet.

Corpus: 3 of 10,722.

Probe: ti-annotation-hash

Open, and not as a gap in this tree. The contradiction is written up as B9 in docs/omg-xtypes-issues.md. The behaviour changed all the same, and for the better: with the default: arm framed (C.3), 50 followed by fourteen hash octets is refused rather than silently shortened to one. The tree follows Annex B, out loud. Which of the two sentences is the right one is not decided by the page in omg-xtypes-issues.md — it is reported there.

C.5 The specification’s SCC form comes back shortened

Fed: the Annex B form — B0, three padding octets, a DHEADER of 24, then F1 as the TypeObjectHashId discriminator, fourteen hash octets, one padding octet, scc_length and scc_index. 32 octets.

Result: decode ok, and 24 octets come back. The reader took the DHEADER for the first octets of the hash. This is B.3 from the other side.

Corpus: 5 of 10,722.

Probe: ti-scc-spec-form

Closed with B.3; see there.

C.6 TypeLookup_getTypes_Out has only the first of its two fields

Spec: §7.6.3.3.3 —

@extensibility(MUTABLE)
struct TypeLookup_getTypes_Out {
    @hashid sequence<TypeIdentifierTypeObjectPair> types;
    @hashid sequence<TypeIdentifierPair>           complete_to_minimal;
};

Result: GetTypesReply in this tree has types: Vec<ReplyTypeObject> and nothing else. That is neither TypeIdentifierTypeObjectPair — the reply carries the TypeObjects without the identifier that was asked for — nor is there a complete_to_minimal. Eight zero octets (two empty sequences) in, four out.

And the framing. All six request/reply structures of §7.6.3.3.3 are @extensibility(MUTABLE) and carry their members with @hashid; under encoding version 2 that means a DHEADER and an EMHEADER per member. The tree writes a bare sequence. The probe tl-mutable-framing feeds 04000000 00000000 — a DHEADER of 4 and then an empty sequence — to GetTypesRequest: the DHEADER is read as an element count, four TK_NONE identifiers come out, and the stream re-encodes byte for byte. No error, anywhere.

Corpus: 7 of 10,722.

Probes: tl-gettypes-out, tl-mutable-framing

Closed. GetTypesReply carries both members, types as sequence<TypeIdentifierTypeObjectPair>; the four parameter types have the MUTABLE framing with the @hashid member ids of §7.3.1.2.1.1. The derivation has a check with a printed answer: the clause publishes TypeLookup_getTypes_HashId = 0x018252d3 and TypeLookup_getDependencies_HashId = 0x05aafb31, and the same function reproduces both. 04000000 00000000 is now refused. Tests: a_dheader_of_four_is_not_a_sequence_length_of_four, an_empty_get_types_request_carries_its_member_header, get_types_reply_carries_types_and_complete_to_minimal, member_ids_are_the_hashid_of_their_name, the_operation_hashes_are_the_ones_the_clause_prints.


D — emitted, and no source in the tree names it

D.1 @default_nested

Spec: §7.3.1.2.1.7 — @annotation default_nested { boolean value default TRUE; };, applied to a module declaration.

Fed: @default_nested(TRUE) module M { @final struct S { long a; }; }; — it passes the front end, the MinimalTypeObject and the CompleteTypeObject.

Corpus: 0 of 10,722. No .rs, no .idl, no .tsv in the tree contains the string @default_nested.

Probe: an-default_nested

Closed, and the zero was the tip of a deeper finding: the annotation was not in BUILTIN_ANNOTATIONS, and a module declaration never had its annotations applied at all. A whole module declared nested emitted IS_NESTED-free TypeObjects — a topic type per endpoint the author did not want. Ast::effective_nested now walks the module chain. Test: crates/idl-lower/tests/annotations.rs::default_nested_on_a_module_reaches_the_types_in_it.

The other 23 annotations of the axis and all 28 TypeKinds have at least one occurrence. That an occurrence is not an exercise is said above.


E — deliberate, with a reason

Not a gap. Each line names its record; the page keeps them apart so they stay distinguishable.

E.1 The must-understand bit on a union discriminator is always set

crates/types/src/from_ast.rs sets UnionDiscriminatorFlag::IS_MUST_UNDERSTAND unconditionally and gives its reason on the spot: §7.2.2.4.4.4.6 (“shall always be true”) and docs/omg-xtypes-issues.md §B2, which records that no implementation sets this bit. That is a deliberate departure from measured vendor behaviour in favour of the specification’s text.

E.2 The placeholder in the SCC hash is stated openly

from_ast.rs carries Scc::PLACEHOLDER_IS_UNSETTLED and says what is measured and what is not: the order, the numbering from one, the fourteen MD5 octets and the XCDR2 stream are the specification’s, the placeholder of step 1 is not, and no implementation in the interop matrix computes an SCC identifier to check it against. That is not a gap against the specification but a disclosed uncertainty.

E.3 EnumTypeFlag follows the vendors against Annex B

Annex B: typedef TypeFlag EnumTypeFlag; // Unused. No flags apply. Table 18 nevertheless requires comparing the extensibility of two enumerations. Measured: RTI Connext 7.7.0, OpenDDS and Cyclone write the TypeFlag extensibility position into the word and IS_DEFAULT (bit 6) on the first literal of an enumeration without @default_literal; Fast DDS writes neither. RTI rehashes a received enumeration from its own rendering and, without the bits, did not find the object under the announced hash. The tree writes the bits as the three vendors do; docs/omg-xtypes-issues.md §B8 holds the contradiction and the measurement.


What this survey does not cover

  • No vendor comparison inside the probes. None of the 201 probes runs against Fast DDS, Cyclone, OpenDDS or RTI; the probe binary talks to none of them. For C.1/C.2 the comparison has since been run beside them and recorded — crates/types/tests/typeobject_vendor_dheader.rs holds Cyclone’s and RTI’s octets, tools/typeobject-bytes/record.sh re-derives them — but it is a test of its own and not a column of this survey.
  • The XML type representation (§7.3.2) and the XSD representation (§7.3.3) are unchecked. Both have tables of their own (26, 27, 28) and would each have needed an extractor; crates/xml implements them and this survey does not touch it.
  • The XCDR encoding rules (§7.4.3.5) are unchecked. The serialization virtual machine with its Tables 36 through 40 is the largest single table in the specification and the axis on which tools/byte-gate and tools/wire-oracle measure — it would have doubled this survey.
  • The dynamic language binding (§7.5.2) is unchecked. Tables 48 through 58 describe 11 interfaces with their operations; what is measured here is only that a CompleteTypeObject becomes a model (the rule column of the to probes).
  • Endianness in Table 60. What is measured are the six pairs of representation and extensibility. Which of the twelve identifier values the encoder actually writes is decided in crates/rtps/src/participant_message_data.rs, and this survey does not touch it.
  • corpus is a string search. A hit does not prove the file exercises anything. A zero proves the string does not occur in the tree.
  • The assign, annotation and datarep axes have a hand-assembled denominator. Every single rule is backed; that they are all of them is not checked.

The tool

tools/xtypes-1.3-gap-scan/roster.py           pull the denominator and check it
tools/xtypes-1.3-gap-scan/roster.py --check   check only, write nothing
tools/xtypes-1.3-gap-scan/scan.sh             measure and compare
tools/xtypes-1.3-gap-scan/scan.sh --record    rewrite expected.tsv
tools/xtypes-1.3-gap-scan/scan.sh --report    print the full measurement
tools/xtypes-1.3-gap-scan/scan.sh --explain   with the first diagnostic per probe
PROBE=/path/to/xtypes-gap-probe tools/xtypes-1.3-gap-scan/scan.sh

Exit codes of scan.sh:

0 measured; the gap is exactly what expected.tsv records
1 a probe that used to get through no longer does — the gap grew
2 nothing was measured: no probe binary. Never a pass.
3 a probe now gets through that expected.tsv records as failing — the gap shrank and this page is behind the tree
4 cases.tsv and expected.tsv name different probes

Exit codes of roster.py: 0 every check holds, 1 a completeness check failed (and roster.tsv is then not written), 2 no PDF or no pdftotext — nothing was extracted.

All five paths of scan.sh and all three of roster.py have been exercised.

Runtime on an M1: 10 seconds for 199 probes over 10,722 committed sources. The probe binary is a workspace of its own under tools/xtypes-1.3-gap-scan/probe/; it is deliberately not in the root member list, so that a measuring instrument does not touch the product’s Cargo.lock.

Method note — what XTypes cost against IDL 4.2

The predecessor’s estimate was “one day per spec for the extractor plus the probes”. It holds, but the distribution is a different one.

What was cheaper here. The probes. An octet stream from Annex B is written on one line (80F3000020 04 is a plain sequence of long), and the driver is one program with seven ops rather than 356 separate files. Of the 199 probes, two had to be reworked because they failed for the wrong reason — the predecessor needed two full rounds of that.

What was more expensive here. The denominator. IDL 4.2 has Annex A; XTypes has seven tables in seven shapes, and roster.py is 437 lines for what was twenty lines of Python there. Three traps cost a round each: pdftotext puts a form feed in front of every clause that lands at the top of a page, so six of 24 clause numbers are not at the start of a line and the hole check reports a hole the document does not have; the table of contents repeats every table caption, so a search over the full text finds the entry rather than the table; and Table 29 contains the string 7.3.4.3 in one of its own cells, so a block that ends there stops before its last row — the very row that carries finding C.4.

Where the full text is needed. Unlike IDL 4.2, for every category. The grammar axes (typekind, typeid, typeobject, typelookup) fall out of Annex B’s IDL; the axes carrying 24 of the 33 findings (assign, annotation, datarep) live entirely in running text and table cells. Without the PDF in full text they would have been unfindable. The same text refuted two claims that would otherwise stand here as gaps: a TI_STRING8_SMALL with bound 0 is not an invalid bound but the unbounded string (Table 29: “It is used for unbounded strings”); and an array of @appendable elements is strongly assignable, because §7.2.4.2 makes appendable types delimited under encoding version 2.

What that means for CORBA 3.3. The predecessor’s recommended order held up: the wire area is the one where an overlooked construct quietly delivers wrong data instead of an error message — six of the nine B lines here are exactly that. For CORBA the cost split is the other way round: Part 1 Annex A carries the consolidated grammar, so the denominator is cheap again, and the running text is ~1,150 pages over three parts, eight times IDL 4.2. The cost driver there will be the B axis, and that needs the full text clause by clause. One day per part is the realistic estimate, not one day for CORBA.

One rule this survey learned. Where the specification contradicts itself, the question is not whether the tree is wrong but which of the two sentences it follows. Three findings here are of that kind (A.8, B.8/B.9, C.4), and each of them would have been written down wrongly as “a gap in the tree”. docs/omg-xtypes-issues.md is the place for that half.

Decision records

The survey itself makes none. The implementation made four, and they are written where they apply in the source rather than here:

  1. @key on a union discriminator is settled on an axis, not by the front end. crates/ast/src/facet/xtypes.rs, discriminator_is_key, and zerodds_idl_lower::KeyOnUnionDiscriminator. Table 19 needs the bit in the TypeObject; three measured vendors refuse a keyed-discriminator IDL outright, so a KeyHash over it would be one nobody else computes. Refused is the default and reports the annotation with the clause that permits it; Carried records it and the KeyHash follows.
  2. EnumTypeFlag carries the extensibility bits and IGNORE_LITERAL_NAMES at bit 5. crates/types/src/from_ast.rs, enumeration. The extensibility bits follow the measurement in E.3; §7.3.1.2.1.11 names a behaviour with no other place to live, and bit 5 sits above the five TypeFlag positions.
  3. The TypeObject DHEADER lives in the encoder, not in the hash path. crates/types/src/type_object/mod.rs, encode_into. Two paths counting it differently are worse than one that writes it nowhere.
  4. An @annotation declaration gets no TypeNode. crates/types/src/from_ast.rs, annotation_type_object. A @Name should resolve to the declaration; the TypeObject is built from the declaration rather than from a type that does not exist.

OMG DDS-XTypes 1.3 — Negativliste

Spec: OMG DDS-XTypes 1.3, formal/2020-02-04, 340 Seiten. PDF im lokalen Cache unter internal/standards/cache/omg/dds-xtypes-1.3.pdf (gitignored).

Werkzeug: tools/xtypes-1.3-gap-scan/scan.sh. Nenner: tools/xtypes-1.3-gap-scan/roster.py und die von ihm geschriebene roster.tsv, dazu anchors.tsv. Belege: tools/xtypes-1.3-gap-scan/cases.tsv (199 Proben) und expected.tsv (der aufgezeichnete Stand).

Die übrigen Seiten unter docs/spec-coverage/ sind Positivlisten: sie nennen, was betrachtet wurde, und wie weit es kam. Was nie betrachtet wurde, kann in ihnen nicht stehen, weil ihr Inhalt aus dem Baum stammt und nicht aus der Spezifikation. Diese Seite ist die andere Richtung.

Jede Zeile hier hat einen Versuch hinter sich: die Oktette oder die IDL, die gefüttert wurden, und was zurückkam. Keine Zeile steht auf einer Vermutung.

Der Nenner, und warum er hier teurer ist

OMG IDL 4.2 hat Annex A: eine konsolidierte, durchnummerierte Grammatik, aus der ein vollständiges Verzeichnis mit Lückenprobe fällt (range(1, 228) gegen die geparsten Nummern). XTypes 1.3 hat keine Grammatik. Sein Verzeichnis ist eine Menge normativer Tabellen und IDL-Blöcke, und jede braucht ihren eigenen Extraktor und ihr eigenes Vollständigkeitsargument. roster.py zieht sieben Achsen aus dem PDF und druckt pro Achse die Probe:

Achse Nenner Vollständigkeitsprobe
typekind 28 Annex B erklärt die TypeKind-Konstanten zweimal — einmal als const octet TK_x, einmal als const TypeKind TK_x. Beide Listen müssen in jedem Namen und jedem Wert übereinstimmen, und kein Wert darf zweimal vorkommen.
typeid 31 Jede TI_*-Konstante, die Annex B erklärt, muss ein case der union TypeIdentifier sein, und jedes case-Label eine erklärte Konstante. Dazu die Zeilen aus Tabelle 29.
annotation 24 §7.3.1.2.1 hat sechzehn nummerierte Unterklauseln; Lückenprobe range(1, 17). Dazu die 19 Annotationen, deren Anwendbarkeit Tabelle 21 einschränkt.
assign 55 §7.2.4.4 hat acht Unterklauseln (Lückenprobe range(1, 9)), Tabellen 14 bis 19 tragen die Definitionen. Die 55 Regeln stehen in anchors.tsv, jede mit einem wörtlichen Textstück aus dem PDF, ohne das roster.py scheitert.
typeobject 13 MinimalTypeObject und CompleteTypeObject sind zwei Unionen über denselben Diskriminator; die Probe ist, dass ihre case-Mengen gleich sind. TypeObject selbst trägt genau EK_COMPLETE und EK_MINIMAL.
typelookup 16 §7.6.3.3 hat vier Unterklauseln (Lückenprobe range(1, 5)), §7.6.3.3.4 genau zwei Operationen, §7.6.3.3.3 zehn TypeLookup_*-Typen.
datarep 16 Tabelle 59 trägt vier Urteile, Tabelle 60 zwölf Zeilen aus Repräsentation, Extensibility, Encoding-Version, Endianness und Identifier-Wert. Alle zwölf müssen sich parsen lassen.

183 Einträge über sieben Achsen, 19 Proben, alle halten.

Was das belegt und was nicht: dass jede Regel, die hier steht, in der Spezifikation steht, ist geprüft. Dass es alle Regeln der genannten Klauseln sind, ist die Behauptung dessen, der sie gelesen hat, und keine Probe. Für typekind, typeid, typeobject und typelookup fällt der Nenner aus dem IDL und ist damit maschinell vollständig; für assign, annotation und datarep ist er von Hand aufgesammelt und nur einzelbelegt.

Wie gemessen wurde

Die Proben sind hier Oktette, nicht Text. Pro Probe drei Messungen; was sie bedeuten, hängt an der Operation, und die Tabelle steht im Kopf von tools/xtypes-1.3-gap-scan/probe/src/main.rs:

Operation decode encode rule
ti die Oktette lesen sich als TypeIdentifier zurückgeschrieben ergeben sie dieselben Oktette §7.2.4.1: der Identifier ist von sich selbst zuweisbar
to die Oktette lesen sich als TypeObject dasselbe §7.6.3: das Objekt wird zu einem Modell
todh der Typ baut aus IDL das codierte TypeObject beginnt mit einem DHEADER über den Rest (§7.4.2 für @extensibility(APPENDABLE)) —
idl die IDL parst, senkt und löst auf das MinimalTypeObject übersteht Codieren, Decodieren, Codieren byte-gleich das CompleteTypeObject baut
assign beide Typen bauen aus einem IDL-Text — §7.2.4.4: is_assignable(T2, T1) sagte ja
datarep — — §7.6.3.1 Tabelle 59: die Aushandlung nahm an
encaps — — §7.6.3.1.2 Tabelle 60: die Paarung steht in der Matrix
tl die Oktette lesen sich als der genannte TypeLookup-Typ zurückgeschrieben ergeben sie dieselben Oktette —

Dazu eine Messung, die nicht am Code hängt: corpus zählt, wie viele der 10.722 committeten Quelldateien des Baums die Zeichenkette der Probe überhaupt enthalten (Spalte witness in cases.tsv). *.md ist absichtlich nicht im Korpus — eine Seite, die ein Konstrukt nennt, ist kein Beleg dafür, dass etwas es ausübt, und diese Seite zählte sich sonst selbst. Eine Null ist eine Tatsache: nichts im Baum nennt das Konstrukt, also kann nichts im Baum es ausgeübt haben. Ein - heißt, dass keine Zeichenkette das Konstrukt isoliert.

Die Zahl

Die Erhebung fand 199 Proben, 166 ohne Befund. Die 33 übrigen verteilten sich so:

Kategorie Proben davon geschlossen
A nicht erkannt — der Baum weist ab, was die Spezifikation erlaubt 17 16
B akzeptiert, obwohl die Spezifikation etwas anderes verlangt 9 7
C erkannt, aber nicht so emittiert 6 5
D emittiert, und keine Quelldatei des Baums nennt es 1 1
E bewusst so, mit Grund 3 —

Kategorie B ist die, die der Vorgänger für OMG IDL 4.2 ergänzt hat und die auch hier die wichtigste ist: dort entsteht keine Fehlermeldung, sondern ein Modell mit falschem Inhalt. Sechs der neun B-Zeilen sind Zuweisbarkeitsregeln, die stillschweigend durchgehen — zwei Endpunkte passen zusammen, die es nach §7.2.4.4 nicht dürfen.

Was daraus wurde

Dreißig der 33 sind geschlossen. Drei bleiben offen, weil sie keine Lücke im Baum sind.

Jede geschlossene Zeile unten trägt einen **Geschlossen:**-Absatz mit dem Test, der sie hält. Die drei übrigen — A.8, B.8/B.9 und C.4 — sind Stellen, an denen die Spezifikation sich selbst widerspricht: dort ist die Frage nicht, ob der Baum falsch ist, sondern welchem der beiden Sätze er folgt, und das ist eine Entscheidung und keine Reparatur. Sie stehen als B7, B8 und B9 in docs/omg-xtypes-issues.md, im Format des OMG-Issue-Formulars.

Die Probenzahl ist von 199 auf 201 gestiegen: zwei Fälle sind dazugekommen, die die neue Rahmung von der alten unterscheiden (tl-getTypes_in-extra-member, tl-call-unknown-operation). Elf Proben haben ein neues arg bekommen — die tl-*- und to-*-Oktette waren gegen die Form des Baums geschrieben und nicht gegen die der Klausel, und der Kopf von cases.tsv sagt, dass eine solche Probe nichts misst.

Der Vendor-Vergleich, den diese Seite als fehlend führte, ist gelaufen. Am 2026-09-06 auf dem Linux-Testhost, mit den dort installierten Toolchains: idlc (CycloneDDS) und rtiddsgen + DDS_TypeObjectV2_create_from_typecode (RTI Connext 7.7.0) über dieselbe IDL. Für Plain COMPLETE melden beide den Hash 7c0c6fab7d066a0c56b8e93c0ea7; Cyclones Strom ist 80 Oktette und beginnt 4c000000 — ein DHEADER von 76 —, RTIs Blob ist 104 Oktette und trägt genau diese 80 ab Offset 24. ZeroDDS erzeugt sie byte-gleich. crates/types/tests/typeobject_vendor_dheader.rs hält alle vier Fälle in beiden Formen; tools/typeobject-bytes/record.sh leitet sie neu aus den Vendoren ab.


A — nicht erkannt

Die Spezifikation erlaubt oder verlangt es, der Baum weist es ab.

A.1 TK_ANNOTATION hat im Baum kein TypeObject

Spec: Annex B, union MinimalTypeObject switch (octet) — case TK_ANNOTATION: MinimalAnnotationType annotation_type;. Tabelle 29 führt TK_ANNOTATION zusätzlich als TypeIdentifier-Diskriminator.

Gefüttert: @annotation A { long v; };

Ergebnis: no type named `A` — die Deklaration parst und senkt, aber das Modell trägt danach keinen Typ A, also kann from_ast kein MinimalAnnotationType bauen. Die Variante gibt es (crates/types/src/type_object/minimal/annotation_type.rs); erzeugt wird sie aus IDL nie.

Corpus: 4 von 10.722.

Probe: tk-annotation

Geschlossen. from_ast::annotation_type_object baut beide Formen, und Ast::annotation_defs ist das Gegenstück zu Ast::types für die andere Deklarationsart, die ein TypeObject hat. Ein TypeNode wird dafür nicht angelegt: @Name soll auf die Deklaration auflösen und nicht auf einen Typ. Test: crates/types/tests/type_object_annotations.rs::an_annotation_declaration_has_a_type_object.

A.2 Sechs der zehn TypeLookup-Typen aus §7.6.3.3.3 gibt es nicht

Spec: §7.6.3.3.3 erklärt zehn TypeLookup_*-Typen: die vier _In/_Out-Strukturen, die beiden _Result-Unionen, TypeLookup_Call, TypeLookup_Return und die beiden RPC-Hüllen TypeLookup_Request (@RPCRequestType) und TypeLookup_Reply (@RPCReplyType).

Ergebnis: crates/types/src/type_lookup.rs hat vier Gegenstücke — GetTypesRequest, GetTypesReply, GetTypeDependenciesRequest, GetTypeDependenciesReply. Die sechs übrigen fehlen. TypeLookup_Call, _Return, _Request und _Reply sind die Nachrichten, in denen die vier über die Built-in-Endpunkte aus Tabelle 61 laufen; ohne sie gibt es keinen Aufruf, nur die Nutzlast eines Aufrufs.

Corpus: 0 von 10.722 für jeden der sechs Namen.

Proben: tl-gettypes_result, tl-gettypedependencies_result, tl-call, tl-return, tl-request, tl-reply

Geschlossen. Alle zehn Typen stehen in crates/types/src/type_lookup.rs, jeder mit der Rahmung seiner Extensibility: die vier Parameter-Typen MUTABLE, die sechs übrigen appendable, weil §7.3.1.2.1.8 sagt, dass ein Typ ohne @extensibility-Angabe appendable ist. RequestHeader ist dort lokal erklärt und nicht aus zerodds-rpc genommen, weil jene Kiste von dieser abhängt. Tests: type_lookup_call_roundtrips_both_operations, type_lookup_return_roundtrips_both_operations, type_lookup_request_and_reply_roundtrip, get_types_result_roundtrips_both_arms, get_type_dependencies_result_roundtrips_both_arms.

A.3 Der default:-Arm von MinimalTypeObject wird abgewiesen

Spec: Annex B — default: MinimalExtendedType extended_type;, mit dem Kommentar // =========== Future extensibility ============.

Gefüttert: F1 C0 00000000 — EK_MINIMAL, dann ein TypeKind, den diese Fassung nicht kennt.

Ergebnis: UnknownTypeKind { kind: 192 }.

Probe: to-extended-arm

Geschlossen. MinimalTypeObject und CompleteTypeObject haben eine Extended-Variante, die den TypeKind und die Member-Liste wörtlich hält — den DHEADER ausgenommen, weil das eine Länge ist, die der Encoder schreibt, und eine Byte-Reihenfolge, die der Wert nicht trägt. Tests: a_type_kind_this_revision_does_not_define_survives_the_round_trip, an_extended_type_kind_keeps_the_member_list_it_arrived_with.

A.4 @non_serialized-Member zählen bei der Kompatibilitätsprüfung mit

Spec: §7.2.4.4.2 — „Members that are marked as non-serialized, see Sub Clause 7.3.1.2.1.14, shall be ignored during type compatibility checking.”

Gefüttert: Leser @final struct R { long a; };, Schreiber @final struct W { long a; @non_serialized long b; };

Ergebnis: No("final struct member count mismatch").

Corpus: 8 von 10.722.

Proben: as-nonser, an-non-serialized-effect

Geschlossen. Der Member steht gar nicht mehr im TypeObject, was beides auf einmal ist: er ist nicht auf der Leitung, und die Regeln, die nur das TypeObject lesen, können ihn nicht sehen. Der Member-Index läuft trotzdem weiter, weil eine @non_serialized-Deklaration eine Position besetzt. Test: crates/types/tests/table19_key_members.rs::a_non_serialized_member_does_not_enter_the_comparison.

A.5 @ignore_literal_names wirkt nicht

Spec: Tabelle 18, Zeile ENUMERATION_TYPE — „Any literals that have the same name in T1 and T2 also have the same value, and any literals that have the same value in T1 and T2 also have the same name. This behavior may be modified with the @ignore_literal_names annotation, see 7.3.1.2.1.11.”

Gefüttert: @ignore_literal_names @appendable enum R { A, B }; gegen @ignore_literal_names @appendable enum W { X, Y }; — gleiche Werte, andere Namen.

Ergebnis: No("an enum literal name and its value do not agree across the two types"). Die Annotation wird vom Frontend angenommen und ändert das Urteil nicht.

Corpus: 4 von 10.722.

Proben: as-enum-ignore-names, an-ignore-literal-names-effect

Geschlossen, mit einer benannten Nebenwirkung. Die Annotation geht jetzt in EnumTypeFlag::IGNORE_LITERAL_NAMES, und der Vergleich verlangt sie auf beiden Seiten, wie §7.3.1.2.1.11 es sagt (“in both enumerated types”); er stand auf ||, was eine Seite allein über die Namen der anderen entscheiden ließ. Damit wird ein Bit eines Flag-Wortes geschrieben, das Annex B // Unused nennt — die Extensibility-Bits desselben Wortes werden weiterhin nicht geschrieben, siehe B.8/B.9 und omg-xtypes-issues.md §B8. Test: crates/types/tests/table19_key_members.rs::ignore_literal_names_needs_the_annotation_on_both_types.

A.6 Tabelle 18 stellt keine Bedingung an die Bitmask-Flags, der Baum schon

Spec: Tabelle 18 — „BITMASK_TYPE if and only if T1.bound == T2.bound”; die Spalte „Object construction” sagt „Preserve bit values by index for all bits identified in both T1 and T2”, rechnet also ausdrücklich mit Bits, die nur eine Seite kennt.

Gefüttert: zwei Bitmasken mit @bit_bound(8), deren Flags auf verschiedenen Positionen sitzen; und eine, bei der der Schreiber ein Flag mehr hat.

Ergebnis: beide Male No("bitmask writer flag unknown in reader").

Proben: as-bm-same-bound, as-bm-writer-extra-flag

Geschlossen. Die Flag-Prüfung ist weg; der Bound bleibt, weil er die Wire-Breite ist. Die Spalte “Object construction” ist das Argument dafür, nicht nur das Fehlen einer Bedingung: “for all bits identified in both T1 and T2” rechnet ausdrücklich mit Bits, die nur eine Seite kennt, und sagt, was mit ihnen geschieht. Test: crates/types/tests/table19_key_members.rs::two_bitmasks_of_the_same_bound_are_assignable_whatever_their_flags.

A.7 Die leere DataRepresentation-Liste

Spec: §7.6.3.1 — „The default value of the DataRepresentationQosPolicy shall be an empty list of preferences. An empty list of preferences shall be taken to be equivalent to a list containing the single element XCDR.”

Gefüttert: einmal ein leeres Angebot gegen [XCDR], einmal [XCDR] gegen eine leere Anforderung.

Ergebnis: beide Male NoOverlap. negotiate_representation wendet den Default nicht an; zwei Endpunkte mit Default-QoS finden über diese Funktion nicht zusammen.

Proben: dr-empty-offer, dr-empty-request

Geschlossen. negotiate_representation setzt für eine leere Liste die Einzelliste [XCDR] ein, auf beiden Seiten. Die Ersetzung ist XCDR und nicht “irgendetwas”: ein leeres Angebot gegen einen Leser, der nur XCDR2 nimmt, findet weiterhin nichts. Test: crates/types/src/qos.rs::an_empty_list_of_preferences_is_the_single_element_xcdr.

A.8 XCDR1 mit APPENDABLE wird abgewiesen, und Tabelle 60 führt es

Spec: Tabelle 60, dritte und vierte Zeile — XCDR | APPENDABLE | 1 | Big Endian | CDR_BE | {0x00, 0x00} und XCDR | APPENDABLE | 1 | Little Endian | CDR_LE | {0x00, 0x01}.

Gefüttert: check_data_repr_extensibility(Xcdr1, Appendable).

Ergebnis: Err("Tab.59 §7.6.3.1: XCDR1 does not support APPENDABLE directly — the encoder must choose FINAL"). Die Ablehnung ist gewollt und trägt ihren Grund im Quelltext; die Fundstelle, auf die sie sich beruft, ist Tabelle 59, und die sagt dazu nichts. Tabelle 60 sagt das Gegenteil.

Probe: enc-xcdr-appendable

Offen, und nicht als Lücke im Baum. Der Widerspruch steht als B7 in docs/omg-xtypes-issues.md. Die Ablehnung bleibt, wie sie ist: sie zu ändern hieße, eine der beiden Tabellen zu wählen, und das Wählen ist Sache der RTF.


B — akzeptiert, obwohl die Spezifikation etwas anderes verlangt

Hier entsteht keine Fehlermeldung, sondern ein Modell oder ein Urteil mit falschem Inhalt.

B.1 TK_NONE ist ein gültiger Typ und von sich selbst zuweisbar

Spec: Tabelle 29, erste Zeile — TK_NONE | N/A | Invalid identifier.

Gefüttert: das Oktett 00.

Ergebnis: decode ok, encode ok, und is_assignable(None, None) ist Yes — die Identitätsabkürzung aus §7.2.4.1 greift, bevor irgendetwas prüft, ob der Identifier gültig ist. Zwei Endpunkte, die beide den ungültigen Identifier tragen, passen damit zusammen.

Corpus: 58 von 10.722.

Probe: ti-prim-none

Geschlossen in der rule-Spalte, und nur dort. is_assignable weist TK_NONE auf beiden Seiten ab, vor der Identitätsabkürzung. Decodieren und Encodieren bleiben erlaubt: TypeInformation schreibt TK_NONE für eine Äquivalenzform, die der Sender ausgelassen hat (§7.6.3.2.1), und ein Strom, der das trägt, ist nicht malformed. Test: crates/types/tests/table19_key_members.rs::tk_none_is_not_assignable_from_itself.

B.2 EK_BOTH geht als TypeIdentifier-Diskriminator durch

Spec: Annex B — die union TypeIdentifier hat case EK_COMPLETE: und case EK_MINIMAL: und keinen Fall für EK_BOTH = 0xF3. EK_BOTH gehört in den PlainCollectionHeader, nicht an diese Stelle.

Gefüttert: F3 und vierzehn Oktette Hash.

Ergebnis: angenommen als Unknown(0xF3); beim Zurückschreiben bleibt ein Oktett übrig, die vierzehn Hash-Oktette sind weg.

Corpus: 5 von 10.722.

Probe: ti-ek-both

Geschlossen. EK_BOTH am TypeIdentifier-Diskriminator wird abgewiesen. Im PlainCollectionHeader, wo es hingehört, ändert sich nichts — das ist ein Oktett-Feld und kein Union-Diskriminator. Test: ek_both_is_not_a_type_identifier_discriminator.

B.3 Die SCC-Form des Baums lässt zwei Felder aus, und liest sie auch nicht

Spec: Annex B —

@extensibility(APPENDABLE) @nested
struct StronglyConnectedComponentId {
    TypeObjectHashId sc_component_id;
    long             scc_length;
    long             scc_index;
};

TypeObjectHashId ist selbst eine Union mit dem Diskriminator EK_COMPLETE/EK_MINIMAL vor dem Hash, und die Datei trägt oben „The types in this file shall be serialized with XCDR encoding version 2” — ein APPENDABLE-Objekt bekommt unter Version 2 einen DHEADER (§7.4.2).

Gefüttert: die Form des Baums, B0 und direkt vierzehn Hash-Oktette, ohne DHEADER und ohne den TypeObjectHashId-Diskriminator.

Ergebnis: decode ok, encode ok — der Strom, den kein konformer Leser annehmen dürfte, geht durch und kommt unverändert zurück. Folge: StronglyConnectedComponentId im Baum hat kein Feld für die Äquivalenzart, und §7.3.4.6 unterscheidet genau daran („those with discriminator TI_STRONG_COMPONENT where the contained TypeObjectHashId has discriminator EK_MINIMAL“). Minimale und vollständige SCC-Identifier sind im Baum nicht unterscheidbar.

Corpus: 5 von 10.722.

Probe: ti-scc-tree-form; die Gegenrichtung steht unter C.5.

Geschlossen. StronglyConnectedComponentId hat jetzt das Feld equivalence_kind und die APPENDABLE-Rahmung; die 24-Oktett-Form des Baums wird abgewiesen. Tests: a_component_identifier_is_the_thirty_two_octets_of_annex_b, a_minimal_and_a_complete_component_identifier_differ, a_component_identifier_takes_only_the_two_equivalence_kinds.

B.4 Der @key am Union-Diskriminator erreicht das TypeObject nicht

Spec: Tabelle 19, Zeile UNION_TYPE, dritter Punkt — „Either the discriminators of both T1 and T2 are keys or neither are keys.”

Gefüttert: @final union R switch (@key long) { case 1: long a; }; gegen @final union W switch (long) { case 1: long a; };

Ergebnis: Yes. crates/types/src/from_ast.rs setzt discriminator_flags fest auf UnionDiscriminatorFlag::IS_MUST_UNDERSTAND | try_construct_bits(…); IS_KEY kommt nie hinein, also kann die Regel nichts unterscheiden.

Corpus: 192 von 10.722.

Probe: as-union-disc-key; die Gegenprobe mit @key auf beiden Seiten (as-union-disc-key-both) ergibt ebenfalls Yes, wie sie soll.

Geschlossen. Das Frontend trägt @key am Diskriminator in XTypesFacet::discriminator_is_key, from_ast in UnionDiscriminatorFlag::IS_KEY, und check_unions vergleicht die beiden. Welche Lesart gilt, entscheidet eine Achse und nicht dieses Frontend: KeyOnUnionDiscriminator::Refused ist der Default und meldet die Annotation — die gemessene Ablehnung durch Cyclone, RTI und Fast DDS (2026-08-10, /root/n11-keyunion/) ist der Grund —, und Carried ist die Bitte darum, bis hin zu einem KeyHash über den Diskriminator allein (keyhash::union_key_discriminator). Tests: crates/types/tests/table19_key_members.rs::one_keyed_union_discriminator_and_one_unkeyed_are_not_assignable, crates/idl-lower/tests/annotations.rs::a_key_on_a_union_discriminator_is_refused_with_the_clause_that_permits_it und …::a_key_on_a_union_discriminator_reaches_the_facet_when_carried.

B.5–B.7 Drei der Schlüsselregeln aus Tabelle 19 greifen nicht

Spec: Tabelle 19, Zeile STRUCTURE_TYPE. Die Regeln stehen dort wörtlich, und die Tabelle schließt mit „Note: The rules regarding key members ensure that the key of T2 can be transformed faithfully into the key of T1 without aliasing or loss of information.”

Regel (Zitat) gefüttert (Leser gegen Schreiber) Ergebnis Probe
„For any string key member m2 in T2, the m1 member of T1 with the same member ID verifies m1.type.length >= m2.type.length.” @key string<8> gegen @key string<16> Yes as-struct-key-string
„For any enumerated key member m2 in T2, the m1 member of T1 with the same member ID verifies that all literals in m2.type appear as literals in m1.type.” @key E1 mit { A } gegen @key E2 mit { A, B } Yes as-struct-key-enum
„For any sequence or map key member m2 in T2, the m1 member of T1 with the same member ID verifies m1.type.length >= m2.type.length.” @key sequence<long,4> gegen @key sequence<long,8> Yes as-struct-key-seqmap

Alle drei sind Fälle, in denen der Schlüssel des Schreibers nicht in den Schlüssel des Lesers passt und die Endpunkte trotzdem zusammenfinden.

Geschlossen. key_members_fit in assignability.rs prüft die drei Regeln, und zwar nur für Member mit IS_KEY: ein string<8>-Leser ist von einem string<16>-Schreiber zuweisbar, solange der Member kein Schlüssel ist — dann entscheidet @try_construct, und erst der Schlüssel macht den Verlust unzulässig. Die Gegenrichtung ist mitgeprüft. Tests in crates/types/tests/table19_key_members.rs: a_shorter_string_key_in_the_reader_is_not_assignable, a_writer_enum_key_literal_the_reader_lacks_is_not_assignable, a_shorter_sequence_key_in_the_reader_is_not_assignable, a_shorter_string_member_that_is_not_a_key_is_still_assignable.

B.8/B.9 Die Extensibility einer Aufzählung wird nicht verglichen

Spec: Tabelle 18, Zeile ENUMERATION_TYPE, erster Punkt — „T1.extensibility == T2.extensibility”, und dritter Punkt — „If extensibility is final T1 and T2 have the same literals.”

Gefüttert: @final enum R { A, B }; gegen @appendable enum W { A, B };, und @final enum R { A, B }; gegen @final enum W { A, B, C };

Ergebnis: beide Male Yes zum Zeitpunkt der Erhebung; seit der Vendor-Messung in E.3 beide Male No.

Warum: dieselbe Spezifikation erklärt in Annex B typedef TypeFlag EnumTypeFlag; // Unused. No flags apply, und from_ast.rs schrieb entsprechend EnumTypeFlag(0). RTI, OpenDDS und Cyclone schreiben die Extensibility-Position von TypeFlag in das Wort, und RTI rechnet den Hash einer empfangenen Aufzählung aus seiner eigenen Darstellung nach; from_ast.rs schreibt die Bits jetzt ebenso, und check_enumerations vergleicht sie, sobald beide Seiten sie tragen. Siehe E.3.

Proben: as-enum-ext, as-enum-final

Offen, und nicht als Lücke im Baum. Der Widerspruch steht als B8 in docs/omg-xtypes-issues.md. Eine Hälfte davon ist inzwischen entschieden und dort benannt: das eine Bit, das §7.3.1.2.1.11 für @ignore_literal_names braucht, wird geschrieben (A.5); die Extensibility-Bits werden es nicht.


C — erkannt, aber nicht so emittiert

Der Baum liest den Wert und schreibt etwas anderes zurück.

C.1/C.2 Das äußere TypeObject bekommt keinen DHEADER

Spec: Annex B —

@extensibility(APPENDABLE) @nested
union TypeObject switch (octet) { // EquivalenceKind
case EK_COMPLETE: CompleteTypeObject complete;
case EK_MINIMAL:  MinimalTypeObject  minimal;
};

und im Kopf derselben Datei: „The types in this file shall be serialized with XCDR encoding version 2”. §7.4.2 gibt einem APPENDABLE-Objekt unter Encoding-Version 2 einen DHEADER — einen UInt32 mit der Länge alles Folgenden.

Gefüttert: @final struct S { long a; }; und @mutable struct S { @id(1) long a; };, jeweils als MinimalTypeObject gebaut und codiert.

Ergebnis: 35 Oktette, die ersten vier ergeben 86.513 beziehungsweise 283.121, der Rest ist 31 Oktette lang. Es gibt keinen DHEADER; das erste Oktett ist der EquivalenceKind. Die inneren APPENDABLE-Strukturen bekommen ihren DHEADER — framing::encode_appendable hat 19 Aufrufe in crates/types/src/type_object/ —, das äußere Union nicht.

Nicht gemessen: ob ein Vendor an derselben Stelle einen DHEADER schreibt. Der Baum tauscht TypeObject-Oktette mit keinem Vendor aus (die Discovery matcht über type_name, siehe den Abschnitt „Wiring status” in crates/types/src/lib.rs), also gibt es im Baum kein aufgezeichnetes Vendor-Byte, gegen das sich das vergleichen ließe. Der nächste Schritt für diesen Befund ist ein PID_TYPE_OBJECT von Fast DDS oder Cyclone auf dem Linux-Testhost.

Proben: to-dheader-minimal-struct, to-dheader-complete-struct

Geschlossen, und gemessen. TypeObject::encode_into schreibt den DHEADER; hash::minimal_hash_input ruft genau diesen Encoder auf und ergänzt nichts mehr. Ein Pfad, nicht zwei — ein zweiter DHEADER würde jedes Oktett jedes Hashes verschieben, und ein Peer, der über einen Hash anderer Meinung ist, sieht aus wie ein Typkonflikt und nicht wie ein Rahmungsfehler.

Der Vendor-Vergleich, den dieser Abschnitt als nächsten Schritt nannte, ist gelaufen: siehe “Was daraus wurde” oben. CycloneDDS’ idlc und RTI Connext 7.7.0 auf dem Testhost, dieselben Oktette, derselbe Hash.

Die Gegenprobe zur RTI-Ablehnung, die diesen Befund begleitet: die angekündigte Größe ist die Länge des Stroms, über den der Hash geht, und das ist die Zahl, die RTI für dasselbe Objekt zählt — die Ablehnung war ein serializedSize mismatch von vier Oktetten. Tests: crates/types/tests/typeobject_vendor_dheader.rs (vier Fälle, beide Formen, plus die Hash- und Größengleichheit) und crates/discovery/src/type_lookup/server.rs::a_dependency_is_announced_at_its_hashed_length.

C.3 TI_EXTENDED verschluckt seinen Rumpf

Spec: Annex B — default: ExtendedTypeDefn extended_defn;, und ExtendedTypeDefn ist @extensibility(MUTABLE), hat unter Encoding-Version 2 also eine eigene Rahmung, die ein Leser überspringen kann.

Gefüttert: C0 und acht Nulloktette.

Ergebnis: decodiert zu Unknown(0xC0), zurückgeschrieben als ein Oktett. Der Leser hat vom Rumpf kein Oktett gelesen. An eingebetteter Stelle — dem Elementtyp einer Plain Sequence etwa — verschiebt das jedes folgende Feld des Stroms, ohne dass irgendwo ein Fehler entsteht.

Corpus: 3 von 10.722.

Probe: ti-extended

Geschlossen. TypeIdentifier::Extended hält Diskriminator und Member-Liste; die Rahmung wird gelesen und geschrieben. Tests: the_extended_arm_carries_its_body, an_extended_arm_with_a_member_list_round_trips_verbatim, the_extended_arm_encodes_its_dheader_too.

C.4 TK_ANNOTATION als TypeIdentifier verliert seinen Hash

Spec: Tabelle 29, vorletzte Zeile — TK_ANNOTATION | Annotation Declaration | Not plain type. Has TypeObject. Uses Hash of the TypeObject representation of the Annotation declaration. Die union TypeIdentifier in Annex B hat für TK_ANNOTATION keinen Fall; roster.py hält diesen Widerspruch als Probe fest, damit er nicht stillschweigend verschwindet.

Gefüttert: 50 und vierzehn Oktette Hash.

Ergebnis: decodiert zu Unknown(0x50), zurückgeschrieben als ein Oktett.

Corpus: 3 von 10.722.

Probe: ti-annotation-hash

Offen, und nicht als Lücke im Baum. Der Widerspruch steht als B9 in docs/omg-xtypes-issues.md. Das Verhalten hat sich trotzdem geändert, und zum Besseren: mit der Rahmung des default:-Arms (C.3) wird 50 samt vierzehn Hash-Oktetten abgewiesen statt stillschweigend auf ein Oktett verkürzt. Der Baum folgt damit Annex B, laut. Welcher der beiden Sätze der richtige ist, entscheidet die Seite in omg-xtypes-issues.md nicht — sie meldet ihn.

C.5 Die SCC-Form der Spezifikation kommt verkürzt zurück

Gefüttert: die Form aus Annex B — B0, drei Füll-Oktette, ein DHEADER von 24, dann F1 als TypeObjectHashId-Diskriminator, vierzehn Hash-Oktette, ein Füll-Oktett, scc_length und scc_index. 32 Oktette.

Ergebnis: decode ok, und zurück kommen 24 Oktette. Der Leser hat den DHEADER als die ersten Hash-Oktette gelesen. Der Befund ist B.3 von der anderen Seite.

Corpus: 5 von 10.722.

Probe: ti-scc-spec-form

Geschlossen mit B.3, siehe dort.

C.6 TypeLookup_getTypes_Out hat nur das erste seiner zwei Felder

Spec: §7.6.3.3.3 —

@extensibility(MUTABLE)
struct TypeLookup_getTypes_Out {
    @hashid sequence<TypeIdentifierTypeObjectPair> types;
    @hashid sequence<TypeIdentifierPair>           complete_to_minimal;
};

Ergebnis: GetTypesReply im Baum hat types: Vec<ReplyTypeObject> und sonst nichts. Das ist weder TypeIdentifierTypeObjectPair — die Antwort trägt die TypeObjects ohne den Identifier, nach dem gefragt wurde — noch gibt es complete_to_minimal. Acht Nulloktette (zwei leere Sequenzen) hinein, vier heraus.

Dazu die Rahmung. Alle sechs Request/Reply-Strukturen aus §7.6.3.3.3 sind @extensibility(MUTABLE) und tragen ihre Member mit @hashid; unter Encoding-Version 2 heißt das DHEADER und EMHEADER pro Member. Der Baum schreibt eine nackte Sequenz. Die Probe tl-mutable-framing füttert 04000000 00000000 — einen DHEADER von 4 und dann eine leere Sequenz — an GetTypesRequest: der DHEADER wird als Elementzahl gelesen, es entstehen vier TK_NONE-Identifier, und der Strom kommt byte-gleich zurück. Kein Fehler, nirgends.

Corpus: 7 von 10.722.

Proben: tl-gettypes-out, tl-mutable-framing

Geschlossen. GetTypesReply trägt beide Member, types als sequence<TypeIdentifierTypeObjectPair>; die vier Parameter-Typen haben die MUTABLE-Rahmung mit den @hashid-Member-IDs aus §7.3.1.2.1.1. Die Ableitung hat ihre eigene Gegenprobe: die Klausel druckt TypeLookup_getTypes_HashId = 0x018252d3 und TypeLookup_getDependencies_HashId = 0x05aafb31, und dieselbe Funktion reproduziert beide. 04000000 00000000 wird jetzt abgewiesen. Tests: a_dheader_of_four_is_not_a_sequence_length_of_four, an_empty_get_types_request_carries_its_member_header, get_types_reply_carries_types_and_complete_to_minimal, member_ids_are_the_hashid_of_their_name, the_operation_hashes_are_the_ones_the_clause_prints.


D — emittiert, und keine Quelldatei des Baums nennt es

D.1 @default_nested

Spec: §7.3.1.2.1.7 — @annotation default_nested { boolean value default TRUE; };, angewandt auf eine Modul-Deklaration.

Gefüttert: @default_nested(TRUE) module M { @final struct S { long a; }; }; — geht durch Frontend, MinimalTypeObject und CompleteTypeObject.

Corpus: 0 von 10.722. Keine .rs, keine .idl, keine .tsv des Baums enthält die Zeichenkette @default_nested.

Probe: an-default_nested

Geschlossen, und die Null war die Spitze eines tieferen Befunds: die Annotation stand nicht in BUILTIN_ANNOTATIONS, und eine Modul-Deklaration bekam ihre Annotationen überhaupt nicht angewandt. Ein ganzes Modul, das als nested erklärt war, gab IS_NESTED-freie TypeObjects aus — ein Topic-Typ pro Endpunkt, den der Autor nicht wollte. Ast::effective_nested läuft jetzt die Modulkette hoch. Test: crates/idl-lower/tests/annotations.rs::default_nested_on_a_module_reaches_the_types_in_it.

Die übrigen 23 Annotationen der Achse und alle 28 TypeKinds haben mindestens eine Fundstelle. Dass eine Fundstelle keine Ausübung belegt, steht oben.


E — bewusst so, mit Grund

Keine Lücke. Jede Zeile nennt ihren Beleg; die Seite hält sie getrennt, damit sie unterscheidbar bleiben.

E.1 Der Must-Understand-Bit am Union-Diskriminator wird immer gesetzt

crates/types/src/from_ast.rs setzt UnionDiscriminatorFlag:: IS_MUST_UNDERSTAND unbedingt und begründet das an Ort und Stelle mit §7.2.2.4.4.4.6 („shall always be true”) und mit docs/omg-xtypes-issues.md §B2, wo festgehalten ist, dass keine Implementierung dieses Bit setzt. Das ist eine bewusste Abweichung vom gemessenen Vendor-Verhalten zugunsten des Spec-Textes.

E.2 Der Platzhalter im SCC-Hash ist offen erklärt

from_ast.rs trägt Scc::PLACEHOLDER_IS_UNSETTLED und sagt dazu, was gemessen ist und was nicht: die Reihenfolge, die Nummerierung ab eins, die vierzehn MD5-Oktette und der XCDR2-Strom sind die der Spezifikation, der Platzhalter aus Schritt 1 ist es nicht, und keine Implementierung der Interop-Matrix berechnet einen SCC-Identifier, gegen den sich das prüfen ließe. Das ist keine Lücke gegen die Spezifikation, sondern eine offengelegte Unsicherheit.

E.3 EnumTypeFlag folgt den Vendoren gegen Annex B

Annex B: typedef TypeFlag EnumTypeFlag; // Unused. No flags apply. Tabelle 18 verlangt trotzdem den Vergleich der Extensibility zweier Aufzählungen. Gemessen: RTI Connext 7.7.0, OpenDDS und Cyclone schreiben die Extensibility-Position von TypeFlag in das Wort und IS_DEFAULT (Bit 6) auf das erste Literal einer Aufzählung ohne @default_literal; Fast DDS schreibt beides nicht. RTI hasht eine empfangene Aufzählung aus der eigenen Darstellung neu, ohne die Bits fand es das Objekt unter dem angekündigten Hash nicht. Der Baum schreibt die Bits wie die drei Vendoren; docs/omg-xtypes-issues.md §B8 hält den Widerspruch und die Messung.


Was diese Erhebung nicht abdeckt

  • Kein Vendor-Vergleich in den Proben selbst. Keine der 201 Proben läuft gegen Fast DDS, Cyclone, OpenDDS oder RTI; das Probe-Binary spricht mit keinem. Für C.1/C.2 ist der Vergleich inzwischen daneben gelaufen und aufgezeichnet — crates/types/tests/typeobject_vendor_dheader.rs hält Cyclones und RTIs Oktette, tools/typeobject-bytes/record.sh leitet sie neu ab —, aber er ist ein eigener Test und keine Spalte dieser Erhebung.
  • Die XML-Typrepräsentation (§7.3.2) und die XSD-Repräsentation (§7.3.3) sind ungeprüft. Beide haben eigene Tabellen (26, 27, 28) und hätten je einen eigenen Extraktor gebraucht; crates/xml implementiert sie und ist von dieser Erhebung nicht berührt.
  • Die XCDR-Encoding-Regeln (§7.4.3.5) sind ungeprüft. Die Serialisierungs-VM mit ihren Tabellen 36 bis 40 ist die größte Einzeltabelle der Spezifikation und die Achse, auf der tools/byte-gate und tools/wire-oracle messen — sie hätte diese Erhebung verdoppelt. Der Zusammenhang steht in docs/spec-coverage/-Positivseiten und in docs/omg-xtypes-issues.md.
  • Das Dynamic Language Binding (§7.5.2) ist ungeprüft. Tabellen 48 bis 58 beschreiben 11 Interfaces mit ihren Operationen; gemessen ist hier nur, dass ein CompleteTypeObject zu einem Modell wird (rule-Spalte der to-Proben).
  • Endianness in Tabelle 60. Gemessen sind die sechs Paare aus Repräsentation und Extensibility. Welchen der zwölf Identifier-Werte der Encoder tatsächlich schreibt, entscheidet crates/rtps/src/participant_message_data.rs, und das ist von dieser Erhebung nicht berührt.
  • corpus ist Zeichenkettensuche. Ein Treffer belegt nicht, dass die Datei etwas ausübt. Eine Null belegt, dass die Zeichenkette im Baum nicht vorkommt.
  • Die Achsen assign, annotation und datarep haben einen von Hand aufgesammelten Nenner. Jede einzelne Regel ist belegt; dass es alle sind, ist nicht geprüft.

Das Werkzeug

tools/xtypes-1.3-gap-scan/roster.py           den Nenner ziehen und prüfen
tools/xtypes-1.3-gap-scan/roster.py --check   nur prüfen, nichts schreiben
tools/xtypes-1.3-gap-scan/scan.sh             messen und vergleichen
tools/xtypes-1.3-gap-scan/scan.sh --record    expected.tsv neu schreiben
tools/xtypes-1.3-gap-scan/scan.sh --report    die volle Messung ausgeben
tools/xtypes-1.3-gap-scan/scan.sh --explain   dazu die erste Diagnose je Probe
PROBE=/pfad/zu/xtypes-gap-probe tools/xtypes-1.3-gap-scan/scan.sh

Exit-Codes von scan.sh:

0 gemessen; die Lücke ist genau die aus expected.tsv
1 eine Probe, die durchkam, kommt nicht mehr durch — die Lücke ist gewachsen
2 nichts gemessen: kein Probe-Binary. Nie ein Pass.
3 eine Probe kommt jetzt durch, die expected.tsv als scheiternd führt — die Lücke ist geschrumpft und diese Seite ist veraltet
4 cases.tsv und expected.tsv nennen verschiedene Proben

Exit-Codes von roster.py: 0 alle Proben halten, 1 eine Vollständigkeitsprobe ist gescheitert (dann wird roster.tsv nicht geschrieben), 2 kein PDF oder kein pdftotext — nichts extrahiert.

Alle fünf Pfade von scan.sh und alle drei von roster.py sind ausgelöst worden.

Laufzeit auf einem M1: 10 Sekunden für 199 Proben über 10.722 committete Quelldateien. Das Probe-Binary ist ein eigener Workspace unter tools/xtypes-1.3-gap-scan/probe/; es steht absichtlich nicht in der Member-Liste der Wurzel, damit ein Messinstrument die Cargo.lock des Produkts nicht anfasst.

Methodennotiz — was XTypes gegenüber IDL 4.2 gekostet hat

Die Schätzung des Vorgängers lautete „pro Spec ein Tag für den Extraktor plus die Proben”. Sie trägt, aber die Verteilung ist eine andere.

Was hier billiger war. Die Proben. Ein Oktettstrom aus Annex B ist in einer Zeile geschrieben (80F3000020 04 ist eine Plain Sequence von long), und der Treiber ist ein Programm mit sieben Operationen statt 356 einzelner Dateien. Von den 199 Proben mussten zwei überarbeitet werden, weil sie aus dem falschen Grund scheiterten — beim Vorgänger waren es zwei ganze Runden Nacharbeit.

Was hier teurer war. Der Nenner. IDL 4.2 hat Annex A; XTypes hat sieben Tabellen in sieben Formen, und roster.py ist 437 Zeilen für das, was dort zwanzig Zeilen Python waren. Drei Fallen haben je eine Runde gekostet: pdftotext setzt ein Form Feed vor jede Klausel am Seitenanfang, sodass sechs von 24 Klauselnummern nicht am Zeilenanfang stehen und die Lückenprobe eine Lücke meldet, die es nicht gibt; das Inhaltsverzeichnis wiederholt jede Tabellenüberschrift, sodass eine Suche über den Volltext den Eintrag findet statt der Tabelle; und Tabelle 29 enthält den String 7.3.4.3 in einer ihrer eigenen Zellen, sodass ein Block, der dort endet, vor seiner letzten Zeile aufhört — genau der Zeile mit dem Befund C.4.

Wo der Volltext gebraucht wird. Anders als bei IDL 4.2 für alle Kategorien. Die Grammatik-Achsen (typekind, typeid, typeobject, typelookup) fallen aus dem IDL von Annex B; die Achsen, auf denen 24 der 33 Befunde liegen (assign, annotation, datarep), stehen ausschließlich im Fließtext und in Tabellenzellen. Ohne PDF im Volltext wären sie unauffindbar gewesen. Zwei Behauptungen hat derselbe Text widerlegt, die sonst als Lücke hier stünden: ein TI_STRING8_SMALL mit Bound 0 ist kein ungültiger Bound, sondern der unbeschränkte String (Tabelle 29: „It is used for unbounded strings”); und ein Array von @appendable-Elementen ist sehr wohl stark zuweisbar, weil §7.2.4.2 appendable Typen unter Encoding-Version 2 zu delimited erklärt.

Was das für CORBA 3.3 heißt. Die Reihenfolge, die der Vorgänger empfohlen hat, hat sich bestätigt: der Wire-Bereich ist der, in dem ein übersehenes Konstrukt still falsche Daten liefert statt einer Fehlermeldung — sechs der neun B-Zeilen hier sind genau das. Für CORBA gilt der Kostenschnitt umgekehrt: Part 1 Annex A trägt die konsolidierte Grammatik, der Nenner ist also wieder billig, und der Fließtext ist mit ~1.150 Seiten über drei Teile achtmal so lang wie IDL 4.2. Der Kostentreiber wird dort die B-Achse sein, und die braucht den Volltext Klausel für Klausel. Ein Tag pro Teil ist die realistische Schätzung, nicht ein Tag für CORBA.

Eine Regel, die diese Erhebung dazugelernt hat. Wo die Spezifikation sich selbst widerspricht, ist die Frage nicht, ob der Baum falsch ist, sondern welcher der beiden Sätze er befolgt. Drei Befunde hier sind von dieser Art (A.8, B.8/B.9, C.4), und jeder von ihnen wäre als „Lücke im Baum” falsch aufgeschrieben. docs/omg-xtypes-issues.md ist der Ort für diese Hälfte.

Decision-Records

Die Erhebung selbst trifft keine. Die Umsetzung hat vier getroffen, und sie stehen an Ort und Stelle im Quelltext, nicht hier:

  1. @key am Union-Diskriminator steht auf einer Achse, nicht in der Hand des Frontends. crates/ast/src/facet/xtypes.rs, discriminator_is_key, und zerodds_idl_lower::KeyOnUnionDiscriminator. Tabelle 19 braucht das Bit im TypeObject; drei gemessene Vendoren weisen eine keyed-Diskriminator-IDL ab, also wäre ein KeyHash darüber einer, den niemand nachrechnet. Refused ist der Default und meldet die Annotation mit der Klausel, die sie zulässt; Carried trägt sie, und der KeyHash folgt.
  2. EnumTypeFlag trägt die Extensibility-Bits und IGNORE_LITERAL_NAMES an Bit 5. crates/types/src/from_ast.rs, enumeration. Die Extensibility-Bits folgen der Messung in E.3; §7.3.1.2.1.11 nennt ein Verhalten, für das es keinen anderen Platz gibt, und Bit 5 liegt über den fünf TypeFlag-Positionen.
  3. Der DHEADER des TypeObject steht im Encoder, nicht im Hash-Pfad. crates/types/src/type_object/mod.rs, encode_into. Zwei Pfade, die ihn unterschiedlich zählen, sind schlimmer als einer, der ihn nirgends schreibt.
  4. Eine @annotation-Deklaration bekommt kein TypeNode. crates/types/src/from_ast.rs, annotation_type_object. Ein @Name soll auf die Deklaration auflösen; das TypeObject wird aus der Deklaration gebaut statt aus einem Typ, den es nicht gibt.