| /** |
| * Find out in what ways control flow can exit a statement block. |
| * |
| * 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/blockexit.d, _blockexit.d) |
| * Documentation: https://dlang.org/phobos/dmd_blockexit.html |
| * Coverage: https://codecov.io/gh/dlang/dmd/src/master/src/dmd/blockexit.d |
| */ |
| |
| module dmd.blockexit; |
| |
| import core.stdc.stdio; |
| |
| import dmd.arraytypes; |
| import dmd.astenums; |
| import dmd.canthrow; |
| import dmd.dclass; |
| import dmd.declaration; |
| import dmd.expression; |
| import dmd.func; |
| import dmd.globals; |
| import dmd.id; |
| import dmd.identifier; |
| import dmd.mtype; |
| import dmd.statement; |
| import dmd.tokens; |
| import dmd.visitor; |
| |
| /** |
| * BE stands for BlockExit. |
| * |
| * It indicates if a statement does transfer control to another block. |
| * A block is a sequence of statements enclosed in { } |
| */ |
| enum BE : int |
| { |
| none = 0, |
| fallthru = 1, |
| throw_ = 2, |
| return_ = 4, |
| goto_ = 8, |
| halt = 0x10, |
| break_ = 0x20, |
| continue_ = 0x40, |
| errthrow = 0x80, |
| any = (fallthru | throw_ | return_ | goto_ | halt), |
| } |
| |
| |
| /********************************************* |
| * Determine mask of ways that a statement can exit. |
| * |
| * Only valid after semantic analysis. |
| * Params: |
| * s = statement to check for block exit status |
| * func = function that statement s is in |
| * mustNotThrow = generate an error if it throws |
| * Returns: |
| * BE.xxxx |
| */ |
| int blockExit(Statement s, FuncDeclaration func, bool mustNotThrow) |
| { |
| extern (C++) final class BlockExit : Visitor |
| { |
| alias visit = Visitor.visit; |
| public: |
| FuncDeclaration func; |
| bool mustNotThrow; |
| int result; |
| |
| extern (D) this(FuncDeclaration func, bool mustNotThrow) |
| { |
| this.func = func; |
| this.mustNotThrow = mustNotThrow; |
| result = BE.none; |
| } |
| |
| override void visit(Statement s) |
| { |
| printf("Statement::blockExit(%p)\n", s); |
| printf("%s\n", s.toChars()); |
| assert(0); |
| } |
| |
| override void visit(ErrorStatement s) |
| { |
| result = BE.none; |
| } |
| |
| override void visit(ExpStatement s) |
| { |
| result = BE.fallthru; |
| if (s.exp) |
| { |
| if (s.exp.op == EXP.halt) |
| { |
| result = BE.halt; |
| return; |
| } |
| if (AssertExp a = s.exp.isAssertExp()) |
| { |
| if (a.e1.toBool().hasValue(false)) // if it's an assert(0) |
| { |
| result = BE.halt; |
| return; |
| } |
| } |
| if (s.exp.type && s.exp.type.toBasetype().isTypeNoreturn()) |
| result = BE.halt; |
| |
| result |= canThrow(s.exp, func, mustNotThrow); |
| } |
| } |
| |
| override void visit(CompileStatement s) |
| { |
| assert(global.errors); |
| result = BE.fallthru; |
| } |
| |
| override void visit(CompoundStatement cs) |
| { |
| //printf("CompoundStatement.blockExit(%p) %d result = x%X\n", cs, cs.statements.dim, result); |
| result = BE.fallthru; |
| Statement slast = null; |
| foreach (s; *cs.statements) |
| { |
| if (s) |
| { |
| //printf("result = x%x\n", result); |
| //printf("s: %s\n", s.toChars()); |
| if (result & BE.fallthru && slast) |
| { |
| slast = slast.last(); |
| if (slast && (slast.isCaseStatement() || slast.isDefaultStatement()) && (s.isCaseStatement() || s.isDefaultStatement())) |
| { |
| // Allow if last case/default was empty |
| CaseStatement sc = slast.isCaseStatement(); |
| DefaultStatement sd = slast.isDefaultStatement(); |
| auto sl = (sc ? sc.statement : (sd ? sd.statement : null)); |
| |
| if (sl && (!sl.hasCode() || sl.isErrorStatement())) |
| { |
| } |
| else if (func.getModule().filetype != FileType.c) |
| { |
| const(char)* gototype = s.isCaseStatement() ? "case" : "default"; |
| // @@@DEPRECATED_2.110@@@ https://issues.dlang.org/show_bug.cgi?id=22999 |
| // Deprecated in 2.100 |
| // Make an error in 2.110 |
| if (sl && sl.isCaseStatement()) |
| s.deprecation("switch case fallthrough - use 'goto %s;' if intended", gototype); |
| else |
| s.error("switch case fallthrough - use 'goto %s;' if intended", gototype); |
| } |
| } |
| } |
| |
| if (!(result & BE.fallthru) && !s.comeFrom()) |
| { |
| if (blockExit(s, func, mustNotThrow) != BE.halt && s.hasCode() && |
| s.loc != Loc.initial) // don't emit warning for generated code |
| s.warning("statement is not reachable"); |
| } |
| else |
| { |
| result &= ~BE.fallthru; |
| result |= blockExit(s, func, mustNotThrow); |
| } |
| slast = s; |
| } |
| } |
| } |
| |
| override void visit(UnrolledLoopStatement uls) |
| { |
| result = BE.fallthru; |
| foreach (s; *uls.statements) |
| { |
| if (s) |
| { |
| int r = blockExit(s, func, mustNotThrow); |
| result |= r & ~(BE.break_ | BE.continue_ | BE.fallthru); |
| if ((r & (BE.fallthru | BE.continue_ | BE.break_)) == 0) |
| result &= ~BE.fallthru; |
| } |
| } |
| } |
| |
| override void visit(ScopeStatement s) |
| { |
| //printf("ScopeStatement::blockExit(%p)\n", s.statement); |
| result = blockExit(s.statement, func, mustNotThrow); |
| } |
| |
| override void visit(WhileStatement s) |
| { |
| assert(global.errors); |
| result = BE.fallthru; |
| } |
| |
| override void visit(DoStatement s) |
| { |
| if (s._body) |
| { |
| result = blockExit(s._body, func, mustNotThrow); |
| if (result == BE.break_) |
| { |
| result = BE.fallthru; |
| return; |
| } |
| if (result & BE.continue_) |
| result |= BE.fallthru; |
| } |
| else |
| result = BE.fallthru; |
| if (result & BE.fallthru) |
| { |
| result |= canThrow(s.condition, func, mustNotThrow); |
| |
| if (!(result & BE.break_) && s.condition.toBool().hasValue(true)) |
| result &= ~BE.fallthru; |
| } |
| result &= ~(BE.break_ | BE.continue_); |
| } |
| |
| override void visit(ForStatement s) |
| { |
| result = BE.fallthru; |
| if (s._init) |
| { |
| result = blockExit(s._init, func, mustNotThrow); |
| if (!(result & BE.fallthru)) |
| return; |
| } |
| if (s.condition) |
| { |
| result |= canThrow(s.condition, func, mustNotThrow); |
| |
| const opt = s.condition.toBool(); |
| if (opt.hasValue(true)) |
| result &= ~BE.fallthru; |
| else if (opt.hasValue(false)) |
| return; |
| } |
| else |
| result &= ~BE.fallthru; // the body must do the exiting |
| if (s._body) |
| { |
| int r = blockExit(s._body, func, mustNotThrow); |
| if (r & (BE.break_ | BE.goto_)) |
| result |= BE.fallthru; |
| result |= r & ~(BE.fallthru | BE.break_ | BE.continue_); |
| } |
| if (s.increment) |
| result |= canThrow(s.increment, func, mustNotThrow); |
| } |
| |
| override void visit(ForeachStatement s) |
| { |
| result = BE.fallthru; |
| result |= canThrow(s.aggr, func, mustNotThrow); |
| |
| if (s._body) |
| result |= blockExit(s._body, func, mustNotThrow) & ~(BE.break_ | BE.continue_); |
| } |
| |
| override void visit(ForeachRangeStatement s) |
| { |
| assert(global.errors); |
| result = BE.fallthru; |
| } |
| |
| override void visit(IfStatement s) |
| { |
| //printf("IfStatement::blockExit(%p)\n", s); |
| result = BE.none; |
| result |= canThrow(s.condition, func, mustNotThrow); |
| |
| const opt = s.condition.toBool(); |
| if (opt.hasValue(true)) |
| { |
| result |= blockExit(s.ifbody, func, mustNotThrow); |
| } |
| else if (opt.hasValue(false)) |
| { |
| result |= blockExit(s.elsebody, func, mustNotThrow); |
| } |
| else |
| { |
| result |= blockExit(s.ifbody, func, mustNotThrow); |
| result |= blockExit(s.elsebody, func, mustNotThrow); |
| } |
| //printf("IfStatement::blockExit(%p) = x%x\n", s, result); |
| } |
| |
| override void visit(ConditionalStatement s) |
| { |
| result = blockExit(s.ifbody, func, mustNotThrow); |
| if (s.elsebody) |
| result |= blockExit(s.elsebody, func, mustNotThrow); |
| } |
| |
| override void visit(PragmaStatement s) |
| { |
| result = BE.fallthru; |
| } |
| |
| override void visit(StaticAssertStatement s) |
| { |
| result = BE.fallthru; |
| } |
| |
| override void visit(SwitchStatement s) |
| { |
| result = BE.none; |
| result |= canThrow(s.condition, func, mustNotThrow); |
| |
| if (s._body) |
| { |
| result |= blockExit(s._body, func, mustNotThrow); |
| if (result & BE.break_) |
| { |
| result |= BE.fallthru; |
| result &= ~BE.break_; |
| } |
| } |
| else |
| result |= BE.fallthru; |
| } |
| |
| override void visit(CaseStatement s) |
| { |
| result = blockExit(s.statement, func, mustNotThrow); |
| } |
| |
| override void visit(DefaultStatement s) |
| { |
| result = blockExit(s.statement, func, mustNotThrow); |
| } |
| |
| override void visit(GotoDefaultStatement s) |
| { |
| result = BE.goto_; |
| } |
| |
| override void visit(GotoCaseStatement s) |
| { |
| result = BE.goto_; |
| } |
| |
| override void visit(SwitchErrorStatement s) |
| { |
| // Switch errors are non-recoverable |
| result = BE.halt; |
| } |
| |
| override void visit(ReturnStatement s) |
| { |
| result = BE.return_; |
| if (s.exp) |
| result |= canThrow(s.exp, func, mustNotThrow); |
| } |
| |
| override void visit(BreakStatement s) |
| { |
| //printf("BreakStatement::blockExit(%p) = x%x\n", s, s.ident ? BE.goto_ : BE.break_); |
| result = s.ident ? BE.goto_ : BE.break_; |
| } |
| |
| override void visit(ContinueStatement s) |
| { |
| result = s.ident ? BE.continue_ | BE.goto_ : BE.continue_; |
| } |
| |
| override void visit(SynchronizedStatement s) |
| { |
| result = blockExit(s._body, func, mustNotThrow); |
| } |
| |
| override void visit(WithStatement s) |
| { |
| result = BE.none; |
| result |= canThrow(s.exp, func, mustNotThrow); |
| result |= blockExit(s._body, func, mustNotThrow); |
| } |
| |
| override void visit(TryCatchStatement s) |
| { |
| assert(s._body); |
| result = blockExit(s._body, func, false); |
| |
| int catchresult = 0; |
| foreach (c; *s.catches) |
| { |
| if (c.type == Type.terror) |
| continue; |
| |
| int cresult = blockExit(c.handler, func, mustNotThrow); |
| |
| /* If we're catching Object, then there is no throwing |
| */ |
| Identifier id = c.type.toBasetype().isClassHandle().ident; |
| if (c.internalCatch && (cresult & BE.fallthru)) |
| { |
| // https://issues.dlang.org/show_bug.cgi?id=11542 |
| // leave blockExit flags of the body |
| cresult &= ~BE.fallthru; |
| } |
| else if (id == Id.Object || id == Id.Throwable) |
| { |
| result &= ~(BE.throw_ | BE.errthrow); |
| } |
| else if (id == Id.Exception) |
| { |
| result &= ~BE.throw_; |
| } |
| catchresult |= cresult; |
| } |
| if (mustNotThrow && (result & BE.throw_)) |
| { |
| // now explain why this is nothrow |
| blockExit(s._body, func, mustNotThrow); |
| } |
| result |= catchresult; |
| } |
| |
| override void visit(TryFinallyStatement s) |
| { |
| result = BE.fallthru; |
| if (s._body) |
| result = blockExit(s._body, func, false); |
| |
| // check finally body as well, it may throw (bug #4082) |
| int finalresult = BE.fallthru; |
| if (s.finalbody) |
| finalresult = blockExit(s.finalbody, func, false); |
| |
| // If either body or finalbody halts |
| if (result == BE.halt) |
| finalresult = BE.none; |
| if (finalresult == BE.halt) |
| result = BE.none; |
| |
| if (mustNotThrow) |
| { |
| // now explain why this is nothrow |
| if (s._body && (result & BE.throw_)) |
| blockExit(s._body, func, mustNotThrow); |
| if (s.finalbody && (finalresult & BE.throw_)) |
| blockExit(s.finalbody, func, mustNotThrow); |
| } |
| |
| version (none) |
| { |
| // https://issues.dlang.org/show_bug.cgi?id=13201 |
| // Mask to prevent spurious warnings for |
| // destructor call, exit of synchronized statement, etc. |
| if (result == BE.halt && finalresult != BE.halt && s.finalbody && s.finalbody.hasCode()) |
| { |
| s.finalbody.warning("statement is not reachable"); |
| } |
| } |
| |
| if (!(finalresult & BE.fallthru)) |
| result &= ~BE.fallthru; |
| result |= finalresult & ~BE.fallthru; |
| } |
| |
| override void visit(ScopeGuardStatement s) |
| { |
| // At this point, this statement is just an empty placeholder |
| result = BE.fallthru; |
| } |
| |
| override void visit(ThrowStatement s) |
| { |
| if (s.internalThrow) |
| { |
| // https://issues.dlang.org/show_bug.cgi?id=8675 |
| // Allow throwing 'Throwable' object even if mustNotThrow. |
| result = BE.fallthru; |
| return; |
| } |
| |
| result = checkThrow(s.loc, s.exp, mustNotThrow); |
| } |
| |
| override void visit(GotoStatement s) |
| { |
| //printf("GotoStatement::blockExit(%p)\n", s); |
| result = BE.goto_; |
| } |
| |
| override void visit(LabelStatement s) |
| { |
| //printf("LabelStatement::blockExit(%p)\n", s); |
| result = blockExit(s.statement, func, mustNotThrow); |
| if (s.breaks) |
| result |= BE.fallthru; |
| } |
| |
| override void visit(CompoundAsmStatement s) |
| { |
| // Assume the worst |
| result = BE.fallthru | BE.return_ | BE.goto_ | BE.halt; |
| if (!(s.stc & STC.nothrow_)) |
| { |
| if (mustNotThrow && !(s.stc & STC.nothrow_)) |
| s.error("`asm` statement is assumed to throw - mark it with `nothrow` if it does not"); |
| else |
| result |= BE.throw_; |
| } |
| } |
| |
| override void visit(ImportStatement s) |
| { |
| result = BE.fallthru; |
| } |
| } |
| |
| if (!s) |
| return BE.fallthru; |
| scope BlockExit be = new BlockExit(func, mustNotThrow); |
| s.accept(be); |
| return be.result; |
| } |
| |
| /++ |
| + Checks whether `throw <exp>` throws an `Exception` or an `Error` |
| + and raises an error if this violates `nothrow`. |
| + |
| + Params: |
| + loc = location of the `throw` |
| + exp = expression yielding the throwable |
| + mustNotThrow = inside of a `nothrow` scope? |
| + |
| + Returns: `BE.[err]throw` depending on the type of `exp` |
| +/ |
| BE checkThrow(ref const Loc loc, Expression exp, const bool mustNotThrow) |
| { |
| import dmd.errors : error; |
| |
| Type t = exp.type.toBasetype(); |
| ClassDeclaration cd = t.isClassHandle(); |
| assert(cd); |
| |
| if (cd == ClassDeclaration.errorException || ClassDeclaration.errorException.isBaseOf(cd, null)) |
| { |
| return BE.errthrow; |
| } |
| if (mustNotThrow) |
| loc.error("`%s` is thrown but not caught", exp.type.toChars()); |
| |
| return BE.throw_; |
| } |