From 1739cf248ff21b21271d1e9d5f77a12589c3856c Mon Sep 17 00:00:00 2001 From: Tom Tromey Date: Tue, 6 Feb 2018 12:10:20 -0700 Subject: [PATCH] Class-ify macro_buffer This patch changes macro_buffer to be a bit more of a C++ class, adding constructors, a destructor, and some members. Then this is used to remove various cleanups in macroexp.c. 2018-02-08 Tom Tromey * macroexp.c (struct macro_buffer): Add initializers for some members. (init_buffer, init_shared_buffer, free_buffer) (free_buffer_return_text): Remove. (macro_buffer): New constructors. (~macro_buffer): New destructor. (macro_buffer::set_shared): New method. (macro_buffer::resize_buffer, macro_buffer::appendc) (macro_buffer::appendmem): Now methods, not free functions. (set_token, append_tokens_without_splicing, stringify) (macro_stringify): Update. (gather_arguments): Change return type. Remove argc_p argument, add args_ptr argument. Use std::vector. (substitute_args): Remove argc argument. Accept std::vector. (expand): Update. Use std::vector. (scan, macro_expand, macro_expand_next): Update. --- gdb/ChangeLog | 19 +++ gdb/macroexp.c | 406 ++++++++++++++++++++----------------------------- 2 files changed, 185 insertions(+), 240 deletions(-) diff --git a/gdb/ChangeLog b/gdb/ChangeLog index 49306ac916b..2a989cc1153 100644 --- a/gdb/ChangeLog +++ b/gdb/ChangeLog @@ -1,3 +1,22 @@ +2018-02-08 Tom Tromey + + * macroexp.c (struct macro_buffer): Add initializers for some + members. + (init_buffer, init_shared_buffer, free_buffer) + (free_buffer_return_text): Remove. + (macro_buffer): New constructors. + (~macro_buffer): New destructor. + (macro_buffer::set_shared): New method. + (macro_buffer::resize_buffer, macro_buffer::appendc) + (macro_buffer::appendmem): Now methods, not free functions. + (set_token, append_tokens_without_splicing, stringify) + (macro_stringify): Update. + (gather_arguments): Change return type. Remove argc_p argument, + add args_ptr argument. Use std::vector. + (substitute_args): Remove argc argument. Accept std::vector. + (expand): Update. Use std::vector. + (scan, macro_expand, macro_expand_next): Update. + 2018-02-08 Tom Tromey * symtab.c (default_collect_symbol_completion_matches_break_on): diff --git a/gdb/macroexp.c b/gdb/macroexp.c index aefb13a9d64..02cf26ff739 100644 --- a/gdb/macroexp.c +++ b/gdb/macroexp.c @@ -65,121 +65,110 @@ struct macro_buffer no token abutting the end of TEXT (it's just whitespace), and again, we set this equal to LEN. We set this to -1 if we don't know the nature of TEXT. */ - int last_token; + int last_token = -1; /* If this buffer is holding the result from get_token, then this is non-zero if it is an identifier token, zero otherwise. */ - int is_identifier; -}; - - -/* Set the macro buffer *B to the empty string, guessing that its - final contents will fit in N bytes. (It'll get resized if it - doesn't, so the guess doesn't have to be right.) Allocate the - initial storage with xmalloc. */ -static void -init_buffer (struct macro_buffer *b, int n) -{ - b->size = n; - if (n > 0) - b->text = (char *) xmalloc (n); - else - b->text = NULL; - b->len = 0; - b->shared = false; - b->last_token = -1; -} - - -/* Set the macro buffer *BUF to refer to the LEN bytes at ADDR, as a - shared substring. */ - -static void -init_shared_buffer (struct macro_buffer *buf, const char *addr, int len) -{ - /* The function accept a "const char *" addr so that clients can - pass in string literals without casts. */ - buf->text = (char *) addr; - buf->len = len; - buf->shared = true; - buf->size = 0; - buf->last_token = -1; -} + int is_identifier = 0; -/* Free the text of the buffer B. Raise an error if B is shared. */ -static void -free_buffer (struct macro_buffer *b) -{ - gdb_assert (! b->shared); - if (b->size) - xfree (b->text); -} + macro_buffer () + : text (NULL), + len (0), + size (0), + shared (false) + { + } -/* Like free_buffer, but return the text as an xstrdup()d string. - This only exists to try to make the API relatively clean. */ + /* Set the macro buffer to the empty string, guessing that its + final contents will fit in N bytes. (It'll get resized if it + doesn't, so the guess doesn't have to be right.) Allocate the + initial storage with xmalloc. */ + explicit macro_buffer (int n) + : len (0), + size (n), + shared (false) + { + if (n > 0) + text = (char *) xmalloc (n); + else + text = NULL; + } -static char * -free_buffer_return_text (struct macro_buffer *b) -{ - gdb_assert (! b->shared); - gdb_assert (b->size); - /* Nothing to do. */ - return b->text; -} + /* Set the macro buffer to refer to the LEN bytes at ADDR, as a + shared substring. */ + macro_buffer (const char *addr, int len) + { + set_shared (addr, len); + } -/* A cleanup function for macro buffers. */ -static void -cleanup_macro_buffer (void *untyped_buf) -{ - free_buffer ((struct macro_buffer *) untyped_buf); -} + /* Set the macro buffer to refer to the LEN bytes at ADDR, as a + shared substring. */ + void set_shared (const char *addr, int len_) + { + text = (char *) addr; + len = len_; + size = 0; + shared = true; + } + ~macro_buffer () + { + if (! shared && size) + xfree (text); + } -/* Resize the buffer B to be at least N bytes long. Raise an error if - B shouldn't be resized. */ -static void -resize_buffer (struct macro_buffer *b, int n) -{ - /* We shouldn't be trying to resize shared strings. */ - gdb_assert (! b->shared); - - if (b->size == 0) - b->size = n; - else - while (b->size <= n) - b->size *= 2; + /* Release the text of the buffer to the caller, which is now + responsible for freeing it. */ + char *release () + { + gdb_assert (! shared); + gdb_assert (size); + char *result = text; + text = NULL; + return result; + } - b->text = (char *) xrealloc (b->text, b->size); -} + /* Resize the buffer to be at least N bytes long. Raise an error if + the buffer shouldn't be resized. */ + void resize_buffer (int n) + { + /* We shouldn't be trying to resize shared strings. */ + gdb_assert (! shared); + if (size == 0) + size = n; + else + while (size <= n) + size *= 2; -/* Append the character C to the buffer B. */ -static void -appendc (struct macro_buffer *b, int c) -{ - int new_len = b->len + 1; + text = (char *) xrealloc (text, size); + } - if (new_len > b->size) - resize_buffer (b, new_len); + /* Append the character C to the buffer. */ + void appendc (int c) + { + int new_len = len + 1; - b->text[b->len] = c; - b->len = new_len; -} + if (new_len > size) + resize_buffer (new_len); + text[len] = c; + len = new_len; + } -/* Append the LEN bytes at ADDR to the buffer B. */ -static void -appendmem (struct macro_buffer *b, const char *addr, int len) -{ - int new_len = b->len + len; + /* Append the COUNT bytes at ADDR to the buffer. */ + void appendmem (const char *addr, int count) + { + int new_len = len + count; - if (new_len > b->size) - resize_buffer (b, new_len); + if (new_len > size) + resize_buffer (new_len); - memcpy (b->text + b->len, addr, len); - b->len = new_len; -} + memcpy (text + len, addr, count); + len = new_len; + } +}; @@ -216,7 +205,7 @@ macro_is_identifier_nondigit (int c) static void set_token (struct macro_buffer *tok, char *start, char *end) { - init_shared_buffer (tok, start, end - start); + tok->set_shared (start, end - start); tok->last_token = 0; /* Presumed; get_identifier may overwrite this. */ @@ -596,7 +585,7 @@ append_tokens_without_splicing (struct macro_buffer *dest, /* First, just try appending the two, and call get_token to see if we got a splice. */ - appendmem (dest, src->text, src->len); + dest->appendmem (src->text, src->len); /* If DEST originally had no token abutting its end, then we can't have spliced anything, so we're done. */ @@ -608,9 +597,8 @@ append_tokens_without_splicing (struct macro_buffer *dest, /* Set DEST_TAIL to point to the last token in DEST, followed by all the stuff we just appended. */ - init_shared_buffer (&dest_tail, - dest->text + dest->last_token, - dest->len - dest->last_token); + dest_tail.set_shared (dest->text + dest->last_token, + dest->len - dest->last_token); /* Re-parse DEST's last token. We know that DEST used to contain at least one token, so if it doesn't contain any after the @@ -630,12 +618,11 @@ append_tokens_without_splicing (struct macro_buffer *dest, its original length and try again, but separate the texts with a space. */ dest->len = original_dest_len; - appendc (dest, ' '); - appendmem (dest, src->text, src->len); + dest->appendc (' '); + dest->appendmem (src->text, src->len); - init_shared_buffer (&dest_tail, - dest->text + dest->last_token, - dest->len - dest->last_token); + dest_tail.set_shared (dest->text + dest->last_token, + dest->len - dest->last_token); /* Try to re-parse DEST's last token, as above. */ if (get_token (&new_token, &dest_tail) @@ -671,7 +658,7 @@ stringify (struct macro_buffer *dest, const char *arg, int len) --len; /* Insert the string. */ - appendc (dest, '"'); + dest->appendc ('"'); while (len > 0) { /* We could try to handle strange cases here, like control @@ -679,7 +666,7 @@ stringify (struct macro_buffer *dest, const char *arg, int len) if (macro_is_whitespace (*arg)) { /* Replace a sequence of whitespace with a single space. */ - appendc (dest, ' '); + dest->appendc (' '); while (len > 1 && macro_is_whitespace (arg[1])) { ++arg; @@ -688,15 +675,15 @@ stringify (struct macro_buffer *dest, const char *arg, int len) } else if (*arg == '\\' || *arg == '"') { - appendc (dest, '\\'); - appendc (dest, *arg); + dest->appendc ('\\'); + dest->appendc (*arg); } else - appendc (dest, *arg); + dest->appendc (*arg); ++arg; --len; } - appendc (dest, '"'); + dest->appendc ('"'); dest->last_token = dest->len; } @@ -705,14 +692,13 @@ stringify (struct macro_buffer *dest, const char *arg, int len) char * macro_stringify (const char *str) { - struct macro_buffer buffer; int len = strlen (str); + struct macro_buffer buffer (len); - init_buffer (&buffer, len); stringify (&buffer, str, len); - appendc (&buffer, '\0'); + buffer.appendc ('\0'); - return free_buffer_return_text (&buffer); + return buffer.release (); } @@ -759,22 +745,19 @@ currently_rescanning (struct macro_name_list *list, const char *name) baz). If SRC doesn't start with an open paren ( token at all, return - zero, leave SRC unchanged, and don't set *ARGC_P to anything. + false, leave SRC unchanged, and don't set *ARGS_PTR to anything. If SRC doesn't contain a properly terminated argument list, then raise an error. - + For a variadic macro, NARGS holds the number of formal arguments to the macro. For a GNU-style variadic macro, this should be the number of named arguments. For a non-variadic macro, NARGS should be -1. - Otherwise, return a pointer to the first element of an array of - macro buffers referring to the argument texts, and set *ARGC_P to - the number of arguments we found --- the number of elements in the - array. The macro buffers share their text with SRC, and their - last_token fields are initialized. The array is allocated with - xmalloc, and the caller is responsible for freeing it. + Otherwise, return true and set *ARGS_PTR to a vector of macro + buffers referring to the argument texts. The macro buffers share + their text with SRC, and their last_token fields are initialized. NOTE WELL: if SRC starts with a open paren ( token followed immediately by a close paren ) token (e.g., the invocation looks @@ -787,53 +770,36 @@ currently_rescanning (struct macro_name_list *list, const char *name) Consume the tokens from SRC; after this call, SRC contains the text following the invocation. */ -static struct macro_buffer * -gather_arguments (const char *name, struct macro_buffer *src, - int nargs, int *argc_p) +static bool +gather_arguments (const char *name, struct macro_buffer *src, int nargs, + std::vector *args_ptr) { struct macro_buffer tok; - int args_len, args_size; - struct macro_buffer *args = NULL; - struct cleanup *back_to = make_cleanup (free_current_contents, &args); + std::vector args; /* Does SRC start with an opening paren token? Read from a copy of SRC, so SRC itself is unaffected if we don't find an opening paren. */ { - struct macro_buffer temp; - - init_shared_buffer (&temp, src->text, src->len); + struct macro_buffer temp (src->text, src->len); if (! get_token (&tok, &temp) || tok.len != 1 || tok.text[0] != '(') - { - discard_cleanups (back_to); - return 0; - } + return false; } /* Consume SRC's opening paren. */ get_token (&tok, src); - args_len = 0; - args_size = 6; - args = XNEWVEC (struct macro_buffer, args_size); - for (;;) { struct macro_buffer *arg; int depth; - /* Make sure we have room for the next argument. */ - if (args_len >= args_size) - { - args_size *= 2; - args = XRESIZEVEC (struct macro_buffer, args, args_size); - } - /* Initialize the next argument. */ - arg = &args[args_len++]; + args.emplace_back (); + arg = &args.back (); set_token (arg, src->text, src->text); /* Gather the argument's tokens. */ @@ -842,7 +808,7 @@ gather_arguments (const char *name, struct macro_buffer *src, { if (! get_token (&tok, src)) error (_("Malformed argument list for macro `%s'."), name); - + /* Is tok an opening paren? */ if (tok.len == 1 && tok.text[0] == '(') depth++; @@ -856,22 +822,15 @@ gather_arguments (const char *name, struct macro_buffer *src, { /* In the varargs case, the last argument may be missing. Add an empty argument in this case. */ - if (nargs != -1 && args_len == nargs - 1) + if (nargs != -1 && args.size () == nargs - 1) { - /* Make sure we have room for the argument. */ - if (args_len >= args_size) - { - args_size++; - args = XRESIZEVEC (struct macro_buffer, args, - args_size); - } - arg = &args[args_len++]; + args.emplace_back (); + arg = &args.back (); set_token (arg, src->text, src->text); } - discard_cleanups (back_to); - *argc_p = args_len; - return args; + *args_ptr = std::move (args); + return true; } depth--; @@ -882,7 +841,7 @@ gather_arguments (const char *name, struct macro_buffer *src, variadic macro and we are computing the last argument, we want to include the comma and remaining tokens. */ else if (tok.len == 1 && tok.text[0] == ',' && depth == 0 - && (nargs == -1 || args_len < nargs)) + && (nargs == -1 || args.size () < nargs)) break; /* Extend the current argument to enclose this token. If @@ -971,7 +930,7 @@ get_next_token_for_substitution (struct macro_buffer *replacement_list, } /* Given the macro definition DEF, being invoked with the actual - arguments given by ARGC and ARGV, substitute the arguments into the + arguments given by ARGV, substitute the arguments into the replacement list, and store the result in DEST. IS_VARARGS should be true if DEF is a varargs macro. In this case, @@ -986,16 +945,14 @@ get_next_token_for_substitution (struct macro_buffer *replacement_list, NO_LOOP. */ static void -substitute_args (struct macro_buffer *dest, +substitute_args (struct macro_buffer *dest, struct macro_definition *def, int is_varargs, const struct macro_buffer *va_arg_name, - int argc, struct macro_buffer *argv, + const std::vector &argv, struct macro_name_list *no_loop, macro_lookup_ftype *lookup_func, void *lookup_baton) { - /* A macro buffer for the macro's replacement list. */ - struct macro_buffer replacement_list; /* The token we are currently considering. */ struct macro_buffer tok; /* The replacement list's pointer from just before TOK was lexed. */ @@ -1008,8 +965,9 @@ substitute_args (struct macro_buffer *dest, lexed. */ char *lookahead_rl_start; - init_shared_buffer (&replacement_list, def->replacement, - strlen (def->replacement)); + /* A macro buffer for the macro's replacement list. */ + struct macro_buffer replacement_list (def->replacement, + strlen (def->replacement)); gdb_assert (dest->len == 0); dest->last_token = 0; @@ -1066,7 +1024,7 @@ substitute_args (struct macro_buffer *dest, /* If __VA_ARGS__ is empty, then drop the contents of __VA_OPT__. */ - if (argv[argc - 1].len == 0) + if (argv.back ().len == 0) continue; } else if (token_is_vaopt) @@ -1082,7 +1040,7 @@ substitute_args (struct macro_buffer *dest, that to DEST. */ if (tok.text > original_rl_start) { - appendmem (dest, original_rl_start, tok.text - original_rl_start); + dest->appendmem (original_rl_start, tok.text - original_rl_start); dest->last_token = dest->len; } @@ -1135,9 +1093,9 @@ substitute_args (struct macro_buffer *dest, def->argc, def->argv); if (arg != -1) - appendmem (dest, argv[arg].text, argv[arg].len); + dest->appendmem (argv[arg].text, argv[arg].len); else - appendmem (dest, tok.text, tok.len); + dest->appendmem (tok.text, tok.len); } /* Apply a possible sequence of ## operators. */ @@ -1160,8 +1118,8 @@ substitute_args (struct macro_buffer *dest, if (! (is_varargs && tok.len == va_arg_name->len && !memcmp (tok.text, va_arg_name->text, tok.len) - && argv[argc - 1].len == 0)) - appendmem (dest, ",", 1); + && argv.back ().len == 0)) + dest->appendmem (",", 1); prev_was_comma = 0; } @@ -1175,9 +1133,9 @@ substitute_args (struct macro_buffer *dest, def->argc, def->argv); if (arg != -1) - appendmem (dest, argv[arg].text, argv[arg].len); + dest->appendmem (argv[arg].text, argv[arg].len); else - appendmem (dest, tok.text, tok.len); + dest->appendmem (tok.text, tok.len); } /* Now read another token. If it is another splice, we @@ -1198,7 +1156,7 @@ substitute_args (struct macro_buffer *dest, if (prev_was_comma) { /* We saw a comma. Insert it now. */ - appendmem (dest, ",", 1); + dest->appendmem (",", 1); } dest->last_token = dest->len; @@ -1221,14 +1179,12 @@ substitute_args (struct macro_buffer *dest, if (arg != -1) { - struct macro_buffer arg_src; - /* Expand any macro invocations in the argument text, and append the result to dest. Remember that scan mutates its source, so we need to scan a new buffer referring to the argument's text, not the argument itself. */ - init_shared_buffer (&arg_src, argv[arg].text, argv[arg].len); + struct macro_buffer arg_src (argv[arg].text, argv[arg].len); scan (dest, &arg_src, no_loop, lookup_func, lookup_baton); substituted = 1; } @@ -1275,22 +1231,15 @@ expand (const char *id, /* What kind of macro are we expanding? */ if (def->kind == macro_object_like) { - struct macro_buffer replacement_list; - - init_shared_buffer (&replacement_list, def->replacement, - strlen (def->replacement)); + struct macro_buffer replacement_list (def->replacement, + strlen (def->replacement)); scan (dest, &replacement_list, &new_no_loop, lookup_func, lookup_baton); return 1; } else if (def->kind == macro_function_like) { - struct cleanup *back_to = make_cleanup (null_cleanup, 0); - int argc = 0; - struct macro_buffer *argv = NULL; - struct macro_buffer substituted; - struct macro_buffer substituted_src; - struct macro_buffer va_arg_name = {0}; + struct macro_buffer va_arg_name; int is_varargs = 0; if (def->argc >= 1) @@ -1299,8 +1248,7 @@ expand (const char *id, { /* In C99-style varargs, substitution is done using __VA_ARGS__. */ - init_shared_buffer (&va_arg_name, "__VA_ARGS__", - strlen ("__VA_ARGS__")); + va_arg_name.set_shared ("__VA_ARGS__", strlen ("__VA_ARGS__")); is_varargs = 1; } else @@ -1313,43 +1261,36 @@ expand (const char *id, /* In GNU-style varargs, the name of the substitution parameter is the name of the formal argument without the "...". */ - init_shared_buffer (&va_arg_name, - def->argv[def->argc - 1], - len - 3); + va_arg_name.set_shared (def->argv[def->argc - 1], len - 3); is_varargs = 1; } } } - make_cleanup (free_current_contents, &argv); - argv = gather_arguments (id, src, is_varargs ? def->argc : -1, - &argc); - + std::vector argv; /* If we couldn't find any argument list, then we don't expand this macro. */ - if (! argv) - { - do_cleanups (back_to); - return 0; - } + if (!gather_arguments (id, src, is_varargs ? def->argc : -1, + &argv)) + return 0; /* Check that we're passing an acceptable number of arguments for this macro. */ - if (argc != def->argc) + if (argv.size () != def->argc) { - if (is_varargs && argc >= def->argc - 1) + if (is_varargs && argv.size () >= def->argc - 1) { /* Ok. */ } /* Remember that a sequence of tokens like "foo()" is a valid invocation of a macro expecting either zero or one arguments. */ - else if (! (argc == 1 + else if (! (argv.size () == 1 && argv[0].len == 0 && def->argc == 0)) error (_("Wrong number of arguments to macro `%s' " "(expected %d, got %d)."), - id, def->argc, argc); + id, def->argc, int (argv.size ())); } /* Note that we don't expand macro invocations in the arguments @@ -1358,10 +1299,9 @@ expand (const char *id, splicing operator "##" don't get macro references expanded, so we can't really tell whether it's appropriate to macro- expand an argument until we see how it's being used. */ - init_buffer (&substituted, 0); - make_cleanup (cleanup_macro_buffer, &substituted); + struct macro_buffer substituted (0); substitute_args (&substituted, def, is_varargs, &va_arg_name, - argc, argv, no_loop, lookup_func, lookup_baton); + argv, no_loop, lookup_func, lookup_baton); /* Now `substituted' is the macro's replacement list, with all argument values substituted into it properly. Re-scan it for @@ -1373,11 +1313,9 @@ expand (const char *id, text pointer around, and we still need to be able to find `substituted's original text buffer after scanning it so we can free it. */ - init_shared_buffer (&substituted_src, substituted.text, substituted.len); + struct macro_buffer substituted_src (substituted.text, substituted.len); scan (dest, &substituted_src, &new_no_loop, lookup_func, lookup_baton); - do_cleanups (back_to); - return 1; } else @@ -1465,7 +1403,7 @@ scan (struct macro_buffer *dest, that to DEST. */ if (tok.text > original_src_start) { - appendmem (dest, original_src_start, tok.text - original_src_start); + dest->appendmem (original_src_start, tok.text - original_src_start); dest->last_token = dest->len; } @@ -1479,7 +1417,7 @@ scan (struct macro_buffer *dest, src, copy it to dest. */ if (src->len) { - appendmem (dest, src->text, src->len); + dest->appendmem (src->text, src->len); dest->last_token = dest->len; } } @@ -1490,21 +1428,16 @@ macro_expand (const char *source, macro_lookup_ftype *lookup_func, void *lookup_func_baton) { - struct macro_buffer src, dest; - struct cleanup *back_to; - - init_shared_buffer (&src, source, strlen (source)); + struct macro_buffer src (source, strlen (source)); - init_buffer (&dest, 0); + struct macro_buffer dest (0); dest.last_token = 0; - back_to = make_cleanup (cleanup_macro_buffer, &dest); scan (&dest, &src, 0, lookup_func, lookup_func_baton); - appendc (&dest, '\0'); + dest.appendc ('\0'); - discard_cleanups (back_to); - return gdb::unique_xmalloc_ptr (dest.text); + return gdb::unique_xmalloc_ptr (dest.release ()); } @@ -1522,23 +1455,18 @@ macro_expand_next (const char **lexptr, macro_lookup_ftype *lookup_func, void *lookup_baton) { - struct macro_buffer src, dest, tok; - struct cleanup *back_to; + struct macro_buffer tok; /* Set up SRC to refer to the input text, pointed to by *lexptr. */ - init_shared_buffer (&src, *lexptr, strlen (*lexptr)); + struct macro_buffer src (*lexptr, strlen (*lexptr)); /* Set up DEST to receive the expansion, if there is one. */ - init_buffer (&dest, 0); + struct macro_buffer dest (0); dest.last_token = 0; - back_to = make_cleanup (cleanup_macro_buffer, &dest); /* Get the text's first preprocessing token. */ if (! get_token (&tok, &src)) - { - do_cleanups (back_to); - return 0; - } + return 0; /* If it's a macro invocation, expand it. */ if (maybe_expand (&dest, &tok, &src, 0, lookup_func, lookup_baton)) @@ -1546,15 +1474,13 @@ macro_expand_next (const char **lexptr, /* It was a macro invocation! Package up the expansion as a null-terminated string and return it. Set *lexptr to the start of the next token in the input. */ - appendc (&dest, '\0'); - discard_cleanups (back_to); + dest.appendc ('\0'); *lexptr = src.text; - return dest.text; + return dest.release (); } else { /* It wasn't a macro invocation. */ - do_cleanups (back_to); return 0; } } -- 2.30.2