diff --git a/compiler/docs/enum union spec.md b/compiler/docs/enum union spec.md new file mode 100644 index 000000000000..065495901fc7 --- /dev/null +++ b/compiler/docs/enum union spec.md @@ -0,0 +1,283 @@ +## Formal Language Specification + +### 1. Declaration syntax + +An enum union is a nominal tagged sum type. It may contain: + +- unit variants: `case Name` +- positional variants: `case Name(T1, T2, ...)` +- record variants: `case Name { ... }` +- bare-type variants: `case T` + +```ebnf +EnumUnionDeclaration: + "enum" "union" [Identifier] "{" EnumUnionMemberList "}" + +EnumUnionMemberList: + EnumUnionMember ("," EnumUnionMember)* ","? [";" MemberDeclarationList] + +EnumUnionMember: + "case" Identifier + | "case" Identifier "(" ParameterList ")" + | "case" Identifier "{" StructBody "}" + | "case" Type +``` + +The declaration is parsed in the current frontend as a `case`-prefixed variant list. `case` is required for every variant form, including bare types and unit cases. + +### 2. Variant forms + +#### Unit variants + +A unit variant carries no payload and is represented by a distinct tag value. + +```d +enum union Traffic +{ + case Red, + case Yellow, + case Green, +} +``` + +This is the canonical zero-byte state form for an enum union. + +#### Positional variants + +A positional variant holds a payload created from the specified parameter list. + +```d +enum union Shape +{ + case Circle(double), + case Rectangle(double, double), + case Point, +} +``` + +The compiler synthesizes a static factory function for each named variant, such as `Shape.Circle(3.5)`. + +#### Record variants + +A record variant stores a synthesized nested payload struct. + +```d +enum union Response +{ + case Success { int code; string payload; }, + case Timeout, +} +``` + +The payload is materialized as a nested struct-like object. In a `switch` arm, record fields can be bound by name. + +#### Bare-type variants + +A bare-type variant is a single type, not a variant name. + +```d +enum union Value +{ + case int, + case double, + case string, +} +``` + +Bare-type variants are supported for a broad set of D types, including pointers, slices, delegates, function pointers, static arrays, `typeof(null)`, and other builtins already accepted by the ordinary implicit conversion rules. + +### 3. Null-like no-value variants + +The implementation accepts the null-like no-value case in two equivalent forms: + +```d +alias None = typeof(null); + +enum union Option(T) +{ + case Some(T), + case None, +} + +enum union NullOption(T) +{ + case Some(T), + case typeof(null), +} +``` + +A direct `null` literal may initialize a variant whose active state is the no-value variant, and the matching `switch` arm can be either `case None => ...` or `case typeof(null) => ...`. + +This is a special case of the general rule that a no-payload enum union case can be constructed from the `typeof(null)` value, because `null` is the canonical empty/none value for that state. + +### 4. Construction and implicit conversion + +The compiler synthesizes factory functions for named variants and also accepts ordinary implicit conversions for bare-type variants and null-like unit cases. + +Examples: + +```d +Val v1 = 5; // bare int case +Val v2 = true; // bare bool case +Val v3 = 3.14; // bare double case + +Shape s = Shape.Circle(2.0); + +Option!int o = null; // valid when the active variant is the null/no-value case +``` + +The conversion check matches the payload type of a bare variant against the source expression, with the usual D conversion rules applied. Ambiguous conversions are rejected. + +A bare-type duplicate is rejected at compile time, as are duplicate named cases. + +### 5. Internal representation + +Each enum union lowers to a tagged aggregate with: + +- a synthesized discriminant field, usually named `__tag` +- an anonymous union payload that overlays the storage of all variants +- one synthesized payload field per variant for the active payload storage + +The discriminator is an index into the declaration order of the enum union variants. + +For example: + +```d +enum union Shape +{ + case Circle(double), + case Rectangle(double, double), + case Point, +} +``` + +has tag values corresponding to `0`, `1`, and `2` in declaration order. + +### 6. Switch expressions + +Switch on an enum union is expression-based and uses fat-arrow arms: + +```d +int score = switch (s) +{ + case Circle(r) => cast(int) (r * 2), + case Rectangle(w, h) => cast(int) (w * h), + case Point => 1, +}; +``` + +The switch arm pattern may bind payload components or match a bare type arm: + +```d +string classify(Value v) +{ + return switch (v) + { + case int i => "int", + case double d => "double", + case string s => "string", + default => "other", + }; +} +``` + +#### Default arm + +A `default` arm is a catch-all branch used when no explicit case matches, or when the switch is intentionally not exhaustive. + +```d +string classify(Shape s) +{ + return switch (s) + { + case Circle(r) => "circle", + default => "other", + }; +} +``` + +A `default` arm is also required when an arm uses an `if` guard, because guarded arms do not unconditionally cover their variant. + +#### Guarded arms + +Guard expressions are allowed on switch arms: + +```d +return switch (v) +{ + case double d if (d > 0.0) => "positive", + case double d => "non-positive", + default => "other", +}; +``` + +The guard executes with the pattern-bound variables in scope. If a guarded arm is present, the switch must have a `default` arm unless the switch is otherwise exhaustive. + +The implementation rejects guard-only redundant cases and invalid pattern matches in the same way it rejects non-exhaustive or unreachable switch arms. + +### 7. Exhaustiveness and redundancy checks + +The implementation enforces compile-time checking for: + +- unhandled variants in non-defaulted switches +- redundant match arms +- switch arms that do not match any enum union variant +- arms with `if` guards but without a `default` arm + +If the switch is exhaustive without a `default`, no default arm is required. If not exhaustive, a `default` arm is required. + +### 8. Duplicate and ambiguity rules + +The implementation rejects: + +- duplicate named cases: two `case Name` entries with the same identifier +- duplicate bare types: two `case T` entries with the same type +- ambiguous implicit construction when the source expression could match more than one bare variant +- record/positional patterns that do not match the active variant + +### 9. Lifecycle rules + +Enum unions obey the lifecycle rules of their payloads. + +- If a variant payload has a destructor, the enum union gets a synthesized destructor that dispatches on the active tag. +- If a payload is move-only or otherwise unsafe to copy, the enum union rejects the declaration. +- Copying a union copies the active payload according to the payload’s copy semantics. + +This is enforced during semantic analysis to avoid raw bitcopying of payloads that require destruction or special copy semantics. + +### 10. Member functions and trailing declarations + +After the variant list, an enum union may continue with member declarations after a semicolon: + +```d +enum union ShapeWithMethods +{ + case Circle(double), + case Rectangle(double, double), + case Point; + + double area() + { + return switch (this) + { + case Circle(r) => 3.14159 * r * r, + case Rectangle(w, h) => w * h, + case Point => 0.0, + }; + } +} +``` + +This is supported by the implementation. Member declarations are part of the enum union after the semicolon following the last variant. + +### 11. Summary + +The implemented model is a D-native tagged sum type with these practical rules: + +- every variant is declared with `case` +- bare types, named unit cases, and record/positional payloads are all valid +- `switch` over an enum union matches the active tag and binds payload fields as needed +- `default` is a catch-all branch and is required for guarded arms unless the switch is exhaustive +- duplicate named cases and duplicate bare types are rejected +- `null` can initialize a null-like no-value variant, including `case None` and `case typeof(null)` +- payload lifecycle safety is enforced through the same semantic checks as aggregate destructors and copying diff --git a/compiler/docs/enum_union_guide.md b/compiler/docs/enum_union_guide.md new file mode 100644 index 000000000000..df089b0b2af6 --- /dev/null +++ b/compiler/docs/enum_union_guide.md @@ -0,0 +1,410 @@ +# Enum unions + +An enum union is a tagged union whose value is one of a fixed set of variants. Each variant can carry data, or it can be a unit case with no payload. The compiler tracks the active variant and allows matching on it with a `switch` expression. + +## 1. Basic declaration + +```d +enum union Shape +{ + case Circle(double), + case Rectangle(double, double), + case Point(), +} +``` + +This declares a type that is either: + +- a circle with one `double` +- a rectangle with two `double`s +- a point with no payload + +Each named case is also a factory function: + +```d +Shape s1 = Shape.Circle(3.5); +Shape s2 = Shape.Point(); +``` + +The active variant is stored internally as a tag. The generated field is `.__tag`. + +--- + +## 2. Variant forms + +An enum union case can be one of four forms. + +### Unit cases + +```d +enum union State +{ + case Idle(), + case Running(), + case Stopped(), +} +``` + +These cases carry no payload. Empty parentheses are required so an +unparenthesized identifier can always be parsed as a type payload. + +### Bare types + +```d +enum union Value +{ + case int, + case string, + case bool, +} +``` + +Each bare case is distinguished by its type. Construction uses the value itself: + +```d +Value v1 = 42; +Value v2 = "hello"; +Value v3 = true; +``` + +This is distinct from a named case with a payload or a separate struct type. For example, the enum union case name and the payload type name are not required to be the same thing: + +```d +struct Success +{ + int code; + string msg; +} + +enum union Response +{ + case Success, + case Failure(string), + case Timeout(), +} +``` + +Here `Success` is a bare type payload, while `Failure` and `Timeout` are named +variants. A named unit variant always uses empty parentheses. + +The compiler checks for duplicate bare types and for ambiguous construction when more than one bare case can accept the same value. + +### Positional payload variants + +```d +enum union Shape +{ + case Circle(double), + case Rectangle(double, double), + case Point(), +} +``` + +The constructor parameters match the payload fields. + +### Record-style payload variants + +```d +enum union Response +{ + case Success { int code; string message; }, + case Failure { string reason; }, + case Timeout(), +} +``` + +The payload is a record-like structure. The compiler synthesizes the corresponding constructor for the case. + +--- + +## 3. Duplicate and ambiguity checks + +The compiler rejects invalid declarations. + +```d +enum union LatLong +{ + case double, + case double, +} +``` + +This is rejected because the bare type appears twice. + +```d +enum union BadNames +{ + case A { int x; }, + case A { string s; }, +} +``` + +This is rejected because the case name is duplicated, even when the payloads differ. + +```d +enum union Funs +{ + case int function(int), + case int delegate(int), +} + +Funs f = () {}; +``` + +This is rejected because the value could match more than one case. + +--- + +## 4. Methods and trailing members + +Enum unions can include member declarations after the case list. + +```d +enum union ShapeWithMethods +{ + case Circle(double), + case Rectangle(double, double), + case Point(); + + double area() + { + return switch (this) + { + case Circle(r) => 3.14159 * r * r, + case Rectangle(w, h) => w * h, + case Point() => 0.0, + }; + } +} + +int score(ShapeWithMethods s) +{ + return s.area(); +} +``` + +These member functions are members of the union itself. Inside the function, "this" refers to the union itself, and a switch expression is required to determine which invariant is active. +--- + +## 5. Destructors, invariants, and lifecycle rules + +An enum union participates in the same D lifecycle rules as other aggregate types. User-defined destructors and invariant logic are allowed. + +The compiler also rejects unsafe payloads. + +```d +struct MoveOnly +{ + int x; + this(return MoveOnly other) { x = other.x; } +} + +enum union Bad +{ + case M(MoveOnly), + case Other(bool), +} +``` + +This is rejected because the payload has a move constructor but no copy constructor. The enum union stores the payload in a union-like representation, and copying it would not be safe. + +The same check applies to disabled copy constructors and disabled postblits. + +--- + +## 6. Matching with `switch` expressions + +The primary matching form for enum unions is a `switch` expression. + +```d +int score(Shape s) +{ + return switch (s) + { + case Circle(r) => cast(int)(r * 2), + case Rectangle(w, h) => cast(int)(w * h), + case Point() => 1, + }; +} +``` + +The switch arm pattern binds the currently active payload. + +```d +case Circle(r) => ... +case Rectangle(w, h) => ... +case Point() => ... +``` + +A `default` arm is the fallback branch for a `switch` expression. It runs when no earlier pattern matches. It does not bind a value, because it is not a case pattern; it is simply the catch-all branch for the remaining cases. + +This is the normal way to inspect an enum union. + +--- + +## 7. Guards + +A switch arm may include a guard. + +```d +enum union Level +{ + case double, + case string, +} + +string classify(Level v) +{ + return switch (v) + { + case double d if (d > 100.0) => "high", + case double d => "normal", + default => "other", + }; +} +``` + +Note that a `default` arm cannot itself have an `if` guard. + +--- + +## 8. Exhaustiveness + +A `switch` expression over an enum union must cover every variant unless it contains a `default` arm: + +```d +string classifyTraffic(Traffic t) +{ + return switch (t) + { + case Red() => "stop", + case Yellow() => "caution", + case Green() => "go", + }; +} +``` + +If a variant is omitted, the compiler reports an error. + +```d +string classifyTraffic(Traffic t) +{ + return switch (t) + { + case Red() => "stop", + case Yellow() => "caution", + }; +} +``` + +This produces an error stating which variant is missing. + +--- + +## 9. Redundant and unmatched arms + +The compiler rejects unreachable cases and invalid pattern names. + +```d +string bad(Traffic t) +{ + return switch (t) + { + case Red() => "stop", + case Red() => "again", + case Yellow() => "caution", + case Green() => "go", + }; +} +``` + +The second `Red` arm is rejected as redundant. + +```d +string bad(Shape s) +{ + return switch (s) + { + case Circle(r) => "circle", + case Square(r) => "square", + case Point() => "point", + }; +} +``` + +This is rejected because `Square` is not a variant of `Shape`. + +## 10. Complete example + +```d +enum union Option(T) +{ + case Some(T), + case None(), +} + +string describe(Option!string value) +{ + return switch (value) + { + case Some(msg) => "value: " ~ msg, + case None() => "empty", + }; +} + +void main() +{ + Option!string a = Option!string.Some("hello"); + Option!string b = Option!string.None(); + + assert(describe(a) == "value: hello"); + assert(describe(b) == "empty"); +} +``` + +This shows the full pattern: declare the union, construct a value with a case factory, and inspect it with a `switch` expression. + +An alternate form is to use the bare type `typeof(null)` for the "none" case: + +```d +enum union Option(T) +{ + case Some(T), + typeof(null), +} + +string describe(Option!string value) +{ + return switch (value) + { + case Some(n) => "value: " ~ n.to!string(), + case typeof(null) => "empty", + }; +} + +void main() +{ + Option!int n = Option!int.Some(42); + Option!int m = null; + + assert(describe(n) == "value: 42"); + assert(describe(m) == "empty"); +} +``` + +This uses the same enum-union shape, but the no-value case is represented by the `typeof(null)` alias instead of a distinct unit case name. + +--- + +## 11. Summary + +An enum union is a fixed set of tagged variants. The declaration form is D-native, and the common access pattern is a `switch` expression that matches the active case and binds its payload. + +The key rules are: + +- each case name must be unique +- bare type duplicates are rejected +- ambiguous bare conversions are rejected +- the switch must be exhaustive unless a `default` is present +- unreachable and invalid arms are rejected +- unsafe lifecycle payloads are rejected diff --git a/compiler/src/build.d b/compiler/src/build.d index aefda5e0d8c4..e3d0f8ba0683 100755 --- a/compiler/src/build.d +++ b/compiler/src/build.d @@ -1518,7 +1518,7 @@ auto sourceFiles() access.d aggregate.d aliasthis.d argtypes_x86.d argtypes_sysv_x64.d argtypes_aarch64.d arrayop.d arraytypes.d astenums.d ast_node.d astcodegen.d asttypename.d attrib.d attribsem.d blockexit.d builtin.d canthrow.d chkformat.d cli.d clone.d compiler.d cond.d constfold.d cpreprocess.d ctfeexpr.d - ctorflow.d dcast.d dclass.d declaration.d delegatize.d denum.d deps.d dimport.d + ctorflow.d dcast.d dclass.d declaration.d decisiontree.d delegatize.d denum.d deps.d dimport.d dinterpret.d dmacro.d dmodule.d doc.d dscope.d dstruct.d dsymbol.d dsymbolsem.d dtemplate.d dtoh.d dversion.d enumsem.d escape.d expression.d expressionsem.d func.d funcsem.d hdrgen.d impcnvtab.d imphint.d importc.d init.d initsem.d inline.d inlinecost.d intrange.d json.d lambdacomp.d diff --git a/compiler/src/dmd/astbase.d b/compiler/src/dmd/astbase.d index dfa88c6517c9..807b543740b3 100644 --- a/compiler/src/dmd/astbase.d +++ b/compiler/src/dmd/astbase.d @@ -62,6 +62,16 @@ struct ASTBase alias Visitor = ParseTimeVisitor!ASTBase; + enum DSYM : ubyte + { + none, + enumUnionDeclaration, + enumUnionCaseDeclaration, + staticIfDeclaration, + staticForeachDeclaration, + pragmaDeclaration, + } + extern (C++) abstract class ASTNode : RootObject { abstract void accept(Visitor v); @@ -71,6 +81,7 @@ struct ASTBase { Loc loc; Identifier ident; + DSYM dsym; UnitTestDeclaration ddocUnittest; UserAttributeDeclaration userAttribDecl; Dsymbol parent; @@ -893,6 +904,53 @@ struct ASTBase } } + struct EnumUnionVariant + { + Loc loc; + Identifier ident; + bool isTypeAlias; + Expressions* udas; + Type[] payload; + Identifier[] payloadNames; + Dsymbols* members; + } + + extern (C++) final class EnumUnionCaseDeclaration : Declaration + { + EnumUnionVariant variant; + + extern (D) this(Loc loc, EnumUnionVariant variant) + { + super(null); + this.loc = loc; + this.dsym = DSYM.enumUnionCaseDeclaration; + this.variant = variant; + } + + override void accept(Visitor v) + { + v.visit(cast(Declaration) this); + } + } + + extern (C++) final class EnumUnionDeclaration : ScopeDsymbol + { + EnumUnionVariant[] variants; + VarDeclaration tagVar; + UnionDeclaration payloadUnion; + + extern (D) this(Loc loc, Identifier id) + { + super(loc, id); + this.dsym = DSYM.enumUnionDeclaration; + } + + override void accept(Visitor v) + { + v.visit(cast(ScopeDsymbol) this); + } + } + extern (C++) abstract class AggregateDeclaration : ScopeDsymbol { Visibility visibility; @@ -1261,6 +1319,7 @@ struct ASTBase this.loc = loc; this.ident = ident; this.args = args; + this.dsym = DSYM.pragmaDeclaration; } override void accept(Visitor v) @@ -1336,6 +1395,7 @@ struct ASTBase extern (D) this(Loc loc, Condition condition, Dsymbols* decl, Dsymbols* elsedecl) { super(loc, condition, decl, elsedecl); + this.dsym = DSYM.staticIfDeclaration; } override void accept(Visitor v) @@ -1352,6 +1412,7 @@ struct ASTBase { super(sfe.loc, null, decl); this.sfe = sfe; + this.dsym = DSYM.staticForeachDeclaration; } override void accept(Visitor v) @@ -4604,6 +4665,7 @@ struct ASTBase inout(DotIdExp) isDotIdExp() { return op == EXP.dotIdentifier ? cast(typeof(return))this : null; } inout(DotTemplateInstanceExp) isDotTemplateInstanceExp() { return op == EXP.dotTemplateInstance ? cast(typeof(return))this : null; } inout(CallExp) isCallExp() { return op == EXP.call ? cast(typeof(return))this : null; } + inout(SwitchExp) isSwitchExp() { return op == EXP.switchExpression ? cast(typeof(return))this : null; } inout(AddrExp) isAddrExp() { return op == EXP.address ? cast(typeof(return))this : null; } inout(PtrExp) isPtrExp() { return op == EXP.star ? cast(typeof(return))this : null; } inout(NegExp) isNegExp() { return op == EXP.negate ? cast(typeof(return))this : null; } @@ -6022,6 +6084,59 @@ struct ASTBase } } + struct CaseExpArm + { + Loc loc; + Expression pattern; + Type typePattern; + Identifier typeBinding; + Identifier[] recordBindings; + bool hasRestPattern; + Identifier[] recordPatternNames; + Expression[] recordPatterns; + Identifier restBinding; + Expression guard; + bool isDefault; + Expression action; + } + + extern (C++) final class SwitchExp : Expression + { + Expression condition; + CaseExpArm[] arms; + bool hasDefault; + + final extern (D) this(Loc loc, Expression condition, CaseExpArm[] arms, bool hasDefault) + { + super(loc, EXP.switchExpression, __traits(classInstanceSize, SwitchExp)); + this.condition = condition; + this.arms = arms; + this.hasDefault = hasDefault; + } + + override SwitchExp syntaxCopy() + { + auto copiedArms = new CaseExpArm[](arms.length); + foreach (i, arm; arms) + { + copiedArms[i] = arm; + copiedArms[i].pattern = arm.pattern ? arm.pattern.syntaxCopy() : null; + copiedArms[i].typePattern = arm.typePattern ? arm.typePattern.syntaxCopy() : null; + copiedArms[i].recordBindings = arm.recordBindings.dup; + copiedArms[i].recordPatternNames = arm.recordPatternNames.dup; + copiedArms[i].recordPatterns = arm.recordPatterns.dup; + copiedArms[i].guard = arm.guard ? arm.guard.syntaxCopy() : null; + copiedArms[i].action = arm.action ? arm.action.syntaxCopy() : null; + } + return new SwitchExp(loc, condition.syntaxCopy(), copiedArms, hasDefault); + } + + override void accept(Visitor v) + { + v.visit(this); + } + } + extern (C++) final class AssignExp : BinExp { extern (D) this(Loc loc, Expression e1, Expression e2) diff --git a/compiler/src/dmd/ctorflow.d b/compiler/src/dmd/ctorflow.d index 4207c681e2c4..64362781625a 100644 --- a/compiler/src/dmd/ctorflow.d +++ b/compiler/src/dmd/ctorflow.d @@ -40,6 +40,7 @@ struct FieldInit struct CtorFlow { CSX callSuper; /// state of calling other constructors + bool thisInitialized; /// an enum-union constructor assigned a complete value to `this` FieldInit[] fieldinit; /// state of field initializations @@ -63,7 +64,7 @@ struct CtorFlow */ CtorFlow clone() { - return CtorFlow(callSuper, fieldinit.arraydup); + return CtorFlow(callSuper, thisInitialized, fieldinit.arraydup); } /********************************** @@ -86,6 +87,7 @@ struct CtorFlow void OR(const ref CtorFlow ctorflow) pure nothrow @safe { callSuper |= ctorflow.callSuper; + thisInitialized |= ctorflow.thisInitialized; if (fieldinit.length && ctorflow.fieldinit.length) { assert(fieldinit.length == ctorflow.fieldinit.length); @@ -100,6 +102,24 @@ struct CtorFlow } } +/**************************************** + * Merge whether `this` has been initialized along all continuing paths. + */ +void mergeThisInitialized(ref bool a, const bool b, const CSX aCSX, const CSX bCSX) + pure nothrow @safe +{ + const aExits = aCSX & (CSX.return_ | CSX.halt); + const bExits = bCSX & (CSX.return_ | CSX.halt); + if (bExits && !aExits) + return; + if (aExits && !bExits) + { + a = b; + return; + } + a &= b; +} + /**************************************** * Merge `b` flow analysis results into `a`. diff --git a/compiler/src/dmd/dcast.d b/compiler/src/dmd/dcast.d index 8c7ddd1beeea..976fd97517e8 100644 --- a/compiler/src/dmd/dcast.d +++ b/compiler/src/dmd/dcast.d @@ -32,6 +32,7 @@ import dmd.func; import dmd.funcsem; import dmd.globals; import dmd.hdrgen; +import dmd.identifier; import dmd.location; import dmd.impcnvtab; import dmd.importc; @@ -113,6 +114,71 @@ Expression implicitCastTo(Expression e, Scope* sc, Type t) { auto eSink = global.errorSink; + if (auto ts = t.toBasetype().isTypeStruct()) + { + if (auto eu = ts.sym.isEnumUnionDeclaration()) + { + size_t matchIndex = size_t.max; + foreach (i, variant; eu.variants) + { + auto payloadType = variant.payloadType && variant.payloadType.fields.length + ? variant.payloadType.fields[0].type : variant.payload.length ? variant.payload[0] : null; + if ((!variant.ident || variant.isTypeAlias) && variant.payload.length == 1 && payloadType && + e.implicitConvTo(payloadType) >= MATCH.convert) + { + if (matchIndex != size_t.max) + { + eSink.error(e.loc, "`%s` is ambiguous between variants `%s` and `%s` of enum union `%s`", + e.toErrMsg(), eu.variants[matchIndex].payloadType && eu.variants[matchIndex].payloadType.fields.length + ? eu.variants[matchIndex].payloadType.fields[0].type.toErrMsg() + : "", + payloadType ? payloadType.toErrMsg() : "", + eu.toPrettyChars()); + return ErrorExp.get(); + } + matchIndex = i; + } + } + if (matchIndex != size_t.max) + { + auto variant = eu.variants[matchIndex]; + // Build `(tmp; tmp.__tag = i; tmp.. = e; tmp)` + // rather than a positional struct literal: the payload union + // promotes one field per variant, so a 2-element literal would + // target the wrong (first) union member when there is more + // than one bare-type variant. + auto tmp = new VarDeclaration(e.loc, t, Identifier.generateId("__enumConv"), null); + tmp.storage_class |= STC.temp; + Expression result = new DeclarationExp(e.loc, tmp).expressionSemantic(sc); + Expression tmpVar = new VarExp(e.loc, tmp); + + auto tagExp = new DotVarExp(e.loc, tmpVar, eu.tagVar); + tagExp.type = eu.tagVar.type; + Expression tagAssign = new AssignExp(e.loc, tagExp, + new IntegerExp(e.loc, matchIndex, Type.tuns8)).expressionSemantic(sc); + result = new CommaExp(e.loc, result, tagAssign); + result.type = tagAssign.type; + + if (variant.payloadType && variant.payloadType.fields.length) + { + auto field = variant.payloadType.fields[0]; + auto payloadAccess = new DotVarExp(e.loc, + new DotVarExp(e.loc, tmpVar, variant.payloadVar), field); + payloadAccess.e1.type = variant.payloadVar.type; + payloadAccess.type = field.type; + Expression payloadAssign = new AssignExp(e.loc, payloadAccess, e); + payloadAssign.type = field.type; + result = new CommaExp(e.loc, result, payloadAssign); + result.type = payloadAssign.type; + } + + result = new CommaExp(e.loc, result, tmpVar); + result.type = t; + return result; + } + } + } + Expression visit(Expression e) { //printf("Expression.implicitCastTo(%s of type %s) => %s\n", e.toChars(), e.type.toChars(), t.toChars()); @@ -1561,6 +1627,24 @@ MATCH implicitConvTo(Expression e, Type t) */ MATCH implicitConvTo(Type from, Type to) { + if (auto ts = to.toBasetype().isTypeStruct()) + { + if (auto eu = ts.sym.isEnumUnionDeclaration()) + { + foreach (variant; eu.variants) + { + auto payloadType = variant.payloadType && variant.payloadType.fields.length + ? variant.payloadType.fields[0].type : variant.payload.length ? variant.payload[0] : null; + const requiredMatch = payloadType && payloadType.isFunction_Delegate_PtrToFunction() && + from.isFunction_Delegate_PtrToFunction() + ? MATCH.convert : MATCH.exact; + if ((!variant.ident || variant.isTypeAlias) && variant.payload.length == 1 && payloadType && + from.implicitConvTo(payloadType) >= requiredMatch) + return MATCH.convert; + } + } + } + MATCH visitType(Type from) { //printf("Type::implicitConvTo(this=%p, to=%p)\n", this, to); diff --git a/compiler/src/dmd/decisiontree.d b/compiler/src/dmd/decisiontree.d new file mode 100644 index 000000000000..91c5b5b5b105 --- /dev/null +++ b/compiler/src/dmd/decisiontree.d @@ -0,0 +1,309 @@ +/** Pattern usefulness and exhaustiveness checking for switch expressions. + * + * The matrix owns only its initial rows. Recursive specialization passes row + * indices and a column offset, so it never clones AST expressions or patterns. + */ +module dmd.decisiontree; + +import dmd.dstruct; +import dmd.declaration; +import dmd.errorsink; +import dmd.expression; +import dmd.location; +import dmd.common.outbuffer; + +private enum PatternKind : ubyte +{ + wildcard, + constructor, + literal, +} + +private struct Pattern +{ + PatternKind kind; + size_t value; + Loc loc; +} + +private struct MatrixRow +{ + Pattern[] columns; + size_t armIndex; +} + +private struct PatternMatrix +{ + MatrixRow[] rows; + size_t numColumns; +} + +private bool matches(ref const Pattern row, ref const Pattern value) +{ + return row.kind == PatternKind.wildcard || + (row.kind == value.kind && row.value == value.value); +} + +private size_t constructorCount(EnumUnionDeclaration eu, size_t column, size_t[] fieldOffsets) +{ + if (column == 0) + return eu.variants.length; + return 0; +} + +/* + * Maranget's usefulness recurrence, represented as a borrowed view over the + * original matrix. The root column is a finite enum-union constructor space; + * bool fields are also finite. Integer fields use the literal/default split, + * which represents all values not named in the matrix with one branch. + */ +private bool isUseful(ref const PatternMatrix matrix, size_t[] rowIndices, + Pattern[] vector, size_t column, EnumUnionDeclaration eu, size_t[] fieldOffsets) +{ + if (!rowIndices.length) + return true; + if (column == matrix.numColumns) + return false; + + const candidate = vector[column]; + const constructors = constructorCount(eu, column, fieldOffsets); + if (candidate.kind == PatternKind.constructor) + { + size_t[] specialized; + foreach (rowIndex; rowIndices) + if (matches(matrix.rows[rowIndex].columns[column], candidate)) + specialized ~= rowIndex; + return isUseful(matrix, specialized, vector, column + 1, eu, fieldOffsets); + } + + if (constructors) + { + // <= 64 enum-union variants are tracked in one mask. The language + // currently caps them at 256, so the larger case falls back to this + // same bounded constructor iteration. + ulong seen; + foreach (rowIndex; rowIndices) + { + const pattern = matrix.rows[rowIndex].columns[column]; + if (pattern.kind == PatternKind.constructor && pattern.value < 64) + seen |= 1UL << pattern.value; + } + foreach (constructor; 0 .. constructors) + { + Pattern specialized = Pattern(PatternKind.constructor, constructor, candidate.loc); + size_t[] rows; + foreach (rowIndex; rowIndices) + if (matches(matrix.rows[rowIndex].columns[column], specialized)) + rows ~= rowIndex; + auto trial = vector.dup; + trial[column] = specialized; + if (isUseful(matrix, rows, trial, column + 1, eu, fieldOffsets)) + return true; + } + return false; + } + + // A bool constructor space is finite even though it is represented by an + // integer literal in the frontend AST. + bool isBool; + foreach (rowIndex; rowIndices) + { + const pattern = matrix.rows[rowIndex].columns[column]; + if (pattern.kind == PatternKind.literal && pattern.value <= 1) + isBool = true; + } + if (isBool) + { + foreach (value; 0 .. 2) + { + Pattern literal = Pattern(PatternKind.literal, value, candidate.loc); + size_t[] rows; + foreach (rowIndex; rowIndices) + if (matches(matrix.rows[rowIndex].columns[column], literal)) + rows ~= rowIndex; + auto trial = vector.dup; + trial[column] = literal; + if (isUseful(matrix, rows, trial, column + 1, eu, fieldOffsets)) + return true; + } + return false; + } + + // Literal partitions: check each named point once, then the single + // default interval that contains every other scalar value. + foreach (rowIndex; rowIndices) + { + const literal = matrix.rows[rowIndex].columns[column]; + if (literal.kind != PatternKind.literal) + continue; + bool seen; + foreach (previous; rowIndices) + { + if (previous == rowIndex) + break; + const other = matrix.rows[previous].columns[column]; + if (other.kind == PatternKind.literal && other.value == literal.value) + { + seen = true; + break; + } + } + if (seen) + continue; + size_t[] rows; + foreach (index; rowIndices) + if (matches(matrix.rows[index].columns[column], literal)) + rows ~= index; + auto trial = vector.dup; + trial[column] = literal; + if (isUseful(matrix, rows, trial, column + 1, eu, fieldOffsets)) + return true; + } + size_t[] defaults; + foreach (rowIndex; rowIndices) + if (matrix.rows[rowIndex].columns[column].kind == PatternKind.wildcard) + defaults ~= rowIndex; + return isUseful(matrix, defaults, vector, column + 1, eu, fieldOffsets); +} + +private bool integerLiteral(Expression expression, out size_t value) +{ + if (auto integer = expression.isIntegerExp()) + { + value = cast(size_t) integer.getInteger(); + return true; + } + return false; +} + +private size_t fieldIndex(EqualExp check, VarDeclaration[] fields) +{ + auto dot = check.e1.isDotVarExp(); + if (!dot) + return size_t.max; + foreach (index, field; fields) + if (dot.var == field) + return index; + return size_t.max; +} + +private MatrixRow makeRow(ref CaseExpArm arm, EnumUnionDeclaration eu, size_t[] fieldOffsets, + size_t totalColumns, size_t armIndex) +{ + MatrixRow row; + row.columns.length = totalColumns; + foreach (ref pattern; row.columns) + pattern = Pattern(PatternKind.wildcard, 0, arm.loc); + row.armIndex = armIndex; + if (arm.isDefault) + return row; + row.columns[0] = Pattern(PatternKind.constructor, arm.variantIndex, arm.loc); + auto variant = eu.variants[arm.variantIndex]; + VarDeclaration[] fields; + if (variant.payloadType) + foreach (field; variant.payloadType.fields) + fields ~= field; + foreach (checkExpression; arm.patternChecks) + { + auto check = checkExpression.isEqualExp(); + if (!check) + continue; + size_t value; + const index = fieldIndex(check, fields); + if (index != size_t.max && integerLiteral(check.e2, value)) + row.columns[fieldOffsets[arm.variantIndex] + index] = Pattern(PatternKind.literal, value, check.loc); + } + return row; +} + +private void missingWitness(ref OutBuffer witness, ref const PatternMatrix matrix, size_t[] allRows, + EnumUnionDeclaration eu, size_t[] fieldOffsets, size_t totalColumns, Loc loc) +{ + foreach (variantIndex, variant; eu.variants) + { + Pattern[] candidate = new Pattern[](totalColumns); + foreach (ref pattern; candidate) + pattern = Pattern(PatternKind.wildcard, 0, loc); + candidate[0] = Pattern(PatternKind.constructor, variantIndex, loc); + if (!isUseful(matrix, allRows, candidate, 0, eu, fieldOffsets)) + continue; + witness.writestring(variant.ident ? variant.ident.toString() : "_"); + if (variant.payloadType && variant.payloadType.fields.length) + { + witness.writeByte('('); + foreach (index; 0 .. variant.payloadType.fields.length) + { + if (index) + witness.writestring(", "); + witness.writeByte('_'); + } + witness.writeByte(')'); + } + return; + } + witness.writestring("_"); +} + +/** Check source-order usefulness and whether unguarded arms cover all cases. */ +public bool checkExhaustivenessAndRedundancy(SwitchExp exp, EnumUnionDeclaration eu, ErrorSink eSink) +{ + size_t[] fieldOffsets; + size_t totalColumns = 1; + foreach (variant; eu.variants) + { + fieldOffsets ~= totalColumns; + totalColumns += variant.payloadType ? variant.payloadType.fields.length : 0; + } + + PatternMatrix matrix = PatternMatrix(null, totalColumns); + bool hasDefault; + foreach (armIndex, ref arm; exp.arms) + { + if (arm.isDefault) + { + Pattern[] wildcard = new Pattern[](totalColumns); + foreach (ref pattern; wildcard) + pattern = Pattern(PatternKind.wildcard, 0, arm.loc); + size_t[] allRows; + foreach (index; 0 .. matrix.rows.length) + allRows ~= index; + if (!isUseful(matrix, allRows, wildcard, 0, eu, fieldOffsets)) + { + eSink.error(arm.loc, "redundant match arm; pattern is unreachable"); + return false; + } + hasDefault = true; + continue; + } + size_t[] allRows; + foreach (index; 0 .. matrix.rows.length) + allRows ~= index; + auto row = makeRow(arm, eu, fieldOffsets, totalColumns, armIndex); + if (!isUseful(matrix, allRows, row.columns, 0, eu, fieldOffsets)) + { + eSink.error(arm.loc, "redundant match arm; pattern is unreachable"); + return false; + } + if (!arm.guard) + matrix.rows ~= row; + } + + if (hasDefault) + return true; + + Pattern[] wildcard = new Pattern[](totalColumns); + foreach (ref pattern; wildcard) + pattern = Pattern(PatternKind.wildcard, 0, exp.loc); + size_t[] allRows; + foreach (index; 0 .. matrix.rows.length) + allRows ~= index; + if (isUseful(matrix, allRows, wildcard, 0, eu, fieldOffsets)) + { + OutBuffer witness; + missingWitness(witness, matrix, allRows, eu, fieldOffsets, totalColumns, exp.loc); + eSink.error(exp.loc, "switch expression is not exhaustive; missing pattern `%s`", + witness.peekChars()); + return false; + } + return true; +} \ No newline at end of file diff --git a/compiler/src/dmd/dfa/fast/expression.d b/compiler/src/dmd/dfa/fast/expression.d index 696871fc78da..560ee20f3784 100644 --- a/compiler/src/dmd/dfa/fast/expression.d +++ b/compiler/src/dmd/dfa/fast/expression.d @@ -1468,6 +1468,13 @@ struct ExpressionWalker return inProgress; } + case EXP.switchExpression: + auto switchExp = expr.isSwitchExp; + this.walk(switchExp.condition); + foreach (arm; switchExp.arms) + this.walk(arm.action); + return DFALatticeRef.init; + case EXP.question: { auto qe = expr.isCondExp; diff --git a/compiler/src/dmd/dscope.d b/compiler/src/dmd/dscope.d index 9a7f362b8c7a..440771d4ff27 100644 --- a/compiler/src/dmd/dscope.d +++ b/compiler/src/dmd/dscope.d @@ -48,6 +48,7 @@ enum Contract : ubyte private extern (D) struct FlagBitFields { bool ctor; /// constructor type + bool allowUninitializedThis; /// analyzing the left side of `this = value` in an enum-union constructor bool noAccessCheck; /// don't do access checks bool condition; /// inside static if/assert condition bool debug_; /// inside debug conditional diff --git a/compiler/src/dmd/dstruct.d b/compiler/src/dmd/dstruct.d index 089307968d57..b5eeb617f713 100644 --- a/compiler/src/dmd/dstruct.d +++ b/compiler/src/dmd/dstruct.d @@ -18,6 +18,8 @@ import core.stdc.stdio; import dmd.aggregate; import dmd.arraytypes; import dmd.astenums; +import dmd.declaration; +import dmd.denum; import dmd.dmodule; import dmd.dsymbol; import dmd.func; @@ -25,6 +27,7 @@ import dmd.id; import dmd.identifier; import dmd.location; import dmd.mtype; +import dmd.rootobject; import dmd.visitor; enum StructFlags : int @@ -33,6 +36,41 @@ enum StructFlags : int hasPointers = 0x1, // NB: should use noPointers as in ClassFlags } +struct EnumUnionVariant +{ + Loc loc; + Identifier ident; + bool isTypeAlias; + bool generated; + Expressions* udas; + Type[] payload; + Identifier[] payloadNames; + Dsymbols* members; + StructDeclaration payloadType; + VarDeclaration payloadVar; +} + +extern (C++) final class EnumUnionCaseDeclaration : Declaration +{ + EnumUnionVariant variant; + + extern (D) this(Loc loc, EnumUnionVariant variant) + { + super(DSYM.enumUnionCaseDeclaration, loc, null); + this.variant = variant; + } + + override EnumUnionCaseDeclaration syntaxCopy(Dsymbol s) + { + return new EnumUnionCaseDeclaration(loc, variant); + } + + override const(char)* kind() const + { + return "enum union case"; + } +} + /*********************************************************** * All `struct` declarations are an instance of this. */ @@ -139,6 +177,47 @@ extern (C++) class StructDeclaration : AggregateDeclaration } +/*********************************************************** + * Tagged aggregate used by `enum union` declarations. + */ +extern (C++) final class EnumUnionDeclaration : StructDeclaration +{ + EnumUnionVariant[] variants; + bool enumUnionCasesExpanded; + bool enumUnionFactoriesSynthesized; + VarDeclaration tagVar; + UnionDeclaration payloadUnion; + + extern (D) this(Loc loc, Identifier id) + { + super(loc, id, false); + this.dsym = DSYM.enumUnionDeclaration; + } + + override EnumUnionDeclaration syntaxCopy(Dsymbol s) + { + auto eu = new EnumUnionDeclaration(loc, ident); + eu.variants = variants; + StructDeclaration.syntaxCopy(eu); + // `members` was just deep-copied above; `tagVar` must point at the + // copy (it's always the first member, see parse.d), not the original + // declaration, or later semantic passes on this instance dereference + // a stale/absent tag variable (null for template instantiations). + eu.tagVar = eu.members && eu.members.length ? (*eu.members)[0].isVarDeclaration() : null; + return eu; + } + + override const(char)* kind() const + { + return "enum union"; + } + + override void accept(Visitor v) + { + v.visit(this); + } +} + /*********************************************************** * Unions are a variation on structs. */ diff --git a/compiler/src/dmd/dsymbol.d b/compiler/src/dmd/dsymbol.d index 30f738a824ea..93224e10572f 100644 --- a/compiler/src/dmd/dsymbol.d +++ b/compiler/src/dmd/dsymbol.d @@ -343,6 +343,8 @@ enum DSYM : ubyte classDeclaration, structDeclaration, unionDeclaration, + enumUnionDeclaration, + enumUnionCaseDeclaration, interfaceDeclaration, scopeDsymbol, forwardingScopeDsymbol, @@ -930,6 +932,7 @@ extern (C++) class Dsymbol : ASTNode inout(TemplateInstance) isTemplateInstance() inout { return (dsym == DSYM.templateInstance || dsym == DSYM.templateMixin) ? cast(inout(TemplateInstance)) cast(void*) this : null; } inout(TemplateMixin) isTemplateMixin() inout { return dsym == DSYM.templateMixin ? cast(inout(TemplateMixin)) cast(void*) this : null; } inout(ForwardingAttribDeclaration) isForwardingAttribDeclaration() inout { return dsym == DSYM.forwardingAttribDeclaration ? cast(inout(ForwardingAttribDeclaration)) cast(void*) this : null; } + inout(StaticForeachDeclaration) isStaticForeachDeclaration() inout { return dsym == DSYM.staticForeachDeclaration ? cast(inout(StaticForeachDeclaration)) cast(void*) this : null; } inout(Nspace) isNspace() inout { return dsym == DSYM.nspace ? cast(inout(Nspace)) cast(void*) this : null; } inout(Declaration) isDeclaration() inout { switch (dsym) @@ -976,6 +979,7 @@ extern (C++) class Dsymbol : ASTNode case DSYM.aggregateDeclaration: case DSYM.structDeclaration: case DSYM.unionDeclaration: + case DSYM.enumUnionDeclaration: case DSYM.classDeclaration: case DSYM.interfaceDeclaration: return cast(inout(AggregateDeclaration)) cast(void*) this; @@ -1033,8 +1037,9 @@ extern (C++) class Dsymbol : ASTNode inout(VersionSymbol) isVersionSymbol() inout { return dsym == DSYM.versionSymbol ? cast(inout(VersionSymbol)) cast(void*) this : null; } inout(DebugSymbol) isDebugSymbol() inout { return dsym == DSYM.debugSymbol ? cast(inout(DebugSymbol)) cast(void*) this : null; } inout(ClassDeclaration) isClassDeclaration() inout { return (dsym == DSYM.classDeclaration || dsym == DSYM.interfaceDeclaration) ? cast(inout(ClassDeclaration)) cast(void*) this : null; } - inout(StructDeclaration) isStructDeclaration() inout { return (dsym == DSYM.structDeclaration || dsym == DSYM.unionDeclaration) ? cast(inout(StructDeclaration)) cast(void*) this : null; } + inout(StructDeclaration) isStructDeclaration() inout { return (dsym == DSYM.structDeclaration || dsym == DSYM.unionDeclaration || dsym == DSYM.enumUnionDeclaration) ? cast(inout(StructDeclaration)) cast(void*) this : null; } inout(UnionDeclaration) isUnionDeclaration() inout { return dsym == DSYM.unionDeclaration ? cast(inout(UnionDeclaration)) cast(void*) this : null; } + inout(EnumUnionDeclaration) isEnumUnionDeclaration() inout { return dsym == DSYM.enumUnionDeclaration ? cast(inout(EnumUnionDeclaration)) cast(void*) this : null; } inout(InterfaceDeclaration) isInterfaceDeclaration() inout { return dsym == DSYM.interfaceDeclaration ? cast(inout(InterfaceDeclaration)) cast(void*) this : null; } inout(ScopeDsymbol) isScopeDsymbol() inout { switch (dsym) @@ -1050,6 +1055,7 @@ extern (C++) class Dsymbol : ASTNode case DSYM.aggregateDeclaration: case DSYM.structDeclaration: case DSYM.unionDeclaration: + case DSYM.enumUnionDeclaration: case DSYM.classDeclaration: case DSYM.interfaceDeclaration: case DSYM.withScopeSymbol: diff --git a/compiler/src/dmd/dsymbolsem.d b/compiler/src/dmd/dsymbolsem.d index 2e8225108b6c..0bf5c8c40e24 100644 --- a/compiler/src/dmd/dsymbolsem.d +++ b/compiler/src/dmd/dsymbolsem.d @@ -2091,6 +2091,526 @@ private void checkImportDeprecation(Module m, Loc loc, Scope* sc) eSink.deprecation(m.loc, "%s `%s` is deprecated", m.kind, m.toPrettyChars); } +private void collectEnumUnionCases(Dsymbols* symbols, Scope* sc, + ref EnumUnionVariant[] variants, Dsymbols* retained, Type forcedPayload = null, + bool discardLoopBindings = false) +{ + auto eSink = global.errorSink; + if (!symbols) + return; + + foreach (d; *symbols) + { + if (d.dsym == DSYM.enumUnionCaseDeclaration) + { + auto caseDecl = cast(EnumUnionCaseDeclaration)d; + auto variant = caseDecl.variant; + variant.payload = variant.payload.dup; + if (variant.payload.length) + { + if (forcedPayload) + variant.payload[0] = forcedPayload; + else + { + const typeName = variant.payload[0].toErrMsg(); + auto resolvedType = trySemantic(variant.payload[0], caseDecl.loc, sc); + if (!resolvedType) + { + eSink.error(caseDecl.loc, + "unknown type `%s`; for a named unit variant, use `case %s()`", + typeName, typeName); + variant.payload[0] = Type.terror; + variants ~= variant; + continue; + } + variant.payload[0] = resolvedType; + } + } + variant.generated = forcedPayload !is null; + if (variant.generated) + { + foreach (existing; variants) + { + if (existing.generated && !existing.ident && existing.payload.length == 1 && + variant.payload.length == 1 && existing.payload[0].equals(variant.payload[0])) + goto skipGeneratedCase; + } + } + variants ~= variant; + skipGeneratedCase: + continue; + } + if (auto sif = d.isStaticIfDeclaration()) + { + auto conditionScope = sc; + if (sif._scope != conditionScope) + sif.setScope(conditionScope); + if (auto sic = sif.condition.isStaticIfCondition()) + { + sic.inc = Include.notComputed; + } + auto conditionResult = dmd.expressionsem.include(sif.condition, conditionScope); + auto selected = conditionResult ? sif.decl : sif.elsedecl; + collectEnumUnionCases(selected, conditionScope, variants, retained, + forcedPayload, discardLoopBindings); + continue; + } + if (auto sfd = d.isStaticForeachDeclaration()) + { + if (!sfd.cached) + { + sfd.sfe.prepare(sfd._scope); + dmd.dsymbolsem.include(sfd, sc); + } + if (sfd.cache) + { + foreach (i, expanded; *sfd.cache) + { + if (auto fad = expanded.isForwardingAttribDeclaration()) + { + auto iterationScope = sc.push(fad.sym); + fad.decl.foreachDsymbol(s => s.setScope(iterationScope)); + Type payload; + foreach (member; *fad.decl) + { + if (auto loopAlias = member.isAliasDeclaration()) + { + payload = loopAlias.type; + if (!payload && loopAlias.aliassym) + payload = loopAlias.aliassym.isType(); + break; + } + } + collectEnumUnionCases(fad.decl, iterationScope, variants, retained, + payload, true); + } + } + } + continue; + } + if (d.isForwardingAttribDeclaration()) + { + auto fad = cast(ForwardingAttribDeclaration)d; + auto iterationScope = sc.push(fad.sym); + fad.decl.foreachDsymbol(s => s.setScope(iterationScope)); + collectEnumUnionCases(fad.decl, iterationScope, variants, retained, null, true); + continue; + } + if (discardLoopBindings && d.dsym == DSYM.pragmaDeclaration) + { + d.setScope(sc); + d.dsymbolSemantic(sc); + continue; + } + if (discardLoopBindings && d.isAliasDeclaration()) + continue; + retained.push(d); + } +} + +private void synthesizeEnumUnionFactories(EnumUnionDeclaration eu, Scope* sc) +{ + auto eSink = global.errorSink; + if (eu.parent && eu.parent.isTemplateDeclaration()) + return; + if (eu.enumUnionFactoriesSynthesized) + return; + eu.enumUnionFactoriesSynthesized = true; + if (eu.members.length < 2) + return; + + if (!(*eu.members)[1]) + return; + auto anon = (*eu.members)[1].isAnonDeclaration(); + if (!anon) + return; + auto tag = (*eu.members)[0].isVarDeclaration(); + // Any trailing declarations after the tag/payload-union are the enum + // union's own member declarations (functions, aliases, etc.), which must + // survive the members array being rebuilt below. + Dsymbol[] extraMembers = (*eu.members)[2 .. eu.members.length]; + bool hasErrors; + + Identifier variantName(ref EnumUnionVariant variant) + { + if (variant.ident) + return variant.ident; + if (variant.payload.length != 1) + return null; + + auto payloadType = variant.payload[0]; + if (auto identifierType = payloadType.isTypeIdentifier()) + return identifierType.ident; + payloadType = payloadType.toBasetype(); + if (auto structType = payloadType.isTypeStruct()) + return structType.sym.ident; + if (auto classType = payloadType.isTypeClass()) + return classType.sym.ident; + if (auto enumType = payloadType.isTypeEnum()) + return enumType.sym.ident; + return null; + } + + // Duplicate-case rule: no two variants (of any kind - unit, positional, + // or record) may share the same identifier, regardless of their payload + // types/signatures. Without this, two same-named variants with different + // signatures would just look like ordinary D function overloads to the + // synthesized factory functions, silently accepted instead of rejected. + foreach (i, variant; eu.variants) + { + auto ident = variantName(variant); + if (!ident) + continue; + bool isDup; + foreach (k; 0 .. i) + isDup = isDup || variantName(eu.variants[k]) == ident; + if (isDup) + continue; // already reported when this name was first seen + foreach (j; i + 1 .. eu.variants.length) + { + if (variantName(eu.variants[j]) == ident) + { + eSink.error(eu.loc, "duplicate case `%s` in enum union `%s`", + ident.toChars(), eu.toPrettyChars()); + hasErrors = true; + break; + } + } + } + + VarDeclaration[] payloadVars; + Dsymbols* payloadMembers = new Dsymbols(); + StructDeclaration unitPayloadType; + foreach (ref variant; eu.variants) + { + auto payloadType = variant.payload.length ? variant.payload[0] : null; + if (!variant.ident && variant.payload.length == 1 && payloadType) + { + const typeName = payloadType.toErrMsg(); + auto resolvedType = trySemantic(payloadType, variant.loc, sc); + if (!resolvedType) + { + eSink.error(variant.loc, + "unknown type `%s`; for a named unit variant, use `case %s()`", + typeName, typeName); + hasErrors = true; + continue; + } + variant.payload[0] = resolvedType; + payloadType = resolvedType; + } + auto typeOf = payloadType ? payloadType.isTypeTypeof() : null; + if (payloadType && (payloadType.ty == Terror || typeOf && typeOf.exp.op == EXP.error)) + { + hasErrors = true; + continue; + } + const isUnitVariant = !variant.payload.length && !variant.members; + StructDeclaration payloadStruct; + if (isUnitVariant) + { + if (!unitPayloadType) + { + unitPayloadType = new StructDeclaration(eu.loc, + Identifier.generateId("__enumUnitPayload"), false); + unitPayloadType.members = new Dsymbols(); + unitPayloadType.parent = eu; + unitPayloadType.dsymbolSemantic(sc); + if (unitPayloadType.errors || unitPayloadType.type.ty == Terror) + { + hasErrors = true; + continue; + } + } + payloadStruct = unitPayloadType; + } + else + { + payloadStruct = new StructDeclaration(eu.loc, + Identifier.generateId("__enumVariantPayload"), false); + payloadStruct.members = variant.members; + if (!payloadStruct.members) + { + payloadStruct.members = new Dsymbols(); + foreach (k, payload; variant.payload) + { + auto ident = k < variant.payloadNames.length && variant.payloadNames[k] + ? variant.payloadNames[k] : Identifier.generateId("__enumPayload"); + payloadStruct.members.push(new VarDeclaration(eu.loc, payload, ident, null)); + } + } + payloadStruct.parent = eu; + payloadStruct.dsymbolSemantic(sc); + if (payloadStruct.errors || payloadStruct.type.ty == Terror) + { + hasErrors = true; + continue; + } + } + variant.payloadType = payloadStruct; + foreach (field; payloadStruct.fields) + { + if (field.type.ty == Terror) + { + hasErrors = true; + break; + } + } + if (hasErrors) + continue; + auto payloadVar = new VarDeclaration(eu.loc, new TypeStruct(payloadStruct), + Identifier.generateId("__enumPayload"), null); + variant.payloadVar = payloadVar; + payloadVars ~= payloadVar; + payloadMembers.push(payloadVar); + } + + // Move-only rule: a variant whose payload has a move constructor but no + // copy constructor cannot be safely stored, since ordinary copies of the + // enum union (e.g. assignment, passing by value) perform a raw bitcopy of + // the payload union rather than invoking the move constructor, leading to + // double-destruction of the payload. + foreach (variant; eu.variants) + { + if (!variant.payloadType) + continue; + foreach (field; variant.payloadType.fields) + { + auto fieldType = field.type.baseElemOf().isTypeStruct(); + if (!fieldType) + continue; + if (fieldType.sym.hasMoveCtor && !fieldType.sym.hasCopyCtor) + { + eSink.error(eu.loc, + "cannot create enum union with element type `%s` that has a move constructor but no copy constructor", + fieldType.sym.toChars()); + hasErrors = true; + } + } + } + + // Duplicate-type rule: unlabeled (bare-type) variants must have distinct + // payload types; ambiguous constructions like `case double, case double,` + // are rejected at compile time. + foreach (i, variant; eu.variants) + { + if (variant.ident || variant.payload.length != 1 || + !variant.payloadType || !variant.payloadType.fields.length) + continue; + auto typeI = variant.payloadType.fields[0].type; + foreach (j; i + 1 .. eu.variants.length) + { + auto other = eu.variants[j]; + if (other.ident || other.payload.length != 1 || + !other.payloadType || !other.payloadType.fields.length) + continue; + auto typeJ = other.payloadType.fields[0].type; + if (typeI.equals(typeJ)) + { + eSink.error(eu.loc, "duplicate case `%s` in enum union `%s`", + typeI.toErrMsg(), eu.toPrettyChars()); + hasErrors = true; + break; + } + } + } + if (hasErrors) + { + anon.decl = payloadMembers; + eu.errors = true; + return; // don't synthesize factories for a broken declaration + } + + anon.decl = payloadMembers; + eu.members = new Dsymbols(); + eu.members.push(tag); + eu.members.push(anon); + + foreach (i, variant; eu.variants) + { + if (!variant.ident) + continue; + + auto parameters = new Parameters(); + auto aliasPayloadType = variant.isTypeAlias && variant.payloadType.fields.length + ? variant.payloadType.fields[0].type : null; + auto aliasStruct = aliasPayloadType ? aliasPayloadType.toBasetype().isTypeStruct() : null; + VarDeclaration[] aliasFields; + if (aliasStruct) + { + foreach (field; aliasStruct.sym.fields) + aliasFields ~= field; + if (!aliasFields.length && aliasStruct.sym.members) + foreach (member; *aliasStruct.sym.members) + if (auto field = member.isVarDeclaration()) + aliasFields ~= field; + } + const nfields = aliasStruct ? aliasFields.length + : variant.payloadType ? variant.payloadType.fields.length : 1; + foreach (k; 0 .. nfields) + { + auto pident = Identifier.generateId("__enumPayloadParam"); + if (k < variant.payloadNames.length && variant.payloadNames[k]) + pident = variant.payloadNames[k]; + else if (!aliasStruct && variant.payloadType && variant.payloadType.fields.length > k) + pident = variant.payloadType.fields[k].ident; + auto fieldType = aliasStruct ? aliasFields[k].type + : variant.payloadType ? variant.payloadType.fields[k].type : variant.payload[0]; + parameters.push(new Parameter(eu.loc, STC.none, fieldType, + pident, null, null, null)); + } + + const stc = nfields ? STC.none : STC.property; + auto functionType = new TypeFunction(ParameterList(parameters), eu.type, LINK.d, stc); + auto fd = new FuncDeclaration(eu.loc, eu.loc, variant.ident, STC.static_, functionType); + fd.isGenerated = true; + auto result = new VarDeclaration(eu.loc, eu.type, Identifier.generateId("__enumResult"), null); + Statements statements; + statements.push(new ExpStatement(eu.loc, result)); + auto tagExp = new DotVarExp(eu.loc, new VarExp(eu.loc, result), tag); + tagExp.type = tag.type; + statements.push(new ExpStatement(eu.loc, new AssignExp(eu.loc, tagExp, + new IntegerExp(eu.loc, i, Type.tuns8)))); + if (aliasStruct) + { + auto payloadExp = new DotVarExp(eu.loc, new VarExp(eu.loc, result), payloadVars[i]); + payloadExp.type = payloadVars[i].type; + auto payloadField = variant.payloadType.fields[0]; + payloadExp = new DotVarExp(eu.loc, payloadExp, payloadField); + payloadExp.type = payloadField.type; + auto arguments = new Expressions(); + foreach (parameter; *parameters) + arguments.push(new IdentifierExp(eu.loc, parameter.ident)); + auto literal = new StructLiteralExp(eu.loc, aliasStruct.sym, arguments, aliasPayloadType); + statements.push(new ExpStatement(eu.loc, new ConstructExp(eu.loc, payloadExp, literal))); + } + else foreach (k; 0 .. nfields) + { + Expression payloadExp = new DotVarExp(eu.loc, new VarExp(eu.loc, result), payloadVars[i]); + payloadExp.type = payloadVars[i].type; + if (variant.payloadType) + { + auto field = variant.payloadType.fields[k]; + payloadExp = new DotVarExp(eu.loc, payloadExp, field); + payloadExp.type = field.type; + } + statements.push(new ExpStatement(eu.loc, new ConstructExp(eu.loc, payloadExp, + new IdentifierExp(eu.loc, (*parameters)[k].ident)))); + } + statements.push(new ReturnStatement(eu.loc, new VarExp(eu.loc, result))); + fd.fbody = new CompoundStatement(eu.loc, statements.move()); + if (variant.udas) + { + auto declarations = new Dsymbols(fd); + eu.members.push(new UserAttributeDeclaration(variant.udas, declarations)); + } + else + eu.members.push(fd); + } + + foreach (m; extraMembers) + eu.members.push(m); +} + +private void synthesizeEnumUnionConstructors(EnumUnionDeclaration eu, Scope* sc) +{ + foreach (i, variant; eu.variants) + { + if ((variant.ident && !variant.isTypeAlias) || variant.payload.length != 1 || !variant.payloadType || + !variant.payloadType.fields.length || !variant.payloadVar) + continue; + + auto field = variant.payloadType.fields[0]; + auto parameter = new Parameter(eu.loc, STC.none, field.type, + Identifier.generateId("__enumPayloadParam"), null, null, null); + auto parameters = new Parameters(parameter); + auto functionType = new TypeFunction(ParameterList(parameters), eu.type, LINK.d, STC.ref_); + auto ctor = new CtorDeclaration(eu.loc, eu.loc, STC.ref_, functionType); + ctor.isGenerated = true; + + Statements statements; + auto tagExp = new DotVarExp(eu.loc, new ThisExp(eu.loc), eu.tagVar); + statements.push(new ExpStatement(eu.loc, new AssignExp(eu.loc, tagExp, + new IntegerExp(eu.loc, i, Type.tuns8)))); + + Expression payloadExp = new DotVarExp(eu.loc, new ThisExp(eu.loc), variant.payloadVar); + payloadExp = new DotVarExp(eu.loc, payloadExp, field); + statements.push(new ExpStatement(eu.loc, new ConstructExp(eu.loc, payloadExp, + new IdentifierExp(eu.loc, parameter.ident)))); + + ctor.fbody = new CompoundStatement(eu.loc, statements.move()); + eu.members.push(ctor); + ctor.addMember(sc, eu); + + Scope* sc2 = sc.push(); + if (variant.udas) + sc2.userAttribDecl = new UserAttributeDeclaration(variant.udas, null); + sc2.stc = STC.none; + sc2.linkage = LINK.d; + ctor.dsymbolSemantic(sc2); + ctor.semantic2(sc2); + ctor.semantic3(sc2); + sc2.pop(); + } +} + +private void synthesizeEnumUnionDtor(EnumUnionDeclaration eu, Scope* sc) +{ + if (eu.dtor) + return; + + CaseStatements cases; + bool hasDtor; + foreach (i, variant; eu.variants) + { + if (!variant.payloadType) + continue; + + Statements caseStatements; + foreach (field; variant.payloadType.fields) + { + auto fieldType = field.type.baseElemOf().isTypeStruct(); + if (!fieldType || !fieldType.sym.dtor) + continue; + + hasDtor = true; + auto payload = new DotVarExp(Loc.initial, new ThisExp(Loc.initial), variant.payloadVar); + auto fieldExp = new DotVarExp(Loc.initial, payload, field); + auto call = new CallExp(Loc.initial, + new DotVarExp(Loc.initial, fieldExp, fieldType.sym.dtor, false)); + call.directcall = true; + caseStatements.push(new ExpStatement(Loc.initial, call)); + } + if (!caseStatements.length) + continue; + caseStatements.push(new BreakStatement(Loc.initial, null)); + auto body = new CompoundStatement(Loc.initial, caseStatements.move()); + cases.push(new CaseStatement(Loc.initial, + new IntegerExp(Loc.initial, i, Type.tuns8), body)); + } + if (!hasDtor) + return; + + Statements bodyStatements; + foreach (c; cases) + bodyStatements.push(c); + bodyStatements.push(new DefaultStatement(Loc.initial, + new BreakStatement(Loc.initial, null))); + auto switchStatement = new SwitchStatement(Loc.initial, null, + new DotVarExp(Loc.initial, new ThisExp(Loc.initial), eu.tagVar), + new CompoundStatement(Loc.initial, bodyStatements.move()), false, Loc.initial); + Statements statements; + statements.push(switchStatement); + + auto dd = new DtorDeclaration(eu.loc, Loc.initial, STC.inference, Id.dtor); + dd.isGenerated = true; + dd.fbody = new CompoundStatement(Loc.initial, statements.move()); + eu.members.push(dd); + dd.addMember(sc, eu); + dd.dsymbolSemantic(sc); + eu.dtor = dd; +} + private extern(C++) final class DsymbolSemanticVisitor : Visitor { import dmd.typesem: size; @@ -2721,7 +3241,8 @@ private extern(C++) final class DsymbolSemanticVisitor : Visitor //printf("sc.stc = %x\n", sc.stc); //printf("storage_class = x%x\n", storage_class); - dsym.type.checkComplexTransition(dsym.loc, sc); + if (!sc.func || !sc.func.isGenerated) + dsym.type.checkComplexTransition(dsym.loc, sc); // Calculate type size + safety checks if (dsym.storage_class & STC.gshared && !dsym.isMember()) @@ -4509,6 +5030,16 @@ private extern(C++) final class DsymbolSemanticVisitor : Visitor TypeFunction tf = ctd.type.toTypeFunction(); immutable dim = tf.parameterList.length; auto sd = ad.isStructDeclaration(); + if (auto eu = ad.isEnumUnionDeclaration()) + { + if (!ctd.isGenerated && dim == 0 && tf.parameterList.varargs == VarArg.none) + { + eSink.error(ctd.loc, "enum union `%s` cannot have a no-argument constructor; use `.init` instead", + eu.toPrettyChars()); + ctd.errors = true; + return; + } + } /* See if it's the default constructor * But, template constructor should not become a default constructor. @@ -5025,6 +5556,43 @@ private extern(C++) final class DsymbolSemanticVisitor : Visitor */ sd.members.foreachDsymbol( s => s.setScope(sc2) ); sd.members.foreachDsymbol( s => s.importAll(sc2) ); + + if (auto eu = sd.isEnumUnionDeclaration()) + { + // Compile-time declarations must be expanded while the aggregate + // symbol table and forwarding scopes are live. Factory synthesis + // consumes the normalized variant list produced by this prepass. + if (!eu.enumUnionCasesExpanded) + { + eu.enumUnionCasesExpanded = true; + auto originalMembers = eu.members; + auto compileTimeMembers = new Dsymbols(); + foreach (i, member; *originalMembers) + { + if (i >= 2) + compileTimeMembers.push(member); + } + auto retainedMembers = new Dsymbols(); + collectEnumUnionCases(compileTimeMembers, sc2, eu.variants, retainedMembers); + eu.members = new Dsymbols(); + eu.members.push((*originalMembers)[0]); + eu.members.push((*originalMembers)[1]); + eu.members.append(retainedMembers); + + eu.symtab = new DsymbolTable(); + eu.members.foreachDsymbol(s => s.addMember(sc, eu)); + eu.members.foreachDsymbol(s => s.setScope(sc2)); + eu.members.foreachDsymbol(s => s.importAll(sc2)); + + synthesizeEnumUnionFactories(eu, sc2); + + eu.symtab = new DsymbolTable(); + eu.members.foreachDsymbol(s => s.addMember(sc, eu)); + eu.members.foreachDsymbol(s => s.setScope(sc2)); + eu.members.foreachDsymbol(s => s.importAll(sc2)); + } + } + sd.members.foreachDsymbol( (s) { s.dsymbolSemantic(sc2); if (sd.errors) s.errors = true; } ); if (sd.errors) @@ -5067,10 +5635,15 @@ private extern(C++) final class DsymbolSemanticVisitor : Visitor */ sd.disableNew = sd.search(Loc.initial, Id.classNew) !is null; + if (auto eu = sd.isEnumUnionDeclaration()) + synthesizeEnumUnionConstructors(eu, sc2); + // Look for the constructor sd.ctor = sd.searchCtor(); buildDtors(sd, sc2); + if (auto eu = sd.isEnumUnionDeclaration()) + synthesizeEnumUnionDtor(eu, sc2); bool hasCopyCtor; bool hasMoveCtor; diff --git a/compiler/src/dmd/expression.d b/compiler/src/dmd/expression.d index 94bd2d2b7af1..873115cc0383 100644 --- a/compiler/src/dmd/expression.d +++ b/compiler/src/dmd/expression.d @@ -450,6 +450,7 @@ extern (C++) abstract class Expression : ASTNode inout(EqualExp) isEqualExp() { return (op == EXP.equal || op == EXP.notEqual) ? cast(typeof(return))this : null; } 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(SwitchExp) isSwitchExp() { return op == EXP.switchExpression ? cast(typeof(return))this : null; } inout(GenericExp) isGenericExp() { return op == EXP._Generic ? 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; } @@ -3782,6 +3783,70 @@ extern (C++) final class CondExp : BinExp } } +/*********************************************************** + * Expression-position switch with pattern/action arms. + */ +struct CaseExpArm +{ + Loc loc; + Expression pattern; + Type typePattern; + Identifier typeBinding; + Identifier[] recordBindings; + bool hasRestPattern; + Identifier[] recordPatternNames; + Expression[] recordPatterns; + Identifier restBinding; + Expression[] patternChecks; + Expression guard; + bool isDefault; + Expression action; + VarDeclaration[] bindings; + size_t variantIndex; + bool hasVariant; +} + +extern (C++) final class SwitchExp : Expression +{ + Expression condition; + CaseExpArm[] arms; + bool hasDefault; + + extern (D) this(Loc loc, Expression condition, CaseExpArm[] arms, bool hasDefault) + { + super(loc, EXP.switchExpression); + this.condition = condition; + this.arms = arms; + this.hasDefault = hasDefault; + } + + override SwitchExp syntaxCopy() + { + auto copiedArms = new CaseExpArm[](arms.length); + foreach (i, arm; arms) + { + copiedArms[i] = arm; + copiedArms[i].pattern = arm.pattern ? arm.pattern.syntaxCopy() : null; + copiedArms[i].typePattern = arm.typePattern ? arm.typePattern.syntaxCopy() : null; + copiedArms[i].recordBindings = arm.recordBindings.dup; + copiedArms[i].hasRestPattern = arm.hasRestPattern; + copiedArms[i].recordPatternNames = arm.recordPatternNames.dup; + copiedArms[i].recordPatterns = arm.recordPatterns.dup; + copiedArms[i].restBinding = arm.restBinding; + copiedArms[i].patternChecks = arm.patternChecks.dup; + copiedArms[i].guard = arm.guard ? arm.guard.syntaxCopy() : null; + copiedArms[i].isDefault = arm.isDefault; + copiedArms[i].action = arm.action ? arm.action.syntaxCopy() : null; + } + return new SwitchExp(loc, condition.syntaxCopy(), copiedArms, hasDefault); + } + + override void accept(Visitor v) + { + v.visit(this); + } +} + /*********************************************************** * A special keyword when used as a function's default argument * @@ -4127,6 +4192,7 @@ alias ExpOpTypePairs = AliasSeq! OpType!(EXP.dot, DotExp), OpType!(EXP.comma, CommaExp), OpType!(EXP.question, CondExp), + OpType!(EXP.switchExpression, SwitchExp), OpType!(EXP.andAnd, LogicalExp), OpType!(EXP.orOr, LogicalExp), OpType!(EXP.prePlusPlus, PreExp), diff --git a/compiler/src/dmd/expressionsem.d b/compiler/src/dmd/expressionsem.d index 666753aff7ca..d22b66c6cfef 100644 --- a/compiler/src/dmd/expressionsem.d +++ b/compiler/src/dmd/expressionsem.d @@ -36,6 +36,7 @@ import dmd.declaration; import dmd.dclass; import dmd.dcast; import dmd.delegatize; +import dmd.decisiontree; import dmd.denum; import dmd.deps; import dmd.dimport; @@ -111,6 +112,8 @@ void merge(Scope* _this, Loc loc, const ref CtorFlow ctorflow) { auto eSink = global.errorSink; + mergeThisInitialized(_this.ctorflow.thisInitialized, ctorflow.thisInitialized, + _this.ctorflow.callSuper, ctorflow.callSuper); if (!mergeCallSuper(_this.ctorflow.callSuper, ctorflow.callSuper)) eSink.error(loc, "one path skips constructor"); @@ -5804,6 +5807,17 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor FuncDeclaration fd = hasThis(sc); // fd is the uplevel function with the 'this' variable AggregateDeclaration ad; + if (fd && fd.isCtorDeclaration() && !fd.isGenerated && !sc.allowUninitializedThis && + !sc.ctorflow.thisInitialized && !(sc.ctorflow.callSuper & CSX.this_ctor)) + { + if (auto eu = fd.isMemberLocal().isEnumUnionDeclaration()) + { + eSink.error(e.loc, "cannot read `this` in constructor `%s` before it is initialized", + fd.toPrettyChars()); + return setError(); + } + } + /* Special case for typeof(this) and typeof(super) since both * should work even if they are not inside a non-static member function */ @@ -7967,6 +7981,13 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor if (t1.ty == Tstruct) { auto sd = (cast(TypeStruct)t1).sym; + if (exp.e1.op == EXP.type && sd.isEnumUnionDeclaration() && + (!exp.arguments || exp.arguments.length == 0)) + { + eSink.error(exp.loc, "enum union `%s` cannot be constructed with no arguments; use `.init` instead", + sd.toPrettyChars()); + return setError(); + } sd.size(exp.loc); // Resolve forward references to construct object if (sd.sizeok != Sizeok.done) return setError(); @@ -12233,6 +12254,10 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor Expression e1old = exp.e1; + const assignsThis = exp.op == EXP.assign && exp.e1.isThisExp() && + sc.func && sc.func.isCtorDeclaration() && + sc.func.isMemberLocal().isEnumUnionDeclaration(); + if (auto e2comma = exp.e2.isCommaExp()) { if (!e2comma.isGenerated && !sc.inCfile) @@ -12362,6 +12387,10 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor /* Run this.e1 semantic. */ { + const allowUninitializedThis = sc.allowUninitializedThis; + if (assignsThis) + sc.allowUninitializedThis = true; + scope (exit) sc.allowUninitializedThis = allowUninitializedThis; Expression e1x = exp.e1; /* With UFCS, e.f = value @@ -13507,6 +13536,8 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor ce.e2 = lowerArrayAssign(ae2, true); } + if (assignsThis && !res.isErrorExp()) + sc.ctorflow.thisInitialized = true; return setResult(res); } @@ -15540,6 +15571,566 @@ private extern (C++) final class ExpressionSemanticVisitor : Visitor result = exp; } + override void visit(SwitchExp exp) + { + exp.condition = exp.condition.expressionSemantic(sc); + exp.condition = resolveProperties(sc, exp.condition); + if (exp.condition.op == EXP.error) + return setError(); + + EnumUnionDeclaration enumUnion; + if (auto ts = exp.condition.type.toBasetype().isTypeStruct()) + enumUnion = ts.sym.isEnumUnionDeclaration(); + + Type resultType; + bool semanticDefaultSeen; + foreach (ref arm; exp.arms) + { + if (arm.isDefault) + { + if (semanticDefaultSeen) + { + eSink.error(arm.loc, "duplicate `default` arm in switch expression"); + return setError(); + } + semanticDefaultSeen = true; + } + Scope* armScope = sc.push(new ScopeDsymbol()); + if (!arm.isDefault && enumUnion) + { + auto eu = enumUnion; + { + Identifier variantId; + Expressions* arguments; + ArgumentLabels* argumentNames; + bool isCallPattern; + bool bindsNamedVariant; + if (arm.pattern) + { + if (auto call = arm.pattern.isCallExp()) + { + isCallPattern = true; + argumentNames = call.names; + if (auto id = call.e1.isIdentifierExp()) + variantId = id.ident; + else if (auto dot = call.e1.isDotIdExp()) + { + auto callee = call.e1.expressionSemantic(armScope); + if (callee.op == EXP.error) + return setError(); + variantId = dot.ident; + } + arguments = call.arguments; + } + else if (auto id = arm.pattern.isIdentifierExp()) + variantId = id.ident; + else if (auto dot = arm.pattern.isDotIdExp()) + { + auto qualified = arm.pattern.expressionSemantic(armScope); + if (qualified.op == EXP.error) + return setError(); + if (auto typeExp = qualified.isTypeExp()) + arm.typePattern = typeExp.type; + else + variantId = dot.ident; + } + } + + if (arm.typePattern && arm.typeBinding) + { + if (auto typeId = arm.typePattern.isTypeIdentifier()) + { + foreach (variant; eu.variants) + { + if (variant.ident == typeId.ident) + { + variantId = typeId.ident; + arm.typePattern = null; + bindsNamedVariant = true; + break; + } + } + } + } + + if (arm.typePattern) + arm.typePattern = arm.typePattern.typeSemantic(arm.loc, armScope); + + // An identifier-only arm first denotes a named variant. If + // there is no such variant, resolve it as an unbound bare + // payload type (e.g. `case MyStruct =>`). + if (!arm.typePattern && variantId && !isCallPattern) + { + bool hasNamedVariant; + foreach (variant; eu.variants) + hasNamedVariant = hasNamedVariant || variant.ident == variantId; + if (!hasNamedVariant) + { + auto typePattern = new TypeIdentifier(arm.loc, variantId); + arm.typePattern = typePattern.typeSemantic(arm.loc, armScope); + } + } + + if (!arm.typePattern && !variantId) + { + if (isCallPattern) + eSink.error(arm.loc, "switch expression call pattern requires a named variant callee"); + else + eSink.error(arm.loc, "switch expression value and expression patterns are not supported; use a named variant or type pattern"); + return setError(); + } + + foreach (variantIndex, variant; eu.variants) + { + if (arm.typePattern) + { + auto payloadType = variant.payloadType && variant.payloadType.fields.length + ? variant.payloadType.fields[0].type + : variant.payload.length ? variant.payload[0] : null; + if ((variant.ident && !variant.isTypeAlias) || variant.payload.length != 1 || !payloadType || + !payloadType.equals(arm.typePattern)) + continue; + } + else if (variant.ident != variantId) + continue; + arm.hasVariant = true; + arm.variantIndex = variantIndex; + if (arm.typeBinding) + { + auto variable = new VarDeclaration(arm.loc, + bindsNamedVariant ? variant.payloadVar.type : arm.typePattern, + arm.typeBinding, null); + if (bindsNamedVariant) + { + auto payload = new DotVarExp(arm.loc, exp.condition, variant.payloadVar); + payload.type = variant.payloadVar.type; + variable._init = new ExpInitializer(arm.loc, payload); + } + else + { + auto payloadVar = variant.payloadVar; + auto payload = new DotVarExp(arm.loc, exp.condition, payloadVar); + payload.type = payloadVar.type; + if (variant.payloadType && variant.payloadType.fields.length) + { + auto field = variant.payloadType.fields[0]; + auto value = new DotVarExp(arm.loc, payload, field); + value.type = field.type; + variable._init = new ExpInitializer(arm.loc, value); + } + else if (variant.payload.length == 1) + { + auto value = new DotVarExp(arm.loc, exp.condition, payloadVar); + value.type = payloadVar.type; + variable._init = new ExpInitializer(arm.loc, value); + } + } + variable.dsymbolSemantic(armScope); + armScope.insert(variable); + arm.bindings ~= variable; + } + else if (arguments) + { + const fieldCount = variant.payloadType ? variant.payloadType.fields.length : 0; + if (arm.hasRestPattern) + { + if (arm.restBinding) + { + Expressions* restValues = new Expressions(); + auto payloadVar = variant.payloadVar; + auto payload = new DotVarExp(arm.loc, exp.condition, payloadVar); + payload.type = payloadVar.type; + foreach (field; variant.payloadType.fields) + { + auto fieldValue = new DotVarExp(arm.loc, payload, field); + fieldValue.type = field.type; + restValues.push(fieldValue); + } + auto rest = new VarDeclaration(arm.loc, null, arm.restBinding, + new ExpInitializer(arm.loc, new TupleExp(arm.loc, restValues))); + rest.dsymbolSemantic(armScope); + armScope.insert(rest); + arm.bindings ~= rest; + } + break; + } + if (arguments.length != fieldCount) + { + eSink.error(arm.loc, + "pattern for variant `%s` has %llu argument(s), expected %llu", + variant.ident.toChars(), cast(ulong) arguments.length, cast(ulong) fieldCount); + return setError(); + } + bool[] used = new bool[](fieldCount); + size_t nextField; + foreach (i, argument; *arguments) + { + auto label = argumentNames && i < argumentNames.length + ? (*argumentNames)[i].name : null; + size_t fieldIndex = size_t.max; + if (label) + { + foreach (j, field; variant.payloadType.fields) + if (field.ident == label) + { + fieldIndex = j; + break; + } + } + else + { + while (nextField < used.length && used[nextField]) + ++nextField; + if (nextField < used.length) + fieldIndex = nextField++; + } + if (fieldIndex == size_t.max || used[fieldIndex]) + { + eSink.error(arm.loc, "no such or duplicate field `%s` in variant pattern", + label ? label.toChars() : ""); + return setError(); + } + used[fieldIndex] = true; + auto field = variant.payloadType.fields[fieldIndex]; + auto binding = argument.isIdentifierExp(); + if (binding && !label) + { + auto variable = new VarDeclaration(binding.loc, field.type, + binding.ident, null); + auto payloadVar = variant.payloadVar; + auto payload = new DotVarExp(binding.loc, exp.condition, payloadVar); + payload.type = payloadVar.type; + auto value = new DotVarExp(binding.loc, payload, field); + value.type = field.type; + variable._init = new ExpInitializer(binding.loc, value); + variable.dsymbolSemantic(armScope); + armScope.insert(variable); + arm.bindings ~= variable; + continue; + } + argument = argument.expressionSemantic(armScope); + auto payloadVar = variant.payloadVar; + auto payload = new DotVarExp(arm.loc, exp.condition, payloadVar); + payload.type = payloadVar.type; + auto value = new DotVarExp(arm.loc, payload, field); + value.type = field.type; + auto check = new EqualExp(EXP.equal, arm.loc, value, argument); + check.type = Type.tbool; + arm.patternChecks ~= check; + } + } + else if (arm.recordBindings.length || arm.recordPatternNames.length || arm.hasRestPattern) + { + if (!variant.payloadType) + continue; + auto aliasPayloadType = variant.isTypeAlias && variant.payloadType.fields.length + ? variant.payloadType.fields[0].type : null; + auto recordType = aliasPayloadType && aliasPayloadType.toBasetype().isTypeStruct() + ? aliasPayloadType.toBasetype().isTypeStruct().sym : variant.payloadType; + VarDeclaration[] recordFields; + foreach (field; recordType.fields) + recordFields ~= field; + if (!recordFields.length && recordType.members) + foreach (member; *recordType.members) + if (auto field = member.isVarDeclaration()) + recordFields ~= field; + if (!arm.hasRestPattern && arm.recordBindings.length + arm.recordPatternNames.length != recordFields.length) + { + eSink.error(arm.loc, + "record pattern for variant `%s` must list all fields or use `...`", + variant.ident ? variant.ident.toChars() : "variant"); + return setError(); + } + bool[] usedFields = new bool[](recordFields.length); + foreach (fieldName; arm.recordBindings) + foreach (i, field; recordFields) + if (field.ident == fieldName) + usedFields[i] = true; + foreach (i, fieldName; arm.recordPatternNames) + { + size_t fieldIndex = size_t.max; + foreach (j, field; recordFields) + if (field.ident == fieldName) + { + fieldIndex = j; + break; + } + if (fieldIndex == size_t.max || usedFields[fieldIndex]) + { + eSink.error(arm.loc, "no such or duplicate field `%s` in variant pattern", + fieldName.toChars()); + return setError(); + } + usedFields[fieldIndex] = true; + auto payloadVar = variant.payloadVar; + auto payload = new DotVarExp(arm.loc, exp.condition, payloadVar); + payload.type = payloadVar.type; + auto payloadField = variant.isTypeAlias ? variant.payloadType.fields[0] : null; + if (payloadField) + { + payload = new DotVarExp(arm.loc, payload, payloadField); + payload.type = payloadField.type; + } + auto fieldValue = new DotVarExp(arm.loc, payload, recordFields[fieldIndex]); + fieldValue.type = recordFields[fieldIndex].type; + auto patternValue = arm.recordPatterns[i]; + auto patternBinding = patternValue.isIdentifierExp(); + if (!patternBinding) + patternValue = patternValue.expressionSemantic(armScope); + if (patternBinding) + { + auto variable = new VarDeclaration(patternBinding.loc, + recordFields[fieldIndex].type, patternBinding.ident, null); + variable._init = new ExpInitializer(patternBinding.loc, fieldValue); + variable.dsymbolSemantic(armScope); + armScope.insert(variable); + arm.bindings ~= variable; + } + else + { + auto check = new EqualExp(EXP.equal, arm.loc, fieldValue, patternValue); + check.type = Type.tbool; + arm.patternChecks ~= check; + } + } + if (arm.restBinding) + { + Expressions* restValues = new Expressions(); + auto payloadVar = variant.payloadVar; + auto payload = new DotVarExp(arm.loc, exp.condition, payloadVar); + payload.type = payloadVar.type; + auto payloadField = variant.isTypeAlias ? variant.payloadType.fields[0] : null; + if (payloadField) + { + payload = new DotVarExp(arm.loc, payload, payloadField); + payload.type = payloadField.type; + } + foreach (i, field; recordFields) + if (!usedFields[i]) + { + auto fieldValue = new DotVarExp(arm.loc, payload, field); + fieldValue.type = field.type; + restValues.push(fieldValue); + } + auto rest = new VarDeclaration(arm.loc, null, arm.restBinding, + new ExpInitializer(arm.loc, new TupleExp(arm.loc, restValues))); + rest.dsymbolSemantic(armScope); + armScope.insert(rest); + arm.bindings ~= rest; + } + foreach (bindingName; arm.recordBindings) + { + VarDeclaration field; + foreach (candidate; recordFields) + { + if (candidate.ident == bindingName) + { + field = candidate; + break; + } + } + if (!field) + { + eSink.error(arm.loc, "record pattern field `%s` is not a field of `%s`", + bindingName.toChars(), variant.ident ? variant.ident.toChars() : "variant"); + return setError(); + } + auto variable = new VarDeclaration(arm.loc, field.type, + bindingName, null); + auto payloadVar = variant.payloadVar; + auto payload = new DotVarExp(arm.loc, exp.condition, payloadVar); + payload.type = payloadVar.type; + auto value = new DotVarExp(arm.loc, payload, field); + value.type = field.type; + variable._init = new ExpInitializer(arm.loc, value); + variable.dsymbolSemantic(armScope); + armScope.insert(variable); + arm.bindings ~= variable; + } + } + break; + } + if (!arm.hasVariant) + { + if (isCallPattern) + eSink.error(arm.loc, "switch expression call pattern `%s` does not name a variant of `%s`", + variantId.toChars(), eu.toPrettyChars()); + else + eSink.error(arm.loc, "switch expression pattern does not match any variant of `%s`", + eu.toPrettyChars()); + return setError(); + } + } + } + else if (!arm.isDefault) + { + eSink.error(arm.loc, "switch expression patterns require an enum union condition"); + armScope.pop(); + return setError(); + } + if (arm.guard) + { + arm.guard = arm.guard.expressionSemantic(armScope); + arm.guard = resolveProperties(armScope, arm.guard); + arm.guard = arm.guard.toBoolean(armScope); + if (arm.guard.op == EXP.error) + return setError(); + } + arm.action = arm.action.expressionSemantic(armScope); + armScope.pop(); + if (arm.action.op == EXP.error) + return setError(); + if (!resultType || arm.action.type.ty == Tnoreturn) + resultType = resultType ? resultType : arm.action.type; + else if (resultType.ty == Tnoreturn) + resultType = arm.action.type; + else if (resultType != arm.action.type) + { + eSink.error(arm.loc, "switch expression arms must have the same type, not `%s` and `%s`", + resultType.toErrMsg(), arm.action.type.toErrMsg()); + return setError(); + } + } + if (!resultType) + { + eSink.error(exp.loc, "switch expression must have at least one arm"); + return setError(); + } + if (auto ts = exp.condition.type.toBasetype().isTypeStruct()) + if (auto eu = ts.sym.isEnumUnionDeclaration()) + if (!checkExhaustivenessAndRedundancy(exp, eu, eSink)) + return setError(); + exp.type = resultType; + Expression defaultAction; + foreach (ref arm; exp.arms) + { + if (arm.isDefault) + defaultAction = arm.action; + } + Expression lowered; + foreach_reverse (ref arm; exp.arms) + { + if (arm.isDefault) + continue; + Expression action = arm.action; + if (arm.hasVariant) + { + auto eu = exp.condition.type.toBasetype().isTypeStruct().sym.isEnumUnionDeclaration(); + auto variant = eu.variants[arm.variantIndex]; + auto tagVar = (*eu.members)[0].isVarDeclaration(); + auto anon = (*eu.members)[1].isAnonDeclaration(); + auto payloadVar = (*anon.decl)[arm.variantIndex].isVarDeclaration(); + if (!payloadVar || !payloadVar.type || !payloadVar.type.toBasetype().isTypeStruct()) + { + eSink.error(arm.loc, "unable to resolve switch expression payload"); + return setError(); + } + auto payloadType = payloadVar.type.toBasetype().isTypeStruct().sym; + auto aliasPayloadType = variant.isTypeAlias && variant.payloadType.fields.length + ? variant.payloadType.fields[0].type : null; + auto aliasStruct = aliasPayloadType ? aliasPayloadType.toBasetype().isTypeStruct() : null; + auto recordType = aliasStruct ? aliasStruct.sym : payloadType; + VarDeclaration[] recordFields; + foreach (field; recordType.fields) + recordFields ~= field; + if (!recordFields.length && recordType.members) + foreach (member; *recordType.members) + if (auto field = member.isVarDeclaration()) + recordFields ~= field; + // Bindings must be declared before the guard runs (the guard may + // reference them), so when a guard is present the declarations are + // threaded through the guard instead of the action; the action then + // just reuses the same (already-declared) binding variables. + Expression guardExpr = arm.guard; + foreach_reverse (binding; arm.bindings) + { + if (!binding._init) + { + VarDeclaration field; + foreach (candidate; recordFields) + { + if (candidate.ident == binding.ident) + { + field = candidate; + break; + } + } + if (!field && !aliasStruct && payloadType.members) + { + foreach (member; *payloadType.members) + { + if (auto candidate = member.isVarDeclaration()) + { + if (candidate.ident == binding.ident) + { + field = candidate; + break; + } + } + } + } + if (!field) + { + eSink.error(arm.loc, "unable to resolve switch pattern payload field `%s`", + binding.ident.toChars()); + return setError(); + } + auto payload = new DotVarExp(arm.loc, exp.condition, payloadVar); + payload.type = payloadVar.type; + if (aliasStruct) + { + auto payloadField = payloadType.fields[0]; + payload = new DotVarExp(arm.loc, payload, payloadField); + payload.type = payloadField.type; + } + auto value = new DotVarExp(arm.loc, payload, field); + value.type = field.type; + binding._init = new ExpInitializer(arm.loc, value); + } + auto declaration = new DeclarationExp(arm.loc, binding); + if (guardExpr) + { + guardExpr = new CommaExp(arm.loc, declaration, guardExpr); + guardExpr.type = Type.tbool; + } + else + { + action = new CommaExp(arm.loc, declaration, action); + action.type = arm.action.type; + } + } + auto tag = new DotVarExp(arm.loc, exp.condition, tagVar); + tag.type = tagVar.type; + Expression match = new EqualExp(EXP.equal, arm.loc, tag, + new IntegerExp(arm.loc, arm.variantIndex, Type.tuns8)); + match.type = Type.tbool; + foreach_reverse (check; arm.patternChecks) + { + match = new LogicalExp(arm.loc, EXP.andAnd, match, check); + match.type = Type.tbool; + } + if (guardExpr) + { + match = new LogicalExp(arm.loc, EXP.andAnd, match, guardExpr); + match.type = Type.tbool; + } + if (lowered) + lowered = new CondExp(arm.loc, match, action, lowered); + else if (arm.guard || defaultAction) + lowered = new CondExp(arm.loc, match, action, defaultAction); + else + lowered = action; // last arm of an exhaustive match with no `default` + if (lowered.op == EXP.question) + lowered.type = resultType; + } + } + if (!lowered) + lowered = defaultAction; + if (!lowered) + return setError(); + result = lowered; + } + override void visit(CondExp exp) { static if (LOGSEMANTIC) diff --git a/compiler/src/dmd/hdrgen.d b/compiler/src/dmd/hdrgen.d index d02bb60e575c..29ed1fa8f297 100644 --- a/compiler/src/dmd/hdrgen.d +++ b/compiler/src/dmd/hdrgen.d @@ -3083,6 +3083,81 @@ private void expressionPrettyPrint(Expression e, ref OutBuffer buf, ref HdrGenSt expToBuffer(e.e2, PREC.cond, buf, hgs); } + void visitSwitch(SwitchExp e) + { + buf.put("switch ("); + expToBuffer(e.condition, PREC.expr, buf, hgs); + buf.put(") {"); + foreach (arm; e.arms) + { + buf.put(" "); + if (arm.isDefault) + buf.put("default"); + else + { + buf.put("case "); + if (arm.typePattern) + { + typeToBuffer(arm.typePattern, null, buf, hgs); + if (arm.typeBinding) + { + buf.put(" "); + buf.put(arm.typeBinding.toString()); + } + } + else if (arm.pattern) + expToBuffer(arm.pattern, PREC.assign, buf, hgs); + if (arm.recordBindings.length || arm.recordPatternNames.length || arm.hasRestPattern) + { + buf.put(" {"); + bool needsComma; + foreach (binding; arm.recordBindings) + { + if (needsComma) + buf.put(","); + buf.put(" "); + buf.put(binding.toString()); + needsComma = true; + } + foreach (i, name; arm.recordPatternNames) + { + if (needsComma) + buf.put(","); + buf.put(" "); + buf.put(name.toString()); + buf.put(": "); + expToBuffer(arm.recordPatterns[i], PREC.assign, buf, hgs); + needsComma = true; + } + if (arm.hasRestPattern) + { + if (needsComma) + buf.put(","); + buf.put(" "); + if (arm.restBinding) + { + buf.put(arm.restBinding.toString()); + buf.put("..."); + } + else + buf.put("..."); + } + buf.put(" }"); + } + } + if (arm.guard) + { + buf.put(" if ("); + expToBuffer(arm.guard, PREC.expr, buf, hgs); + buf.put(")"); + } + buf.put(" => "); + expToBuffer(arm.action, PREC.assign, buf, hgs); + buf.put(","); + } + buf.put(" }"); + } + void visitDefaultInit(DefaultInitExp e) { buf.put(Token.toString(e.tok)); @@ -3178,6 +3253,7 @@ private void expressionPrettyPrint(Expression e, ref OutBuffer buf, ref HdrGenSt case EXP.prePlusPlus: return visitPre(e.isPreExp()); case EXP.remove: return visitRemove(e.isRemoveExp()); case EXP.question: return visitCond(e.isCondExp()); + case EXP.switchExpression: return visitSwitch(e.isSwitchExp()); case EXP.classReference: return visitClassReference(e.isClassReferenceExp()); case EXP.loweredAssignExp: return visitLoweredAssignExp(e.isLoweredAssignExp()); case EXP.construct: return visitConstructExp(e.isConstructExp()); @@ -3927,6 +4003,8 @@ private Expression arrowFuncLiteralResult(FuncLiteralDeclaration f) // to be called if e could be loweredFrom another expression instead of acessing precedence[e.op] directly private PREC expPrecedence(ref HdrGenState hgs, Expression e) { + if (e.op == EXP.switchExpression) + return PREC.assign; if (!hgs.vcg_ast) { if (auto ce = e.isCallExp()) @@ -4708,6 +4786,7 @@ string EXPtoString(EXP op) EXP.assocArrayLiteral : "assocarrayliteral", EXP.classReference : "classreference", EXP.defaultInit : "defaultinit", + EXP.switchExpression : "switchExpression", EXP.typeid_ : "typeid", EXP.is_ : "is", EXP.assert_ : "assert", diff --git a/compiler/src/dmd/parse.d b/compiler/src/dmd/parse.d index 49674ec5216e..c31fb8a57c9e 100644 --- a/compiler/src/dmd/parse.d +++ b/compiler/src/dmd/parse.d @@ -14,7 +14,6 @@ module dmd.parse; import core.stdc.stdio; -import core.stdc.string; import dmd.astenums; import dmd.errorsink; @@ -388,20 +387,42 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer switch (token.value) { + case TOK.case_: + if (pLastDecl && *pLastDecl && ((*pLastDecl).dsym == AST.DSYM.enumUnionDeclaration || + (*pLastDecl).dsym == AST.DSYM.enumUnionCaseDeclaration || + (*pLastDecl).dsym == AST.DSYM.staticIfDeclaration || + (*pLastDecl).dsym == AST.DSYM.staticForeachDeclaration || + (*pLastDecl).dsym == AST.DSYM.pragmaDeclaration)) + { + const loc = token.loc; + nextToken(); + AST.EnumUnionVariant variant; + variant.payload ~= parseType(); + if (!variant.payload.length) + { + error(loc, "enum union variant type expected"); + break; + } + check(TOK.semicolon); + s = new AST.EnumUnionCaseDeclaration(loc, variant); + break; + } + goto default; + case TOK.enum_: { /* Determine if this is a manifest constant declaration, * or a conventional enum. */ const tv = peekNext(); - if (tv == TOK.leftCurly || tv == TOK.colon) + if (tv == TOK.union_ || tv == TOK.leftCurly || tv == TOK.colon) s = parseEnum(); else if (tv != TOK.identifier) goto Ldeclaration; else { const nextv = peekNext2(); - if (nextv == TOK.leftCurly || nextv == TOK.colon || nextv == TOK.semicolon) + if (nextv == TOK.union_ || nextv == TOK.leftCurly || nextv == TOK.colon || nextv == TOK.semicolon) s = parseEnum(); else goto Ldeclaration; @@ -3328,20 +3349,30 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer /************************************* */ - private AST.EnumDeclaration parseEnum() + private AST.Dsymbol parseEnum() { - AST.EnumDeclaration e; + AST.EnumDeclaration e = null; + AST.EnumUnionDeclaration eu = null; Identifier id; AST.Type memtype; + AST.TemplateParameters* tpl = null; + bool isUnion = false; auto loc = token.loc; // printf("Parser::parseEnum()\n"); nextToken(); + if (token.value == TOK.union_) + { + isUnion = true; + nextToken(); + } id = null; if (token.value == TOK.identifier) { id = token.ident; nextToken(); + if (isUnion && token.value == TOK.leftParenthesis) + tpl = parseTemplateParameterList(); } memtype = null; @@ -3355,10 +3386,150 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer checkCstyleTypeSyntax(typeLoc, memtype, alt, null); } - e = new AST.EnumDeclaration(loc, id, memtype); + if (isUnion) + eu = new AST.EnumUnionDeclaration(loc, id); + else + e = new AST.EnumDeclaration(loc, id, memtype); // opaque type if (token.value == TOK.semicolon && id) nextToken(); + else if (isUnion && token.value == TOK.leftCurly) + { + eu.variants = []; + auto memberDecls = new AST.Dsymbols(); + nextToken(); + while (token.value != TOK.rightCurly && token.value != TOK.endOfFile) + { + const variantLoc = token.loc; + AST.EnumUnionVariant variant; + variant.loc = variantLoc; + + while (token.value == TOK.at) + { + if (STC stc = parseAttribute(variant.udas)) + { + error(variantLoc, "attribute `%s` is not allowed on enum union variants", + token.toChars()); + nextToken(); + } + } + + if (token.value == TOK.static_ || token.value == TOK.pragma_) + { + AST.Dsymbol lastDecl = cast(AST.Dsymbol) eu; + auto declarations = parseDeclDefs(1, &lastDecl); + if (declarations) + memberDecls.append(declarations); + continue; + } + + if (token.value == TOK.case_) + { + nextToken(); + if (token.value == TOK.identifier) + { + if (peekNext() == TOK.assign) + { + variant.ident = token.ident; + variant.isTypeAlias = true; + nextToken(); + nextToken(); + if (token.value == TOK.identifier && token.ident is variant.ident && + (peekNext() == TOK.comma || peekNext() == TOK.semicolon || + peekNext() == TOK.rightCurly)) + { + error(variantLoc, "`case %s = %s` cannot alias itself, use a qualified name", + variant.ident.toChars(), token.ident.toChars()); + } + variant.payload ~= parseType(); + if (!variant.payload.length) + { + error(variantLoc, "enum union variant type expected"); + break; + } + } + else + { + if (peekNext() != TOK.leftParenthesis && peekNext() != TOK.leftCurly) + variant.payload ~= parseType(); + else + { + variant.ident = token.ident; + nextToken(); + if (token.value == TOK.leftParenthesis) + { + nextToken(); + while (token.value != TOK.rightParenthesis && token.value != TOK.endOfFile) + { + Identifier payloadIdent; + variant.payload ~= parseType(&payloadIdent); + variant.payloadNames ~= payloadIdent; + if (token.value != TOK.comma) + break; + nextToken(); + } + check(TOK.rightParenthesis); + } + else if (token.value == TOK.leftCurly) + { + nextToken(); + variant.members = parseDeclDefs(0); + check(TOK.rightCurly); + } + } + } + } + else + { + variant.payload ~= parseType(); + if (!variant.payload.length) + { + error(variantLoc, "enum union variant name expected"); + break; + } + } + } + else + { + error(variantLoc, "`case` expected for enum union variant"); + break; + } + + eu.variants ~= variant; + if (token.value == TOK.comma) + nextToken(); + else if (token.value == TOK.semicolon) + break; // `;` introduces a trailing MemberDeclarationList + else if (token.value != TOK.rightCurly) + error(token.loc, "`,` or `}` expected after enum union variant"); + } + if (token.value == TOK.semicolon) + { + nextToken(); + auto declarations = parseDeclDefs(0); + if (declarations) + memberDecls.append(declarations); + } + check(TOK.rightCurly); + + if (eu.variants.length > 256) + error(loc, "enum union cannot have more than 256 variants"); + + eu.tagVar = new AST.VarDeclaration(loc, AST.Type.tuns8, Id.__tag, null); + eu.payloadUnion = new AST.UnionDeclaration(loc, null); + eu.payloadUnion.members = new AST.Dsymbols(); + foreach (variant; eu.variants) + { + foreach (i, payload; variant.payload) + eu.payloadUnion.members.push(new AST.VarDeclaration( + loc, payload, Identifier.generateId("__enumPayload"), null)); + } + eu.members = new AST.Dsymbols(); + eu.members.push(eu.tagVar); + eu.members.push(new AST.AnonDeclaration(loc, true, eu.payloadUnion.members)); + if (memberDecls) + eu.members.append(memberDecls); + } else if (token.value == TOK.leftCurly) { bool isAnonymousEnum = !id; @@ -3529,7 +3700,13 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer error("expected `{`, not `%s` for enum declaration", token.toChars()); } //printf("-parseEnum() %s\n", e.toChars()); - return e; + if (tpl) + { + auto members = new AST.Dsymbols(); + members.push(isUnion ? cast(AST.Dsymbol) eu : cast(AST.Dsymbol) e); + return new AST.TemplateDeclaration(loc, id, tpl, null, members); + } + return isUnion ? cast(AST.Dsymbol) eu : cast(AST.Dsymbol) e; } /******************************** @@ -4615,12 +4792,12 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer case TOK.enum_: { const tv = peekNext(); - if (tv == TOK.leftCurly || tv == TOK.colon) + if (tv == TOK.union_ || tv == TOK.leftCurly || tv == TOK.colon) break; if (tv == TOK.identifier) { const nextv = peekNext2(); - if (nextv == TOK.leftCurly || nextv == TOK.colon || nextv == TOK.semicolon) + if (nextv == TOK.union_ || nextv == TOK.leftCurly || nextv == TOK.colon || nextv == TOK.semicolon) break; } stc = STC.manifest; @@ -4752,6 +4929,27 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer auto a = new AST.Dsymbols(); a.push(d); + if (d.dsym == AST.DSYM.enumUnionDeclaration) + { + if (storage_class) + { + d = new AST.StorageClassDeclaration(storage_class, a); + a = new AST.Dsymbols(); + a.push(d); + } + if (setAlignment) + { + d = new AST.AlignDeclaration(d.loc, ealign, a); + a = new AST.Dsymbols(); + a.push(d); + } + if (link != linkage) + { + d = new AST.LinkDeclaration(linkloc, link, a); + a = new AST.Dsymbols(); + a.push(d); + } + } if (udas) { d = new AST.UserAttributeDeclaration(udas, a); @@ -6296,7 +6494,8 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer * we check if the next token is a semicolon and simply output the error, * otherwise we fall back on the old path (advancing the token). */ - if (token.value != TOK.semicolon && peek(&token).value == TOK.semicolon) + if (token.value != TOK.semicolon && + (token.value == TOK.rightCurly || peek(&token).value == TOK.semicolon)) error("found `%s` when expecting `;` following expression", token.toChars()); else { @@ -6442,7 +6641,7 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer */ AST.Dsymbol d; const tv = peekNext(); - if (tv == TOK.leftCurly || tv == TOK.colon) + if (tv == TOK.union_ || tv == TOK.leftCurly || tv == TOK.colon) d = parseEnum(); else if (tv != TOK.identifier) goto Ldeclaration; @@ -6762,8 +6961,39 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer break; } case TOK.switch_: - isfinal = false; - goto Lswitch; + { + auto afterCondition = peekPastParen(peek(&token)); + if (afterCondition.value == TOK.leftCurly) + { + size_t nesting; + for (auto lookahead = peek(afterCondition); lookahead.value != TOK.endOfFile; + lookahead = peek(lookahead)) + { + if (lookahead.value == TOK.leftParenthesis || lookahead.value == TOK.leftBracket || + lookahead.value == TOK.leftCurly) + ++nesting; + else if (lookahead.value == TOK.rightParenthesis || lookahead.value == TOK.rightBracket || + lookahead.value == TOK.rightCurly) + { + if (!nesting) + break; + --nesting; + } + else if (!nesting && lookahead.value == TOK.goesTo) + { + auto exp = parsePrimaryExp(); + s = new AST.ExpStatement(loc, exp); + break; + } + else if (!nesting && lookahead.value == TOK.colon) + break; + } + } + if (s) + break; + isfinal = false; + goto Lswitch; + } Lswitch: { @@ -8463,6 +8693,191 @@ class Parser(AST, Lexer = dmd.lexer.Lexer) : Lexer //printf("parsePrimaryExp(): loc = %d\n", loc.linnum); switch (token.value) { + case TOK.switch_: + { + nextToken(); + check(TOK.leftParenthesis); + auto condition = parseExpression(); + check(TOK.rightParenthesis); + check(TOK.leftCurly); + + AST.CaseExpArm[] arms; + bool hasDefault; + while (token.value != TOK.rightCurly && token.value != TOK.endOfFile) + { + auto armLoc = token.loc; + AST.Expression pattern; + AST.Type typePattern; + Identifier typeBinding; + Identifier[] recordBindings; + bool hasRestPattern; + Identifier[] recordPatternNames; + AST.Expression[] recordPatterns; + Identifier restBinding; + bool isDefault; + if (token.value == TOK.case_) + { + nextToken(); + Token* patternEnd = &token; + if ((token.value != TOK.identifier && isBasicType(&patternEnd)) || + (token.value == TOK.identifier && + (peekNext() == TOK.identifier || peekNext() == TOK.not))) + { + typePattern = parseType(&typeBinding); + } + else if (token.value == TOK.identifier) + { + auto patternLoc = token.loc; + pattern = new AST.IdentifierExp(patternLoc, token.ident); + nextToken(); + while (token.value == TOK.dot) + { + const dotLoc = token.loc; + nextToken(); + if (token.value != TOK.identifier) + { + error(token.loc, "identifier expected following `.` in switch expression pattern"); + break; + } + pattern = new AST.DotIdExp(dotLoc, pattern, token.ident); + nextToken(); + } + if (token.value == TOK.leftCurly) + { + nextToken(); + while (1) + { + if (token.value == TOK.rightCurly) + { + nextToken(); + break; + } + if (token.value == TOK.identifier) + { + auto fieldName = token.ident; + if (peekNext() == TOK.colon) + { + nextToken(); + nextToken(); + recordPatternNames ~= fieldName; + recordPatterns ~= parseAssignExp(); + } + else if (peekNext() == TOK.dotDotDot) + { + restBinding = fieldName; + hasRestPattern = true; + nextToken(); + nextToken(); + } + else + { + recordBindings ~= fieldName; + nextToken(); + } + } + else if (token.value == TOK.dotDotDot) + { + hasRestPattern = true; + nextToken(); + } + else if (token.value == TOK.comma) + { + nextToken(); + } + else if (token.value == TOK.endOfFile) + { + break; + } + else + { + break; + } + } + } + } + else + pattern = parsePrimaryExp(); + while (token.value == TOK.leftParenthesis) + { + auto args = new AST.Expressions(); + auto names = new AST.ArgumentLabels(); + if (peekNext() == TOK.dotDotDot) + { + nextToken(); + nextToken(); + check(TOK.rightParenthesis); + hasRestPattern = true; + } + else if (peekNext() == TOK.identifier && peekNext2() == TOK.dotDotDot) + { + nextToken(); + restBinding = token.ident; + nextToken(); + nextToken(); + check(TOK.rightParenthesis); + hasRestPattern = true; + } + else + parseNamedArguments(args, names); + pattern = new AST.CallExp(pattern.loc, pattern, args, names); + } + } + else if (token.value == TOK.default_) + { + hasDefault = true; + isDefault = true; + nextToken(); + } + else + { + error(armLoc, "`case` or `default` expected in switch expression"); + goto Lerr; + } + AST.Expression guard; + if (token.value == TOK.if_) + { + if (isDefault) + error(token.loc, "`default` arm cannot have an `if` guard"); + nextToken(); + check(TOK.leftParenthesis); + guard = parseExpression(); + check(TOK.rightParenthesis); + } + if (token.value != TOK.goesTo) + { + error(token.loc, "`=>` expected in switch expression arm"); + goto Lerr; + } + nextToken(); + auto action = parseAssignExp(); + if (typePattern) + pattern = new AST.TypeExp(armLoc, typePattern); + auto arm = AST.CaseExpArm(); + arm.loc = armLoc; + arm.pattern = pattern; + arm.typePattern = typePattern; + arm.typeBinding = typeBinding; + arm.recordBindings = recordBindings; + arm.hasRestPattern = hasRestPattern; + arm.recordPatternNames = recordPatternNames; + arm.recordPatterns = recordPatterns; + arm.restBinding = restBinding; + arm.guard = guard; + arm.isDefault = isDefault; + arm.action = action; + arms ~= arm; + if (token.value == TOK.comma) + nextToken(); + else if (token.value != TOK.rightCurly) + { + error(token.loc, "`,` or `}` expected after switch expression arm"); + goto Lerr; + } + } + check(TOK.rightCurly); + e = new AST.SwitchExp(loc, condition, arms, hasDefault); + break; + } case TOK.identifier: { if (peekNext() == TOK.arrow) diff --git a/compiler/src/dmd/statementsem.d b/compiler/src/dmd/statementsem.d index d5518ca42ce5..dda53fb4309b 100644 --- a/compiler/src/dmd/statementsem.d +++ b/compiler/src/dmd/statementsem.d @@ -242,11 +242,20 @@ Statement statementSemanticVisit(Statement s, Scope* sc) result = s; return; } + + bool hasSwitchExpressionSideEffect(Expression exp) + { + if (auto call = exp.isCallExp()) + if (call.f && call.f.isGenerated && call.f.parent.isEnumUnionDeclaration()) + return false; + return hasSideEffect(exp); + } //printf("ExpStatement::semantic() %s\n", exp.toChars()); // Allow CommaExp in ExpStatement because return isn't used CommaExp.allow(s.exp); + auto switchExp = s.exp.isSwitchExp(); s.exp = s.exp.expressionSemantic(sc); s.exp = resolveProperties(sc, s.exp); s.exp = s.exp.addDtorHook(sc); @@ -259,8 +268,24 @@ Statement statementSemanticVisit(Statement s, Scope* sc) } if (checkMustUse(s.exp, sc)) s.exp = ErrorExp.get(); - if (!sc.inCfile && discardValue(s.exp)) - s.exp = ErrorExp.get(); + if (!sc.inCfile) + { + if (switchExp) + { + bool hasEffect = hasSwitchExpressionSideEffect(switchExp.condition); + foreach (arm; switchExp.arms) + hasEffect = hasEffect || hasSideEffect(arm.action) || + (arm.guard && hasSideEffect(arm.guard)); + if (!hasEffect) + { + eSink.error(switchExp.loc, + "switch expression has no effect; use `cast(void)` to discard its value"); + s.exp = ErrorExp.get(); + } + } + else if (discardValue(s.exp)) + s.exp = ErrorExp.get(); + } s.exp = s.exp.optimize(WANTvalue); s.exp = s.exp.checkGC(sc); diff --git a/compiler/src/dmd/tokens.d b/compiler/src/dmd/tokens.d index eff7c68681fe..33b8bd5cbc23 100644 --- a/compiler/src/dmd/tokens.d +++ b/compiler/src/dmd/tokens.d @@ -377,6 +377,7 @@ enum EXP : ubyte dot, comma, question, + switchExpression, andAnd, orOr, prePlusPlus, diff --git a/compiler/src/dmd/visitor/parsetime.d b/compiler/src/dmd/visitor/parsetime.d index 8109a599701a..c5c5c58ec3fa 100644 --- a/compiler/src/dmd/visitor/parsetime.d +++ b/compiler/src/dmd/visitor/parsetime.d @@ -204,6 +204,7 @@ public: void visit(AST.TupleExp e) { visit(cast(AST.Expression)e); } void visit(AST.ThisExp e) { visit(cast(AST.Expression)e); } void visit(AST.GenericExp e) { visit(cast(AST.Expression)e); } + void visit(AST.SwitchExp e) { visit(cast(AST.Expression)e); } // Miscellaneous void visit(AST.VarExp e) { visit(cast(AST.SymbolExp)e); } diff --git a/compiler/test/compilable/enum_union_alias_bare_type.d b/compiler/test/compilable/enum_union_alias_bare_type.d new file mode 100644 index 000000000000..967ddab2117b --- /dev/null +++ b/compiler/test/compilable/enum_union_alias_bare_type.d @@ -0,0 +1,25 @@ +class C1 {} +class C2 : C1 {} + +enum union Pointers +{ + case First = C1, + case Second = C2, +} + +string classify(Pointers value) +{ + return switch (value) + { + case C1 => "C1", + case C2 => "C2", + }; +} + +void main() +{ + assert(classify(Pointers(new C1)) == "C1"); + assert(classify(Pointers(new C2)) == "C2"); + assert(classify(Pointers.First(new C1)) == "C1"); + assert(classify(Pointers.Second(new C2)) == "C2"); +} \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_bare_construct_pointer.d b/compiler/test/compilable/enum_union_bare_construct_pointer.d new file mode 100644 index 000000000000..ea92fb6f1d53 --- /dev/null +++ b/compiler/test/compilable/enum_union_bare_construct_pointer.d @@ -0,0 +1,21 @@ +enum union Pointers +{ + case int*, + case bool*, +} + +int classify(Pointers pointers) +{ + return switch (pointers) + { + case int* => 1, + case bool* => 2, + }; +} + +void main() +{ + int value; + auto pointers = Pointers(&value); + assert(classify(pointers) == 1); +} \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_case_uda.d b/compiler/test/compilable/enum_union_case_uda.d new file mode 100644 index 000000000000..9b90386c797b --- /dev/null +++ b/compiler/test/compilable/enum_union_case_uda.d @@ -0,0 +1,21 @@ +struct UDA {} + +enum union Named +{ + @UDA case Number(int), + case Empty(); +} + +static assert(__traits(getAttributes, Named.Number).length == 1); + +enum union Bare +{ + @UDA case int, + case bool; +} + +void main() +{ + Bare number = 1; + assert(number.__tag == 0); +} diff --git a/compiler/test/compilable/enum_union_complex_transition.d b/compiler/test/compilable/enum_union_complex_transition.d new file mode 100644 index 000000000000..d31033b00d97 --- /dev/null +++ b/compiler/test/compilable/enum_union_complex_transition.d @@ -0,0 +1,15 @@ +// REQUIRED_ARGS: -verrors=simple + +/* +TEST_OUTPUT: +--- +compilable/enum_union_complex_transition.d(11): Deprecation: use of complex type `cdouble` is deprecated, use `std.complex.Complex!(double)` instead +compilable/enum_union_complex_transition.d(11): Deprecation: use of imaginary type `idouble` is deprecated, use `double` instead +--- +*/ + +enum union Values +{ + case cdouble, + case idouble; +} diff --git a/compiler/test/compilable/enum_union_constructor_read_this_after_branch_init.d b/compiler/test/compilable/enum_union_constructor_read_this_after_branch_init.d new file mode 100644 index 000000000000..89b7a734ee3d --- /dev/null +++ b/compiler/test/compilable/enum_union_constructor_read_this_after_branch_init.d @@ -0,0 +1,21 @@ +enum union Pointers +{ + case int*, + case bool*; + + this(typeof(null) value, bool selectInt) + { + if (selectInt) + this = cast(int*) null; + else + this = cast(bool*) null; + + assert(switch (this) + { + case int* => true, + case bool* => true, + }); + } +} + +Pointers pointers = Pointers(null, true); \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_constructor_read_this_after_init.d b/compiler/test/compilable/enum_union_constructor_read_this_after_init.d new file mode 100644 index 000000000000..d101aa614ecd --- /dev/null +++ b/compiler/test/compilable/enum_union_constructor_read_this_after_init.d @@ -0,0 +1,17 @@ +enum union Pointers +{ + case int*, + case bool*; + + this(typeof(null) value) + { + this = cast(int*) null; + assert(switch (this) + { + case int* => true, + case bool* => false, + }); + } +} + +Pointers pointers = null; \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_default_init.d b/compiler/test/compilable/enum_union_default_init.d new file mode 100644 index 000000000000..dc2cbf73cc45 --- /dev/null +++ b/compiler/test/compilable/enum_union_default_init.d @@ -0,0 +1,10 @@ +enum union Value +{ + case Number(int), +} + +void main() +{ + Value value = Value.init; + assert(value.__tag == 0); +} \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_identifier_patterns.d b/compiler/test/compilable/enum_union_identifier_patterns.d new file mode 100644 index 000000000000..0ed1e8d17f06 --- /dev/null +++ b/compiler/test/compilable/enum_union_identifier_patterns.d @@ -0,0 +1,53 @@ +class C1 {} +class C2 : C1 {} +struct S {} + +enum union Bare +{ + case C1, + case C2, + case S, +} + +enum union Named +{ + case Unit(), + case Tuple(int), +} + +string classifyBare(Bare value) +{ + return switch (value) + { + case C1 => "C1", + case C2 => "C2", + case S => "S", + }; +} + +string classifyNamed(Named value) +{ + return switch (value) + { + case Unit => "unit", + case Tuple => "tuple", + }; +} + +string classifyUnitCall(Named value) +{ + return switch (value) + { + case Unit() => "unit", + case Tuple => "tuple", + }; +} + +void main() +{ + assert(classifyBare(Bare(new C2)) == "C2"); + assert(classifyBare(Bare(S())) == "S"); + assert(classifyNamed(Named.Unit) == "unit"); + assert(classifyNamed(Named.Tuple(1)) == "tuple"); + assert(classifyUnitCall(Named.Unit()) == "unit"); +} \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_named_pattern_binding.d b/compiler/test/compilable/enum_union_named_pattern_binding.d new file mode 100644 index 000000000000..81d13f85a69b --- /dev/null +++ b/compiler/test/compilable/enum_union_named_pattern_binding.d @@ -0,0 +1,33 @@ +enum union Value +{ + case Unit(), + case Pair(int left, int right), + case Record { int value; } +} + +enum union Units +{ + case First(), + case Second(), +} + +auto unitPayload(Units value) +{ + return switch (value) + { + case First payload => payload, + case Second payload => payload, + }; +} + +static assert(unitPayload(Units.First).sizeof == 1); + +int main() +{ + return switch (Value.Unit) + { + case Unit value => cast(int) value.sizeof, + case Pair value => value.left + value.right, + case Record value => value.value, + }; +} \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_network_event.d b/compiler/test/compilable/enum_union_network_event.d new file mode 100644 index 000000000000..5964fff14c14 --- /dev/null +++ b/compiler/test/compilable/enum_union_network_event.d @@ -0,0 +1,83 @@ +module net.event_processor; + +import core.stdc.stdio; +import std.format : format; + +// 1. Hybrid Tagged Union: Primitives, Slices, Tuples, and Named Records +enum union NetworkEvent +{ + // Bare primitive & slice types (types act directly as discriminant tags) + case int, // Raw error code + case ubyte[], // Unparsed raw payload buffer + + // Unit variants + case Disconnected(), + case Heartbeat(), + + // Positional (tuple-like) variants + case Ping(ulong timestamp, ushort sequenceId), + + // Named record variants + case HttpRequest { string method; string path; ushort statusCode; }; + + // Embedded methods + string summary() const @safe + { + // Switch expression with fat-arrow arms and comma separators + return switch (this) + { + case int errCode => format("Socket Error: %d", errCode), + case ubyte[] data => format("Raw Frame (%d bytes)", data.length), + case Disconnected() => "Connection Closed", + case Heartbeat() => "Keep-Alive ACK", + case Ping(ts, seq) => format("Ping [seq=%d, ts=%d]", seq, ts), + case HttpRequest { method, path, statusCode } => format("%s %s -> %d", method, path, statusCode), + }; + } +} + +// 2. Request Dispatcher demonstrating elimination and return type unification +struct ConnectionHandler +{ + ulong activeSessionId; + + // Dispatches an incoming event and computes an action response code + int handleEvent(NetworkEvent event) @safe + { + // All arms strictly unify via Least Upper Bound (LUB) + return switch (event) + { + case int err => err < 0 ? err : -1, + case ubyte[] frame => processFrame(frame), + case Heartbeat() => 0, + case Ping(ts, seq) => sendPong(ts, seq), + case HttpRequest { statusCode, ... } => cast(int) statusCode, // Partial record destructuring + case Disconnected() => throw new Exception("Terminating disconnected session"), + }; + } + + private int processFrame(const ubyte[] frame) @safe pure nothrow => 200; + private int sendPong(ulong ts, ushort seq) @safe nothrow => 1; +} + +void main() +{ + // Supports assignment-style construction + NetworkEvent e1 = 404; + ubyte[] payload = [0xDE, 0xAD, 0xBE, 0xEF]; + NetworkEvent e2 = payload; + + // Labeled variant construction via synthesized static factories for named case variants + NetworkEvent e3 = NetworkEvent.Heartbeat; + NetworkEvent e4 = NetworkEvent.Ping(1_700_000_000, 42); + NetworkEvent e5 = NetworkEvent.HttpRequest("GET", "/api/v1/status", 200); + + auto handler = ConnectionHandler(1001); + + assert(handler.handleEvent(e1) == -1); + assert(handler.handleEvent(e3) == 0); + assert(handler.handleEvent(e4) == 1); + assert(handler.handleEvent(e5) == 200); + assert(e2.summary() == "Raw Frame (4 bytes)"); + assert(e5.summary() == "GET /api/v1/status -> 200"); +} \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_qualified_patterns.d b/compiler/test/compilable/enum_union_qualified_patterns.d new file mode 100644 index 000000000000..f82143649dd2 --- /dev/null +++ b/compiler/test/compilable/enum_union_qualified_patterns.d @@ -0,0 +1,55 @@ +module enum_union_qualified_patterns; + +class C1 {} +class C2 : C1 {} + +enum union Value +{ + case C1, + case C3 = C2, + case Unit(), + case Tuple(int), +} + +string classify(Value value) +{ + return switch (value) + { + case C1 => "C1", + case Value.C3 => "C2", + case Value.Unit => "unit", + case Value.Tuple => "tuple", + }; +} + +string classifyByType(Value value) +{ + return switch (value) + { + case C1 => "C1", + case enum_union_qualified_patterns.C2 => "C2", + case Value.Unit => "unit", + case Value.Tuple => "tuple", + }; +} + +string classifyExplicitUnit(Value value) +{ + return switch (value) + { + case Value.Unit() => "unit", + case Value.Tuple => "tuple", + case C1 => "C1", + case C2 => "C2", + }; +} + +void main() +{ + assert(classify(Value(new C1)) == "C1"); + assert(classify(Value(new C2)) == "C2"); + assert(classify(Value.Unit) == "unit"); + assert(classify(Value.Tuple(1)) == "tuple"); + assert(classifyByType(Value(new C2)) == "C2"); + assert(classifyExplicitUnit(Value.Unit()) == "unit"); +} \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_static_local.d b/compiler/test/compilable/enum_union_static_local.d new file mode 100644 index 000000000000..fa28382d670c --- /dev/null +++ b/compiler/test/compilable/enum_union_static_local.d @@ -0,0 +1,11 @@ +void main() +{ + static enum union Value + { + case int, + case string; + } + + Value value = 1; + assert(value.__tag == 0); +} \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_switch_property_condition.d b/compiler/test/compilable/enum_union_switch_property_condition.d new file mode 100644 index 000000000000..4a1953c7e7e0 --- /dev/null +++ b/compiler/test/compilable/enum_union_switch_property_condition.d @@ -0,0 +1,16 @@ +enum union Value +{ + case Unit(), + case Number(int), +} + +string describe() +{ + return switch (Value.Unit) + { + case Unit => "unit", + case Number => "number", + }; +} + +static assert(describe() == "unit"); \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_test.d b/compiler/test/compilable/enum_union_test.d new file mode 100644 index 000000000000..55328547a351 --- /dev/null +++ b/compiler/test/compilable/enum_union_test.d @@ -0,0 +1,126 @@ +module enumsunion_test; + +import core.stdc.stdio; + +// 1. Basic Unit & Positional Variants +enum union Option(T) { + case Some(T), + case None(), +} + +// 2. Hybrid Union: Bare Types, Records, and Unit Sentinels +enum union Response { + case double, // Bare type + case string, // Bare type + case Timeout(), // Unit variant + case Success { int code; string payload; } // Record variant +} + +// 3. RAII Resource Tracking +struct Resource { + int id; + static int liveCount = 0; + this(int id) { this.id = id; liveCount++; } + this(ref typeof(this) rhs) { this.id = rhs.id; liveCount++; } + ~this() { liveCount--; } +} + +enum union Managed { + case Handle(Resource), + case Empty(), +} + +// 4. Field access across variants +enum union Entity { + case Player { int id; string name; }, + case Monster { int id; int hp; }, +} + +struct Box(T) { + T value; +} + +struct Struct { + int n; +} + +enum union GenericValue { + case Box!int, + case Struct = .Struct, + case Done(), +} + +void main() { + // Test Option Matching & Type Unification + Option!int opt = Option!int.Some(42); + int val = switch (opt) { + case Some(v) => v * 2, + case None() => 0, + }; + assert(val == 84); + + // Test 'noreturn' arm unification + int safeVal = switch (opt) { + case Some(v) => v, + case None() => throw new Exception("Empty"), // Unifies: LUB(int, noreturn) -> int + }; + assert(safeVal == 42); + + // Test Bare Type Matching and Direct Construction + Response r1 = 3.14; + Response r2 = Response.Success(200, "OK"); + Response r3 = Response.Timeout; + string status = switch (r1) { + case double d => "Floating-point", + case string s => "String", + case Timeout() => "Timeout", + case Success { code, ... } => "Success", + }; + assert(status == "Floating-point"); + + int successCode = switch (r2) { + case double d => -1, + case string s => -2, + case Timeout() => -3, + case Success { code, ... } => code, + }; + assert(successCode == 200); + + // Test RAII Destruction and Re-tagging + { + assert(Resource.liveCount == 0); + Managed m = Managed.Handle(Resource(1)); + assert(Resource.liveCount == 1); + + m = Managed.Empty; // Overwrite must invoke Resource.~this() + assert(Resource.liveCount == 0); + } + assert(Resource.liveCount == 0); + + // Field access across variants goes through a switch, not direct `.field`. + Entity e = Entity.Player(10, "Hero"); + int id = switch (e) { + case Player { id, ... } => id, + case Monster { id, ... } => id, + }; + assert(id == 10); + + GenericValue generic = Box!int(7); + int genericValue = switch (generic) { + case Box!int box => box.value, + case Struct value => value.n, + case Done() => 0, + }; + assert(genericValue == 7); + + GenericValue structValue = Struct(42); + int structResult = switch (structValue) { + case Struct { n } => n, + case Box!int box => box.value, + case Done() => 0, + }; + assert(structResult == 42); + + GenericValue factoryValue = GenericValue.Struct(43); + assert(factoryValue.__tag == 1); +} \ No newline at end of file diff --git a/compiler/test/compilable/enum_union_uda.d b/compiler/test/compilable/enum_union_uda.d new file mode 100644 index 000000000000..a7de70e20a90 --- /dev/null +++ b/compiler/test/compilable/enum_union_uda.d @@ -0,0 +1,43 @@ +enum union Tag +{ + case Number(int), + case Empty(); +} + +enum union BareTag +{ + case int, + case bool; +} + +@(Tag.Number(42)) +struct NumberAnnotated {} + +@(Tag.Empty) +struct EmptyAnnotated {} + +@(BareTag(true)) +struct BareAnnotated {} + +enum numberAttribute = __traits(getAttributes, NumberAnnotated)[0]; +enum emptyAttribute = __traits(getAttributes, EmptyAnnotated)[0]; +enum bareAttribute = __traits(getAttributes, BareAnnotated)[0]; + +static assert(numberAttribute.__tag == 0); +static assert(emptyAttribute.__tag == 1); +static assert(bareAttribute.__tag == 1); + +void main() +{ + assert(switch (numberAttribute) + { + case Number(value) => value == 42, + case Empty() => false, + }); + + assert(switch (bareAttribute) + { + case int => false, + case bool => true, + }); +} diff --git a/compiler/test/compilable/enum_union_variant_grammar.d b/compiler/test/compilable/enum_union_variant_grammar.d new file mode 100644 index 000000000000..5b830b89ee32 --- /dev/null +++ b/compiler/test/compilable/enum_union_variant_grammar.d @@ -0,0 +1,87 @@ +struct ExternalStruct +{ + int id; +} + +struct Wrapper(T) +{ + T value; +} + +enum union Result(T) +{ + case Success(T), + case Failure(), + case ExternalStruct, + case Wrapper!int, + case string, +} + +enum union NamedUnit +{ + case ExternalStruct(), +} + +enum union VariantPack(Types...) +{ + case Empty(), + static foreach (T; Types) + case T; +} + +alias MyResult = Result!int; +alias MyPack = VariantPack!(ExternalStruct, int); + +int resultValue(MyResult result) +{ + return switch (result) + { + case Success(value) => value, + case Failure() => -1, + case ExternalStruct value => value.id, + case Wrapper!int value => value.value, + case string value => cast(int) value.length, + }; +} + +int packValue(MyPack value) +{ + return switch (value) + { + case Empty() => 0, + case ExternalStruct value => value.id, + case int value => value, + }; +} + +int namedUnitBare(NamedUnit value) +{ + return switch (value) + { + case ExternalStruct => 1, + }; +} + +int namedUnitCall(NamedUnit value) +{ + return switch (value) + { + case ExternalStruct() => 2, + }; +} + +void main() +{ + assert(resultValue(MyResult.Success(1)) == 1); + assert(resultValue(MyResult.Failure()) == -1); + assert(resultValue(ExternalStruct(10)) == 10); + assert(resultValue(Wrapper!int(20)) == 20); + assert(resultValue("hello") == 5); + assert(NamedUnit.ExternalStruct().__tag == 0); + assert(namedUnitBare(NamedUnit.ExternalStruct) == 1); + assert(namedUnitCall(NamedUnit.ExternalStruct()) == 2); + + assert(packValue(MyPack.Empty()) == 0); + assert(packValue(ExternalStruct(20)) == 20); + assert(packValue(30) == 30); +} \ No newline at end of file diff --git a/compiler/test/compilable/switch_expression_statement.d b/compiler/test/compilable/switch_expression_statement.d new file mode 100644 index 000000000000..ffd304f9fc74 --- /dev/null +++ b/compiler/test/compilable/switch_expression_statement.d @@ -0,0 +1,26 @@ +enum union Value +{ + case Unit(), + case Number(int), +} + +void consume() +{ +} + +void main() +{ + switch (Value.Unit) + { + case Unit => consume(), + case Number value => consume(), + } + + switch (0) + { + case 0: + break; + default: + break; + } +} \ No newline at end of file diff --git a/compiler/test/compilable/switch_expression_statement_side_effect.d b/compiler/test/compilable/switch_expression_statement_side_effect.d new file mode 100644 index 000000000000..0342b7d4349b --- /dev/null +++ b/compiler/test/compilable/switch_expression_statement_side_effect.d @@ -0,0 +1,18 @@ +enum union Value +{ + case Unit(), + case Number(int), +} + +void consume() +{ +} + +void main() +{ + switch (Value.Unit) + { + case Unit value => "unit", + case Number value => { consume(); return "number"; }(), + } +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_alias_case_self.d b/compiler/test/fail_compilation/enum_union_alias_case_self.d new file mode 100644 index 000000000000..655d2d2dc910 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_alias_case_self.d @@ -0,0 +1,13 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_alias_case_self.d(12): Error: `case C1 = C1` cannot alias itself, use a qualified name +--- +*/ + +class C1 {} + +enum union Pointers +{ + case C1 = C1, +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_bare_call_diff_signature.d b/compiler/test/fail_compilation/enum_union_bare_call_diff_signature.d new file mode 100644 index 000000000000..9f17f9030dab --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_bare_call_diff_signature.d @@ -0,0 +1,24 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_bare_call_diff_signature.d(23): Error: enum union `Funs` does not overload () +--- +*/ + +// Same as enum_union_bare_call_same_signature.d, but with two variants that +// have completely different callable signatures. Direct-call syntax is not +// supported at all currently, so this fails identically either way; even if +// it were ever added, differing signatures could never work since the call +// site needs one statically-known parameter/return type to type-check +// against. +enum union Funs +{ + case int function(int), + case void delegate(string), +} + +void test() +{ + Funs f = delegate(string s) {}; + f(); +} diff --git a/compiler/test/fail_compilation/enum_union_bare_call_same_signature.d b/compiler/test/fail_compilation/enum_union_bare_call_same_signature.d new file mode 100644 index 000000000000..a3de8dd30264 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_bare_call_same_signature.d @@ -0,0 +1,21 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_bare_call_same_signature.d(20): Error: enum union `Funs` does not overload () +--- +*/ + +// Calling an enum union value directly (`f(42)`) is not a supported feature: +// there is no synthesized `opCall`, so this fails regardless of whether the +// callable variants share the same signature. +enum union Funs +{ + case int function(int), + case int delegate(int), +} + +void test() +{ + Funs f = delegate(int x) { return 0; }; + assert(f(42) == 0); +} diff --git a/compiler/test/fail_compilation/enum_union_bare_construct_ambiguous_array.d b/compiler/test/fail_compilation/enum_union_bare_construct_ambiguous_array.d new file mode 100644 index 000000000000..341562f269a9 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_bare_construct_ambiguous_array.d @@ -0,0 +1,17 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_bare_construct_ambiguous_array.d(16): Error: `[]` is ambiguous between variants `int[]` and `void[]` of enum union `enum_union_bare_construct_ambiguous_array.Arrs` +--- +*/ + +enum union Arrs +{ + case int[], + case void[], +} + +void test() +{ + Arrs arrs = []; // [] implicitly converts to both int[] and void[] +} diff --git a/compiler/test/fail_compilation/enum_union_bare_construct_ambiguous_lambda.d b/compiler/test/fail_compilation/enum_union_bare_construct_ambiguous_lambda.d new file mode 100644 index 000000000000..d71018dbca9d --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_bare_construct_ambiguous_lambda.d @@ -0,0 +1,19 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_bare_construct_ambiguous_lambda.d(18): Error: `() { }` is ambiguous between variants `void function()` and `void delegate()` of enum union `enum_union_bare_construct_ambiguous_lambda.Funs` +--- +*/ + +enum union Funs +{ + case void function(), + case void delegate(), +} + +void test() +{ + // A non-capturing lambda literal is implicitly convertible to *either* a + // function pointer or a delegate with the same signature. + Funs f = (){}; +} diff --git a/compiler/test/fail_compilation/enum_union_bare_construct_ambiguous_null.d b/compiler/test/fail_compilation/enum_union_bare_construct_ambiguous_null.d new file mode 100644 index 000000000000..0de00d733f38 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_bare_construct_ambiguous_null.d @@ -0,0 +1,20 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_bare_construct_ambiguous_null.d(19): Error: `enum_union_bare_construct_ambiguous_null.Pointers.__ctor` called with argument types `(typeof(null))` matches multiple overloads after qualifier conversion: +fail_compilation/enum_union_bare_construct_ambiguous_null.d(11): `enum_union_bare_construct_ambiguous_null.Pointers.this(int* __enumPayloadParam11)` +and: +fail_compilation/enum_union_bare_construct_ambiguous_null.d(11): `enum_union_bare_construct_ambiguous_null.Pointers.this(bool* __enumPayloadParam12)` +--- +*/ + +enum union Pointers +{ + case int*, + case bool*, +} + +void main() +{ + Pointers pointers = Pointers(null); +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_bare_implicit_ambiguous_integral.d b/compiler/test/fail_compilation/enum_union_bare_implicit_ambiguous_integral.d new file mode 100644 index 000000000000..5024329d819f --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_bare_implicit_ambiguous_integral.d @@ -0,0 +1,17 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_bare_implicit_ambiguous_integral.d(16): Error: `0` is ambiguous between variants `int` and `long` of enum union `enum_union_bare_implicit_ambiguous_integral.Integral` +--- +*/ + +enum union Integral +{ + case int, + case long, +} + +void main() +{ + Integral value = 0; +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_bare_implicit_ambiguous_null.d b/compiler/test/fail_compilation/enum_union_bare_implicit_ambiguous_null.d new file mode 100644 index 000000000000..c8efe46687b6 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_bare_implicit_ambiguous_null.d @@ -0,0 +1,20 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_bare_implicit_ambiguous_null.d(19): Error: `enum_union_bare_implicit_ambiguous_null.Pointers.__ctor` called with argument types `(typeof(null))` matches multiple overloads after qualifier conversion: +fail_compilation/enum_union_bare_implicit_ambiguous_null.d(11): `enum_union_bare_implicit_ambiguous_null.Pointers.this(int* __enumPayloadParam11)` +and: +fail_compilation/enum_union_bare_implicit_ambiguous_null.d(11): `enum_union_bare_implicit_ambiguous_null.Pointers.this(bool* __enumPayloadParam12)` +--- +*/ + +enum union Pointers +{ + case int*, + case bool*, +} + +void main() +{ + Pointers pointers = null; +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_constructor_read_this_before_init.d b/compiler/test/fail_compilation/enum_union_constructor_read_this_before_init.d new file mode 100644 index 000000000000..fa72b294f042 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_constructor_read_this_before_init.d @@ -0,0 +1,21 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_constructor_read_this_before_init.d(15): Error: cannot read `this` in constructor `enum_union_constructor_read_this_before_init.Pointers.this` before it is initialized +--- +*/ + +enum union Pointers +{ + case int*, + case bool*; + + this(typeof(null) value) + { + this = switch (this) + { + case int* => cast(bool*) null, + case bool* => new bool(), + }; + } +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_constructor_read_this_partial_init.d b/compiler/test/fail_compilation/enum_union_constructor_read_this_partial_init.d new file mode 100644 index 000000000000..b1a77992738a --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_constructor_read_this_partial_init.d @@ -0,0 +1,24 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_constructor_read_this_partial_init.d(18): Error: cannot read `this` in constructor `enum_union_constructor_read_this_partial_init.Pointers.this` before it is initialized +--- +*/ + +enum union Pointers +{ + case int*, + case bool*; + + this(typeof(null) value, bool selectInt) + { + if (selectInt) + this = cast(int*) null; + + assert(switch (this) + { + case int* => true, + case bool* => true, + }); + } +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_disabled_copy_variant.d b/compiler/test/fail_compilation/enum_union_disabled_copy_variant.d new file mode 100644 index 000000000000..12c7f7f1c2a5 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_disabled_copy_variant.d @@ -0,0 +1,19 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_disabled_copy_variant.d(15): Error: copy constructor `enum_union_disabled_copy_variant.DisabledCopy.this` cannot be used because it is annotated with `@disable` +--- +*/ + +struct DisabledCopy +{ + int x; + @disable this(ref DisabledCopy); + this(int v) { x = v; } +} + +enum union HasDisabledCopy +{ + case Wrapped(DisabledCopy), + case Flag(bool), +} diff --git a/compiler/test/fail_compilation/enum_union_disabled_postblit_variant.d b/compiler/test/fail_compilation/enum_union_disabled_postblit_variant.d new file mode 100644 index 000000000000..410c8102fa2c --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_disabled_postblit_variant.d @@ -0,0 +1,18 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_disabled_postblit_variant.d(14): Error: struct `enum_union_disabled_postblit_variant.DisabledPostBlit` is not copyable because it has a disabled postblit +--- +*/ + +struct DisabledPostBlit +{ + int x; + @disable this(this); +} + +enum union HasDisabledPostBlit +{ + case Wrapped(DisabledPostBlit), + case Flag(bool), +} diff --git a/compiler/test/fail_compilation/enum_union_duplicate_bare_type.d b/compiler/test/fail_compilation/enum_union_duplicate_bare_type.d new file mode 100644 index 000000000000..cba57e6ad731 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_duplicate_bare_type.d @@ -0,0 +1,12 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_duplicate_bare_type.d(8): Error: duplicate case `double` in enum union `enum_union_duplicate_bare_type.LatLong` +--- +*/ + +enum union LatLong +{ + case double, + case double, +} diff --git a/compiler/test/fail_compilation/enum_union_duplicate_case_name.d b/compiler/test/fail_compilation/enum_union_duplicate_case_name.d new file mode 100644 index 000000000000..78a4712577dc --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_duplicate_case_name.d @@ -0,0 +1,14 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_duplicate_case_name.d(10): Error: duplicate case `StructVariant` in enum union `enum_union_duplicate_case_name.Test1` +--- +*/ + +// Two record variants sharing the same name are rejected, even though their +// bodies are identical (or, as tested separately, different). +enum union Test1 +{ + case StructVariant {}, + case StructVariant {}, +} diff --git a/compiler/test/fail_compilation/enum_union_duplicate_case_name_bare_type_positional.d b/compiler/test/fail_compilation/enum_union_duplicate_case_name_bare_type_positional.d new file mode 100644 index 000000000000..37de4fa03faa --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_duplicate_case_name_bare_type_positional.d @@ -0,0 +1,14 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_duplicate_case_name_bare_type_positional.d(10): Error: duplicate case `ExternalStruct` in enum union `enum_union_duplicate_case_name_bare_type_positional.Test` +--- +*/ + +struct ExternalStruct {} + +enum union Test +{ + case ExternalStruct, + case ExternalStruct(int, string), +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_duplicate_case_name_bare_type_unit.d b/compiler/test/fail_compilation/enum_union_duplicate_case_name_bare_type_unit.d new file mode 100644 index 000000000000..31d85a9a713a --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_duplicate_case_name_bare_type_unit.d @@ -0,0 +1,14 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_duplicate_case_name_bare_type_unit.d(10): Error: duplicate case `ExternalStruct` in enum union `enum_union_duplicate_case_name_bare_type_unit.Test` +--- +*/ + +struct ExternalStruct {} + +enum union Test +{ + case ExternalStruct, + case ExternalStruct(), +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_duplicate_case_name_diff_fields.d b/compiler/test/fail_compilation/enum_union_duplicate_case_name_diff_fields.d new file mode 100644 index 000000000000..bb875a7f8728 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_duplicate_case_name_diff_fields.d @@ -0,0 +1,16 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_duplicate_case_name_diff_fields.d(12): Error: duplicate case `StructVariant` in enum union `enum_union_duplicate_case_name_diff_fields.Test2` +--- +*/ + +// Same variant name reused with DIFFERENT record fields must still be +// rejected: without an explicit identifier-uniqueness check, this used to +// silently compile since the synthesized factory functions merely looked +// like two ordinary (differently-signatured) D function overloads. +enum union Test2 +{ + case StructVariant { int id; }, + case StructVariant { string s; }, +} diff --git a/compiler/test/fail_compilation/enum_union_duplicate_case_name_mixed_kind.d b/compiler/test/fail_compilation/enum_union_duplicate_case_name_mixed_kind.d new file mode 100644 index 000000000000..dd204c937aba --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_duplicate_case_name_mixed_kind.d @@ -0,0 +1,14 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_duplicate_case_name_mixed_kind.d(10): Error: duplicate case `Variant1` in enum union `enum_union_duplicate_case_name_mixed_kind.Test5` +--- +*/ + +// The same name reused across DIFFERENT variant kinds (record vs positional) +// is also rejected. +enum union Test5 +{ + case Variant1 { int id; }, + case Variant1(int), +} diff --git a/compiler/test/fail_compilation/enum_union_move_only_variant.d b/compiler/test/fail_compilation/enum_union_move_only_variant.d new file mode 100644 index 000000000000..db4911caf7aa --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_move_only_variant.d @@ -0,0 +1,22 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_move_only_variant.d(18): Error: cannot create enum union with element type `MoveOnly` that has a move constructor but no copy constructor +--- +*/ + +// A payload type with a move constructor but no copy constructor cannot be +// stored in an enum union: ordinary copies of the enum union (assignment, +// pass-by-value) would raw-bitcopy the payload union instead of invoking the +// move constructor, leading to double-destruction of the payload. +struct MoveOnly +{ + int x; + this(return MoveOnly other) { x = other.x; } +} + +enum union WithMoveOnly +{ + case Moved(MoveOnly), + case Other(bool), +} diff --git a/compiler/test/fail_compilation/enum_union_noarg_construction.d b/compiler/test/fail_compilation/enum_union_noarg_construction.d new file mode 100644 index 000000000000..576827cbaa1a --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_noarg_construction.d @@ -0,0 +1,13 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_noarg_construction.d(13): Error: enum union `enum_union_noarg_construction.Value` cannot be constructed with no arguments; use `.init` instead +--- +*/ + +enum union Value +{ + case Number(int), +} + +auto value = Value(); \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_noarg_constructor.d b/compiler/test/fail_compilation/enum_union_noarg_constructor.d new file mode 100644 index 000000000000..ceb8d75a58b5 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_noarg_constructor.d @@ -0,0 +1,13 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_noarg_constructor.d(12): Error: enum union `enum_union_noarg_constructor.Value` cannot have a no-argument constructor; use `.init` instead +--- +*/ + +enum union Value +{ + case Number(int); + + this() {} +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_shorthand_lambda_no_infer.d b/compiler/test/fail_compilation/enum_union_shorthand_lambda_no_infer.d new file mode 100644 index 000000000000..76dbd3c3d5f4 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_shorthand_lambda_no_infer.d @@ -0,0 +1,21 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_shorthand_lambda_no_infer.d(20): Error: template lambda has no value +--- +*/ + +// The shorthand single-param lambda syntax (`n => n`) cannot infer `n`'s +// type here: the assignment target is the enum union struct itself, not a +// concrete callable type, so there is nothing for the compiler to infer the +// parameter type from. +enum union Funs +{ + case int delegate(int), + case int function(int), +} + +void test() +{ + Funs f = n => n; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_constrained_redundant.d b/compiler/test/fail_compilation/enum_union_switch_constrained_redundant.d new file mode 100644 index 000000000000..47e81cc01a8d --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_constrained_redundant.d @@ -0,0 +1,20 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_constrained_redundant.d(18): Error: redundant match arm; pattern is unreachable +--- +*/ + +enum union Shape +{ + case Square(int height, int width), +} + +int describe(Shape shape) +{ + return switch (shape) + { + case Square(height, width) => 1, + case Square(10, 5) => 2, + }; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_default_guard.d b/compiler/test/fail_compilation/enum_union_switch_default_guard.d new file mode 100644 index 000000000000..f37db67d8e87 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_default_guard.d @@ -0,0 +1,21 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_default_guard.d(19): Error: `default` arm cannot have an `if` guard +--- +*/ + +enum union Shape +{ + case Circle(double), + case Point(), +} + +string guardedDefault(Shape s) +{ + return switch (s) + { + case Circle(r) => "circle", + default if (true) => "point", + }; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_duplicate_default.d b/compiler/test/fail_compilation/enum_union_switch_duplicate_default.d new file mode 100644 index 000000000000..81759ef16b54 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_duplicate_default.d @@ -0,0 +1,21 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_duplicate_default.d(19): Error: duplicate `default` arm in switch expression +--- +*/ + +enum union E +{ + case A(), +} + +int test(E value) +{ + return switch (value) + { + case A() => 0, + default => 1, + default => 2, + }; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_expression_statement.d b/compiler/test/fail_compilation/enum_union_switch_expression_statement.d new file mode 100644 index 000000000000..5c286ae19f6c --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_expression_statement.d @@ -0,0 +1,21 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_expression_statement.d(20): Error: found `}` when expecting `:` +fail_compilation/enum_union_switch_expression_statement.d(21): Error: matching `}` expected following compound statement, not `End of File` +fail_compilation/enum_union_switch_expression_statement.d(16): unmatched `{` +--- +*/ + +enum union Test +{ + case Variant(), +} + +void main() +{ + switch (Test.Variant) + { + case Variant => "" + } +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_switch_guard_no_default.d b/compiler/test/fail_compilation/enum_union_switch_guard_no_default.d new file mode 100644 index 000000000000..d17c45d8dcb4 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_guard_no_default.d @@ -0,0 +1,21 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_guard_no_default.d(16): Error: switch expression is not exhaustive; missing pattern `Circle(_)` +--- +*/ + +enum union Shape +{ + case Circle(double), + case Point(), +} + +string missingDefault(Shape s) +{ + return switch (s) + { + case Circle(r) if (r > 0.0) => "circle", + case Point() => "point", + }; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_guarded_not_exhaustive.d b/compiler/test/fail_compilation/enum_union_switch_guarded_not_exhaustive.d new file mode 100644 index 000000000000..4a425238f5d4 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_guarded_not_exhaustive.d @@ -0,0 +1,19 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_guarded_not_exhaustive.d(15): Error: switch expression is not exhaustive; missing pattern `Square(_, _)` +--- +*/ + +enum union Shape +{ + case Square(int height, int width), +} + +int describe(Shape shape) +{ + return switch (shape) + { + case Square(height, width) if (height > 0) => 1, + }; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_non_union.d b/compiler/test/fail_compilation/enum_union_switch_non_union.d new file mode 100644 index 000000000000..efd5b03a8ece --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_non_union.d @@ -0,0 +1,15 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_non_union.d(12): Error: switch expression patterns require an enum union condition +--- +*/ + +int test() +{ + return switch (1) + { + case value => 0, + default => 1, + }; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_not_exhaustive.d b/compiler/test/fail_compilation/enum_union_switch_not_exhaustive.d new file mode 100644 index 000000000000..1718186ffbe7 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_not_exhaustive.d @@ -0,0 +1,23 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_not_exhaustive.d(17): Error: switch expression is not exhaustive; missing pattern `Point` +--- +*/ + +enum union Shape +{ + case Circle(double), + case Rectangle(double, double), + case Point(), +} + +string describe(Shape s) +{ + return switch (s) + { + case Circle(r) => "circle", + case Rectangle(w, h) => "rectangle", + // Point is missing, and there's no `default`. + }; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_positional_extra.d b/compiler/test/fail_compilation/enum_union_switch_positional_extra.d new file mode 100644 index 000000000000..c6c661abd285 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_positional_extra.d @@ -0,0 +1,21 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_positional_extra.d(18): Error: pattern for variant `Pair` has 3 argument(s), expected 2 +--- +*/ + +enum union E +{ + case Pair(int, int), + case Done(), +} + +int test(E value) +{ + return switch (value) + { + case Pair(left, right, extra) => left + right, + case Done() => 0, + }; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_positional_pattern.d b/compiler/test/fail_compilation/enum_union_switch_positional_pattern.d new file mode 100644 index 000000000000..6cd16f6ea09c --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_positional_pattern.d @@ -0,0 +1,22 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_positional_pattern.d(18): Error: pattern for variant `Pair` has 1 argument(s), expected 2 +--- +*/ + +enum union E +{ + case Pair(int, int), + case Done(), +} + +int test(E value) +{ + return switch (value) + { + case Pair(1) => 0, + case Pair(left, right) => left + right, + case Done() => 0, + }; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_redundant_arm.d b/compiler/test/fail_compilation/enum_union_switch_redundant_arm.d new file mode 100644 index 000000000000..ad91fe70e565 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_redundant_arm.d @@ -0,0 +1,22 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_redundant_arm.d(20): Error: redundant match arm; pattern is unreachable +--- +*/ + +enum union Shape +{ + case Circle(double), + case Point(), +} + +string describe(Shape s) +{ + return switch (s) + { + case Circle(r) => "circle", + case Point() => "point", + case Point() => "point again", // already covered by the earlier arm + }; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_redundant_default.d b/compiler/test/fail_compilation/enum_union_switch_redundant_default.d new file mode 100644 index 000000000000..0d1fdbe5157e --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_redundant_default.d @@ -0,0 +1,22 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_redundant_default.d(20): Error: redundant match arm; pattern is unreachable +--- +*/ + +enum union Shape +{ + case Circle(double), + case Point(), +} + +string describe(Shape shape) +{ + return switch (shape) + { + case Circle(radius) => "circle", + case Point() => "point", + default => "unreachable", + }; +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_switch_unexpected_integer_pattern.d b/compiler/test/fail_compilation/enum_union_switch_unexpected_integer_pattern.d new file mode 100644 index 000000000000..2b16250c18b4 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_unexpected_integer_pattern.d @@ -0,0 +1,22 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_unexpected_integer_pattern.d(18): Error: switch expression value and expression patterns are not supported; use a named variant or type pattern +--- +*/ + +enum union Value +{ + case int, + case string, +} + +string classify(Value value) +{ + return switch (value) + { + case 0 => "zero", + case int => "integer", + case string => "string", + }; +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_switch_unexpected_null_pattern.d b/compiler/test/fail_compilation/enum_union_switch_unexpected_null_pattern.d new file mode 100644 index 000000000000..69e04c4ddc05 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_unexpected_null_pattern.d @@ -0,0 +1,22 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_unexpected_null_pattern.d(18): Error: switch expression value and expression patterns are not supported; use a named variant or type pattern +--- +*/ + +enum union Pointers +{ + case int*, + case bool*, +} + +string classify(Pointers pointers) +{ + return switch (pointers) + { + case null => "null", + case int* => "integer", + case bool* => "boolean", + }; +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_switch_unmatched_pattern.d b/compiler/test/fail_compilation/enum_union_switch_unmatched_pattern.d new file mode 100644 index 000000000000..c488fe265214 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_unmatched_pattern.d @@ -0,0 +1,22 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_unmatched_pattern.d(18): Error: switch expression call pattern `Square` does not name a variant of `enum_union_switch_unmatched_pattern.Shape` +--- +*/ + +enum union Shape +{ + case Circle(double), + case Point(), +} + +string describe(Shape s) +{ + return switch (s) + { + case Square(r) => "square", // no such variant + case Point() => "point", + default => "circle", + }; +} diff --git a/compiler/test/fail_compilation/enum_union_switch_unnamed_call_pattern.d b/compiler/test/fail_compilation/enum_union_switch_unnamed_call_pattern.d new file mode 100644 index 000000000000..211a6e31f58b --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_switch_unnamed_call_pattern.d @@ -0,0 +1,20 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_switch_unnamed_call_pattern.d(17): Error: switch expression call pattern requires a named variant callee +--- +*/ + +enum union Value +{ + case int, +} + +int classify(Value value) +{ + return switch (value) + { + case (0)(1) => 0, + case int => 1, + }; +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_too_many_variants.d b/compiler/test/fail_compilation/enum_union_too_many_variants.d new file mode 100644 index 000000000000..4888b7b0d9d6 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_too_many_variants.d @@ -0,0 +1,43 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_too_many_variants.d(8): Error: enum union cannot have more than 256 variants +--- +*/ + +enum union TooMany +{ + case V0(), case V1(), case V2(), case V3(), case V4(), case V5(), case V6(), case V7(), + case V8(), case V9(), case V10(), case V11(), case V12(), case V13(), case V14(), case V15(), + case V16(), case V17(), case V18(), case V19(), case V20(), case V21(), case V22(), case V23(), + case V24(), case V25(), case V26(), case V27(), case V28(), case V29(), case V30(), case V31(), + case V32(), case V33(), case V34(), case V35(), case V36(), case V37(), case V38(), case V39(), + case V40(), case V41(), case V42(), case V43(), case V44(), case V45(), case V46(), case V47(), + case V48(), case V49(), case V50(), case V51(), case V52(), case V53(), case V54(), case V55(), + case V56(), case V57(), case V58(), case V59(), case V60(), case V61(), case V62(), case V63(), + case V64(), case V65(), case V66(), case V67(), case V68(), case V69(), case V70(), case V71(), + case V72(), case V73(), case V74(), case V75(), case V76(), case V77(), case V78(), case V79(), + case V80(), case V81(), case V82(), case V83(), case V84(), case V85(), case V86(), case V87(), + case V88(), case V89(), case V90(), case V91(), case V92(), case V93(), case V94(), case V95(), + case V96(), case V97(), case V98(), case V99(), case V100(), case V101(), case V102(), case V103(), + case V104(), case V105(), case V106(), case V107(), case V108(), case V109(), case V110(), case V111(), + case V112(), case V113(), case V114(), case V115(), case V116(), case V117(), case V118(), case V119(), + case V120(), case V121(), case V122(), case V123(), case V124(), case V125(), case V126(), case V127(), + case V128(), case V129(), case V130(), case V131(), case V132(), case V133(), case V134(), case V135(), + case V136(), case V137(), case V138(), case V139(), case V140(), case V141(), case V142(), case V143(), + case V144(), case V145(), case V146(), case V147(), case V148(), case V149(), case V150(), case V151(), + case V152(), case V153(), case V154(), case V155(), case V156(), case V157(), case V158(), case V159(), + case V160(), case V161(), case V162(), case V163(), case V164(), case V165(), case V166(), case V167(), + case V168(), case V169(), case V170(), case V171(), case V172(), case V173(), case V174(), case V175(), + case V176(), case V177(), case V178(), case V179(), case V180(), case V181(), case V182(), case V183(), + case V184(), case V185(), case V186(), case V187(), case V188(), case V189(), case V190(), case V191(), + case V192(), case V193(), case V194(), case V195(), case V196(), case V197(), case V198(), case V199(), + case V200(), case V201(), case V202(), case V203(), case V204(), case V205(), case V206(), case V207(), + case V208(), case V209(), case V210(), case V211(), case V212(), case V213(), case V214(), case V215(), + case V216(), case V217(), case V218(), case V219(), case V220(), case V221(), case V222(), case V223(), + case V224(), case V225(), case V226(), case V227(), case V228(), case V229(), case V230(), case V231(), + case V232(), case V233(), case V234(), case V235(), case V236(), case V237(), case V238(), case V239(), + case V240(), case V241(), case V242(), case V243(), case V244(), case V245(), case V246(), case V247(), + case V248(), case V249(), case V250(), case V251(), case V252(), case V253(), case V254(), case V255(), + case V256(), +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_unit_pattern_post_call_binding.d b/compiler/test/fail_compilation/enum_union_unit_pattern_post_call_binding.d new file mode 100644 index 000000000000..433eba56524f --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_unit_pattern_post_call_binding.d @@ -0,0 +1,18 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_unit_pattern_post_call_binding.d(17): Error: `=>` expected in switch expression arm +fail_compilation/enum_union_unit_pattern_post_call_binding.d(17): Error: semicolon expected following auto declaration, not `=>` +fail_compilation/enum_union_unit_pattern_post_call_binding.d(17): Error: declaration expected, not `=>` +--- +*/ + +enum union Value +{ + case Unit(), +} + +auto value = switch (Value.Unit) +{ + case Unit() binding => 0, +}; \ No newline at end of file diff --git a/compiler/test/fail_compilation/enum_union_unknown_bare_type.d b/compiler/test/fail_compilation/enum_union_unknown_bare_type.d new file mode 100644 index 000000000000..19da7296a4a9 --- /dev/null +++ b/compiler/test/fail_compilation/enum_union_unknown_bare_type.d @@ -0,0 +1,11 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/enum_union_unknown_bare_type.d(10): Error: unknown type `ExternalStruct`; for a named unit variant, use `case ExternalStruct()` +--- +*/ + +enum union Test +{ + case ExternalStruct, +} \ No newline at end of file diff --git a/compiler/test/fail_compilation/switch_expression_missing_semicolon.d b/compiler/test/fail_compilation/switch_expression_missing_semicolon.d new file mode 100644 index 000000000000..953b60172078 --- /dev/null +++ b/compiler/test/fail_compilation/switch_expression_missing_semicolon.d @@ -0,0 +1,21 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/switch_expression_missing_semicolon.d(19): Error: found `}` when expecting `;` following expression +--- +*/ + +enum union Value +{ + case Unit(), +} + +void test() +{ + cast(void)switch (Value.Unit) + { + case Unit => 0, + } +} + +private int shouldNotCauseACascade; \ No newline at end of file diff --git a/compiler/test/fail_compilation/switch_expression_statement_no_effect.d b/compiler/test/fail_compilation/switch_expression_statement_no_effect.d new file mode 100644 index 000000000000..bdda71a5574c --- /dev/null +++ b/compiler/test/fail_compilation/switch_expression_statement_no_effect.d @@ -0,0 +1,21 @@ +/* +TEST_OUTPUT: +--- +fail_compilation/switch_expression_statement_no_effect.d(16): Error: switch expression has no effect; use `cast(void)` to discard its value +--- +*/ + +enum union Value +{ + case Unit(), + case Number(int), +} + +void main() +{ + switch (Value.Unit) + { + case Unit value => "unit", + case Number value => "number", + } +} \ No newline at end of file diff --git a/compiler/test/runnable/testenumunion.d b/compiler/test/runnable/testenumunion.d new file mode 100644 index 000000000000..7bc925b0c52c --- /dev/null +++ b/compiler/test/runnable/testenumunion.d @@ -0,0 +1,843 @@ +import std.complex; + +/* +TEST_OUTPUT: +--- +runnable/testenumunion.d(592): Deprecation: use of complex type `cdouble` is deprecated, use `std.complex.Complex!(double)` instead +enum union BareCompoundTypes +^ +runnable/testenumunion.d(592): Deprecation: use of imaginary type `idouble` is deprecated, use `double` instead +enum union BareCompoundTypes +^ +--- +*/ + +alias None = typeof(null); +enum union Option(T) +{ + case Some(T), + case None, +} + +enum union Shape +{ + case Circle(double), + case Rectangle(double, double), + case Point(), +} + +void main() +{ + auto s1 = Shape.Circle(3.5); + auto s2 = Shape.Point; + assert(s1.__tag == 0); + assert(s2.__tag == 2); + + int getScore(Shape s) + { + return switch (s) + { + case Circle(r) => cast(int)(r * 2), + case Rectangle(w, h) => cast(int)(w * h), + case Point() => 1, + }; + } + assert(getScore(s1) == 7); + assert(getScore(s2) == 1); + assert(getScore(Shape.Rectangle(4.0, 5.0)) == 20); + + auto r1 = Shape.Rectangle(4.0, 5.0); + assert(r1.__tag == 1); + + Option!string opt = Option!string.Some("hello"); + assert(opt.__tag == 0); + Option!string empty = null; + assert(empty.__tag == 1); + + string text = switch (opt) + { + case Some(msg) => msg ~ " world", + case None => "empty", + }; + assert(text == "hello world"); + + testImplicitConversion(); + testBareTypes(); + testNullLikeBareType(); + testStructVariant(); + testHybridResponse(); + testGuards(); + testCompoundTypes(); + testCallables(); + testBareCompoundTypes(); + testBareCallables(); + testMemberFunctions(); + testExhaustiveness(); + testLifecycleCopyableVariant(); + testNamedArgumentsAndPatterns(); + testPatternMatrixExhaustiveness(); +} + +enum union NamedPatterns +{ + case Point(int x, int y), + case Square { int height; int width; } +} + +void testNamedArgumentsAndPatterns() +{ + auto square = NamedPatterns.Square(height: 10, width: 5); + auto point = NamedPatterns.Point(4, y: 0); + + auto squareValue = switch (square) + { + case Square { height: 10, width } => width, + case Square { height: h, width: 10 } if (h == 10) => h, + case Square(height, width) => height + width, + case Point(...) => 0, + }; + assert(squareValue == 5); + + auto pointValue = switch (point) + { + case Point(x: 0, y: 0) => 1, + case Point(5, 5) => 2, + case Point(x, y) => x + y, + case Square { ... } => 0, + }; + assert(pointValue == 4); + + auto rest = switch (square) + { + case Square { height, fields... } => fields[0], + case Point(...) => 0, + }; + assert(rest == 5); + + auto tupleRest = switch (square) + { + case Square(fields...) => fields[0] + fields[1], + case Point(...) => 0, + }; + assert(tupleRest == 15); +} + +enum union MatrixShape +{ + case Square { bool active; bool filled; } +} + +void testPatternMatrixExhaustiveness() +{ + auto shape = MatrixShape.Square(true, false); + auto value = switch (shape) + { + case Square { active: true, filled: true } => 1, + case Square { active: true, filled: false } => 2, + case Square { active: false, filled: true } => 3, + case Square { active: false, filled: false } => 4, + }; + assert(value == 2); +} + +// Regression test: implicit conversion of variant-construction expressions +// on `return` and when passing arguments to a function parameter. +enum union Account +{ + case User(int, string), + case Admin(int, string), +} + +Account getAccount() +{ + return Account.User(42, "alice"); // implicit conversion on return +} + +int accessAccount(Account a) // implicit conversion on argument passing +{ + return switch (a) + { + case User(id, name) => id, + case Admin(id, name) => -id, + }; +} + +void testImplicitConversion() +{ + assert(accessAccount(Account.Admin(7, "root")) == -7); + + Account a = getAccount(); + assert(accessAccount(a) == 42); +} + +// Regression test: bare (non-string) type variants construct correctly. +// (`switch` type-pattern matching currently only supports `double`/`string` +// bare-type arms, so `int`/`bool` are verified via `.__tag` and direct +// construction instead.) +enum union Val +{ + case int, + case bool, + case double, +} + +Val makeInt() { return Val(5); } +Val makeBool() { return Val(true); } +Val makeDouble() { return Val(3.14); } + +int classify(Val v) +{ + return switch (v) + { + case double d => cast(int) d, + default => -1, + }; +} + +void testBareTypes() +{ + Val vi = Val(5); + Val vb = Val(true); + Val vd = Val(3.14); + assert(vi.__tag == 0); + assert(vb.__tag == 1); + assert(vd.__tag == 2); + + assert(makeInt().__tag == 0); + assert(makeBool().__tag == 1); + assert(makeDouble().__tag == 2); + + assert(classify(Val(3.14)) == 3); + assert(classify(makeDouble()) == 3); +} + +enum union NullOption(T) +{ + case Some(T), + case typeof(null), +} + +void testNullLikeBareType() +{ + NullOption!int empty = null; + assert(switch (empty) + { + case Some(v) => v, + case typeof(null) => -1, + } == -1); + + Option!int empty2 = null; + assert(switch (empty2) + { + case Some(v) => v, + case None => -1, + } == -1); +} + +// Regression test: struct/record variants convert implicitly on +// both argument passing and `return`. +enum union Response2 +{ + case double, + case Success { int code; string payload; } +} + +Response2 makeDouble2() { return 3.14; } +Response2 makeSuccess2() { return Response2.Success(200, "OK"); } + +string describe2(Response2 r) +{ + return switch (r) + { + case double d => "double", + case Success { code, ... } => "success", + }; +} + +void testStructVariant() +{ + assert(describe2(3.14) == "double"); + assert(describe2(Response2.Success(1, "ok")) == "success"); + assert(describe2(makeDouble2()) == "double"); + assert(describe2(makeSuccess2()) == "success"); +} + +// Regression test: `string` bare-type variants used to be misidentified by +// `switch` pattern matching (matched the wrong arm at runtime) because the +// unresolved `TypeIdentifier` for `string` was compared without running +// `typeSemantic()` on it first. See dcast.d/expressionsem.d fixes. +enum union Response +{ + case double, + case string, + case Success { int code; string payload; } +} + +Response makeDouble3() { return 3.14; } +Response makeString3() { return "hello"; } +Response makeSuccess3() { return Response.Success(200, "OK"); } + +string describe(Response r) +{ + return switch (r) + { + case double d => "double", + case string s => "string", + case Success { code, ... } => "success", + }; +} + +void testHybridResponse() +{ + assert(describe(3.14) == "double"); + assert(describe("hi") == "string"); + assert(describe(Response.Success(1, "ok")) == "success"); + assert(describe(makeDouble3()) == "double"); + assert(describe(makeString3()) == "string"); + assert(describe(makeSuccess3()) == "success"); +} + +// Regression test: `if` guards on switch expression arms, including +// multiple guarded arms for the same variant, a plain (unguarded) arm for +// the same variant as a guard-fallback, and bindings that are visible to +// both the guard condition and the arm action. +string classifyShape(Shape s) +{ + return switch (s) + { + case Circle(r) if (r > 10.0) => "big circle", + case Circle(r) if (r <= 10.0) => "small circle", + case Rectangle(w, h) if (w == h) => "square", + case Rectangle(w, h) => "rectangle", + case Point() => "point", + default => "unreachable", + }; +} + +enum union GuardVal +{ + case double, + case string, +} + +string classifyGuardVal(GuardVal v) +{ + return switch (v) + { + case double d if (d > 0.0) => "positive double", + case double d => "non-positive double", + case string s => "string", + }; +} + +void testGuards() +{ + assert(classifyShape(Shape.Circle(20.0)) == "big circle"); + assert(classifyShape(Shape.Circle(3.0)) == "small circle"); + assert(classifyShape(Shape.Rectangle(4.0, 4.0)) == "square"); + assert(classifyShape(Shape.Rectangle(4.0, 5.0)) == "rectangle"); + assert(classifyShape(Shape.Point) == "point"); + + assert(classifyGuardVal(5.0) == "positive double"); + assert(classifyGuardVal(-5.0) == "non-positive double"); + assert(classifyGuardVal("hi") == "string"); +} + +// Regression test: enum union variants wrapping "compound" payload types: +// arrays, associative arrays, pointers, void[]/void*, noreturn*/noreturn[], +// function/delegate pointers, char/wchar/dchar, string/wstring/dstring, +// static arrays, and complex/imaginary numerics. +enum union CompoundTypes +{ + case Arr(int[]), + case AssocArr(int[string]), + case Ptr(int*), + case VoidArr(void[]), + case VoidPtr(void*), + case NoReturnPtr(noreturn*), + case NoReturnArr(noreturn[]), + case FuncPtr(int function(int)), + case Del(int delegate(int)), + case Ch(char), + case WCh(wchar), + case DCh(dchar), + case Str(string), + case WStr(wstring), + case DStr(dstring), + case StaticArr(int[4]), + case Cplx(Complex!double), + case Imag(double), +} + +private int addOne(int x) { return x + 1; } + +void testCompoundTypes() +{ + int local = 42; + int[string] aa; + aa["x"] = 1; + + auto arr = CompoundTypes.Arr([1, 2, 3]); + auto assocArr = CompoundTypes.AssocArr(aa); + auto ptr = CompoundTypes.Ptr(&local); + auto voidArr = CompoundTypes.VoidArr(cast(void[])[1, 2, 3]); + auto voidPtr = CompoundTypes.VoidPtr(cast(void*)&local); + auto noReturnPtr = CompoundTypes.NoReturnPtr(null); + auto noReturnArr = CompoundTypes.NoReturnArr([]); + auto funcPtr = CompoundTypes.FuncPtr(&addOne); + int delegate(int) dg = (int x) => x * 2; + auto del = CompoundTypes.Del(dg); + auto ch = CompoundTypes.Ch('a'); + auto wch = CompoundTypes.WCh('b'); + auto dch = CompoundTypes.DCh('c'); + auto str = CompoundTypes.Str("hello"); + auto wstr = CompoundTypes.WStr("world"w); + auto dstr = CompoundTypes.DStr("!"d); + int[4] sa = [1, 2, 3, 4]; + auto staticArr = CompoundTypes.StaticArr(sa); + auto cplx = CompoundTypes.Cplx(Complex!double(1.0, 2.0)); + auto imag = CompoundTypes.Imag(3.0); + + assert(arr.__tag == 0); + assert(assocArr.__tag == 1); + assert(ptr.__tag == 2); + assert(voidArr.__tag == 3); + assert(voidPtr.__tag == 4); + assert(noReturnPtr.__tag == 5); + assert(noReturnArr.__tag == 6); + assert(funcPtr.__tag == 7); + assert(del.__tag == 8); + assert(ch.__tag == 9); + assert(wch.__tag == 10); + assert(dch.__tag == 11); + assert(str.__tag == 12); + assert(wstr.__tag == 13); + assert(dstr.__tag == 14); + assert(staticArr.__tag == 15); + assert(cplx.__tag == 16); + assert(imag.__tag == 17); + + int sum = switch (arr) + { + case Arr(a) => a[0] + a[1] + a[2], + default => -1, + }; + assert(sum == 6); + + int v = switch (assocArr) + { + case AssocArr(a) => a["x"], + default => -1, + }; + assert(v == 1); + + int derefed = switch (ptr) + { + case Ptr(p) => *p, + default => -1, + }; + assert(derefed == 42); + + size_t voidArrLen = switch (voidArr) + { + case VoidArr(a) => a.length, + default => size_t.max, + }; + assert(voidArrLen == 12); // 3 ints * 4 bytes + + bool voidPtrNonNull = switch (voidPtr) + { + case VoidPtr(p) => p !is null, + default => false, + }; + assert(voidPtrNonNull); + + int called = switch (funcPtr) + { + case FuncPtr(f) => f(9), + default => -1, + }; + assert(called == 10); + + int doubled = switch (del) + { + case Del(d) => d(9), + default => -1, + }; + assert(doubled == 18); + + char c = switch (ch) + { + case Ch(x) => x, + default => '?', + }; + assert(c == 'a'); + + string s = switch (str) + { + case Str(x) => x, + default => "", + }; + assert(s == "hello"); + + int staticArrSum = switch (staticArr) + { + case StaticArr(a) => a[0] + a[1] + a[2] + a[3], + default => -1, + }; + assert(staticArrSum == 10); +} + +// Regression test: enum union variants holding a plain function and +// multiple delegates. Reassigning between variants (and within the same +// variant) with different closures must never leave a stale/mixed-up +// delegate context pointer behind. +enum union Callable +{ + case Fn(int function(int)), + case Dg(int delegate(int)), + case Dg2(int delegate(int)), +} + +private int call(Callable c) +{ + return switch (c) + { + case Fn(f) => f(1), + case Dg(d) => d(1), + case Dg2(d) => d(1), + }; +} + +private int makeClosureAndCall(int captured) +{ + int delegate(int) dg = (int x) => x + captured; + return call(Callable.Dg(dg)); +} + +enum union Handler +{ + case OnClick { int delegate(int) callback; }, + case OnHover { int delegate(int) callback; }, +} + +private int callHandler(Handler h, int x) +{ + return switch (h) + { + case OnClick { callback } => callback(x), + case OnHover { callback } => callback(x), + }; +} + +void testCallables() +{ + Callable cf = Callable.Fn((int x) => x + 1); + assert(call(cf) == 2); + + // Two delegates capturing DIFFERENT locals: a stale/mixed-up context + // pointer would silently add the wrong value. + int a = 100; + int b = 200; + int delegate(int) dgA = (int x) => x + a; + int delegate(int) dgB = (int x) => x + b; + + Callable c1 = Callable.Dg(dgA); + assert(call(c1) == 101); + + // Reassign the SAME variant tag (Dg -> Dg) with a different closure. + c1 = Callable.Dg(dgB); + assert(call(c1) == 201); + + // Reassign to a DIFFERENT variant tag (Dg -> Dg2) with yet another closure. + int cCap = 300; + int delegate(int) dgC = (int x) => x + cCap; + c1 = Callable.Dg2(dgC); + assert(call(c1) == 301); + + // Reassign back to Dg with dgA: context must be dgA's, not dgC's leftover. + c1 = Callable.Dg(dgA); + assert(call(c1) == 101); + + // Each call creates a distinct closure; verifies no cross-contamination. + assert(makeClosureAndCall(5) == 6); + assert(makeClosureAndCall(50) == 51); + + // Common delegate field, accessed via switch. + int clicks, hovers; + int delegate(int) onClick = (int x) { clicks += x; return clicks; }; + int delegate(int) onHover = (int x) { hovers += x; return hovers; }; + + Handler h = Handler.OnClick(onClick); + assert(callHandler(h, 5) == 5); + assert(clicks == 5 && hovers == 0); + + h = Handler.OnHover(onHover); + assert(callHandler(h, 3) == 3); + assert(hovers == 3 && clicks == 5); + + // Reassign the same variant with a different closure; the switch-based + // call must use the new context, not the previous one. + int otherClicks; + int delegate(int) onClick2 = (int x) { otherClicks += x * 2; return otherClicks; }; + h = Handler.OnClick(onClick2); + assert(callHandler(h, 4) == 8); + assert(otherClicks == 8 && clicks == 5); +} + +// Regression test: all the compound types tested as NAMED variant payloads +// above also work as bare (unnamed) variant types, including identifier- +// spelled types followed by `*`/`[]` (e.g. `noreturn*`), which previously +// mis-parsed as a named unit variant called e.g. `noreturn`. +enum union BareCompoundTypes +{ + case int[], + case int[string], + case int*, + case void[], + case void*, + case noreturn*, + case noreturn[], + case int function(int), + case int delegate(int), + case char, + case wchar, + case dchar, + case string, + case wstring, + case dstring, + case int[4], + case cdouble, + case idouble, + case double, +} + +void testBareCompoundTypes() +{ + int local = 7; + int[string] aa; + aa["y"] = 2; + int[4] sa = [1, 2, 3, 4]; + + BareCompoundTypes v0 = BareCompoundTypes([1, 2, 3]); + BareCompoundTypes v1 = BareCompoundTypes(aa); + BareCompoundTypes v2 = BareCompoundTypes(&local); + BareCompoundTypes v3 = BareCompoundTypes(cast(void[])[1, 2, 3]); + BareCompoundTypes v4 = BareCompoundTypes(cast(void*)&local); + BareCompoundTypes v5 = BareCompoundTypes(cast(noreturn*) null); + BareCompoundTypes v6 = BareCompoundTypes(cast(noreturn[])[]); + int function(int) plainFn = (int x) => x + 1; + BareCompoundTypes v7 = BareCompoundTypes(plainFn); + BareCompoundTypes v8 = BareCompoundTypes((int x) => x + local); + BareCompoundTypes v9 = BareCompoundTypes('a'); + BareCompoundTypes v10 = BareCompoundTypes(cast(wchar)'b'); + BareCompoundTypes v11 = BareCompoundTypes(cast(dchar)'c'); + BareCompoundTypes v12 = BareCompoundTypes("hello"); + BareCompoundTypes v13 = BareCompoundTypes("world"w); + BareCompoundTypes v14 = BareCompoundTypes("!"d); + BareCompoundTypes v15 = BareCompoundTypes(sa); + BareCompoundTypes v16 = BareCompoundTypes(1.0 + 2.0i); + BareCompoundTypes v17 = BareCompoundTypes(3.0i); + + assert(v0.__tag == 0); + assert(v1.__tag == 1); + assert(v2.__tag == 2); + assert(v3.__tag == 3); + assert(v4.__tag == 4); + assert(v5.__tag == 5); + assert(v6.__tag == 6); + assert(v7.__tag == 7); + assert(v8.__tag == 8); + assert(v9.__tag == 9); + assert(v10.__tag == 10); + assert(v11.__tag == 11); + assert(v12.__tag == 12); + assert(v13.__tag == 13); + assert(v14.__tag == 14); + assert(v15.__tag == 15); + assert(v16.__tag == 16); + assert(v17.__tag == 17); +} + + +// Regression test: bare function-pointer and delegate variants with the same +// signature. A lambda's inferred attributes (`pure nothrow @nogc @safe`) +// used to prevent implicit construction entirely, since the enum union +// implicit-conversion check required an exact type match; attribute +// widening is now allowed specifically for callable payload types (it can +// never introduce cross-variant ambiguity the way numeric widening would). +enum union Funs +{ + case int function(int), + case int delegate(int), +} + +void testBareCallables() +{ + // A non-capturing lambda is convertible to *either* a function pointer or + // a delegate with the same signature, so assigning it directly to `Funs` + // is genuinely ambiguous and must go through an explicitly-typed + // intermediate (same as f3/f4 below); a capturing closure, however, can + // only ever be a delegate, so it unambiguously selects that variant. + int function(int) plainFn = (int x) => x + 1; + Funs f1 = plainFn; // decays to a plain function pointer + int captured = 10; + Funs f2 = (int x) => x + captured; // closure -> delegate (unambiguous) + + assert(f1.__tag == 0); + assert(f2.__tag == 1); + + int function(int) fp = (int x) => x + 1; + int delegate(int) dg = (int x) => x + captured; + Funs f3 = fp; + Funs f4 = dg; + assert(f3.__tag == 0); + assert(f4.__tag == 1); +} + +// Regression test: `enum union` member declarations (functions, static +// functions, manifest constants) after a `;` following the variant list, per +// the grammar's `(";" MemberDeclarationList)?`. Member functions can use +// `switch (this)` to dispatch on the active variant, and named-variant +// factory functions (`Shape.Circle(...)`) still get synthesized correctly +// alongside user-declared members. +enum union ShapeWithMethods +{ + case Circle(double), + case Rectangle(double, double), + case Point(); + + double area() + { + return switch (this) + { + case Circle(r) => 3.14159 * r * r, + case Rectangle(w, h) => w * h, + case Point() => 0.0, + }; + } + + string describe() + { + return switch (this) + { + case Circle(r) => "circle", + case Rectangle(w, h) => "rectangle", + case Point() => "point", + }; + } + + static ShapeWithMethods unit() { return ShapeWithMethods.Point; } + + enum string kind = "shape"; +} + +void testMemberFunctions() +{ + ShapeWithMethods c = ShapeWithMethods.Circle(2.0); + assert(c.describe() == "circle"); + assert(c.area() > 12.5 && c.area() < 12.6); + + ShapeWithMethods r = ShapeWithMethods.Rectangle(3.0, 4.0); + assert(r.describe() == "rectangle"); + assert(r.area() == 12.0); + + ShapeWithMethods u = ShapeWithMethods.unit(); + assert(u.describe() == "point"); + assert(u.area() == 0.0); + + assert(ShapeWithMethods.kind == "shape"); +} + +// Regression test: exhaustiveness checking. A `switch` covering every +// variant (unguarded) needs no `default` and compiles/runs fine; combining +// a guarded arm, an unguarded fallback for the same variant, and a +// `default` for the rest is exhaustive and non-redundant. +enum union Traffic +{ + case Red(), + case Yellow(), + case Green(), +} + +string classifyTraffic(Traffic t) +{ + return switch (t) + { + case Red() => "stop", + case Yellow() => "caution", + case Green() => "go", + }; +} + +enum union Level +{ + case double, + case string, +} + +string classifyLevel(Level v) +{ + return switch (v) + { + case double d if (d > 100.0) => "high", + case double d => "normal", + default => "other", + }; +} + +void testExhaustiveness() +{ + assert(classifyTraffic(Traffic.Red) == "stop"); + assert(classifyTraffic(Traffic.Yellow) == "caution"); + assert(classifyTraffic(Traffic.Green) == "go"); + + assert(classifyLevel(150.0) == "high"); + assert(classifyLevel(50.0) == "normal"); + assert(classifyLevel("hi") == "other"); +} + +int lifecycleCopyCount; + +struct CopyablePayload +{ + int x; + this(ref CopyablePayload other) + { + lifecycleCopyCount++; + x = other.x; + } +} + +enum union WithCopyable +{ + case Wrapped(CopyablePayload), + case Flag(bool), +} + +void testLifecycleCopyableVariant() +{ + lifecycleCopyCount = 0; + WithCopyable a = WithCopyable.Wrapped(CopyablePayload(7)); + WithCopyable b = a; + assert(lifecycleCopyCount == 1); + assert(a.__tag == 0); + assert(b.__tag == 0); +} + +import std.traits: isBuiltinType; + +enum union TemplateVarargs(Types...) +{ + static foreach(U; Types) + static if (isBuiltinType!U) + case U; +} + +void testTemplateVarargs() +{ + TemplateVarargs!(int, void*, string) t = "asdf"; + assert(t.__tag == 2); + t = 42; + assert(t.__tag == 0); +} \ No newline at end of file