Expressions
Contents- Definitions and Terms
- Order Of Evaluation
- Lifetime of Temporaries
- Expressions
- Assign Expressions
- Conditional Expressions
- OrOr Expressions
- AndAnd Expressions
- Bitwise Expressions
- Compare Expressions
- Equality Expressions
- Relational Expressions
- In Expressions
- Shift Expressions
- Add Expressions
- Cat Expressions
- Mul Expressions
- Unary Expressions
- Pow Expressions
- Postfix Expressions
- Index Expressions
- Slice Expressions
- Primary Expressions
- Special Keywords
- Associativity and Commutativity
An expression is a sequence of operators and operands that specifies an evaluation. The syntax, order of evaluation, and semantics of expressions are as follows.
Expressions are used to compute values with a resulting type. These values can then be assigned, tested, or ignored. Expressions can also have side effects.
Definitions and Terms
Definition (“Full expression”): For any expression expr, the full expression of expr is defined as follows. If expr parses as a subexpression of another expression expr1, then the full expression of expr is the full expression of expr1. Otherwise, expr is its own full expression.
Each expression has a unique full expression.
Example: in the statement return f() + g() * 2;
, the full expression of g() * 2
is f() + g() * 2
, but not the full expression of f() + g()
because the latter is not parsed as a subexpression.
Note: Although the definition is straightforward, a few subtleties exist related to function literals. In the statement return (() => x + f())() * g();
, the full expression of f()
is x + f()
, not the expression passed to return
. This is because the parent of x + f()
has function literal type, not expression type.
Definition (“Lvalue”): The following expressions, and no others, are called lvalue expressions or lvalues:
-
this
insidestruct
andunion
member functions; - a variable or the result of the DotIdentifier grammatical construct
.
(left side may be missing) when the rightmost side of the dot is a variable, field (direct orstatic
), function name, or invocation of a function that returns by reference; - the result of the following expressions:
- built-in unary operators
+
(when applied to an lvalue),*
,++
(prefix only),--
(prefix only); - built-in indexing operator
[]
(but not the slicing operator); - built-in assignment binary operators, i.e.
=
,+=
,*=
,/=
,%=
,&=
,|=
,^=
,~=
,<<=
,>>=
,>>>=
, and^^=
; - the ternary operator e
?
e1:
e2 under the following circumstances:
- e1 and e2 are lvalues of the same type; OR
- One of e1 and e2 is an lvalue of type
T
and the other has andalias this
converting it toref T
;
- built-in unary operators
- user-defined operators if and only if the function called as a result of lowering returns by reference;
-
mixin
expressions if and only if the compilation of the expression resulting from compiling the argument(s) tomixin
is an lvalue; -
cast(U)
expressions applied to lvalues of typeT
whenT*
is implicitly convertible toU*
; -
cast()
andcast(
qualifier list)
when applied to an lvalue.
Definition (“Rvalue”): Expressions that are not lvalues are rvalues.
Note: Rvalues include all literals, special value keywords such as __FILE__
and __LINE__
, enum
values, and the result of expressions not defined as lvalues above.
The built-in address-of operator (unary &
) may only be applied to lvalues.
Definition (“Smallest short-circuit expression”): Given an expression expr that is a subexpression of a full expression fullexpr, the smallest short-circuit expression, if any, is the shortest subexpression scexpr of fullexpr that is an AndAndExpression (&&
) or an OrOrExpression (||
), such that expr is a subexpression of scexpr.
((f() * 2 && g()) + 1) || h()
, the smallest short-circuit expression of the subexpression f() * 2
is f() * 2 && g()
. In the expression (f() && g()) + h()
, the subexpression h()
has no smallest short-circuit expression. Order Of Evaluation
Built-in prefix unary expressions ++
and --
are evaluated as if lowered (rewritten) to assignments as follows: ++expr
becomes ((expr) += 1)
, and --expr
becomes ((expr) -= 1)
. Therefore, the result of prefix ++
and --
is the lvalue after the side effect has been effected.
Built-in postfix unary expressions ++
and --
are evaluated as if lowered (rewritten) to lambda invocations as follows: expr++
becomes (ref T x){auto t = x; ++x; return t;}(expr)
, and expr--
becomes (ref T x){auto t = x; --x; return t;}(expr)
. Therefore, the result of postfix ++
and --
is an rvalue just before the side effect has been effected.
Binary expressions except for AssignExpression, OrOrExpression, and AndAndExpression are evaluated in lexical order (left-to-right). Example:
int i = 2; i = ++i * i++ + i; assert(i == 3 * 3 + 4);
OrOrExpression and AndAndExpression evaluate their left-hand side argument first. Then, OrOrExpression evaluates its right-hand side if and only if its left-hand side does not evaluate to nonzero. AndAndExpression evaluates its right-hand side if and only if its left-hand side evaluates to nonzero.
ConditionalExpression evaluates its left-hand side argument first. Then, if the result is nonzero, the second operand is evaluated. Otherwise, the third operand is evaluated.
Calls to functions with extern(D)
linkage (which is the default linkage) are evaluated in the following order: first, if necessary, the address of the function to call is evaluated (e.g. in the case of a computed function pointer or delegate). Then, arguments are evaluated left to right. Finally, transfer is passed to the function. Example:
import std.stdio; void function(int a, int b, int c) fun() { writeln("fun() called"); static void r(int a, int b, int c) { writeln("callee called"); } return &r; } int f1() { writeln("f1() called"); return 1; } int f2() { writeln("f2() called"); return 2; } int f3(int x) { writeln("f3() called"); return x + 3; } int f4() { writeln("f4() called"); return 4; } // evaluates fun() then f1() then f2() then f3() then f4() // after which control is transferred to the callee fun()(f1(), f3(f2()), f4());Implementation Defined:
- The order of evaluation of the operands of AssignExpression.
- The order of evaluation of function arguments for functions with linkage other than
extern (D)
.
Lifetime of Temporaries
Expressions and statements may create and/or consume rvalues. Such values are called temporaries and do not have a name or a visible scope. Their lifetime is managed automatically as defined in this section.
For each evaluation that yields a temporary value, the lifetime of that temporary begins at the evaluation point, similarly to creation of a usual named value initialized with an expression.
Termination of lifetime of temporaries does not obey the customary scoping rules and is defined as follows:
- If:
- the full expression has a smallest short-circuit expression expr; and
- the temporary is created on the right-hand side of the
&&
or||
operator; and - the right-hand side is evaluated,
bool
. Evaluation of destructors proceeds in reverse order of construction. - For all other cases, the temporaries generated for the purpose of invoking functions are deferred to the end of the full expression. The order of destruction is inverse to the order of construction.
If a subexpression of an expression throws an exception, all temporaries created up to the evaluation of that subexpression will be destroyed per the rules above. No destructor calls will be issued for temporaries not yet constructed.
Note: An intuition behind these rules is that destructors of temporaries are deferred to the end of full expression and in reverse order of construction, with the exception that the right-hand side of &&
and ||
are considered their own full expressions even when part of larger expressions.
Note: The ternary expression e1 ? e2 : e3 is not a special case although it evaluates expressions conditionally: e1 and one of e2 and e3 may create temporaries. Their destructors are inserted to the end of the full expression in the reverse order of creation.
Example:
import std.stdio; struct S { int x; this(int n) { x = n; writefln("S(%s)", x); } ~this() { writefln("~S(%s)", x); } } bool b = (S(1) == S(2) || S(3) != S(4)) && S(5) == S(6);The output of the code above is:
S(1) S(2) S(3) S(4) ~S(4) ~S(3) S(5) S(6) ~S(6) ~S(5) ~S(2) ~S(1)First,
S(1)
and S(2)
are evaluated in lexical order. Per the rules, they will be destroyed at the end of the full expression and in reverse order. The comparison S(1) == S(2)
yields false
, so the right-hand side of the ||
is evaluated causing S(3)
and S(4)
to be evaluated, also in lexical order. However, their destruction is not deferred to the end of the full expression. Instead, S(4)
and then S(3)
are destroyed at the end of the ||
expression. Following their destruction, S(5)
and S(6)
are constructed in lexical order. Again they are not destroyed at the end of the full expression, but right at the end of the &&
expression. Consequently, the destruction of S(6)
and S(5)
is carried before that of S(2)
and S(1)
. Expressions
Expression: CommaExpression CommaExpression: AssignExpression AssignExpression , CommaExpression
The left operand of the ,
is evaluated, then the right operand is evaluated. The type of the expression is the type of the right operand, and the result is the result of the right operand. Using the result of comma expressions isn't allowed.
Assign Expressions
AssignExpression: ConditionalExpression ConditionalExpression = AssignExpression ConditionalExpression += AssignExpression ConditionalExpression -= AssignExpression ConditionalExpression *= AssignExpression ConditionalExpression /= AssignExpression ConditionalExpression %= AssignExpression ConditionalExpression &= AssignExpression ConditionalExpression |= AssignExpression ConditionalExpression ^= AssignExpression ConditionalExpression ~= AssignExpression ConditionalExpression <<= AssignExpression ConditionalExpression >>= AssignExpression ConditionalExpression >>>= AssignExpression ConditionalExpression ^^= AssignExpression
For all assign expressions, the left operand must be a modifiable lvalue. The type of the assign expression is the type of the left operand, and the value is the value of the left operand after assignment occurs. The resulting expression is a modifiable lvalue.
Undefined Behavior: If either operand is a reference type and one of the following:- the operands have partially overlapping storage
- the operands' storage overlaps exactly but the types are different
- the operands have partially overlapping storage
- the operands' storage overlaps exactly but the types are different
Simple Assignment Expression
If the operator is =
then it is simple assignment. The right operand is implicitly converted to the type of the left operand, and assigned to it.
If the left and right operands are of the same struct type, and the struct type has a Postblit, then the copy operation is as described in Struct Postblit.
If the lvalue is the .length
property of a dynamic array, the behavior is as described in Setting Dynamic Array Length.
If the lvalue is a static array or a slice, the behavior is as described in Array Copying and Array Setting.
If the lvalue is a user-defined property, the behavior is as described in Property Functions.
Assignment Operator Expressions
For arguments of built-in types, assignment operator expressions such as
a op= bare semantically equivalent to:
a = cast(typeof(a))(a op b)except that
- operand
a
is only evaluated once, - overloading op uses a different function than overloading op= does, and
- the left operand of
>>>=
does not undergo Integer Promotions before shifting.
For user-defined types, assignment operator expressions are overloaded separately from the binary operator. Still the left operand must be an lvalue.
Conditional Expressions
ConditionalExpression: OrOrExpression OrOrExpression ? Expression : ConditionalExpression
The first expression is converted to bool
, and is evaluated.
If it is true
, then the second expression is evaluated, and its result is the result of the conditional expression.
If it is false
, then the third expression is evaluated, and its result is the result of the conditional expression.
If either the second or third expressions are of type void
, then the resulting type is void
. Otherwise, the second and third expressions are implicitly converted to a common type which becomes the result type of the conditional expression.
Note: When a conditional expression is the left operand of an assign expression, parentheses are required for disambiguation:
bool test; int a, b, c; ... test ? a = b : c = 2; // Deprecated (test ? a = b : c) = 2; // Equivalent
This makes the intent clearer, because the first statement can easily be misread as the following code:
test ? a = b : (c = 2);
OrOr Expressions
OrOrExpression: AndAndExpression OrOrExpression || AndAndExpression
The result type of an OrOrExpression is bool
, unless the right operand has type void
, when the result is type void
.
The OrOrExpression evaluates its left operand.
If the left operand, converted to type bool
, evaluates to true
, then the right operand is not evaluated. If the result type of the OrOrExpression is bool
then the result of the expression is true
.
If the left operand is false
, then the right operand is evaluated. If the result type of the OrOrExpression is bool
then the result of the expression is the right operand converted to type bool
.
AndAnd Expressions
AndAndExpression: OrExpression AndAndExpression && OrExpression
The result type of an AndAndExpression is bool
, unless the right operand has type void
, when the result is type void
.
The AndAndExpression evaluates its left operand.
If the left operand, converted to type bool
, evaluates to false
, then the right operand is not evaluated. If the result type of the AndAndExpression is bool
then the result of the expression is false
.
If the left operand is true
, then the right operand is evaluated. If the result type of the AndAndExpression is bool
then the result of the expression is the right operand converted to type bool
.
Bitwise Expressions
Bit wise expressions perform a bitwise operation on their operands. Their operands must be integral types. First, the Usual Arithmetic Conversions are done. Then, the bitwise operation is done.
Or Expressions
OrExpression: XorExpression OrExpression | XorExpression
The operands are OR'd together.
Xor Expressions
XorExpression: AndExpression XorExpression ^ AndExpression
The operands are XOR'd together.
And Expressions
AndExpression: CmpExpression AndExpression & CmpExpression
The operands are AND'd together.
Compare Expressions
CmpExpression: ShiftExpression EqualExpression IdentityExpression RelExpression InExpression
Equality Expressions
EqualExpression: ShiftExpression == ShiftExpression ShiftExpression != ShiftExpression
Equality expressions compare the two operands for equality (==
) or inequality (!=
). The type of the result is bool
.
Inequality is defined as the logical negation of equality.
If the operands are integral values, the Usual Arithmetic Conversions are applied to bring them to a common type before comparison. Equality is defined as the bit patterns of the common type match exactly.
If the operands are pointers, equality is defined as the bit patterns of the operands match exactly.
For float, double, and real values, the Usual Arithmetic Conversions are applied to bring them to a common type before comparison. The values -0
and +0
are considered equal. If either or both operands are NAN, then ==
returns false and !=
returns true
. Otherwise, the bit patterns of the common type are compared for equality.
For complex numbers, equality is defined as equivalent to:
x.re == y.re && x.im == y.im
For struct objects, equality means the result of the opEquals()
member function. If an opEquals()
is not provided, equality is defined as the logical product of all equality results of the corresponding object fields.
opEquals()
can account for which of the overlapping fields contains valid data. An opEquals()
can override the default behavior of floating point NaN values always comparing as unequal. Be careful using memcmp()
to implement opEquals()
if: - there are any alignment gaps
- if any fields have an
opEquals()
- there are any floating point fields that may contain NaN or
-0
values
For class and struct objects, the expression (a == b)
is rewritten as a.opEquals(b)
, and (a != b)
is rewritten as !a.opEquals(b)
.
For class objects, the ==
and !=
operators are intended to compare the contents of the objects, however an appropriate opEquals
override must be defined for this to work. The default opEquals
provided by the root Object
class is equivalent to the is
operator. Comparing against null
is invalid, as null
has no contents. Use the is
and !is
operators instead.
class C; C c; if (c == null) // error ... if (c is null) // ok ...
For static and dynamic arrays, equality is defined as the lengths of the arrays matching, and all the elements are equal.
Identity Expressions
IdentityExpression: ShiftExpression is ShiftExpression ShiftExpression !is ShiftExpression
The is
compares for identity. To compare for nonidentity, use e1 !is e2
. The type of the result is bool
. The operands undergo the Usual Arithmetic Conversions to bring them to a common type before comparison.
For class objects, identity is defined as the object references are for the same object. Null class objects can be compared with is
.
For struct objects and floating point values, identity is defined as the bits in the operands being identical.
For static and dynamic arrays, identity is defined as referring to the same array elements and the same number of elements.
For other operand types, identity is defined as being the same as equality.
The identity operator is
cannot be overloaded.
Relational Expressions
RelExpression: ShiftExpression < ShiftExpression ShiftExpression <= ShiftExpression ShiftExpression > ShiftExpression ShiftExpression >= ShiftExpression
First, the Usual Arithmetic Conversions are done on the operands. The result type of a relational expression is bool
.
For class objects, the result of Object.opCmp() forms the left operand, and 0 forms the right operand. The result of the relational expression (o1 op o2) is:
(o1.opCmp(o2) op 0)
It is an error to compare objects if one is null
.
For static and dynamic arrays, the result of the relational op is the result of the operator applied to the first non-equal element of the array. If two arrays compare equal, but are of different lengths, the shorter array compares as "less" than the longer array.
Integer comparisons
Integer comparisons happen when both operands are integral types.
Operator | Relation |
---|---|
< |
less |
> |
greater |
< =
|
less or equal |
>= |
greater or equal |
== |
equal |
!= |
not equal |
It is an error to have one operand be signed and the other unsigned for a <
, <
=
, >
or >
=
expression. Use casts to make both operands signed or both operands unsigned.
Floating point comparisons
If one or both operands are floating point, then a floating point comparison is performed.
A relational operator can have NaN
operands. If either or both operands is NaN
, the floating point comparison operation returns as follows:
Operator | Relation | Returns |
---|---|---|
< |
less | false |
> |
greater | false |
< =
|
less or equal | false |
>= |
greater or equal | false |
== |
equal | false |
!= |
unordered, less, or greater | true |
Class comparisons
For class objects, the relational operators compare the contents of the objects. Therefore, comparing against null
is invalid, as null
has no contents.
class C; C c; if (c < null) // error ...
In Expressions
InExpression: ShiftExpression in ShiftExpression ShiftExpression !in ShiftExpression
An associative array can be tested to see if an element is in the array:
int foo[string]; ... if ("hello" in foo) ...
The in
expression has the same precedence as the relational expressions <
, <
=
, etc. The return value of the InExpression is null
if the element is not in the array; if it is in the array it is a pointer to the element.
The !in
expression is the logical negation of the in
operation.
Shift Expressions
ShiftExpression: AddExpression ShiftExpression << AddExpression ShiftExpression >> AddExpression ShiftExpression >>> AddExpression
The operands must be integral types, and undergo the Integer Promotions. The result type is the type of the left operand after the promotions. The result value is the result of shifting the bits by the right operand's value.
<
<
is a left shift. >
>
is a signed right shift. >
>
>
is an unsigned right shift.
It's illegal to shift by the same or more bits than the size of the quantity being shifted:
int c; auto x = c << 33; // error
Add Expressions
AddExpression: MulExpression AddExpression + MulExpression AddExpression - MulExpression CatExpression
If the operands are of integral types, they undergo the Usual Arithmetic Conversions, and then are brought to a common type using the Usual Arithmetic Conversions.
If either operand is a floating point type, the other is implicitly converted to floating point and they are brought to a common type via the Usual Arithmetic Conversions.
If the operator is +
or -
, and the first operand is a pointer, and the second is an integral type, the resulting type is the type of the first operand, and the resulting value is the pointer plus (or minus) the second operand multiplied by the size of the type pointed to by the first operand.
If the second operand is a pointer, and the first is an integral type, and the operator is +
, the operands are reversed and the pointer arithmetic just described is applied.
If both operands are pointers, and the operator is +
, then it is illegal.
If both operands are pointers, and the operator is -
, the pointers are subtracted and the result is divided by the size of the type pointed to by the operands. In this calculation the assumed size of void
is one byte. It is an error if the pointers point to different types. The type of the result is ptrdiff_t
.
If both operands are of integral types and an overflow or underflow occurs in the computation, wrapping will happen. For example, uint.max + 1 == uint.min
, uint.min - 1 == uint.max
, int.max + 1 == int.min
, and int.min - 1 == int.max
.
Add expressions for floating point operands are not associative.
Cat Expressions
CatExpression: AddExpression ~ MulExpression
A CatExpression concatenates arrays, producing a dynamic array with the result. The arrays must be arrays of the same element type. If one operand is an array and the other is of that array's element type, that element is converted to an array of length 1 of that element, and then the concatenation is performed.
Mul Expressions
MulExpression: UnaryExpression MulExpression * UnaryExpression MulExpression / UnaryExpression MulExpression % UnaryExpression
The operands must be arithmetic types. They undergo the Usual Arithmetic Conversions.
For integral operands, the *
, /
, and %
correspond to multiply, divide, and modulus operations. For multiply, overflows are ignored and simply chopped to fit into the integral type.
For integral operands of the /
and %
operators, the quotient rounds towards zero and the remainder has the same sign as the dividend.
The following divide or modulus integral operands:
- denominator is 0
- signed
int.min
is the numerator and-1
is the denominator - signed
long.min
is the numerator and-1L
is the denominator
are illegal if encountered during Compile Time Execution.
Undefined Behavior: is exhibited if they are encountered during run time.core.checkedint
can be used to check for them and select a defined behavior. For floating point operands, the * and / operations correspond to the IEEE 754 floating point equivalents. % is not the same as the IEEE 754 remainder. For example, 15.0 % 10.0 == 5.0, whereas for IEEE 754, remainder(15.0,10.0) == -5.0.
Mul expressions for floating point operands are not associative.
Unary Expressions
UnaryExpression: & UnaryExpression ++ UnaryExpression -- UnaryExpression * UnaryExpression - UnaryExpression + UnaryExpression ! UnaryExpression ComplementExpression ( Type ) . Identifier ( Type ) . TemplateInstance DeleteExpression CastExpression PowExpression
Complement Expressions
ComplementExpression: ~ UnaryExpression
ComplementExpressions work on integral types (except bool
). All the bits in the value are complemented.
Note: the usual Integer Promotions are not performed prior to the complement operation.
New Expressions
NewExpression: new AllocatorArgumentsopt Type NewExpressionWithArgs NewExpressionWithArgs: new AllocatorArgumentsopt Type [ AssignExpression ] new AllocatorArgumentsopt Type ( ArgumentListopt ) NewAnonClassExpression AllocatorArguments: ( ArgumentListopt ) ArgumentList: AssignExpression AssignExpression , AssignExpression , ArgumentList
NewExpressions are used to allocate memory on the garbage collected heap (default) or using a class or struct specific allocator.
To allocate multidimensional arrays, the declaration reads in the same order as the prefix array declaration order.
char[][] foo; // dynamic array of strings ... foo = new char[][30]; // allocate array of 30 strings
The above allocation can also be written as:
foo = new char[][](30); // allocate array of 30 strings
To allocate the nested arrays, multiple arguments can be used:
int[][][] bar; ... bar = new int[][][](5, 20, 30);The code above is equivalent to:
bar = new int[][][5]; foreach (ref a; bar) { a = new int[][20]; foreach (ref b; a) { b = new int[30]; } }
If there is a new (
ArgumentList )
, then those arguments are passed to the class or struct specific allocator function after the size argument.
If a NewExpression is used as an initializer for a function local variable with scope
storage class, and the ArgumentList to new
is empty, then the instance is allocated on the stack rather than the heap or using the class specific allocator.
Delete Expressions
DeleteExpression: delete UnaryExpression
NOTE: delete
has been deprecated. Instead, please use destroy
if feasible, or core.memory.__delete
as a last resort.
If the UnaryExpression is a class object reference, and there is a destructor for that class, the destructor is called for that object instance.
Next, if the UnaryExpression is a class object reference, or a pointer to a struct instance, and the class or struct has overloaded operator delete, then that operator delete is called for that class object instance or struct instance.
Otherwise, the garbage collector is called to immediately free the memory allocated for the class instance or struct instance.
If the UnaryExpression is a pointer or a dynamic array, the garbage collector is called to immediately release the memory.
The pointer, dynamic array, or reference is set to null
after the delete is performed. Any attempt to reference the data after the deletion via another reference to it will result in undefined behavior.
If UnaryExpression is a variable allocated on the stack, the class destructor (if any) is called for that instance. Neither the garbage collector nor any class deallocator is called.
Undefined Behavior:- Using
delete
to free memory not allocated by the garbage collector. - Referring to data that has been the operand of
delete
.
Cast Expressions
CastExpression: cast ( Type ) UnaryExpression cast ( TypeCtorsopt ) UnaryExpression
A CastExpression converts the UnaryExpression to Type.
cast(foo) -p; // cast (-p) to type foo (foo) - p; // subtract p from foo
Any casting of a class reference to a derived class reference is done with a runtime check to make sure it really is a downcast. null
is the result if it isn't.
class A { ... } class B : A { ... } void test(A a, B b) { B bx = a; // error, need cast B bx = cast(B) a; // bx is null if a is not a B A ax = b; // no cast needed A ax = cast(A) b; // no runtime check needed for upcast }
In order to determine if an object o
is an instance of a class B
use a cast:
if (cast(B) o) { // o is an instance of B } else { // o is not an instance of B }
Casting a pointer type to and from a class type is done as a type paint (i.e. a reinterpret cast).
Casting a dynamic array to another dynamic array is done only if the array lengths multiplied by the element sizes match. The cast is done as a type paint, with the array length adjusted to match any change in element size. If there's not a match, a runtime error is generated.
import std.stdio; int main() { byte[] a = [1,2,3]; auto b = cast(int[])a; // runtime array cast misalignment int[] c = [1, 2, 3]; auto d = cast(byte[])c; // ok // prints: // [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0] writeln(d); return 0; }
Casting a static array to another static array is done only if the array lengths multiplied by the element sizes match; a mismatch is illegal. The cast is done as a type paint (aka a reinterpret cast). The contents of the array are not changed.
import core.stdc.stdio; void main() { byte[16] b = 3; int[4] ia = cast(int[4]) b; foreach (i; ia) printf("%x\n", i); /* prints: 3030303 3030303 3030303 3030303 */ }
Casting a floating point literal from one type to another changes its type, but internally it is retained at full precision for the purposes of constant folding.
void test() { real a = 3.40483L; real b; b = 3.40483; // literal is not truncated to double precision assert(a == b); assert(a == 3.40483); assert(a == 3.40483L); assert(a == 3.40483F); double d = 3.40483; // truncate literal when assigned to variable assert(d != a); // so it is no longer the same const double x = 3.40483; // assignment to const is not assert(x == a); // truncated if the initializer is visible }
Casting a floating point value to an integral type is the equivalent of converting to an integer using truncation.
void main() { int a = cast(int) 0.8f; assert(a == 0); long b = cast(long) 1.5; assert(b == 1L); long c = cast(long) -1.5; assert(c == -1); }
Casting a value v to a struct S, when value is not a struct of the same type, is equivalent to:
S(v)
Casting to a CastQual replaces the qualifiers to the type of the UnaryExpression.
shared int x; assert(is(typeof(cast(const)x) == const int));
Casting with no Type or CastQual removes any top level const
, immutable
, shared
or inout
type modifiers from the type of the UnaryExpression.
shared int x; assert(is(typeof(cast()x) == int));
Casting an expression to void
type is allowed to mark that the result is unused. On ExpressionStatement, it could be used properly to avoid "has no effect" error.
void foo(lazy void exp) {} void main() { foo(10); // NG - has no effect in expression '10' foo(cast(void)10); // OK }
Pow Expressions
PowExpression: PostfixExpression PostfixExpression ^^ UnaryExpression
PowExpression raises its left operand to the power of its right operand.
Postfix Expressions
PostfixExpression: PrimaryExpression PostfixExpression . Identifier PostfixExpression . TemplateInstance PostfixExpression . NewExpression PostfixExpression ++ PostfixExpression -- PostfixExpression ( ArgumentListopt ) TypeCtorsopt BasicType ( ArgumentListopt ) IndexExpression SliceExpression
Index Expressions
IndexExpression: PostfixExpression [ ArgumentList ]
PostfixExpression is evaluated. If PostfixExpression is an expression of type static array or dynamic array, the symbol $ is set to be the number of elements in the array. If PostfixExpression is a ValueSeq, the symbol $ is set to be the number of elements in the sequence. A new declaration scope is created for the evaluation of the ArgumentList and $ appears in that scope only.
If PostfixExpression is a ValueSeq, then the ArgumentList must consist of only one argument, and that must be statically evaluatable to an integral constant. That integral constant n then selects the nth expression in the ValueSeq, which is the result of the IndexExpression. It is an error if n is out of bounds of the ValueSeq.
Slice Expressions
SliceExpression: PostfixExpression [ ] PostfixExpression [ Slice ,opt ] Slice: AssignExpression AssignExpression , Slice AssignExpression .. AssignExpression AssignExpression .. AssignExpression , Slice
PostfixExpression is evaluated. if PostfixExpression is an expression of type static array or dynamic array, the special variable $ is declared and set to be the length of the array. A new declaration scope is created for the evaluation of the AssignExpression..AssignExpression and $ appears in that scope only.
The first AssignExpression is taken to be the inclusive lower bound of the slice, and the second AssignExpression is the exclusive upper bound. The result of the expression is a slice of the PostfixExpression array.
If the [ ]
form is used, the slice is of the entire array.
The type of the slice is a dynamic array of the element type of the PostfixExpression.
A SliceExpression is not a modifiable lvalue.
If the slice bounds can be known at compile time, the slice expression is implicitly convertible to an lvalue of static array. For example:
arr[a .. b] // typed T[]If both
a
and b
are integers (may be constant-folded), the slice expression can be converted to a static array type T[b - a]
. void foo(int[2] a) { assert(a == [2, 3]); } void bar(ref int[2] a) { assert(a == [2, 3]); a[0] = 4; a[1] = 5; assert(a == [4, 5]); } void baz(int[3] a) {} void main() { int[] arr = [1, 2, 3]; foo(arr[1 .. 3]); assert(arr == [1, 2, 3]); bar(arr[1 .. 3]); assert(arr == [1, 4, 5]); //baz(arr[1 .. 3]); // cannot match length }
The following forms of slice expression can be convertible to a static array type:
e
- An expression that contains no side effects.
-
a
,b
- Integers (that may be constant-folded).
Form | The length calculated at compile time |
---|---|
arr[] |
The compile time length of arr if it's known. |
arr[a .. b] |
b - a
|
arr[e-a .. e] |
a
|
arr[e .. e+b] |
b
|
arr[e-a .. e+b] |
a + b |
arr[e+a .. e+b] |
b - a if a <= b
|
arr[e-a .. e-b] |
a - b if a >= b
|
If PostfixExpression is a ValueSeq, then the result of the slice is a new ValueSeq formed from the upper and lower bounds, which must statically evaluate to integral constants. It is an error if those bounds are out of range.
Primary Expressions
PrimaryExpression: Identifier . Identifier TemplateInstance . TemplateInstance this super null true false $ IntegerLiteral FloatLiteral CharacterLiteral StringLiterals ArrayLiteral AssocArrayLiteral FunctionLiteral AssertExpression MixinExpression ImportExpression NewExpressionWithArgs FundamentalType . Identifier FundamentalType ( ArgumentListopt ) TypeCtor ( Type ) . Identifier TypeCtor ( Type ) ( ArgumentListopt ) Typeof TypeidExpression IsExpression ( Expression ) SpecialKeyword TraitsExpression
.Identifier
Identifier is looked up at module scope, rather than the current lexically nested scope.
this
Within a non-static member function, this
resolves to a reference to the object for which the function was called. If the object is an instance of a struct, this
will be a pointer to that instance. If a member function is called with an explicit reference to typeof(this)
, a non-virtual call is made:
class A { char get() { return 'A'; } char foo() { return typeof(this).get(); } char bar() { return this.get(); } } class B : A { override char get() { return 'B'; } } void main() { B b = new B(); assert(b.foo() == 'A'); assert(b.bar() == 'B'); }
Assignment to this
is not allowed.
super
super
is identical to this
, except that it is cast to this
's base class. It is an error if there is no base class. It is an error to use super
within a struct member function. (Only class Object
has no base class.) If a member function is called with an explicit reference to super
, a non-virtual call is made.
Assignment to super
is not allowed.
null
null
represents the null value for pointers, pointers to functions, delegates, dynamic arrays, associative arrays, and class objects. If it has not already been cast to a type, it is given the singular type typeof(null)
and it is an exact conversion to convert it to the null value for pointers, pointers to functions, delegates, etc. After it is cast to a type, such conversions are implicit, but no longer exact.
true, false
These are of type bool
and when cast to another integral type become the values 1 and 0, respectively.
Character Literals
Character literals are single characters and resolve to one of type char
, wchar
, or dchar
. If the literal is a \u
escape sequence, it resolves to type wchar
. If the literal is a \U
escape sequence, it resolves to type dchar
. Otherwise, it resolves to the type with the smallest size it will fit into.
String Literals
StringLiterals: StringLiteral StringLiterals StringLiteral
String literals can implicitly convert to any of the following types, they have equal weight:
immutable(char)* |
immutable(wchar)* |
immutable(dchar)* |
immutable(char)[] |
immutable(wchar)[] |
immutable(dchar)[] |
By default, a string literal is typed as a dynamic array, but the element count is known at compile time. So all string literals can be implicitly converted to static array types.
void foo(char[2] a) { assert(a == "bc"); } void bar(ref const char[2] a) { assert(a == "bc"); } void baz(const char[3] a) {} void main() { string str = "abc"; foo(str[1 .. 3]); bar(str[1 .. 3]); //baz(str[1 .. 3]); // cannot match length }
String literals have a 0 appended to them, which makes them easy to pass to C or C++ functions expecting a const char*
string. The 0 is not included in the .length
property of the string literal.
Array Literals
ArrayLiteral: [ ArgumentListopt ]
Array literals are a comma-separated list of AssignExpressions between square brackets [
and ]
. The AssignExpressions form the elements of a dynamic array, the length of the array is the number of elements. The common type of the all elements is taken to be the type of the array element, and all elements are implicitly converted to that type.
auto a1 = [1,2,3]; // type is int[], with elements 1, 2 and 3 auto a2 = [1u,2,3]; // type is uint[], with elements 1u, 2u, and 3u
By default, an array literal is typed as a dynamic array, but the element count is known at compile time. So all array literals can be implicitly converted to static array types.
void foo(long[2] a) { assert(a == [2, 3]); } void bar(ref long[2] a) { assert(a == [2, 3]); a[0] = 4; a[1] = 5; assert(a == [4, 5]); } void baz(const char[3] a) {} void main() { long[] arr = [1, 2, 3]; foo(arr[1 .. 3]); assert(arr == [1, 2, 3]); bar(arr[1 .. 3]); assert(arr == [1, 4, 5]); //baz(arr[1 .. 3]); // cannot match length }
If any of the arguments in the ArgumentList are a ValueSeq, then the elements of the ValueSeq are inserted as arguments in place of the sequence.
Array literals are allocated on the memory managed heap. Thus, they can be returned safely from functions:
int[] foo() { return [1, 2, 3]; }
When array literals are cast to another array type, each element of the array is cast to the new element type. When arrays that are not literals are cast, the array is reinterpreted as the new type, and the length is recomputed:
import std.stdio; void main() { // cast array literal const short[] ct = cast(short[]) [cast(byte)1, 1]; // this is equivalent with: // const short[] ct = [cast(short)1, cast(short)1]; writeln(ct); // writes [1, 1] // cast other array expression // --> normal behavior of CastExpression byte[] arr = [cast(byte)1, cast(byte)1]; short[] rt = cast(short[]) arr; writeln(rt); // writes [257] }In other words, casting literal expression will change the literal type.
Associative Array Literals
AssocArrayLiteral: [ KeyValuePairs ] KeyValuePairs: KeyValuePair KeyValuePair , KeyValuePairs KeyValuePair: KeyExpression : ValueExpression KeyExpression: AssignExpression ValueExpression: AssignExpression
Associative array literals are a comma-separated list of key:
value pairs between square brackets [
and ]
. The list cannot be empty. The common type of the all keys is taken to be the key type of the associative array, and all keys are implicitly converted to that type. The common type of the all values is taken to be the value type of the associative array, and all values are implicitly converted to that type. An AssocArrayLiteral cannot be used to statically initialize anything.
[21u:"he", 38:"ho", 2:"hi"]; // type is string[uint], // with keys 21u, 38u and 2u // and values "he", "ho", and "hi"
If any of the keys or values in the KeyValuePairs are a ValueSeq, then the elements of the ValueSeq are inserted as arguments in place of the sequence.
Function Literals
FunctionLiteral: function refopt Typeopt ParameterWithAttributes opt FunctionLiteralBody2 delegate refopt Typeopt ParameterWithMemberAttributes opt FunctionLiteralBody2 refopt ParameterWithMemberAttributes FunctionLiteralBody2 FunctionLiteralBody Identifier => AssignExpression ParameterWithAttributes: Parameters FunctionAttributesopt ParameterWithMemberAttributes: Parameters MemberFunctionAttributesopt FunctionLiteralBody2: => AssignExpression FunctionLiteralBody FunctionLiteralBody: BlockStatement FunctionContractsopt BodyStatement
FunctionLiterals (also known as Lambdas) enable embedding anonymous functions and anonymous delegates directly into expressions. Type is the return type of the function or delegate, if omitted it is inferred from any ReturnStatements in the FunctionLiteralBody. ParameterWithAttributes or ParameterWithMemberAttributes can be used to specify the parameters for the function. If these are omitted, the function defaults to the empty parameter list ( )
. The type of a function literal is pointer to function or pointer to delegate. If the keywords function
or delegate
are omitted, it is inferred from whether FunctionLiteralBody is actually accessing to the outer context.
For example:
int function(char c) fp; // declare pointer to a function void test() { static int foo(char c) { return 6; } fp = &foo; }is exactly equivalent to:
int function(char c) fp; void test() { fp = function int(char c) { return 6;} ; }
Also:
int abc(int delegate(int i)); void test() { int b = 3; int foo(int c) { return 6 + b; } abc(&foo); }is exactly equivalent to:
int abc(int delegate(int i)); void test() { int b = 3; abc( delegate int(int c) { return 6 + b; } ); }
and the following where the return type int
and function
/delegate
are inferred:
int abc(int delegate(int i)); int def(int function(int s)); void test() { int b = 3; abc( (int c) { return 6 + b; } ); // inferred to delegate def( (int c) { return c * 2; } ); // inferred to function //def( (int c) { return c * b; } ); // error! // Because the FunctionLiteralBody accesses b, then the function literal type // is inferred to delegate. But def cannot receive delegate. }
If the type of a function literal can be uniquely determined from its context, the parameter type inference is possible.
void foo(int function(int) fp); void test() { int function(int) fp = (n) { return n * 2; }; // The type of parameter n is inferred to int. foo((n) { return n * 2; }); // The type of parameter n is inferred to int. }
Anonymous delegates can behave like arbitrary statement literals. For example, here an arbitrary statement is executed by a loop:
double test() { double d = 7.6; float f = 2.3; void loop(int k, int j, void delegate() statement) { for (int i = k; i < j; i++) { statement(); } } loop(5, 100, { d += 1; }); loop(3, 10, { f += 3; }); return d + f; }
The syntax => AssignExpression
is equivalent to { return AssignExpression; }
.
The syntax Identifier => AssignExpression
is equivalent to (Identifier) { return AssignExpression; }
.
Example usage:
import std.stdio; void main() { auto i = 3; auto twice = function (int x) => x * 2; auto square = delegate (int x) => x * x; auto n = 5; auto mul_n = (int x) => x * n; writeln(twice(i)); // prints 6 writeln(square(i)); // prints 9 writeln(mul_n(i)); // prints 15 }
When comparing with nested functions, the function
form is analogous to static or non-nested functions, and the delegate
form is analogous to non-static nested functions. In other words, a delegate literal can access stack variables in its enclosing function, a function literal cannot.
Uniform construction syntax for built-in scalar types
The implicit conversions of built-in scalar types can be explicitly represented by using function call syntax. For example:
auto a = short(1); // implicitly convert an integer literal '1' to short auto b = double(a); // implicitly convert a short variable 'a' to double auto c = byte(128); // error, 128 cannot be represented in a byte
If the argument is omitted, it means default construction of the scalar type:
auto a = ushort(); // same as: ushort.init auto b = wchar(); // same as: wchar.init auto c = creal(); // same as: creal.init
Assert Expressions
AssertExpression: assert ( AssertArguments ) AssertArguments: AssignExpression ,opt AssignExpression , AssignExpression ,opt
The first AssignExpression must evaluate to true. If it does not, an Assert Failure has occurred and the program enters an Invalid State.
If the first AssignExpression consists entirely of compile time constants, and evaluates to false, it is a special case; it signifies that it is unreachable code. Compile Time Function Execution (CTFE) is not attempted.
AssertExpression has different semantics if it is in a unittest
or in
contract.
The second AssignExpression, if present, must be implicitly convertible to type const(char)[]
.
If the first AssignExpression is a reference to a class instance for which a Class Invariant exists, the Class Invariant must hold.
If the first AssignExpression is a pointer to a struct instance for which a Struct Invariant exists, the Struct Invariant must hold.
The type of an AssertExpression is void
.
- continuing execution
- immediately halting via execution of a special CPU instruction
- aborting the program
- calling the assert failure function in the corresponding C runtime library
- throwing the
AssertError
exception in the D runtime library
void main() { assert(0, "an" ~ " error message"); }
When compiled and run, it will produce the message:
[email protected](3) an error message
The implementation may handle the case of the first AssignExpression evaluating at compile time to false differently in that in release mode it may simply generate a HLT
instruction or equivalent.
- Do not have side effects in either AssignExpression that subsequent code depends on.
- AssertExpressions are intended to detect bugs in the program, do not use for detecting input or environmental errors.
- Do not attempt to resume normal execution after an Assert Failure.
Mixin Expressions
MixinExpression: mixin ( ArgumentList )
Each AssignExpression in the ArgumentList is evaluated at compile time, and the result must be representable as a string. The resulting strings are concatenated to form a string. The text contents of the string must be compilable as a valid Expression, and is compiled as such.
int foo(int x) { return mixin("x +", 1) * 7; // same as ((x + 1) * 7) }
Import Expressions
ImportExpression: import ( AssignExpression )
The AssignExpression must evaluate at compile time to a constant string. The text contents of the string are interpreted as a file name. The file is read, and the exact contents of the file become a string literal.
Implementations may restrict the file name in order to avoid directory traversal security vulnerabilities. A possible restriction might be to disallow any path components in the file name.
Note that by default an import expression will not compile unless one or more paths are passed via the -J switch. This tells the compiler where it should look for the files to import. This is a security feature.
void foo() { // Prints contents of file foo.txt writeln(import("foo.txt")); }
Typeid Expressions
TypeidExpression: typeid ( Type ) typeid ( Expression )
If Type, returns an instance of class TypeInfo
corresponding to Type.
If Expression, returns an instance of class TypeInfo
corresponding to the type of the Expression. If the type is a class, it returns the TypeInfo
of the dynamic type (i.e. the most derived type). The Expression is always executed.
class A { } class B : A { } void main() { writeln(typeid(int)); // int uint i; writeln(typeid(i++)); // uint writeln(i); // 1 A a = new B(); writeln(typeid(a)); // B writeln(typeid(typeof(a))); // A }
IsExpression
IsExpression: is ( Type ) is ( Type : TypeSpecialization ) is ( Type == TypeSpecialization ) is ( Type : TypeSpecialization , TemplateParameterList ) is ( Type == TypeSpecialization , TemplateParameterList ) is ( Type Identifier ) is ( Type Identifier : TypeSpecialization ) is ( Type Identifier == TypeSpecialization ) is ( Type Identifier : TypeSpecialization , TemplateParameterList ) is ( Type Identifier == TypeSpecialization , TemplateParameterList ) TypeSpecialization: Type struct union class interface enum __vector function delegate super const immutable inout shared return __parameters module package
IsExpressions are evaluated at compile time and are used for checking for valid types, comparing types for equivalence, determining if one type can be implicitly converted to another, and deducing the subtypes of a type. The result of an IsExpression is a boolean of value true
if the condition is satisfied. If the condition is not satisfied, the result is a boolean of value false
.
Type is the type being tested. It must be syntactically correct, but it need not be semantically correct. If it is not semantically correct, the condition is not satisfied.
Identifier is declared to be an alias of the resulting type if the condition is satisfied. The Identifier forms can only be used if the IsExpression appears in a StaticIfCondition.
TypeSpecialization is the type that Type is being compared against.
The forms of the IsExpression are:
-
is (
Type)
The condition is satisfied ifType
is semantically correct (it must be syntactically correct regardless).alias int func(int); // func is a alias to a function type void foo() { if (is(func[])) // not satisfied because arrays of // functions are not allowed writeln("satisfied"); else writeln("not satisfied"); if (is([][])) // error, [][] is not a syntactically valid type ... }
-
is (
Type:
TypeSpecialization)
The condition is satisfied if Type is semantically correct and it is the same as or can be implicitly converted to TypeSpecialization. TypeSpecialization is only allowed to be a Type.alias Bar = short; void foo() { if (is(Bar : int)) // satisfied because short can be // implicitly converted to int writeln("satisfied"); else writeln("not satisfied"); }
-
is (
Type==
TypeSpecialization)
The condition is satisfied if Type is semantically correct and is the same type as TypeSpecialization. If TypeSpecialization is one ofstruct
union
class
interface
enum
function
delegate
const
immutable
shared
module
package
then the condition is satisfied if Type is one of those. Package modules are considered to be both packages and modules.alias Bar = short; void foo() { if (is(Bar == int)) // not satisfied because short is not // the same type as int writeln("satisfied"); else writeln("not satisfied"); }
-
is (
Type Identifier)
The condition is satisfied if Type is semantically correct. If so, Identifier is declared to be an alias of Type.alias Bar = short; void foo() { static if (is(Bar T)) alias S = T; else alias S = long; writeln(typeid(S)); // prints "short" if (is(Bar T)) // error, Identifier T form can // only be in StaticIfConditions ... }
-
is (
Type Identifier:
TypeSpecialization)
The condition is satisfied if Type is the same as TypeSpecialization, or if Type is a class and TypeSpecialization is a base class or base interface of it. The Identifier is declared to be either an alias of the TypeSpecialization or, if TypeSpecialization is dependent on Identifier, the deduced type.alias Bar = int; alias Abc = long*; void foo() { static if (is(Bar T : int)) alias S = T; else alias S = long; writeln(typeid(S)); // prints "int" static if (is(Abc U : U*)) { U u; writeln(typeid(typeof(u))); // prints "long" } }
The way the type of Identifier is determined is analogous to the way template parameter types are determined by TemplateTypeParameterSpecialization. -
is (
Type Identifier==
TypeSpecialization)
The condition is satisfied if Type is semantically correct and is the same as TypeSpecialization. The Identifier is declared to be either an alias of the TypeSpecialization or, if TypeSpecialization is dependent on Identifier, the deduced type. If TypeSpecialization is one ofstruct
union
class
interface
enum
function
delegate
const
immutable
shared
then the condition is satisfied if Type is one of those. Furthermore, Identifier is set to be an alias of the type:keyword alias type for Identifier struct
Type union
Type class
Type interface
Type super
TypeSeq of base classes and interfaces enum
the base type of the enum function
TypeSeq of the function parameter types. For C- and D-style variadic functions, only the non-variadic parameters are included. For typesafe variadic functions, the ...
is ignored.delegate
the function type of the delegate return
the return type of the function, delegate, or function pointer __parameters
the parameter sequence of a function, delegate, or function pointer. This includes the parameter types, names, and default values. const
Type immutable
Type shared
Type alias Bar = short; enum E : byte { Emember } void foo() { static if (is(Bar T == int)) // not satisfied, short is not int alias S = T; alias U = T; // error, T is not defined static if (is(E V == enum)) // satisfied, E is an enum V v; // v is declared to be a byte }
-
is (
Type:
TypeSpecialization,
TemplateParameterList)
is (
Type==
TypeSpecialization,
TemplateParameterList)
is (
Type Identifier:
TypeSpecialization,
TemplateParameterList)
is (
Type Identifier==
TypeSpecialization,
TemplateParameterList)
More complex types can be pattern matched; the TemplateParameterList declares symbols based on the parts of the pattern that are matched, analogously to the way implied template parameters are matched.import std.stdio, std.typecons; void main() { alias Tup = Tuple!(int, string); alias AA = long[string]; static if (is(Tup : Template!Args, alias Template, Args...)) { writeln(__traits(isSame, Template, Tuple)); // true writeln(is(Template!(int, string) == Tup)); // true writeln(typeid(Args[0])); // int writeln(typeid(Args[1])); // immutable(char)[] } static if (is(AA T : T[U], U : string)) { writeln(typeid(T)); // long writeln(typeid(U)); // string } static if (is(AA A : A[B], B : int)) { assert(0); // should not match, as B is not an int } static if (is(int[10] W : W[len], int len)) { writeln(typeid(W)); // int writeln(len); // 10 } static if (is(int[10] X : X[len], int len : 5)) { assert(0); // should not match, len should be 10 } }
Special Keywords
SpecialKeyword: __FILE__ __FILE_FULL_PATH__ __MODULE__ __LINE__ __FUNCTION__ __PRETTY_FUNCTION__
__FILE__
and __LINE__
expand to the source file name and line number at the point of instantiation. The path of the source file is left up to the compiler.
__FILE_FULL_PATH__
expands to the absolute source file name at the point of instantiation.
__MODULE__
expands to the module name at the point of instantiation.
__FUNCTION__
expands to the fully qualified name of the function at the point of instantiation.
__PRETTY_FUNCTION__
is similar to __FUNCTION__
, but also expands the function return type, its parameter types, and its attributes.
Example:
module test; import std.stdio; void test(string file = __FILE__, size_t line = __LINE__, string mod = __MODULE__, string func = __FUNCTION__, string pretty = __PRETTY_FUNCTION__, string fileFullPath = __FILE_FULL_PATH__) { writefln("file: '%s', line: '%s', module: '%s',\nfunction: '%s', " ~ "pretty function: '%s',\nfile full path: '%s'", file, line, mod, func, pretty, fileFullPath); } int main(string[] args) { test(); return 0; }
Assuming the file was at /example/test.d, this will output:
file: 'test.d', line: '13', module: 'test', function: 'test.main', pretty function: 'int test.main(string[] args)', file full path: '/example/test.d'
Associativity and Commutativity
An implementation may rearrange the evaluation of expressions according to arithmetic associativity and commutativity rules as long as, within that thread of execution, no observable difference is possible.
This rule precludes any associative or commutative reordering of floating point expressions.
© 1999–2021 The D Language Foundation
Licensed under the Boost License 1.0.
https://dlang.org/spec/expression.html