| /** |
| * Find side-effects of expressions. |
| * |
| * Copyright: Copyright (C) 1999-2022 by The D Language Foundation, All Rights Reserved |
| * Authors: $(LINK2 https://www.digitalmars.com, Walter Bright) |
| * License: $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) |
| * Source: $(LINK2 https://github.com/dlang/dmd/blob/master/src/dmd/sideeffect.d, _sideeffect.d) |
| * Documentation: https://dlang.org/phobos/dmd_sideeffect.html |
| * Coverage: https://codecov.io/gh/dlang/dmd/src/master/src/dmd/sideeffect.d |
| */ |
| |
| module dmd.sideeffect; |
| |
| import dmd.apply; |
| import dmd.astenums; |
| import dmd.declaration; |
| import dmd.dscope; |
| import dmd.expression; |
| import dmd.expressionsem; |
| import dmd.func; |
| import dmd.globals; |
| import dmd.identifier; |
| import dmd.init; |
| import dmd.mtype; |
| import dmd.tokens; |
| import dmd.visitor; |
| |
| /************************************************** |
| * Front-end expression rewriting should create temporary variables for |
| * non trivial sub-expressions in order to: |
| * 1. save evaluation order |
| * 2. prevent sharing of sub-expression in AST |
| */ |
| extern (C++) bool isTrivialExp(Expression e) |
| { |
| extern (C++) final class IsTrivialExp : StoppableVisitor |
| { |
| alias visit = typeof(super).visit; |
| public: |
| extern (D) this() |
| { |
| } |
| |
| override void visit(Expression e) |
| { |
| /* https://issues.dlang.org/show_bug.cgi?id=11201 |
| * CallExp is always non trivial expression, |
| * especially for inlining. |
| */ |
| if (e.op == EXP.call) |
| { |
| stop = true; |
| return; |
| } |
| // stop walking if we determine this expression has side effects |
| stop = lambdaHasSideEffect(e); |
| } |
| } |
| |
| scope IsTrivialExp v = new IsTrivialExp(); |
| return walkPostorder(e, v) == false; |
| } |
| |
| /******************************************** |
| * Determine if Expression has any side effects. |
| * |
| * Params: |
| * e = the expression |
| * assumeImpureCalls = whether function calls should always be assumed to |
| * be impure (e.g. debug is allowed to violate purity) |
| */ |
| extern (C++) bool hasSideEffect(Expression e, bool assumeImpureCalls = false) |
| { |
| extern (C++) final class LambdaHasSideEffect : StoppableVisitor |
| { |
| alias visit = typeof(super).visit; |
| public: |
| extern (D) this() |
| { |
| } |
| |
| override void visit(Expression e) |
| { |
| // stop walking if we determine this expression has side effects |
| stop = lambdaHasSideEffect(e, assumeImpureCalls); |
| } |
| } |
| |
| scope LambdaHasSideEffect v = new LambdaHasSideEffect(); |
| return walkPostorder(e, v); |
| } |
| |
| /******************************************** |
| * Determine if the call of f, or function type or delegate type t1, has any side effects. |
| * Returns: |
| * 0 has any side effects |
| * 1 nothrow + strongly pure |
| * 2 nothrow + strongly pure + only immutable indirections in the return |
| * type |
| */ |
| int callSideEffectLevel(FuncDeclaration f) |
| { |
| /* https://issues.dlang.org/show_bug.cgi?id=12760 |
| * ctor call always has side effects. |
| */ |
| if (f.isCtorDeclaration()) |
| return 0; |
| assert(f.type.ty == Tfunction); |
| TypeFunction tf = cast(TypeFunction)f.type; |
| if (!tf.isnothrow) |
| return 0; |
| final switch (f.isPure()) |
| { |
| case PURE.impure: |
| case PURE.fwdref: |
| case PURE.weak: |
| return 0; |
| |
| case PURE.const_: |
| return mutabilityOfType(tf.isref, tf.next) == 2 ? 2 : 1; |
| } |
| } |
| |
| int callSideEffectLevel(Type t) |
| { |
| t = t.toBasetype(); |
| TypeFunction tf; |
| if (t.ty == Tdelegate) |
| tf = cast(TypeFunction)(cast(TypeDelegate)t).next; |
| else |
| { |
| assert(t.ty == Tfunction); |
| tf = cast(TypeFunction)t; |
| } |
| if (!tf.isnothrow) // function can throw |
| return 0; |
| |
| tf.purityLevel(); |
| PURE purity = tf.purity; |
| if (t.ty == Tdelegate && purity > PURE.weak) |
| { |
| if (tf.isMutable()) |
| purity = PURE.weak; |
| else if (!tf.isImmutable()) |
| purity = PURE.const_; |
| } |
| |
| if (purity == PURE.const_) |
| return mutabilityOfType(tf.isref, tf.next) == 2 ? 2 : 1; |
| |
| return 0; |
| } |
| |
| private bool lambdaHasSideEffect(Expression e, bool assumeImpureCalls = false) |
| { |
| switch (e.op) |
| { |
| // Sort the cases by most frequently used first |
| case EXP.assign: |
| case EXP.plusPlus: |
| case EXP.minusMinus: |
| case EXP.declaration: |
| case EXP.construct: |
| case EXP.blit: |
| case EXP.addAssign: |
| case EXP.minAssign: |
| case EXP.concatenateAssign: |
| case EXP.concatenateElemAssign: |
| case EXP.concatenateDcharAssign: |
| case EXP.mulAssign: |
| case EXP.divAssign: |
| case EXP.modAssign: |
| case EXP.leftShiftAssign: |
| case EXP.rightShiftAssign: |
| case EXP.unsignedRightShiftAssign: |
| case EXP.andAssign: |
| case EXP.orAssign: |
| case EXP.xorAssign: |
| case EXP.powAssign: |
| case EXP.in_: |
| case EXP.remove: |
| case EXP.assert_: |
| case EXP.halt: |
| case EXP.throw_: |
| case EXP.delete_: |
| case EXP.new_: |
| case EXP.newAnonymousClass: |
| return true; |
| case EXP.call: |
| { |
| if (assumeImpureCalls) |
| return true; |
| |
| if (e.type && e.type.ty == Tnoreturn) |
| return true; |
| |
| CallExp ce = cast(CallExp)e; |
| /* Calling a function or delegate that is pure nothrow |
| * has no side effects. |
| */ |
| if (ce.e1.type) |
| { |
| Type t = ce.e1.type.toBasetype(); |
| if (t.ty == Tdelegate) |
| t = (cast(TypeDelegate)t).next; |
| |
| const level = t.ty == Tfunction && (ce.f ? callSideEffectLevel(ce.f) : callSideEffectLevel(ce.e1.type)); |
| if (level == 0) // 0 means the function has a side effect |
| return true; |
| } |
| break; |
| } |
| case EXP.cast_: |
| { |
| CastExp ce = cast(CastExp)e; |
| /* if: |
| * cast(classtype)func() // because it may throw |
| */ |
| if (ce.to.ty == Tclass && ce.e1.op == EXP.call && ce.e1.type.ty == Tclass) |
| return true; |
| break; |
| } |
| default: |
| break; |
| } |
| return false; |
| } |
| |
| /*********************************** |
| * The result of this expression will be discarded. |
| * Print error messages if the operation has no side effects (and hence is meaningless). |
| * Returns: |
| * true if expression has no side effects |
| */ |
| bool discardValue(Expression e) |
| { |
| if (lambdaHasSideEffect(e)) // check side-effect shallowly |
| return false; |
| switch (e.op) |
| { |
| case EXP.cast_: |
| { |
| CastExp ce = cast(CastExp)e; |
| if (ce.to.equals(Type.tvoid)) |
| { |
| /* |
| * Don't complain about an expression with no effect if it was cast to void |
| */ |
| return false; |
| } |
| break; // complain |
| } |
| // Assumption that error => no side effect |
| case EXP.error: |
| return true; |
| case EXP.variable: |
| { |
| VarDeclaration v = (cast(VarExp)e).var.isVarDeclaration(); |
| if (v && (v.storage_class & STC.temp)) |
| { |
| // https://issues.dlang.org/show_bug.cgi?id=5810 |
| // Don't complain about an internal generated variable. |
| return false; |
| } |
| break; |
| } |
| case EXP.call: |
| /* Issue 3882: */ |
| if (global.params.warnings != DiagnosticReporting.off && !global.gag) |
| { |
| CallExp ce = cast(CallExp)e; |
| if (e.type.ty == Tvoid) |
| { |
| /* Don't complain about calling void-returning functions with no side-effect, |
| * because purity and nothrow are inferred, and because some of the |
| * runtime library depends on it. Needs more investigation. |
| * |
| * One possible solution is to restrict this message to only be called in hierarchies that |
| * never call assert (and or not called from inside unittest blocks) |
| */ |
| } |
| else if (ce.e1.type) |
| { |
| Type t = ce.e1.type.toBasetype(); |
| if (t.ty == Tdelegate) |
| t = (cast(TypeDelegate)t).next; |
| if (t.ty == Tfunction && (ce.f ? callSideEffectLevel(ce.f) : callSideEffectLevel(ce.e1.type)) > 0) |
| { |
| const(char)* s; |
| if (ce.f) |
| s = ce.f.toPrettyChars(); |
| else if (ce.e1.op == EXP.star) |
| { |
| // print 'fp' if ce.e1 is (*fp) |
| s = (cast(PtrExp)ce.e1).e1.toChars(); |
| } |
| else |
| s = ce.e1.toChars(); |
| e.warning("calling `%s` without side effects discards return value of type `%s`; prepend a `cast(void)` if intentional", s, e.type.toChars()); |
| } |
| } |
| } |
| return false; |
| case EXP.andAnd: |
| case EXP.orOr: |
| { |
| LogicalExp aae = cast(LogicalExp)e; |
| return discardValue(aae.e2); |
| } |
| case EXP.question: |
| { |
| CondExp ce = cast(CondExp)e; |
| /* https://issues.dlang.org/show_bug.cgi?id=6178 |
| * https://issues.dlang.org/show_bug.cgi?id=14089 |
| * Either CondExp::e1 or e2 may have |
| * redundant expression to make those types common. For example: |
| * |
| * struct S { this(int n); int v; alias v this; } |
| * S[int] aa; |
| * aa[1] = 0; |
| * |
| * The last assignment statement will be rewitten to: |
| * |
| * 1 in aa ? aa[1].value = 0 : (aa[1] = 0, aa[1].this(0)).value; |
| * |
| * The last DotVarExp is necessary to take assigned value. |
| * |
| * int value = (aa[1] = 0); // value = aa[1].value |
| * |
| * To avoid false error, discardValue() should be called only when |
| * the both tops of e1 and e2 have actually no side effects. |
| */ |
| if (!lambdaHasSideEffect(ce.e1) && !lambdaHasSideEffect(ce.e2)) |
| { |
| return discardValue(ce.e1) | |
| discardValue(ce.e2); |
| } |
| return false; |
| } |
| case EXP.comma: |
| { |
| CommaExp ce = cast(CommaExp)e; |
| // Don't complain about compiler-generated comma expressions |
| if (ce.isGenerated) |
| return false; |
| |
| // Don't check e1 until we cast(void) the a,b code generation. |
| // This is concretely done in expressionSemantic, if a CommaExp has Tvoid as type |
| return discardValue(ce.e2); |
| } |
| case EXP.tuple: |
| /* Pass without complaint if any of the tuple elements have side effects. |
| * Ideally any tuple elements with no side effects should raise an error, |
| * this needs more investigation as to what is the right thing to do. |
| */ |
| if (!hasSideEffect(e)) |
| break; |
| return false; |
| case EXP.identity, EXP.notIdentity: |
| case EXP.equal, EXP.notEqual: |
| /* |
| `[side effect] == 0` |
| Technically has a side effect but is clearly wrong; |
| */ |
| BinExp tmp = e.isBinExp(); |
| assert(tmp); |
| |
| e.error("the result of the equality expression `%s` is discarded", e.toChars()); |
| bool seenSideEffect = false; |
| foreach(expr; [tmp.e1, tmp.e2]) |
| { |
| if (hasSideEffect(expr)) { |
| expr.errorSupplemental("note that `%s` may have a side effect", expr.toChars()); |
| seenSideEffect |= true; |
| } |
| } |
| return !seenSideEffect; |
| default: |
| break; |
| } |
| e.error("`%s` has no effect", e.toChars()); |
| return true; |
| } |
| |
| /************************************************** |
| * Build a temporary variable to copy the value of e into. |
| * Params: |
| * stc = storage classes will be added to the made temporary variable |
| * name = name for temporary variable |
| * e = original expression |
| * Returns: |
| * Newly created temporary variable. |
| */ |
| VarDeclaration copyToTemp(StorageClass stc, const char[] name, Expression e) |
| { |
| assert(name[0] == '_' && name[1] == '_'); |
| auto vd = new VarDeclaration(e.loc, e.type, |
| Identifier.generateId(name), |
| new ExpInitializer(e.loc, e)); |
| vd.storage_class = stc | STC.temp | STC.ctfe; // temporary is always CTFEable |
| return vd; |
| } |
| |
| /************************************************** |
| * Build a temporary variable to extract e's evaluation, if e is not trivial. |
| * Params: |
| * sc = scope |
| * name = name for temporary variable |
| * e0 = a new side effect part will be appended to it. |
| * e = original expression |
| * alwaysCopy = if true, build new temporary variable even if e is trivial. |
| * Returns: |
| * When e is trivial and alwaysCopy == false, e itself is returned. |
| * Otherwise, a new VarExp is returned. |
| * Note: |
| * e's lvalue-ness will be handled well by STC.ref_ or STC.rvalue. |
| */ |
| Expression extractSideEffect(Scope* sc, const char[] name, |
| ref Expression e0, Expression e, bool alwaysCopy = false) |
| { |
| //printf("extractSideEffect(e: %s)\n", e.toChars()); |
| |
| /* The trouble here is that if CTFE is running, extracting the side effect |
| * results in an assignment, and then the interpreter says it cannot evaluate the |
| * side effect assignment variable. But we don't have to worry about side |
| * effects in function calls anyway, because then they won't CTFE. |
| * https://issues.dlang.org/show_bug.cgi?id=17145 |
| */ |
| if (!alwaysCopy && |
| ((sc.flags & SCOPE.ctfe) ? !hasSideEffect(e) : isTrivialExp(e))) |
| return e; |
| |
| auto vd = copyToTemp(0, name, e); |
| vd.storage_class |= e.isLvalue() ? STC.ref_ : STC.rvalue; |
| |
| e0 = Expression.combine(e0, new DeclarationExp(vd.loc, vd) |
| .expressionSemantic(sc)); |
| |
| return new VarExp(vd.loc, vd) |
| .expressionSemantic(sc); |
| } |