| // Part of the Crubit project, under the Apache License v2.0 with LLVM |
| // Exceptions. See /LICENSE for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| |
| #include "lifetime_analysis/lifetime_analysis.h" |
| |
| #include <cassert> |
| #include <cstddef> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <variant> |
| #include <vector> |
| |
| #include "lifetime_analysis/lifetime_constraints.h" |
| #include "lifetime_analysis/lifetime_lattice.h" |
| #include "lifetime_analysis/object.h" |
| #include "lifetime_analysis/object_repository.h" |
| #include "lifetime_analysis/object_set.h" |
| #include "lifetime_analysis/points_to_map.h" |
| #include "lifetime_annotations/function_lifetimes.h" |
| #include "lifetime_annotations/pointee_type.h" |
| #include "lifetime_annotations/type_lifetimes.h" |
| #include "clang/AST/Decl.h" |
| #include "clang/AST/DeclCXX.h" |
| #include "clang/AST/Expr.h" |
| #include "clang/AST/ExprCXX.h" |
| #include "clang/AST/OperationKinds.h" |
| #include "clang/AST/Stmt.h" |
| #include "clang/AST/StmtVisitor.h" |
| #include "clang/AST/TemplateBase.h" |
| #include "clang/AST/Type.h" |
| #include "clang/Analysis/CFG.h" |
| #include "clang/Analysis/FlowSensitive/DataflowEnvironment.h" |
| #include "clang/Basic/DiagnosticIDs.h" |
| #include "clang/Basic/LLVM.h" |
| #include "llvm/ADT/DenseMap.h" |
| #include "llvm/ADT/DenseSet.h" |
| #include "llvm/Support/ErrorHandling.h" |
| #include "llvm/Support/raw_ostream.h" |
| |
| namespace clang { |
| namespace tidy { |
| namespace lifetimes { |
| |
| namespace { |
| |
| class TransferStmtVisitor |
| : public clang::StmtVisitor<TransferStmtVisitor, |
| std::optional<std::string>> { |
| public: |
| TransferStmtVisitor( |
| ObjectRepository& object_repository, PointsToMap& points_to_map, |
| LifetimeConstraints& constraints, ObjectSet& single_valued_objects, |
| const clang::FunctionDecl* func, |
| const llvm::DenseMap<const clang::FunctionDecl*, |
| FunctionLifetimesOrError>& callee_lifetimes, |
| const DiagnosticReporter& diag_reporter) |
| : object_repository_(object_repository), |
| points_to_map_(points_to_map), |
| constraints_(constraints), |
| single_valued_objects_(single_valued_objects), |
| func_(func), |
| callee_lifetimes_(callee_lifetimes), |
| diag_reporter_(diag_reporter) {} |
| |
| std::optional<std::string> VisitExpr(const clang::Expr* expr); |
| std::optional<std::string> VisitDeclRefExpr( |
| const clang::DeclRefExpr* decl_ref); |
| std::optional<std::string> VisitStringLiteral( |
| const clang::StringLiteral* strlit); |
| std::optional<std::string> VisitCastExpr(const clang::CastExpr* cast); |
| std::optional<std::string> VisitReturnStmt( |
| const clang::ReturnStmt* return_stmt); |
| std::optional<std::string> VisitDeclStmt(const clang::DeclStmt* decl_stmt); |
| std::optional<std::string> VisitUnaryOperator(const clang::UnaryOperator* op); |
| std::optional<std::string> VisitArraySubscriptExpr( |
| const clang::ArraySubscriptExpr* subscript); |
| std::optional<std::string> VisitBinaryOperator( |
| const clang::BinaryOperator* op); |
| std::optional<std::string> VisitConditionalOperator( |
| const clang::ConditionalOperator* op); |
| std::optional<std::string> VisitInitListExpr( |
| const clang::InitListExpr* init_list); |
| std::optional<std::string> VisitMaterializeTemporaryExpr( |
| const clang::MaterializeTemporaryExpr* temporary_expr); |
| std::optional<std::string> VisitMemberExpr(const clang::MemberExpr* member); |
| std::optional<std::string> VisitCXXThisExpr( |
| const clang::CXXThisExpr* this_expr); |
| std::optional<std::string> VisitCallExpr(const clang::CallExpr* call); |
| std::optional<std::string> VisitCXXConstructExpr( |
| const clang::CXXConstructExpr* construct_expr); |
| |
| private: |
| ObjectRepository& object_repository_; |
| PointsToMap& points_to_map_; |
| LifetimeConstraints& constraints_; |
| ObjectSet& single_valued_objects_; |
| const clang::FunctionDecl* func_; |
| const llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>& |
| callee_lifetimes_; |
| const DiagnosticReporter& diag_reporter_; |
| }; |
| |
| void GenerateConstraintsForSingleAssignment(const Object* oldp, |
| const Object* newp, |
| LifetimeConstraints& constraints) { |
| if (oldp->GetFuncLifetimes().has_value() && |
| newp->GetFuncLifetimes().has_value()) { |
| // The order of `newp` and `oldp` here may seem surprising. However, this |
| // can be thought of as: "I am assigning `newp` where before I had `oldp`, |
| // therefore `oldp` needs to be able to represent a call to whatever it is |
| // that `newp` represents, hence I need to generate constraints for |
| // replacing `newp` with `oldp`". At least this is why veluca@ thinks this |
| // is the correct order (the opposite order generates incorrect results). |
| constraints.join(LifetimeConstraints::ForCallableSubstitutionFull( |
| *newp->GetFuncLifetimes(), *oldp->GetFuncLifetimes())); |
| } |
| constraints.AddOutlivesConstraint(oldp->GetLifetime(), newp->GetLifetime()); |
| } |
| |
| void GenerateConstraintsForAssignmentNonRecursive( |
| const ObjectSet& old_pointees, const ObjectSet& new_pointees, |
| bool is_in_invariant_context, LifetimeConstraints& constraints) { |
| // The new pointees must always outlive the old pointees. |
| for (const Object* old : old_pointees) { |
| for (const Object* newp : new_pointees) { |
| GenerateConstraintsForSingleAssignment(old, newp, constraints); |
| } |
| } |
| |
| // If we are in an invariant context, we need to insert constraints in the |
| // opposite direction too (i.e. we need equality). |
| if (is_in_invariant_context) { |
| for (const Object* old : old_pointees) { |
| for (const Object* newp : new_pointees) { |
| GenerateConstraintsForSingleAssignment(newp, old, constraints); |
| } |
| } |
| } |
| } |
| |
| // TODO(veluca): this is quadratic. |
| void GenerateConstraintsForAssignmentRecursive( |
| const ObjectSet& pointers, const ObjectSet& new_pointees, |
| clang::QualType pointer_type, const ObjectRepository& object_repository, |
| const PointsToMap& points_to_map, bool is_in_invariant_context, |
| LifetimeConstraints& constraints, |
| llvm::DenseSet<std::pair<const Object*, const Object*>>& seen_pairs) { |
| // Check for cycles. |
| { |
| size_t num_seen_pairs = seen_pairs.size(); |
| for (auto pointer : pointers) { |
| for (auto pointee : new_pointees) { |
| seen_pairs.insert({pointer, pointee}); |
| } |
| } |
| // All done: all the pairs we have were already seen. |
| if (num_seen_pairs == seen_pairs.size()) return; |
| } |
| |
| if (pointer_type->isIncompleteType()) { |
| // Nothing we *can* do. |
| return; |
| } |
| assert(!pointer_type->isRecordType()); |
| if (!pointer_type->isPointerType() && !pointer_type->isReferenceType()) { |
| // Nothing to do. |
| return; |
| } |
| |
| ObjectSet old_pointees = points_to_map.GetPointerPointsToSet(pointers); |
| |
| GenerateConstraintsForAssignmentNonRecursive( |
| old_pointees, new_pointees, is_in_invariant_context, constraints); |
| |
| // See https://doc.rust-lang.org/nomicon/subtyping.html for an explanation of |
| // variance; here in particular, we use the fact that the pointee of a pointer |
| // is covariant if the pointer points to a const-qualified type, and invariant |
| // otherwise. |
| is_in_invariant_context = !pointer_type->getPointeeType().isConstQualified(); |
| |
| // Recurse in pointees. As the pointee might be of struct type, we need first |
| // to extract all field pointers from it. |
| struct RecursiveVisitInfo { |
| clang::QualType type; |
| ObjectSet old_pointees; |
| ObjectSet new_pointees; |
| }; |
| |
| std::vector<RecursiveVisitInfo> calls_to_make; |
| calls_to_make.push_back( |
| {pointer_type->getPointeeType(), old_pointees, new_pointees}); |
| while (!calls_to_make.empty()) { |
| RecursiveVisitInfo call = std::move(calls_to_make.back()); |
| calls_to_make.pop_back(); |
| |
| if (call.type->isIncompleteType()) { |
| // Nothing we *can* do. |
| continue; |
| } |
| |
| if (const auto* record_type = call.type->getAs<clang::RecordType>()) { |
| for (auto field : record_type->getDecl()->fields()) { |
| calls_to_make.push_back( |
| {field->getType(), |
| object_repository.GetFieldObject(call.old_pointees, field), |
| object_repository.GetFieldObject(call.new_pointees, field)}); |
| } |
| if (auto* cxxrecord = |
| clang::dyn_cast<clang::CXXRecordDecl>(record_type->getDecl())) { |
| for (const clang::CXXBaseSpecifier& base : cxxrecord->bases()) { |
| calls_to_make.push_back({base.getType(), |
| object_repository.GetBaseClassObject( |
| call.old_pointees, base.getType()), |
| object_repository.GetBaseClassObject( |
| call.new_pointees, base.getType())}); |
| } |
| } |
| } else { |
| GenerateConstraintsForAssignmentRecursive( |
| call.old_pointees, |
| points_to_map.GetPointerPointsToSet(call.new_pointees), call.type, |
| object_repository, points_to_map, is_in_invariant_context, |
| constraints, seen_pairs); |
| } |
| } |
| } |
| |
| void GenerateConstraintsForAssignment(const ObjectSet& pointers, |
| const ObjectSet& new_pointees, |
| clang::QualType pointer_type, |
| const ObjectRepository& object_repository, |
| PointsToMap& points_to_map, |
| LifetimeConstraints& constraints) { |
| llvm::DenseSet<std::pair<const Object*, const Object*>> seen_pairs; |
| // Outer-most pointers are never invariant. |
| GenerateConstraintsForAssignmentRecursive( |
| pointers, new_pointees, pointer_type, object_repository, points_to_map, |
| /*is_in_invariant_context=*/false, constraints, seen_pairs); |
| } |
| |
| void GenerateConstraintsForObjectLifetimeEquality( |
| const ObjectSet& a, const ObjectSet& b, const clang::QualType& type, |
| const PointsToMap& points_to_map, const ObjectRepository& object_repository, |
| LifetimeConstraints& constraints) { |
| GenerateConstraintsForAssignmentNonRecursive( |
| a, b, /*is_in_invariant_context=*/true, constraints); |
| if (const auto* record_type = type->getAs<clang::RecordType>()) { |
| for (auto field : record_type->getDecl()->fields()) { |
| GenerateConstraintsForObjectLifetimeEquality( |
| object_repository.GetFieldObject(a, field), |
| object_repository.GetFieldObject(b, field), field->getType(), |
| points_to_map, object_repository, constraints); |
| } |
| if (auto* cxxrecord = |
| clang::dyn_cast<clang::CXXRecordDecl>(record_type->getDecl())) { |
| for (const clang::CXXBaseSpecifier& base : cxxrecord->bases()) { |
| GenerateConstraintsForObjectLifetimeEquality( |
| object_repository.GetBaseClassObject(a, base.getType()), |
| object_repository.GetBaseClassObject(b, base.getType()), |
| base.getType(), points_to_map, object_repository, constraints); |
| } |
| } |
| } else if (!PointeeType(type).isNull()) { |
| GenerateConstraintsForObjectLifetimeEquality( |
| points_to_map.GetPointerPointsToSet(a), |
| points_to_map.GetPointerPointsToSet(b), PointeeType(type), |
| points_to_map, object_repository, constraints); |
| } |
| } |
| |
| } // namespace |
| |
| void HandlePointsToSetExtension(const ObjectSet& pointers, |
| const ObjectSet& new_pointees, |
| clang::QualType pointer_type, |
| const ObjectRepository& object_repository, |
| PointsToMap& points_to_map, |
| LifetimeConstraints& constraints) { |
| // Record types should not get to this point at all, as |
| // their initialization is done by constructor calls. |
| assert(!pointer_type->isRecordType()); |
| GenerateConstraintsForAssignment(pointers, new_pointees, pointer_type, |
| object_repository, points_to_map, |
| constraints); |
| for (const Object* pointer : pointers) { |
| points_to_map.ExtendPointerPointsToSet(pointer, new_pointees); |
| } |
| } |
| |
| void TransferInitializer(const Object* dest, clang::QualType type, |
| const ObjectRepository& object_repository, |
| const clang::Expr* init_expr, |
| TargetPointeeBehavior pointee_behavior, |
| PointsToMap& points_to_map, |
| LifetimeConstraints& constraints) { |
| type = type.getCanonicalType(); |
| if (type->isArrayType()) { |
| type = type->castAsArrayTypeUnsafe()->getElementType(); |
| } |
| |
| // Initializer lists are handled one member/field at a time. |
| if (type->isRecordType()) { |
| if (auto init_list_expr = clang::dyn_cast<clang::InitListExpr>(init_expr)) { |
| // We assume that initializers are always the semantic form of |
| // InitListExpr. |
| assert(init_list_expr->isSemanticForm()); |
| size_t init = 0; |
| for (auto f : type->getAs<clang::RecordType>()->getDecl()->fields()) { |
| assert(init < init_list_expr->getNumInits()); |
| auto field_init = init_list_expr->getInit(init); |
| ++init; |
| TransferInitializer(object_repository.GetFieldObject(dest, f), |
| f->getType(), object_repository, field_init, |
| pointee_behavior, points_to_map, constraints); |
| } |
| return; |
| } |
| } |
| |
| if (type->isPointerType() || type->isReferenceType() || |
| type->isStructureOrClassType()) { |
| ObjectSet init_points_to = points_to_map.GetExprObjectSet(init_expr); |
| if (pointee_behavior == TargetPointeeBehavior::kKeep) { |
| // It's important to use "Extend" (not "Set") here because we process |
| // initializers for member variables only _after_ the dataflow analysis |
| // has run. |
| HandlePointsToSetExtension({dest}, init_points_to, type, |
| object_repository, points_to_map, constraints); |
| } else { |
| points_to_map.SetPointerPointsToSet(dest, init_points_to); |
| } |
| } |
| } |
| |
| LifetimeLattice LifetimeAnalysis::initialElement() { |
| return LifetimeLattice(object_repository_.InitialPointsToMap(), |
| object_repository_.InitialSingleValuedObjects()); |
| } |
| |
| std::string LifetimeAnalysis::ToString(const LifetimeLattice& state) { |
| return state.ToString(); |
| } |
| |
| bool LifetimeAnalysis::IsEqual(const LifetimeLattice& state1, |
| const LifetimeLattice& state2) { |
| return state1 == state2; |
| } |
| |
| void LifetimeAnalysis::transfer(const clang::CFGElement& elt, |
| LifetimeLattice& state, |
| clang::dataflow::Environment& /*environment*/) { |
| if (state.IsError()) return; |
| |
| auto cfg_stmt = elt.getAs<clang::CFGStmt>(); |
| if (!cfg_stmt) return; |
| auto stmt = cfg_stmt->getStmt(); |
| |
| TransferStmtVisitor visitor(object_repository_, state.PointsTo(), |
| state.Constraints(), state.SingleValuedObjects(), |
| func_, callee_lifetimes_, diag_reporter_); |
| if (std::optional<std::string> err = |
| visitor.Visit(const_cast<clang::Stmt*>(stmt))) { |
| state = LifetimeLattice(*err); |
| } |
| } |
| |
| namespace { |
| |
| std::optional<std::string> TransferStmtVisitor::VisitExpr( |
| const clang::Expr* expr) { |
| // Ensure that we don't attempt to analyze code that contains errors. |
| // This is triggered by TypoExpr and RecoveryExpr, but rather than handling |
| // these particular expression types individually, we just check |
| // Expr::containsErrors(). |
| if (expr->containsErrors()) { |
| return "encountered an expression containing errors"; |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitDeclRefExpr( |
| const clang::DeclRefExpr* decl_ref) { |
| auto* decl = decl_ref->getDecl(); |
| if (!clang::isa<clang::VarDecl>(decl) && |
| !clang::isa<clang::FunctionDecl>(decl)) { |
| return std::nullopt; |
| } |
| |
| const Object* object = object_repository_.GetDeclObject(decl); |
| |
| assert(decl_ref->isGLValue() || decl_ref->getType()->isBuiltinType()); |
| |
| clang::QualType type = decl->getType().getCanonicalType(); |
| |
| if (type->isReferenceType()) { |
| points_to_map_.SetExprObjectSet( |
| decl_ref, points_to_map_.GetPointerPointsToSet(object)); |
| } else { |
| points_to_map_.SetExprObjectSet(decl_ref, {object}); |
| } |
| |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitStringLiteral( |
| const clang::StringLiteral* strlit) { |
| points_to_map_.SetExprObjectSet( |
| strlit, {object_repository_.GetStringLiteralObject()}); |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitCastExpr( |
| const clang::CastExpr* cast) { |
| switch (cast->getCastKind()) { |
| case clang::CK_LValueToRValue: { |
| if (cast->getType()->isPointerType()) { |
| // Converting from a glvalue to a prvalue means that we need to perform |
| // a dereferencing operation because the objects associated with |
| // glvalues and prvalues have different meanings: |
| // - A glvalue is associated with the object identified by the glvalue. |
| // - A prvalue is only associated with an object if the prvalue is of |
| // pointer type; the object it is associated with is the object the |
| // pointer points to. |
| // See also documentation for PointsToMap. |
| ObjectSet points_to = points_to_map_.GetPointerPointsToSet( |
| points_to_map_.GetExprObjectSet(cast->getSubExpr())); |
| points_to_map_.SetExprObjectSet(cast, points_to); |
| } |
| break; |
| } |
| case clang::CK_NullToPointer: { |
| points_to_map_.SetExprObjectSet(cast, {}); |
| break; |
| } |
| // These casts are just no-ops from a Object point of view. |
| case clang::CK_FunctionToPointerDecay: |
| case clang::CK_BuiltinFnToFnPtr: |
| case clang::CK_ArrayToPointerDecay: |
| case clang::CK_UserDefinedConversion: |
| // Note on CK_UserDefinedConversion: The actual conversion happens in a |
| // CXXMemberCallExpr that is a subexpression of this CastExpr. The |
| // CK_UserDefinedConversion is just used to mark the fact that this is a |
| // user-defined conversion; it's therefore a no-op for our purposes. |
| case clang::CK_NoOp: { |
| clang::QualType type = cast->getType().getCanonicalType(); |
| if (type->isPointerType() || cast->isGLValue()) { |
| points_to_map_.SetExprObjectSet( |
| cast, points_to_map_.GetExprObjectSet(cast->getSubExpr())); |
| } |
| break; |
| } |
| case clang::CK_DerivedToBase: |
| case clang::CK_UncheckedDerivedToBase: |
| case clang::CK_BaseToDerived: |
| case clang::CK_Dynamic: { |
| // These need to be mapped to what the subexpr points to. |
| // (Simple cases just work okay with this; may need to be revisited when |
| // we add more inheritance support.) |
| ObjectSet points_to = points_to_map_.GetExprObjectSet(cast->getSubExpr()); |
| points_to_map_.SetExprObjectSet(cast, points_to); |
| break; |
| } |
| case clang::CK_BitCast: |
| case clang::CK_LValueBitCast: |
| case clang::CK_IntegralToPointer: { |
| // We don't support analyzing functions that perform a reinterpret_cast. |
| diag_reporter_( |
| func_->getBeginLoc(), |
| "cannot infer lifetimes because function uses a type-unsafe cast", |
| clang::DiagnosticIDs::Warning); |
| diag_reporter_(cast->getBeginLoc(), "type-unsafe cast occurs here", |
| clang::DiagnosticIDs::Note); |
| return "type-unsafe cast prevents analysis"; |
| } |
| default: { |
| if (cast->isGLValue() || |
| cast->getType().getCanonicalType()->isPointerType()) { |
| llvm::errs() << "Unknown cast type:\n"; |
| cast->dump(); |
| // No-noop casts of pointer types are not handled yet. |
| llvm::report_fatal_error("unknown cast type encountered"); |
| } |
| } |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitReturnStmt( |
| const clang::ReturnStmt* return_stmt) { |
| clang::QualType return_type = func_->getReturnType(); |
| // We only need to handle pointers and references. |
| // For record types, initialization of the return value has already been |
| // handled in VisitCXXConstructExpr() or VisitInitListExpr(), so nothing |
| // to do here. |
| if (!return_type->isPointerType() && !return_type->isReferenceType()) { |
| return std::nullopt; |
| } |
| |
| const clang::Expr* ret_expr = return_stmt->getRetValue(); |
| // This occurs when computing `ret_expr`s result includes creating temporary |
| // objects with destructors. We want to find the value to be returned inside |
| // the ExprWithCleanups. |
| // |
| // The PointsToMap::GetExprObjectSet() function could do this but it doesn't |
| // understand the context from which it is being called. This operation needs |
| // to be done only in cases where we are leaving scope - that is, the return |
| // statement. And the return statement also needs to look for initializers in |
| // its sub expressions, after looking inside ExprWithCleanups. |
| // |
| // That means GetExprObjectSet() would need to also look for initializers but |
| // we don't want to do this on every call to GetExprObjectSet(). |
| if (auto cleanups = clang::dyn_cast<clang::ExprWithCleanups>(ret_expr)) { |
| ret_expr = cleanups->getSubExpr(); |
| } |
| |
| ObjectSet expr_points_to = points_to_map_.GetExprObjectSet(ret_expr); |
| GenerateConstraintsForAssignment( |
| {object_repository_.GetReturnObject()}, expr_points_to, return_type, |
| object_repository_, points_to_map_, constraints_); |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitDeclStmt( |
| const clang::DeclStmt* decl_stmt) { |
| for (const clang::Decl* decl : decl_stmt->decls()) { |
| if (const auto* var_decl = clang::dyn_cast<clang::VarDecl>(decl)) { |
| const Object* var_object = object_repository_.GetDeclObject(var_decl); |
| |
| // Don't need to record initializers because initialization has already |
| // happened in VisitCXXConstructExpr(), VisitInitListExpr(), or |
| // VisitCallExpr(). |
| if (var_decl->hasInit() && !var_decl->getType()->isRecordType()) { |
| TransferInitializer(var_object, var_decl->getType(), object_repository_, |
| var_decl->getInit(), TargetPointeeBehavior::kIgnore, |
| points_to_map_, constraints_); |
| } |
| } |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitUnaryOperator( |
| const clang::UnaryOperator* op) { |
| if (!op->isGLValue() && !op->getType()->isPointerType() && |
| !op->getType()->isArrayType()) { |
| return std::nullopt; |
| } |
| |
| ObjectSet sub_points_to = points_to_map_.GetExprObjectSet(op->getSubExpr()); |
| |
| // Maybe surprisingly, the code here doesn't do any actual address-taking or |
| // dereferencing. |
| // This is because AddrOf and Deref really only do a reinterpretation: |
| // - AddrOf reinterprets a glvalue of type T as a prvalue of type T* |
| // - Deref reinterprets an prvalue of type T* as a glvalue of type T |
| // (See also the assertions below.) |
| // The actual dereferencing happens in the LValueToRValue CastExpr, |
| // see TransferCastExpr(). |
| |
| switch (op->getOpcode()) { |
| case clang::UO_AddrOf: |
| assert(!op->isGLValue()); |
| assert(op->getSubExpr()->isGLValue()); |
| points_to_map_.SetExprObjectSet(op, sub_points_to); |
| break; |
| |
| case clang::UO_Deref: |
| assert(op->isGLValue()); |
| assert(!op->getSubExpr()->isGLValue()); |
| points_to_map_.SetExprObjectSet(op, sub_points_to); |
| break; |
| |
| case clang::UO_PostInc: |
| case clang::UO_PostDec: |
| assert(!op->isGLValue()); |
| assert(op->getSubExpr()->isGLValue()); |
| points_to_map_.SetExprObjectSet( |
| op, points_to_map_.GetPointerPointsToSet(sub_points_to)); |
| break; |
| |
| case clang::UO_PreInc: |
| case clang::UO_PreDec: |
| assert(op->isGLValue()); |
| assert(op->getSubExpr()->isGLValue()); |
| points_to_map_.SetExprObjectSet(op, sub_points_to); |
| break; |
| |
| default: |
| break; |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitArraySubscriptExpr( |
| const clang::ArraySubscriptExpr* subscript) { |
| // For our purposes here, a subscripting operation is equivalent to a |
| // dereference on its base - we don't make a distinction between different |
| // lifetimes in an array. This effectively merges the points-to sets of all |
| // elements in the array. See [/docs/design/lifetimes_static_analysis.md](/docs/design/lifetimes_static_analysis.md) for why we |
| // don't track individual array elements. |
| |
| ObjectSet sub_points_to = |
| points_to_map_.GetExprObjectSet(subscript->getBase()); |
| |
| assert(subscript->isGLValue()); |
| assert(!subscript->getBase()->isGLValue()); |
| points_to_map_.SetExprObjectSet(subscript, sub_points_to); |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitBinaryOperator( |
| const clang::BinaryOperator* op) { |
| switch (op->getOpcode()) { |
| case clang::BO_Assign: { |
| assert(op->getLHS()->isGLValue()); |
| ObjectSet lhs_points_to = points_to_map_.GetExprObjectSet(op->getLHS()); |
| points_to_map_.SetExprObjectSet(op, lhs_points_to); |
| // Because of how we handle reference-like structs, a member access to a |
| // non-reference-like field in a struct might still produce lifetimes. We |
| // don't want to change points-to sets in those cases. |
| if (!op->getLHS()->getType()->isPointerType()) break; |
| ObjectSet rhs_points_to = points_to_map_.GetExprObjectSet(op->getRHS()); |
| // We can overwrite (instead of extend) the destination points-to-set |
| // only in very specific circumstances: |
| // - We need to know unambiguously what the LHS refers to, so that we |
| // know we're definitely writing to a particular object, and |
| // - That destination object needs to be "single-valued" (see docstring of |
| // LifetimeLattice::SingleValuedObjects for the definition of this |
| // term). |
| if (lhs_points_to.size() == 1 && |
| single_valued_objects_.Contains(*lhs_points_to.begin())) { |
| // Replacing the points-to-set entirely does not generate any |
| // constraints. |
| points_to_map_.SetPointerPointsToSet(lhs_points_to, rhs_points_to); |
| } else { |
| HandlePointsToSetExtension(lhs_points_to, rhs_points_to, |
| op->getLHS()->getType(), object_repository_, |
| points_to_map_, constraints_); |
| } |
| break; |
| } |
| |
| case clang::BO_Add: |
| case clang::BO_Sub: { |
| // Pointer arithmetic. |
| // We are only interested in the case in which exactly one of the two |
| // operands is a pointer (in particular we want to exclude int* - int*). |
| if (op->getLHS()->getType()->isPointerType() ^ |
| op->getRHS()->getType()->isPointerType()) { |
| if (op->getLHS()->getType()->isPointerType()) { |
| points_to_map_.SetExprObjectSet( |
| op, points_to_map_.GetExprObjectSet(op->getLHS())); |
| } else { |
| points_to_map_.SetExprObjectSet( |
| op, points_to_map_.GetExprObjectSet(op->getRHS())); |
| } |
| } |
| break; |
| } |
| |
| default: |
| break; |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitConditionalOperator( |
| const clang::ConditionalOperator* op) { |
| clang::QualType type = op->getType().getCanonicalType(); |
| |
| if (op->isGLValue() || type->isPointerType()) { |
| // It is possible that either of the expressions may not have an ObjectSet |
| // if the node is pruned as it is considered unreachable. |
| assert(points_to_map_.ExprHasObjectSet(op->getTrueExpr()) || |
| points_to_map_.ExprHasObjectSet(op->getFalseExpr())); |
| ObjectSet points_to_true = |
| points_to_map_.ExprHasObjectSet(op->getTrueExpr()) |
| ? points_to_map_.GetExprObjectSet(op->getTrueExpr()) |
| : ObjectSet(); |
| ObjectSet points_to_false = |
| points_to_map_.ExprHasObjectSet(op->getFalseExpr()) |
| ? points_to_map_.GetExprObjectSet(op->getFalseExpr()) |
| : ObjectSet(); |
| points_to_map_.SetExprObjectSet(op, points_to_true.Union(points_to_false)); |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitInitListExpr( |
| const clang::InitListExpr* init_list) { |
| if (init_list->isSyntacticForm()) { |
| // We are only interested in the semantic form, which is fully realized, |
| // and is the one considered to be the initializer. |
| return std::nullopt; |
| } |
| if (IsInitExprInitializingARecordObject(init_list)) { |
| if (init_list->isTransparent()) { |
| // A transparent initializer list does nothing, the actual initializer |
| // terminating expression is within, and has already transferred lifetimes |
| // up to the object being initialized. |
| return std::nullopt; |
| } |
| // The object set for each field should be pointing to the initializers. |
| const Object* init_object = object_repository_.GetResultObject(init_list); |
| TransferInitializer(init_object, init_list->getType(), object_repository_, |
| init_list, TargetPointeeBehavior::kKeep, points_to_map_, |
| constraints_); |
| } else { |
| // If the InitListExpr is not initializing a record object, we assume it's |
| // initializing an array or a reference and hence associate the InitListExpr |
| // with the union of the points-to sets of the initializers (as the analysis |
| // is array-insensitive). |
| ObjectSet targets; |
| for (clang::Expr* expr : init_list->inits()) { |
| // If we are constructing an initializer list of non-pointer types, we |
| // don't need to do anything here. Note that initializer list elements |
| // must all have the same type in this case. |
| if (PointeeType(expr->getType()).isNull() && !expr->isGLValue()) { |
| return std::nullopt; |
| } |
| targets.Add(points_to_map_.GetExprObjectSet(expr)); |
| } |
| points_to_map_.SetExprObjectSet(init_list, std::move(targets)); |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitMaterializeTemporaryExpr( |
| const clang::MaterializeTemporaryExpr* temporary_expr) { |
| const Object* temp_object = |
| object_repository_.GetTemporaryObject(temporary_expr); |
| points_to_map_.SetExprObjectSet(temporary_expr, {temp_object}); |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitMemberExpr( |
| const clang::MemberExpr* member) { |
| ObjectSet struct_points_to = |
| points_to_map_.GetExprObjectSet(member->getBase()); |
| |
| if (const auto* method = |
| clang::dyn_cast<clang::CXXMethodDecl>(member->getMemberDecl())) { |
| // It doesn't really make sense to associate an object set with a non-static |
| // member function. |
| // If the member function is being called, we're not interested in its |
| // "value" anyway. If the non-static member function is used outside of a |
| // function call, then, it's a pointer-to-member, but those aren't |
| // really pointers anyway, and we'll need special treatment for them. |
| if (method->isStatic()) { |
| points_to_map_.SetExprObjectSet( |
| member, {object_repository_.GetDeclObject(method)}); |
| } |
| return std::nullopt; |
| } |
| |
| auto field = clang::dyn_cast<clang::FieldDecl>(member->getMemberDecl()); |
| if (field == nullptr) { |
| llvm::report_fatal_error("indirect member access is not supported yet"); |
| } |
| ObjectSet expr_points_to = |
| object_repository_.GetFieldObject(struct_points_to, field); |
| if (field->getType()->isReferenceType()) { |
| expr_points_to = points_to_map_.GetPointerPointsToSet(expr_points_to); |
| } |
| points_to_map_.SetExprObjectSet(member, expr_points_to); |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitCXXThisExpr( |
| const clang::CXXThisExpr* this_expr) { |
| std::optional<const Object*> this_object = object_repository_.GetThisObject(); |
| assert(this_object.has_value()); |
| points_to_map_.SetExprObjectSet(this_expr, ObjectSet{this_object.value()}); |
| return std::nullopt; |
| } |
| |
| namespace { |
| void ConstrainFunctionLifetimesForCall( |
| const FunctionLifetimes& callee_lifetimes, |
| const FunctionLifetimes& placeholder_lifetimes, |
| LifetimeConstraints& constraints) { |
| // We handle function calls as follows: |
| // - We create a placeholder FunctionLifetimes for each call location, meant |
| // to indicate the concrete lifetimes that the callee is instantiated with for |
| // that specific call. |
| // - When we analyze the call, we constrain the concrete lifetimes so that |
| // they are compatible with the lifetimes of the arguments. |
| // - We then constrain the placeholder lifetimes so that the actual callee |
| // could be used in place of the placeholder callee. |
| // - As a consequence, the lifetimes of the return object are also constrained |
| // correctly (ie. in such a way that they are compatible with the callee and |
| // the call arguments). |
| // TODO(veluca): this code assumes that the lifetime of variables cannot |
| // change across function call boundaries. It is not an intrinsic limitation |
| // (could potentially be resolved by updating the PointsToMap after the fact, |
| // or - even better - by converting the entire CFG into SSA form), but for |
| // simplicity we are leaving things as they are for now; if real-world usage |
| // shows this to be an important limitation, we should revisit this decision. |
| constraints.join(LifetimeConstraints::ForCallableSubstitutionFull( |
| callee_lifetimes, placeholder_lifetimes)); |
| } |
| } // namespace |
| |
| std::optional<std::string> TransferStmtVisitor::VisitCallExpr( |
| const clang::CallExpr* call) { |
| struct CalleeInfo { |
| bool is_member_operator; |
| FunctionLifetimes lifetimes; |
| // Type of the function being called. Note that this might not know anything |
| // about the `this` argument for non-static methods. |
| const clang::FunctionProtoType* type; |
| }; |
| |
| llvm::SmallVector<CalleeInfo, 1> callees; |
| |
| auto add_callee_from_decl = |
| [&callees, call, |
| this](const clang::FunctionDecl* decl) -> std::optional<std::string> { |
| const FunctionLifetimesOrError& callee_lifetimes_or_error = |
| GetFunctionLifetimes(decl, callee_lifetimes_); |
| |
| if (!std::holds_alternative<FunctionLifetimes>(callee_lifetimes_or_error)) { |
| // Note: It is possible that this does not have an entry if the function |
| // is not analyzed (because its body is not defined in this TU). If this |
| // happens, we currently bail without analyzing further. |
| return "No lifetimes for callee '" + decl->getNameAsString() + "': " + |
| std::get<FunctionAnalysisError>(callee_lifetimes_or_error).message; |
| } |
| FunctionLifetimes callee_lifetimes = |
| std::get<FunctionLifetimes>(callee_lifetimes_or_error); |
| |
| bool is_member_operator = clang::isa<clang::CXXOperatorCallExpr>(call) && |
| clang::isa<clang::CXXMethodDecl>(decl); |
| callees.push_back( |
| CalleeInfo{is_member_operator, callee_lifetimes, |
| decl->getType()->getAs<clang::FunctionProtoType>()}); |
| return std::nullopt; |
| }; |
| |
| const clang::FunctionDecl* direct_callee = call->getDirectCallee(); |
| if (direct_callee) { |
| // This code path is needed for non-static member functions, as those don't |
| // have an `Object` for their callees. |
| if (auto err = add_callee_from_decl(direct_callee); err.has_value()) { |
| return err; |
| } |
| } else { |
| const clang::Expr* callee = call->getCallee(); |
| for (const auto& object : points_to_map_.GetExprObjectSet(callee)) { |
| if (const std::optional<FunctionLifetimes>& func_lifetimes = |
| object->GetFuncLifetimes(); |
| func_lifetimes.has_value()) { |
| callees.push_back( |
| {.is_member_operator = false, |
| .lifetimes = *func_lifetimes, |
| .type = object->Type()->getAs<clang::FunctionProtoType>()}); |
| } |
| } |
| } |
| |
| for (const CalleeInfo& callee : callees) { |
| ConstrainFunctionLifetimesForCall( |
| callee.lifetimes, object_repository_.GetCallExprVirtualLifetimes(call), |
| constraints_); |
| |
| for (size_t i = callee.is_member_operator ? 1 : 0; i < call->getNumArgs(); |
| i++) { |
| // We can't just use SetPointerPointsToSet here because call->getArg(i) |
| // might not have an ObjectSet (for example for integer constants); it |
| // also may be needed for struct initialization. |
| // Note that we don't need to worry about possibly extending the |
| // PointsToSet more than needed, as dataflow analysis relies on points-to |
| // sets never shrinking. |
| TransferInitializer( |
| object_repository_.GetCallExprArgumentObject(call, i), |
| callee.type->getParamType(callee.is_member_operator ? i - 1 : i), |
| object_repository_, call->getArg(i), TargetPointeeBehavior::kKeep, |
| points_to_map_, constraints_); |
| } |
| |
| std::optional<ObjectSet> this_object_set; |
| if (callee.is_member_operator) { |
| this_object_set = points_to_map_.GetExprObjectSet(call->getArg(0)); |
| } else if (const auto* member_call = |
| clang::dyn_cast<clang::CXXMemberCallExpr>(call)) { |
| this_object_set = points_to_map_.GetExprObjectSet( |
| member_call->getImplicitObjectArgument()); |
| } |
| if (this_object_set.has_value()) { |
| const Object* this_ptr = object_repository_.GetCallExprThisPointer(call); |
| HandlePointsToSetExtension({this_ptr}, *this_object_set, this_ptr->Type(), |
| object_repository_, points_to_map_, |
| constraints_); |
| } |
| } |
| |
| if (IsInitExprInitializingARecordObject(call)) { |
| const Object* init_object = object_repository_.GetResultObject(call); |
| GenerateConstraintsForObjectLifetimeEquality( |
| {init_object}, {object_repository_.GetCallExprRetObject(call)}, |
| init_object->Type(), points_to_map_, object_repository_, constraints_); |
| } else { |
| ObjectSet ret_pts = points_to_map_.GetPointerPointsToSet( |
| object_repository_.GetCallExprRetObject(call)); |
| // SetExprObjectSet will assert-fail if `call` does not have a type that can |
| // have an object set; this `if` guards against that. |
| if (!ret_pts.empty()) { |
| points_to_map_.SetExprObjectSet(call, ret_pts); |
| } |
| } |
| return std::nullopt; |
| } |
| |
| std::optional<std::string> TransferStmtVisitor::VisitCXXConstructExpr( |
| const clang::CXXConstructExpr* construct_expr) { |
| const clang::CXXConstructorDecl* constructor = |
| construct_expr->getConstructor(); |
| |
| assert(callee_lifetimes_.count(constructor->getCanonicalDecl())); |
| const FunctionLifetimesOrError& callee_lifetimes_or_error = |
| callee_lifetimes_.lookup(constructor->getCanonicalDecl()); |
| if (!std::holds_alternative<FunctionLifetimes>(callee_lifetimes_or_error)) { |
| return "No lifetimes for constructor " + constructor->getNameAsString(); |
| } |
| const FunctionLifetimes& callee_lifetimes = |
| std::get<FunctionLifetimes>(callee_lifetimes_or_error); |
| |
| ConstrainFunctionLifetimesForCall( |
| callee_lifetimes, |
| object_repository_.GetCallExprVirtualLifetimes(construct_expr), |
| constraints_); |
| |
| // We check <= instead of == because of default arguments. |
| assert(construct_expr->getNumArgs() <= constructor->getNumParams()); |
| |
| for (size_t i = 0; i < construct_expr->getNumArgs(); i++) { |
| TransferInitializer( |
| object_repository_.GetCXXConstructExprArgumentObject(construct_expr, i), |
| constructor->getParamDecl(i)->getType(), object_repository_, |
| construct_expr->getArg(i), TargetPointeeBehavior::kKeep, points_to_map_, |
| constraints_); |
| } |
| |
| // Handle the `this` parameter, which should point to the object getting |
| // initialized. |
| HandlePointsToSetExtension( |
| {object_repository_.GetCXXConstructExprThisPointer(construct_expr)}, |
| {object_repository_.GetResultObject(construct_expr)}, |
| constructor->getThisType(), object_repository_, points_to_map_, |
| constraints_); |
| |
| return std::nullopt; |
| } |
| |
| } // namespace |
| |
| } // namespace lifetimes |
| } // namespace tidy |
| } // namespace clang |