# HG changeset patch
# User paulo
# Date 1682434155 14400
#      Tue Apr 25 10:49:15 2023 -0400
# Node ID b4d798789b2684f27a5b56778606c34b23df4a66
# Parent  17a09d2bbe0f98b7a6518ceddba234fceba3ccaf
classdef: add support for cli and gui breakpoints (bug #46451)

* libinterp/octave-value/cdef-class.cc: Add
cdef_class::cdef_class_rep::get_method (size_t line) const.

* libinterp/parse-tree/bp-table.cc: Add user_code_provider class to enable
setting breakpoints and using the dbstatus and dbclear commands within classdef
classes.

* libinterp/octave-value/cdef-class.h: Add OCTINTERP_API octave_value get_method
(size_t line) const.

* test/classdef-debug/breakpoints/classdef_breakpoints.m: New m-file containing
a simple classdef class for use in testing the new functionality.

* test/classdef-debug/breakpoints/test_classdef_breakpoints.m: New script for
testing breakpoints functionality.

diff -r 17a09d2bbe0f -r b4d798789b26 libinterp/octave-value/cdef-class.cc
--- a/libinterp/octave-value/cdef-class.cc	Sun Apr 23 11:08:16 2023 +0200
+++ b/libinterp/octave-value/cdef-class.cc	Tue Apr 25 10:49:15 2023 -0400
@@ -708,6 +708,37 @@
   return p->second.get_function ();
 }
 
+octave_value
+cdef_class::cdef_class_rep::get_method (size_t line) const
+{
+  octave_value closest_match;
+  int closest_match_end_line = std::numeric_limits<int>::max ();
+  // Since we have a dynamic cast performance could be an issue
+  // If this is called from a critical path.
+  // If performance is an issue we can cache an ordered version of the method map
+  for(method_const_iterator i = m_method_map.cbegin(); i != m_method_map.cend(); ++i)
+  {
+    const octave_value& fcn = i->second.get_function();
+    octave_user_code *user_code = fcn.user_code_value();
+    
+    if(user_code == nullptr)
+      continue;
+
+    octave_user_function* pfcn = dynamic_cast<octave_user_function*>(user_code);
+
+    if(pfcn == nullptr)
+      continue;
+
+    const int e = pfcn->ending_line ();
+    if(line <= e && e <= closest_match_end_line && pfcn->is_defined () && pfcn->is_user_code ())
+    {
+      closest_match = fcn;
+      closest_match_end_line = e;
+    }
+  }
+
+  return closest_match;
+}
 
 octave_value
 cdef_class::cdef_class_rep::construct (const octave_value_list& args)
diff -r 17a09d2bbe0f -r b4d798789b26 libinterp/octave-value/cdef-class.h
--- a/libinterp/octave-value/cdef-class.h	Sun Apr 23 11:08:16 2023 +0200
+++ b/libinterp/octave-value/cdef-class.h	Tue Apr 25 10:49:15 2023 -0400
@@ -124,6 +124,8 @@
 
     OCTINTERP_API octave_value get_method (const std::string& name) const;
 
+    OCTINTERP_API octave_value get_method (size_t line) const;
+
     OCTINTERP_API octave_value construct (const octave_value_list& args);
 
     OCTINTERP_API cdef_object
@@ -357,6 +359,11 @@
     return get_rep ()->get_method (nm);
   }
 
+  octave_value get_method (int ln) const
+  {
+    return get_rep ()->get_method (ln);
+  }
+
   OCTINTERP_API octave_value get_method_function (const std::string& nm);
 
   octave_value get_constructor_function ()
diff -r 17a09d2bbe0f -r b4d798789b26 libinterp/parse-tree/bp-table.cc
--- a/libinterp/parse-tree/bp-table.cc	Sun Apr 23 11:08:16 2023 +0200
+++ b/libinterp/parse-tree/bp-table.cc	Tue Apr 25 10:49:15 2023 -0400
@@ -33,6 +33,7 @@
 #include <map>
 #include <set>
 #include <string>
+#include <vector>
 
 #include "file-ops.h"
 #include "oct-env.h"
@@ -53,9 +54,14 @@
 #include "pt-exp.h"
 #include "pt-stmt.h"
 #include "sighandlers.h"
+#include "cdef-utils.h"
 
 OCTAVE_BEGIN_NAMESPACE(octave)
 
+static octave_user_code * find_fcn_by_line (octave_user_code *main_fcn,
+    int lineno,
+    int *end_line = nullptr);
+
 class bp_file_info
 {
 public:
@@ -112,6 +118,119 @@
   std::string m_class_name;
 };
 
+// A class that encapsulates the "get_user_code" operation.
+// This operation changes depending on the symbol being a funciton or classdef.
+// Octave used to assume everything was a function, one per file.
+// Classdefs can have several functions in a single file.
+class user_code_provider
+{
+public:
+  user_code_provider(
+    const std::string& fname,
+    const std::string& cname,
+    octave_user_code* pfcn
+  )
+    : m_ptr(nullptr)
+    , m_find_function_by_line(nullptr)
+    , m_is_classdef(false)
+  { 
+    if (pfcn)
+      {
+        m_find_function_by_line = [](void* fcn, int ln) -> auto {
+          return find_fcn_by_line (static_cast<octave_user_code*>(fcn), ln);
+        };
+        m_ptr = static_cast<void*>(pfcn);
+      }
+    else
+      {
+        
+        if (cname.empty())
+        {
+          // Is it a classdef file plus line numbers?
+          m_cls = lookup_class (fname, false, false);
+          m_is_classdef = m_cls.ok() && m_cls.get_name() == fname;
+          if (m_is_classdef)
+          {
+            m_find_function_by_line = [](void* pcls, int ln) -> auto {
+              return static_cast<cdef_class*>(pcls)->get_method(ln).user_code_value (true);
+            };
+            m_ptr = static_cast<void*>(&m_cls);
+            populate_function_cache();
+          }
+        }
+      }
+    
+    if(!m_find_function_by_line)
+      error ("add_breakpoints_in_function: unable to find function '%s'\n", fname.c_str ());
+  }
+
+  octave_user_code* operator()(int line)
+  {
+    if(is_valid())
+      return (*m_find_function_by_line)(m_ptr, line);
+    return nullptr;
+  }
+
+  octave_user_code* operator()()
+  {
+    return (*this)(1);
+  }
+
+  bool is_function() const { return !m_is_classdef; }
+
+  bool is_valid() const { return m_find_function_by_line != nullptr; }
+
+  size_t number_of_functions() const
+  {
+    if(!is_valid())
+      return 0;
+    
+    if(is_function())
+      return 1;
+
+    return m_methods_cache.size();
+  }
+
+  octave_user_code* get_function(const size_t i) const
+  {
+    if(!is_valid())
+      return nullptr;
+
+    if(is_function())
+      return static_cast<octave_user_code*>(m_ptr);
+
+    if(i < m_methods_cache.size())
+      return m_methods_cache[i];
+
+    return nullptr;
+  }
+
+private:
+  void populate_function_cache()
+  {
+      if(m_methods_cache.empty())
+        {
+          // Not the most effecient, but reuses code:
+          const std::map<std::string, cdef_method>& map = m_cls.get_method_map(false, true);
+          typedef std::map<std::string, cdef_method>::const_iterator method_const_iterator;
+          for(method_const_iterator i = map.cbegin(); i != map.cend(); ++i)
+            {
+              octave_user_code* fcn = m_cls.get_method(i->first).user_code_value (true);
+              if(fcn != nullptr)
+                m_methods_cache.push_back(fcn);
+            }
+        }
+  }
+
+private:
+  // Don't want to use std::function, so to avoid capture we pass a void*:
+  void* m_ptr;
+  octave_user_code* (*m_find_function_by_line)(void*, int);
+  std::vector<octave_user_code*> m_methods_cache;
+  cdef_class m_cls;
+  bool m_is_classdef;
+};
+
 // Clear all reasons to stop, other than breakpoints.
 
 void bp_table::dbclear_all_signals ()
@@ -608,7 +727,7 @@
 
 static octave_user_code * find_fcn_by_line (octave_user_code *main_fcn,
     int lineno,
-    int *end_line = nullptr)
+    int *end_line)
 {
   octave_user_code *retval = nullptr;
   octave_user_code *next_fcn = nullptr;  // 1st function starting after lineno
@@ -690,11 +809,7 @@
                                        const bp_table::bp_lines& lines,
                                        const std::string& condition)
 {
-  octave_user_code *main_fcn = m_evaluator.get_user_code (fname, class_name);
-
-  if (! main_fcn)
-    error ("add_breakpoints_in_function: unable to find function '%s'\n",
-           fname.c_str ());
+  user_code_provider get_user_code(fname, class_name, m_evaluator.get_user_code (fname, class_name));
 
   condition_valid (condition);  // Throw error if condition not valid.
 
@@ -702,7 +817,7 @@
 
   for (const auto& lineno : lines)
     {
-      octave_user_code *dbg_fcn = find_fcn_by_line (main_fcn, lineno);
+      octave_user_code *dbg_fcn = get_user_code(lineno);
 
       // We've found the right (sub)function.  Now insert the breakpoint.
       bp_lines line_info;
@@ -814,7 +929,7 @@
 
 int
 bp_table::remove_breakpoints_from_function (const std::string& fname,
-    const bp_table::bp_lines& lines)
+                                            const bp_table::bp_lines& lines)
 {
   int retval = 0;
 
@@ -826,32 +941,65 @@
   else
     {
       octave_user_code *dbg_fcn = m_evaluator.get_user_code (fname);
+      user_code_provider get_user_code(fname, "", dbg_fcn);
 
-      if (! dbg_fcn)
+      if(get_user_code.is_valid())
+      {
+        // Remove all breakpoints from all functions
+        for(const auto line : lines)
+          {
+            octave_user_code *fcn = get_user_code(line);
+            std::string file = fcn->fcn_file_name ();
+
+            tree_statement_list *cmds = fcn->body ();
+            if (cmds)
+              {
+                octave_value_list results = cmds->list_breakpoints ();
+
+                if (results.length () > 0)
+                  {
+                    interpreter& interp = m_evaluator.get_interpreter ();
+                    event_manager& evmgr = interp.get_event_manager ();
+
+                    cmds->delete_breakpoint (line);
+
+                    if (! file.empty ())
+                      evmgr.update_breakpoint (false, file, line);
+                  }
+              }
+          }
+        // Remove all breakpoints from all subfunctions
+        if(dbg_fcn != nullptr)
+          {
+            // Search subfunctions in the order they appear in the file.
+            const std::list<std::string> subfcn_names
+              = dbg_fcn->subfunction_names ();
+
+            std::map<std::string, octave_value> subfcns
+              = dbg_fcn->subfunctions ();
+
+            for (const auto& subf_nm : subfcn_names)
+              {
+                const auto q = subfcns.find (subf_nm);
+
+                if (q != subfcns.end ())
+                  {
+                    octave_user_code *dbg_subfcn = q->second.user_code_value ();
+
+                    retval += remove_breakpoint_1 (dbg_subfcn, fname, lines);
+                  }
+              }
+          }
+        // Remove file from breakpoint set if no breakpoints remain
+        octave_value_list fname_list = {fname};
+        const bool no_breakpoints = get_breakpoint_list (fname_list).empty();
+        auto it = m_bp_set.find (fname);
+        if (no_breakpoints && it != m_bp_set.end ())
+          m_bp_set.erase (it);
+      }
+      else
         error ("remove_breakpoints_from_function: unable to find function %s\n",
                fname.c_str ());
-
-      retval = remove_breakpoint_1 (dbg_fcn, fname, lines);
-
-      // Search subfunctions in the order they appear in the file.
-
-      const std::list<std::string> subfcn_names
-        = dbg_fcn->subfunction_names ();
-
-      std::map<std::string, octave_value> subfcns
-        = dbg_fcn->subfunctions ();
-
-      for (const auto& subf_nm : subfcn_names)
-        {
-          const auto q = subfcns.find (subf_nm);
-
-          if (q != subfcns.end ())
-            {
-              octave_user_code *dbg_subfcn = q->second.user_code_value ();
-
-              retval += remove_breakpoint_1 (dbg_subfcn, fname, lines);
-            }
-        }
     }
 
   m_evaluator.reset_debug_state ();
@@ -863,30 +1011,34 @@
 
 bp_table::bp_lines
 bp_table::remove_all_breakpoints_from_function (const std::string& fname,
-    bool silent)
+                                                bool silent)
 {
   bp_lines retval;
 
-  octave_user_code *dbg_fcn = m_evaluator.get_user_code (fname);
+  octave_user_code *fcn = m_evaluator.get_user_code (fname);
+  user_code_provider get_user_code(fname, "", fcn);
 
-  if (dbg_fcn)
+  if (get_user_code.is_valid())
     {
-      std::string file = dbg_fcn->fcn_file_name ();
+      for(size_t i = 0; i != get_user_code.number_of_functions(); ++i)
+      {
+        octave_user_code *dbg_fcn = get_user_code.get_function(i);
+        std::string file = dbg_fcn->fcn_file_name ();
 
-      tree_statement_list *cmds = dbg_fcn->body ();
+        tree_statement_list *cmds = dbg_fcn->body ();
 
-      if (cmds)
-        {
-          interpreter& interp = m_evaluator.get_interpreter ();
+        if (cmds)
+          {
+            interpreter& interp = m_evaluator.get_interpreter ();
 
-          event_manager& evmgr = interp.get_event_manager ();
+            event_manager& evmgr = interp.get_event_manager ();
 
-          retval = cmds->remove_all_breakpoints (evmgr, file);
-
-          auto it = m_bp_set.find (fname);
-          if (it != m_bp_set.end ())
-            m_bp_set.erase (it);
-        }
+            retval = cmds->remove_all_breakpoints (evmgr, file);
+          }
+      }
+      auto it = m_bp_set.find (fname);
+      if (it != m_bp_set.end ())
+        m_bp_set.erase (it);
     }
   else if (! silent)
     error ("remove_all_breakpoints_from_function: "
@@ -979,29 +1131,43 @@
 
   // make copy since changes may invalidate iters of m_bp_set.
   std::set<std::string> tmp_bp_set = m_bp_set;
-
+  
   for (auto& bp_fname : tmp_bp_set)
     {
       if (fname_list.empty ()
           || find_bkpt_list (fname_list, bp_fname) != "")
         {
           octave_user_code *dbg_fcn = m_evaluator.get_user_code (bp_fname);
-
-          if (dbg_fcn)
+          user_code_provider get_user_code(bp_fname, "", dbg_fcn);
+          // Gather breakpoints from all functions in the file
+          if(get_user_code.is_valid())
             {
-              tree_statement_list *cmds = dbg_fcn->body ();
+              std::list<bp_type> all_bkpts;
+              std::list<bp_type>::iterator it(all_bkpts.begin());
+              for(size_t i = 0; i != get_user_code.number_of_functions(); ++i)
+                {
+                  octave_user_code *fcn = get_user_code.get_function(i);
+                  if (fcn)
+                    {
+                      tree_statement_list *cmds = fcn->body ();
+                      // FIXME: move the operation on cmds to the
+                      //        tree_statement_list class?
+                      if (cmds)
+                        {
+                          const std::list<bp_type>& bp = cmds->breakpoints_and_conds ();
+                          if(!bp.empty())
+                            it = all_bkpts.insert(it, bp.cbegin(), bp.cend());
+                        }
+                    }
 
-              // FIXME: move the operation on cmds to the
-              //        tree_statement_list class?
-              if (cmds)
-                {
-                  std::list<bp_type> bkpts = cmds->breakpoints_and_conds ();
+                }
+              if (! all_bkpts.empty ())
+                retval[bp_fname] = all_bkpts;
+            }
 
-                  if (! bkpts.empty ())
-                    retval[bp_fname] = bkpts;
-                }
-
-              // look for breakpoints in subfunctions
+          // look for breakpoints in subfunctions
+          if (dbg_fcn != nullptr)
+            {
               const std::list<std::string> subf_nm
                 = dbg_fcn->subfunction_names ();
 
@@ -1017,7 +1183,7 @@
                       octave_user_code *dbg_subfcn
                         = q->second.user_code_value ();
 
-                      cmds = dbg_subfcn->body ();
+                      tree_statement_list *cmds = dbg_subfcn->body ();
                       if (cmds)
                         {
                           std::list<bp_type> bkpts
diff -r 17a09d2bbe0f -r b4d798789b26 test/classdef-debug/breakpoints/classdef_breakpoints.m
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/classdef-debug/breakpoints/classdef_breakpoints.m	Tue Apr 25 10:49:15 2023 -0400
@@ -0,0 +1,21 @@
+classdef classdef_breakpoints
+	properties
+		m_prop = [];
+	end
+	methods
+		function this = classdef_breakpoints(prop)
+			this.m_prop = prop;
+		end
+		function this = foo(this, prop)
+			this.m_prop = prop;
+		end
+		function this = bar(this)
+			printf("prop: %f\n", this.m_prop);
+		end
+	end
+end
+
+function MySubfunction(n)
+	% MySubfunction helo
+	printf("subfunction: %f\n", n);
+end
diff -r 17a09d2bbe0f -r b4d798789b26 test/classdef-debug/breakpoints/test_classdef_breakpoints.m
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/test/classdef-debug/breakpoints/test_classdef_breakpoints.m	Tue Apr 25 10:49:15 2023 -0400
@@ -0,0 +1,36 @@
+function test_classdef_breakpoints()
+	printf('Testing classdef breakpoints\n');
+	printf('--------------------------------------------------------------------------------\n');
+	printf('1) If the classdef is not in the path, no breakpoint should exist.\n');
+	dbstop classdef_breakpoints 13;
+	assertBreakpoints([]);
+
+	printf('--------------------------------------------------------------------------------\n');
+	printf('2) Add classdef to path, and now we should be able to set breakpoints.\n');
+	addpath ../test/classdef-debug/breakpoints/
+	dbstop classdef_breakpoints 13 7 10;
+	assertBreakpoints([7 10 13]);
+
+	printf('--------------------------------------------------------------------------------\n');
+	printf('3) Remove one breakpont and confirm the other remain.\n');
+	dbclear classdef_breakpoints 10;
+	assertBreakpoints([7 13]);
+
+	printf('--------------------------------------------------------------------------------\n');
+	printf('4) Clear all breakpoints, none should be left\n');
+	dbclear classdef_breakpoints;
+	assertBreakpoints([]);
+end
+
+function assertBreakpoints(bp0)
+	dbs = dbstatus;
+	bp = [dbs(1:length(bp0)).line];
+	printf('checking sizes\n');
+	assert(length(bp) == length(bp0));
+	if length(bp) ~= 0
+		printf('checking breakpoints\n');
+		assert(sort(bp) == bp0);
+	end
+end
+
+test_classdef_breakpoints();
\ No newline at end of file
