mirror of
https://sourceware.org/git/binutils-gdb.git
synced 2024-11-24 02:24:46 +08:00
aa9bd44528
This changes the exp_uses_objfile function to be a method of 'expression'. Reviewed-By: Lancelot Six <lancelot.six@amd.com>
693 lines
20 KiB
C
693 lines
20 KiB
C
/* Parse expressions for GDB.
|
||
|
||
Copyright (C) 1986-2023 Free Software Foundation, Inc.
|
||
|
||
Modified from expread.y by the Department of Computer Science at the
|
||
State University of New York at Buffalo, 1991.
|
||
|
||
This file is part of GDB.
|
||
|
||
This program is free software; you can redistribute it and/or modify
|
||
it under the terms of the GNU General Public License as published by
|
||
the Free Software Foundation; either version 3 of the License, or
|
||
(at your option) any later version.
|
||
|
||
This program is distributed in the hope that it will be useful,
|
||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
GNU General Public License for more details.
|
||
|
||
You should have received a copy of the GNU General Public License
|
||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||
|
||
/* Parse an expression from text in a string,
|
||
and return the result as a struct expression pointer.
|
||
That structure contains arithmetic operations in reverse polish,
|
||
with constants represented by operations that are followed by special data.
|
||
See expression.h for the details of the format.
|
||
What is important here is that it can be built up sequentially
|
||
during the process of parsing; the lower levels of the tree always
|
||
come first in the result. */
|
||
|
||
#include "defs.h"
|
||
#include <ctype.h>
|
||
#include "arch-utils.h"
|
||
#include "symtab.h"
|
||
#include "gdbtypes.h"
|
||
#include "frame.h"
|
||
#include "expression.h"
|
||
#include "value.h"
|
||
#include "command.h"
|
||
#include "language.h"
|
||
#include "parser-defs.h"
|
||
#include "gdbcmd.h"
|
||
#include "symfile.h" /* for overlay functions */
|
||
#include "inferior.h"
|
||
#include "target-float.h"
|
||
#include "block.h"
|
||
#include "source.h"
|
||
#include "objfiles.h"
|
||
#include "user-regs.h"
|
||
#include <algorithm>
|
||
#include "gdbsupport/gdb_optional.h"
|
||
#include "c-exp.h"
|
||
|
||
static unsigned int expressiondebug = 0;
|
||
static void
|
||
show_expressiondebug (struct ui_file *file, int from_tty,
|
||
struct cmd_list_element *c, const char *value)
|
||
{
|
||
gdb_printf (file, _("Expression debugging is %s.\n"), value);
|
||
}
|
||
|
||
|
||
/* True if an expression parser should set yydebug. */
|
||
bool parser_debug;
|
||
|
||
static void
|
||
show_parserdebug (struct ui_file *file, int from_tty,
|
||
struct cmd_list_element *c, const char *value)
|
||
{
|
||
gdb_printf (file, _("Parser debugging is %s.\n"), value);
|
||
}
|
||
|
||
|
||
static expression_up parse_exp_in_context
|
||
(const char **, CORE_ADDR,
|
||
const struct block *, int,
|
||
bool, innermost_block_tracker *,
|
||
std::unique_ptr<expr_completion_base> *);
|
||
|
||
/* Documented at it's declaration. */
|
||
|
||
void
|
||
innermost_block_tracker::update (const struct block *b,
|
||
innermost_block_tracker_types t)
|
||
{
|
||
if ((m_types & t) != 0
|
||
&& (m_innermost_block == NULL
|
||
|| contained_in (b, m_innermost_block)))
|
||
m_innermost_block = b;
|
||
}
|
||
|
||
|
||
|
||
/* Return the type of MSYMBOL, a minimal symbol of OBJFILE. If
|
||
ADDRESS_P is not NULL, set it to the MSYMBOL's resolved
|
||
address. */
|
||
|
||
type *
|
||
find_minsym_type_and_address (minimal_symbol *msymbol,
|
||
struct objfile *objfile,
|
||
CORE_ADDR *address_p)
|
||
{
|
||
bound_minimal_symbol bound_msym = {msymbol, objfile};
|
||
struct obj_section *section = msymbol->obj_section (objfile);
|
||
enum minimal_symbol_type type = msymbol->type ();
|
||
|
||
bool is_tls = (section != NULL
|
||
&& section->the_bfd_section->flags & SEC_THREAD_LOCAL);
|
||
|
||
/* The minimal symbol might point to a function descriptor;
|
||
resolve it to the actual code address instead. */
|
||
CORE_ADDR addr;
|
||
if (is_tls)
|
||
{
|
||
/* Addresses of TLS symbols are really offsets into a
|
||
per-objfile/per-thread storage block. */
|
||
addr = bound_msym.minsym->value_raw_address ();
|
||
}
|
||
else if (msymbol_is_function (objfile, msymbol, &addr))
|
||
{
|
||
if (addr != bound_msym.value_address ())
|
||
{
|
||
/* This means we resolved a function descriptor, and we now
|
||
have an address for a code/text symbol instead of a data
|
||
symbol. */
|
||
if (msymbol->type () == mst_data_gnu_ifunc)
|
||
type = mst_text_gnu_ifunc;
|
||
else
|
||
type = mst_text;
|
||
section = NULL;
|
||
}
|
||
}
|
||
else
|
||
addr = bound_msym.value_address ();
|
||
|
||
if (overlay_debugging)
|
||
addr = symbol_overlayed_address (addr, section);
|
||
|
||
if (is_tls)
|
||
{
|
||
/* Skip translation if caller does not need the address. */
|
||
if (address_p != NULL)
|
||
*address_p = target_translate_tls_address (objfile, addr);
|
||
return objfile_type (objfile)->nodebug_tls_symbol;
|
||
}
|
||
|
||
if (address_p != NULL)
|
||
*address_p = addr;
|
||
|
||
switch (type)
|
||
{
|
||
case mst_text:
|
||
case mst_file_text:
|
||
case mst_solib_trampoline:
|
||
return objfile_type (objfile)->nodebug_text_symbol;
|
||
|
||
case mst_text_gnu_ifunc:
|
||
return objfile_type (objfile)->nodebug_text_gnu_ifunc_symbol;
|
||
|
||
case mst_data:
|
||
case mst_file_data:
|
||
case mst_bss:
|
||
case mst_file_bss:
|
||
return objfile_type (objfile)->nodebug_data_symbol;
|
||
|
||
case mst_slot_got_plt:
|
||
return objfile_type (objfile)->nodebug_got_plt_symbol;
|
||
|
||
default:
|
||
return objfile_type (objfile)->nodebug_unknown_symbol;
|
||
}
|
||
}
|
||
|
||
bool
|
||
expr_complete_tag::complete (struct expression *exp,
|
||
completion_tracker &tracker)
|
||
{
|
||
collect_symbol_completion_matches_type (tracker, m_name.get (),
|
||
m_name.get (), m_code);
|
||
return true;
|
||
}
|
||
|
||
/* See parser-defs.h. */
|
||
|
||
void
|
||
parser_state::mark_struct_expression (expr::structop_base_operation *op)
|
||
{
|
||
gdb_assert (parse_completion && m_completion_state == nullptr);
|
||
m_completion_state.reset (new expr_complete_structop (op));
|
||
}
|
||
|
||
/* Indicate that the current parser invocation is completing a tag.
|
||
TAG is the type code of the tag, and PTR and LENGTH represent the
|
||
start of the tag name. */
|
||
|
||
void
|
||
parser_state::mark_completion_tag (enum type_code tag, const char *ptr,
|
||
int length)
|
||
{
|
||
gdb_assert (parse_completion && m_completion_state == nullptr);
|
||
gdb_assert (tag == TYPE_CODE_UNION
|
||
|| tag == TYPE_CODE_STRUCT
|
||
|| tag == TYPE_CODE_ENUM);
|
||
m_completion_state.reset
|
||
(new expr_complete_tag (tag, make_unique_xstrndup (ptr, length)));
|
||
}
|
||
|
||
/* See parser-defs.h. */
|
||
|
||
void
|
||
parser_state::push_c_string (int kind, struct stoken_vector *vec)
|
||
{
|
||
std::vector<std::string> data (vec->len);
|
||
for (int i = 0; i < vec->len; ++i)
|
||
data[i] = std::string (vec->tokens[i].ptr, vec->tokens[i].length);
|
||
|
||
push_new<expr::c_string_operation> ((enum c_string_type_values) kind,
|
||
std::move (data));
|
||
}
|
||
|
||
/* See parser-defs.h. */
|
||
|
||
void
|
||
parser_state::push_symbol (const char *name, block_symbol sym)
|
||
{
|
||
if (sym.symbol != nullptr)
|
||
{
|
||
if (symbol_read_needs_frame (sym.symbol))
|
||
block_tracker->update (sym);
|
||
push_new<expr::var_value_operation> (sym);
|
||
}
|
||
else
|
||
{
|
||
struct bound_minimal_symbol msymbol = lookup_bound_minimal_symbol (name);
|
||
if (msymbol.minsym != NULL)
|
||
push_new<expr::var_msym_value_operation> (msymbol);
|
||
else if (!have_full_symbols () && !have_partial_symbols ())
|
||
error (_("No symbol table is loaded. Use the \"file\" command."));
|
||
else
|
||
error (_("No symbol \"%s\" in current context."), name);
|
||
}
|
||
}
|
||
|
||
/* See parser-defs.h. */
|
||
|
||
void
|
||
parser_state::push_dollar (struct stoken str)
|
||
{
|
||
struct block_symbol sym;
|
||
struct bound_minimal_symbol msym;
|
||
struct internalvar *isym = NULL;
|
||
std::string copy;
|
||
|
||
/* Handle the tokens $digits; also $ (short for $0) and $$ (short for $$1)
|
||
and $$digits (equivalent to $<-digits> if you could type that). */
|
||
|
||
int negate = 0;
|
||
int i = 1;
|
||
/* Double dollar means negate the number and add -1 as well.
|
||
Thus $$ alone means -1. */
|
||
if (str.length >= 2 && str.ptr[1] == '$')
|
||
{
|
||
negate = 1;
|
||
i = 2;
|
||
}
|
||
if (i == str.length)
|
||
{
|
||
/* Just dollars (one or two). */
|
||
i = -negate;
|
||
goto handle_last;
|
||
}
|
||
/* Is the rest of the token digits? */
|
||
for (; i < str.length; i++)
|
||
if (!(str.ptr[i] >= '0' && str.ptr[i] <= '9'))
|
||
break;
|
||
if (i == str.length)
|
||
{
|
||
i = atoi (str.ptr + 1 + negate);
|
||
if (negate)
|
||
i = -i;
|
||
goto handle_last;
|
||
}
|
||
|
||
/* Handle tokens that refer to machine registers:
|
||
$ followed by a register name. */
|
||
i = user_reg_map_name_to_regnum (gdbarch (),
|
||
str.ptr + 1, str.length - 1);
|
||
if (i >= 0)
|
||
goto handle_register;
|
||
|
||
/* Any names starting with $ are probably debugger internal variables. */
|
||
|
||
copy = copy_name (str);
|
||
isym = lookup_only_internalvar (copy.c_str () + 1);
|
||
if (isym)
|
||
{
|
||
push_new<expr::internalvar_operation> (isym);
|
||
return;
|
||
}
|
||
|
||
/* On some systems, such as HP-UX and hppa-linux, certain system routines
|
||
have names beginning with $ or $$. Check for those, first. */
|
||
|
||
sym = lookup_symbol (copy.c_str (), NULL, VAR_DOMAIN, NULL);
|
||
if (sym.symbol)
|
||
{
|
||
push_new<expr::var_value_operation> (sym);
|
||
return;
|
||
}
|
||
msym = lookup_bound_minimal_symbol (copy.c_str ());
|
||
if (msym.minsym)
|
||
{
|
||
push_new<expr::var_msym_value_operation> (msym);
|
||
return;
|
||
}
|
||
|
||
/* Any other names are assumed to be debugger internal variables. */
|
||
|
||
push_new<expr::internalvar_operation>
|
||
(create_internalvar (copy.c_str () + 1));
|
||
return;
|
||
handle_last:
|
||
push_new<expr::last_operation> (i);
|
||
return;
|
||
handle_register:
|
||
str.length--;
|
||
str.ptr++;
|
||
push_new<expr::register_operation> (copy_name (str));
|
||
block_tracker->update (expression_context_block,
|
||
INNERMOST_BLOCK_FOR_REGISTERS);
|
||
return;
|
||
}
|
||
|
||
|
||
|
||
const char *
|
||
find_template_name_end (const char *p)
|
||
{
|
||
int depth = 1;
|
||
int just_seen_right = 0;
|
||
int just_seen_colon = 0;
|
||
int just_seen_space = 0;
|
||
|
||
if (!p || (*p != '<'))
|
||
return 0;
|
||
|
||
while (*++p)
|
||
{
|
||
switch (*p)
|
||
{
|
||
case '\'':
|
||
case '\"':
|
||
case '{':
|
||
case '}':
|
||
/* In future, may want to allow these?? */
|
||
return 0;
|
||
case '<':
|
||
depth++; /* start nested template */
|
||
if (just_seen_colon || just_seen_right || just_seen_space)
|
||
return 0; /* but not after : or :: or > or space */
|
||
break;
|
||
case '>':
|
||
if (just_seen_colon || just_seen_right)
|
||
return 0; /* end a (nested?) template */
|
||
just_seen_right = 1; /* but not after : or :: */
|
||
if (--depth == 0) /* also disallow >>, insist on > > */
|
||
return ++p; /* if outermost ended, return */
|
||
break;
|
||
case ':':
|
||
if (just_seen_space || (just_seen_colon > 1))
|
||
return 0; /* nested class spec coming up */
|
||
just_seen_colon++; /* we allow :: but not :::: */
|
||
break;
|
||
case ' ':
|
||
break;
|
||
default:
|
||
if (!((*p >= 'a' && *p <= 'z') || /* allow token chars */
|
||
(*p >= 'A' && *p <= 'Z') ||
|
||
(*p >= '0' && *p <= '9') ||
|
||
(*p == '_') || (*p == ',') || /* commas for template args */
|
||
(*p == '&') || (*p == '*') || /* pointer and ref types */
|
||
(*p == '(') || (*p == ')') || /* function types */
|
||
(*p == '[') || (*p == ']'))) /* array types */
|
||
return 0;
|
||
}
|
||
if (*p != ' ')
|
||
just_seen_space = 0;
|
||
if (*p != ':')
|
||
just_seen_colon = 0;
|
||
if (*p != '>')
|
||
just_seen_right = 0;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
|
||
/* Return a null-terminated temporary copy of the name of a string token.
|
||
|
||
Tokens that refer to names do so with explicit pointer and length,
|
||
so they can share the storage that lexptr is parsing.
|
||
When it is necessary to pass a name to a function that expects
|
||
a null-terminated string, the substring is copied out
|
||
into a separate block of storage. */
|
||
|
||
std::string
|
||
copy_name (struct stoken token)
|
||
{
|
||
return std::string (token.ptr, token.length);
|
||
}
|
||
|
||
|
||
/* Read an expression from the string *STRINGPTR points to,
|
||
parse it, and return a pointer to a struct expression that we malloc.
|
||
Use block BLOCK as the lexical context for variable names;
|
||
if BLOCK is zero, use the block of the selected stack frame.
|
||
Meanwhile, advance *STRINGPTR to point after the expression,
|
||
at the first nonwhite character that is not part of the expression
|
||
(possibly a null character).
|
||
|
||
If COMMA is nonzero, stop if a comma is reached. */
|
||
|
||
expression_up
|
||
parse_exp_1 (const char **stringptr, CORE_ADDR pc, const struct block *block,
|
||
int comma, innermost_block_tracker *tracker)
|
||
{
|
||
return parse_exp_in_context (stringptr, pc, block, comma, false,
|
||
tracker, nullptr);
|
||
}
|
||
|
||
/* As for parse_exp_1, except that if VOID_CONTEXT_P, then
|
||
no value is expected from the expression. */
|
||
|
||
static expression_up
|
||
parse_exp_in_context (const char **stringptr, CORE_ADDR pc,
|
||
const struct block *block,
|
||
int comma, bool void_context_p,
|
||
innermost_block_tracker *tracker,
|
||
std::unique_ptr<expr_completion_base> *completer)
|
||
{
|
||
const struct language_defn *lang = NULL;
|
||
|
||
if (*stringptr == 0 || **stringptr == 0)
|
||
error_no_arg (_("expression to compute"));
|
||
|
||
const struct block *expression_context_block = block;
|
||
CORE_ADDR expression_context_pc = 0;
|
||
|
||
innermost_block_tracker local_tracker;
|
||
if (tracker == nullptr)
|
||
tracker = &local_tracker;
|
||
|
||
/* If no context specified, try using the current frame, if any. */
|
||
if (!expression_context_block)
|
||
expression_context_block = get_selected_block (&expression_context_pc);
|
||
else if (pc == 0)
|
||
expression_context_pc = expression_context_block->entry_pc ();
|
||
else
|
||
expression_context_pc = pc;
|
||
|
||
/* Fall back to using the current source static context, if any. */
|
||
|
||
if (!expression_context_block)
|
||
{
|
||
struct symtab_and_line cursal = get_current_source_symtab_and_line ();
|
||
|
||
if (cursal.symtab)
|
||
expression_context_block
|
||
= cursal.symtab->compunit ()->blockvector ()->static_block ();
|
||
|
||
if (expression_context_block)
|
||
expression_context_pc = expression_context_block->entry_pc ();
|
||
}
|
||
|
||
if (language_mode == language_mode_auto && block != NULL)
|
||
{
|
||
/* Find the language associated to the given context block.
|
||
Default to the current language if it can not be determined.
|
||
|
||
Note that using the language corresponding to the current frame
|
||
can sometimes give unexpected results. For instance, this
|
||
routine is often called several times during the inferior
|
||
startup phase to re-parse breakpoint expressions after
|
||
a new shared library has been loaded. The language associated
|
||
to the current frame at this moment is not relevant for
|
||
the breakpoint. Using it would therefore be silly, so it seems
|
||
better to rely on the current language rather than relying on
|
||
the current frame language to parse the expression. That's why
|
||
we do the following language detection only if the context block
|
||
has been specifically provided. */
|
||
struct symbol *func = block_linkage_function (block);
|
||
|
||
if (func != NULL)
|
||
lang = language_def (func->language ());
|
||
if (lang == NULL || lang->la_language == language_unknown)
|
||
lang = current_language;
|
||
}
|
||
else
|
||
lang = current_language;
|
||
|
||
/* get_current_arch may reset CURRENT_LANGUAGE via select_frame.
|
||
While we need CURRENT_LANGUAGE to be set to LANG (for lookup_symbol
|
||
and others called from *.y) ensure CURRENT_LANGUAGE gets restored
|
||
to the value matching SELECTED_FRAME as set by get_current_arch. */
|
||
|
||
parser_state ps (lang, get_current_arch (), expression_context_block,
|
||
expression_context_pc, comma, *stringptr,
|
||
completer != nullptr, tracker, void_context_p);
|
||
|
||
scoped_restore_current_language lang_saver;
|
||
set_language (lang->la_language);
|
||
|
||
try
|
||
{
|
||
lang->parser (&ps);
|
||
}
|
||
catch (const gdb_exception &except)
|
||
{
|
||
/* If parsing for completion, allow this to succeed; but if no
|
||
expression elements have been written, then there's nothing
|
||
to do, so fail. */
|
||
if (! ps.parse_completion || ps.expout->op == nullptr)
|
||
throw;
|
||
}
|
||
|
||
expression_up result = ps.release ();
|
||
result->op->set_outermost ();
|
||
|
||
if (expressiondebug)
|
||
result->dump (gdb_stdlog);
|
||
|
||
if (completer != nullptr)
|
||
*completer = std::move (ps.m_completion_state);
|
||
*stringptr = ps.lexptr;
|
||
return result;
|
||
}
|
||
|
||
/* Parse STRING as an expression, and complain if this fails to use up
|
||
all of the contents of STRING. TRACKER, if non-null, will be
|
||
updated by the parser. VOID_CONTEXT_P should be true to indicate
|
||
that the expression may be expected to return a value with void
|
||
type. Parsers are free to ignore this, or to use it to help with
|
||
overload resolution decisions. */
|
||
|
||
expression_up
|
||
parse_expression (const char *string, innermost_block_tracker *tracker,
|
||
bool void_context_p)
|
||
{
|
||
expression_up exp = parse_exp_in_context (&string, 0, nullptr, 0,
|
||
void_context_p,
|
||
tracker, nullptr);
|
||
if (*string)
|
||
error (_("Junk after end of expression."));
|
||
return exp;
|
||
}
|
||
|
||
/* Same as parse_expression, but using the given language (LANG)
|
||
to parse the expression. */
|
||
|
||
expression_up
|
||
parse_expression_with_language (const char *string, enum language lang)
|
||
{
|
||
gdb::optional<scoped_restore_current_language> lang_saver;
|
||
if (current_language->la_language != lang)
|
||
{
|
||
lang_saver.emplace ();
|
||
set_language (lang);
|
||
}
|
||
|
||
return parse_expression (string);
|
||
}
|
||
|
||
/* Parse STRING as an expression. If the parse is marked for
|
||
completion, set COMPLETER and return the expression. In all other
|
||
cases, return NULL. */
|
||
|
||
expression_up
|
||
parse_expression_for_completion
|
||
(const char *string,
|
||
std::unique_ptr<expr_completion_base> *completer)
|
||
{
|
||
expression_up exp;
|
||
|
||
try
|
||
{
|
||
exp = parse_exp_in_context (&string, 0, 0, 0, false, nullptr, completer);
|
||
}
|
||
catch (const gdb_exception_error &except)
|
||
{
|
||
/* Nothing, EXP remains NULL. */
|
||
}
|
||
|
||
/* If we didn't get a completion result, be sure to also not return
|
||
an expression to our caller. */
|
||
if (*completer == nullptr)
|
||
return nullptr;
|
||
|
||
return exp;
|
||
}
|
||
|
||
/* Parse floating point value P of length LEN.
|
||
Return false if invalid, true if valid.
|
||
The successfully parsed number is stored in DATA in
|
||
target format for floating-point type TYPE.
|
||
|
||
NOTE: This accepts the floating point syntax that sscanf accepts. */
|
||
|
||
bool
|
||
parse_float (const char *p, int len,
|
||
const struct type *type, gdb_byte *data)
|
||
{
|
||
return target_float_from_string (data, type, std::string (p, len));
|
||
}
|
||
|
||
/* Return true if the number N_SIGN * N fits in a type with TYPE_BITS and
|
||
TYPE_SIGNED_P. N_SIGNED is either 1 or -1. */
|
||
|
||
bool
|
||
fits_in_type (int n_sign, ULONGEST n, int type_bits, bool type_signed_p)
|
||
{
|
||
/* Normalize -0. */
|
||
if (n == 0 && n_sign == -1)
|
||
n_sign = 1;
|
||
|
||
if (n_sign == -1 && !type_signed_p)
|
||
/* Can't fit a negative number in an unsigned type. */
|
||
return false;
|
||
|
||
if (type_bits > sizeof (ULONGEST) * 8)
|
||
return true;
|
||
|
||
ULONGEST smax = (ULONGEST)1 << (type_bits - 1);
|
||
if (n_sign == -1)
|
||
{
|
||
/* Negative number, signed type. */
|
||
return (n <= smax);
|
||
}
|
||
else if (n_sign == 1 && type_signed_p)
|
||
{
|
||
/* Positive number, signed type. */
|
||
return (n < smax);
|
||
}
|
||
else if (n_sign == 1 && !type_signed_p)
|
||
{
|
||
/* Positive number, unsigned type. */
|
||
return ((n >> 1) >> (type_bits - 1)) == 0;
|
||
}
|
||
else
|
||
gdb_assert_not_reached ("");
|
||
}
|
||
|
||
/* This function avoids direct calls to fprintf
|
||
in the parser generated debug code. */
|
||
void
|
||
parser_fprintf (FILE *x, const char *y, ...)
|
||
{
|
||
va_list args;
|
||
|
||
va_start (args, y);
|
||
if (x == stderr)
|
||
gdb_vprintf (gdb_stderr, y, args);
|
||
else
|
||
{
|
||
gdb_printf (gdb_stderr, " Unknown FILE used.\n");
|
||
gdb_vprintf (gdb_stderr, y, args);
|
||
}
|
||
va_end (args);
|
||
}
|
||
|
||
void _initialize_parse ();
|
||
void
|
||
_initialize_parse ()
|
||
{
|
||
add_setshow_zuinteger_cmd ("expression", class_maintenance,
|
||
&expressiondebug,
|
||
_("Set expression debugging."),
|
||
_("Show expression debugging."),
|
||
_("When non-zero, the internal representation "
|
||
"of expressions will be printed."),
|
||
NULL,
|
||
show_expressiondebug,
|
||
&setdebuglist, &showdebuglist);
|
||
add_setshow_boolean_cmd ("parser", class_maintenance,
|
||
&parser_debug,
|
||
_("Set parser debugging."),
|
||
_("Show parser debugging."),
|
||
_("When non-zero, expression parser "
|
||
"tracing will be enabled."),
|
||
NULL,
|
||
show_parserdebug,
|
||
&setdebuglist, &showdebuglist);
|
||
}
|