[debugger] Expand inline frames.

When an instruction pointer is inside an inline frame, this patch will
expand the inline chain to multiple stack frames. The result is that the
list in the "backtrace" and "frame" commands much better represents the
programmer's understanding of the code.

Thread control commands have not yet been updated to understand these
inline frames. As a result, step, next, and finish will do unexpected
things when these inline frames are encountered. However, they didn't
work the way users expected before this patch either so this is not a
notable regression. These commands will be updated in later passes.

Add inline function call location to the Function symbol.

TEST=unit,manual

Change-Id: Ib709c62a482605387dfc0b2e3591b282750b2126
diff --git a/bin/zxdb/client/BUILD.gn b/bin/zxdb/client/BUILD.gn
index 4fb691c..fd6f8e5 100644
--- a/bin/zxdb/client/BUILD.gn
+++ b/bin/zxdb/client/BUILD.gn
@@ -191,6 +191,7 @@
     "setting_schema_unittest.cc",
     "setting_store_unittest.cc",
     "setting_value_unittest.cc",
+    "stack_unittest.cc",
     "step_over_thread_controller_unittest.cc",
     "step_thread_controller_unittest.cc",
     "string_util_unittest.cc",
diff --git a/bin/zxdb/client/frame_impl_unittest.cc b/bin/zxdb/client/frame_impl_unittest.cc
index 64cd046..4ca90c7 100644
--- a/bin/zxdb/client/frame_impl_unittest.cc
+++ b/bin/zxdb/client/frame_impl_unittest.cc
@@ -70,10 +70,15 @@
     FXL_NOTREACHED();  // All frames are available.
   }
   std::unique_ptr<Frame> MakeFrameForStack(
-      const debug_ipc::StackFrame& input) override {
+      const debug_ipc::StackFrame& input,
+      Location location) override {
     FXL_NOTREACHED();  // Should not get called since we provide stack frames.
     return std::unique_ptr<Frame>();
   }
+  Location GetSymbolizedLocationForStackFrame(
+      const debug_ipc::StackFrame& input) override {
+    return Location(Location::State::kSymbolized, input.ip);
+  }
 
   std::string thread_name_ = "test thread";
   Process* process_;
diff --git a/bin/zxdb/client/stack.cc b/bin/zxdb/client/stack.cc
index 542d1b4..28e6c18 100644
--- a/bin/zxdb/client/stack.cc
+++ b/bin/zxdb/client/stack.cc
@@ -8,11 +8,89 @@
 
 #include "garnet/bin/zxdb/client/frame.h"
 #include "garnet/bin/zxdb/client/frame_fingerprint.h"
+#include "garnet/bin/zxdb/expr/expr_eval_context.h"
+#include "garnet/bin/zxdb/symbols/function.h"
 #include "garnet/lib/debug_ipc/records.h"
 #include "lib/fxl/logging.h"
+#include "lib/fxl/macros.h"
 
 namespace zxdb {
 
+namespace {
+
+// Implementation of Frame for inlined frames. Inlined frames have a different
+// location in the source code, but refer to the underlying physical frame for
+// most data.
+class InlineFrame final : public Frame {
+ public:
+  // The physical_frame must outlive this class. Normally both are owned by the
+  // Stack and have the same lifetime.
+  InlineFrame(Frame* physical_frame, Location loc)
+      : Frame(physical_frame->session()),
+        physical_frame_(physical_frame),
+        location_(loc) {}
+  ~InlineFrame() override = default;
+
+  // Frame implementation.
+  Thread* GetThread() const override { return physical_frame_->GetThread(); }
+  bool IsInline() const override { return true; }
+  const Frame* GetPhysicalFrame() const override { return physical_frame_; }
+  const Location& GetLocation() const override { return location_; }
+  uint64_t GetAddress() const override { return location_.address(); }
+  uint64_t GetBasePointerRegister() const override {
+    return physical_frame_->GetBasePointerRegister();
+  }
+  std::optional<uint64_t> GetBasePointer() const override {
+    return physical_frame_->GetBasePointer();
+  }
+  void GetBasePointerAsync(std::function<void(uint64_t bp)> cb) override {
+    return physical_frame_->GetBasePointerAsync(std::move(cb));
+  }
+  uint64_t GetStackPointer() const override {
+    return physical_frame_->GetStackPointer();
+  }
+  fxl::RefPtr<SymbolDataProvider> GetSymbolDataProvider() const override {
+    return physical_frame_->GetSymbolDataProvider();
+  }
+  fxl::RefPtr<ExprEvalContext> GetExprEvalContext() const override {
+    return physical_frame_->GetExprEvalContext();
+  }
+
+ private:
+  Frame* physical_frame_;  // Non-owning.
+  Location location_;
+
+  FXL_DISALLOW_COPY_AND_ASSIGN(InlineFrame);
+};
+
+// Returns a fixed-up location referring to an indexed element in an inlined
+// function call chain. This also handles the case where there are no inline
+// calls and the function is the only one (this returns the same location).
+//
+// The main_location is the location returned by symbol lookup for the
+// current address.
+Location LocationForInlineFrameChain(
+    const std::vector<const Function*>& inline_chain, size_t chain_index,
+    const Location& main_location) {
+  // The file/line is the call location of the next (into the future) inlined
+  // function. Fall back on the file/line from the main lookup.
+  const FileLine* new_line = &main_location.file_line();
+  int new_column = main_location.column();
+  if (chain_index > 0) {
+    const Function* next_call = inline_chain[chain_index - 1];
+    if (next_call->call_line().is_valid()) {
+      new_line = &next_call->call_line();
+      new_column = 0;  // DWARF doesn't contain inline call column.
+    }
+  }
+
+  return Location(main_location.address(), *new_line, new_column,
+                  main_location.symbol_context(),
+                  LazySymbol(inline_chain[chain_index]));
+}
+
+}  // namespace
+
 Stack::Stack(Delegate* delegate) : delegate_(delegate) {}
 
 Stack::~Stack() = default;
@@ -48,29 +126,9 @@
 
 void Stack::SetFrames(debug_ipc::ThreadRecord::StackAmount amount,
                       const std::vector<debug_ipc::StackFrame>& frames) {
-  // The goal is to preserve pointer identity for frames. If a frame is the
-  // same, weak pointers to it should remain valid.
-  using IpSp = std::pair<uint64_t, uint64_t>;
-  std::map<IpSp, std::unique_ptr<Frame>> existing;
-  for (auto& cur : frames_) {
-    IpSp key(cur->GetAddress(), cur->GetStackPointer());
-    existing[key] = std::move(cur);
-  }
-
   frames_.clear();
-  for (size_t i = 0; i < frames.size(); i++) {
-    IpSp key(frames[i].ip, frames[i].sp);
-    auto found = existing.find(key);
-    if (found == existing.end()) {
-      // New frame we haven't seen.
-      frames_.push_back(delegate_->MakeFrameForStack(frames[i]));
-    } else {
-      // Can re-use existing pointer.
-      frames_.push_back(std::move(found->second));
-      existing.erase(found);
-    }
-  }
-
+  for (const debug_ipc::StackFrame& frame : frames)
+    AppendFrame(frame);
   has_all_frames_ = amount == debug_ipc::ThreadRecord::StackAmount::kFull;
 }
 
@@ -90,4 +148,49 @@
   return true;
 }
 
+void Stack::AppendFrame(const debug_ipc::StackFrame& record) {
+  // This symbolizes all stack frames since the expansion of inline frames
+  // depends on the symbols. Its possible some stack objects will never have
+  // their frames queried which makes this duplicate work. A possible addition
+  // is to just save the debug_ipc::StackFrames and only expand the inline
+  // frames when the frame list is accessed.
+
+  // The symbols will provide the location for the innermost inlined function.
+  Location inner_loc = delegate_->GetSymbolizedLocationForStackFrame(record);
+
+  const Function* cur_func = inner_loc.symbol().Get()->AsFunction();
+  if (!cur_func) {
+    // No function associated with this location.
+    frames_.push_back(delegate_->MakeFrameForStack(record, inner_loc));
+    return;
+  }
+
+  // The Location object will reference the most-specific inline function but
+  // we need the whole chain.
+  std::vector<const Function*> inline_chain = cur_func->GetInlineChain();
+  if (inline_chain.back()->is_inline()) {
+    // A non-inline frame was not found. The symbols are corrupt so give up
+    // on inline processing and add the physical frame only.
+    frames_.push_back(delegate_->MakeFrameForStack(record, inner_loc));
+    return;
+  }
+
+  // Need to make the base "physical" frame first because all of the inline
+  // frames refer to it.
+  auto physical_frame = delegate_->MakeFrameForStack(
+      record, LocationForInlineFrameChain(inline_chain, inline_chain.size() - 1,
+                                          inner_loc));
+
+  // Add all inline functions (skipping the last which is the physical frame
+  // made above).
+  for (size_t i = 0; i < inline_chain.size() - 1; i++) {
+    frames_.push_back(std::make_unique<InlineFrame>(
+        physical_frame.get(),
+        LocationForInlineFrameChain(inline_chain, i, inner_loc)));
+  }
+
+  // Physical frame goes last (back in time).
+  frames_.push_back(std::move(physical_frame));
+}
+
 }  // namespace zxdb
diff --git a/bin/zxdb/client/stack.h b/bin/zxdb/client/stack.h
index 2ee40ef..95dea99 100644
--- a/bin/zxdb/client/stack.h
+++ b/bin/zxdb/client/stack.h
@@ -6,6 +6,7 @@
 
 #include <vector>
 
+#include "garnet/bin/zxdb/symbols/location.h"
 #include "garnet/lib/debug_ipc/protocol.h"
 #include "lib/fxl/macros.h"
 
@@ -38,9 +39,15 @@
     // processing.
     virtual void SyncFramesForStack(std::function<void()> callback) = 0;
 
-    // Constructs a Frame implementation for the given IPC stack frame
-    // information.
+    // Constructs a Frame implementation for the given IPC stack frame and
+    // location. The location must be an input since inline frame expansion
+    // requires stack frames be constructed with different symbols than just
+    // looking up the address in the symbols.
     virtual std::unique_ptr<Frame> MakeFrameForStack(
+        const debug_ipc::StackFrame& input,
+        Location location) = 0;
+
+    virtual Location GetSymbolizedLocationForStackFrame(
         const debug_ipc::StackFrame& input) = 0;
   };
 
@@ -94,6 +101,11 @@
   bool ClearFrames();
 
  private:
+  // Adds the given stack frame to the end of the current stack (going
+  // backwards in time). Inline frames will be expanded so this may append more
+  // than one frame.
+  void AppendFrame(const debug_ipc::StackFrame& record);
+
   Delegate* delegate_;
 
   std::vector<std::unique_ptr<Frame>> frames_;
diff --git a/bin/zxdb/client/stack_unittest.cc b/bin/zxdb/client/stack_unittest.cc
new file mode 100644
index 0000000..6a3d702
--- /dev/null
+++ b/bin/zxdb/client/stack_unittest.cc
@@ -0,0 +1,133 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <map>
+
+#include "garnet/bin/zxdb/client/mock_frame.h"
+#include "garnet/bin/zxdb/client/stack.h"
+#include "garnet/bin/zxdb/symbols/function.h"
+#include "gtest/gtest.h"
+#include "lib/fxl/logging.h"
+
+namespace zxdb {
+
+namespace {
+
+class MockStackDelegate : public Stack::Delegate {
+ public:
+  // Adds the given location to the list of things returned by
+  // GetSymbolizedLocationForStackFrame().
+  void AddLocation(const Location& loc) { locations_[loc.address()] = loc; }
+
+  void SyncFramesForStack(std::function<void()> callback) override {
+    // Not needed by this test.
+    FXL_NOTREACHED();
+  }
+
+  std::unique_ptr<Frame> MakeFrameForStack(const debug_ipc::StackFrame& input,
+                                           Location location) override {
+    return std::make_unique<MockFrame>(nullptr, nullptr, input, location);
+  }
+
+  Location GetSymbolizedLocationForStackFrame(
+      const debug_ipc::StackFrame& input) override {
+    auto found = locations_.find(input.ip);
+    if (found == locations_.end())
+      return Location(Location::State::kSymbolized, input.ip);
+    return found->second;
+  }
+
+ private:
+  std::map<uint64_t, Location> locations_;
+};
+
+}  // namespace
+
+// Tests that stack frames inside inline functions are expanded so that the
+// inline functions have their own "inline" frames.
+TEST(Stack, InlineExpansion) {
+  constexpr uint64_t kBottomAddr = 0x127365;  // IP for bottom stack frame.
+  constexpr uint64_t kTopAddr = 0x893746123;  // IP for top stack frale.
+
+  const char kFileName[] = "file.cc";
+  FileLine inline_call_line(kFileName, 10);
+  FileLine inline_exec_line(kFileName, 20);
+  FileLine top_line(kFileName, 30);
+
+  MockStackDelegate delegate;
+  SymbolContext symbol_context = SymbolContext::ForRelativeAddresses();
+
+  // Non-inline location for the top stack frame.
+  auto top_func = fxl::MakeRefCounted<Function>(Symbol::kTagSubprogram);
+  top_func->set_assigned_name("Top");
+  Location top_location(kTopAddr, top_line, 0, symbol_context,
+                        LazySymbol(top_func));
+  delegate.AddLocation(top_location);
+
+  // Bottom stack frame has a real function and an inline function.
+  auto bottom_inline_func =
+      fxl::MakeRefCounted<Function>(Symbol::kTagInlinedSubroutine);
+  bottom_inline_func->set_assigned_name("Inline");
+  bottom_inline_func->set_code_ranges(
+      {AddressRange(kBottomAddr, kBottomAddr + 8)});
+  bottom_inline_func->set_call_line(inline_call_line);
+
+  auto bottom_func = fxl::MakeRefCounted<Function>(Symbol::kTagSubprogram);
+  bottom_func->set_assigned_name("Bottom");
+  bottom_func->set_code_ranges(
+      {AddressRange(kBottomAddr - 8, kBottomAddr + 16)});
+
+  // For convenience, the inline function is nested inside the "bottom" func.
+  // This is not something you can actually do in C++ and will give a name
+  // "Bottom::Inline()". In real life the inline function will reference the
+  // actualy function definition in the correct namespace.
+  bottom_inline_func->set_parent(LazySymbol(bottom_func));
+
+  // The location returned by the symbol function will have the file/line
+  // inside the inline function.
+  Location bottom_location(kBottomAddr, inline_exec_line, 0,
+                           symbol_context, LazySymbol(bottom_inline_func));
+  delegate.AddLocation(bottom_location);
+
+  Stack stack(&delegate);
+
+  // Send IPs that will map to the bottom and top addresses.
+  stack.SetFrames(debug_ipc::ThreadRecord::StackAmount::kFull,
+                  {debug_ipc::StackFrame(kTopAddr, 0x100, 0x100),
+                   debug_ipc::StackFrame(kBottomAddr, 0x200, 0x200)});
+
+  // This should expand to tree stack entries, the one in the middle should
+  // be the inline function expanded from the "bottom".
+  EXPECT_EQ(3u, stack.size());
+
+  // Bottom stack frame should be the non-inline bottom function.
+  EXPECT_FALSE(stack[2]->IsInline());
+  EXPECT_EQ(stack[2], stack[2]->GetPhysicalFrame());
+  EXPECT_EQ(kBottomAddr, stack[2]->GetAddress());
+  Location loc = stack[2]->GetLocation();
+  EXPECT_EQ(kBottomAddr, loc.address());
+  EXPECT_EQ(inline_call_line, loc.file_line());
+  EXPECT_EQ(bottom_func.get(), loc.symbol().Get()->AsFunction());
+
+  // Middle stack frame should be the inline bottom function at the same
+  // address, referencing the bottom one as the physical frame.
+  EXPECT_TRUE(stack[1]->IsInline());
+  EXPECT_EQ(stack[2], stack[1]->GetPhysicalFrame());
+  EXPECT_EQ(kBottomAddr, stack[1]->GetAddress());
+  loc = stack[1]->GetLocation();
+  EXPECT_EQ(kBottomAddr, loc.address());
+  EXPECT_EQ(inline_exec_line, loc.file_line());
+  EXPECT_EQ(bottom_inline_func.get(), loc.symbol().Get()->AsFunction());
+
+  // Top stack frame.
+  EXPECT_FALSE(stack[0]->IsInline());
+  EXPECT_EQ(stack[0], stack[0]->GetPhysicalFrame());
+  EXPECT_EQ(kTopAddr, stack[0]->GetAddress());
+  loc = stack[0]->GetLocation();
+  EXPECT_EQ(kTopAddr, loc.address());
+  EXPECT_EQ(top_line, loc.file_line());
+  EXPECT_EQ(top_func.get(), loc.symbol().Get()->AsFunction());
+}
+
+}  // namespace zxdb
diff --git a/bin/zxdb/client/thread_impl.cc b/bin/zxdb/client/thread_impl.cc
index 624e0a7..aacc047 100644
--- a/bin/zxdb/client/thread_impl.cc
+++ b/bin/zxdb/client/thread_impl.cc
@@ -315,9 +315,18 @@
 }
 
 std::unique_ptr<Frame> ThreadImpl::MakeFrameForStack(
+    const debug_ipc::StackFrame& input, Location location) {
+  return std::make_unique<FrameImpl>(this, input, std::move(location));
+}
+
+Location ThreadImpl::GetSymbolizedLocationForStackFrame(
     const debug_ipc::StackFrame& input) {
-  return std::make_unique<FrameImpl>(
-      this, input, Location(Location::State::kAddress, input.ip));
+  auto vect = GetProcess()->GetSymbols()->ResolveInputLocation(
+      InputLocation(input.ip));
+
+  // Symbolizing an address should always give exactly one result.
+  FXL_DCHECK(vect.size() == 1u);
+  return vect[0];
 }
 
 void ThreadImpl::ClearFrames() {
diff --git a/bin/zxdb/client/thread_impl.h b/bin/zxdb/client/thread_impl.h
index 9158f99..05914a9 100644
--- a/bin/zxdb/client/thread_impl.h
+++ b/bin/zxdb/client/thread_impl.h
@@ -63,6 +63,8 @@
   // Stack::Delegate implementation.
   void SyncFramesForStack(std::function<void()> callback) override;
   std::unique_ptr<Frame> MakeFrameForStack(
+      const debug_ipc::StackFrame& input, Location location) override;
+  Location GetSymbolizedLocationForStackFrame(
       const debug_ipc::StackFrame& input) override;
 
   // Invalidates the cached frames.
diff --git a/bin/zxdb/client/thread_impl_unittest.cc b/bin/zxdb/client/thread_impl_unittest.cc
index 3c5f3c5..c7a596c 100644
--- a/bin/zxdb/client/thread_impl_unittest.cc
+++ b/bin/zxdb/client/thread_impl_unittest.cc
@@ -76,9 +76,6 @@
   EXPECT_EQ(kAddress1, stack[0]->GetAddress());
   EXPECT_EQ(kStack1, stack[0]->GetStackPointer());
 
-  // The top stack frame object should be preserved across the updates below.
-  const Frame* top_stack = stack[0];
-
   // Construct what the full stack will be returned to the thread. The top
   // element should match the one already there.
   constexpr uint64_t kAddress2 = 0x34567890;
@@ -103,9 +100,6 @@
   EXPECT_EQ(kAddress2, stack[1]->GetAddress());
   EXPECT_EQ(kStack2, stack[1]->GetStackPointer());
 
-  // The unchanged stack element @ index 0 should be the same Frame object.
-  EXPECT_EQ(top_stack, stack[0]);
-
   // Resuming the thread should be asynchronous so nothing should change.
   thread->Continue();
   EXPECT_EQ(2u, thread->GetStack().size());
@@ -116,16 +110,6 @@
   // nothing should change. If we have better thread state notifications in
   // the future, it would be nice if the thread reported itself as running
   // and the stack was cleared at this point.
-
-  // Stopping the thread again should clear the stack back to the one frame
-  // we sent. Since the address didn't change from last time, it should be the
-  // same frame object.
-  InjectException(break_notification);
-  EXPECT_FALSE(thread->GetStack().has_all_frames());
-  ASSERT_EQ(1u, stack.size());
-  EXPECT_EQ(kAddress1, stack[0]->GetAddress());
-  EXPECT_EQ(kStack1, stack[0]->GetStackPointer());
-  EXPECT_EQ(top_stack, stack[0]);
 }
 
 // Tests that general exceptions still run thread controllers. If the exception
diff --git a/bin/zxdb/symbols/code_block.cc b/bin/zxdb/symbols/code_block.cc
index bfffd3f..51b744a 100644
--- a/bin/zxdb/symbols/code_block.cc
+++ b/bin/zxdb/symbols/code_block.cc
@@ -4,6 +4,7 @@
 
 #include "garnet/bin/zxdb/symbols/code_block.h"
 
+#include "garnet/bin/zxdb/symbols/function.h"
 #include "garnet/bin/zxdb/symbols/symbol_context.h"
 #include "lib/fxl/logging.h"
 
@@ -71,4 +72,19 @@
   return nullptr;
 }
 
+std::vector<const Function*> CodeBlock::GetInlineChain() const {
+  std::vector<const Function*> result;
+
+  const CodeBlock* cur_block = this;
+  while (cur_block) {
+    if (const Function* function = cur_block->AsFunction()) {
+      result.push_back(function);
+      if (!function->is_inline())
+        break;  // Just added containing non-inline function.
+    }
+    cur_block = cur_block->parent().Get()->AsCodeBlock();
+  }
+  return result;
+}
+
 }  // namespace zxdb
diff --git a/bin/zxdb/symbols/code_block.h b/bin/zxdb/symbols/code_block.h
index b6ca2fa..d0d12d4 100644
--- a/bin/zxdb/symbols/code_block.h
+++ b/bin/zxdb/symbols/code_block.h
@@ -4,6 +4,8 @@
 
 #pragma once
 
+#include <vector>
+
 #include "garnet/bin/zxdb/common/address_range.h"
 #include "garnet/bin/zxdb/symbols/symbol.h"
 
@@ -78,6 +80,20 @@
   // should be inside functions).
   const Function* GetContainingFunction() const;
 
+  // Returns the chain of inline functions to the current code block.
+  //
+  // The returned vector will go back in time. The 0 item will be the most
+  // specific function containing this code block (always
+  // GetContainingFunction(), will be = |this| if this is a function).
+  //
+  // The back "should" be the containing non-inlined function (this depends on
+  // the symbols declaring a function for the code block which they should do,
+  // but calling code shouldn't crash on malformed symbols).
+  //
+  // If the current block is not in an inline function, the returned vector
+  // will have one element.
+  std::vector<const Function*> GetInlineChain() const;
+
  protected:
   FRIEND_REF_COUNTED_THREAD_SAFE(CodeBlock);
   FRIEND_MAKE_REF_COUNTED(CodeBlock);
diff --git a/bin/zxdb/symbols/dwarf_symbol_factory.cc b/bin/zxdb/symbols/dwarf_symbol_factory.cc
index 87abffc..e260948 100644
--- a/bin/zxdb/symbols/dwarf_symbol_factory.cc
+++ b/bin/zxdb/symbols/dwarf_symbol_factory.cc
@@ -260,12 +260,20 @@
   llvm::DWARFDie return_type;
   decoder.AddReference(llvm::dwarf::DW_AT_type, &return_type);
 
+  // Declaration location.
   llvm::Optional<std::string> decl_file;
-  decoder.AddFile(llvm::dwarf::DW_AT_decl_file, &decl_file);
-
   llvm::Optional<uint64_t> decl_line;
+  decoder.AddFile(llvm::dwarf::DW_AT_decl_file, &decl_file);
   decoder.AddUnsignedConstant(llvm::dwarf::DW_AT_decl_line, &decl_line);
 
+  // Call location (inline functions only).
+  llvm::Optional<std::string> call_file;
+  llvm::Optional<uint64_t> call_line;
+  if (tag == Symbol::kTagInlinedSubroutine) {
+    decoder.AddFile(llvm::dwarf::DW_AT_call_file, &call_file);
+    decoder.AddUnsignedConstant(llvm::dwarf::DW_AT_call_line, &call_line);
+  }
+
   VariableLocation frame_base;
   decoder.AddCustom(llvm::dwarf::DW_AT_frame_base,
                     [unit = die.getDwarfUnit(),
@@ -302,6 +310,7 @@
     function->set_linkage_name(*linkage_name);
   function->set_code_ranges(GetCodeRanges(die));
   function->set_decl_line(MakeFileLine(decl_file, decl_line));
+  function->set_call_line(MakeFileLine(call_file, call_line));
   if (return_type)
     function->set_return_type(MakeLazy(return_type));
   function->set_frame_base(std::move(frame_base));
diff --git a/bin/zxdb/symbols/function.h b/bin/zxdb/symbols/function.h
index 98b4851..a3cc56b 100644
--- a/bin/zxdb/symbols/function.h
+++ b/bin/zxdb/symbols/function.h
@@ -59,6 +59,10 @@
   const FileLine& decl_line() const { return decl_line_; }
   void set_decl_line(FileLine decl) { decl_line_ = std::move(decl); }
 
+  // For inline functions, this can be set to indicate the call location.
+  const FileLine& call_line() const { return call_line_; }
+  void set_call_line(FileLine call) { call_line_ = std::move(call); }
+
   // The return value type. This should be some kind of Type object. Will be
   // empty for void return types.
   const LazySymbol& return_type() const { return return_type_; }
@@ -131,6 +135,7 @@
   std::string assigned_name_;
   std::string linkage_name_;
   FileLine decl_line_;
+  FileLine call_line_;
   LazySymbol return_type_;
   std::vector<LazySymbol> parameters_;
   VariableLocation frame_base_;
diff --git a/bin/zxdb/symbols/lazy_symbol.cc b/bin/zxdb/symbols/lazy_symbol.cc
index 5c8e899..4c3a700 100644
--- a/bin/zxdb/symbols/lazy_symbol.cc
+++ b/bin/zxdb/symbols/lazy_symbol.cc
@@ -25,6 +25,8 @@
       factory_data_ptr_(factory_data_ptr),
       factory_data_offset_(factory_data_offset) {}
 LazySymbol::LazySymbol(fxl::RefPtr<Symbol> symbol) : symbol_(symbol) {}
+LazySymbol::LazySymbol(const Symbol* symbol)
+    : symbol_(const_cast<Symbol*>(symbol)) {}
 LazySymbol::~LazySymbol() = default;
 
 LazySymbol& LazySymbol::operator=(const LazySymbol& other) = default;
diff --git a/bin/zxdb/symbols/lazy_symbol.h b/bin/zxdb/symbols/lazy_symbol.h
index 648ab88..9b66e5d 100644
--- a/bin/zxdb/symbols/lazy_symbol.h
+++ b/bin/zxdb/symbols/lazy_symbol.h
@@ -27,6 +27,7 @@
              uint32_t factory_data_offset);
   // Creates a non-lazy one, mostly for tests.
   explicit LazySymbol(fxl::RefPtr<Symbol> symbol);
+  explicit LazySymbol(const Symbol* symbol);
   ~LazySymbol();
 
   LazySymbol& operator=(const LazySymbol& other);
diff --git a/bin/zxdb/symbols/location.h b/bin/zxdb/symbols/location.h
index 0e2d36f..2d915d5 100644
--- a/bin/zxdb/symbols/location.h
+++ b/bin/zxdb/symbols/location.h
@@ -79,16 +79,17 @@
   const FileLine& file_line() const { return file_line_; }
   int column() const { return column_; }
 
-  // The symbol associated with this address, if any. In the case of code, this
-  // will be the inline or regular function covering the given address. It
-  // could also be a variable symbol corresponding to a global or static
-  // variable.
+  // The symbol associated with this address, if any. In the case of code this
+  // will be a Function. It will not be the code block inside the function
+  // (code wanting lexical blocks can look inside the function's children as
+  // needed). It could also be a variable symbol corresponding to a global or
+  // static variable.
   //
   // When looking up code locations from the symbol system, this will be the
-  // most specific symbol covering the code in question (the innermost inlined
-  // function if there is one). But Locations may be generated (e.g. by the
-  // stack unwinder) for any of the other inlined functions that may cover the
-  // same address.
+  // most specific function covering the code in question (the innermost
+  // inlined function if there is one). But Locations may be generated (e.g. by
+  // the stack unwinder) for any of the other inlined functions that may cover
+  // the same address.
   //
   // This isn't necessarily valid, even if the State == kSymbolized. It could
   // be the symbol table indicates file/line info for this address but could