From 074bcfb0cc33ae5d985bb39c5ee3de63d4d164c1 Mon Sep 17 00:00:00 2001 From: jmh530 Date: Wed, 4 Nov 2026 08:40:20 -0500 Subject: [PATCH] Preserve equality attributes for annotated algebraic values Recursive equality attribute inference can fail depending on compiler source-file order, as reported in dlang/dmd#24202. This affects algebraic variants containing Annotated or AnnotatedOnce alternatives. Explicitly declare pure, nothrow, and @nogc on both wrappers' equality methods when the wrapped type is a Mir algebraic variant, whose equality already guarantees those attributes. Preserve attribute inference for other wrapped types. Add regression coverage for nested equality, differing annotations and values, const receivers, and calls requiring all three attributes. Verify that wrappers around effectful custom types retain their original attribute restrictions. AI-Assisted-By: Codex (GPT-6 Astra Medium) --- source/mir/algebraic_alias/ion.d | 53 ++++++++++++++++++++++ source/mir/annotated.d | 77 ++++++++++++++++++++++++++++---- 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/source/mir/algebraic_alias/ion.d b/source/mir/algebraic_alias/ion.d index c98e8971..fb3c7853 100644 --- a/source/mir/algebraic_alias/ion.d +++ b/source/mir/algebraic_alias/ion.d @@ -173,3 +173,56 @@ unittest value = annotated.IonAlgebraic; assert(value == annotated); } + +version (unittest) +{ + private import mir.algebraic : Variant; + private import mir.annotated : AnnotatedOnce; + private alias OnceAnnotatedAlgebraic = Variant!(int, AnnotatedOnce!This); +} + +// Compile this module together with mir.annotated in both source orders. +// Recursive equality must preserve attributes and compare tags and values. +unittest +{ + import mir.annotated : AnnotatedOnce; + + static foreach (once; [false, true]) + {{ + static if (!once) + { + alias Node = IonAlgebraic; + alias W = Annotated!Node; + auto a = W(["tag"], 1); + auto b = W(["tag"], 1); + auto differentTag = W(["other"], 1); + auto differentValue = W(["tag"], 2); + auto parentA = W(["parent"], a); + auto parentB = W(["parent"], b); + } + else + { + alias Node = OnceAnnotatedAlgebraic; + alias W = AnnotatedOnce!Node; + auto a = W("tag", 1); + auto b = W("tag", 1); + auto differentTag = W("other", 1); + auto differentValue = W("tag", 2); + auto parentA = W("parent", a); + auto parentB = W("parent", b); + } + static bool equal(scope const ref W lhs, scope const ref W rhs) pure nothrow @nogc + { + return lhs == rhs; + } + assert(a == b); + const ca = a; + assert(ca == b); + assert(equal(a, b)); + assert(!equal(a, differentTag)); + assert(!equal(a, differentValue)); + assert(equal(parentA, parentB)); + parentB.value = differentValue; + assert(!equal(parentA, parentB)); + }} +} diff --git a/source/mir/annotated.d b/source/mir/annotated.d index 7b2d7c81..91271b50 100644 --- a/source/mir/annotated.d +++ b/source/mir/annotated.d @@ -8,6 +8,7 @@ Macros: module mir.annotated; import mir.internal.meta: basicElementType; +import mir.algebraic: isVariant; import mir.serde: serdeRegister, serdeAnnotation, serdeIsDynamicAlgebraic; static immutable excMsg = "At least one annotation is required to create an annotated value."; @@ -33,7 +34,6 @@ struct Annotated(T) { private enum _alloc_ = true; else { - import mir.algebraic: isVariant; static if (isVariant!T) private enum _alloc_ = true; else @@ -63,10 +63,23 @@ struct Annotated(T) { return *_value; } - /// - bool opEquals(scope const Annotated rhs) scope const + // Algebraic equality already promises these attributes. State them here + // to break recursive attribute inference through annotated alternatives. + static if (isVariant!T) + { + /// + bool opEquals(scope const Annotated rhs) scope const pure nothrow @nogc + { + return annotations == rhs.annotations && value == rhs.value; + } + } + else { - return annotations == rhs.annotations && value == rhs.value; + /// ditto + bool opEquals(scope const Annotated rhs) scope const + { + return annotations == rhs.annotations && value == rhs.value; + } } size_t toHash() scope @trusted const pure nothrow @nogc @@ -186,7 +199,6 @@ struct AnnotatedOnce(T) { private enum _alloc_ = true; else { - import mir.algebraic: isVariant; static if (isVariant!T) private enum _alloc_ = true; else @@ -215,10 +227,23 @@ struct AnnotatedOnce(T) { return *_value; } - /// - bool opEquals(scope const AnnotatedOnce rhs) scope const + // Algebraic equality already promises these attributes. State them here + // to break recursive attribute inference through annotated alternatives. + static if (isVariant!T) + { + /// + bool opEquals(scope const AnnotatedOnce rhs) scope const pure nothrow @nogc + { + return annotation == rhs.annotation && value == rhs.value; + } + } + else { - return annotation == rhs.annotation && value == rhs.value; + /// ditto + bool opEquals(scope const AnnotatedOnce rhs) scope const + { + return annotation == rhs.annotation && value == rhs.value; + } } } else @@ -300,3 +325,39 @@ unittest assert(ac.annotation == annotation); assert(ac.value.x == 5); } + +// Equality must not acquire attributes that the wrapped value cannot satisfy. +unittest +{ + static struct Effectful + { + enum _serdeRecursiveAlgebraic = true; + int payload; + static int comparisons; + + bool opEquals(scope const Effectful rhs) const + { + ++comparisons; + auto copy = new int(payload); + if (payload < 0) + throw new Exception("negative payload"); + return *copy == rhs.payload; + } + } + static foreach (once; [false, true]) + {{ + static if (once) + alias W = AnnotatedOnce!Effectful; + else + alias W = Annotated!Effectful; + static assert(!__traits(compiles, () pure { W a, b; return a == b; })); + static assert(!__traits(compiles, () nothrow { W a, b; return a == b; })); + static assert(!__traits(compiles, () @nogc { W a, b; return a == b; })); + W a, b; + a.value = Effectful(1); + b.value = Effectful(1); + auto before = Effectful.comparisons; + assert(a == b); + assert(Effectful.comparisons > before); + }} +}