[v6,1/3] gdb: Do not create variables when parsing expressions

Message ID 20250406013809.230156-2-nt8r@protonmail.com
State New
Headers
Series Tab complete convenience variables |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_gdb_build--master-arm success Build passed

Commit Message

Antonio Rische April 6, 2025, 1:38 a.m. UTC
  We do this by introducing a new type of expression for internalvars
that had not been given a value at parse time.

This is not particularly useful on its own, but allows us to parse
expressions without having side-effects on the set of internalvars.

This will then allow us to tab-complete internalvars in expressions
without creating new internalvars, which would otherwise defeat the
purpose of tab completion (completing only variables that have been
set).

In addition, this allows us to make the 'init-if-undefined' command
behave as it is documented to: previously, it treated convenience
vars that have been set (or re-set) to void as undefined. Now, it
only initialized variables that have actually never been defined.

The old behavior can be accessed by instead guarding assignment to
a convenience variable with 'if($_isvoid($var))'.
---
 gdb/NEWS            |  5 ++++
 gdb/ada-exp.h       | 12 +++++++--
 gdb/ax-gdb.c        | 61 +++++++++++++++++++++++++++++++++++++--------
 gdb/cli/cli-utils.c |  2 +-
 gdb/expop.h         | 55 +++++++++++++++++++++++++++++++++++++++-
 gdb/parse.c         |  7 +++---
 gdb/value.c         | 23 ++++++++---------
 7 files changed, 135 insertions(+), 30 deletions(-)
  

Comments

Eli Zaretskii April 6, 2025, 5:20 a.m. UTC | #1
> Date: Sun, 06 Apr 2025 01:38:59 +0000
> From: Antonio Rische <nt8r@protonmail.com>
> Cc: Antonio Rische <nt8r@protonmail.com>
> 
>  gdb/NEWS            |  5 ++++
>  gdb/ada-exp.h       | 12 +++++++--
>  gdb/ax-gdb.c        | 61 +++++++++++++++++++++++++++++++++++++--------
>  gdb/cli/cli-utils.c |  2 +-
>  gdb/expop.h         | 55 +++++++++++++++++++++++++++++++++++++++-
>  gdb/parse.c         |  7 +++---
>  gdb/value.c         | 23 ++++++++---------
>  7 files changed, 135 insertions(+), 30 deletions(-)

Thanks.

> diff --git a/gdb/NEWS b/gdb/NEWS
> index 6a557bb4a..07c1193d8 100644
> --- a/gdb/NEWS
> +++ b/gdb/NEWS
> @@ -116,6 +116,11 @@ qXfer:threads:read
>    subsystem to be disabled at configure time, in the form of
>    --disable-gdb-compile.
>  
> +* The =init-if-undefined command now strictly obeys its documentation to
         ^
That "=" should be removed, I believe.

> +  initialize only convenience variables that have not been set. It
                                                                 ^^
Please leave 2 spaces between sentences, per our conventions.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
  
Keith Seitz April 11, 2025, 7:54 p.m. UTC | #2
Hi,

Thank you for another revision. I'd really like to see this land!

On 4/5/25 6:38 PM, Antonio Rische wrote:
> We do this by introducing a new type of expression for internalvars
> that had not been given a value at parse time.
> 

I hate to be "that person," but the commit logs for this series
do not read well. I'm not asking for any changes, but please keep
in mind for the future that reading the log should not require
the subject. It's just awkward IMO.

> This is not particularly useful on its own, but allows us to parse
> expressions without having side-effects on the set of internalvars.
> 
> This will then allow us to tab-complete internalvars in expressions
> without creating new internalvars, which would otherwise defeat the
> purpose of tab completion (completing only variables that have been
> set).
> 
> In addition, this allows us to make the 'init-if-undefined' command
> behave as it is documented to: previously, it treated convenience
> vars that have been set (or re-set) to void as undefined. Now, it
> only initialized variables that have actually never been defined.

"initializes"?

> diff --git a/gdb/parse.c b/gdb/parse.c
> index ffefe6fee..8de3e19d2 100644
> --- a/gdb/parse.c
> +++ b/gdb/parse.c
> @@ -240,10 +240,9 @@ parser_state::push_dollar (struct stoken str)
>         return;
>       }
>   
> -  /* Any other names are assumed to be debugger internal variables.  */
> -
> -  push_new<expr::internalvar_operation>
> -    (create_internalvar (copy.c_str () + 1));
> +  /* Any other names are assumed to be debugger internal variables which
> +     have not yet been initialized.  */
> +  push_new<expr::uninit_internalvar_operation> (copy.c_str () + 1);
>   }
>   
>   /* See parser-defs.h.  */

In v5 Tom raised a really good question:

"Really what I'm asking is: if you apply your patch, then change the
last spot using internalvar_operation to instead use the new
uninit_internalvar_operation, does anything fail?"

For giggles, I hacked at parser_state::push_dollar to do just this
and did not detect any test failures, suggesting that we can, indeed,
adapt the existing class's behavior instead of introducing a new
class.

I'd like to ask again: Is there some use case that this would break or
any reason you can think of that would/should prevent modifying
internalvar_operation instead?

Keith
  
Antonio Rische May 1, 2025, 4:39 a.m. UTC | #3
Thanks for the feedback, I really appreciate it.

I'll reword the commit messages; I had no idea of the convention to
make body independent of the title. Thanks for the typo catch on
"initialized" that should be "initializes".

With respect to Tom's question on v5: No, no tests fail on master
plus this changeset (up to v6) if we replace the last use of
internalvar_operation with uninit_internalvar_operation.

But if we add the following test cases:

diff --git a/gdb/testsuite/gdb.base/gdbvars.exp b/gdb/testsuite/gdb.base/gdbvars.exp
index 7ec7df36b..1902833e4 100644
--- a/gdb/testsuite/gdb.base/gdbvars.exp
+++ b/gdb/testsuite/gdb.base/gdbvars.exp
@@ -52,6 +52,26 @@ proc test_convenience_variables {} {
     gdb_test "print \$bar"             " = void" \
        "Print contents of uninitialized convenience variable"
 
+    gdb_test_no_output "init-if-undefined \$bar = 1" \
+       "Set a convenience value only if undefined"
+
+    gdb_test "print \$bar"             " = 1" \
+       "Check that init-if-undefined set undefined var"
+
+    gdb_test_no_output "init-if-undefined \$bar = 2" \
+       "Set a convenience var holding integer only if undefined"
+
+    gdb_test "print \$bar"             " = 1" \
+       "Check that init-if-undefined did not overwrite integer var"
+
+    gdb_test_no_output "set \$bar = (void)0" \
+       "Set the convenience value to void"
+
+    gdb_test_no_output "init-if-undefined \$bar = 3" \
+       "Set a convenience var holding void only if undefined"
+
+    gdb_test "print \$bar"             " = void" \
+       "Check that init-if-undefined did not overwrite void var"
 
     gdb_test "print \$str = \"some string\"" \
        " = \"some string\"" \


(Which I believe are a good idea especially as 'init-if-undefined'
seems to have no tests at present; I'll put them in v7) then tests
fail prior to commit #1 of this series, pass afterward, and fail again
after subsequently making the suggested change of the last usage of
internalvar_operation to uninit_internalvar_operation instead:

diff --git a/gdb/parse.c b/gdb/parse.c
index 8de3e19d2..70e1196af 100644
--- a/gdb/parse.c
+++ b/gdb/parse.c
@@ -218,7 +218,7 @@ parser_state::push_dollar (struct stoken str)
   isym = lookup_only_internalvar (copy.c_str () + 1);
   if (isym)
     {
-      push_new<expr::internalvar_operation> (isym);
+      push_new<expr::uninit_internalvar_operation> (copy.c_str () + 1);
       return;
     }
 

That said, creating a new expr::*_operation class is not the only
possible way we could distinguish between expressions reading from
internalvars that did or did not exist at parse-time. We could fold
them into one class and distinguish with a field rather than with
a distinct subtype.

But I think doing so leads to worse code. Both types inherit from
tuple_holding_operation<T> for different types T; for uninitialized
internalvars we hold the name of the variable as a std::string and
look it up at evaluation time based on its name as a string. For
internalvars that existed at parse time, we simply hold a pointer
to the internalvar.

Holding a string in all cases and always performing late lookup of
the internalvar itself could work for both cases; we would just add
a boolean field to distinguish the two, e.g. for init-if-undefined.
But it's less elegant to defer lookup to expression evaluation time
when we can do it in most cases at parse time. And to avoid changing
the current behavior of `parser_state::push_dollar` would still have
to call `lookup_only_internalvar` at parse time because internalvars
(if they exist) shadow symbols that start with '$' as handled by the
code below the comment about "HP-UX and hppa-linux".

Again, if this is really a big deal I can rewrite the patch as
described above with one class, late lookup and a boolean. Or even
with one class, eager-*or*-late lookup, and a std::variant. But to
me personally this seems like splitting hairs.

Anyhow, thanks for the careful review.

Antonio

On Friday, April 11th, 2025 at 7:54 PM, Keith Seitz <keiths@redhat.com> wrote:

> Hi,
> 
> Thank you for another revision. I'd really like to see this land!
> 
> On 4/5/25 6:38 PM, Antonio Rische wrote:
> 
> > We do this by introducing a new type of expression for internalvars
> > that had not been given a value at parse time.
> 
> 
> I hate to be "that person," but the commit logs for this series
> do not read well. I'm not asking for any changes, but please keep
> in mind for the future that reading the log should not require
> the subject. It's just awkward IMO.
> 
> > This is not particularly useful on its own, but allows us to parse
> > expressions without having side-effects on the set of internalvars.
> > 
> > This will then allow us to tab-complete internalvars in expressions
> > without creating new internalvars, which would otherwise defeat the
> > purpose of tab completion (completing only variables that have been
> > set).
> > 
> > In addition, this allows us to make the 'init-if-undefined' command
> > behave as it is documented to: previously, it treated convenience
> > vars that have been set (or re-set) to void as undefined. Now, it
> > only initialized variables that have actually never been defined.
> 
> 
> "initializes"?
> 
> > diff --git a/gdb/parse.c b/gdb/parse.c
> > index ffefe6fee..8de3e19d2 100644
> > --- a/gdb/parse.c
> > +++ b/gdb/parse.c
> > @@ -240,10 +240,9 @@ parser_state::push_dollar (struct stoken str)
> > return;
> > }
> > 
> > - /* Any other names are assumed to be debugger internal variables. /
> > -
> > - push_newexpr::internalvar_operation
> > - (create_internalvar (copy.c_str () + 1));
> > + / Any other names are assumed to be debugger internal variables which
> > + have not yet been initialized. */
> > + push_newexpr::uninit_internalvar_operation (copy.c_str () + 1);
> > }
> > 
> > /* See parser-defs.h. */
> 
> 
> In v5 Tom raised a really good question:
> 
> "Really what I'm asking is: if you apply your patch, then change the
> last spot using internalvar_operation to instead use the new
> uninit_internalvar_operation, does anything fail?"
> 
> For giggles, I hacked at parser_state::push_dollar to do just this
> and did not detect any test failures, suggesting that we can, indeed,
> adapt the existing class's behavior instead of introducing a new
> class.
> 
> I'd like to ask again: Is there some use case that this would break or
> any reason you can think of that would/should prevent modifying
> internalvar_operation instead?
> 
> Keith
  

Patch

diff --git a/gdb/NEWS b/gdb/NEWS
index 6a557bb4a..07c1193d8 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -116,6 +116,11 @@  qXfer:threads:read
   subsystem to be disabled at configure time, in the form of
   --disable-gdb-compile.
 
+* The =init-if-undefined command now strictly obeys its documentation to
+  initialize only convenience variables that have not been set. It
+  previously treated variables set to void as having never been set (see
+  the $_isvoid convenience function for this behavior).
+
 *** Changes in GDB 16
 
 * Support for Nios II targets has been removed as this architecture
diff --git a/gdb/ada-exp.h b/gdb/ada-exp.h
index 0c8161ea8..7120563a3 100644
--- a/gdb/ada-exp.h
+++ b/gdb/ada-exp.h
@@ -544,8 +544,16 @@  class ada_assign_operation
      assignment.  */
   value *eval_for_resolution (struct expression *exp)
   {
-    return std::get<0> (m_storage)->evaluate (nullptr, exp,
-					      EVAL_AVOID_SIDE_EFFECTS);
+    operation *lhs_op = std::get<0> (m_storage).get ();
+
+    /* If the operation is an internalvar that was not initialized at
+     parse time, ensure that it exists. */
+    auto *uninit_var_op = dynamic_cast<uninit_internalvar_operation *>
+			  (lhs_op);
+    if (uninit_var_op != nullptr)
+      lookup_internalvar (uninit_var_op->get_name ());
+
+    return lhs_op->evaluate (nullptr, exp, EVAL_AVOID_SIDE_EFFECTS);
   }
 
   /* The parser must construct the assignment node before parsing the
diff --git a/gdb/ax-gdb.c b/gdb/ax-gdb.c
index 2b7d6cef6..5330bc563 100644
--- a/gdb/ax-gdb.c
+++ b/gdb/ax-gdb.c
@@ -1668,6 +1668,33 @@  register_operation::do_generate_ax (struct expression *exp,
   value->type = register_type (ax->gdbarch, reg);
 }
 
+/* Generate remote agent bytecode for the operation that accesses a
+   yet-to-be-initialized internalvar.  */
+void
+uninit_internalvar_operation::do_generate_ax (struct expression *exp,
+				       struct agent_expr *ax,
+				       struct axs_value *value,
+				       struct type *cast_type)
+{
+  const char *name = get_name ();
+  struct internalvar *var = lookup_only_internalvar (name);
+  struct trace_state_variable *tsv;
+
+  tsv = find_trace_state_variable (name);
+  if (tsv != nullptr)
+    {
+      ax_tsv (ax, aop_getv, tsv->number);
+      if (ax->tracing)
+	ax_tsv (ax, aop_tracev, tsv->number);
+      /* Trace state variables are always 64-bit integers.  */
+      value->kind = axs_rvalue;
+      value->type = builtin_type (ax->gdbarch)->builtin_long_long;
+    }
+  else if (!compile_internalvar_to_ax (var, ax, value))
+    error (_("$%s is not a trace state variable; GDB agent "
+	     "expressions cannot use convenience variables."), name);
+}
+
 void
 internalvar_operation::do_generate_ax (struct expression *exp,
 				       struct agent_expr *ax,
@@ -1679,7 +1706,7 @@  internalvar_operation::do_generate_ax (struct expression *exp,
   struct trace_state_variable *tsv;
 
   tsv = find_trace_state_variable (name);
-  if (tsv)
+  if (tsv != nullptr)
     {
       ax_tsv (ax, aop_getv, tsv->number);
       if (ax->tracing)
@@ -1688,7 +1715,7 @@  internalvar_operation::do_generate_ax (struct expression *exp,
       value->kind = axs_rvalue;
       value->type = builtin_type (ax->gdbarch)->builtin_long_long;
     }
-  else if (! compile_internalvar_to_ax (var, ax, value))
+  else if (!compile_internalvar_to_ax (var, ax, value))
     error (_("$%s is not a trace state variable; GDB agent "
 	     "expressions cannot use convenience variables."), name);
 }
@@ -1911,6 +1938,26 @@  op_this_operation::do_generate_ax (struct expression *exp,
 	   sym->print_name ());
 }
 
+/* Get the name of the internalvar referenced by an operation, or
+   nullptr if the operation does not reference an internalvar.  */
+static const char *
+internalvar_op_name (operation *op)
+{
+  gdb_assert (op->opcode () == OP_INTERNALVAR);
+
+  internalvar_operation *ivar_op
+    = dynamic_cast<internalvar_operation *> (op);
+  if (ivar_op != nullptr)
+    return internalvar_name (ivar_op->get_internalvar ());
+
+  uninit_internalvar_operation *uninit_ivar_op
+    = dynamic_cast<uninit_internalvar_operation *> (op);
+  if (uninit_ivar_op != nullptr)
+    return uninit_ivar_op->get_name ();
+
+  return nullptr;
+}
+
 void
 assign_operation::do_generate_ax (struct expression *exp,
 				  struct agent_expr *ax,
@@ -1921,10 +1968,7 @@  assign_operation::do_generate_ax (struct expression *exp,
   if (subop->opcode () != OP_INTERNALVAR)
     error (_("May only assign to trace state variables"));
 
-  internalvar_operation *ivarop
-    = gdb::checked_static_cast<internalvar_operation *> (subop);
-
-  const char *name = internalvar_name (ivarop->get_internalvar ());
+  const char *name = internalvar_op_name (subop);
   struct trace_state_variable *tsv;
 
   std::get<1> (m_storage)->generate_ax (exp, ax, value);
@@ -1950,10 +1994,7 @@  assign_modify_operation::do_generate_ax (struct expression *exp,
   if (subop->opcode () != OP_INTERNALVAR)
     error (_("May only assign to trace state variables"));
 
-  internalvar_operation *ivarop
-    = gdb::checked_static_cast<internalvar_operation *> (subop);
-
-  const char *name = internalvar_name (ivarop->get_internalvar ());
+  const char *name = internalvar_op_name (subop);
   struct trace_state_variable *tsv;
 
   tsv = find_trace_state_variable (name);
diff --git a/gdb/cli/cli-utils.c b/gdb/cli/cli-utils.c
index 152fee96f..ca19f4766 100644
--- a/gdb/cli/cli-utils.c
+++ b/gdb/cli/cli-utils.c
@@ -49,7 +49,7 @@  get_ulongest (const char **pp, int trailer)
 	  while (isalnum (*p) || *p == '_')
 	    p++;
 	  std::string varname (start, p - start);
-	  if (!get_internalvar_integer (lookup_internalvar (varname.c_str ()),
+	  if (!get_internalvar_integer (lookup_only_internalvar (varname.c_str ()),
 				       &retval))
 	    error (_("Convenience variable $%s does not have integer value."),
 		   varname.c_str ());
diff --git a/gdb/expop.h b/gdb/expop.h
index 580a71e94..fbcfdf03c 100644
--- a/gdb/expop.h
+++ b/gdb/expop.h
@@ -877,6 +877,42 @@  class bool_operation
   { return true; }
 };
 
+/* Reference to a variable that had not been defined at parse time.  */
+class uninit_internalvar_operation
+  : public tuple_holding_operation<std::string>
+{
+public:
+
+  using tuple_holding_operation::tuple_holding_operation;
+
+  value *evaluate (struct type *expect_type,
+		   struct expression *exp,
+		   enum noside noside) override
+  {
+    internalvar *iv = lookup_only_internalvar (std::get<0> (m_storage).c_str ());
+    if (iv == nullptr)
+      return value::allocate (builtin_type (exp->gdbarch)->builtin_void);
+
+    return value_of_internalvar (exp->gdbarch, iv);
+  }
+
+  const char *get_name () const
+  {
+    return std::get<0> (m_storage).c_str ();
+  }
+
+  enum exp_opcode opcode () const override
+  { return OP_INTERNALVAR; }
+
+protected:
+
+  void do_generate_ax (struct expression *exp,
+		       struct agent_expr *ax,
+		       struct axs_value *value,
+		       struct type *cast_type)
+    override;
+};
+
 class internalvar_operation
   : public tuple_holding_operation<internalvar *>
 {
@@ -1895,7 +1931,16 @@  class assign_operation
 		   struct expression *exp,
 		   enum noside noside) override
   {
-    value *lhs = std::get<0> (m_storage)->evaluate (nullptr, exp, noside);
+    operation *lhs_op = std::get<0> (m_storage).get ();
+
+    /* If the operation is an internalvar that was not initialized at
+       parse time, ensure that it exists. */
+    auto *uninit_var_op = dynamic_cast<uninit_internalvar_operation *> (lhs_op);
+    if (uninit_var_op != nullptr)
+      lookup_internalvar (uninit_var_op->get_name ());
+
+    value *lhs = lhs_op->evaluate (nullptr, exp, noside);
+
     /* Special-case assignments where the left-hand-side is a
        convenience variable -- in these, don't bother setting an
        expected type.  This avoids a weird case where re-assigning a
@@ -1944,6 +1989,14 @@  class assign_modify_operation
 		   struct expression *exp,
 		   enum noside noside) override
   {
+    operation *lhs_op = std::get<1> (m_storage).get ();
+
+    /* If the operation is an internalvar that was not initialized at
+       parse time, ensure that it exists. */
+    auto *uninit_var_op = dynamic_cast<uninit_internalvar_operation *> (lhs_op);
+    if (uninit_var_op != nullptr)
+      lookup_internalvar (uninit_var_op->get_name ());
+
     value *lhs = std::get<1> (m_storage)->evaluate (nullptr, exp, noside);
     value *rhs = std::get<2> (m_storage)->evaluate (expect_type, exp, noside);
     return eval_binop_assign_modify (expect_type, exp, noside,
diff --git a/gdb/parse.c b/gdb/parse.c
index ffefe6fee..8de3e19d2 100644
--- a/gdb/parse.c
+++ b/gdb/parse.c
@@ -240,10 +240,9 @@  parser_state::push_dollar (struct stoken str)
       return;
     }
 
-  /* Any other names are assumed to be debugger internal variables.  */
-
-  push_new<expr::internalvar_operation>
-    (create_internalvar (copy.c_str () + 1));
+  /* Any other names are assumed to be debugger internal variables which
+     have not yet been initialized.  */
+  push_new<expr::uninit_internalvar_operation> (copy.c_str () + 1);
 }
 
 /* See parser-defs.h.  */
diff --git a/gdb/value.c b/gdb/value.c
index d4548b876..0fa13f54e 100644
--- a/gdb/value.c
+++ b/gdb/value.c
@@ -1908,8 +1908,6 @@  static std::map<std::string, internalvar> internalvars;
 static void
 init_if_undefined_command (const char* args, int from_tty)
 {
-  struct internalvar *intvar = nullptr;
-
   /* Parse the expression - this is taken from set_command().  */
   expression_up expr = parse_expression (args);
 
@@ -1927,18 +1925,19 @@  init_if_undefined_command (const char* args, int from_tty)
       expr::operation *lhs = assign->get_lhs ();
       expr::internalvar_operation *ivarop
 	= dynamic_cast<expr::internalvar_operation *> (lhs);
+      /* If the lvalue is already a defined variable, return
+	 without evaluating the expression.  */
       if (ivarop != nullptr)
-	intvar = ivarop->get_internalvar ();
-    }
-
-  if (intvar == nullptr)
-    error (_("The first parameter to init-if-undefined "
-	     "should be a GDB variable."));
+	return;
 
-  /* Only evaluate the expression if the lvalue is void.
-     This may still fail if the expression is invalid.  */
-  if (intvar->kind == INTERNALVAR_VOID)
-    expr->evaluate ();
+      expr::uninit_internalvar_operation *uninit_ivarop
+	= dynamic_cast<expr::uninit_internalvar_operation *> (lhs);
+      if (uninit_ivarop != nullptr)
+	expr->evaluate ();
+      else
+	error (_("The first parameter to init-if-undefined "
+		 "should be a GDB variable."));
+    }
 }