diff --git a/compiler/include/dmd/aggregate.h b/compiler/include/dmd/aggregate.h index 65ce7732b738..a704c38cb812 100644 --- a/compiler/include/dmd/aggregate.h +++ b/compiler/include/dmd/aggregate.h @@ -13,6 +13,8 @@ #include "dsymbol.h" #include "objc.h" +class TypeSumType; + class AliasThis; class Identifier; class Type; @@ -162,6 +164,7 @@ class StructDeclaration : public AggregateDeclaration // ABI-specific type(s) if the struct can be passed in registers TypeTuple *argTypes; + TypeSumType *sumtype; // if non-null, this struct is the lowered form of a __sumtype structalign_t alignment; // alignment applied outside of the struct ThreeState ispod; // if struct is POD private: diff --git a/compiler/include/dmd/expression.h b/compiler/include/dmd/expression.h index 6d135576e86b..48d53b83c2fe 100644 --- a/compiler/include/dmd/expression.h +++ b/compiler/include/dmd/expression.h @@ -706,6 +706,7 @@ class DotVarExp final : public UnaExp public: Declaration *var; d_bool hasOverloads; + d_bool compilerOverlappedAccess; void accept(Visitor *v) override { v->visit(this); } }; @@ -1256,6 +1257,30 @@ class GenericExp final : Expression /****************************************************************/ +struct SumTypeMatchArmInfo +{ + VarDeclaration *vd; + Expression *guard; + int variantIndex; + int originalIndex; +}; + +class MatchExp final : Expression +{ +public: + Expression *arg; + Array *armInfos; + Type *resultType; + StructDeclaration *loweredStruct; + TypeSumType *sumtypeType; + + MatchExp *syntaxCopy() override; + + void accept(Visitor *v) override { v->visit(this); } +}; + +/****************************************************************/ + class DefaultInitExp : public Expression { public: diff --git a/compiler/include/dmd/mtype.h b/compiler/include/dmd/mtype.h index b6e08000006f..b8042807a976 100644 --- a/compiler/include/dmd/mtype.h +++ b/compiler/include/dmd/mtype.h @@ -106,6 +106,8 @@ enum class TY : uint8_t Ttraits, Tmixin, Tnoreturn, + Ttag, + Tsumtype, TMAX }; @@ -284,6 +286,7 @@ class Type : public ASTNode TypeTraits *isTypeTraits(); TypeNoreturn *isTypeNoreturn(); TypeTag *isTypeTag(); + TypeSumType *isTypeSumType(); void accept(Visitor *v) override { v->visit(this); } }; @@ -713,6 +716,30 @@ class TypeTag final : public Type /**************************************************************/ +struct SumTypeVariantInfo +{ + Type *type; + Identifier *name; + Expressions *udas; + const char *comment; +}; + +typedef Array SumTypeVariantInfos; + +class TypeSumType final : public Type +{ +public: + SumTypeVariantInfos *variantInfos; + StructDeclaration *loweredStruct; + size_t defaultVariantIdx; + + TypeSumType *syntaxCopy() override; + + void accept(Visitor *v) override { v->visit(this); } +}; + +/**************************************************************/ + namespace dmd { // If the type is a class or struct, returns the symbol for it, else null. diff --git a/compiler/include/dmd/tokens.h b/compiler/include/dmd/tokens.h index 0c50aad11d52..8efa4dae480e 100644 --- a/compiler/include/dmd/tokens.h +++ b/compiler/include/dmd/tokens.h @@ -259,6 +259,8 @@ enum class TOK : unsigned char whitespace, rvalue, + sumtype_, + // C only keywords inline_, register_, @@ -430,6 +432,10 @@ enum class EXP : unsigned char _Generic_, interval, + loweredAssignExp, + rvalue, + matchExp, + MAX }; diff --git a/compiler/include/dmd/visitor.h b/compiler/include/dmd/visitor.h index 674ade585c20..01553a788de2 100644 --- a/compiler/include/dmd/visitor.h +++ b/compiler/include/dmd/visitor.h @@ -86,6 +86,7 @@ class TypeNoreturn; class TypeTraits; class TypeMixin; class TypeTag; +class TypeSumType; class Dsymbol; @@ -298,6 +299,7 @@ class ClassReferenceExp; class VoidInitExp; class ThrownExceptionExp; class GenericExp; +class MatchExp; class TemplateParameter; class TemplateTypeParameter; @@ -450,6 +452,7 @@ class ParseTimeVisitor virtual void visit(TypeTraits *t) { visit((Type *)t); } virtual void visit(TypeMixin *t) { visit((Type *)t); } virtual void visit(TypeTag *t) { visit((Type *)t); } + virtual void visit(TypeSumType *t) { visit((Type *)t); } // TypeNext virtual void visit(TypeReference *t) { visit((TypeNext *)t); } @@ -499,6 +502,7 @@ class ParseTimeVisitor virtual void visit(TupleExp *e) { visit((Expression *)e); } virtual void visit(ThisExp *e) { visit((Expression *)e); } virtual void visit(GenericExp *e) { visit((Expression *)e); } + virtual void visit(MatchExp *e) { visit((Expression *)e); } // Miscellaneous virtual void visit(VarExp *e) { visit((SymbolExp *)e); } diff --git a/compiler/src/dmd/arraytypes.d b/compiler/src/dmd/arraytypes.d index 1d73cfc175ea..a767d9e73e3d 100644 --- a/compiler/src/dmd/arraytypes.d +++ b/compiler/src/dmd/arraytypes.d @@ -55,3 +55,5 @@ alias TemplateInstances = Array!(TemplateInstance); alias Ensures = Array!(Ensure); alias Designators = Array!(Designator); alias DesigInits = Array!(DesigInit); +alias SumTypeVariantInfos = Array!(SumTypeVariantInfo); +alias SumTypeMatchArmInfos = Array!(SumTypeMatchArmInfo); diff --git a/compiler/src/dmd/astbase.d b/compiler/src/dmd/astbase.d index dfa88c6517c9..6c243f0c2f94 100644 --- a/compiler/src/dmd/astbase.d +++ b/compiler/src/dmd/astbase.d @@ -2702,6 +2702,7 @@ struct ASTBase sizeTy[Tmixin] = __traits(classInstanceSize, TypeMixin); sizeTy[Tnoreturn] = __traits(classInstanceSize, TypeNoreturn); sizeTy[Ttag] = __traits(classInstanceSize, TypeTag); + sizeTy[Tsumtype] = __traits(classInstanceSize, TypeSumType); return sizeTy; }(); @@ -3784,6 +3785,39 @@ struct ASTBase } } + struct SumTypeVariantInfo + { + Type type; + Identifier name; + Expressions* udas; + const(char)* comment; + } + + alias SumTypeVariantInfos = Array!(SumTypeVariantInfo); + + extern (C++) final class TypeSumType : Type + { + SumTypeVariantInfos* variantInfos; + StructDeclaration loweredStruct; + size_t defaultVariantIdx; + + extern (D) this(SumTypeVariantInfos* variantInfos = null) + { + super(Tsumtype); + this.variantInfos = variantInfos; + } + + override TypeSumType syntaxCopy() + { + return this; + } + + override void accept(Visitor v) + { + v.visit(this); + } + } + extern (C++) final class TypeReference : TypeNext { extern (D) this(Type t) @@ -6270,6 +6304,35 @@ struct ASTBase } } + struct SumTypeMatchArmInfo + { + VarDeclaration vd; + Expression guard; + int variantIndex; + int originalIndex; + } + + alias SumTypeMatchArmInfos = Array!(SumTypeMatchArmInfo); + + extern (C++) final class MatchExp : Expression + { + Expression arg; + SumTypeMatchArmInfos* armInfos; + Type resultType; + + extern (D) this(Loc loc, Expression arg, SumTypeMatchArmInfos* armInfos) + { + super(loc, EXP.matchExp, __traits(classInstanceSize, MatchExp)); + this.arg = arg; + this.armInfos = armInfos; + } + + override void accept(Visitor v) + { + v.visit(this); + } + } + extern (C++) final class ErrorExp : Expression { extern (D) this() diff --git a/compiler/src/dmd/astcodegen.d b/compiler/src/dmd/astcodegen.d index ae0c3629178b..bcc5c3f1cda8 100644 --- a/compiler/src/dmd/astcodegen.d +++ b/compiler/src/dmd/astcodegen.d @@ -74,6 +74,7 @@ struct ASTCodegen alias Tvoid = dmd.mtype.Tvoid; alias Twchar = dmd.mtype.Twchar; alias Tnoreturn = dmd.mtype.Tnoreturn; + alias Tsumtype = dmd.mtype.Tsumtype; alias Timaginary32 = dmd.mtype.Timaginary32; alias Timaginary64 = dmd.mtype.Timaginary64; diff --git a/compiler/src/dmd/astenums.d b/compiler/src/dmd/astenums.d index dcaa9dbd8da1..90e33fce399a 100644 --- a/compiler/src/dmd/astenums.d +++ b/compiler/src/dmd/astenums.d @@ -228,6 +228,7 @@ enum TY : ubyte Tmixin, Tnoreturn, Ttag, + Tsumtype, } enum TMAX = TY.max + 1; @@ -279,6 +280,7 @@ alias Ttraits = TY.Ttraits; alias Tmixin = TY.Tmixin; alias Tnoreturn = TY.Tnoreturn; alias Ttag = TY.Ttag; +alias Tsumtype = TY.Tsumtype; enum TFlags { diff --git a/compiler/src/dmd/dcast.d b/compiler/src/dmd/dcast.d index aefe160f8333..6685d28821f0 100644 --- a/compiler/src/dmd/dcast.d +++ b/compiler/src/dmd/dcast.d @@ -1549,6 +1549,72 @@ MATCH implicitConvTo(Expression e, Type t) } } +/******************************** + * If `from` is a sumtype and `to` is a (wider) sumtype that contains + * every variant of `from`, then `from` can be implicitly converted to `to`. + * + * Params: + * from = candidate source type + * to = candidate destination type + * Returns: + * MATCH.convert if `from` widens into `to`, MATCH.nomatch otherwise. + */ +private MATCH sumtypeWidenMatch(Type from, Type to) +{ + TypeSumType fromSum; + if (auto ts = from.isTypeSumType()) + fromSum = ts; + else if (auto tsa = from.isTypeStruct()) + if (tsa.sym && tsa.sym.sumtype) + fromSum = tsa.sym.sumtype; + if (!fromSum) + return MATCH.nomatch; + + TypeSumType toSum; + if (auto ts = to.isTypeSumType()) + toSum = ts; + else if (auto tsa = to.isTypeStruct()) + if (tsa.sym && tsa.sym.sumtype) + toSum = tsa.sym.sumtype; + if (!toSum || fromSum == toSum) + return MATCH.nomatch; + + // Every source variant must be a variant of the target. + // + // This is two-pass: first look for an exact type match, then fall back to + // an implicit conversion. This is deliberate — with a single + // "equals || implicitConvTo" check, a bool source variant would match an + // int target variant (because bool implicitly converts to int), corrupting + // the active variant during widening. Preferring the exact match preserves + // which variant is active. + foreach (vi; *fromSum.variantInfos) + { + bool found = false; + foreach (vj; *toSum.variantInfos) + { + if (vi.type.equals(vj.type)) + { + found = true; + break; + } + } + if (!found) + { + foreach (vj; *toSum.variantInfos) + { + if (vi.type.implicitConvTo(vj.type) != MATCH.nomatch) + { + found = true; + break; + } + } + } + if (!found) + return MATCH.nomatch; + } + return MATCH.convert; +} + /******************************** * Determine if 'from' can be implicitly converted * to type 'to'. @@ -1903,6 +1969,21 @@ MATCH implicitConvTo(Type from, Type to) MATCH visitStruct(TypeStruct from) { //printf("TypeStruct::implicitConvTo(%s => %s)\n", from.toChars(), to.toChars()); + + // For sumtypes, the normal struct conversion handles the same-struct + // case (e.g. adding const); widening only applies to a *different* + // (wider) sumtype struct. + if (from.sym.sumtype !is null) + { + auto tos = to.isTypeStruct(); + if (tos && from.sym == tos.sym) + { + MATCH m = from.implicitConvToWithoutAliasThis(to); + return m == MATCH.nomatch ? from.implicitConvToThroughAliasThis(to) : m; + } + return sumtypeWidenMatch(from.sym.sumtype, to); + } + MATCH m = from.implicitConvToWithoutAliasThis(to); return m == MATCH.nomatch ? from.implicitConvToThroughAliasThis(to) : m; } @@ -2003,6 +2084,7 @@ MATCH implicitConvTo(Type from, Type to) case Ttuple: return visitTuple(from.isTypeTuple()); case Tnull: return visitNull(from.isTypeNull()); case Tnoreturn: return visitNoreturn(from.isTypeNoreturn()); + case Tsumtype: { const m = sumtypeWidenMatch(from, to); return m == MATCH.nomatch ? visitType(from) : m; } } } @@ -2152,6 +2234,13 @@ Expression castTo(Expression e, Scope* sc, Type t, Type att = null) return result; } + // Sumtype widening: a narrower sumtype is implicitly convertible to a wider one + if (sumtypeWidenMatch(e.type, tob) != MATCH.nomatch) + { + if (auto widened = sumtypeWidenExpression(e, sc, tob)) + return widened; + } + /* Make semantic error against invalid cast between concrete types. * Assume that 'e' is never be any placeholder expressions. * The result of these checks should be consistent with CastExp::toElem(). diff --git a/compiler/src/dmd/dfa/fast/expression.d b/compiler/src/dmd/dfa/fast/expression.d index 990e1f27d289..13cc407e0e36 100644 --- a/compiler/src/dmd/dfa/fast/expression.d +++ b/compiler/src/dmd/dfa/fast/expression.d @@ -1559,6 +1559,7 @@ struct ExpressionWalker case EXP._Generic: case EXP.interval: + case EXP.matchExp: case EXP.rvalue: if (dfaCommon.debugUnknownAST) { diff --git a/compiler/src/dmd/doc.d b/compiler/src/dmd/doc.d index 1962e3f6f8ff..93beec80bf4d 100644 --- a/compiler/src/dmd/doc.d +++ b/compiler/src/dmd/doc.d @@ -1344,6 +1344,70 @@ void emitVisibility(ref OutBuffer buf, Visibility vis) buf.writeByte(' '); } +void emitSumTypeMemberComments(TypeSumType tst, Dsymbol sym, ref OutBuffer buf, Scope* sc, Loc loc) +{ + if (!(*tst.variantInfos).length) + return; + bool hasAnyComment = false; + foreach (vi; (*tst.variantInfos)) + { + if (vi.comment && vi.comment[0]) + { + hasAnyComment = true; + break; + } + } + if (!hasAnyComment) + return; + + auto symArr = new Dsymbols(1); + (*symArr)[0] = sym; + + buf.writestring("$(DDOC_SUMTYPE_MEMBERS "); + foreach (i, vi; (*tst.variantInfos)) + { + if (!vi.comment || !vi.comment[0]) + continue; + buf.writestring("$(DDOC_MEMBER"); + buf.writestring("$(DDOC_MEMBER_HEADER"); + { + buf.writestring("$(DDOC_ANCHOR "); + if (vi.name) + buf.writestring(vi.name.toChars()); + else + { + buf.writestring("Variant"); + buf.print(i); + } + buf.writeByte(')'); + buf.writeByte(' '); + { + HdrGenState hgs; + hgs.ddoc = true; + toCBuffer(vi.type, buf, null, hgs); + if (vi.name) + { + buf.writeByte(' '); + buf.writestring(vi.name.toChars()); + } + } + } + buf.writeByte(')'); + buf.writestring(ddoc_decl_dd_s); + buf.writestring("$(DDOC_SECTIONS "); + buf.writestring("$(DDOC_SUMMARY "); + size_t o = buf.length; + buf.writestring(vi.comment); + escapeStrayParenthesis(loc, buf, o, true, sc.eSink); + highlightText(sc, symArr, loc, buf, o); + buf.writestring(")"); + buf.writestring(")"); + buf.writestring(ddoc_decl_dd_e); + buf.writeByte(')'); + } + buf.writestring(")"); +} + void emitComment(Dsymbol s, ref OutBuffer buf, Scope* sc) { extern (C++) final class EmitComment : Visitor @@ -1441,11 +1505,22 @@ void emitComment(Dsymbol s, ref OutBuffer buf, Scope* sc) { dc.writeSections(sc, &dc.a, *buf); foreach (sym; dc.a) + { if (ScopeDsymbol sds = sym.isScopeDsymbol()) { emitMemberComments(sds, *buf, sc); break; } + if (AliasDeclaration ad = sym.isAliasDeclaration()) + { + Type origType = ad.originalType ? ad.originalType : ad.type; + if (auto tst = origType ? origType.isTypeSumType() : null) + { + emitSumTypeMemberComments(tst, ad, *buf, sc, ad.loc); + break; + } + } + } } buf.writestring(ddoc_decl_dd_e); buf.writeByte(')'); @@ -1785,6 +1860,27 @@ void toDocBuffer(Dsymbol s, ref OutBuffer buf, Scope* sc) if (ad.isDeprecated()) buf.writestring("deprecated "); emitVisibility(*buf, ad); + Type origType = ad.originalType ? ad.originalType : ad.type; + if (auto tst = origType ? origType.isTypeSumType() : null) + { + buf.printf("__sumtype %s", ad.toChars()); + buf.writestring(" = "); + foreach (i, vi; (*tst.variantInfos)) + { + if (i > 0) + buf.writestring(" | "); + HdrGenState hgs; + hgs.ddoc = true; + toCBuffer(vi.type, *buf, null, hgs); + if (vi.name) + { + buf.writeByte(' '); + buf.writestring(vi.name.toChars()); + } + } + buf.writestring(";\n"); + return; + } buf.printf("alias %s = ", ad.toChars()); if (Dsymbol s = ad.aliassym) // ident alias { diff --git a/compiler/src/dmd/dstruct.d b/compiler/src/dmd/dstruct.d index 089307968d57..acc7d395f9c0 100644 --- a/compiler/src/dmd/dstruct.d +++ b/compiler/src/dmd/dstruct.d @@ -50,6 +50,8 @@ extern (C++) class StructDeclaration : AggregateDeclaration // ABI-specific type(s) if the struct can be passed in registers TypeTuple argTypes; + TypeSumType sumtype; // if non-null, this struct is the lowered form of a __sumtype + structalign_t alignment; // alignment applied outside of the struct ThreeState ispod; // if struct is POD diff --git a/compiler/src/dmd/expression.d b/compiler/src/dmd/expression.d index 37aa5710c612..7a7f034e4f81 100644 --- a/compiler/src/dmd/expression.d +++ b/compiler/src/dmd/expression.d @@ -451,6 +451,7 @@ extern (C++) abstract class Expression : ASTNode inout(IdentityExp) isIdentityExp() { return (op == EXP.identity || op == EXP.notIdentity) ? cast(typeof(return))this : null; } inout(CondExp) isCondExp() { return op == EXP.question ? cast(typeof(return))this : null; } inout(GenericExp) isGenericExp() { return op == EXP._Generic ? cast(typeof(return))this : null; } + inout(MatchExp) isMatchExp() { return op == EXP.matchExp ? cast(typeof(return))this : null; } inout(DefaultInitExp) isDefaultInitExp() { return op == EXP.defaultInit ? cast(typeof(return))this : null; } inout(ObjcClassReferenceExp) isObjcClassReferenceExp() { return op == EXP.objcClassReference ? cast(typeof(return))this : null; } inout(ClassReferenceExp) isClassReferenceExp() { return op == EXP.classReference ? cast(typeof(return))this : null; } @@ -2225,6 +2226,7 @@ extern (C++) final class DotVarExp : UnaExp { Declaration var; bool hasOverloads; + bool compilerOverlappedAccess; // compiler-generated access to overlapped field (e.g. match internals) extern (D) this(Loc loc, Expression e, Declaration var, bool hasOverloads = true) @safe { @@ -3961,6 +3963,58 @@ extern (C++) final class GenericExp : Expression } } +/*********************************************************** + * Match expression: val.match { (Type id) if (guard) => expr, ... } + */ + +struct SumTypeMatchArmInfo +{ + VarDeclaration vd; /// arm declaration (type = param type, ident = param name, _init = body expr) + Expression guard; /// guard expression, null if none + int variantIndex; /// which variant this arm matches (-2 = catch-all, filled during semantic) + int originalIndex; /// source order position (for stable sort) +} + +extern (C++) final class MatchExp : Expression +{ + Expression arg; /// scrutinee expression + SumTypeMatchArmInfos* armInfos; /// match arms with optional guards, sorted by variantIndex after semantic + Type resultType; /// computed result type + StructDeclaration loweredStruct; /// lowered sumtype struct + TypeSumType sumtypeType; /// the sumtype declaration this matches on + + extern (D) this(Loc loc, Expression arg, SumTypeMatchArmInfos* armInfos) @safe + { + super(loc, EXP.matchExp); + this.arg = arg; + this.armInfos = armInfos; + } + + override MatchExp syntaxCopy() + { + auto copyInfos = new SumTypeMatchArmInfos(); + foreach (i, ref ai; *armInfos) + { + SumTypeMatchArmInfo cai; + cai.vd = cast(VarDeclaration) ai.vd.syntaxCopy(null); + cai.guard = ai.guard ? ai.guard.syntaxCopy() : null; + cai.variantIndex = ai.variantIndex; + cai.originalIndex = ai.originalIndex; + copyInfos.push(cai); + } + auto copy = new MatchExp(loc, arg.syntaxCopy(), copyInfos); + copy.resultType = resultType; + copy.loweredStruct = loweredStruct; + copy.sumtypeType = sumtypeType; + return copy; + } + + override void accept(Visitor v) + { + v.visit(this); + } +} + /** * Verify if the given identifier is _d_array{,set}ctor. * diff --git a/compiler/src/dmd/expressionsem.d b/compiler/src/dmd/expressionsem.d index bd603486c702..17112b75ff9e 100644 --- a/compiler/src/dmd/expressionsem.d +++ b/compiler/src/dmd/expressionsem.d @@ -7848,6 +7848,130 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor // Check for call operator overload if (t1) { + // Handle sumtype constructor: S(42) or S(x: 5) + if (exp.e1.op == EXP.type) + { + TypeSumType ts; + StructDeclaration sd; + if (t1.ty == Tsumtype) + { + ts = t1.isTypeSumType(); + sd = ts.loweredStruct; + } + else if (t1.ty == Tstruct) + { + sd = (cast(TypeStruct)t1).sym; + ts = sd.sumtype; + if (ts is null) + sd = null; + } + if (sd !is null) + { + if ((*ts.variantInfos).length == 0) + { + error(exp.loc, "cannot construct empty sumtype"); + return setError(); + } + + // Determine which variant to initialize + int variantIdx = -1; + + // Check if first argument has a name (named variant syntax) + bool hasName = exp.names && exp.names.length > 0 && (*exp.names)[0].name !is null; + + if (hasName) + { + // Named variant: S(x: 5) + auto name = (*exp.names)[0].name; + + foreach (j, vi; (*ts.variantInfos)) + { + if (vi.name !is null && vi.name.toString() == name.toString()) + { + variantIdx = cast(int)j; + break; + } + } + + if (variantIdx == -1) + { + error(exp.loc, "no variant named `%s` in sumtype", name.toChars()); + return setError(); + } + else if (exp.arguments.length != 1) + { + error(exp.loc, "expected exactly one argument for named variant `%s`", name.toChars()); + return setError(); + } + } + else if (exp.arguments && exp.arguments.length == 1) + { + // Unnamed variant type inference: S(42) + auto argType = (*exp.arguments)[0].type; + + if (argType) + { + argType = argType.toBasetype(); + + // Variant selection is two-pass: first look for an + // exact type match, then fall back to an implicit + // conversion. This is deliberate — with a single + // "equals || implicitConvTo" check, S(true) on + // __sumtype(int | bool) would pick the int variant, + // because bool implicitly converts to int. Preferring + // the exact match keeps the value 1 (which represents + // both `true` and the int 1) from silently choosing + // the wrong variant. + foreach (j, vi; (*ts.variantInfos)) + { + if (argType.equals(vi.type)) + { + variantIdx = cast(int)j; + break; + } + } + if (variantIdx == -1) + { + foreach (j, vi; (*ts.variantInfos)) + { + if (argType.implicitConvTo(vi.type)) + { + variantIdx = cast(int)j; + break; + } + } + } + } + if (variantIdx == -1) + { + error(exp.loc, "cannot determine which variant to initialize from argument type `%s`", + argType ? argType.toErrMsg() : "unknown"); + return setError(); + } + } + else + { + error(exp.loc, "sumtype constructor requires exactly one argument"); + return setError(); + } + + // Generate StructLiteralExp: + // elements[0] = IntegerExp(variantIdx) (the tag field) + // elements[variantIdx+1] = the argument value + auto elements = new Expressions(sd.fields.length); + elements.zero; + (*elements)[0] = new IntegerExp(exp.loc, variantIdx, sd.fields[0].type); + + // Set the variant field + auto arg = (*exp.arguments)[0]; + (*elements)[variantIdx + 1] = arg; + + auto sle = new StructLiteralExp(exp.loc, sd, elements, sd.type); + result = sle.expressionSemantic(sc); + return; + } + } + if (t1.ty == Tstruct) { auto sd = (cast(TypeStruct)t1).sym; @@ -7856,6 +7980,7 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor return setError(); if (!sd.ctor) sd.ctor = sd.searchCtor(); + /* If `sd.ctor` is a generated copy constructor, this means that it is the single constructor that this struct has. In order to not disable default construction, the ctor is nullified. The side effect @@ -9209,6 +9334,25 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor tded = (cast(TypeVector)e.targ).basetype; break; + case TOK.sumtype_: + // is(T == __sumtype) is true when T is a sumtype, whether it is + // referred to by its source `__sumtype(...)` type or by its + // lowered struct form. + if (e.targ.isTypeSumType()) + { + tded = e.targ; + break; + } + if (auto tsa = e.targ.isTypeStruct()) + { + if (tsa.sym && tsa.sym.sumtype !is null) + { + tded = e.targ; + break; + } + } + return no(); + default: assert(0); } @@ -12069,6 +12213,10 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor } Expression e1old = exp.e1; + // Save auto-tag info before e1 is resolved + DotIdExp autoTagDie = null; + if (auto die = e1old.isDotIdExp()) + autoTagDie = die; if (auto e2comma = exp.e2.isCommaExp()) { @@ -12470,6 +12618,49 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor auto e2x = exp.e2; auto sd = (cast(TypeStruct)t1).sym; + /* + We need to handle the following cases: + exp.op == EXP.assign + exp.op == EXP.construct + */ + + // Sumtype assignment: lower to direct field copies via CondExp chain + // Works for both same-type and cross-sumtype assignments. + // Avoids MatchExp/opAssign temporaries that cause extra copies/destructors. + // We don't execute this code for blit's, this is a byte-by-byte move. + if (sd.sumtype && e2x.type && exp.op != EXP.blit) + { + auto t2 = e2x.type.toBasetype(); + StructDeclaration sd2; + if (t2.ty == Tsumtype) + sd2 = t2.isTypeSumType().loweredStruct; + else if (t2.ty == Tstruct) + sd2 = (cast(TypeStruct)t2).sym; + + // We need to handle the case where lhs != rhs types + // Basically we gotta convert rhs into the same type as lhs + if (sd2 && sd2.sumtype && sd !is sd2) + { + auto srcTs = sd2.sumtype; + auto dstTs = sd.sumtype; + auto loc = exp.loc; + + // Reuse the shared sumtype widening lowering. It builds the + // CondExp chain that dispatches on the source tag and copies + // the active variant field into a freshly constructed value + // of the target sumtype — the same lowering used for implicit + // widening in return values and function call arguments. + if (auto widened = sumtypeWidenExpression(e2x, sc, t1)) + e2x = widened; + else + { + .error(loc, "cannot assign `%s` to `%s`: not all source variants are covered", + srcTs.toChars(), dstTs.toChars()); + return setError(); + } + } + } + if (exp.op == EXP.construct) { Type t2 = e2x.type.toBasetype(); @@ -13340,6 +13531,54 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor ce.e2 = lowerArrayAssign(ae2, true); } + // Auto-tag: if assigning to a named variant field of a sumtype, + // prepend a tag assignment via comma expression. + // e.g. n.x = 42 => (n.tag = index, n.x = 42) + if (autoTagDie) + { + // Look up the struct from the VarExp's type + if (auto ve = autoTagDie.e1.isVarExp()) + { + auto varType = ve.type; + if (varType) + varType = varType.toBasetype(); + StructDeclaration sd = null; + if (varType) + { + if (auto ts = varType.isTypeSumType()) + sd = ts.loweredStruct; + else if (auto tsa = varType.isTypeStruct()) + sd = tsa.sym; + } + if (sd !is null) + { + if (auto ts = sd.sumtype) + { + foreach (j, name; (*ts.variantInfos)) + { + if (name.name !is null && autoTagDie.ident == name.name) + { + // Get the resolved receiver from exp.e1 + Expression tagObj; + + if (auto dve = exp.e1.isDotVarExp()) + tagObj = dve.e1; + else + tagObj = ve; + + auto tagLhs = new DotIdExp(exp.loc, tagObj, Id.tag); + auto tagRhs = new IntegerExp(exp.loc, cast(int)j, sd.fields[0].type); + auto tagAssign = new AssignExp(exp.loc, tagLhs, tagRhs); + + res = Expression.combine(tagAssign.expressionSemantic(sc), res); + break; + } + } + } + } + } + } + return setResult(res); } @@ -15610,6 +15849,347 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor result = exps[imatch]; } + override void visit(MatchExp exp) + { + static if (LOGSEMANTIC) + { + printf("MatchExp::semantic('%s')\n", exp.toErrMsg()); + } + + // Step 1 — Validate scrutinee + exp.arg = exp.arg.expressionSemantic(sc).arrayFuncConv(sc); + if (exp.arg.isErrorExp()) + return setError(); + + auto scrutineeType = exp.arg.type; + if (scrutineeType is null) + { + error(exp.loc, "cannot match on expression type `%s`", exp.arg.toChars()); + return setError(); + } + + auto ts = scrutineeType.isTypeSumType(); + if (ts is null) + { + auto tsa = scrutineeType.isTypeStruct(); + if (tsa is null || tsa.sym is null || tsa.sym.sumtype is null) + { + error(exp.loc, ".match requires a sumtype, not `%s`", scrutineeType.toErrMsg()); + return setError(); + } + } + + StructDeclaration sd; + SumTypeVariantInfos* variantInfos; + if (ts) + { + sd = ts.loweredStruct; + variantInfos = ts.variantInfos; + } + else + { + auto tsa = scrutineeType.isTypeStruct(); + sd = tsa.sym; + auto ts2 = sd.sumtype; + variantInfos = ts2.variantInfos; + } + + if ((*variantInfos).length == 0) + { + error(exp.loc, "cannot match on empty sumtype"); + return setError(); + } + + // Step 2 — Classify arms: fill in variantIndex for each MatchArmInfo + int wildcardIndex = -1; + + foreach (i, ref ai; *exp.armInfos) + { + auto vd = ai.vd; + if (vd.type is null) + { + // Typeless (catch-all) + wildcardIndex = cast(int)i; + ai.variantIndex = -2; + continue; + } + + // Typed arm: match by name against variant names, fall back to positional + bool found = false; + if ((*variantInfos).length > 0 && vd.ident !is null) + { + foreach (j, vi; (*variantInfos)) + { + if (vi.name !is null && vi.name.toString() == vd.ident.toString()) + { + ai.variantIndex = cast(int)j; + found = true; + break; + } + } + } + + if (!found) + { + // Positional fallback: match arm type against variant type + if (vd.type !is null) + { + foreach (j, vi; (*variantInfos)) + { + if (vd.type.ty == vi.type.ty) + { + ai.variantIndex = cast(int)j; + found = true; + break; + } + } + } + if (!found) + ai.variantIndex = cast(int)i; + } + } + + // Step 3 — Sort armInfos by variantIndex (catch-all last, unclassified after that) + // For same variant: guarded arms first, unguarded last (so inside-out builds correctly) + // Within same variant and guard status: preserve source order + static int armInfoCmp(const SumTypeMatchArmInfo* a, const SumTypeMatchArmInfo* b) @safe + { + // catch-all (-2) sorts last + if (a.variantIndex == -2) return 1; + if (b.variantIndex == -2) return -1; + // unclassified (-1) sorts after catch-all + if (a.variantIndex == -1) return 1; + if (b.variantIndex == -1) return -1; + // normal: sort by variant index + if (a.variantIndex < b.variantIndex) return -1; + if (a.variantIndex > b.variantIndex) return 1; + // same variant: guarded arms first, unguarded last + if (a.guard !is null && b.guard is null) return -1; + if (a.guard is null && b.guard !is null) return 1; + // same variant, same guard status: preserve source order + return a.originalIndex - b.originalIndex; + } + exp.armInfos.sort!(armInfoCmp); + + // Step 4 — Exhaustiveness check + // Only unguarded typed arms count toward coverage + int numCoveredByTyped = 0; + foreach (ref ai; *exp.armInfos) + { + if (ai.variantIndex >= 0 && ai.guard is null) + numCoveredByTyped++; + } + int numUncovered = cast(int)(*variantInfos).length - numCoveredByTyped; + + if (numUncovered == 0 && wildcardIndex >= 0) + { + error(exp.loc, "redundant catch-all in match expression"); + return setError(); + } + if (numUncovered > 0 && wildcardIndex < 0) + { + error(exp.loc, "non-exhaustive match, missing variant for type `%s`", (*variantInfos)[numCoveredByTyped].type.toChars()); + return setError(); + } + + // Step 5 — Build per-variant expressions + auto variantExprs = new Expression[]((*variantInfos).length); + + // Handle scrutinee side effects: if arg has side effects, use a temp + Expression scrutRef = exp.arg; + Expression declPrefix = null; + if (exp.arg.hasSideEffect()) + { + auto scrutTmp = copyToTemp(STC.ref_, "__matchScrut", exp.arg); + scrutTmp.dsymbolSemantic(sc); + declPrefix = new DeclarationExp(exp.loc, scrutTmp); + scrutRef = new VarExp(exp.loc, scrutTmp); + scrutRef.type = exp.arg.type; + } + + // Analyze the tag expression up front + auto tag = new DotVarExp(exp.loc, scrutRef, sd.fields[0]); + tag.type = sd.fields[0].type; + auto tagResolved = tag.expressionSemantic(sc); + + Type resultType = null; + + // Substitute arm parameter name with a VarExp in an expression tree + Expression substitute(Expression e, Identifier paramName, VarExp replacement) + { + if (e is null) return null; + if (auto ie = e.isIdentifierExp()) + { + if (paramName !is null && ie.ident == paramName) + return replacement; + } + // CondExp must be checked before BinExp (CondExp extends BinExp) + if (auto ce = e.isCondExp()) + { + ce.econd = substitute(ce.econd, paramName, replacement); + ce.e1 = substitute(ce.e1, paramName, replacement); + ce.e2 = substitute(ce.e2, paramName, replacement); + return e; + } + if (auto ce = e.isCommaExp()) + { + ce.e1 = substitute(ce.e1, paramName, replacement); + ce.e2 = substitute(ce.e2, paramName, replacement); + return e; + } + if (auto be = e.isBinExp()) + { + be.e1 = substitute(be.e1, paramName, replacement); + be.e2 = substitute(be.e2, paramName, replacement); + return e; + } + if (auto ce = e.isCallExp()) + { + ce.e1 = substitute(ce.e1, paramName, replacement); + if (ce.arguments) + { + foreach (ref arg; *ce.arguments) + arg = substitute(arg, paramName, replacement); + } + return e; + } + if (auto ue = e.isUnaExp()) + { + ue.e1 = substitute(ue.e1, paramName, replacement); + return e; + } + if (auto ae = e.isCastExp()) + { + ae.e1 = substitute(ae.e1, paramName, replacement); + return e; + } + return e; + } + + // For each variant, find all arms that match it (may be multiple if guards are present) + // and build a nested CondExp chain for guard fallthrough. + // Build inside-out: last arm is the base, earlier arms wrap it in CondExp. + foreach (vi; 0 .. (*variantInfos).length) + { + // Collect all arms for this variant in source order + // Typed arms matching this variant, plus wildcard as fallback + SumTypeMatchArmInfo[] matchingArms; + foreach (ref ai; *exp.armInfos) + { + if (ai.variantIndex == vi) + matchingArms ~= ai; + } + + // If no typed arm matched, use the wildcard (catch-all) arm + if (matchingArms.length == 0 && wildcardIndex >= 0) + matchingArms ~= (*exp.armInfos)[wildcardIndex]; + + // Also append catch-all as final fallthrough when all typed arms are guarded + // (when a guard fails, execution should fall through to the catch-all) + if (wildcardIndex >= 0 && matchingArms.length > 0 && + matchingArms[$ - 1].variantIndex != -2) + { + // Only if all matching arms are guarded (none unguarded) + bool allGuarded = true; + foreach (ref mai; matchingArms) + { + if (mai.guard is null) + { + allGuarded = false; + break; + } + } + if (allGuarded) + matchingArms ~= (*exp.armInfos)[wildcardIndex]; + } + + if (matchingArms.length == 0) + continue; + + // Build from last arm to first (inside-out) + Expression fallthrough = null; + + for (int i = cast(int)matchingArms.length - 1; i >= 0; i--) + { + auto armVD = matchingArms[i].vd; + + // Create DotVarExp for scrut.field_i + auto variantField = sd.fields[vi + 1]; // +1 for tag + auto scrutVariant = new DotVarExp(exp.loc, scrutRef, variantField); + scrutVariant.compilerOverlappedAccess = true; + scrutVariant.type = variantField.type; + + // Create VarDeclaration for this branch — use generateId for unique naming + auto branchType = armVD.type !is null ? armVD.type : variantField.type; + auto branchVD = new VarDeclaration(exp.loc, branchType, Identifier.generateId("__matchArm"), + new ExpInitializer(exp.loc, scrutVariant), armVD.storage_class | STC.ctfe); + + // Build: (branchVD = scrutVariant, substitutedBody) + auto armBody = armVD._init.isExpInitializer().exp; + auto branchVar = new VarExp(exp.loc, branchVD); + branchVar.type = branchType; + + auto substitutedBody = substitute(armBody, armVD.ident, branchVar); + auto declExp = new DeclarationExp(exp.loc, branchVD); + + if (matchingArms[i].guard !is null) + { + // Substitute parameter name in guard too + auto substitutedGuard = substitute(matchingArms[i].guard, armVD.ident, branchVar); + // Guard present: CommaExp(decl, CondExp(guard, body, fallthrough)) + fallthrough = new CommaExp(exp.loc, declExp, + new CondExp(exp.loc, substitutedGuard, substitutedBody, + fallthrough !is null ? fallthrough : substitutedBody)); + } + else + { + // No guard: CommaExp(decl, body) — always matches, becomes new base + fallthrough = new CommaExp(exp.loc, declExp, substitutedBody); + } + } + + // Analyze the complete tree in a pushed scope + auto ss = new ScopeDsymbol(); + auto branchSc = sc.push(ss); + auto resolved = fallthrough.expressionSemantic(branchSc); + branchSc.pop(); + + if (resolved.isErrorExp()) + return setError(); + + if (resultType is null) + resultType = resolved.type; + + variantExprs[vi] = resolved; + } + + // Step 6 — Build CondExp chain (right-folded) from already-resolved branches + Expression chain = variantExprs[(*variantInfos).length - 1]; + + for (size_t i = (*variantInfos).length - 1; i > 0;) + { + i--; + if (variantExprs[i] is null) + continue; + auto cmp = new EqualExp(EXP.equal, exp.loc, tagResolved, new IntegerExp(exp.loc, i, tagResolved.type)); + cmp.type = Type.tbool; + auto cond = new CondExp(exp.loc, cmp, variantExprs[i], chain); + cond.type = resultType; + chain = cond; + } + + // Prepend scrutinee temp declaration if needed + if (declPrefix !is null) + { + auto ce = new CommaExp(exp.loc, declPrefix, chain); + ce.type = resultType; + chain = ce; + } + + // Step 7 — Return lowered expression + result = chain; + } + override void visit(DefaultInitExp e) { e.type = e.tok == TOK.line ? Type.tint32 : Type.tstring; @@ -19997,3 +20577,158 @@ BitFieldDeclaration isBitField(Expression e) return null; } + +/*************************************** + * Build an expression that widens a sumtype value `e` into the wider + * sumtype type `to`. + * + * Every variant of `e`'s type must be a variant of `to`. The result + * reads `e.tag` and, depending on its value, copies the matching variant + * field into a freshly constructed value of `to`. + * + * Params: + * e = source expression (a sumtype value) + * sc = scope + * to = destination sumtype type (either the `__sumtype(...)` form or its + * lowered struct form) + * + * Returns: + * The widened expression, or null if the widening is not applicable. + */ +Expression sumtypeWidenExpression(Expression e, Scope* sc, Type to) +{ + Type toB = to.toBasetype(); + TypeSumType toTs; + StructDeclaration toSd; + if (auto ts = toB.isTypeSumType()) + { + toTs = ts; + toSd = ts.loweredStruct; + } + else if (auto tsa = toB.isTypeStruct()) + { + toSd = tsa.sym; + toTs = toSd.sumtype; + } + if (!toTs || !toSd) + return null; + + Type fromB = e.type.toBasetype(); + TypeSumType fromTs; + StructDeclaration fromSd; + if (auto ts = fromB.isTypeSumType()) + { + fromTs = ts; + fromSd = ts.loweredStruct; + } + else if (auto tsa = fromB.isTypeStruct()) + { + fromSd = tsa.sym; + fromTs = fromSd.sumtype; + } + if (!fromTs || !fromSd || fromTs == toTs || fromSd == toSd) + return null; + + // Validate: every source variant must have a matching target variant. + // + // This is two-pass: first look for an exact type match, then fall back to + // an implicit conversion. This is deliberate — with a single + // "equals || implicitConvTo" check, a bool source variant would match an + // int target variant (because bool implicitly converts to int), corrupting + // the active variant during widening. Preferring the exact match preserves + // which variant is active. + foreach (srcVI; *fromTs.variantInfos) + { + bool found = false; + foreach (dstVI; *toTs.variantInfos) + { + if (srcVI.type.equals(dstVI.type)) + { + found = true; + break; + } + } + if (!found) + { + foreach (dstVI; *toTs.variantInfos) + { + if (srcVI.type.implicitConvTo(dstVI.type) != MATCH.nomatch) + { + found = true; + break; + } + } + } + if (!found) + return null; + } + + auto loc = e.loc; + + Expression rhsRef, rhsRefVarsDecl; + if (e.isVarExp is null) + { + VarDeclaration rhsRefVar = copyToTemp(STC.ref_, "__sumtypewiden", e); + rhsRefVarsDecl = new DeclarationExp(loc, rhsRefVar); + rhsRef = new VarExp(loc, rhsRefVar); + } + else + rhsRef = e; + + auto srcTagDecl = fromSd.fields[0].isDeclaration; + Expression chain = new HaltExp(loc); + + foreach_reverse (i, srcVI; *fromTs.variantInfos) + { + // Find the matching target variant. + // + // This is two-pass: first look for an exact type match, then fall back + // to an implicit conversion. This is deliberate — with a single + // "equals || implicitConvTo" check, a bool source variant would map to + // an int target variant (because bool implicitly converts to int), + // corrupting the active variant during widening. Preferring the exact + // match preserves which variant is active. + int targetIdx = -1; + foreach (j, dstVI; *toTs.variantInfos) + { + if (srcVI.type.equals(dstVI.type)) + { + targetIdx = cast(int)j; + break; + } + } + if (targetIdx == -1) + { + foreach (j, dstVI; *toTs.variantInfos) + { + if (srcVI.type.implicitConvTo(dstVI.type) != MATCH.nomatch) + { + targetIdx = cast(int)j; + break; + } + } + } + assert(targetIdx != -1); + + // Build target struct literal with tag = targetIdx, field = src.__vi + auto toConstructArgs = new Expressions(toSd.fields.length); + toConstructArgs.zero; + (*toConstructArgs)[0] = new IntegerExp(loc, targetIdx, toSd.fields[0].type); + + auto srcFieldDecl = fromSd.fields[i + 1].isDeclaration; + auto srcFieldExp = new DotVarExp(loc, rhsRef, srcFieldDecl); + srcFieldExp.compilerOverlappedAccess = true; + (*toConstructArgs)[targetIdx + 1] = srcFieldExp.expressionSemantic(sc); + + Expression buildExp = new StructLiteralExp(loc, toSd, toConstructArgs, toSd.type); + buildExp = buildExp.expressionSemantic(sc); + + auto cond = new EqualExp(EXP.equal, loc, + new DotVarExp(loc, rhsRef, srcTagDecl), + new IntegerExp(loc, i, fromSd.fields[0].type)); + + chain = new CondExp(loc, cond, buildExp, chain); + } + + return Expression.combine(rhsRefVarsDecl, chain).expressionSemantic(sc); +} diff --git a/compiler/src/dmd/hdrgen.d b/compiler/src/dmd/hdrgen.d index c180e8de37ed..f66aeb164614 100644 --- a/compiler/src/dmd/hdrgen.d +++ b/compiler/src/dmd/hdrgen.d @@ -3149,6 +3149,36 @@ private void expressionPrettyPrint(Expression e, ref OutBuffer buf, ref HdrGenSt case EXP.question: return visitCond(e.isCondExp()); case EXP.classReference: return visitClassReference(e.isClassReferenceExp()); case EXP.loweredAssignExp: return visitLoweredAssignExp(e.isLoweredAssignExp()); + case EXP.matchExp: + { + auto me = e.isMatchExp(); + me.arg.expressionToBuffer(buf, hgs); + buf.put(".match {"); + foreach (i, ref ai; *me.armInfos) + { + if (i > 0) + buf.put(','); + buf.put(" ("); + if (ai.vd.storage_class & STC.ref_) + buf.put("ref "); + if (ai.vd.type) + typeToBuffer(ai.vd.type, ai.vd.ident, buf, hgs); + else if (ai.vd.ident) + buf.put(ai.vd.ident.toString()); + buf.put(")"); + if (ai.guard) + { + buf.put(" if ("); + ai.guard.expressionToBuffer(buf, hgs); + buf.put(")"); + } + buf.put(" => "); + if (ai.vd._init && ai.vd._init.isExpInitializer()) + ai.vd._init.isExpInitializer().exp.expressionToBuffer(buf, hgs); + } + buf.put(" }"); + return; + } } } @@ -4566,6 +4596,18 @@ private void typeToBufferx(Type t, ref OutBuffer buf, ref HdrGenState hgs) } } + void visitSumType(TypeSumType t) + { + buf.put("__sumtype("); + foreach (i, vi; (*t.variantInfos)[]) + { + if (i > 0) + buf.put(" | "); + visitWithMask(vi.type, t.mod, buf, hgs); + } + buf.put(')'); + } + void visitTuple(TypeTuple t) { parametersToBuffer(ParameterList(t.arguments, VarArg.none), buf, hgs); @@ -4633,6 +4675,7 @@ private void typeToBufferx(Type t, ref OutBuffer buf, ref HdrGenState hgs) case Tmixin: return visitMixin(cast(TypeMixin)t); case Tnoreturn: return visitNoreturn(cast(TypeNoreturn)t); case Ttag: return visitTag(cast(TypeTag)t); + case Tsumtype: return visitSumType(cast(TypeSumType)t); } } @@ -4686,6 +4729,7 @@ string EXPtoString(EXP op) EXP.void_ : "void", EXP.vectorArray : "vectorarray", EXP._Generic : "_Generic", + EXP.matchExp : "match", // post EXP.dotTemplateInstance : "dotti", diff --git a/compiler/src/dmd/id.d b/compiler/src/dmd/id.d index 8e5a030d40b9..8440c9d9419e 100644 --- a/compiler/src/dmd/id.d +++ b/compiler/src/dmd/id.d @@ -137,8 +137,10 @@ immutable Msgtable[] msgtable = { "_assert", "assert" }, { "_unittest", "unittest" }, { "_body", "body" }, + { "match" }, { "printf" }, { "scanf" }, + { "tag" }, { "TypeInfo" }, { "TypeInfo_Class" }, @@ -507,7 +509,6 @@ immutable Msgtable[] msgtable = // for C compiler { "ImportC", "__C" }, - { "__tag" }, { "dllimport" }, { "dllexport" }, { "naked" }, diff --git a/compiler/src/dmd/impcnvtab.d b/compiler/src/dmd/impcnvtab.d index 767b7b239588..2aea4badd593 100644 --- a/compiler/src/dmd/impcnvtab.d +++ b/compiler/src/dmd/impcnvtab.d @@ -114,6 +114,7 @@ ImpCnvTab generateImpCnvTab() Tmixin, Tnoreturn, Ttag, + Tsumtype, ]; ImpCnvTab impCnvTab; diff --git a/compiler/src/dmd/mangle/basic.d b/compiler/src/dmd/mangle/basic.d index f50faaa9b5a3..732174a7d88b 100644 --- a/compiler/src/dmd/mangle/basic.d +++ b/compiler/src/dmd/mangle/basic.d @@ -82,6 +82,7 @@ immutable char[TMAX] mangleChar = Tmixin : '@', Ttag : '@', Tnoreturn : '@', // becomes 'Nn' + Tsumtype : '@', ]; unittest diff --git a/compiler/src/dmd/mtype.d b/compiler/src/dmd/mtype.d index 09e742b164a1..60855dd55d46 100644 --- a/compiler/src/dmd/mtype.d +++ b/compiler/src/dmd/mtype.d @@ -520,6 +520,7 @@ extern (C++) abstract class Type : ASTNode sizeTy[Tmixin] = __traits(classInstanceSize, TypeMixin); sizeTy[Tnoreturn] = __traits(classInstanceSize, TypeNoreturn); sizeTy[Ttag] = __traits(classInstanceSize, TypeTag); + sizeTy[Tsumtype] = __traits(classInstanceSize, TypeSumType); return sizeTy; }(); @@ -801,6 +802,7 @@ extern (C++) abstract class Type : ASTNode inout(TypeTraits) isTypeTraits() { return ty == Ttraits ? cast(typeof(return))this : null; } inout(TypeNoreturn) isTypeNoreturn() { return ty == Tnoreturn ? cast(typeof(return))this : null; } inout(TypeTag) isTypeTag() { return ty == Ttag ? cast(typeof(return))this : null; } + inout(TypeSumType) isTypeSumType() { return ty == Tsumtype ? cast(typeof(return))this : null; } extern (D) bool isStaticOrDynamicArray() const { return ty == Tarray || ty == Tsarray; } } @@ -2176,6 +2178,47 @@ extern (C++) final class TypeTag : Type } } +/*********************************************************** + */ + +struct SumTypeVariantInfo +{ + Type type; + Identifier name; + Expressions* udas; + const(char)* comment; +} + +extern (C++) final class TypeSumType : Type +{ + SumTypeVariantInfos* variantInfos; + StructDeclaration loweredStruct; /// lowered struct representation + size_t defaultVariantIdx; /// index of the default variant for .init + + extern (D) this(SumTypeVariantInfos* variantInfos) @safe + { + super(Tsumtype); + this.variantInfos = variantInfos; + } + + + override const(char)* kind() const + { + return "sumtype"; + } + + override TypeSumType syntaxCopy() + { + // No semantic analysis done, no need to copy + return this; + } + + override void accept(Visitor v) + { + v.visit(this); + } +} + /*********************************************************** * Represents a function's formal parameters + variadics info. * Length, indexing and iteration are based on a depth-first tuple expansion. @@ -2869,6 +2912,7 @@ mixin template VisitType(Result) case TY.Tmixin: mixin(visitTYCase("Mixin")); case TY.Tnoreturn: mixin(visitTYCase("Noreturn")); case TY.Ttag: mixin(visitTYCase("Tag")); + case TY.Tsumtype: mixin(visitTYCase("SumType")); case TY.Tnone: assert(0); } } diff --git a/compiler/src/dmd/parse.d b/compiler/src/dmd/parse.d index 49674ec5216e..e8a3a280a585 100644 --- a/compiler/src/dmd/parse.d +++ b/compiler/src/dmd/parse.d @@ -481,6 +481,7 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer case TOK.class_: case TOK.interface_: case TOK.traits: + case TOK.sumtype_: Ldeclaration: a = parseDeclarations(false, pAttrs, pAttrs.comment); if (a && a.length) @@ -4093,6 +4094,10 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer check(TOK.rightParenthesis); break; + case TOK.sumtype_: + t = parseSumType(); + break; + default: error("basic type expected, not `%s`", token.toChars()); if (token.value == TOK.else_) @@ -4103,6 +4108,228 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer return t; } + /******************************** + * Parse __sumtype(Type | Type | ...) as a type + * Parser is sitting on the `__sumtype` token. + */ + private AST.Type parseSumType() + { + nextToken(); + check(TOK.leftParenthesis, "`__sumtype`"); + + auto variants = new AST.Types(); + + // Parse first type + if (token.value == TOK.rightParenthesis) + { + error("at least one type expected in `__sumtype(...)`"); + nextToken(); + return AST.Type.terror; + } + + AST.SumTypeVariantInfos variantInfos; + + AST.Expressions* firstUdas; + while (token.value == TOK.at) + { + AST.Expressions* udaList; + parseAttribute(udaList); + if (firstUdas is null) + firstUdas = udaList; + else if (udaList !is null) + firstUdas = AST.UserAttributeDeclaration.concat(firstUdas, udaList); + } + + // Capture ddoc comment before first variant + const(char)* firstComment = token.blockComment.ptr; + Identifier firstName = parseSumTypeVariant(variants); + + AST.SumTypeVariantInfo firstInfo; + firstInfo.type = (*variants)[variants.length - 1]; + firstInfo.name = firstName; + firstInfo.udas = firstUdas; + firstInfo.comment = firstComment; + variantInfos.push(firstInfo); + + // Parse remaining types separated by `|` + while (token.value == TOK.or) + { + nextToken(); + AST.Expressions* udas; + while (token.value == TOK.at) + { + AST.Expressions* udaList; + parseAttribute(udaList); + if (udas is null) + udas = udaList; + else if (udaList !is null) + udas = AST.UserAttributeDeclaration.concat(udas, udaList); + } + + // Capture ddoc comment before this variant + const(char)* vcomment = token.blockComment.ptr; + auto name = parseSumTypeVariant(variants); + + AST.SumTypeVariantInfo info; + info.type = (*variants)[variants.length - 1]; + info.name = name; + info.udas = udas; + info.comment = vcomment; + variantInfos.push(info); + } + + check(TOK.rightParenthesis, "`__sumtype`"); + + auto ts = new AST.TypeSumType(new AST.SumTypeVariantInfos(variantInfos[])); + return ts; + } + + /******************************** + * Parse a single variant: Type [name] or (Type name) + */ + private Identifier parseSumTypeVariant(AST.Types* variants) + { + Identifier name; + + // Support bracketed form: (Type name) + if (token.value == TOK.leftParenthesis) + { + nextToken(); + auto t = parseBasicType(); + t = parseTypeSuffixes(t); + variants.push(t); + + if (token.value == TOK.identifier) + { + name = token.ident; + nextToken(); + } + check(TOK.rightParenthesis, "named variant"); + return name; + } + + // Bracketless form: Type [name] + auto t = parseBasicType(); + t = parseTypeSuffixes(t); + variants.push(t); + + // Optional name: if next token is identifier, treat as variant name + if (token.value == TOK.identifier) + { + name = token.ident; + nextToken(); + } + return name; + } + + /******************************** + * Parse sumtype block form: __sumtype S { Type name, Type name, ... } + */ + private AST.Dsymbols* parseSumTypeDeclarations(const(char)* comment, STC storage_class) + { + const loc = token.loc; + nextToken(); // consume `__sumtype` + + if (token.value != TOK.identifier) + { + error("identifier expected following `__sumtype`"); + return new AST.Dsymbols(); + } + + Identifier id = token.ident; + nextToken(); + + // Optional template parameter list: + // __sumtype S(Types...) = Types | bool; + AST.TemplateParameters* tpl = null; + if (token.value == TOK.leftParenthesis) + tpl = parseTemplateParameterList(); + + // Form 2: direct form __sumtype S = Type | Type; + // or: __sumtype S = Type name | Type; + // or: __sumtype S = (Type name) | (Type name); + if (token.value == TOK.assign) + { + nextToken(); + auto variants = new AST.Types(); + + AST.SumTypeVariantInfos variantInfos; + + AST.Expressions* firstUdas; + while (token.value == TOK.at) + { + AST.Expressions* udaList; + parseAttribute(udaList); + if (firstUdas is null) + firstUdas = udaList; + else if (udaList !is null) + firstUdas = AST.UserAttributeDeclaration.concat(firstUdas, udaList); + } + + // Capture ddoc comment before first variant + const(char)* firstComment = token.blockComment.ptr; + Identifier firstName = parseSumTypeVariant(variants); + + AST.SumTypeVariantInfo firstInfo; + firstInfo.type = (*variants)[variants.length - 1]; + firstInfo.name = firstName; + firstInfo.udas = firstUdas; + firstInfo.comment = firstComment; + variantInfos.push(firstInfo); + + while (token.value == TOK.or) + { + nextToken(); + AST.Expressions* udas; + while (token.value == TOK.at) + { + AST.Expressions* udaList; + parseAttribute(udaList); + if (udas is null) + udas = udaList; + else if (udaList !is null) + udas = AST.UserAttributeDeclaration.concat(udas, udaList); + } + + // Capture ddoc comment before this variant + const(char)* vcomment = token.blockComment.ptr; + auto name = parseSumTypeVariant(variants); + + AST.SumTypeVariantInfo info; + info.type = (*variants)[variants.length - 1]; + info.name = name; + info.udas = udas; + info.comment = vcomment; + variantInfos.push(info); + } + + auto ts = new AST.TypeSumType(new AST.SumTypeVariantInfos(variantInfos[])); + auto ad = new AST.AliasDeclaration(loc, id, ts); + AST.Dsymbol s = ad; + if (tpl) + { + // __sumtype S(Types...) = Types | bool; is a template + auto a2 = new AST.Dsymbols(); + a2.push(ad); + s = new AST.TemplateDeclaration(loc, id, tpl, null, a2); + } + auto a = new AST.Dsymbols(); + a.push(s); + if (storage_class) + { + auto scd = new AST.StorageClassDeclaration(storage_class, a); + a = new AST.Dsymbols(); + a.push(scd); + } + check(TOK.semicolon, "`__sumtype` declaration"); + addComment(s, comment); + return a; + } + + error("expected `=` following `__sumtype %s`", id.toChars()); + return new AST.Dsymbols(); + } + private AST.Type parseBasicTypeStartingAt(AST.TypeQualified tid, bool dontLookDotIdents) { AST.Type maybeArray = null; @@ -4762,6 +4989,10 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer addComment(d, comment); return a; } + if (token.value == TOK.sumtype_) + { + return parseSumTypeDeclarations(comment, storage_class); + } if (token.value == TOK.struct_ || token.value == TOK.union_ || token.value == TOK.class_ || @@ -6409,6 +6640,7 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer case TOK.union_: case TOK.class_: case TOK.interface_: + case TOK.sumtype_: Ldeclaration: { AST.Dsymbols* a = parseDeclarations(false, null, null); @@ -8863,9 +9095,10 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer || token.value == TOK.const_ && peekNext() == TOK.rightParenthesis || token.value == TOK.immutable_ && peekNext() == TOK.rightParenthesis || token.value == TOK.shared_ && peekNext() == TOK.rightParenthesis - || token.value == TOK.inout_ && peekNext() == TOK.rightParenthesis || token.value == TOK.function_ + || token.value == TOK.inout_ && peekNext() == TOK.rightParenthesis || token.value == TOK.function_ || token.value == TOK.delegate_ || token.value == TOK.return_ - || (token.value == TOK.vector && peekNext() == TOK.rightParenthesis))) + || (token.value == TOK.vector && peekNext() == TOK.rightParenthesis) + || token.value == TOK.sumtype_)) { tok2 = token.value; nextToken(); @@ -9283,10 +9516,11 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer case TOK.complex80: case TOK.void_: { - // (type) una_exp + // (type) una_exp nextToken(); // Note: `t` may be an expression that looks like a type auto t = parseType(); + t = parseTypeSuffixes(t); check(TOK.rightParenthesis); // if .identifier @@ -9368,6 +9602,99 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer { case TOK.dot: nextToken(); + if (token.value == TOK.identifier && token.ident is Id.match && peekNext() == TOK.leftCurly) + { + nextToken(); // consume 'match' + check(TOK.leftCurly, "`match`"); + + AST.SumTypeMatchArmInfos armInfos; + + while (token.value != TOK.rightCurly && token.value != TOK.endOfFile) + { + // Parse (Type id) [if (guard)] => expr or (id) [if (guard)] => expr (catch-all) + // or (ref Type id) [if (guard)] => expr or (ref id) [if (guard)] => expr + if (token.value == TOK.leftParenthesis) + { + nextToken(); + + STC storageClass = STC.none; + + // Check for ref storage class + if (token.value == TOK.ref_) + { + storageClass |= STC.ref_; + nextToken(); + } + + AST.Type paramType; + Identifier paramName; + + // Determine if typeless (catch-all) or typed: + // identifier followed by ')' is typeless (catch-all) + if (token.value == TOK.identifier && peekNext() == TOK.rightParenthesis) + { + paramType = null; + paramName = token.ident; + nextToken(); + } + else + { + paramType = parseBasicType(); + paramType = parseTypeSuffixes(paramType); + paramName = null; + if (token.value == TOK.identifier) + { + paramName = token.ident; + nextToken(); + } + } + + check(TOK.rightParenthesis, "match arm parameter"); + + // Parse optional guard: if (expr) + AST.Expression guardExpr = null; + if (token.value == TOK.if_) + { + nextToken(); + check(TOK.leftParenthesis, "`if` condition in match arm"); + guardExpr = parseAssignExp(); + check(TOK.rightParenthesis, "`if` condition in match arm"); + } + + if (token.value != TOK.goesTo) + { + error("expected `=>` in match arm"); + break; + } + nextToken(); + + AST.Expression bodyExpr = parseAssignExp(); + + auto vd = new AST.VarDeclaration(loc, paramType, paramName, + new AST.ExpInitializer(loc, bodyExpr), storageClass); + AST.SumTypeMatchArmInfo ai; + ai.vd = vd; + ai.guard = guardExpr; + ai.variantIndex = -1; + ai.originalIndex = cast(int)armInfos.length; + armInfos.push(ai); + } + else + { + error("expected `(Type id)` in match arm"); + break; + } + + if (token.value == TOK.comma) + nextToken(); + else + break; + } + + check(TOK.rightCurly, "`match`"); + e = new AST.MatchExp(loc, e, new AST.SumTypeMatchArmInfos(armInfos[])); + continue; + } if (token.value == TOK.identifier) { Identifier id = token.ident; @@ -10187,6 +10514,8 @@ immutable PREC[EXP.max + 1] precedence = EXP.declaration : PREC.expr, EXP.interval : PREC.assign, + + EXP.matchExp : PREC.primary, ]; enum ParseStatementFlags : int diff --git a/compiler/src/dmd/res/default_ddoc_theme.ddoc b/compiler/src/dmd/res/default_ddoc_theme.ddoc index 20269e1a084d..8b5f0abeb960 100644 --- a/compiler/src/dmd/res/default_ddoc_theme.ddoc +++ b/compiler/src/dmd/res/default_ddoc_theme.ddoc @@ -629,6 +629,7 @@ DDOC_CLASS_MEMBERS = $(DDOC_MEMBERS $0)$(LF) DDOC_STRUCT_MEMBERS = $(DDOC_MEMBERS $0)$(LF) DDOC_ENUM_MEMBERS = $(DDOC_MEMBERS $0)$(LF) DDOC_TEMPLATE_MEMBERS = $(DDOC_MEMBERS $0)$(LF) +DDOC_SUMTYPE_MEMBERS = $(H4 Variants)$(DDOC_MEMBERS $0)$(LF) DDOC_MEMBERS =