From b0528ec384418840f42258d12f244d0a5ad6bfca Mon Sep 17 00:00:00 2001 From: Quirin Schroll Date: Wed, 26 Jun 2024 18:30:34 +0200 Subject: [PATCH] Fix Bugzilla Issues 23666, 17953, and 24633 --- spec/statement.dd | 250 +++++++++++++++++++++++++++++++++------------- 1 file changed, 182 insertions(+), 68 deletions(-) diff --git a/spec/statement.dd b/spec/statement.dd index 0e2d106a0c..5e1a9a6426 100644 --- a/spec/statement.dd +++ b/spec/statement.dd @@ -698,32 +698,25 @@ foreach (string s, double d; aa) $(H3 $(LNAME2 foreach_over_struct_and_classes, Foreach over Structs and Classes with `opApply`)) - $(P If the aggregate expression is a struct or class object, - the $(D foreach) is defined by the - special $(LEGACY_LNAME2 opApply, op-apply, $(D opApply)) member function, and the - `foreach_reverse` behavior is defined by the special + $(P If the aggregate expression is a `struct` or `class` object, + iteration with $(D foreach) can be defined by implementing the + $(LEGACY_LNAME2 opApply, op-apply, $(D opApply)) member function, and the + `foreach_reverse` behavior is defined by the $(LEGACY_LNAME2 opApplyReverse, op-apply-reverse, $(D opApplyReverse)) member function. These functions must each have the signature below: ) $(GRAMMAR -$(GNAME OpApplyDeclaration): - `int opApply` `(` `scope` `int delegate` `(` $(I OpApplyParameters) `)` `dg` `)` `;` + `int opApply` `(` `scope` `int delegate` `(` $(I OpApplyParameters) `)` `body` `)` `;` $(GNAME OpApplyParameters): - *OpApplyParameter* - *OpApplyParameter*, *OpApplyParameters* + $(GLINK ParameterDeclaration) + $(GLINK ParameterDeclaration), *OpApplyParameters* $(GNAME OpApplyParameter): - $(GLINK ForeachTypeAttributes)$(OPT) $(GLINK2 type, BasicType) $(GLINK2 declaration, Declarator) + $(GLINK ParameterStorageClass)$(OPT) $(GLINK2 type, BasicType) $(GLINK2 declaration, Declarator) ) - $(P where each $(I OpApplyParameter) of `dg` must match a $(GLINK ForeachType) - in a $(GLINK ForeachStatement), - otherwise the *ForeachStatement* will cause an error.) - - $(P Any *ForeachTypeAttribute* cannot be `enum`.) - $(PANEL To support a `ref` iteration variable, the delegate must take a `ref` parameter: @@ -731,43 +724,62 @@ $(GNAME OpApplyParameter): --- struct S { - int opApply(scope int delegate(ref uint n) dg); + uint n; + int opApply(scope int delegate(ref uint) body) => body(n); } - void f(S s) + void main() { + S s; foreach (ref uint i; s) - i++; + { + i++; // effectively s.n++ + } + foreach (i; s) + { + static assert(is(typeof(i) == uint)); + assert(i == 1); + } } --- ) - Above, `opApply` is still matched when `i` is not `ref`, so by using - a `ref` delegate parameter both forms are supported. - ) + Above, `opApply` is matched both when `i` is and is not `ref`, so by using + a `ref` delegate parameter, both forms are supported. - $(P There can be multiple $(D opApply) and $(D opApplyReverse) functions - - one is selected - by matching each parameter of `dg` to each $(I ForeachType) - declared in the $(I ForeachStatement).) + Stating the type of the variable like in the first loop is optional, + as is showcased in the second loop, where the type is inferred. + ) - $(P The body of the apply - function iterates over the elements it aggregates, passing each one - in successive calls to the `dg` delegate. The delegate return value - determines whether to interrupt iteration:) + $(P The apply + function iterates over the elements it aggregates by passing each one + in successive calls to the `body` delegate. The delegate return value + of the `body` delegate + determines how to proceed iteration:) $(UL - $(LI If the result is nonzero, apply must cease + $(LI If the result is nonzero, the apply function must cease iterating and return that value.) - $(LI If the result is 0, then iteration should continue. + $(LI If the result is `0`, then iteration should continue. If there are no more elements to iterate, - apply must return 0.) + the apply function must return `0`.) ) - $(P The result of calling the delegate will be nonzero if the *ForeachStatement* + $(P The result of the `body` delegate will be nonzero if the *ForeachStatement* body executes a matching $(GLINK BreakStatement), $(GLINK ReturnStatement), or $(GLINK GotoStatement) whose matching label is outside the *ForeachStatement*. ) - $(P For example, consider a class that is a container for two elements:) + $(P The $(D opApply) and $(D opApplyReverse) member functions can be overloaded. + Selection works similar to overload resolution by + comparing the number of `foreach` variables and number of parameters of `body` and, + if more than one overload remains unique overload, + matching the parameter types of `body` and each $(I ForeachType) + declared in the $(I ForeachStatement) when given.) + + $(BEST_PRACTICE Overload apply functions only with delegates differing in number of parameters + to enable type inference for `foreach` variables. + ) + + $(P For example, consider a class that is a container for some elements:) $(SPEC_RUNNABLE_EXAMPLE_RUN -------------- @@ -775,11 +787,11 @@ $(GNAME OpApplyParameter): { uint[] array; - int opApply(scope int delegate(ref uint) dg) + int opApply(scope int delegate(ref uint) body) { foreach (e; array) { - int result = dg(e); + int result = body(e); if (result) return result; } @@ -811,57 +823,159 @@ $(CONSOLE 73 82 ) - $(PANEL - The `scope` storage class on the $(D dg) parameter means that the delegate does - not escape the scope of the $(D opApply) function (an example would be assigning $(D dg) to a - global variable). If it cannot be statically guaranteed that $(D dg) does not escape, a closure may - be allocated for it on the heap instead of the stack. $(BEST_PRACTICE Annotate delegate parameters to `opApply` functions with `scope` when possible.) + + $(PANEL + The `scope` storage class on the `body` parameter means that the delegate does + not escape the scope of the apply function (an example would be assigning`body` to a + global variable). If it cannot be statically guaranteed that `body` does not escape, a closure may + be allocated for it on the heap instead of the stack. ) - $(P $(B Important:) If $(D opApply) catches any exceptions, ensure that those - exceptions did not originate from the delegate passed to $(D opApply). The user would expect + $(P $(B Important:) If apply functions catch any exceptions, ensure that those + exceptions did not originate from the delegate. The user would expect exceptions thrown from a `foreach` body to both terminate the loop, and propagate outside the `foreach` body. ) $(H4 $(LNAME2 template-op-apply, Template `opApply`)) - $(P $(D opApply) can also be a templated function, - which will infer the types of parameters based on the $(I ForeachStatement). - For example:) + $(P `opApply` and `opApplyReverse` can also be a function templates, + which can optionally infer the types of parameters based on the $(I ForeachStatement). + ) + + $(P $(B Note:) An apply function template cannot infer `foreach` variable types.) $(SPEC_RUNNABLE_EXAMPLE_RUN -------------- struct S { - import std.traits : ParameterTypeTuple; // introspection template - import std.stdio; - - int opApply(Dg)(scope Dg dg) - if (ParameterTypeTuple!Dg.length == 2) // foreach with 2 parameters + int opApply(Body)(scope Body body) { - writeln(2); - return 0; - } - - int opApply(Dg)(scope Dg dg) - if (ParameterTypeTuple!Dg.length == 3) // foreach with 3 parameters - { - writeln(3); + pragma(msg, Body); return 0; } } void main() { - foreach (int a, int b; S()) { } // calls first opApply function - foreach (int a, int b, float c; S()) { } // calls second opApply function + foreach (int a, int b; S()) { } // int delegate(ref int, ref int) pure nothrow @nogc @safe + foreach (bool b, string s; S()) { } // int delegate(ref bool, ref string) pure nothrow @nogc @safe + //foreach (x; S()) { } // Error: cannot infer type for `foreach` variable `x`, perhaps set it explicitly } -------------- ) +$(H4 $(LNAME2 template-instance-op-apply, `opApply` as an alias to a explicit function template instance)) + + $(P `opApply` and `opApplyReverse` can be aliases to an appropriate function or function template. + However, special treatment is given to apply functions that are aliases of an appropriate function template instance.) + + $(P In that case, the function template instance is used for overload selection and `foreach` variable type inference, + but the template is instantiated again with the actual delegate type of the `foreach` body. + This way, apply functions can infer attributes depending on the attributes of `body` delegate.) + + $(SPEC_RUNNABLE_EXAMPLE_RUN + -------------- + struct A + { + int opApply(scope int delegate(long) body) => body(42); + } + struct B + { + int opApply(Body)(scope Body body) => body(42); + } + struct C + { + int opApplyImpl(Body)(scope Body body) => body(42); + alias opApply = opApplyImpl!(int delegate(long)); + } + void main() @nogc nothrow pure @safe + { + // Error: `@nogc` function `D main` cannot call non-@nogc function `onlineapp.A.opApply` + // Error: `pure` function `D main` cannot call impure function `onlineapp.A.opApply` + // Error: `@safe` function `D main` cannot call `@system` function `onlineapp.A.opApply` + // Error: function `onlineapp.A.opApply` is not `nothrow` + static assert(!__traits(compiles, () @safe { + foreach (x; A()) { } + })); + + // Error: cannot infer type for `foreach` variable `x`, perhaps set it explicitly + static assert(!__traits(compiles, { + foreach (x; B()) { } + })); + + // Good: + foreach (x; C()) + { + static assert(is(typeof(x) == long)); + assert(x == 42); + } + } + -------------- + ) + + $(P The `opApplyImpl` pattern is generally preferable to + overloading many apply functions with all possible combinations of attributes.) + + $(P Multiple apply function aliases can exist, and selection and `foreach` variable type inference work:) + + -------------- + class Tree(T) + { + private T label; + private Tree[] children; + + this(T label, Tree[] children = null) + { + this.label = label; + this.children = children; + } + + alias opApply = opApplyImpl!(int delegate(ref T label)); + alias opApply = opApplyImpl!(int delegate(size_t depth, ref T label)); + alias opApply = opApplyImpl!(int delegate(size_t depth, bool isLastChild, ref T label)); + + int opApplyImpl(Body)(scope Body body) => opApplyImpl2(0, true, body); + int opApplyImpl2(Body)(size_t depth, bool lastChild, scope Body body) + { + import std.meta : AliasSeq; + static if (is(Body : int delegate(size_t, bool, ref T))) + alias args = AliasSeq!(depth, lastChild, label); + else static if (is(Body : int delegate(size_t, ref T))) + alias args = AliasSeq!(depth, label); + else + alias args = label; + if (auto result = body(args)) return result; + foreach (i, child; children) + { + if (auto result = child.opApplyImpl2!Body(depth + 1, i + 1 == children.length, body)) + return result; + } + return 0; + } + } + + void printTree(Tree)(Tree tree) + { + // Selects the unique one with 2 parameters. + // Infers types: size_t and whatever label type the tree has. + foreach (depth, ref label; tree) + { + import std.stdio; + foreach (_; 0 .. depth) write(" "); + writeln(label); + } + } + + void main() @safe + { + alias T = Tree!int; + printTree(new T(0, [new T(1, [new T(2), new T(3)]), new T(4, [new T(5)])])); + } + -------------- + $(H3 $(LEGACY_LNAME2 foreach_with_ranges, foreach-with-ranges, Foreach over Structs and Classes with Ranges)) $(P If the aggregate expression is a struct or class object, but the @@ -1028,16 +1142,16 @@ $(H3 $(LNAME2 foreach_over_delegates, Foreach over Delegates)) $(SPEC_RUNNABLE_EXAMPLE_RUN -------------- // Custom loop implementation, that iterates over powers of 2 with - // alternating sign. The foreach loop body is passed in dg. - int myLoop(scope int delegate(int) dg) + // alternating sign. The foreach loop body is passed in `body`. + int myLoop(scope int delegate(int) body) { for (int z = 1; z < 128; z *= -2) { - auto ret = dg(z); + auto result = body(z); - // If the loop body contains a break, ret will be non-zero. - if (ret != 0) - return ret; + // If the loop body contains a break, result will be non-zero. + if (result != 0) + return result; } return 0; } @@ -1583,7 +1697,7 @@ foreach (item; list) $(P Any intervening $(RELATIVE_LINK2 try-statement, `finally`) clauses are executed, and any intervening synchronization objects are released.) - $(P $(D Note:) If a `finally` clause executes a `throw` out of the finally + $(P $(B Note:) If a `finally` clause executes a `throw` out of the finally clause, the continue target is never reached.) $(H2 $(LEGACY_LNAME2 BreakStatement, break-statement, Break Statement))