[llvm-branch-commits] [clang] [LifetimeSafety][NFC] Add field-labeled child edges to OriginNode and generalize subtree walks (PR #201510)

Zhijie Wang via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Tue Jun 9 22:55:05 PDT 2026


================
@@ -66,47 +66,78 @@ struct Origin {
   }
 };
 
-/// A list of origins representing levels of indirection for pointer-like types.
+/// A tree of origins representing the structure of a pointer-like or
+/// record type.
 ///
-/// Each node in the list contains an OriginID representing a level of
-/// indirection. The list structure captures the multi-level nature of
-/// pointer and reference types in the lifetime analysis.
+/// Each node carries an OriginID and is connected to children via labeled
+/// edges: either a pointee edge (one level of pointer/reference indirection)
+/// or a field edge (a named field of a record). Pointer-like types form a
+/// pointee chain; record types fan out via field edges.
 ///
 /// Examples:
-///   - For `int& x`, the list has size 2:
+///   - For `int& x`, the chain has length 2:
 ///     * Outer: origin for the reference storage itself (the lvalue `x`)
 ///     * Inner: origin for what `x` refers to
 ///
-///   - For `int* p`, the list has size 2:
+///   - For `int* p`, the chain has length 2:
 ///     * Outer: origin for the pointer variable `p`
 ///     * Inner: origin for what `p` points to
 ///
-///   - For `View v` (where View is gsl::Pointer), the list has size 2:
+///   - For `View v` (where View is gsl::Pointer), the chain has length 2:
 ///     * Outer: origin for the view object itself
 ///     * Inner: origin for what the view refers to
 ///
-///   - For `int** pp`, the list has size 3:
+///   - For `int** pp`, the chain has length 3:
 ///     * Outer: origin for `pp` itself
 ///     * Inner: origin for `*pp` (what `pp` points to)
 ///     * Inner->Inner: origin for `**pp` (what `*pp` points to)
 ///
-/// The list structure enables the analysis to track how loans flow through
-/// different levels of indirection when assignments and dereferences occur.
-///
-/// TODO: Currently list-shaped (each node has at most one pointee child).
-/// Will become tree-shaped once field children are added to support
-/// origin trees for records whose fields have origins.
+/// The structure enables the analysis to track how loans flow through
+/// levels of indirection and across record fields when assignments and
+/// dereferences occur.
 class OriginNode {
 public:
+  /// A labeled edge from this node to a child. The label distinguishes how
+  /// the child is reached: a null `FD` means a pointee edge (one level of
+  /// pointer/reference indirection); a non-null `FD` means a field edge
+  /// (the named field of a record). Putting the label on the edge lets
+  /// one child node play different roles per parent. For example, the subtree
+  /// for `s`'s `v` field is reached from `s`'s record (FD=v) and from
+  /// the lvalue outer built for the MemberExpr `s.v` (FD=null).
+  struct Edge {
+    const FieldDecl *FD;
+    OriginNode *Child;
+  };
+
   OriginNode(OriginID OID) : OID(OID) {}
 
+  OriginID getOriginID() const { return OID; }
+
+  llvm::ArrayRef<Edge> children() const { return Children; }
+
   OriginNode *getPointeeChild() const {
-    return Children.empty() ? nullptr : Children[0];
+    for (const Edge &E : Children)
+      if (!E.FD)
+        return E.Child;
+    return nullptr;
   }
 
-  OriginID getOriginID() const { return OID; }
+  OriginNode *getFieldChild(const FieldDecl *F) const {
+    assert(F);
+    for (const Edge &E : Children)
+      if (E.FD == F)
+        return E.Child;
+    return nullptr;
+  }
+
+  OriginNode *getFieldChildInChain(const FieldDecl *FD) const {
----------------
aeft wrote:

Yes, this method is not very clear. It actually goes at most 2 hops. I introduced it because the field can sit at different depths depending on the base: (1) p->v walks two hops, the DRE outer then the pointer indirection; (2) s.v walks one hop, just the DRE outer.

I use it in `getOrCreateNode` when resolving a `MemberExpr` (s.v / p->v) (in PR #195603): we walk down the base to the record and pick the field.

One thing I noticed: the pointee edge currently mixes two semantics, (1) pointer/reference indirection and (2) the lvalue storage wrap (like the one above for s.v). We already distinguish pointee edges from field edges via FD. I'm not sure it's worth it here, but should we make it clearer by introducing an EdgeKind enum, so we'd have three kinds: pointer indirection, storage wrap, and field?

https://github.com/llvm/llvm-project/pull/201510


More information about the llvm-branch-commits mailing list