Skip to content

[lldb][NFCI] Remove CommandObjectProcessHandle::VerifyCommandOptionValue #79901

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 14, 2024

Conversation

bulbazord
Copy link
Member

I was refactoring something else but ran into this function. It was somewhat confusing to read through and understand, but it boils down to two steps:

  • First we try OptionArgParser::ToBoolean. If that works, then we're good to go.
  • Second, we try llvm::to_integer to see if it's an integer. If it parses to 0 or 1, we're good.
  • Failing either of the steps above means we cannot parse it into a bool.

Instead of having an integer out param and a bool return value, the interface is better served with an optional -- Either it parses into true or false, or you get back nothing (nullopt).

@llvmbot
Copy link
Member

llvmbot commented Jan 29, 2024

@llvm/pr-subscribers-lldb

Author: Alex Langford (bulbazord)

Changes

I was refactoring something else but ran into this function. It was somewhat confusing to read through and understand, but it boils down to two steps:

  • First we try OptionArgParser::ToBoolean. If that works, then we're good to go.
  • Second, we try llvm::to_integer to see if it's an integer. If it parses to 0 or 1, we're good.
  • Failing either of the steps above means we cannot parse it into a bool.

Instead of having an integer out param and a bool return value, the interface is better served with an optional<bool> -- Either it parses into true or false, or you get back nothing (nullopt).


Full diff: https://github.com/llvm/llvm-project/pull/79901.diff

1 Files Affected:

  • (modified) lldb/source/Commands/CommandObjectProcess.cpp (+48-55)
diff --git a/lldb/source/Commands/CommandObjectProcess.cpp b/lldb/source/Commands/CommandObjectProcess.cpp
index c7b874d1979377..f089a86275dc69 100644
--- a/lldb/source/Commands/CommandObjectProcess.cpp
+++ b/lldb/source/Commands/CommandObjectProcess.cpp
@@ -1591,24 +1591,24 @@ class CommandObjectProcessHandle : public CommandObjectParsed {
 
   Options *GetOptions() override { return &m_options; }
 
-  bool VerifyCommandOptionValue(const std::string &option, int &real_value) {
-    bool okay = true;
+  std::optional<bool> VerifyCommandOptionValue(const std::string &option) {
+    if (option.empty())
+      return std::nullopt;
+
     bool success = false;
     bool tmp_value = OptionArgParser::ToBoolean(option, false, &success);
+    if (success)
+      return tmp_value;
+
+    int parsed_value = -1;
+    if (llvm::to_integer(option, parsed_value)) {
+      if (parsed_value != 0 && parsed_value != 1)
+        return std::nullopt;
 
-    if (success && tmp_value)
-      real_value = 1;
-    else if (success && !tmp_value)
-      real_value = 0;
-    else {
-      // If the value isn't 'true' or 'false', it had better be 0 or 1.
-      if (!llvm::to_integer(option, real_value))
-        real_value = 3;
-      if (real_value != 0 && real_value != 1)
-        okay = false;
+      return parsed_value == 0 ? false : true;
     }
 
-    return okay;
+    return std::nullopt;
   }
 
   void PrintSignalHeader(Stream &str) {
@@ -1666,33 +1666,31 @@ class CommandObjectProcessHandle : public CommandObjectParsed {
     // the user's options.
     ProcessSP process_sp = target.GetProcessSP();
 
-    int stop_action = -1;   // -1 means leave the current setting alone
-    int pass_action = -1;   // -1 means leave the current setting alone
-    int notify_action = -1; // -1 means leave the current setting alone
+    std::optional<bool> stop_action = VerifyCommandOptionValue(m_options.stop);
+    std::optional<bool> pass_action = VerifyCommandOptionValue(m_options.pass);
+    std::optional<bool> notify_action =
+        VerifyCommandOptionValue(m_options.notify);
 
-    if (!m_options.stop.empty() &&
-        !VerifyCommandOptionValue(m_options.stop, stop_action)) {
+    if (!m_options.stop.empty() && !stop_action.has_value()) {
       result.AppendError("Invalid argument for command option --stop; must be "
                          "true or false.\n");
       return;
     }
 
-    if (!m_options.notify.empty() &&
-        !VerifyCommandOptionValue(m_options.notify, notify_action)) {
-      result.AppendError("Invalid argument for command option --notify; must "
-                         "be true or false.\n");
+    if (!m_options.pass.empty() && !pass_action.has_value()) {
+      result.AppendError("Invalid argument for command option --pass; must be "
+                         "true or false.\n");
       return;
     }
 
-    if (!m_options.pass.empty() &&
-        !VerifyCommandOptionValue(m_options.pass, pass_action)) {
-      result.AppendError("Invalid argument for command option --pass; must be "
-                         "true or false.\n");
+    if (!m_options.notify.empty() && !notify_action.has_value()) {
+      result.AppendError("Invalid argument for command option --notify; must "
+                         "be true or false.\n");
       return;
     }
 
-    bool no_actions = (stop_action == -1 && pass_action == -1
-        && notify_action == -1);
+    bool no_actions = (!stop_action.has_value() && !pass_action.has_value() &&
+                       !notify_action.has_value());
     if (m_options.only_target_values && !no_actions) {
       result.AppendError("-t is for reporting, not setting, target values.");
       return;
@@ -1732,14 +1730,14 @@ class CommandObjectProcessHandle : public CommandObjectParsed {
           if (signo != LLDB_INVALID_SIGNAL_NUMBER) {
             // Casting the actions as bools here should be okay, because
             // VerifyCommandOptionValue guarantees the value is either 0 or 1.
-            if (stop_action != -1)
-              signals_sp->SetShouldStop(signo, stop_action);
-            if (pass_action != -1) {
-              bool suppress = !pass_action;
+            if (stop_action.has_value())
+              signals_sp->SetShouldStop(signo, *stop_action);
+            if (pass_action.has_value()) {
+              bool suppress = !*pass_action;
               signals_sp->SetShouldSuppress(signo, suppress);
             }
-            if (notify_action != -1)
-              signals_sp->SetShouldNotify(signo, notify_action);
+            if (notify_action.has_value())
+              signals_sp->SetShouldNotify(signo, *notify_action);
             ++num_signals_set;
           } else {
             result.AppendErrorWithFormat("Invalid signal name '%s'\n",
@@ -1759,40 +1757,35 @@ class CommandObjectProcessHandle : public CommandObjectParsed {
           }
          num_signals_set = num_args;
         }
-        auto set_lazy_bool = [] (int action) -> LazyBool {
-          LazyBool lazy;
-          if (action == -1)
-            lazy = eLazyBoolCalculate;
-          else if (action)
-            lazy = eLazyBoolYes;
-          else
-            lazy = eLazyBoolNo;
-          return lazy;
+        auto set_lazy_bool = [](std::optional<bool> action) -> LazyBool {
+          if (!action.has_value())
+            return eLazyBoolCalculate;
+          return (*action) ? eLazyBoolYes : eLazyBoolNo;
         };
 
         // If there were no actions, we're just listing, don't add the dummy:
         if (!no_actions)
-          target.AddDummySignal(arg.ref(),
-                                set_lazy_bool(pass_action),
-                                set_lazy_bool(notify_action),
-                                set_lazy_bool(stop_action));
+         target.AddDummySignal(arg.ref(), set_lazy_bool(pass_action),
+                               set_lazy_bool(notify_action),
+                               set_lazy_bool(stop_action));
       }
     } else {
       // No signal specified, if any command options were specified, update ALL
       // signals.  But we can't do this without a process since we don't know
       // all the possible signals that might be valid for this target.
-      if (((notify_action != -1) || (stop_action != -1) || (pass_action != -1))
-          && process_sp) {
+      if ((notify_action.has_value() || stop_action.has_value() ||
+           pass_action.has_value()) &&
+          process_sp) {
         if (m_interpreter.Confirm(
                 "Do you really want to update all the signals?", false)) {
           int32_t signo = signals_sp->GetFirstSignalNumber();
           while (signo != LLDB_INVALID_SIGNAL_NUMBER) {
-            if (notify_action != -1)
-              signals_sp->SetShouldNotify(signo, notify_action);
-            if (stop_action != -1)
-              signals_sp->SetShouldStop(signo, stop_action);
-            if (pass_action != -1) {
-              bool suppress = !pass_action;
+            if (notify_action.has_value())
+              signals_sp->SetShouldNotify(signo, *notify_action);
+            if (stop_action.has_value())
+              signals_sp->SetShouldStop(signo, *stop_action);
+            if (pass_action.has_value()) {
+              bool suppress = !*pass_action;
               signals_sp->SetShouldSuppress(signo, suppress);
             }
             signo = signals_sp->GetNextSignalNumber(signo);

Copy link

github-actions bot commented Jan 29, 2024

✅ With the latest revision this PR passed the C/C++ code formatter.

@jimingham
Copy link
Collaborator

This version is still a little awkward because there's an ambiguity about what the incoming string being empty means. In the VerifyCommandOptionValue, it means "No Value" and the return is not distinguishable from "error" - but in a bunch of the places where you use VerifyCommandOptionValue an empty string just means a value wasn't provided, so you end up with constructs where you set the optional (checking the incoming string for empty) then check the input string again so you can tell empty apart from error, THEN make up the error on the outside w/o knowing what actually was wrong with the string.

I wonder if it would be better to add a Status to the call. Then empty input -> "Empty Optional but no error"; error in parsing -> "Empty Optional but error" and result means success. Then you wouldn't have to do two checks on the input string in different places, or make up the error string.

@bulbazord
Copy link
Member Author

This version is still a little awkward because there's an ambiguity about what the incoming string being empty means. In the VerifyCommandOptionValue, it means "No Value" and the return is not distinguishable from "error" - but in a bunch of the places where you use VerifyCommandOptionValue an empty string just means a value wasn't provided, so you end up with constructs where you set the optional (checking the incoming string for empty) then check the input string again so you can tell empty apart from error, THEN make up the error on the outside w/o knowing what actually was wrong with the string.

I did this intentionally so that VerifyCommandOptionValue would not attempt to interpret the result. The caller knows whether or not its input is empty, so it's able to interpret the results of VerifyCommandOptionValue however it wants. Maybe VerifyCommandOptionValue is not a good name for this? It seems to just be trying to take a string and trying to get a bool out of it. Thinking about it further, I'm starting to question the value of VerifyCommandOptionValue... Maybe we should be using OptionArgParser::ToBoolean? That seems to be the end goal anyway.

I wonder if it would be better to add a Status to the call. Then empty input -> "Empty Optional but no error"; error in parsing -> "Empty Optional but error" and result means success. Then you wouldn't have to do two checks on the input string in different places, or make up the error string.

@jimingham
Copy link
Collaborator

This version is still a little awkward because there's an ambiguity about what the incoming string being empty means. In the VerifyCommandOptionValue, it means "No Value" and the return is not distinguishable from "error" - but in a bunch of the places where you use VerifyCommandOptionValue an empty string just means a value wasn't provided, so you end up with constructs where you set the optional (checking the incoming string for empty) then check the input string again so you can tell empty apart from error, THEN make up the error on the outside w/o knowing what actually was wrong with the string.

I did this intentionally so that VerifyCommandOptionValue would not attempt to interpret the result. The caller knows whether or not its input is empty, so it's able to interpret the results of VerifyCommandOptionValue however it wants. Maybe VerifyCommandOptionValue is not a good name for this? It seems to just be trying to take a string and trying to get a bool out of it. Thinking about it further, I'm starting to question the value of VerifyCommandOptionValue... Maybe we should be using OptionArgParser::ToBoolean? That seems to be the end goal anyway.

By treating "empty string" exactly the same as "parse error" this kind of is interpreting the results, requiring a "pre-interpretation" to shadow this.

But the only thing VerifyCommandOptionValue adds in this case is treating 0 => false and 1 => true, and it's not at all clear to me why that isn't the job of ToBoolean. And that API is weird anyway, it takes an input string, checks whether it can reasonably be interpreted as a boolean (including 0->false, 1->true) then converts the boolean value back to an integer value of 0 or 1. It's very possible this is my doing, I can't remember, but that is kind of odd to begin with. And why is it only used for these three booleans and no others?

I think getting rid of it and just using ToBoolean seems the better path.

I wonder if it would be better to add a Status to the call. Then empty input -> "Empty Optional but no error"; error in parsing -> "Empty Optional but error" and result means success. Then you wouldn't have to do two checks on the input string in different places, or make up the error string.

Copy link
Collaborator

@clayborg clayborg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be modify OptionArgParser::ToBoolean(...) to return a std::optional<bool> instead of a local function. It already handles integer strings "0" and "1" correctly. Granted it doesn't allow "00" or "01" to be specified, but I am not sure we care. If we do, we can fix OptionArgParser::ToBoolean(...)

@@ -1591,24 +1591,24 @@ class CommandObjectProcessHandle : public CommandObjectParsed {

Options *GetOptions() override { return &m_options; }

bool VerifyCommandOptionValue(const std::string &option, int &real_value) {
bool okay = true;
std::optional<bool> VerifyCommandOptionValue(const std::string &option) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should modify OptionArgParser::ToBoolean(...) to return a std::optional<bool>. it already handles "0" and "1" strings correctly, so this integer conversion stuff is just not doing anything useful

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was actually in the middle of doing that when I found VerifyCommandOptionValue. I was going to upload a PR for it after removing this.

@jimingham
Copy link
Collaborator

jimingham commented Jan 30, 2024 via email

@bulbazord
Copy link
Member Author

Okay, here's what I'll do then:

  1. I will remove VerifyCommandOptionValue and replace uses of it with OptionArgParser::ToBoolean.
  2. Afterwards, I will change the interface of OptionArgParser::ToBoolean.

…andOptionValue

I was refactoring something else but ran into this function. It was
somewhat confusing to read through and understand, but it boils down to
two steps:
- First we try `OptionArgParser::ToBoolean`. If that works, then we're
  good to go.
- Second, we try `llvm::to_integer` to see if it's an integer. If it
  parses to 0 or 1, we're good.
- Failing either of the steps above means we cannot parse it into a
  bool.

Instead of having an integer out param and a bool return value, it seems
like the interface is better served with an optional<bool> -- Either it
parses into true or false, or you get back nothing (nullopt).
@bulbazord bulbazord force-pushed the verify-command-option-value branch from 3a7810a to 8e78455 Compare January 30, 2024 01:16
@jimingham
Copy link
Collaborator

Sounds like a good plan.

@bulbazord bulbazord changed the title [lldb][NFCI] Minor refactor to CommandObjectProcessHandle::VerifyCommandOptionValue [lldb][NFCI] Remove CommandObjectProcessHandle::VerifyCommandOptionValue Jan 30, 2024
bool success = false;
bool value = OptionArgParser::ToBoolean(m_options.stop, false, &success);
if (!success) {
result.AppendError(
Copy link
Collaborator

@jimingham jimingham Jan 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is up to you, since the old code also lied. But this error is not true, the values can also be yes, no, 1, 0. Really should be: should be a <boolean> value, and then all we have to do is teach help <boolean> not to lie:
(lldb) help <boolean> <boolean> -- A Boolean value: 'true' or 'false'

It might be nice if these OptionArgParser::ToWhatever functions could help produce the error string, it's awkward that everybody has to make up their own version of the "what is right" string.

@bulbazord
Copy link
Member Author

@clayborg how does this look to you?

@bulbazord
Copy link
Member Author

ping @clayborg How does this look now?

Comment on lines +1653 to +1655
if (!m_options.stop.empty()) {
bool success = false;
bool value = OptionArgParser::ToBoolean(m_options.stop, false, &success);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we do this error checking in CommandObjectProcessHandle::CommandOptions::SetOptionValue(...)? We can return an error from there instead of making it successfully though option parsing only to look closer at the option values and return an error later in this function?

Also, I see many places use this version of OptionArgParser::ToBoolean(...), and there is a ton of duplicated code like:

  • call OptionArgParser::ToBoolean(...)
  • check value of success
  • make error string and everyone picks their own "hey this isn't a valid boolean value (like Jim already mentioned below).

Since there are already so many uses of the current and only version of OptionArgParser::ToBoolean(...), maybe we make an overload of this:

llvm::Expected<bool> OptionArgParser::ToBoolean(llvm::StringRef ref);

Then in the `CommandObjectProcessHandle::CommandOptions::SetOptionValue(...) function we can do:

case 's':
  if (auto ExpectedBool = OptionArgParser::ToBoolean(option_arg)) {
    stop = *ExpectedBool;
  else
    error = Status(ExpectedBool.takeError());

And the new OptionArgParser::ToBoolean() overload would return an error as Jim wants below:

"Invalid <boolean> value "%s"

Where %s gets substitued with "option_arg.str().c_str()". And then the help would help people figure this out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My plan (that I wrote up 2 weeks ago) was to remove VerifyCommandOptionValue entirely in this commit and then refactor ToBoolean the way you suggested as a follow-up to this change. Does that sound good to you?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is fine.

@bulbazord bulbazord merged commit 307cd88 into llvm:main Feb 14, 2024
@bulbazord bulbazord deleted the verify-command-option-value branch February 14, 2024 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants