// See the file "COPYING" in the main distribution directory for copyright. #include "zeek/script_opt/Reduce.h" #include "zeek/Desc.h" #include "zeek/Expr.h" #include "zeek/Func.h" #include "zeek/ID.h" #include "zeek/Reporter.h" #include "zeek/Scope.h" #include "zeek/Stmt.h" #include "zeek/Var.h" #include "zeek/script_opt/ExprOptInfo.h" #include "zeek/script_opt/FuncInfo.h" #include "zeek/script_opt/StmtOptInfo.h" #include "zeek/script_opt/TempVar.h" namespace zeek::detail { Reducer::Reducer(const ScriptFunc* func, std::shared_ptr _pf, ProfileFuncs& _pfs) : pf(std::move(_pf)), pfs(_pfs) { auto& ft = func->GetType(); // Track the parameters so we don't remap them. int num_params = ft->Params()->NumFields(); auto& scope_vars = current_scope()->OrderedVars(); for ( auto i = 0; i < num_params; ++i ) tracked_ids.insert(scope_vars[i].get()); // Now include any captures. if ( ft->GetCaptures() ) for ( auto& c : *ft->GetCaptures() ) tracked_ids.insert(c.Id().get()); } StmtPtr Reducer::Reduce(StmtPtr s) { reduction_root = std::move(s); try { return reduction_root->Reduce(this); } catch ( InterpreterException& e ) { /* Already reported. */ return reduction_root; } } ExprPtr Reducer::GenTemporaryExpr(const TypePtr& t, ExprPtr rhs) { auto e = make_intrusive(GenTemporary(t, rhs)); e->SetLocationInfo(rhs->GetLocationInfo()); // No need to associate with current statement, since these // are not generated during optimization. return e; } NameExprPtr Reducer::UpdateName(NameExprPtr n) { if ( NameIsReduced(n.get()) ) return n; auto ne = make_intrusive(FindNewLocal(n)); // This name can be used by follow-on optimization analysis, // so need to associate it with its statement. BindExprToCurrStmt(ne); return ne; } bool Reducer::NameIsReduced(const NameExpr* n) { return ID_IsReducedOrTopLevel(n->Id()); } void Reducer::UpdateIDs(IDPList* ids) { loop_over_list(*ids, i) { IDPtr id = {NewRef{}, (*ids)[i]}; if ( ! ID_IsReducedOrTopLevel(id) ) { Unref((*ids)[i]); (*ids)[i] = UpdateID(id).release(); } } } void Reducer::UpdateIDs(std::vector& ids) { for ( auto& id : ids ) if ( ! ID_IsReducedOrTopLevel(id) ) id = UpdateID(id); } bool Reducer::IDsAreReduced(const IDPList* ids) const { for ( auto& id : *ids ) if ( ! ID_IsReduced(id) ) return false; return true; } bool Reducer::IDsAreReduced(const std::vector& ids) const { for ( const auto& id : ids ) if ( ! ID_IsReduced(id) ) return false; return true; } IDPtr Reducer::UpdateID(IDPtr id) { if ( ID_IsReducedOrTopLevel(id) ) return id; return FindNewLocal(id); } bool Reducer::ID_IsReducedOrTopLevel(const ID* id) { if ( inline_block_level == 0 ) { tracked_ids.insert(id); return true; } return ID_IsReduced(id); } bool Reducer::ID_IsReduced(const ID* id) const { return inline_block_level == 0 || tracked_ids.count(id) > 0 || id->IsGlobal() || IsTemporary(id); } StmtPtr Reducer::GenParam(const IDPtr& id, ExprPtr rhs, bool is_modified) { auto param = GenInlineBlockName(id); auto rhs_id = rhs->Tag() == EXPR_NAME ? rhs->AsNameExpr()->IdPtr() : nullptr; if ( rhs_id && pf->Locals().count(rhs_id.get()) == 0 && ! rhs_id->IsConst() ) // It's hard to guarantee the RHS won't change during // the inline block's execution. is_modified = true; if ( ! is_modified ) { // Can use a temporary variable, which then supports // optimization via alias propagation. auto param_id = GenTemporary(id->GetType(), rhs, param->IdPtr()); auto& tv = ids_to_temps[param_id.get()]; if ( rhs_id ) tv->SetAlias(rhs_id); else if ( rhs->Tag() == EXPR_CONST ) tv->SetConst(rhs->AsConstExpr()); param_temps.insert(param_id.get()); param = make_intrusive(param_id); } auto assign = make_intrusive(param, rhs, false, nullptr, nullptr, false); return make_intrusive(assign); } NameExprPtr Reducer::GenInlineBlockName(const IDPtr& id) { // We do this during reduction, not optimization, so no need // to associate with curr_stmt. return make_intrusive(GenLocal(id)); } NameExprPtr Reducer::PushInlineBlock(TypePtr type) { ++inline_block_level; block_locals.emplace_back(std::unordered_map()); if ( ! type || type->Tag() == TYPE_VOID ) return nullptr; IDPtr ret_id = install_ID("@retvar", "", false, false); ret_id->SetType(type); ret_id->GetOptInfo()->SetTemp(); ret_vars.insert(ret_id.get()); // Track this as a new local *if* we're in the outermost inlining // block. If we're recursively deeper into inlining, then this // variable will get mapped to a local anyway, so no need. if ( inline_block_level == 1 ) AddNewLocal(ret_id); return GenInlineBlockName(ret_id); } void Reducer::PopInlineBlock() { --inline_block_level; for ( auto& l : block_locals.back() ) { auto key = l.first; auto prev = l.second; if ( prev ) orig_to_new_locals[key] = prev; else orig_to_new_locals.erase(key); } block_locals.pop_back(); } bool Reducer::SameVal(const Val* v1, const Val* v2) const { if ( is_atomic_val(v1) ) return same_atomic_val(v1, v2); else return v1 == v2; } ExprPtr Reducer::NewVarUsage(IDPtr var, const Expr* orig) { auto var_usage = make_intrusive(var); BindExprToCurrStmt(var_usage); return var_usage; } void Reducer::BindExprToCurrStmt(const ExprPtr& e) { e->GetOptInfo()->stmt_num = curr_stmt->GetOptInfo()->stmt_num; } void Reducer::BindStmtToCurrStmt(const StmtPtr& s) { s->GetOptInfo()->stmt_num = curr_stmt->GetOptInfo()->stmt_num; } bool Reducer::SameOp(const Expr* op1, const Expr* op2) { if ( op1 == op2 ) return true; if ( op1->Tag() != op2->Tag() ) return false; if ( op1->Tag() == EXPR_NAME ) { // Needs to be both the same identifier and in contexts // where the identifier has the same definitions. auto op1_n = op1->AsNameExpr(); auto op2_n = op2->AsNameExpr(); auto op1_id = op1_n->Id(); auto op2_id = op2_n->Id(); if ( op1_id != op2_id ) return false; auto e_stmt_1 = op1->GetOptInfo()->stmt_num; auto e_stmt_2 = op2->GetOptInfo()->stmt_num; auto def_1 = op1_id->GetOptInfo()->DefinitionBefore(e_stmt_1); auto def_2 = op2_id->GetOptInfo()->DefinitionBefore(e_stmt_2); return def_1 == def_2 && def_1 != NO_DEF; } else if ( op1->Tag() == EXPR_CONST ) { auto op1_c = op1->AsConstExpr(); auto op2_c = op2->AsConstExpr(); auto op1_v = op1_c->Value(); auto op2_v = op2_c->Value(); return SameVal(op1_v, op2_v); } else if ( op1->Tag() == EXPR_LIST ) { auto op1_l = op1->AsListExpr()->Exprs(); auto op2_l = op2->AsListExpr()->Exprs(); if ( op1_l.length() != op2_l.length() ) return false; for ( auto i = 0; i < op1_l.length(); ++i ) if ( ! SameExpr(op1_l[i], op2_l[i]) ) return false; return true; } reporter->InternalError("bad singleton tag"); return false; } bool Reducer::SameExpr(const Expr* e1, const Expr* e2) { if ( e1 == e2 ) return true; if ( e1->Tag() != e2->Tag() ) return false; if ( ! same_type(e1->GetType(), e2->GetType()) ) return false; switch ( e1->Tag() ) { case EXPR_NAME: case EXPR_CONST: return SameOp(e1, e2); case EXPR_CLONE: case EXPR_RECORD_CONSTRUCTOR: case EXPR_TABLE_CONSTRUCTOR: case EXPR_SET_CONSTRUCTOR: case EXPR_VECTOR_CONSTRUCTOR: case EXPR_EVENT: case EXPR_SCHEDULE: // These always generate a new value. return false; case EXPR_INCR: case EXPR_DECR: case EXPR_AND_AND: case EXPR_OR_OR: case EXPR_ASSIGN: case EXPR_FIELD_ASSIGN: case EXPR_INDEX_SLICE_ASSIGN: // All of these should have been translated into something // else. reporter->InternalError("Unexpected tag in Reducer::SameExpr"); case EXPR_ANY_INDEX: { auto a1 = e1->AsAnyIndexExpr(); auto a2 = e2->AsAnyIndexExpr(); if ( a1->Index() != a2->Index() ) return false; return SameOp(a1->GetOp1(), a2->GetOp1()); } case EXPR_FIELD: { auto f1 = e1->AsFieldExpr(); auto f2 = e2->AsFieldExpr(); if ( f1->Field() != f2->Field() ) return false; return SameOp(f1->GetOp1(), f2->GetOp1()); } case EXPR_HAS_FIELD: { auto f1 = e1->AsHasFieldExpr(); auto f2 = e2->AsHasFieldExpr(); if ( f1->Field() != f2->Field() ) return false; return SameOp(f1->GetOp1(), f2->GetOp1()); } case EXPR_LIST: { auto l1 = e1->AsListExpr()->Exprs(); auto l2 = e2->AsListExpr()->Exprs(); ASSERT(l1.length() == l2.length()); for ( int i = 0; i < l1.length(); ++i ) if ( ! SameExpr(l1[i], l2[i]) ) return false; return true; } case EXPR_CALL: { auto c1 = e1->AsCallExpr(); auto c2 = e2->AsCallExpr(); auto f1 = c1->Func(); auto f2 = c2->Func(); if ( f1 != f2 ) return false; if ( ! f1->IsPure() ) return false; return SameExpr(c1->Args(), c2->Args()); } case EXPR_LAMBDA: return false; case EXPR_IS: { if ( ! SameOp(e1->GetOp1(), e2->GetOp1()) ) return false; auto i1 = e1->AsIsExpr(); auto i2 = e2->AsIsExpr(); return same_type(i1->TestType(), i2->TestType()); } default: if ( ! e1->GetOp1() ) reporter->InternalError("Bad default in Reducer::SameExpr"); if ( ! SameOp(e1->GetOp1(), e2->GetOp1()) ) return false; if ( e1->GetOp2() && ! SameOp(e1->GetOp2(), e2->GetOp2()) ) return false; if ( e1->GetOp3() && ! SameOp(e1->GetOp3(), e2->GetOp3()) ) return false; return true; } } IDPtr Reducer::FindExprTmp(const Expr* rhs, const Expr* a, const std::shared_ptr& lhs_tmp) { for ( const auto& et_i : expr_temps ) { if ( et_i->Alias() || ! et_i->IsActive() || et_i == lhs_tmp ) // This can happen due to re-reduction while // optimizing. continue; auto et_i_expr = et_i->RHS(); if ( SameExpr(rhs, et_i_expr) ) { // We have an apt candidate. Make sure its value // always makes it here. auto id = et_i->Id().get(); auto stmt_num = a->GetOptInfo()->stmt_num; auto def = id->GetOptInfo()->DefinitionBefore(stmt_num); if ( def == NO_DEF ) // The temporary's value isn't guaranteed // to make it here. continue; // Make sure there aren't ambiguities due to // possible modifications to aggregates. if ( ! ExprValid(id, et_i_expr, a) ) continue; return et_i->Id(); } } return nullptr; } bool Reducer::ExprValid(const ID* id, const Expr* e1, const Expr* e2) const { // First check for whether e1 is already known to itself have side effects. // If so, then it's never safe to reuse its associated identifier in lieu // of e2. std::optional& e1_se = e1->GetOptInfo()->SideEffects(); if ( ! e1_se ) { bool has_side_effects = false; if ( e1->Tag() == EXPR_INDEX ) { auto aggr = e1->GetOp1(); auto aggr_t = aggr->GetType(); if ( pfs.HasSideEffects(SideEffectsOp::READ, aggr_t) ) has_side_effects = true; else if ( aggr_t->Tag() == TYPE_TABLE && pfs.IsTableWithDefaultAggr(aggr_t.get()) ) has_side_effects = true; } else if ( e1->Tag() == EXPR_RECORD_CONSTRUCTOR || e1->Tag() == EXPR_RECORD_COERCE ) has_side_effects = pfs.HasSideEffects(SideEffectsOp::CONSTRUCTION, e1->GetType()); e1_se = ExprSideEffects(has_side_effects); } if ( e1_se->HasSideEffects() ) { // We already know that e2 is structurally identical to e1. e2->GetOptInfo()->SideEffects() = ExprSideEffects(true); return false; } // Here are the considerations for expression validity. // // * None of the operands used in the given expression can // have been assigned. // // * If the expression yields an aggregate, or one of the // operands in the expression is an aggregate, then there // must not be any assignments to aggregates of the same // type(s). This is to deal with possible aliases. // // * Same goes to modifications of aggregates via "add" or "delete" // or "+=" append. // // * Assessment of any record constructors or coercions, or // table references or modifications, for possible invocation of // associated handlers that have side effects. // // * Assessment of function calls for potential side effects. // // These latter two are guided by the global profile of the full set // of script functions. // Tracks which ID's are germane for our analysis. std::vector ids; ids.push_back(id); // Identify variables involved in the expression. CheckIDs(e1->GetOp1().get(), ids); CheckIDs(e1->GetOp2().get(), ids); CheckIDs(e1->GetOp3().get(), ids); if ( e1->Tag() == EXPR_NAME ) ids.push_back(e1->AsNameExpr()->Id()); CSE_ValidityChecker vc(pfs, ids, e1, e2); reduction_root->Traverse(&vc); return vc.IsValid(); } void Reducer::CheckIDs(const Expr* e, std::vector& ids) const { if ( ! e ) return; if ( e->Tag() == EXPR_LIST ) { const auto& e_l = e->AsListExpr()->Exprs(); for ( auto i = 0; i < e_l.length(); ++i ) CheckIDs(e_l[i], ids); } else if ( e->Tag() == EXPR_NAME ) ids.push_back(e->AsNameExpr()->Id()); } bool Reducer::IsCSE(const AssignExpr* a, const NameExpr* lhs, const Expr* rhs) { auto lhs_id = lhs->Id(); auto lhs_tmp = FindTemporary(lhs_id); // nil if LHS not a temporary auto rhs_tmp = FindExprTmp(rhs, a, lhs_tmp); ExprPtr new_rhs; if ( rhs_tmp ) { // We already have a temporary new_rhs = NewVarUsage(rhs_tmp, rhs); rhs = new_rhs.get(); } if ( lhs_tmp ) { if ( rhs->Tag() == EXPR_CONST ) { // mark temporary as just being a constant lhs_tmp->SetConst(rhs->AsConstExpr()); return true; } if ( rhs->Tag() == EXPR_NAME ) { auto rhs_id = rhs->AsNameExpr()->IdPtr(); auto rhs_tmp_var = FindTemporary(rhs_id.get()); if ( rhs_tmp_var ) { if ( rhs_tmp_var->Const() ) // temporary can be replaced with constant lhs_tmp->SetConst(rhs_tmp_var->Const()); else lhs_tmp->SetAlias(rhs_id); return true; } } expr_temps.emplace_back(lhs_tmp); } return false; } const ConstExpr* Reducer::CheckForConst(const IDPtr& id, int stmt_num) const { if ( id->GetType()->Tag() == TYPE_ANY ) // Don't propagate identifiers of type "any" as constants. // This is because the identifier might be used in some // context that's dynamically unreachable due to the type // of its value (such as via a type-switch), but for which // constant propagation of the constant value to that // context can result in compile-time errors when folding // expressions in which the identifier appears (and is // in that context presumed to have a different type). return nullptr; auto oi = id->GetOptInfo(); auto c = oi->Const(); if ( c ) return c; auto e = id->GetOptInfo()->DefExprBefore(stmt_num); if ( e ) { auto ce = constant_exprs.find(e.get()); if ( ce != constant_exprs.end() ) e = ce->second; if ( e->Tag() == EXPR_CONST ) return e->AsConstExpr(); // Follow aliases. if ( e->Tag() != EXPR_NAME ) return nullptr; return CheckForConst(e->AsNameExpr()->IdPtr(), stmt_num); } return nullptr; } ConstExprPtr Reducer::Fold(ExprPtr e) { auto c = make_intrusive(e->Eval(nullptr)); FoldedTo(e, c); return c; } void Reducer::FoldedTo(ExprPtr e, ConstExprPtr c) { om.AddObj(e.get()); constant_exprs[e.get()] = std::move(c); folded_exprs.push_back(std::move(e)); } ExprPtr Reducer::OptExpr(Expr* e) { StmtPtr opt_stmts; auto opt_e = e->Reduce(this, opt_stmts); if ( opt_stmts ) reporter->InternalError("Generating new statements while optimizing"); if ( opt_e->Tag() == EXPR_NAME ) return UpdateExpr(opt_e); return opt_e; } ExprPtr Reducer::UpdateExpr(ExprPtr e) { if ( e->Tag() != EXPR_NAME ) return OptExpr(e); auto n = e->AsNameExpr(); auto id = n->Id(); if ( id->IsGlobal() ) return e; auto tmp_var = FindTemporary(id); if ( ! tmp_var ) { IDPtr id_ptr = {NewRef{}, id}; auto stmt_num = e->GetOptInfo()->stmt_num; auto is_const = CheckForConst(id_ptr, stmt_num); if ( is_const ) { // Remember this variable as one whose value // we used for constant propagation. That // ensures we can subsequently not complain // about it being assigned but not used (though // we can still omit the assignment). constant_vars.insert(id); return make_intrusive(is_const->ValuePtr()); } return e; } if ( tmp_var->Const() ) return make_intrusive(tmp_var->Const()->ValuePtr()); auto alias = tmp_var->Alias(); if ( alias ) { // Make sure that the definitions for the alias here are // the same as when the alias was created. auto alias_tmp = FindTemporary(alias.get()); // Resolve any alias chains. while ( alias_tmp && alias_tmp->Alias() ) { alias = alias_tmp->Alias(); alias_tmp = FindTemporary(alias.get()); } return NewVarUsage(alias, e.get()); } auto rhs = tmp_var->RHS(); if ( rhs->Tag() != EXPR_CONST ) return e; auto c = rhs->AsConstExpr(); return make_intrusive(c->ValuePtr()); } StmtPtr Reducer::MergeStmts(const NameExpr* lhs, ExprPtr rhs, const StmtPtr& succ_stmt) { // First check for tmp=rhs. auto lhs_id = lhs->Id(); auto lhs_tmp = FindTemporary(lhs_id); if ( ! lhs_tmp ) return nullptr; // We have tmp=rhs. Now look for var=tmp. if ( succ_stmt->Tag() != STMT_EXPR ) return nullptr; auto s_e = succ_stmt->AsExprStmt()->StmtExpr(); if ( s_e->Tag() != EXPR_ASSIGN ) return nullptr; auto a = s_e->AsAssignExpr(); auto a_lhs = a->GetOp1(); auto a_rhs = a->GetOp2(); if ( a_lhs->Tag() != EXPR_REF || a_rhs->Tag() != EXPR_NAME ) // Complex 2nd-statement assignment, or RHS not a candidate. return nullptr; auto a_lhs_deref = a_lhs->AsRefExprPtr()->GetOp1(); if ( a_lhs_deref->Tag() != EXPR_NAME ) // Complex 2nd-statement assignment. return nullptr; auto a_lhs_var = a_lhs_deref->AsNameExpr()->Id(); auto a_rhs_var = a_rhs->AsNameExpr()->Id(); if ( a_rhs_var != lhs_id ) // 2nd statement is var=something else. return nullptr; if ( a_lhs_var->GetType()->Tag() != a_rhs_var->GetType()->Tag() ) // This can happen when we generate an assignment // specifically to convert to/from an "any" type. return nullptr; if ( FindTemporary(a_lhs_var) ) { // "var" is itself a temporary. Don't complain, as // complex reductions can generate these. We'll wind // up folding the chain once it hits a regular variable. return nullptr; } // Got it. Mark the original temporary as no longer relevant. lhs_tmp->Deactivate(); auto merge_e = make_intrusive(a_lhs_deref, rhs, false, nullptr, nullptr, false); auto merge_e_stmt = make_intrusive(merge_e); // Update the associated stmt_num's. For strict correctness, we // want both of these bound to the earlier of the two statements // we're merging (though in practice, either will work, since // we're eliding the only difference between the two). Our // caller ensures this. BindExprToCurrStmt(merge_e); BindStmtToCurrStmt(merge_e_stmt); return merge_e_stmt; } IDPtr Reducer::GenTemporary(TypePtr t, ExprPtr rhs, IDPtr id) { if ( Optimizing() ) reporter->InternalError("Generating a new temporary while optimizing"); if ( omitted_stmts.size() > 0 ) reporter->InternalError("Generating a new temporary while pruning statements"); auto temp = std::make_shared(temps.size(), rhs); IDPtr temp_id; if ( id ) temp_id = id; else temp_id = install_ID(temp->Name(), "", false, false); temp->SetID(temp_id); temp_id->SetType(t); temps.push_back(temp); om.AddObj(temp_id.get()); ids_to_temps[temp_id.get()] = temp; return temp_id; } IDPtr Reducer::FindNewLocal(const IDPtr& id) { auto mapping = orig_to_new_locals.find(id.get()); if ( mapping != orig_to_new_locals.end() ) return mapping->second; return GenLocal(id); } void Reducer::AddNewLocal(const IDPtr& l) { new_locals.insert(l.get()); tracked_ids.insert(l.get()); } IDPtr Reducer::GenLocal(const IDPtr& orig) { if ( Optimizing() ) reporter->InternalError("Generating a new local while optimizing"); if ( omitted_stmts.size() > 0 ) reporter->InternalError("Generating a new local while pruning statements"); // Make sure the identifier is not being re-re-mapped. ASSERT(strchr(orig->Name(), '.') == nullptr); char buf[8192]; int n = new_locals.size(); snprintf(buf, sizeof buf, "%s.%d", orig->Name(), n); IDPtr local_id = install_ID(buf, "", false, false); local_id->SetType(orig->GetType()); local_id->SetAttrs(orig->GetAttrs()); if ( orig->IsBlank() ) local_id->SetBlank(); if ( orig->GetOptInfo()->IsTemp() ) local_id->GetOptInfo()->SetTemp(); IDPtr prev; if ( orig_to_new_locals.count(orig.get()) ) prev = orig_to_new_locals[orig.get()]; AddNewLocal(local_id); om.AddObj(orig.get()); orig_to_new_locals[orig.get()] = local_id; if ( ! block_locals.empty() && ret_vars.count(orig.get()) == 0 ) block_locals.back()[orig.get()] = prev; return local_id; } bool Reducer::IsNewLocal(const ID* id) const { ID* non_const_ID = (ID*)id; // I don't get why C++ requires this return new_locals.count(non_const_ID) != 0; } std::shared_ptr Reducer::FindTemporary(const ID* id) const { auto tmp = ids_to_temps.find(id); if ( tmp == ids_to_temps.end() ) return nullptr; else return tmp->second; } CSE_ValidityChecker::CSE_ValidityChecker(ProfileFuncs& _pfs, const std::vector& _ids, const Expr* _start_e, const Expr* _end_e) : pfs(_pfs), ids(_ids) { start_e = _start_e; end_e = _end_e; // Track whether this is a record assignment, in which case // we're attuned to assignments to the same field for the // same type of record. if ( start_e->Tag() == EXPR_FIELD ) { field = start_e->AsFieldExpr()->Field(); // Track the type of the record, too, so we don't confuse // field references to different records that happen to // have the same offset as potential aliases. field_type = start_e->GetOp1()->GetType(); } else field = -1; // flags that there's no relevant field } TraversalCode CSE_ValidityChecker::PreStmt(const Stmt* s) { auto t = s->Tag(); if ( t == STMT_WHEN ) { // These are too hard to analyze - they result in lambda calls // that can affect aggregates, etc. is_valid = false; return TC_ABORTALL; } if ( t == STMT_ADD || t == STMT_DELETE ) in_aggr_mod_stmt = true; return TC_CONTINUE; } TraversalCode CSE_ValidityChecker::PostStmt(const Stmt* s) { if ( s->Tag() == STMT_ADD || s->Tag() == STMT_DELETE ) in_aggr_mod_stmt = false; return TC_CONTINUE; } TraversalCode CSE_ValidityChecker::PreExpr(const Expr* e) { if ( e == start_e ) { ASSERT(! have_start_e); have_start_e = true; // Don't analyze the expression, as it's our starting // point and we don't want to conflate its properties // with those of any intervening expressions. return TC_CONTINUE; } if ( e == end_e ) { if ( ! have_start_e ) reporter->InternalError("CSE_ValidityChecker: saw end but not start"); ASSERT(! have_end_e); have_end_e = true; // ... and we're now done. return TC_ABORTALL; } if ( ! have_start_e ) // We don't yet have a starting point. return TC_CONTINUE; // We have a starting point, and not yet an ending point. auto t = e->Tag(); switch ( t ) { case EXPR_ASSIGN: { auto lhs_ref = e->GetOp1()->AsRefExprPtr(); auto lhs = lhs_ref->GetOp1()->AsNameExpr(); if ( CheckID(lhs->Id(), false) ) return TC_ABORTALL; // Note, we don't use CheckAggrMod() because this is a plain // assignment. It might be changing a variable's binding to // an aggregate ("aggr_var = new_aggr_val"), but we don't // introduce temporaries that are simply aliases of existing // variables (e.g., we don't have "::#8 = aggr_var"), // and so there's no concern that the temporary could now be // referring to the wrong aggregate. If instead we have // "::#8 = aggr_var$foo", then a reassignment here // to "aggr_var" will already be caught by CheckID(). } break; case EXPR_INDEX_ASSIGN: { auto lhs_aggr = e->GetOp1(); auto lhs_aggr_id = lhs_aggr->AsNameExpr()->Id(); if ( CheckID(lhs_aggr_id, true) || CheckTableMod(lhs_aggr->GetType()) ) return TC_ABORTALL; } break; case EXPR_FIELD_LHS_ASSIGN: { auto lhs = e->GetOp1(); auto lhs_aggr_id = lhs->AsNameExpr()->Id(); auto lhs_field = e->AsFieldLHSAssignExpr()->Field(); if ( CheckID(lhs_aggr_id, true) || (lhs_field == field && same_type(lhs_aggr_id->GetType(), field_type)) ) return TC_ABORTALL; } break; case EXPR_APPEND_TO: // This doesn't directly change any identifiers, but does // alter an aggregate. if ( CheckAggrMod(e->GetType()) ) return TC_ABORTALL; break; case EXPR_CALL: if ( CheckCall(e->AsCallExpr()) ) return TC_ABORTALL; break; case EXPR_TABLE_CONSTRUCTOR: // These have EXPR_ASSIGN's in them that don't // correspond to actual assignments to variables, // so we don't want to traverse them. return TC_ABORTSTMT; case EXPR_RECORD_COERCE: case EXPR_RECORD_CONSTRUCTOR: // Note, record coercion behaves like constructors in terms of // potentially executing &default functions. In either case, // the type of the expression reflects the type we want to analyze // for side effects. if ( CheckRecordConstructor(e->GetType()) ) return TC_ABORTALL; break; case EXPR_INDEX: case EXPR_FIELD: { // We treat these together because they both have to be checked // when inside an "add" or "delete" statement. auto aggr = e->GetOp1(); auto aggr_t = aggr->GetType(); if ( in_aggr_mod_stmt ) { auto aggr_id = aggr->AsNameExpr()->Id(); if ( CheckID(aggr_id, true) || CheckAggrMod(aggr_t) ) return TC_ABORTALL; } else if ( t == EXPR_INDEX && aggr_t->Tag() == TYPE_TABLE ) { if ( CheckTableRef(aggr_t) ) return TC_ABORTALL; } } break; default: break; } return TC_CONTINUE; } bool CSE_ValidityChecker::CheckID(const ID* id, bool ignore_orig) { for ( auto i : ids ) { if ( ignore_orig && i == ids.front() ) continue; if ( id == i ) return Invalid(); // reassignment } return false; } bool CSE_ValidityChecker::CheckAggrMod(const TypePtr& t) { if ( ! IsAggr(t) ) return false; for ( auto i : ids ) if ( same_type(t, i->GetType()) ) return Invalid(); return false; } bool CSE_ValidityChecker::CheckRecordConstructor(const TypePtr& t) { if ( t->Tag() != TYPE_RECORD ) return false; return CheckSideEffects(SideEffectsOp::CONSTRUCTION, t); } bool CSE_ValidityChecker::CheckTableMod(const TypePtr& t) { if ( CheckAggrMod(t) ) return true; if ( t->Tag() != TYPE_TABLE ) return false; return CheckSideEffects(SideEffectsOp::WRITE, t); } bool CSE_ValidityChecker::CheckTableRef(const TypePtr& t) { return CheckSideEffects(SideEffectsOp::READ, t); } bool CSE_ValidityChecker::CheckCall(const CallExpr* c) { auto func = c->Func(); std::string desc; if ( func->Tag() != EXPR_NAME ) // Can't analyze indirect calls. return Invalid(); IDSet non_local_ids; TypeSet aggrs; bool is_unknown = false; auto resolved = pfs.GetCallSideEffects(func->AsNameExpr(), non_local_ids, aggrs, is_unknown); ASSERT(resolved); if ( is_unknown || CheckSideEffects(non_local_ids, aggrs) ) return Invalid(); return false; } bool CSE_ValidityChecker::CheckSideEffects(SideEffectsOp::AccessType access, const TypePtr& t) { IDSet non_local_ids; TypeSet aggrs; if ( pfs.GetSideEffects(access, t.get(), non_local_ids, aggrs) ) return Invalid(); return CheckSideEffects(non_local_ids, aggrs); } bool CSE_ValidityChecker::CheckSideEffects(const IDSet& non_local_ids, const TypeSet& aggrs) { if ( non_local_ids.empty() && aggrs.empty() ) // This is far and away the most common case. return false; for ( auto i : ids ) { for ( auto nli : non_local_ids ) if ( nli == i ) return Invalid(); auto i_t = i->GetType(); for ( auto a : aggrs ) if ( same_type(a, i_t.get()) ) return Invalid(); } return false; } const Expr* non_reduced_perp; bool checking_reduction; bool NonReduced(const Expr* perp) { if ( checking_reduction ) non_reduced_perp = perp; return false; } } // namespace zeek::detail