From b35b1b1a403faaa83fa1838cecc4659f1e5fe8b3 Mon Sep 17 00:00:00 2001 From: Paul Backus Date: Mon, 3 Mar 2025 10:18:18 -0500 Subject: [PATCH 1/2] Rename SumType.get to getByIndex --- src/sumtype.d | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/sumtype.d b/src/sumtype.d index 2e36eeb..1dc5dad 100644 --- a/src/sumtype.d +++ b/src/sumtype.d @@ -329,7 +329,7 @@ private: @trusted // Explicit return type omitted // Workaround for https://github.com/dlang/dmd/issues/20549 - ref get(size_t tid)() inout + ref getByIndex(size_t tid)() inout if (tid < Types.length) { assert(tag == tid, @@ -1001,7 +1001,7 @@ version (D_BetterC) {} else alias MySum = SumType!(ubyte, void*[2]); MySum x = [null, cast(void*) 0x12345678]; - void** p = &x.get!1[1]; + void** p = &x.getByIndex!1[1]; x = ubyte(123); assert(*p != cast(void*) 0x12345678); @@ -1030,8 +1030,8 @@ version (D_BetterC) {} else } catch (Exception e) {} assert( - (x.tag == 0 && x.get!0.value == 123) || - (x.tag == 1 && x.get!1.value == 456) + (x.tag == 0 && x.getByIndex!0.value == 123) || + (x.tag == 1 && x.getByIndex!1.value == 456) ); } @@ -1089,8 +1089,8 @@ version (D_BetterC) {} else SumType!(S[1]) x = [S(0)]; SumType!(S[1]) y = x; - auto xval = x.get!0[0].n; - auto yval = y.get!0[0].n; + auto xval = x.getByIndex!0[0].n; + auto yval = y.getByIndex!0[0].n; assert(xval != yval); } @@ -1165,8 +1165,8 @@ version (D_BetterC) {} else SumType!S y; y = x; - auto xval = x.get!0.n; - auto yval = y.get!0.n; + auto xval = x.getByIndex!0.n; + auto yval = y.getByIndex!0.n; assert(xval != yval); } @@ -1237,8 +1237,8 @@ version (D_BetterC) {} else SumType!S x = S(); SumType!S y = x; - auto xval = x.get!0.n; - auto yval = y.get!0.n; + auto xval = x.getByIndex!0.n; + auto yval = y.getByIndex!0.n; assert(xval != yval); } @@ -1793,10 +1793,10 @@ private template matchImpl(Flag!"exhaustive" exhaustive, handlers...) * argument's tag, so there's no need to use TagTuple. */ enum handlerArgs(size_t caseId) = - "args[0].get!(" ~ toCtString!caseId ~ ")()"; + "args[0].getByIndex!(" ~ toCtString!caseId ~ ")()"; alias valueTypes(size_t caseId) = - typeof(args[0].get!(caseId)()); + typeof(args[0].getByIndex!(caseId)()); enum numCases = SumTypes[0].Types.length; // Multiple dispatch (slow path) @@ -1820,7 +1820,7 @@ private template matchImpl(Flag!"exhaustive" exhaustive, handlers...) template getType(size_t i) { - alias getType = typeof(args[i].get!(tags[i])()); + alias getType = typeof(args[i].getByIndex!(tags[i])()); } alias valueTypes = Map!(getType, Iota!(tags.length)); @@ -2023,7 +2023,7 @@ template handlerArgs(size_t caseId, typeCounts...) static foreach (i; 0 .. tags.length) { handlerArgs = AliasSeq!( handlerArgs, - "args[" ~ toCtString!i ~ "].get!(" ~ toCtString!(tags[i]) ~ ")(), " + "args[" ~ toCtString!i ~ "].getByIndex!(" ~ toCtString!(tags[i]) ~ ")(), " ); } } @@ -2271,7 +2271,7 @@ version (D_Exceptions) (ref double d) { d *= 2; } ); - assert(value.get!1.isClose(6.28)); + assert(value.getByIndex!1.isClose(6.28)); } // Handlers that return by ref From c6f232a5c25eb4b9b3253e0f842bdb4ceff9766e Mon Sep 17 00:00:00 2001 From: Paul Backus Date: Tue, 4 Mar 2025 07:19:00 -0500 Subject: [PATCH 2/2] Add procedural API This change introduces three new convenience functions that allow code using SumType to be written in a more procedural style (as opposed to the functional style of match). * has!T checks whether a SumType contains a value of type T. * get!T accesses a SumType's value if it has type T, or asserts if it does not. * tryGet!T is like get!T, but throws an exception instead of asserting on failure. To make room for get!T, the existing SumType.get method is renamed to getByIndex. Frontend requirement is bumped to 2.103 for __traits(fullyQualifiedName). Phobos PR: https://github.com/dlang/phobos/pull/10650/ --- .github/workflows/test.yml | 6 +- dub.json | 2 +- src/sumtype.d | 384 +++++++++++++++++++++++++++++++++++++ 3 files changed, 388 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ea61415..14bf6fc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,13 +16,13 @@ jobs: - macOS-latest dc: - dmd-latest - - dmd-2.101.2 + - dmd-2.103.1 - ldc-latest - - ldc-1.31.0 + - ldc-1.33.0 exclude: # macOS requires DMD >= 2.107.1 - os: macOS-latest - dc: dmd-2.101.2 + dc: dmd-2.103.1 runs-on: ${{ matrix.os }} steps: - name: Checkout repository diff --git a/dub.json b/dub.json index 900cf96..c8dc3a3 100644 --- a/dub.json +++ b/dub.json @@ -8,7 +8,7 @@ "license": "BSL-1.0", "toolchainRequirements": { - "frontend": ">=2.101" + "frontend": ">=2.103" }, "dflags": ["-preview=fieldwise"], diff --git a/src/sumtype.d b/src/sumtype.d index 1dc5dad..0926991 100644 --- a/src/sumtype.d +++ b/src/sumtype.d @@ -2490,6 +2490,390 @@ static if (haveDip1000 && __VERSION__ >= 2100) })); } +/** + * Checks whether a `SumType` contains a value of a given type. + * + * The types must match exactly, without implicit conversions. + * + * Params: + * T = the type to check for. + */ +template has(T) +{ + /** + * The actual `has` function. + * + * Params: + * self = the `SumType` to check. + * + * Returns: true if `self` contains a `T`, otherwise false. + */ + bool has(Self)(auto ref Self self) + if (isSumType!Self) + { + return self.match!checkType; + } + + // Helper to avoid redundant template instantiations + private bool checkType(Value)(ref Value value) + { + return is(Value == T); + } +} + +/// Basic usage +@safe unittest { + SumType!(string, double) example = "hello"; + + assert( example.has!string); + assert(!example.has!double); + + // If T isn't part of the SumType, has!T will always return false + assert(!example.has!int); +} + +/// With type qualifiers +@safe unittest { + alias Example = SumType!(string, double); + + Example m = "mutable"; + const Example c = "const"; + immutable Example i = "immutable"; + + assert( m.has!string); + assert(!m.has!(const(string))); + assert(!m.has!(immutable(string))); + + assert(!c.has!string); + assert( c.has!(const(string))); + assert(!c.has!(immutable(string))); + + assert(!i.has!string); + assert(!i.has!(const(string))); + assert( i.has!(immutable(string))); +} + +/// As a predicate +version (D_BetterC) {} else +@safe unittest { + import std.algorithm.iteration: filter; + import std.algorithm.comparison: equal; + + alias Example = SumType!(string, double); + + auto arr = [ + Example("foo"), + Example(0), + Example("bar"), + Example(1), + Example(2), + Example("baz") + ]; + + auto strings = arr.filter!(has!string); + auto nums = arr.filter!(has!double); + + assert(strings.equal([Example("foo"), Example("bar"), Example("baz")])); + assert(nums.equal([Example(0), Example(1), Example(2)])); +} + +// Non-copyable types +@safe unittest { + static struct NoCopy + { + @disable this(this); + } + + SumType!NoCopy x; + + assert(x.has!NoCopy); +} + +/** + * Accesses a `SumType`'s value. + * + * The value must be of the specified type. Use [has] to check. + * + * Params: + * T = the type of the value being accessed. + */ +template get(T) +{ + /** + * The actual `get` function. + * + * Params: + * self = the `SumType` whose value is being accessed. + * + * Returns: the `SumType`'s value. + */ + auto ref T get(Self)(auto ref Self self) + if (isSumType!Self) + { + import std.typecons : No; + + static if (__traits(isRef, self)) + return self.match!(getLvalue!(No.try_, T)); + else + return self.match!(getRvalue!(No.try_, T)); + } +} + +/// Basic usage +@safe unittest { + SumType!(string, double) example1 = "hello"; + SumType!(string, double) example2 = 3.14; + + assert(example1.get!string == "hello"); + assert(example2.get!double == 3.14); +} + +/// With type qualifiers +@safe unittest { + alias Example = SumType!(string, double); + + Example m = "mutable"; + const(Example) c = "const"; + immutable(Example) i = "immutable"; + + assert(m.get!string == "mutable"); + assert(c.get!(const(string)) == "const"); + assert(i.get!(immutable(string)) == "immutable"); +} + +/// As a predicate +version (D_BetterC) {} else +@safe unittest { + import std.algorithm.iteration: map; + import std.algorithm.comparison: equal; + + alias Example = SumType!(string, double); + + auto arr = [Example(0), Example(1), Example(2)]; + auto values = arr.map!(get!double); + + assert(values.equal([0, 1, 2])); +} + +// Non-copyable types +@safe unittest { + static struct NoCopy + { + @disable this(this); + } + + SumType!NoCopy lvalue; + auto rvalue() { return SumType!NoCopy(); } + + assert(lvalue.get!NoCopy == NoCopy()); + assert(rvalue.get!NoCopy == NoCopy()); +} + +// Immovable rvalues +@safe unittest { + auto rvalue() { return const(SumType!string)("hello"); } + + assert(rvalue.get!(const(string)) == "hello"); +} + +// Nontrivial rvalues at compile time +@safe unittest { + static struct ElaborateCopy + { + this(this) {} + } + + enum rvalue = SumType!ElaborateCopy(); + enum ctResult = rvalue.get!ElaborateCopy; + + assert(ctResult == ElaborateCopy()); +} + +/** + * Attempt to access a `SumType`'s value. + * + * If the `SumType` does not contain a value of the specified type, an + * exception is thrown. + * + * Params: + * T = the type of the value being accessed. + */ +version (D_Exceptions) +template tryGet(T) +{ + /** + * The actual `tryGet` function. + * + * Params: + * self = the `SumType` whose value is being accessed. + * + * Throws: `MatchException` if the value does not have the expected type. + * + * Returns: the `SumType`'s value. + */ + auto ref T tryGet(Self)(auto ref Self self) + if (isSumType!Self) + { + import std.typecons: Yes; + + static if (__traits(isRef, self)) + return self.match!(getLvalue!(Yes.try_, T)); + else + return self.match!(getRvalue!(Yes.try_, T)); + } +} + +/// Basic usage +version (D_Exceptions) +@safe unittest { + SumType!(string, double) example = "hello"; + + assert(example.tryGet!string == "hello"); + + double result = double.nan; + try + result = example.tryGet!double; + catch (MatchException e) + result = 0; + + // Exception was thrown + assert(result == 0); +} + +/// With type qualifiers +version (D_Exceptions) +@safe unittest { + import std.exception: assertThrown; + + const(SumType!(string, double)) example = "const"; + + // Qualifier mismatch; throws exception + assertThrown!MatchException(example.tryGet!string); + // Qualifier matches; no exception + assert(example.tryGet!(const(string)) == "const"); +} + +/// As a predicate +version (D_BetterC) {} else +@safe unittest { + import std.algorithm.iteration: map, sum; + import std.functional: pipe; + import std.exception: assertThrown; + + alias Example = SumType!(string, double); + + auto arr1 = [Example(0), Example(1), Example(2)]; + auto arr2 = [Example("foo"), Example("bar"), Example("baz")]; + + alias trySum = pipe!(map!(tryGet!double), sum); + + assert(trySum(arr1) == 0 + 1 + 2); + assertThrown!MatchException(trySum(arr2)); +} + +// Throws if requested type is impossible +version (D_Exceptions) +@safe unittest { + import std.exception: assertThrown; + + SumType!int x; + + assertThrown!MatchException(x.tryGet!string); +} + +// Non-copyable types +version (D_Exceptions) +@safe unittest { + static struct NoCopy + { + @disable this(this); + } + + SumType!NoCopy lvalue; + auto rvalue() { return SumType!NoCopy(); } + + assert(lvalue.tryGet!NoCopy == NoCopy()); + assert(rvalue.tryGet!NoCopy == NoCopy()); +} + +// Immovable types +version (D_Exceptions) +@safe unittest { + auto rvalue() { return const(SumType!string)("hello"); } + + assert(rvalue.tryGet!(const(string)) == "hello"); +} + +// Nontrivial rvalues at compile time +version (D_Exceptions) +@safe unittest { + static struct ElaborateCopy + { + this(this) {} + } + + enum rvalue = SumType!ElaborateCopy(); + enum ctResult = rvalue.tryGet!ElaborateCopy; + + assert(ctResult == ElaborateCopy()); +} + +private template failedGetMessage(Expected, Actual) +{ + static if (Expected.stringof == Actual.stringof) { + enum expectedStr = __traits(fullyQualifiedName, Expected); + enum actualStr = __traits(fullyQualifiedName, Actual); + } else { + enum expectedStr = Expected.stringof; + enum actualStr = Actual.stringof; + } + + enum failedGetMessage = + "Tried to get `" ~ expectedStr ~ "`" ~ + " but found `" ~ actualStr ~ "`"; +} + +private template getLvalue(Flag!"try_" try_, T) +{ + ref T getLvalue(Value)(ref Value value) + { + static if (is(Value == T)) { + return value; + } else { + static if (try_) + throw new MatchException(failedGetMessage!(T, Value)); + else + assert(false, failedGetMessage!(T, Value)); + } + } +} + +private template getRvalue(Flag!"try_" try_, T) +{ + T getRvalue(Value)(ref Value value) + { + static if (is(Value == T)) { + import core.lifetime: move; + + // Move if possible; otherwise fall back to copy + static if (is(typeof(move(value)))) { + static if (isCopyable!Value) + // Workaround for https://issues.dlang.org/show_bug.cgi?id=21542 + return __ctfe ? value : move(value); + else + return move(value); + } else { + return value; + } + } else { + static if (try_) + throw new MatchException(failedGetMessage!(T, Value)); + else + assert(false, failedGetMessage!(T, Value)); + } + } +} + private void destroyIfOwner(T)(ref T value) { static if (hasElaborateDestructor!T) {