Skip to content

THRIFT-6075: Generate Equal method for Delphi Thrift structs#3612

Draft
Jens-G wants to merge 4 commits into
apache:masterfrom
Jens-G:THRIFT-6075
Draft

THRIFT-6075: Generate Equal method for Delphi Thrift structs#3612
Jens-G wants to merge 4 commits into
apache:masterfrom
Jens-G:THRIFT-6075

Conversation

@Jens-G

@Jens-G Jens-G commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds function Equal(const other: I{Name}): Boolean to every generated Delphi struct interface and its implementation class
  • Overrides function Equals(Obj: TObject): Boolean on the implementation class to delegate to Equal via interface check
  • Overrides Equals(Obj: TObject) on generated exception wrapper classes (T{Name}) to compare the inner FData struct
  • Implements field-by-field comparison respecting __isset semantics for optional fields and recursing into all container/struct types

Detail

The generator gains two new methods:

generate_equal_container — recursive helper that emits comparison code for any field type into a pair of ostringstream buffers (code and vars). Uses the boolean-variable pattern (_eqN := True; ... if not _eqN then Exit(False)) so comparison never uses Exit inside loops, supporting arbitrary nesting depth. Strategies per type:

  • Scalar: direct <> comparison
  • Binary (TBytes / IThriftBytes): length check then System.SysUtils.CompareMem
  • Struct: .Equal() recursive call
  • List: indexed element loop
  • Set with scalar elements: Contains() (O(n), value-hash equality)
  • Set with interface-typed elements: O(n²) scan calling .Equal() on each pair
  • Map with scalar keys: ContainsKey() + value comparison
  • Map with interface-typed keys: O(n²) key scan

generate_delphi_struct_equality_impl — walks all struct fields. For optional/unqualified fields emits __isset guard first (mismatch ⇒ Exit(False)); required fields compare values directly with no isset check.

Tests

Extended TTestSerializer.Test_Equal in lib/delphi/test/serializer/TestSerializer.Tests.pas:

  • Unqualified scalar fields with isset guards (Bonk)
  • Optional fields: isset mismatch, both-unset, both-set (TupleProtocolTestStruct)
  • Nil vs non-nil nested struct, structural mismatch (Nesting)
  • List count mismatch and element mismatch (OneOfEach.Byte_list)
  • Set-of-scalar match/count-mismatch/element-mismatch (CompactProtoTestStruct.I32_set)
  • Set-of-struct match/nil-vs-non-nil (CompactProtoTestStruct.Struct_set)
  • Map match/missing-key/value-mismatch (CompactProtoTestStruct.Byte_byte_map)
  • Binary field match/content-mismatch/length-mismatch (Base64.B1, COM and non-COM branches)
  • Equals(TObject) compatible type, value mismatch, incompatible type (TBonkImpl)
  • Exception wrapper equality/mismatch/incompatible type (TMutableException)

Test plan

  • CI Delphi build passes
  • TTestSerializer.Test_Equal assertions all pass
  • make style clean

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

@Jens-G Jens-G marked this pull request as ready for review June 30, 2026 23:02
@Jens-G Jens-G marked this pull request as draft July 1, 2026 00:41
Client: delphi
Patch: Jens Geyer

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Jens-G and others added 3 commits July 2, 2026 23:30
…ssion tests

Client: delphi

The set/map equality scan in generate_equal_container checked only that
every lhs element had some match in rhs, without marking matched rhs
entries as consumed. One rhs element could satisfy multiple lhs elements,
producing a false Equal=True when one side held reference-distinct but
value-equal elements (e.g. two struct-typed set elements with the same
field values, reachable because the generated collections use no custom
IEqualityComparer and Delphi's default comparer for interface-typed
elements is reference-based). Both the set and map branches now match
against an indexed copy of rhs plus a parallel used-flags array, marking
entries consumed on match.

Adds EqualityItem/EqualityItemContainers fixtures to SimpleException.thrift
(Empty has no fields and can't express two distinct-but-equal instances)
and set/map duplicate-value and duplicate-key regression cases to
Test_Equal, alongside a sanity check for the legitimate-equal case.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ew code

Client: delphi

generate_delphi_struct_equality_impl took an is_exception parameter whose
only call site always passed false; the true branch was unreachable
(exceptions get their equality via a separate hand-written Equals(TObject)
wrapper). Removed the parameter and the dead branch.

Ran git clang-format scoped to the lines changed since 289a1c1, rather
than a full-file reformat, since this file was not clang-format-conformant
before this PR and a full pass would have produced an unrelated diff.

Verified both changes are no-ops: rebuilt the compiler and regenerated
test/DebugProtoTest.thrift (default and com_types) before and after,
output is byte-identical.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Client: delphi

Three changes to generate_equal_container, none of which alter the
comparison semantics:

1. Capture operands into locals: the recursion previously passed
   composed access-path strings down, so expressions like
   Self.List_field[_i][_k][_k2] (property getter + GetItem + two hash
   lookups) were re-evaluated at every mention, up to ~8x per loop
   iteration in deeply nested containers. Operands are now captured
   into fresh locals once per level (already_local skips the copy where
   the operand is a loop variable, pair field or snapshot array slot).

2. Compare maps via pair enumeration and TryGetValue: the scalar-key
   path iterates lhs pairs and fetches the rhs value with a single
   lookup (was three: ContainsKey + lhs[k] + rhs[k], plus repeated
   getters). The non-scalar-key consume-scan snapshots rhs keys and
   values into parallel arrays, removing hash lookups from that path
   entirely.

3. Exit(False) directly on mismatch: a new exit_on_fail mode lets
   field-level, list-element and scalar-key-map comparisons leave the
   generated function immediately instead of threading _eq flags and
   break statements through every nesting level. The flag-and-fall-
   through form remains only for the candidate probes inside the
   set/map consume-scans, where a mismatch means "try the next
   candidate" rather than "structs differ".

Verified by rebuilding the compiler and regenerating ThriftTest,
DebugProtoTest and SimpleException in default, com_types and rtti
modes: all 358 generated Equal/Equals functions are begin/end-balanced,
structs without containers/binaries generate byte-identical code, and
the duplicate-element consume-scan regression shape is preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant