From 95c2919d2bfd034e1df179081a0f4cee44374650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Fri, 22 Feb 2019 00:28:15 +0100 Subject: [PATCH 01/10] Introduce TaggedUnion as the low-level basis for TaggedAlgebraic. TaggedUnion has a number of convenience features compared to TaggedAlgebraic that are possible because of the missing dynamic dispatch functionality. If the latter is not required, TaggedUnion provides a much less complex and more robust way to store a fixed set of types/kinds. --- source/taggedalgebraic.d | 494 +++++++++++++++++++++++++++------------ 1 file changed, 340 insertions(+), 154 deletions(-) diff --git a/source/taggedalgebraic.d b/source/taggedalgebraic.d index 82447ad..a981431 100644 --- a/source/taggedalgebraic.d +++ b/source/taggedalgebraic.d @@ -1,12 +1,13 @@ /** - * Algebraic data type implementation based on a tagged union. + * Generic tagged union and algebraic data type implementations. * - * Copyright: Copyright 2015-2016, Sönke Ludwig. + * Copyright: Copyright 2015-2019, Sönke Ludwig. * License: $(WEB www.boost.org/LICENSE_1_0.txt, Boost License 1.0). * Authors: Sönke Ludwig */ module taggedalgebraic; +import std.algorithm.mutation : move, swap; import std.typetuple; import std.traits : Unqual, isInstanceOf; @@ -14,6 +15,301 @@ import std.traits : Unqual, isInstanceOf; // - distinguish between @property and non@-property methods. // - verify that static methods are handled properly + +/** Implements a generic tagged union type. + + This struct takes a `union` or `struct` declaration as an input and builds + an algebraic data type from its fields, using an automatically generated + `Kind` enumeration to identify which field of the union is currently used. + Multiple fields with the same value are supported. + + For each field defined by `U` a number of convenience members are generated. + For a given field "foo", these fields are: + + $(UL + $(LI `static foo(value)`) - returns a new tagged union with the specified value) + $(LI `isFoo` - equivalent to `kind == Kind.foo`) + $(LI `setFoo(value)` - equivalent to `set!(Kind.foo)(value)`) + $(LI `getFoo` - equivalent to `get!(Kind.foo)`) + ) +*/ +struct TaggedUnion(U) if (is(U == union) || is(U == struct)) +{ + import std.traits : FieldTypeTuple, FieldNameTuple, Largest, + hasElaborateCopyConstructor, hasElaborateDestructor, isCopyable; + import std.ascii : toUpper; + + alias FieldTypes = FieldTypeTuple!U; + alias fieldNames = FieldNameTuple!U; + + static assert(FieldTypes.length > 0, "The TaggedUnions's union type must have at least one field."); + static assert(FieldTypes.length == fieldNames.length); + + /// A type enum that identifies the type of value currently stored. + alias Kind = TypeEnum!U; + + private { + static if (isUnionType!(FieldTypes[0]) || __VERSION__ < 2072) { + void[Largest!FieldTypes.sizeof] m_data; + } else { + union Dummy { + FieldTypes[0] initField; + void[Largest!FieldTypes.sizeof] data; + alias data this; + } + Dummy m_data = { initField: FieldTypes[0].init }; + } + Kind m_kind; + } + + this(TaggedUnion other) + { + rawSwap(this, other); + } + + void opAssign(TaggedUnion other) + { + rawSwap(this, other); + } + + // disable default construction if first type is not a null/Void type + static if (!isUnionType!(FieldTypes[0]) && __VERSION__ < 2072) { + @disable this(); + } + + // postblit constructor + static if (!allSatisfy!(isCopyable, FieldTypes)) { + @disable this(this); + } else static if (anySatisfy!(hasElaborateCopyConstructor, FieldTypes)) { + this(this) + { + switch (m_kind) { + default: break; + foreach (i, tname; fieldNames) { + alias T = FieldTypes[i]; + static if (hasElaborateCopyConstructor!T) + { + case __traits(getMember, Kind, tname): + typeid(T).postblit(cast(void*)&trustedGet!T()); + return; + } + } + } + } + } + + // destructor + static if (anySatisfy!(hasElaborateDestructor, FieldTypes)) { + ~this() + { + final switch (m_kind) { + foreach (i, tname; fieldNames) { + alias T = FieldTypes[i]; + case __traits(getMember, Kind, tname): + static if (hasElaborateDestructor!T) { + .destroy(trustedGet!T); + } + return; + } + } + } + } + + /// Enables conversion or extraction of the stored value. + T opCast(T)() + { + import std.conv : to; + + final switch (m_kind) { + foreach (i, FT; FieldTypes) { + case __traits(getMember, Kind, fieldNames[i]): + static if (is(typeof(trustedGet!FT) : T)) + return trustedGet!FT; + else static if (is(typeof(to!T(trustedGet!FT)))) { + return to!T(trustedGet!FT); + } else { + assert(false, "Cannot cast a " ~ fieldNames[i] + ~ " value of type " ~ FT.stringof ~ " to " ~ T.stringof); + } + } + } + assert(false); // never reached + } + /// ditto + T opCast(T)() const + { + // this method needs to be duplicated because inout doesn't work with to!() + import std.conv : to; + + final switch (m_kind) { + foreach (i, FT; FieldTypes) { + case __traits(getMember, Kind, fieldNames[i]): + static if (is(typeof(trustedGet!FT) : T)) + return trustedGet!FT; + else static if (is(typeof(to!T(trustedGet!FT)))) { + return to!T(trustedGet!FT); + } else { + assert(false, "Cannot cast a " ~ fieldNames[i] + ~ " value of type" ~ FT.stringof ~ " to " ~ T.stringof); + } + } + } + assert(false); // never reached + } + + /// Enables equality comparison with the stored value. + bool opEquals()(auto ref inout(TaggedUnion) other) + inout { + if (this.kind != other.kind) return false; + + final switch (this.kind) { + foreach (i, fname; TaggedUnion!U.fieldNames) + case __traits(getMember, Kind, fname): + return trustedGet!(FieldTypes[i]) == other.trustedGet!(FieldTypes[i]); + } + assert(false); // never reached + } + + /// The type ID of the currently stored value. + @property Kind kind() const { return m_kind; } + + static foreach (i, name; fieldNames) { + // NOTE: using getX/setX here because using just x would be prone to + // misuse (attempting to "get" a value for modification when + // a different kind is set instead of assigning a new value) + mixin("alias get"~pascalCase(name)~" = get!(Kind."~name~");"); + mixin("alias set"~pascalCase(name)~" = set!(Kind."~name~");"); + mixin("@property bool is"~pascalCase(name)~"() const { return m_kind == Kind."~name~"; }"); + + static if (!isUnionType!(FieldTypes[i])) { + mixin("static TaggedUnion "~name~"(FieldTypes["~i.stringof~"] value)" + ~ "{ TaggedUnion tu; tu.set!(Kind."~name~")(move(value)); return tu; }"); + + // TODO: define assignment operator for unique types + } else { + mixin("static @property TaggedUnion "~name~"() { TaggedUnion tu; tu.set!(Kind."~name~"); return tu; }"); + } + + } + + ref inout(FieldTypes[kind]) get(Kind kind)() + inout { + if (this.kind != kind) { + enum msg(.string k_is) = "Attempt to get kind "~kind.stringof~" from tagged union with kind "~k_is; + final switch (this.kind) { + static foreach (i, n; fieldNames) + case __traits(getMember, Kind, n): + assert(false, msg!n); + } + } + //return trustedGet!(FieldTypes[kind]); + return *() @trusted { return cast(const(FieldTypes[kind])*)m_data.ptr; } (); + } + + + ref inout(T) get(T)() inout + if (staticIndexOf!(T, FieldTypes) >= 0) + { + final switch (this.kind) { + static foreach (n; fieldNames) { + case __traits(getMember, Kind, n): + static if (is(FieldTypes[__traits(getMember, Kind, n)] == T)) + return trustedGet!T; + else assert(false, "Attempting to get type "~T.stringof + ~ " from a TaggedUnion with type " + ~ FieldTypes[__traits(getMember, Kind, n)].stringof); + } + } + } + + ref FieldTypes[kind] set(Kind kind)(FieldTypes[kind] value) + if (!isUnionType!(FieldTypes[kind])) + { + if (m_kind != kind) { + destroy(this); + m_data.rawEmplace(value); + } else { + rawSwap(trustedGet!(FieldTypes[kind]), value); + } + m_kind = kind; + + return trustedGet!(FieldTypes[kind]); + } + + void set(Kind kind)() + if (isUnionType!(FieldTypes[kind])) + { + if (m_kind != kind) { + destroy(this); + } + m_kind = kind; + } + + private @trusted @property ref inout(T) trustedGet(T)() inout { return *cast(inout(T)*)m_data.ptr; } +} + +/// +@safe nothrow unittest { + union Kinds { + int count; + string text; + } + alias TU = TaggedUnion!Kinds; + + // default initialized to the first field defined + TU tu; + assert(tu.kind == TU.Kind.count); + assert(tu.isCount); // qequivalent to the line above + assert(!tu.isText); + assert(tu.get!(TU.Kind.count) == int.init); + + // set to a specific count + tu.setCount(42); + assert(tu.isCount); + assert(tu.getCount() == 42); + assert(tu.get!(TU.Kind.count) == 42); + assert(tu.get!int == 42); // can also get by type + assert(tu.getCount() == 42); + + // assign a new tagged algebraic value + tu = TU.count(43); + + // test equivalence with other tagged unions + assert(tu == TU.count(43)); + assert(tu != TU.count(42)); + assert(tu != TU.text("hello")); + + // modify by reference + tu.getCount()++; + assert(tu.getCount() == 44); + + // set the second field + tu.setText("hello"); + assert(!tu.isCount); + assert(tu.isText); + assert(tu.kind == TU.Kind.text); + assert(tu.getText() == "hello"); +} + +unittest { // test for name clashes + union U { .string string; } + alias TU = TaggedUnion!U; + TU tu; + tu = TU.string("foo"); + assert(tu.isString); + assert(tu.getString() == "foo"); +} + +enum isUnionType(T) = is(T == Void) || is(T == void) || is(T == typeof(null)); + +private string pascalCase(string camel_case) +{ + if (!__ctfe) assert(false); + import std.ascii : toUpper; + return camel_case[0].toUpper ~ camel_case[1 .. $]; +} + + /** Implements a generic algebraic type using an enum to identify the stored type. This struct takes a `union` or `struct` declaration as an input and builds @@ -43,40 +339,20 @@ struct TaggedAlgebraic(U) if (is(U == union) || is(U == struct)) { import std.algorithm : among; import std.string : format; - import std.traits : FieldTypeTuple, FieldNameTuple, Largest, hasElaborateCopyConstructor, hasElaborateDestructor; /// Alias of the type used for defining the possible storage types/kinds. alias Union = U; - private alias FieldTypes = FieldTypeTuple!U; - private alias fieldNames = FieldNameTuple!U; - - static assert(FieldTypes.length > 0, "The TaggedAlgebraic's union type must have at least one field."); - static assert(FieldTypes.length == fieldNames.length); - - - private { - static if (is(FieldTypes[0] == typeof(null)) || is(FieldTypes[0] == Void) || __VERSION__ < 2072) { - void[Largest!FieldTypes.sizeof] m_data; - } else { - union Dummy { - FieldTypes[0] initField; - void[Largest!FieldTypes.sizeof] data; - alias data this; - } - Dummy m_data = { initField: FieldTypes[0].init }; - } - Kind m_kind; - } + private TaggedUnion!U m_union; /// A type enum that identifies the type of value currently stored. - alias Kind = TypeEnum!U; + alias Kind = TaggedUnion!U.Kind; /// Compatibility alias deprecated("Use 'Kind' instead.") alias Type = Kind; /// The type ID of the currently stored value. - @property Kind kind() const { return m_kind; } + @property Kind kind() const { return m_union.kind; } // Compatibility alias deprecated("Use 'kind' instead.") @@ -96,85 +372,10 @@ struct TaggedAlgebraic(U) if (is(U == union) || is(U == struct)) rawSwap(this, other); } - // postblit constructor - static if (anySatisfy!(hasElaborateCopyConstructor, FieldTypes)) - { - this(this) - { - switch (m_kind) { - default: break; - foreach (i, tname; fieldNames) { - alias T = typeof(__traits(getMember, U, tname)); - static if (hasElaborateCopyConstructor!T) - { - case __traits(getMember, Kind, tname): - typeid(T).postblit(cast(void*)&trustedGet!tname()); - return; - } - } - } - } - } - - // destructor - static if (anySatisfy!(hasElaborateDestructor, FieldTypes)) - { - ~this() - { - final switch (m_kind) { - foreach (i, tname; fieldNames) { - alias T = typeof(__traits(getMember, U, tname)); - case __traits(getMember, Kind, tname): - static if (hasElaborateDestructor!T) { - .destroy(trustedGet!tname); - } - return; - } - } - } - } - /// Enables conversion or extraction of the stored value. - T opCast(T)() - { - import std.conv : to; - - final switch (m_kind) { - foreach (i, FT; FieldTypes) { - case __traits(getMember, Kind, fieldNames[i]): - static if (is(typeof(trustedGet!(fieldNames[i])) : T)) - return trustedGet!(fieldNames[i]); - else static if (is(typeof(to!T(trustedGet!(fieldNames[i]))))) { - return to!T(trustedGet!(fieldNames[i])); - } else { - assert(false, "Cannot cast a " ~ fieldNames[i] - ~ " value of type " ~ FT.stringof ~ " to " ~ T.stringof); - } - } - } - assert(false); // never reached - } + T opCast(T)() { return cast(T)m_union; } /// ditto - T opCast(T)() const - { - // this method needs to be duplicated because inout doesn't work with to!() - import std.conv : to; - - final switch (m_kind) { - foreach (i, FT; FieldTypes) { - case __traits(getMember, Kind, fieldNames[i]): - static if (is(typeof(trustedGet!(fieldNames[i])) : T)) - return trustedGet!(fieldNames[i]); - else static if (is(typeof(to!T(trustedGet!(fieldNames[i]))))) { - return to!T(trustedGet!(fieldNames[i])); - } else { - assert(false, "Cannot cast a " ~ fieldNames[i] - ~ " value of type" ~ FT.stringof ~ " to " ~ T.stringof); - } - } - } - assert(false); // never reached - } + T opCast(T)() const { return cast(T)m_union; } /// Uses `cast(string)`/`to!string` to return a string representation of the enclosed value. string toString() const { return cast(string)this; } @@ -193,12 +394,7 @@ struct TaggedAlgebraic(U) if (is(U == union) || is(U == struct)) if (is(Unqual!T == TaggedAlgebraic) || hasOp!(TA, OpKind.binary, "==", T)) { static if (is(Unqual!T == TaggedAlgebraic)) { - if (this.kind != other.kind) return false; - final switch (this.kind) - foreach (i, fname; fieldNames) - case __traits(getMember, Kind, fname): - return trustedGet!fname == other.trustedGet!fname; - assert(false); // never reached + return m_union == other.m_union; } else return implementOp!(OpKind.binary, "==")(this, other); } /// Enables relational comparisons with the stored value. @@ -219,9 +415,6 @@ struct TaggedAlgebraic(U) if (is(U == union) || is(U == struct)) auto opIndexAssign(this TA, ARGS...)(auto ref ARGS args) if (hasOp!(TA, OpKind.indexAssign, null, ARGS)) { return implementOp!(OpKind.indexAssign, null)(this, args); } /// Enables call syntax operations on the stored value. auto opCall(this TA, ARGS...)(auto ref ARGS args) if (hasOp!(TA, OpKind.call, null, ARGS)) { return implementOp!(OpKind.call, null)(this, args); } - - private @trusted @property ref inout(typeof(__traits(getMember, U, f))) trustedGet(string f)() inout { return trustedGet!(inout(typeof(__traits(getMember, U, f)))); } - private @trusted @property ref inout(T) trustedGet(T)() inout { return *cast(inout(T)*)m_data.ptr; } } /// @@ -688,7 +881,7 @@ unittest { // issue #13 */ bool hasType(T, U)(in ref TaggedAlgebraic!U ta) { - alias Fields = Filter!(fieldMatchesType!(U, T), ta.fieldNames); + alias Fields = Filter!(fieldMatchesType!(U, T), ta.m_union.fieldNames); static assert(Fields.length > 0, "Type "~T.stringof~" cannot be stored in a "~(TaggedAlgebraic!U).stringof~"."); switch (ta.kind) { @@ -779,16 +972,12 @@ static if (__VERSION__ >= 2072) { */ ref inout(T) get(T, U)(ref inout(TaggedAlgebraic!U) ta) { - import std.format : format; - assert(hasType!(T, U)(ta), "Type mismatch!"); - return ta.trustedGet!T; + return ta.m_union.get!T; } /// ditto inout(T) get(T, U)(inout(TaggedAlgebraic!U) ta) { - import std.format : format; - assert(hasType!(T, U)(ta), "Type mismatch!"); - return ta.trustedGet!T; + return ta.m_union.get!T; } @nogc @safe nothrow unittest { @@ -820,9 +1009,9 @@ auto apply(alias handler, TA)(TA ta) if (isInstanceOf!(TaggedAlgebraic, TA)) { final switch (ta.kind) { - foreach (i, fn; TA.fieldNames) { + foreach (i, fn; TA.m_union.fieldNames) { case __traits(getMember, ta.Kind, fn): - return handler(get!(TA.FieldTypes[i])(ta)); + return handler(get!(TA.m_union.FieldTypes[i])(ta)); } } static if (__VERSION__ <= 2068) assert(false); @@ -956,26 +1145,26 @@ private static auto implementOp(OpKind kind, string name, T, ARGS...)(ref T self //pragma(msg, typeof(T.Union.tupleof)); //import std.meta : staticMap; pragma(msg, staticMap!(isMatchingUniqueType!(T.Union), info.ReturnTypes)); - switch (self.m_kind) { + switch (self.kind) { enum assert_msg = "Operator "~name~" ("~kind.stringof~") can only be used on values of the following types: "~[info.fields].join(", "); default: assert(false, assert_msg); foreach (i, f; info.fields) { alias FT = typeof(__traits(getMember, T.Union, f)); case __traits(getMember, T.Kind, f): static if (NoDuplicates!(info.ReturnTypes).length == 1) - return info.perform(self.trustedGet!FT, args); + return info.perform(self.m_union.trustedGet!FT, args); else static if (allSatisfy!(isMatchingUniqueType!(T.Union), info.ReturnTypes)) - return TaggedAlgebraic!(T.Union)(info.perform(self.trustedGet!FT, args)); + return TaggedAlgebraic!(T.Union)(info.perform(self.m_union.trustedGet!FT, args)); else static if (allSatisfy!(isNoVariant, info.ReturnTypes)) { alias Alg = Algebraic!(NoDuplicates!(info.ReturnTypes)); - info.ReturnTypes[i] ret = info.perform(self.trustedGet!FT, args); + info.ReturnTypes[i] ret = info.perform(self.m_union.trustedGet!FT, args); import std.traits : isInstanceOf; return Alg(ret); } else static if (is(FT == Variant)) - return info.perform(self.trustedGet!FT, args); + return info.perform(self.m_union.trustedGet!FT, args); else - return Variant(info.perform(self.trustedGet!FT, args)); + return Variant(info.perform(self.m_union.trustedGet!FT, args)); } } @@ -1168,49 +1357,46 @@ private string generateConstructors(U)() string ret; - static if (__VERSION__ < 2072) { - // disable default construction if first type is not a null/Void type - static if (!is(FieldTypeTuple!U[0] == typeof(null)) && !is(FieldTypeTuple!U[0] == Void)) - { - ret ~= q{ - @disable this(); - }; - } - } // normal type constructors foreach (tname; UniqueTypeFields!U) ret ~= q{ - this(typeof(U.%s) value) + this(typeof(U.%1$s) value) { - m_data.rawEmplace(value); - m_kind = Kind.%s; + static if (isUnionType!(typeof(U.%1$s))) + m_union.set!(Kind.%1$s)(); + else + m_union.set!(Kind.%1$s)(value); } - void opAssign(typeof(U.%s) value) + void opAssign(typeof(U.%1$s) value) { - if (m_kind != Kind.%s) { - // NOTE: destroy(this) doesn't work for some opDispatch-related reason - static if (is(typeof(&this.__xdtor))) - this.__xdtor(); - m_data.rawEmplace(value); - } else { - trustedGet!"%s" = value; - } - m_kind = Kind.%s; + static if (isUnionType!(typeof(U.%1$s))) + m_union.set!(Kind.%1$s)(); + else + m_union.set!(Kind.%1$s)(value); } - }.format(tname, tname, tname, tname, tname, tname); + }.format(tname); // type constructors with explicit type tag foreach (tname; TypeTuple!(UniqueTypeFields!U, AmbiguousTypeFields!U)) ret ~= q{ - this(typeof(U.%s) value, Kind type) + this(typeof(U.%1$s) value, Kind type) { - assert(type.among!(%s), format("Invalid type ID for type %%s: %%s", typeof(U.%s).stringof, type)); - m_data.rawEmplace(value); - m_kind = type; + switch (type) { + default: assert(false, format("Invalid type ID for type %%s: %%s", typeof(U.%1$s).stringof, type)); + foreach (i, n; TaggedUnion!U.fieldNames) { + static if (is(typeof(U.%1$s) == typeof(__traits(getMember, U, n)))) { + case __traits(getMember, Kind, n): + static if (isUnionType!(m_union.FieldTypes[i])) + m_union.set!(__traits(getMember, Kind, n))(); + else m_union.set!(__traits(getMember, Kind, n))(value); + return; + } + } + } } - }.format(tname, [SameTypeFields!(U, tname)].map!(f => "Kind."~f).join(", "), tname); + }.format(tname); return ret; } From 78ca6e0cb557af086dc2e878a697f8be90f67aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Fri, 22 Feb 2019 00:40:04 +0100 Subject: [PATCH 02/10] Reduce reliance on typeof(U.field) to prepare for supporting U==enum. --- source/taggedalgebraic.d | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/source/taggedalgebraic.d b/source/taggedalgebraic.d index a981431..ee0f4f0 100644 --- a/source/taggedalgebraic.d +++ b/source/taggedalgebraic.d @@ -39,6 +39,7 @@ struct TaggedUnion(U) if (is(U == union) || is(U == struct)) hasElaborateCopyConstructor, hasElaborateDestructor, isCopyable; import std.ascii : toUpper; + alias FieldDefinitionType = U; alias FieldTypes = FieldTypeTuple!U; alias fieldNames = FieldNameTuple!U; @@ -48,6 +49,8 @@ struct TaggedUnion(U) if (is(U == union) || is(U == struct)) /// A type enum that identifies the type of value currently stored. alias Kind = TypeEnum!U; + private alias FieldTypeByName(string name) = FieldTypes[__traits(getMember, Kind, name)]; + private { static if (isUnionType!(FieldTypes[0]) || __VERSION__ < 2072) { void[Largest!FieldTypes.sizeof] m_data; @@ -341,12 +344,15 @@ struct TaggedAlgebraic(U) if (is(U == union) || is(U == struct)) import std.string : format; /// Alias of the type used for defining the possible storage types/kinds. - alias Union = U; + deprecated alias Union = U; + + /// The underlying tagged union type + alias UnionType = TaggedUnion!U; private TaggedUnion!U m_union; /// A type enum that identifies the type of value currently stored. - alias Kind = TaggedUnion!U.Kind; + alias Kind = UnionType.Kind; /// Compatibility alias deprecated("Use 'Kind' instead.") alias Type = Kind; @@ -1149,7 +1155,7 @@ private static auto implementOp(OpKind kind, string name, T, ARGS...)(ref T self enum assert_msg = "Operator "~name~" ("~kind.stringof~") can only be used on values of the following types: "~[info.fields].join(", "); default: assert(false, assert_msg); foreach (i, f; info.fields) { - alias FT = typeof(__traits(getMember, T.Union, f)); + alias FT = T.UnionType.FieldTypeByName!f; case __traits(getMember, T.Kind, f): static if (NoDuplicates!(info.ReturnTypes).length == 1) return info.perform(self.m_union.trustedGet!FT, args); @@ -1361,17 +1367,17 @@ private string generateConstructors(U)() // normal type constructors foreach (tname; UniqueTypeFields!U) ret ~= q{ - this(typeof(U.%1$s) value) + this(UnionType.FieldTypeByName!"%1$s" value) { - static if (isUnionType!(typeof(U.%1$s))) + static if (isUnionType!(UnionType.FieldTypeByName!"%1$s")) m_union.set!(Kind.%1$s)(); else m_union.set!(Kind.%1$s)(value); } - void opAssign(typeof(U.%1$s) value) + void opAssign(UnionType.FieldTypeByName!"%1$s" value) { - static if (isUnionType!(typeof(U.%1$s))) + static if (isUnionType!(UnionType.FieldTypeByName!"%1$s")) m_union.set!(Kind.%1$s)(); else m_union.set!(Kind.%1$s)(value); @@ -1381,14 +1387,14 @@ private string generateConstructors(U)() // type constructors with explicit type tag foreach (tname; TypeTuple!(UniqueTypeFields!U, AmbiguousTypeFields!U)) ret ~= q{ - this(typeof(U.%1$s) value, Kind type) + this(UnionType.FieldTypeByName!"%1$s" value, Kind type) { switch (type) { - default: assert(false, format("Invalid type ID for type %%s: %%s", typeof(U.%1$s).stringof, type)); + default: assert(false, format("Invalid type ID for type %%s: %%s", UnionType.FieldTypeByName!"%1$s".stringof, type)); foreach (i, n; TaggedUnion!U.fieldNames) { - static if (is(typeof(U.%1$s) == typeof(__traits(getMember, U, n)))) { + static if (is(UnionType.FieldTypeByName!"%1$s" == UnionType.FieldTypes[i])) { case __traits(getMember, Kind, n): - static if (isUnionType!(m_union.FieldTypes[i])) + static if (isUnionType!(UnionType.FieldTypes[i])) m_union.set!(__traits(getMember, Kind, n))(); else m_union.set!(__traits(getMember, Kind, n))(value); return; From 39ebd11570248db94793db31f24b3ba899382dcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Fri, 22 Feb 2019 01:06:52 +0100 Subject: [PATCH 03/10] Use AliasSeq instead of TypeTuple. --- source/taggedalgebraic.d | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/source/taggedalgebraic.d b/source/taggedalgebraic.d index ee0f4f0..13d7960 100644 --- a/source/taggedalgebraic.d +++ b/source/taggedalgebraic.d @@ -8,7 +8,7 @@ module taggedalgebraic; import std.algorithm.mutation : move, swap; -import std.typetuple; +import std.meta; import std.traits : Unqual, isInstanceOf; // TODO: @@ -1077,7 +1077,7 @@ private template hasOp(TA, OpKind kind, string name, ARGS...) { import std.traits : CopyTypeQualifiers; alias UQ = CopyTypeQualifiers!(TA, TA.Union); - enum hasOp = TypeTuple!(OpInfo!(UQ, kind, name, ARGS).fields).length > 0; + enum hasOp = AliasSeq!(OpInfo!(UQ, kind, name, ARGS).fields).length > 0; } unittest { @@ -1252,9 +1252,9 @@ private auto performOp(U, OpKind kind, string name, T, ARGS...)(ref T value, /*a static if (i < TA.FieldTypes.length) { alias FT = TA.FieldTypes[i]; static if (is(typeof(&performOpRaw!(U, kind, name, T, FT, ARGS[1 .. $])))) - alias MTypesImpl = TypeTuple!(FT, MTypesImpl!(i+1)); - else alias MTypesImpl = TypeTuple!(MTypesImpl!(i+1)); - } else alias MTypesImpl = TypeTuple!(); + alias MTypesImpl = AliasSeq!(FT, MTypesImpl!(i+1)); + else alias MTypesImpl = AliasSeq!(MTypesImpl!(i+1)); + } else alias MTypesImpl = AliasSeq!(); } alias MTypes = NoDuplicates!(MTypesImpl!0); static assert(MTypes.length > 0, "No type of the TaggedAlgebraic parameter matches any function declaration."); @@ -1292,7 +1292,7 @@ private template OpInfo(U, OpKind kind, string name, ARGS...) private template isOpEnabled(string field) { - alias attribs = TypeTuple!(__traits(getAttributes, __traits(getMember, U, field))); + alias attribs = AliasSeq!(__traits(getAttributes, __traits(getMember, U, field))); template impl(size_t i) { static if (i < attribs.length) { static if (is(typeof(attribs[i]) == DisableOpAttribute)) { @@ -1309,17 +1309,17 @@ private template OpInfo(U, OpKind kind, string name, ARGS...) { static if (i < FieldTypes.length) { static if (isOpEnabled!(fieldNames[i]) && is(typeof(&performOp!(U, kind, name, FieldTypes[i], ARGS)))) { - alias fieldsImpl = TypeTuple!(fieldNames[i], fieldsImpl!(i+1)); + alias fieldsImpl = AliasSeq!(fieldNames[i], fieldsImpl!(i+1)); } else alias fieldsImpl = fieldsImpl!(i+1); - } else alias fieldsImpl = TypeTuple!(); + } else alias fieldsImpl = AliasSeq!(); } alias fields = fieldsImpl!0; template ReturnTypesImpl(size_t i) { static if (i < fields.length) { alias FT = CopyTypeQualifiers!(U, typeof(__traits(getMember, U, fields[i]))); - alias ReturnTypesImpl = TypeTuple!(ReturnType!(performOp!(U, kind, name, FT, ARGS)), ReturnTypesImpl!(i+1)); - } else alias ReturnTypesImpl = TypeTuple!(); + alias ReturnTypesImpl = AliasSeq!(ReturnType!(performOp!(U, kind, name, FT, ARGS)), ReturnTypesImpl!(i+1)); + } else alias ReturnTypesImpl = AliasSeq!(); } alias ReturnTypes = ReturnTypesImpl!0; @@ -1385,7 +1385,7 @@ private string generateConstructors(U)() }.format(tname); // type constructors with explicit type tag - foreach (tname; TypeTuple!(UniqueTypeFields!U, AmbiguousTypeFields!U)) + foreach (tname; AliasSeq!(UniqueTypeFields!U, AmbiguousTypeFields!U)) ret ~= q{ this(UnionType.FieldTypeByName!"%1$s" value, Kind type) { @@ -1417,9 +1417,9 @@ private template UniqueTypeFields(U) { enum name = FieldNameTuple!U[i]; alias T = Types[i]; static if (staticIndexOf!(T, Types) == i && staticIndexOf!(T, Types[i+1 .. $]) < 0) - alias impl = TypeTuple!(name, impl!(i+1)); - else alias impl = TypeTuple!(impl!(i+1)); - } else alias impl = TypeTuple!(); + alias impl = AliasSeq!(name, impl!(i+1)); + else alias impl = AliasSeq!(impl!(i+1)); + } else alias impl = AliasSeq!(); } alias UniqueTypeFields = impl!0; } @@ -1434,9 +1434,9 @@ private template AmbiguousTypeFields(U) { enum name = FieldNameTuple!U[i]; alias T = Types[i]; static if (staticIndexOf!(T, Types) == i && staticIndexOf!(T, Types[i+1 .. $]) >= 0) - alias impl = TypeTuple!(name, impl!(i+1)); + alias impl = AliasSeq!(name, impl!(i+1)); else alias impl = impl!(i+1); - } else alias impl = TypeTuple!(); + } else alias impl = AliasSeq!(); } alias AmbiguousTypeFields = impl!0; } @@ -1462,9 +1462,9 @@ private template SameTypeFields(U, string field) { static if (i < Types.length) { enum name = FieldNameTuple!U[i]; static if (is(Types[i] == T)) - alias impl = TypeTuple!(name, impl!(i+1)); - else alias impl = TypeTuple!(impl!(i+1)); - } else alias impl = TypeTuple!(); + alias impl = AliasSeq!(name, impl!(i+1)); + else alias impl = AliasSeq!(impl!(i+1)); + } else alias impl = AliasSeq!(); } alias SameTypeFields = impl!0; } From f7a03cdb02bf72ea2545d91f14038039f161d46e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Fri, 22 Feb 2019 01:07:40 +0100 Subject: [PATCH 04/10] Add enum support to TaggedUnion. Allows to define the accepted types using an annotated enum instead of a struct/union. --- source/taggedalgebraic.d | 78 ++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/source/taggedalgebraic.d b/source/taggedalgebraic.d index 13d7960..ab41610 100644 --- a/source/taggedalgebraic.d +++ b/source/taggedalgebraic.d @@ -9,7 +9,7 @@ module taggedalgebraic; import std.algorithm.mutation : move, swap; import std.meta; -import std.traits : Unqual, isInstanceOf; +import std.traits : EnumMembers, FieldNameTuple, Unqual, isInstanceOf; // TODO: // - distinguish between @property and non@-property methods. @@ -33,22 +33,23 @@ import std.traits : Unqual, isInstanceOf; $(LI `getFoo` - equivalent to `get!(Kind.foo)`) ) */ -struct TaggedUnion(U) if (is(U == union) || is(U == struct)) +struct TaggedUnion(U) if (is(U == union) || is(U == struct) || is(U == enum)) { import std.traits : FieldTypeTuple, FieldNameTuple, Largest, hasElaborateCopyConstructor, hasElaborateDestructor, isCopyable; import std.ascii : toUpper; alias FieldDefinitionType = U; - alias FieldTypes = FieldTypeTuple!U; - alias fieldNames = FieldNameTuple!U; + + /// A type enum that identifies the type of value currently stored. + alias Kind = UnionFieldEnum!U; + + alias FieldTypes = UnionKindTypes!Kind; + alias fieldNames = UnionKindNames!Kind; static assert(FieldTypes.length > 0, "The TaggedUnions's union type must have at least one field."); static assert(FieldTypes.length == fieldNames.length); - /// A type enum that identifies the type of value currently stored. - alias Kind = TypeEnum!U; - private alias FieldTypeByName(string name) = FieldTypes[__traits(getMember, Kind, name)]; private { @@ -180,11 +181,12 @@ struct TaggedUnion(U) if (is(U == union) || is(U == struct)) // NOTE: using getX/setX here because using just x would be prone to // misuse (attempting to "get" a value for modification when // a different kind is set instead of assigning a new value) - mixin("alias get"~pascalCase(name)~" = get!(Kind."~name~");"); mixin("alias set"~pascalCase(name)~" = set!(Kind."~name~");"); mixin("@property bool is"~pascalCase(name)~"() const { return m_kind == Kind."~name~"; }"); static if (!isUnionType!(FieldTypes[i])) { + mixin("alias get"~pascalCase(name)~" = get!(Kind."~name~");"); + mixin("static TaggedUnion "~name~"(FieldTypes["~i.stringof~"] value)" ~ "{ TaggedUnion tu; tu.set!(Kind."~name~")(move(value)); return tu; }"); @@ -294,6 +296,31 @@ struct TaggedUnion(U) if (is(U == union) || is(U == struct)) assert(tu.getText() == "hello"); } +/// +@safe nothrow unittest { + // Enum annotations supported since DMD 2.082.0. The mixin below is + // necessary to keep the parser happy on older versions. + static if (__VERSION__ >= 2082) { + alias myint = int; + // tagged unions can be defined in terms of an annotated enum + mixin(q{enum E { + none, + @string text + }}); + + alias TU = TaggedUnion!E; + static assert(is(TU.Kind == E)); + + TU tu; + assert(tu.isNone); + assert(tu.kind == E.none); + + tu.setText("foo"); + assert(tu.kind == E.text); + assert(tu.getText == "foo"); + } +} + unittest { // test for name clashes union U { .string string; } alias TU = TaggedUnion!U; @@ -813,8 +840,6 @@ static if (__VERSION__ >= 2072) unittest { // default initialization unittest { - import std.meta : AliasSeq; - union U { int[int] a; } foreach (TA; AliasSeq!(TaggedAlgebraic!U, const(TaggedAlgebraic!U))) @@ -951,11 +976,18 @@ static if (__VERSION__ >= 2072) { `kind` must be a value of the `TaggedAlgebraic!T.Kind` enumeration. */ template TypeOf(alias kind) - if (isInstanceOf!(TypeEnum, typeof(kind))) + if (is(typeof(kind) == enum)) { - import std.traits : FieldTypeTuple, TemplateArgsOf; - alias U = TemplateArgsOf!(typeof(kind)); - alias TypeOf = FieldTypeTuple!U[kind]; + static if (isInstanceOf!(UnionFieldEnum, typeof(kind))) { + import std.traits : FieldTypeTuple, TemplateArgsOf; + alias U = TemplateArgsOf!(typeof(kind)); + alias TypeOf = FieldTypeTuple!U[kind]; + } else { + alias Types = UnionKindTypes!(typeof(kind)); + alias uda = AliasSeq!(__traits(getAttributes, kind)); + static if (uda.length == 0) alias TypeOf = void; + else alias TypeOf = uda[0]; + } } /// @@ -1347,13 +1379,23 @@ private enum OpKind { call } -private template TypeEnum(U) +private template UnionFieldEnum(U) { - import std.array : join; - import std.traits : FieldNameTuple; - mixin("enum TypeEnum { " ~ [FieldNameTuple!U].join(", ") ~ " }"); + static if (is(U == enum)) alias UnionFieldEnum = U; + else { + import std.array : join; + import std.traits : FieldNameTuple; + mixin("enum UnionFieldEnum { " ~ [FieldNameTuple!U].join(", ") ~ " }"); + } } +deprecated alias TypeEnum(U) = UnionFieldEnum!U; + +alias UnionKindTypes(FieldEnum) = staticMap!(TypeOf, EnumMembers!FieldEnum); +alias UnionKindNames(FieldEnum) = AliasSeq!(__traits(allMembers, FieldEnum)); + + + private string generateConstructors(U)() { import std.algorithm : map; From eb8accd63c7c4e6ecc7b095a1c60a923a79ce0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Fri, 22 Feb 2019 01:12:19 +0100 Subject: [PATCH 05/10] Test on the latest compiler versions and drop support for DMD 2.074.1 and below. --- .travis.yml | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2d257b4..256dbc9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,19 +3,25 @@ language: d matrix: include: - d: dmd-beta - - d: dmd-2.079.0 + - d: dmd-2.084.1 env: [COVERAGE=true] - - d: dmd-2.071.2 + - d: dmd-2.076.1 + - d: ldc-1.14.0 + - d: ldc-1.13.0 + - d: ldc-1.12.0 + - d: ldc-1.11.0 + - d: ldc-1.10.0 + - d: ldc-1.9.0 - d: ldc-1.8.0 - d: ldc-1.7.0 - d: ldc-1.6.0 - - d: ldc-1.2.0 - - d: ldc-1.1.0 - - d: dmd-2.078.2 + - d: dmd-2.083.1 + - d: dmd-2.082.1 + - d: dmd-2.081.2 + - d: dmd-2.080.1 + - d: dmd-2.079.1 + - d: dmd-2.078.3 - d: dmd-2.077.1 - - d: dmd-2.074.1 - - d: dmd-2.073.2 - - d: dmd-2.072.2 sudo: false From 88308fcba46417869f66cf67b87a8c3f07703d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Fri, 22 Feb 2019 01:29:26 +0100 Subject: [PATCH 06/10] Split up TaggedUnion and TaggedAlgebraic into separate modules. --- source/taggedalgebraic/package.d | 3 + .../{ => taggedalgebraic}/taggedalgebraic.d | 410 +---------------- source/taggedalgebraic/taggedunion.d | 417 ++++++++++++++++++ 3 files changed, 424 insertions(+), 406 deletions(-) create mode 100644 source/taggedalgebraic/package.d rename source/{ => taggedalgebraic}/taggedalgebraic.d (74%) create mode 100644 source/taggedalgebraic/taggedunion.d diff --git a/source/taggedalgebraic/package.d b/source/taggedalgebraic/package.d new file mode 100644 index 0000000..04a26c8 --- /dev/null +++ b/source/taggedalgebraic/package.d @@ -0,0 +1,3 @@ +module taggedalgebraic; + +public import taggedalgebraic.taggedalgebraic; diff --git a/source/taggedalgebraic.d b/source/taggedalgebraic/taggedalgebraic.d similarity index 74% rename from source/taggedalgebraic.d rename to source/taggedalgebraic/taggedalgebraic.d index ab41610..c2ac462 100644 --- a/source/taggedalgebraic.d +++ b/source/taggedalgebraic/taggedalgebraic.d @@ -1,11 +1,13 @@ /** - * Generic tagged union and algebraic data type implementations. + * Algebraic data type implementation based on a tagged union. * * Copyright: Copyright 2015-2019, Sönke Ludwig. * License: $(WEB www.boost.org/LICENSE_1_0.txt, Boost License 1.0). * Authors: Sönke Ludwig */ -module taggedalgebraic; +module taggedalgebraic.taggedalgebraic; + +public import taggedalgebraic.taggedunion; import std.algorithm.mutation : move, swap; import std.meta; @@ -16,330 +18,6 @@ import std.traits : EnumMembers, FieldNameTuple, Unqual, isInstanceOf; // - verify that static methods are handled properly -/** Implements a generic tagged union type. - - This struct takes a `union` or `struct` declaration as an input and builds - an algebraic data type from its fields, using an automatically generated - `Kind` enumeration to identify which field of the union is currently used. - Multiple fields with the same value are supported. - - For each field defined by `U` a number of convenience members are generated. - For a given field "foo", these fields are: - - $(UL - $(LI `static foo(value)`) - returns a new tagged union with the specified value) - $(LI `isFoo` - equivalent to `kind == Kind.foo`) - $(LI `setFoo(value)` - equivalent to `set!(Kind.foo)(value)`) - $(LI `getFoo` - equivalent to `get!(Kind.foo)`) - ) -*/ -struct TaggedUnion(U) if (is(U == union) || is(U == struct) || is(U == enum)) -{ - import std.traits : FieldTypeTuple, FieldNameTuple, Largest, - hasElaborateCopyConstructor, hasElaborateDestructor, isCopyable; - import std.ascii : toUpper; - - alias FieldDefinitionType = U; - - /// A type enum that identifies the type of value currently stored. - alias Kind = UnionFieldEnum!U; - - alias FieldTypes = UnionKindTypes!Kind; - alias fieldNames = UnionKindNames!Kind; - - static assert(FieldTypes.length > 0, "The TaggedUnions's union type must have at least one field."); - static assert(FieldTypes.length == fieldNames.length); - - private alias FieldTypeByName(string name) = FieldTypes[__traits(getMember, Kind, name)]; - - private { - static if (isUnionType!(FieldTypes[0]) || __VERSION__ < 2072) { - void[Largest!FieldTypes.sizeof] m_data; - } else { - union Dummy { - FieldTypes[0] initField; - void[Largest!FieldTypes.sizeof] data; - alias data this; - } - Dummy m_data = { initField: FieldTypes[0].init }; - } - Kind m_kind; - } - - this(TaggedUnion other) - { - rawSwap(this, other); - } - - void opAssign(TaggedUnion other) - { - rawSwap(this, other); - } - - // disable default construction if first type is not a null/Void type - static if (!isUnionType!(FieldTypes[0]) && __VERSION__ < 2072) { - @disable this(); - } - - // postblit constructor - static if (!allSatisfy!(isCopyable, FieldTypes)) { - @disable this(this); - } else static if (anySatisfy!(hasElaborateCopyConstructor, FieldTypes)) { - this(this) - { - switch (m_kind) { - default: break; - foreach (i, tname; fieldNames) { - alias T = FieldTypes[i]; - static if (hasElaborateCopyConstructor!T) - { - case __traits(getMember, Kind, tname): - typeid(T).postblit(cast(void*)&trustedGet!T()); - return; - } - } - } - } - } - - // destructor - static if (anySatisfy!(hasElaborateDestructor, FieldTypes)) { - ~this() - { - final switch (m_kind) { - foreach (i, tname; fieldNames) { - alias T = FieldTypes[i]; - case __traits(getMember, Kind, tname): - static if (hasElaborateDestructor!T) { - .destroy(trustedGet!T); - } - return; - } - } - } - } - - /// Enables conversion or extraction of the stored value. - T opCast(T)() - { - import std.conv : to; - - final switch (m_kind) { - foreach (i, FT; FieldTypes) { - case __traits(getMember, Kind, fieldNames[i]): - static if (is(typeof(trustedGet!FT) : T)) - return trustedGet!FT; - else static if (is(typeof(to!T(trustedGet!FT)))) { - return to!T(trustedGet!FT); - } else { - assert(false, "Cannot cast a " ~ fieldNames[i] - ~ " value of type " ~ FT.stringof ~ " to " ~ T.stringof); - } - } - } - assert(false); // never reached - } - /// ditto - T opCast(T)() const - { - // this method needs to be duplicated because inout doesn't work with to!() - import std.conv : to; - - final switch (m_kind) { - foreach (i, FT; FieldTypes) { - case __traits(getMember, Kind, fieldNames[i]): - static if (is(typeof(trustedGet!FT) : T)) - return trustedGet!FT; - else static if (is(typeof(to!T(trustedGet!FT)))) { - return to!T(trustedGet!FT); - } else { - assert(false, "Cannot cast a " ~ fieldNames[i] - ~ " value of type" ~ FT.stringof ~ " to " ~ T.stringof); - } - } - } - assert(false); // never reached - } - - /// Enables equality comparison with the stored value. - bool opEquals()(auto ref inout(TaggedUnion) other) - inout { - if (this.kind != other.kind) return false; - - final switch (this.kind) { - foreach (i, fname; TaggedUnion!U.fieldNames) - case __traits(getMember, Kind, fname): - return trustedGet!(FieldTypes[i]) == other.trustedGet!(FieldTypes[i]); - } - assert(false); // never reached - } - - /// The type ID of the currently stored value. - @property Kind kind() const { return m_kind; } - - static foreach (i, name; fieldNames) { - // NOTE: using getX/setX here because using just x would be prone to - // misuse (attempting to "get" a value for modification when - // a different kind is set instead of assigning a new value) - mixin("alias set"~pascalCase(name)~" = set!(Kind."~name~");"); - mixin("@property bool is"~pascalCase(name)~"() const { return m_kind == Kind."~name~"; }"); - - static if (!isUnionType!(FieldTypes[i])) { - mixin("alias get"~pascalCase(name)~" = get!(Kind."~name~");"); - - mixin("static TaggedUnion "~name~"(FieldTypes["~i.stringof~"] value)" - ~ "{ TaggedUnion tu; tu.set!(Kind."~name~")(move(value)); return tu; }"); - - // TODO: define assignment operator for unique types - } else { - mixin("static @property TaggedUnion "~name~"() { TaggedUnion tu; tu.set!(Kind."~name~"); return tu; }"); - } - - } - - ref inout(FieldTypes[kind]) get(Kind kind)() - inout { - if (this.kind != kind) { - enum msg(.string k_is) = "Attempt to get kind "~kind.stringof~" from tagged union with kind "~k_is; - final switch (this.kind) { - static foreach (i, n; fieldNames) - case __traits(getMember, Kind, n): - assert(false, msg!n); - } - } - //return trustedGet!(FieldTypes[kind]); - return *() @trusted { return cast(const(FieldTypes[kind])*)m_data.ptr; } (); - } - - - ref inout(T) get(T)() inout - if (staticIndexOf!(T, FieldTypes) >= 0) - { - final switch (this.kind) { - static foreach (n; fieldNames) { - case __traits(getMember, Kind, n): - static if (is(FieldTypes[__traits(getMember, Kind, n)] == T)) - return trustedGet!T; - else assert(false, "Attempting to get type "~T.stringof - ~ " from a TaggedUnion with type " - ~ FieldTypes[__traits(getMember, Kind, n)].stringof); - } - } - } - - ref FieldTypes[kind] set(Kind kind)(FieldTypes[kind] value) - if (!isUnionType!(FieldTypes[kind])) - { - if (m_kind != kind) { - destroy(this); - m_data.rawEmplace(value); - } else { - rawSwap(trustedGet!(FieldTypes[kind]), value); - } - m_kind = kind; - - return trustedGet!(FieldTypes[kind]); - } - - void set(Kind kind)() - if (isUnionType!(FieldTypes[kind])) - { - if (m_kind != kind) { - destroy(this); - } - m_kind = kind; - } - - private @trusted @property ref inout(T) trustedGet(T)() inout { return *cast(inout(T)*)m_data.ptr; } -} - -/// -@safe nothrow unittest { - union Kinds { - int count; - string text; - } - alias TU = TaggedUnion!Kinds; - - // default initialized to the first field defined - TU tu; - assert(tu.kind == TU.Kind.count); - assert(tu.isCount); // qequivalent to the line above - assert(!tu.isText); - assert(tu.get!(TU.Kind.count) == int.init); - - // set to a specific count - tu.setCount(42); - assert(tu.isCount); - assert(tu.getCount() == 42); - assert(tu.get!(TU.Kind.count) == 42); - assert(tu.get!int == 42); // can also get by type - assert(tu.getCount() == 42); - - // assign a new tagged algebraic value - tu = TU.count(43); - - // test equivalence with other tagged unions - assert(tu == TU.count(43)); - assert(tu != TU.count(42)); - assert(tu != TU.text("hello")); - - // modify by reference - tu.getCount()++; - assert(tu.getCount() == 44); - - // set the second field - tu.setText("hello"); - assert(!tu.isCount); - assert(tu.isText); - assert(tu.kind == TU.Kind.text); - assert(tu.getText() == "hello"); -} - -/// -@safe nothrow unittest { - // Enum annotations supported since DMD 2.082.0. The mixin below is - // necessary to keep the parser happy on older versions. - static if (__VERSION__ >= 2082) { - alias myint = int; - // tagged unions can be defined in terms of an annotated enum - mixin(q{enum E { - none, - @string text - }}); - - alias TU = TaggedUnion!E; - static assert(is(TU.Kind == E)); - - TU tu; - assert(tu.isNone); - assert(tu.kind == E.none); - - tu.setText("foo"); - assert(tu.kind == E.text); - assert(tu.getText == "foo"); - } -} - -unittest { // test for name clashes - union U { .string string; } - alias TU = TaggedUnion!U; - TU tu; - tu = TU.string("foo"); - assert(tu.isString); - assert(tu.getString() == "foo"); -} - -enum isUnionType(T) = is(T == Void) || is(T == void) || is(T == typeof(null)); - -private string pascalCase(string camel_case) -{ - if (!__ctfe) assert(false); - import std.ascii : toUpper; - return camel_case[0].toUpper ~ camel_case[1 .. $]; -} - - /** Implements a generic algebraic type using an enum to identify the stored type. This struct takes a `union` or `struct` declaration as an input and builds @@ -970,42 +648,6 @@ unittest { } -static if (__VERSION__ >= 2072) { - /** Maps a kind enumeration value to the corresponding field type. - - `kind` must be a value of the `TaggedAlgebraic!T.Kind` enumeration. - */ - template TypeOf(alias kind) - if (is(typeof(kind) == enum)) - { - static if (isInstanceOf!(UnionFieldEnum, typeof(kind))) { - import std.traits : FieldTypeTuple, TemplateArgsOf; - alias U = TemplateArgsOf!(typeof(kind)); - alias TypeOf = FieldTypeTuple!U[kind]; - } else { - alias Types = UnionKindTypes!(typeof(kind)); - alias uda = AliasSeq!(__traits(getAttributes, kind)); - static if (uda.length == 0) alias TypeOf = void; - else alias TypeOf = uda[0]; - } - } - - /// - unittest { - static struct S { - int a; - string b; - string c; - } - alias TA = TaggedAlgebraic!S; - - static assert(is(TypeOf!(TA.Kind.a) == int)); - static assert(is(TypeOf!(TA.Kind.b) == string)); - static assert(is(TypeOf!(TA.Kind.c) == string)); - } -} - - /** Gets the value stored in an algebraic type based on its data type. */ ref inout(T) get(T, U)(ref inout(TaggedAlgebraic!U) ta) @@ -1092,10 +734,6 @@ unittest { }); } - -/// Convenience type that can be used for union fields that have no value (`void` is not allowed). -struct Void {} - /// User-defined attibute to disable `opIndex` forwarding for a particular tagged union member. @property auto disableIndex() { assert(__ctfe, "disableIndex must only be used as an attribute."); return DisableOpAttribute(OpKind.index, null); } @@ -1379,22 +1017,8 @@ private enum OpKind { call } -private template UnionFieldEnum(U) -{ - static if (is(U == enum)) alias UnionFieldEnum = U; - else { - import std.array : join; - import std.traits : FieldNameTuple; - mixin("enum UnionFieldEnum { " ~ [FieldNameTuple!U].join(", ") ~ " }"); - } -} - deprecated alias TypeEnum(U) = UnionFieldEnum!U; -alias UnionKindTypes(FieldEnum) = staticMap!(TypeOf, EnumMembers!FieldEnum); -alias UnionKindNames(FieldEnum) = AliasSeq!(__traits(allMembers, FieldEnum)); - - private string generateConstructors(U)() { @@ -1566,32 +1190,6 @@ private template isNoVariant(T) { enum isNoVariant = !is(T == Variant); } -private void rawEmplace(T)(void[] dst, ref T src) -{ - T[] tdst = () @trusted { return cast(T[])dst[0 .. T.sizeof]; } (); - static if (is(T == class)) { - tdst[0] = src; - } else { - import std.conv : emplace; - emplace!T(&tdst[0]); - tdst[0] = src; - } -} - -// std.algorithm.mutation.swap sometimes fails to compile due to -// internal errors in hasElaborateAssign!T/isAssignable!T. This is probably -// caused by cyclic dependencies. However, there is no reason to do these -// checks in this context, so we just directly move the raw memory. -private void rawSwap(T)(ref T a, ref T b) -@trusted { - void[T.sizeof] tmp = void; - void[] ab = (cast(void*)&a)[0 .. T.sizeof]; - void[] bb = (cast(void*)&b)[0 .. T.sizeof]; - tmp[] = ab[]; - ab[] = bb[]; - bb[] = tmp[]; -} - unittest { struct TU { int i; } diff --git a/source/taggedalgebraic/taggedunion.d b/source/taggedalgebraic/taggedunion.d new file mode 100644 index 0000000..e58c13c --- /dev/null +++ b/source/taggedalgebraic/taggedunion.d @@ -0,0 +1,417 @@ +/** + * Generic tagged union and algebraic data type implementations. + * + * Copyright: Copyright 2015-2019, Sönke Ludwig. + * License: $(WEB www.boost.org/LICENSE_1_0.txt, Boost License 1.0). + * Authors: Sönke Ludwig +*/ +module taggedalgebraic.taggedunion; + +import std.algorithm.mutation : move, swap; +import std.meta; +import std.traits : EnumMembers, FieldNameTuple, Unqual, isInstanceOf; + +/** Implements a generic tagged union type. + + This struct takes a `union` or `struct` declaration as an input and builds + an algebraic data type from its fields, using an automatically generated + `Kind` enumeration to identify which field of the union is currently used. + Multiple fields with the same value are supported. + + For each field defined by `U` a number of convenience members are generated. + For a given field "foo", these fields are: + + $(UL + $(LI `static foo(value)`) - returns a new tagged union with the specified value) + $(LI `isFoo` - equivalent to `kind == Kind.foo`) + $(LI `setFoo(value)` - equivalent to `set!(Kind.foo)(value)`) + $(LI `getFoo` - equivalent to `get!(Kind.foo)`) + ) +*/ +struct TaggedUnion(U) if (is(U == union) || is(U == struct) || is(U == enum)) +{ + import std.traits : FieldTypeTuple, FieldNameTuple, Largest, + hasElaborateCopyConstructor, hasElaborateDestructor, isCopyable; + import std.ascii : toUpper; + + alias FieldDefinitionType = U; + + /// A type enum that identifies the type of value currently stored. + alias Kind = UnionFieldEnum!U; + + alias FieldTypes = UnionKindTypes!Kind; + alias fieldNames = UnionKindNames!Kind; + + static assert(FieldTypes.length > 0, "The TaggedUnions's union type must have at least one field."); + static assert(FieldTypes.length == fieldNames.length); + + package alias FieldTypeByName(string name) = FieldTypes[__traits(getMember, Kind, name)]; + + private { + static if (isUnionType!(FieldTypes[0]) || __VERSION__ < 2072) { + void[Largest!FieldTypes.sizeof] m_data; + } else { + union Dummy { + FieldTypes[0] initField; + void[Largest!FieldTypes.sizeof] data; + alias data this; + } + Dummy m_data = { initField: FieldTypes[0].init }; + } + Kind m_kind; + } + + this(TaggedUnion other) + { + rawSwap(this, other); + } + + void opAssign(TaggedUnion other) + { + rawSwap(this, other); + } + + // disable default construction if first type is not a null/Void type + static if (!isUnionType!(FieldTypes[0]) && __VERSION__ < 2072) { + @disable this(); + } + + // postblit constructor + static if (!allSatisfy!(isCopyable, FieldTypes)) { + @disable this(this); + } else static if (anySatisfy!(hasElaborateCopyConstructor, FieldTypes)) { + this(this) + { + switch (m_kind) { + default: break; + foreach (i, tname; fieldNames) { + alias T = FieldTypes[i]; + static if (hasElaborateCopyConstructor!T) + { + case __traits(getMember, Kind, tname): + typeid(T).postblit(cast(void*)&trustedGet!T()); + return; + } + } + } + } + } + + // destructor + static if (anySatisfy!(hasElaborateDestructor, FieldTypes)) { + ~this() + { + final switch (m_kind) { + foreach (i, tname; fieldNames) { + alias T = FieldTypes[i]; + case __traits(getMember, Kind, tname): + static if (hasElaborateDestructor!T) { + .destroy(trustedGet!T); + } + return; + } + } + } + } + + /// Enables conversion or extraction of the stored value. + T opCast(T)() + { + import std.conv : to; + + final switch (m_kind) { + foreach (i, FT; FieldTypes) { + case __traits(getMember, Kind, fieldNames[i]): + static if (is(typeof(trustedGet!FT) : T)) + return trustedGet!FT; + else static if (is(typeof(to!T(trustedGet!FT)))) { + return to!T(trustedGet!FT); + } else { + assert(false, "Cannot cast a " ~ fieldNames[i] + ~ " value of type " ~ FT.stringof ~ " to " ~ T.stringof); + } + } + } + assert(false); // never reached + } + /// ditto + T opCast(T)() const + { + // this method needs to be duplicated because inout doesn't work with to!() + import std.conv : to; + + final switch (m_kind) { + foreach (i, FT; FieldTypes) { + case __traits(getMember, Kind, fieldNames[i]): + static if (is(typeof(trustedGet!FT) : T)) + return trustedGet!FT; + else static if (is(typeof(to!T(trustedGet!FT)))) { + return to!T(trustedGet!FT); + } else { + assert(false, "Cannot cast a " ~ fieldNames[i] + ~ " value of type" ~ FT.stringof ~ " to " ~ T.stringof); + } + } + } + assert(false); // never reached + } + + /// Enables equality comparison with the stored value. + bool opEquals()(auto ref inout(TaggedUnion) other) + inout { + if (this.kind != other.kind) return false; + + final switch (this.kind) { + foreach (i, fname; TaggedUnion!U.fieldNames) + case __traits(getMember, Kind, fname): + return trustedGet!(FieldTypes[i]) == other.trustedGet!(FieldTypes[i]); + } + assert(false); // never reached + } + + /// The type ID of the currently stored value. + @property Kind kind() const { return m_kind; } + + static foreach (i, name; fieldNames) { + // NOTE: using getX/setX here because using just x would be prone to + // misuse (attempting to "get" a value for modification when + // a different kind is set instead of assigning a new value) + mixin("alias set"~pascalCase(name)~" = set!(Kind."~name~");"); + mixin("@property bool is"~pascalCase(name)~"() const { return m_kind == Kind."~name~"; }"); + + static if (!isUnionType!(FieldTypes[i])) { + mixin("alias get"~pascalCase(name)~" = get!(Kind."~name~");"); + + mixin("static TaggedUnion "~name~"(FieldTypes["~i.stringof~"] value)" + ~ "{ TaggedUnion tu; tu.set!(Kind."~name~")(move(value)); return tu; }"); + + // TODO: define assignment operator for unique types + } else { + mixin("static @property TaggedUnion "~name~"() { TaggedUnion tu; tu.set!(Kind."~name~"); return tu; }"); + } + + } + + ref inout(FieldTypes[kind]) get(Kind kind)() + inout { + if (this.kind != kind) { + enum msg(.string k_is) = "Attempt to get kind "~kind.stringof~" from tagged union with kind "~k_is; + final switch (this.kind) { + static foreach (i, n; fieldNames) + case __traits(getMember, Kind, n): + assert(false, msg!n); + } + } + //return trustedGet!(FieldTypes[kind]); + return *() @trusted { return cast(const(FieldTypes[kind])*)m_data.ptr; } (); + } + + + ref inout(T) get(T)() inout + if (staticIndexOf!(T, FieldTypes) >= 0) + { + final switch (this.kind) { + static foreach (n; fieldNames) { + case __traits(getMember, Kind, n): + static if (is(FieldTypes[__traits(getMember, Kind, n)] == T)) + return trustedGet!T; + else assert(false, "Attempting to get type "~T.stringof + ~ " from a TaggedUnion with type " + ~ FieldTypes[__traits(getMember, Kind, n)].stringof); + } + } + } + + ref FieldTypes[kind] set(Kind kind)(FieldTypes[kind] value) + if (!isUnionType!(FieldTypes[kind])) + { + if (m_kind != kind) { + destroy(this); + m_data.rawEmplace(value); + } else { + rawSwap(trustedGet!(FieldTypes[kind]), value); + } + m_kind = kind; + + return trustedGet!(FieldTypes[kind]); + } + + void set(Kind kind)() + if (isUnionType!(FieldTypes[kind])) + { + if (m_kind != kind) { + destroy(this); + } + m_kind = kind; + } + + package @trusted @property ref inout(T) trustedGet(T)() inout { return *cast(inout(T)*)m_data.ptr; } +} + +/// +@safe nothrow unittest { + union Kinds { + int count; + string text; + } + alias TU = TaggedUnion!Kinds; + + // default initialized to the first field defined + TU tu; + assert(tu.kind == TU.Kind.count); + assert(tu.isCount); // qequivalent to the line above + assert(!tu.isText); + assert(tu.get!(TU.Kind.count) == int.init); + + // set to a specific count + tu.setCount(42); + assert(tu.isCount); + assert(tu.getCount() == 42); + assert(tu.get!(TU.Kind.count) == 42); + assert(tu.get!int == 42); // can also get by type + assert(tu.getCount() == 42); + + // assign a new tagged algebraic value + tu = TU.count(43); + + // test equivalence with other tagged unions + assert(tu == TU.count(43)); + assert(tu != TU.count(42)); + assert(tu != TU.text("hello")); + + // modify by reference + tu.getCount()++; + assert(tu.getCount() == 44); + + // set the second field + tu.setText("hello"); + assert(!tu.isCount); + assert(tu.isText); + assert(tu.kind == TU.Kind.text); + assert(tu.getText() == "hello"); +} + +/// +@safe nothrow unittest { + // Enum annotations supported since DMD 2.082.0. The mixin below is + // necessary to keep the parser happy on older versions. + static if (__VERSION__ >= 2082) { + alias myint = int; + // tagged unions can be defined in terms of an annotated enum + mixin(q{enum E { + none, + @string text + }}); + + alias TU = TaggedUnion!E; + static assert(is(TU.Kind == E)); + + TU tu; + assert(tu.isNone); + assert(tu.kind == E.none); + + tu.setText("foo"); + assert(tu.kind == E.text); + assert(tu.getText == "foo"); + } +} + +unittest { // test for name clashes + union U { .string string; } + alias TU = TaggedUnion!U; + TU tu; + tu = TU.string("foo"); + assert(tu.isString); + assert(tu.getString() == "foo"); +} + +enum isUnionType(T) = is(T == Void) || is(T == void) || is(T == typeof(null)); + +private string pascalCase(string camel_case) +{ + if (!__ctfe) assert(false); + import std.ascii : toUpper; + return camel_case[0].toUpper ~ camel_case[1 .. $]; +} + +static if (__VERSION__ >= 2072) { + /** Maps a kind enumeration value to the corresponding field type. + + `kind` must be a value of the `TaggedAlgebraic!T.Kind` enumeration. + */ + template TypeOf(alias kind) + if (is(typeof(kind) == enum)) + { + static if (isInstanceOf!(UnionFieldEnum, typeof(kind))) { + import std.traits : FieldTypeTuple, TemplateArgsOf; + alias U = TemplateArgsOf!(typeof(kind)); + alias TypeOf = FieldTypeTuple!U[kind]; + } else { + alias Types = UnionKindTypes!(typeof(kind)); + alias uda = AliasSeq!(__traits(getAttributes, kind)); + static if (uda.length == 0) alias TypeOf = void; + else alias TypeOf = uda[0]; + } + } + + /// + unittest { + static struct S { + int a; + string b; + string c; + } + alias TU = TaggedUnion!S; + + static assert(is(TypeOf!(TU.Kind.a) == int)); + static assert(is(TypeOf!(TU.Kind.b) == string)); + static assert(is(TypeOf!(TU.Kind.c) == string)); + } +} + + +/// Convenience type that can be used for union fields that have no value (`void` is not allowed). +struct Void {} + +private template UnionFieldEnum(U) +{ + static if (is(U == enum)) alias UnionFieldEnum = U; + else { + import std.array : join; + import std.traits : FieldNameTuple; + mixin("enum UnionFieldEnum { " ~ [FieldNameTuple!U].join(", ") ~ " }"); + } +} + +deprecated alias TypeEnum(U) = UnionFieldEnum!U; + +private alias UnionKindTypes(FieldEnum) = staticMap!(TypeOf, EnumMembers!FieldEnum); +private alias UnionKindNames(FieldEnum) = AliasSeq!(__traits(allMembers, FieldEnum)); + + + +package void rawEmplace(T)(void[] dst, ref T src) +{ + T[] tdst = () @trusted { return cast(T[])dst[0 .. T.sizeof]; } (); + static if (is(T == class)) { + tdst[0] = src; + } else { + import std.conv : emplace; + emplace!T(&tdst[0]); + tdst[0] = src; + } +} + +// std.algorithm.mutation.swap sometimes fails to compile due to +// internal errors in hasElaborateAssign!T/isAssignable!T. This is probably +// caused by cyclic dependencies. However, there is no reason to do these +// checks in this context, so we just directly move the raw memory. +package void rawSwap(T)(ref T a, ref T b) +@trusted { + void[T.sizeof] tmp = void; + void[] ab = (cast(void*)&a)[0 .. T.sizeof]; + void[] bb = (cast(void*)&b)[0 .. T.sizeof]; + tmp[] = ab[]; + ab[] = bb[]; + bb[] = tmp[]; +} From 194e0dca410e7c7abddb7880b4d881ad97676da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Fri, 22 Feb 2019 02:28:43 +0100 Subject: [PATCH 07/10] Implement a simple version of visit/tryVisit for TaggedUnion. See #3. --- source/taggedalgebraic/taggedunion.d | 135 +++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/source/taggedalgebraic/taggedunion.d b/source/taggedalgebraic/taggedunion.d index e58c13c..103ad3e 100644 --- a/source/taggedalgebraic/taggedunion.d +++ b/source/taggedalgebraic/taggedunion.d @@ -11,6 +11,7 @@ import std.algorithm.mutation : move, swap; import std.meta; import std.traits : EnumMembers, FieldNameTuple, Unqual, isInstanceOf; + /** Implements a generic tagged union type. This struct takes a `union` or `struct` declaration as an input and builds @@ -325,8 +326,142 @@ unittest { // test for name clashes assert(tu.getString() == "foo"); } + +/** Dispatches the value contained on a `TaggedUnion` to a set of visitors. + + A visitor can have one of three forms: + + $(UL + $(LI function or delegate taking a single typed parameter) + $(LI function or delegate taking no parameters) + $(LI function or delegate template taking any single parameter) + ) + + .... +*/ +template visit(VISITORS...) { + auto visit(TU)(auto ref TU tu) + if (isInstanceOf!(TaggedUnion, TU)) + { + final switch (tu.kind) { + static foreach (k; EnumMembers!(TU.Kind)) { + case k: { + static if (isUnionType!(TU.FieldTypes[k])) + alias T = void; + else alias T = TU.FieldTypes[k]; + alias h = selectHandler!(T, VISITORS); + static if (is(h == void)) static assert(false, "No handler is able to take type "~T.stringof); + else static if (is(typeof(h) == string)) static assert(false, h); + else static if (is(T == void)) return h(); + else return h(tu.get!k); + } + } + } + } +} + +/// +unittest { + union U { + int number; + string text; + } + alias TU = TaggedUnion!U; + + auto tu = TU.number(42); + tu.visit!( + (int n) { assert(n == 42); }, + (string s) { assert(false); } + ); + + assert(tu.visit!((v) => to!int(v)) == 42); + + tu.setText("43"); + + assert(tu.visit!((v) => to!int(v)) == 43); +} + +// workaround for "template to is not defined" error in the unit test above +// happens on DMD 2.080 and below +private U to(U, T)(T val) { + static import std.conv; + return std.conv.to!U(val); +} + + +/** The same as `visit`, except that failure to handle types is checked at runtime. + + Instead of failing to compile, `tryVisit` will throw an `Exception` if none + of the handlers is able to handle the value contained in `tu`. +*/ +template tryVisit(VISITORS...) { + auto tryVisit(TU)(auto ref TU tu) + if (isInstanceOf!(TaggedUnion, TU)) + { + final switch (tu.kind) { + static foreach (k; EnumMembers!(TU.Kind)) { + case k: { + static if (isUnionType!(TU.FieldTypes[k])) + alias T = void; + else alias T = TU.FieldTypes[k]; + alias h = selectHandler!(T, VISITORS); + static if (is(h == void)) throw new Exception("Type "~T.stringof~" not handled by any visitor."); + else static if (is(typeof(h) == string)) static assert(false, h); + else static if (is(T == void)) return h(); + else return h(tu.get!k); + } + } + } + } +} + +/// +unittest { + import std.exception : assertThrown; + + union U { + int number; + string text; + } + alias TU = TaggedUnion!U; + + auto tu = TU.number(42); + tu.tryVisit!((int n) { assert(n == 42); }); + assertThrown(tu.tryVisit!((string s) { assert(false); })); +} + enum isUnionType(T) = is(T == Void) || is(T == void) || is(T == typeof(null)); +private template selectHandler(T, VISITORS...) +{ + import std.traits : ParameterTypeTuple, isSomeFunction; + + // TODO: error out for ambiguous handlers and handlers that don't match any type! + + template impl(int i) { + static if (i < VISITORS.length) { + alias fun = VISITORS[i]; + static if (isSomeFunction!fun) { + alias Params = ParameterTypeTuple!fun; + static if (Params.length == 0) { + static if (is(T == void)) + alias impl = fun; + else alias impl = impl!(i+1); + } else static if (Params.length == 1) { + static if (is(T : Params[0])) + alias impl = fun; + else alias impl = impl!(i+1); + } else enum impl = "Visitor at index "~i.stringof~" must not take more than one parameter."; + } else static if (isSomeFunction!(fun!T)) { + static if (ParameterTypeTuple!(fun!T).length == 1) + alias impl = fun!T; + else enum impl = "Generic visitor at index "~i.stringof~" must have a single parameter."; + } else enum impl = "Visitor at index "~i.stringof~" (or its template instantiation with type "~T.stringof~") must be a valid function or delegate."; + } else alias impl = void; + } + alias selectHandler = impl!0; +} + private string pascalCase(string camel_case) { if (!__ctfe) assert(false); From 59826ad7194d6358d7447266fd177b9f7793a2a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Fri, 22 Feb 2019 11:14:56 +0100 Subject: [PATCH 08/10] Error out on extraneous visitors. --- source/taggedalgebraic/taggedunion.d | 109 +++++++++++++++++++++------ 1 file changed, 88 insertions(+), 21 deletions(-) diff --git a/source/taggedalgebraic/taggedunion.d b/source/taggedalgebraic/taggedunion.d index 103ad3e..91b1c42 100644 --- a/source/taggedalgebraic/taggedunion.d +++ b/source/taggedalgebraic/taggedunion.d @@ -350,7 +350,7 @@ template visit(VISITORS...) { alias T = void; else alias T = TU.FieldTypes[k]; alias h = selectHandler!(T, VISITORS); - static if (is(h == void)) static assert(false, "No handler is able to take type "~T.stringof); + static if (is(typeof(h) == typeof(null))) static assert(false, "No visitor defined for type type "~T.stringof); else static if (is(typeof(h) == string)) static assert(false, h); else static if (is(T == void)) return h(); else return h(tu.get!k); @@ -381,6 +381,34 @@ unittest { assert(tu.visit!((v) => to!int(v)) == 43); } +unittest { + union U { + Void none; + int count; + float length; + } + TaggedUnion!U u; + + // + static assert(is(typeof(u.visit!((int) {}, (float) {}, () {})))); + u.visit!((_) {}, () {}); + static assert(is(typeof(u.visit!((_) {}, () {})))); + static assert(is(typeof(u.visit!((_) {}, (float) {}, () {})))); + static assert(is(typeof(u.visit!((float) {}, (_) {}, () {})))); + + static assert(!is(typeof(u.visit!((_) {})))); // missing void handler + static assert(!is(typeof(u.visit!(() {})))); // missing value handler + + static assert(!is(typeof(u.visit!((_) {}, () {}, (string) {})))); // invalid typed handler + static assert(!is(typeof(u.visit!((int) {}, (float) {}, () {}, () {})))); // duplicate void handler + static assert(!is(typeof(u.visit!((_) {}, () {}, (_) {})))); // duplicate generic handler + static assert(!is(typeof(u.visit!((int) {}, (float) {}, (float) {}, () {})))); // duplicate typed handler + + // TODO: error out for superfluous generic handlers + //static assert(!is(typeof(u.visit!((int) {}, (float) {}, () {}, (_) {})))); // superfluous generic handler +} + + // workaround for "template to is not defined" error in the unit test above // happens on DMD 2.080 and below private U to(U, T)(T val) { @@ -405,7 +433,7 @@ template tryVisit(VISITORS...) { alias T = void; else alias T = TU.FieldTypes[k]; alias h = selectHandler!(T, VISITORS); - static if (is(h == void)) throw new Exception("Type "~T.stringof~" not handled by any visitor."); + static if (is(typeof(h) == typeof(null))) throw new Exception("Type "~T.stringof~" not handled by any visitor."); else static if (is(typeof(h) == string)) static assert(false, h); else static if (is(T == void)) return h(); else return h(tu.get!k); @@ -432,34 +460,73 @@ unittest { enum isUnionType(T) = is(T == Void) || is(T == void) || is(T == typeof(null)); +private template validateHandlers(TU, VISITORS...) +{ + alias Types = TU.FieldTypes; + + static foreach (int i; 0 .. VISITORS.length) { + static assert(anySatisfy!(matchesType!(VISITORS[i]), Types), + "Visitor at index "~i.stringof~" does not match any type of "~TU.stringof); + } +} + +private template matchesType(alias fun, T) +{ + static if (isSomeFunction!fun) { + alias Params = ParameterTypeTuple!fun; + static if (Params.length == 0 && is(T == void)) enum matchesType = true; + else static if (Params.length == 1 && is(T == Params[0])) enum matchesType = true; + else enum matchesType = false; + } else static if (!is(T == void)) { + static if (isSomeFunction!(fun!T)) { + alias Parms = ParameterTypeTuple!fun; + static if (Params.length == 1 && is(T == Params[0])) enum matchesType = true; + else enum matchesType = false; + } else enum matchesType = false; + } else enum matchesType = false; +} + private template selectHandler(T, VISITORS...) { import std.traits : ParameterTypeTuple, isSomeFunction; - // TODO: error out for ambiguous handlers and handlers that don't match any type! - - template impl(int i) { + template typedIndex(int i, int matched_index = -1) { static if (i < VISITORS.length) { alias fun = VISITORS[i]; static if (isSomeFunction!fun) { alias Params = ParameterTypeTuple!fun; - static if (Params.length == 0) { - static if (is(T == void)) - alias impl = fun; - else alias impl = impl!(i+1); - } else static if (Params.length == 1) { - static if (is(T : Params[0])) - alias impl = fun; - else alias impl = impl!(i+1); - } else enum impl = "Visitor at index "~i.stringof~" must not take more than one parameter."; - } else static if (isSomeFunction!(fun!T)) { - static if (ParameterTypeTuple!(fun!T).length == 1) - alias impl = fun!T; - else enum impl = "Generic visitor at index "~i.stringof~" must have a single parameter."; - } else enum impl = "Visitor at index "~i.stringof~" (or its template instantiation with type "~T.stringof~") must be a valid function or delegate."; - } else alias impl = void; + static if (Params.length > 1) enum typedIndex = "Visitor at index "~i.stringof~" must not take more than one parameter."; + else static if (Params.length == 0 && is(T == void) || Params.length == 1 && is(T == Params[0])) { + static if (matched_index >= 0) enum typedIndex = "Vistor at index "~i.stringof~" conflicts with visitor at index "~matched_index~"."; + else enum typedIndex = typedIndex!(i+1, i); + } else enum typedIndex = typedIndex!(i+1, matched_index); + } else enum typedIndex = typedIndex!(i+1, matched_index); + } else enum typedIndex = matched_index; } - alias selectHandler = impl!0; + + template genericIndex(int i, int matched_index = -1) { + static if (i < VISITORS.length) { + alias fun = VISITORS[i]; + static if (!isSomeFunction!fun) { + static if (isSomeFunction!(fun!T)) { + static if (ParameterTypeTuple!(fun!T).length == 1) { + static if (matched_index >= 0) enum genericIndex = "Only one generic visitor allowed"; + else enum genericIndex = genericIndex!(i+1, i); + } else enum genericIndex = "Generic visitor at index "~i.stringof~" must have a single parameter."; + } else enum genericIndex = "Visitor at index "~i.stringof~" (or its template instantiation with type "~T.stringof~") must be a valid function or delegate."; + } else enum genericIndex = genericIndex!(i+1, matched_index); + } else enum genericIndex = matched_index; + } + + enum typed_index = typedIndex!0; + static if (is(T == void)) enum generic_index = -1; + else enum generic_index = genericIndex!0; + + static if (is(typeof(typed_index) == string)) enum selectHandler = typed_index; + else static if (is(typeof(generic_index == string))) enum selectHandler = generic_index; + else static if (typed_index >= 0) alias selectHandler = VISITORS[typed_index]; + else static if (generic_index >= 0) alias selectHandler = VISITORS[generic_index]; + else enum selectHandler = null; } private string pascalCase(string camel_case) From 2d9557dc213409a38e403aa9d69ab965a54883a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Fri, 22 Feb 2019 11:30:31 +0100 Subject: [PATCH 09/10] Update README. Adds a TaggedUnion example and adds a section for supported compiler versions. --- README.md | 112 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 94 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 9d360ab..589d2d7 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,81 @@ TaggedAlgebraic =============== -Implementation of a generic algebraic data type with a tagged union storage. All operations of the contained types are available for the `TaggedAlgebraic` +Implementation of a generic `TaggedUnion` type along with a `TaggedAlgebraic` type that forwards all methods and operators of the contained types using dynamic dispatch. [![Build Status](https://travis-ci.org/s-ludwig/taggedalgebraic.svg?branch=master)](https://travis-ci.org/s-ludwig/taggedalgebraic) [![codecov](https://codecov.io/gh/s-ludwig/taggedalgebraic/branch/master/graph/badge.svg)](https://codecov.io/gh/s-ludwig/taggedalgebraic) -Usage ------ +Usage of `TaggedUnion` +---------------------- + +```d +import taggedalgebraic; + +struct Foo { + string name; + void bar() {} +} + +union Base { + int count; + int offset; + string str; + Foo foo; +} + +alias TUnion = TaggedUnion!Base; + +// Instantiate +TUnion taggedInt = TUnion.count(5); +TUnion taggedString = TUnion.str("Hello"); +TUnion taggedFoo = TUnion.foo; +TUnion taggedAny = taggedInt; +taggedAny = taggedString; +taggedAny = taggedFoo; + +// Default initializes to the first field +TUnion taggedDef; +assert(taggedDef.isCount); +assert(taggedDef.getCount == int.init); + +// Check type: TUnion.Kind is an enum +assert(taggedInt.kind == TUnion.Kind.count); +assert(taggedString.kind == TUnion.Kind.str); +assert(taggedFoo.kind == TUnion.Kind.foo); +assert(taggedAny.kind == TUnion.Kind.foo); + +// A shorter syntax is also available +assert(taggedInt.isCount); +assert(!taggedInt.isOffset); +assert(taggedString.isStr); +assert(taggedFoo.isFoo); +assert(taggedAny.isFoo); + +// Set to a different type +taggedAny.setStr("bar"); +assert(taggedAny.isStr); +assert(taggedAny.getStr() == "bar"); + +// Modify contained value by reference +taggedAny.getStr() = "baz"; +assert(taggedAny.getStr() == "baz"); + +// In addition to the getter, the contained value can be extracted using get!() +// or by casting +assert(taggedInt.get!(TUnion.Kind.count) == 5); +assert(taggedInt.get!int == 5); +assert(cast(byte)taggedInt == 5); + +// Multiple kinds of the same type are supported +taggedAny.setOffset(5); +assert(taggedAny.isOffset); +assert(taggedAny.isCount); +``` + + +Usage of `TaggedAlgebraic` +-------------------------- ```d import taggedalgebraic; @@ -23,27 +91,27 @@ union Base { Foo foo; } -alias Tagged = TaggedAlgebraic!Base; +alias TAlgebraic = TaggedAlgebraic!Base; // Instantiate -Tagged taggedInt = 5; -Tagged taggedString = "Hello"; -Tagged taggedFoo = Foo(); -Tagged taggedAny = taggedInt; +TAlgebraic taggedInt = 5; +TAlgebraic taggedString = "Hello"; +TAlgebraic taggedFoo = Foo(); +TAlgebraic taggedAny = taggedInt; taggedAny = taggedString; taggedAny = taggedFoo; -// Check type: Tagged.Kind is an enum -assert(taggedInt.kind == Tagged.Kind.i); -assert(taggedString.kind == Tagged.Kind.str); -assert(taggedFoo.kind == Tagged.Kind.foo); -assert(taggedAny.kind == Tagged.Kind.foo); +// Check type: TAlgebraic.Kind is an enum +assert(taggedInt.kind == TAlgebraic.Kind.i); +assert(taggedString.kind == TAlgebraic.Kind.str); +assert(taggedFoo.kind == TAlgebraic.Kind.foo); +assert(taggedAny.kind == TAlgebraic.Kind.foo); // In most cases, can simply use as-is auto num = 4 + taggedInt; auto msg = taggedString ~ " World!"; taggedFoo.bar(); -if (taggedAny.kind == Tagged.Kind.foo) // Make sure to check type first! +if (taggedAny.kind == TAlgebraic.Kind.foo) // Make sure to check type first! taggedAny.bar(); //taggedString.bar(); // AssertError: Not a Foo! @@ -51,22 +119,30 @@ if (taggedAny.kind == Tagged.Kind.foo) // Make sure to check type first! auto i = cast(int) taggedInt; auto str = cast(string) taggedString; auto foo = cast(Foo) taggedFoo; -if (taggedAny.kind == Tagged.Kind.foo) // Make sure to check type first! +if (taggedAny.kind == TAlgebraic.Kind.foo) // Make sure to check type first! auto foo2 = cast(Foo) taggedAny; //cast(Foo) taggedString; // AssertError! // Kind is an enum, so final switch is supported: final switch (taggedAny.kind) { - case Tagged.Kind.i: + case TAlgebraic.Kind.i: // It's "int i" break; - case Tagged.Kind.str: + case TAlgebraic.Kind.str: // It's "string str" break; - case Tagged.Kind.foo: + case TAlgebraic.Kind.foo: // It's "Foo foo" break; } ``` + +Compiler support +---------------- + +The library is tested to work on the following compilers: + +- DMD 2.076.1 up to 2.084.1 +- LDC 1.6.0 up to 1.14.0 From 1c88c3c65d6670fd0067238eaa96a9b62fef2b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6nke=20Ludwig?= Date: Fri, 22 Feb 2019 12:22:38 +0100 Subject: [PATCH 10/10] Add template constraint to visit/tryVisit. --- source/taggedalgebraic/taggedunion.d | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/source/taggedalgebraic/taggedunion.d b/source/taggedalgebraic/taggedunion.d index 91b1c42..c01c8c3 100644 --- a/source/taggedalgebraic/taggedunion.d +++ b/source/taggedalgebraic/taggedunion.d @@ -339,7 +339,9 @@ unittest { // test for name clashes .... */ -template visit(VISITORS...) { +template visit(VISITORS...) + if (VISITORS.length > 0) +{ auto visit(TU)(auto ref TU tu) if (isInstanceOf!(TaggedUnion, TU)) { @@ -422,7 +424,9 @@ private U to(U, T)(T val) { Instead of failing to compile, `tryVisit` will throw an `Exception` if none of the handlers is able to handle the value contained in `tu`. */ -template tryVisit(VISITORS...) { +template tryVisit(VISITORS...) + if (VISITORS.length > 0) +{ auto tryVisit(TU)(auto ref TU tu) if (isInstanceOf!(TaggedUnion, TU)) {