From 87641933b99a85323e546d2b8cf0df14ef4446a7 Mon Sep 17 00:00:00 2001 From: Ehud Tamir Date: Thu, 15 Oct 2026 14:17:36 +0300 Subject: [PATCH 1/2] Take no `.init` of the inspected type in traits WEKAPP-683364 With LDC's `.init` size check (weka/ldc), Phobos templates failed on types whose `.init` literal is huge: the range traits (`isInputRange`, `isForwardRange`, `isBidirectionalRange`, `hasSlicing`, `ElementType`), `CommonType`, `isIterable`, `ForeachType`, `fullyQualifiedName`, `hasNested`, `hasElaborateAssign`, the alias-this `*TypeOf` traits, `ifTestable`, `isTuple`, `Rebindable`, `apply`, `map`, `each`, `substitute`, `min`/`max` (`extremum`) and `Appender.put`. Each built `T.init` only to name a type or call a member. They now use `lvalueOf`/`rvalueOf`, a null dereference, or a type-only form, so no literal is built. `CommonType` and `ifTestable` keep `.init` for operands that are not structs or unions: null and array literals merge differently in `?:`, and a scalar or null literal converts where a function result does not. The range traits add `is(typeof(R.sizeof))`, which lays out a struct before its member functions' attributes are inferred, as `R.init` did. Answers that change: `ifTestable` with a `const` or `immutable` parameter is false for a struct or union with indirections; `ElementType` of an enum based on an array is `void` (it was `inout(int)` for `int[]`, and `isInputRange` is false for such enums); `each` with a `ref` parameter compiles on a range whose `front` is a field. --- std/algorithm/iteration.d | 14 ++--- std/algorithm/searching.d | 10 ++-- std/array.d | 4 +- std/range/primitives.d | 26 ++++++--- std/traits.d | 107 +++++++++++++++++++++++++++++--------- std/typecons.d | 23 ++++---- 6 files changed, 128 insertions(+), 56 deletions(-) diff --git a/std/algorithm/iteration.d b/std/algorithm/iteration.d index 1453d2b64b9..359e6b6c911 100644 --- a/std/algorithm/iteration.d +++ b/std/algorithm/iteration.d @@ -461,7 +461,7 @@ if (fun.length >= 1) // this validation loop can be moved into a template. foreach (f; _funs) { - static assert(!is(typeof(f(RE.init)) == void), + static assert(!is(typeof(f(rvalueOf!RE)) == void), "Mapping function(s) must not return void: " ~ _funs.stringof); } } @@ -472,7 +472,7 @@ if (fun.length >= 1) // Do the validation separately for single parameters due to // https://issues.dlang.org/show_bug.cgi?id=15777. - static assert(!is(typeof(_fun(RE.init)) == void), + static assert(!is(typeof(_fun(rvalueOf!RE)) == void), "Mapping function(s) must not return void: " ~ _funs.stringof); } @@ -584,7 +584,7 @@ private struct MapResult(alias fun, Range) static if (isRandomAccessRange!R) { - static if (is(typeof(Range.init[ulong.max]))) + static if (is(typeof(lvalueOf!Range[ulong.max]))) private alias opIndex_t = ulong; else private alias opIndex_t = uint; @@ -885,10 +885,10 @@ private: alias BinaryArgs = AliasSeq!(fun, "i", "a"); enum isRangeUnaryIterable(R) = - is(typeof(unaryFun!fun(R.init.front))); + is(typeof(unaryFun!fun(lvalueOf!R.front))); enum isRangeBinaryIterable(R) = - is(typeof(binaryFun!BinaryArgs(0, R.init.front))); + is(typeof(binaryFun!BinaryArgs(0, lvalueOf!R.front))); enum isRangeIterable(R) = isInputRange!R && @@ -6883,10 +6883,10 @@ if (isInputRange!R && Substs.length >= 2 && !is(CommonType!(Substs) == void)) private { - typeof(R.init.drop(0)) rest; + typeof(rvalueOf!R.drop(0)) rest; Ins needles; - typeof(R.init.take(0)) skip; // skip before next hit + typeof(rvalueOf!R.take(0)) skip; // skip before next hit alias Hit = size_t; // 0 iff no hit, otherwise hit in needles[index-1] alias E = Tuple!(typeof(skip), Hit); Hit hitNr; // hit number: 0 means no hit, otherwise index+1 to needles that matched diff --git a/std/algorithm/searching.d b/std/algorithm/searching.d index 465723c16ed..efdceca224e 100644 --- a/std/algorithm/searching.d +++ b/std/algorithm/searching.d @@ -1308,7 +1308,7 @@ Returns: */ private auto extremum(alias map, alias selector = "a < b", Range)(Range r) if (isInputRange!Range && !isInfinite!Range && - is(typeof(unaryFun!map(ElementType!(Range).init)))) + is(typeof(unaryFun!map(rvalueOf!(ElementType!Range))))) in { assert(!r.empty, "r is an empty range"); @@ -1328,7 +1328,7 @@ private auto extremum(alias map, alias selector = "a < b", Range, (Range r, RangeElementType seedElement) if (isInputRange!Range && !isInfinite!Range && !is(CommonType!(ElementType!Range, RangeElementType) == void) && - is(typeof(unaryFun!map(ElementType!(Range).init)))) + is(typeof(unaryFun!map(rvalueOf!(ElementType!Range))))) { import std.typecons : Rebindable2; @@ -1367,7 +1367,7 @@ if (isInputRange!Range && !isInfinite!Range && } else { - alias MapType = Unqual!(typeof(mapFun(CommonElement.init))); + alias MapType = Unqual!(typeof(mapFun(rvalueOf!CommonElement))); MapType extremeElementMapped = mapFun(extremeElement.get); // direct access via a random access range is faster @@ -1402,7 +1402,7 @@ if (isInputRange!Range && !isInfinite!Range && private auto extremum(alias selector = "a < b", Range)(Range r) if (isInputRange!Range && !isInfinite!Range && - !is(typeof(unaryFun!selector(ElementType!(Range).init)))) + !is(typeof(unaryFun!selector(rvalueOf!(ElementType!Range))))) { return extremum!(a => a, selector)(r); } @@ -1413,7 +1413,7 @@ private auto extremum(alias selector = "a < b", Range, (Range r, RangeElementType seedElement) if (isInputRange!Range && !isInfinite!Range && !is(CommonType!(ElementType!Range, RangeElementType) == void) && - !is(typeof(unaryFun!selector(ElementType!(Range).init)))) + !is(typeof(unaryFun!selector(rvalueOf!(ElementType!Range))))) { return extremum!(a => a, selector)(r, seedElement); } diff --git a/std/array.d b/std/array.d index 494fa297b4b..047694a014c 100644 --- a/std/array.d +++ b/std/array.d @@ -3653,13 +3653,13 @@ if (isDynamicArray!A) enum bool canPutConstRange = isInputRange!(Unqual!Range) && !isInputRange!Range && - is(typeof(Appender.init.put(Range.init.front))); + is(typeof(Appender.init.put(rvalueOf!Range.front))); } private template canPutRange(Range) { enum bool canPutRange = isInputRange!Range && - is(typeof(Appender.init.put(Range.init.front))); + is(typeof(Appender.init.put(rvalueOf!Range.front))); } /** diff --git a/std/range/primitives.d b/std/range/primitives.d index dddcae9afd2..880054b0641 100644 --- a/std/range/primitives.d +++ b/std/range/primitives.d @@ -172,11 +172,14 @@ Params: Returns: `true` if R is an input range (possibly with element type `E`), `false` if not */ +// `R.sizeof` lays out a struct before the lambdas below infer its member functions' attributes: a range +// trait evaluated inside the struct's own declaration (a member's return type, like `Chunks.opSlice`) would +// otherwise infer those members impure and `@system`. enum bool isInputRange(R) = - is(typeof(R.init) == R) - && is(typeof((R r) { return r.empty; } (R.init)) == bool) + is(typeof(lvalueOf!R) == R) && is(typeof(R.sizeof)) + && is(typeof((R r) { return r.empty; } (rvalueOf!R)) == bool) && (is(typeof((return ref R r) => r.front)) || is(typeof(ref (return ref R r) => r.front))) - && !is(typeof((R r) { return r.front; } (R.init)) == void) + && !is(typeof((R r) { return r.front; } (rvalueOf!R)) == void) && is(typeof((R r) => r.popFront)); /// ditto @@ -1022,7 +1025,7 @@ Returns: `true` if R is a forward range (possibly with element type `E`), `false` if not */ enum bool isForwardRange(R) = isInputRange!R - && is(typeof((R r) { return r.save; } (R.init)) == R); + && is(typeof((R r) { return r.save; } (rvalueOf!R)) == R); /// ditto enum bool isForwardRange(R, E) = @@ -1090,7 +1093,7 @@ Returns: enum bool isBidirectionalRange(R) = isForwardRange!R && is(typeof((R r) => r.popBack)) && (is(typeof((return ref R r) => r.back)) || is(typeof(ref (return ref R r) => r.back))) - && is(typeof(R.init.back.init) == ElementType!R); + && is(typeof(backOf(lvalueOf!R)) == ElementType!R); /// ditto enum bool isBidirectionalRange(R, E) = @@ -1363,6 +1366,12 @@ enum bool hasMobileElements(R) = static assert( hasMobileElements!(dchar[])); } +// `r.front` / `r.back` in a return statement, so a member function is called whether or not it is a `@property` +// (in a bare `typeof(r.front)` it is not), a field or a `ref` result is returned by reference (no copy, so a +// non-copyable element type is answered too), and no `.init` of the element type is ever built. +private auto ref frontOf(R)(return ref R r) { return r.front; } +private auto ref backOf(R)(return ref R r) { return r.back; } + /** The element type of `R`. `R` does not have to be a range. The element type is determined as the type yielded by `r.front` for an @@ -1373,7 +1382,8 @@ object `r` of type `R`. For example, `ElementType!(T[])` is */ template ElementType(R) { - static if (is(typeof(R.init.front.init) T)) + // `R.sizeof`: see `isInputRange`. + static if (is(typeof(R.sizeof)) && is(typeof(frontOf(lvalueOf!R)) T)) alias ElementType = T; else alias ElementType = void; @@ -1763,7 +1773,7 @@ The following expression must be true for `hasSlicing` to be `true`: ---- isForwardRange!R && !(isAutodecodableString!R && !isAggregateType!R) - && is(typeof((R r) { return r[1 .. 1].length; } (R.init)) == size_t) + && is(typeof((R r) { return r[1 .. 1].length; } (rvalueOf!R)) == size_t) && (is(typeof(lvalueOf!R[1 .. 1]) == R) || isInfinite!R) && (!is(typeof(lvalueOf!R[0 .. $])) || is(typeof(lvalueOf!R[0 .. $]) == R)) && (!is(typeof(lvalueOf!R[0 .. $])) || isInfinite!R @@ -1776,7 +1786,7 @@ The following expression must be true for `hasSlicing` to be `true`: */ enum bool hasSlicing(R) = isForwardRange!R && !(isAutodecodableString!R && !isAggregateType!R) - && is(typeof((R r) { return r[1 .. 1].length; } (R.init)) == size_t) + && is(typeof((R r) { return r[1 .. 1].length; } (rvalueOf!R)) == size_t) && (is(typeof(lvalueOf!R[1 .. 1]) == R) || isInfinite!R) && (!is(typeof(lvalueOf!R[0 .. $])) || is(typeof(lvalueOf!R[0 .. $]) == R)) && (!is(typeof(lvalueOf!R[0 .. $])) || isInfinite!R diff --git a/std/traits.d b/std/traits.d index e718e4d2305..a4181cf4dd6 100644 --- a/std/traits.d +++ b/std/traits.d @@ -848,13 +848,13 @@ private template fqnType(T, { import std.conv : to; enum fqnType = chain!( - fqnType!(typeof(T.init[0]), qualifiers) ~ "[" ~ to!string(T.length) ~ "]" + fqnType!(typeof(lvalueOf!T[0]), qualifiers) ~ "[" ~ to!string(T.length) ~ "]" ); } else static if (isArray!T) { enum fqnType = chain!( - fqnType!(typeof(T.init[0]), qualifiers) ~ "[]" + fqnType!(typeof(lvalueOf!T[0]), qualifiers) ~ "[]" ); } else static if (isAssociativeArray!T) @@ -2575,7 +2575,7 @@ template hasNested(T) import std.meta : Filter; static if (isStaticArray!T && T.length) - enum hasNested = hasNested!(typeof(T.init[0])); + enum hasNested = hasNested!(typeof(lvalueOf!T[0])); else static if (is(T == class) || is(T == struct) || is(T == union)) { // prevent infinite recursion for class with member of same type @@ -3659,12 +3659,12 @@ template hasElaborateAssign(S) { static if (isStaticArray!S && S.length) { - enum bool hasElaborateAssign = hasElaborateAssign!(typeof(S.init[0])); + enum bool hasElaborateAssign = hasElaborateAssign!(typeof(lvalueOf!S[0])); } else static if (is(S == struct)) { - enum hasElaborateAssign = is(typeof(S.init.opAssign(rvalueOf!S))) || - is(typeof(S.init.opAssign(lvalueOf!S))) || + enum hasElaborateAssign = is(typeof(lvalueOf!S.opAssign(rvalueOf!S))) || + is(typeof(lvalueOf!S.opAssign(lvalueOf!S))) || anySatisfy!(.hasElaborateAssign, FieldTypeTuple!S); } else @@ -4842,6 +4842,23 @@ if (is(T == class)) // Type Conversion //:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::// +// A struct's `.init` builds its whole literal, so a function result of the type stands in +// for it. Other operands keep `.init`, whose null and array literals merge differently in `?:`. +// Operands can be values as well as types: `among` passes its values through `CommonType`. +private enum isAggregateOperand(T...) = is(T[0] == struct) || is(T[0] == union); + +private template CommonTypeOfTwo(T...) +{ + static if (isAggregateOperand!(T[0]) && isAggregateOperand!(T[1])) + alias CommonTypeOfTwo = typeof(true ? rvalueOf!(T[0]) : rvalueOf!(T[1])); + else static if (isAggregateOperand!(T[0])) + alias CommonTypeOfTwo = typeof(true ? rvalueOf!(T[0]) : T[1].init); + else static if (isAggregateOperand!(T[1])) + alias CommonTypeOfTwo = typeof(true ? T[0].init : rvalueOf!(T[1])); + else + alias CommonTypeOfTwo = typeof(true ? T[0].init : T[1].init); +} + /** Get the type that all types can be implicitly converted to. Useful e.g. in figuring out an array type from a bunch of initializing @@ -4851,8 +4868,13 @@ types have no common type. template CommonType(T...) { static if (T.length == 1) - alias CommonType = typeof(T[0].init); - else static if (is(typeof(true ? T[0].init : T[1].init) U)) + { + static if (isAggregateOperand!(T[0])) + alias CommonType = typeof(rvalueOf!(T[0])); + else + alias CommonType = typeof(T[0].init); + } + else static if (is(CommonTypeOfTwo!(T[0], T[1]) U)) alias CommonType = CommonType!(U, T[2 .. $]); else alias CommonType = void; @@ -5773,7 +5795,7 @@ Note: Trying to use returned value will result in a */ template BooleanTypeOf(T) { - static if (is(typeof(__traits(getMember, T.init, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) + static if (is(typeof(__traits(getMember, lvalueOf!T, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) alias X = BooleanTypeOf!AT; else alias X = OriginalType!T; @@ -5824,7 +5846,7 @@ template BooleanTypeOf(T) */ template IntegralTypeOf(T) { - static if (is(typeof(__traits(getMember, T.init, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) + static if (is(typeof(__traits(getMember, lvalueOf!T, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) alias X = IntegralTypeOf!AT; else alias X = OriginalType!T; @@ -5860,7 +5882,7 @@ template IntegralTypeOf(T) */ template FloatingPointTypeOf(T) { - static if (is(typeof(__traits(getMember, T.init, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) + static if (is(typeof(__traits(getMember, lvalueOf!T, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) alias X = FloatingPointTypeOf!AT; else alias X = OriginalType!T; @@ -5945,7 +5967,7 @@ template SignedTypeOf(T) */ template CharTypeOf(T) { - static if (is(typeof(__traits(getMember, T.init, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) + static if (is(typeof(__traits(getMember, lvalueOf!T, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) alias X = CharTypeOf!AT; else alias X = OriginalType!T; @@ -5986,7 +6008,7 @@ template CharTypeOf(T) */ template StaticArrayTypeOf(T) { - static if (is(typeof(__traits(getMember, T.init, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) + static if (is(typeof(__traits(getMember, lvalueOf!T, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) alias X = StaticArrayTypeOf!AT; else alias X = OriginalType!T; @@ -6143,7 +6165,7 @@ template StringTypeOf(T) */ template AssocArrayTypeOf(T) { - static if (is(typeof(__traits(getMember, T.init, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) + static if (is(typeof(__traits(getMember, lvalueOf!T, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) alias X = AssocArrayTypeOf!AT; else alias X = OriginalType!T; @@ -6184,7 +6206,7 @@ template BuiltinTypeOf(T) alias BuiltinTypeOf = void; else { - static if (is(typeof(__traits(getMember, T.init, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) + static if (is(typeof(__traits(getMember, lvalueOf!T, __traits(getAliasThis, T)[0])) AT) && !is(AT[] == AT)) alias X = BuiltinTypeOf!AT; else alias X = OriginalType!T; @@ -7273,7 +7295,11 @@ enum bool isAggregateType(T) = is(T == struct) || is(T == union) || * that define `opApply` with a single loop variable, and builtin dynamic, * static and associative arrays. */ -enum bool isIterable(T) = is(typeof({ foreach (elem; T.init) {} })); +// An lvalue serves opApply (a null dereference: an opApply taking a `ref` `@safe` delegate +// refuses `lvalueOf!T`, and every opApply refuses `rvalueOf!T`); an rvalue serves a +// non-copyable range, which foreach would otherwise copy. +enum bool isIterable(T) = is(typeof({ T* p = null; foreach (elem; *p) {} })) + || is(typeof({ foreach (elem; rvalueOf!T) {} })); /// @safe unittest @@ -8057,15 +8083,31 @@ has both opApply and a range interface. */ template ForeachType(T) { - alias ForeachType = typeof( - (inout int x = 0) + static if (is(typeof({ T* p = null; foreach (elem; *p) {} }))) { - foreach (elem; T.init) + alias ForeachType = typeof( + (inout int x = 0) { - return elem; - } - assert(0); - }()); + T* p = null; + foreach (elem; *p) + { + return elem; + } + assert(0); + }()); + } + else + { + alias ForeachType = typeof( + (inout int x = 0) + { + foreach (elem; rvalueOf!T) + { + return elem; + } + assert(0); + }()); + } } /// @@ -9044,7 +9086,15 @@ enum bool allSameType(Ts...) = Returns: `true` iff the type `T` can be tested in an $(D if)-expression, that is if $(D if (pred(T.init)) {}) is compilable. */ -enum ifTestable(T, alias pred = a => a) = __traits(compiles, { if (pred(T.init)) {} }); +template ifTestable(T, alias pred = a => a) +{ + // See `isAggregateOperand`: a scalar's `.init` converts by value range (`int` to `ubyte`) and a + // null literal to any qualifier, which a function result does not. + static if (isAggregateOperand!T) + enum ifTestable = __traits(compiles, { if (pred(rvalueOf!T)) {} }); + else + enum ifTestable = __traits(compiles, { if (pred(T.init)) {} }); +} /// @safe unittest @@ -9075,6 +9125,15 @@ enum ifTestable(T, alias pred = a => a) = __traits(compiles, { if (pred(T.init)) static assert(!ifTestable!(bool, a => BoolWrapper(a))); } +@safe unittest +{ + class C {} + static assert( ifTestable!(int, (ubyte a) => a)); + static assert( ifTestable!(int*, (immutable(int)* p) => p)); + static assert( ifTestable!(int[], (immutable(int)[] a) => a)); + static assert( ifTestable!(C, (immutable C c) => c)); +} + /** * Detect whether `X` is a type. Analogous to `is(X)`. This is useful when used * in conjunction with other templates, e.g. `allSatisfy!(isType, X)`. diff --git a/std/typecons.d b/std/typecons.d index 460cd427ed0..c653b73263e 100644 --- a/std/typecons.d +++ b/std/typecons.d @@ -531,8 +531,8 @@ private enum areCompatibleTuples(Tup1, Tup2, string op) = { static foreach (i; 0 .. Tup1.Types.length) {{ - auto lhs = typeof(tup1.field[i]).init; - auto rhs = typeof(tup2.field[i]).init; + auto lhs = rvalueOf!(typeof(tup1.field[i])); + auto rhs = rvalueOf!(typeof(tup2.field[i])); static if (op == "=") lhs = rhs; else @@ -681,7 +681,7 @@ if (distinctFieldNames!(Specs)) { static foreach (i; 0 .. Types.length) // Rely on the field layout { - static assert(typeof(return).init.tupleof[i].offsetof == + static assert(typeof(return).tupleof[i].offsetof == expand[i].offsetof); } return *cast(typeof(return)*) &(field[0]); @@ -2274,10 +2274,13 @@ template tuple(Names...) Returns: true if `T` is a `Tuple` type, false otherwise. */ -enum isTuple(T) = __traits(compiles, +// The `is` covers qualified tuples whose fields have indirections, which the call below +// cannot convert to the mutable `Tuple`. +enum isTuple(T) = is(Unqual!T == Tuple!Specs, Specs...) || + __traits(compiles, { void f(Specs...)(Tuple!Specs tup) {} - f(T.init); + f(rvalueOf!T); } ); /// @@ -2445,11 +2448,11 @@ struct Rebindable(T) if (!is(T == class) && !is(T == interface) && !isDynamicArray!T && !isAssociativeArray!T) { private: - static if (isAssignable!(typeof(cast() T.init))) + static if (isAssignable!(typeof(cast() rvalueOf!T))) { enum useQualifierCast = true; - typeof(cast() T.init) data; + typeof(cast() rvalueOf!T) data; } else { @@ -3003,11 +3006,11 @@ Rebindable!T rebindable(T)(Rebindable!T obj) package(std) struct Rebindable2(T) { private: - static if (isAssignable!(typeof(cast() T.init))) + static if (isAssignable!(typeof(cast() rvalueOf!T))) { enum useQualifierCast = true; - typeof(cast() T.init) data; + typeof(cast() rvalueOf!T) data; } else { @@ -4834,7 +4837,7 @@ template apply(alias fun) auto apply(T)(auto ref T t) if (isInstanceOf!(Nullable, T)) { - alias FunType = typeof(unaryFun!fun(T.init.get)); + alias FunType = typeof(unaryFun!fun(rvalueOf!T.get)); enum MustWrapReturn = !isInstanceOf!(Nullable, FunType); From 4a612034920136990f24664c5a42f65650652e57 Mon Sep 17 00:00:00 2001 From: Ehud Tamir Date: Wed, 21 Oct 2026 15:07:02 +0300 Subject: [PATCH 2/2] Sync with the upstream review: `ifTestable` keeps its answers WEKAPP-683364 Brings in the review fixes of dlang/phobos#11119 and #11143. `ifTestable` on a struct or union: when `pred` does not take a `T`, try an rvalue of each other qualifier of `T`, since the unique literal `T.init` converted to all of them. A struct with indirections and a `const` or `immutable` predicate parameter is testable again, as before this change. Tests for `isIterable`/`ForeachType` (`ref @safe` opApply, non-copyable range), the range traits without `@property`, a member named `init`, an enum based on an array, `each` with a `ref` parameter on a field `front`, `Chunks` over an infinite range in `@safe pure` code, and qualified `isTuple`. Comments reworded. --- std/algorithm/iteration.d | 17 +++++++++ std/range/package.d | 12 ++++++ std/range/primitives.d | 80 ++++++++++++++++++++++++++++++++++++--- std/traits.d | 50 +++++++++++++++++++++--- std/typecons.d | 2 + 5 files changed, 149 insertions(+), 12 deletions(-) diff --git a/std/algorithm/iteration.d b/std/algorithm/iteration.d index 359e6b6c911..110bcb97b43 100644 --- a/std/algorithm/iteration.d +++ b/std/algorithm/iteration.d @@ -1136,6 +1136,23 @@ public: assert(b == [ 3, 4, 5 ]); } +// A `ref` parameter binds to a forward range's `front` when `front` is an lvalue +@safe unittest +{ + static struct Fwd + { + int front; + int left = 3; + bool empty() const { return left == 0; } + void popFront() { --left; } + Fwd save() { return this; } + } + + int[] seen; + Fwd(1).each!((ref e) { seen ~= e; e *= 2; }); + assert(seen == [1, 2, 4]); +} + // https://issues.dlang.org/show_bug.cgi?id=15358 // application of `each` with >2 args (opApply) @system unittest diff --git a/std/range/package.d b/std/range/package.d index 9f530418a85..dc24435448d 100644 --- a/std/range/package.d +++ b/std/range/package.d @@ -9035,6 +9035,18 @@ if (isInputRange!Source) assert(equal!`equal(a, b)`(oddsByPairs[3 .. $].take(2), [[13, 15], [17, 19]])); } +// Over an infinite sliceable source, `Chunks.opSlice` returns `typeof(takeExactly(this, 0))`, +// so `isInputRange!Chunks` is evaluated while `Chunks` is still being analysed. That must not +// leave `front` and `popFront` inferred `@system` or impure. +@safe pure unittest +{ + import std.algorithm.comparison : equal; + + auto r = sequence!"a[0] + n * a[1]"(1, 2).chunks(2); + r.popFront(); + assert(r.front.equal([5, 7])); +} + /** diff --git a/std/range/primitives.d b/std/range/primitives.d index 880054b0641..4792c83d687 100644 --- a/std/range/primitives.d +++ b/std/range/primitives.d @@ -172,9 +172,9 @@ Params: Returns: `true` if R is an input range (possibly with element type `E`), `false` if not */ -// `R.sizeof` lays out a struct before the lambdas below infer its member functions' attributes: a range -// trait evaluated inside the struct's own declaration (a member's return type, like `Chunks.opSlice`) would -// otherwise infer those members impure and `@system`. +// `R.sizeof` lays out a struct before the lambdas below infer its member functions' attributes, +// as `R.init` did: a range trait evaluated inside the struct's own declaration (in a member's +// return type, like `Chunks.opSlice`) would otherwise infer those members impure and `@system`. enum bool isInputRange(R) = is(typeof(lvalueOf!R) == R) && is(typeof(R.sizeof)) && is(typeof((R r) { return r.empty; } (rvalueOf!R)) == bool) @@ -286,6 +286,26 @@ enum bool isInputRange(R, E) = assert(r.equal([ 0, 1, 2 ])); } +// A range type with a member named `init`, here from a mixin template, hides the built-in `.init` +@safe unittest +{ + mixin template SetUp() + { + void init(int v) { i = v; } + } + static struct R + { + int i; + bool empty() const { return i >= 3; } + int front() const { return i; } + void popFront() { ++i; } + R save() { return this; } + mixin SetUp; + } + static assert(isInputRange!R); + static assert(isForwardRange!R); +} + /+ puts the whole raw element `e` into `r`. doPut will not attempt to iterate, slice or transcode `e` in any way shape or form. It will $(B only) @@ -1366,9 +1386,10 @@ enum bool hasMobileElements(R) = static assert( hasMobileElements!(dchar[])); } -// `r.front` / `r.back` in a return statement, so a member function is called whether or not it is a `@property` -// (in a bare `typeof(r.front)` it is not), a field or a `ref` result is returned by reference (no copy, so a -// non-copyable element type is answered too), and no `.init` of the element type is ever built. +// `r.front` / `r.back` in a return statement, so a member function is called whether or not +// it is a `@property` (in a bare `typeof(r.front)` it is not), a field or a `ref` result is +// returned by reference (no copy, so a non-copyable element type is answered too), and no +// `.init` of the element type is built. private auto ref frontOf(R)(return ref R r) { return r.front; } private auto ref backOf(R)(return ref R r) { return r.back; } @@ -1462,6 +1483,53 @@ template ElementType(R) static assert(is(ElementType!R == E)); } +// The range traits also work when no primitive is a `@property` +@safe unittest +{ + static struct R + { + size_t i; + bool empty() const { return i >= 3; } + int front() const { return cast(int) i; } + void popFront() { ++i; } + R save() { return this; } + int back() const { return cast(int) i; } + void popBack() { ++i; } + size_t length() const { return 3 - i; } + R opSlice(size_t, size_t) { return this; } + } + static assert(isInputRange!R); + static assert(isForwardRange!R); + static assert(isBidirectionalRange!R); + static assert(hasSlicing!R); + static assert(is(ElementType!R == int)); +} + +// An element type with a member named `init`, here from a mixin template, hides the built-in `.init` +@safe unittest +{ + mixin template SetUp() + { + void init(int v) { x = v; } + } + static struct S + { + int x; + mixin SetUp; + } + static assert(is(ElementType!(S[]) == S)); + static assert(isRandomAccessRange!(S[])); +} + +// A variable of an enum type whose base type is a non-string array has no `front` +@safe unittest +{ + enum E : int[] { a = [1, 2] } + E e = E.a; + static assert(!__traits(compiles, e.front)); + static assert(is(ElementType!E == void)); +} + /** The encoding element type of `R`. For narrow strings (`char[]`, `wchar[]` and their qualified variants including `string` and diff --git a/std/traits.d b/std/traits.d index a4181cf4dd6..b01c246bae2 100644 --- a/std/traits.d +++ b/std/traits.d @@ -7295,9 +7295,9 @@ enum bool isAggregateType(T) = is(T == struct) || is(T == union) || * that define `opApply` with a single loop variable, and builtin dynamic, * static and associative arrays. */ -// An lvalue serves opApply (a null dereference: an opApply taking a `ref` `@safe` delegate -// refuses `lvalueOf!T`, and every opApply refuses `rvalueOf!T`); an rvalue serves a -// non-copyable range, which foreach would otherwise copy. +// `*p` is for an opApply taking a `@safe` delegate: in a function literal whose attributes are +// inferred, a call to the bodyless `lvalueOf!T` or `rvalueOf!T` makes the loop body's delegate +// `@system`. `rvalueOf!T` is for a non-copyable range, which `foreach` over `*p` would copy. enum bool isIterable(T) = is(typeof({ T* p = null; foreach (elem; *p) {} })) || is(typeof({ foreach (elem; rvalueOf!T) {} })); @@ -7324,6 +7324,28 @@ enum bool isIterable(T) = is(typeof({ T* p = null; foreach (elem; *p) {} })) static assert(!isIterable!uint); } +// `opApply` with a `ref` `@safe` delegate, and a range that cannot be copied +@safe unittest +{ + static struct SafeOpApply + { + int opApply(scope int delegate(ref int) @safe dg) @safe { int x; return dg(x); } + } + + static struct NoCopyRange + { + @disable this(this); + enum bool empty = false; + int front() { return 0; } + void popFront() {} + } + + static assert(isIterable!SafeOpApply); + static assert(isIterable!NoCopyRange); + static assert(is(ForeachType!SafeOpApply == int)); + static assert(is(ForeachType!NoCopyRange == int)); +} + /** * Returns true if T is not const or immutable. Note that isMutable is true for * string, or immutable(char)[], because the 'head' is mutable. @@ -8083,6 +8105,7 @@ has both opApply and a range interface. */ template ForeachType(T) { + // See `isIterable` for the two forms. static if (is(typeof({ T* p = null; foreach (elem; *p) {} }))) { alias ForeachType = typeof( @@ -9088,10 +9111,18 @@ enum bool allSameType(Ts...) = */ template ifTestable(T, alias pred = a => a) { - // See `isAggregateOperand`: a scalar's `.init` converts by value range (`int` to `ubyte`) and a - // null literal to any qualifier, which a function result does not. static if (isAggregateOperand!T) - enum ifTestable = __traits(compiles, { if (pred(rvalueOf!T)) {} }); + { + // See `CommonType`: only a struct or union operand avoids `.init`. The literal `T.init` + // is unique, so where `pred` does not take a `T` it converted to any other qualifier of + // `T`; an rvalue of each stands in. + enum testable(U) = __traits(compiles, { if (pred(rvalueOf!U)) {} }); + alias U = Unqual!T; + static if (__traits(compiles, pred(rvalueOf!T))) + enum ifTestable = testable!T; + else + enum ifTestable = anySatisfy!(testable, U, const U, immutable U, shared U, shared const U); + } else enum ifTestable = __traits(compiles, { if (pred(T.init)) {} }); } @@ -9132,6 +9163,13 @@ template ifTestable(T, alias pred = a => a) static assert( ifTestable!(int*, (immutable(int)* p) => p)); static assert( ifTestable!(int[], (immutable(int)[] a) => a)); static assert( ifTestable!(C, (immutable C c) => c)); + + struct P + { + int* p; + bool opCast(T : bool)() const { return p !is null; } + } + static assert( ifTestable!(P, (immutable P a) => a)); } /** diff --git a/std/typecons.d b/std/typecons.d index c653b73263e..5d80faf957f 100644 --- a/std/typecons.d +++ b/std/typecons.d @@ -2297,6 +2297,8 @@ enum isTuple(T) = is(Unqual!T == Tuple!Specs, Specs...) || { static assert(isTuple!(const Tuple!(int))); static assert(isTuple!(immutable Tuple!(int))); + static assert(isTuple!(const Tuple!(int[], int))); + static assert(isTuple!(immutable Tuple!(int*))); static assert(!isTuple!(int)); static assert(!isTuple!(const int));