This is an archive of the discontinued LLVM Phabricator instance.

[Symbol] Add InvalidTypeError, a force-checked recoverable error
AbandonedPublic

Authored by vsk on Feb 28 2018, 2:22 PM.

Details

Summary

LLDB CompilerTypes may be invalid, i.e they should be checked for
validity before use.

Compared to a more typical model in which only valid types exist [1],
this has a few disadvantages:

  1. The debugger is guaranteed to operate on invalid types as if they

were valid, leading to unexpected or unpredictable results. Because type
validity checks are not mandatory, they can and will be skipped.

E.g in this patch I chose a random function which returns a type. Its
callers checked for invalid results inconsistently, hiding a bug.

  1. Operations on types have high complexity, because each operation has

fallback paths to deal with invalid types [2]. Beyond making code hard
to read, this results in redundant double-checking of type validity.

Going forward, we should transition to a model in which CompilerTypes
are either valid or do not exist. This is a first, incremental step
towards that goal. Ultimately we should delete "Type::IsValid()".

[1] See llvm::Type, clang::Type, swift::Type, etc. All of these either
exist and are valid, or do not exist.

[2] See the methods in CompilerType. Each of these branch on the type's
validity.

[3] For an overview of the principles and advantages of llvm::Error, see
Lang Hames's talk: https://www.youtube.com/watch?v=Wq8fNK98WGw.

Diff Detail

Event Timeline

vsk created this revision.Feb 28 2018, 2:22 PM

Going forward, we should transition to a model in which CompilerTypes are either valid or do not exist.

I don't understand very well how the LLDB type system works so excuse my naive questions: Does this account for lazyness? I.e., could there be value in having an unverified type that might be sufficient for what LLDB is trying to do with it, where validating it (which may involve recursively materializing all of its children) might fail? I could imagine that for some use-cases just knowing the size of a type would be sufficient.
I guess what I'm trying to say is: Are there code paths in LLDB that do something useful with a type where type.IsValid()==false ?

JDevlieghere added inline comments.Feb 28 2018, 2:36 PM
include/lldb/Symbol/CompilerType.h
446

I think this should be called InvalidTypeError?

I agree that we should start using this in a lot more places. I think you should be able to shorten the logging code a bit by using the LLDB_LOG_ERROR macro, which I've created for this purpose.

source/Plugins/ExpressionParser/Clang/ClangExpressionDeclMap.cpp
334–339

How about:

if (!user_type_or_err) {
  LLDB_LOG_ERROR(log, user_type_or_err.takeError(), "{0}");
  return false;
}
zturner added inline comments.Feb 28 2018, 2:59 PM
source/Plugins/ExpressionParser/Clang/ClangExpressionDeclMap.cpp
375–380

I wonder if we need a macro like

#define RETURN_IF_UNEXPECTED(item, ret_val, log_categories)           \
  if (auto E = item.takeError()) {                                    \
    std::string Reason = llvm::toString(std::move(E));                \
    Log *log(lldb_private::GetLogIfAllCategoriesSet(log_categories)); \
    if (log) \
      log->Printf("%s", Reason.c_str());
    return ret_val;
  }

then we say:

RETURN_IF_UNEXPECTED(user_type_or_err, false, LIBLLDB_LOG_EXPRESSIONS);

I don't have too strong of an opinion, but as this pattern becomes more pervasive, it's annoying to have to write out 5-6 lines of code every time you call a function that returns an Expected<T>.

Going forward, we should transition to a model in which CompilerTypes are either valid or do not exist.

I don't understand very well how the LLDB type system works so excuse my naive questions: Does this account for lazyness? I.e., could there be value in having an unverified type that might be sufficient for what LLDB is trying to do with it, where validating it (which may involve recursively materializing all of its children) might fail? I could imagine that for some use-cases just knowing the size of a type would be sufficient.
I guess what I'm trying to say is: Are there code paths in LLDB that do something useful with a type where type.IsValid()==false ?

No, I don't think so. Laziness might make a type go from valid (we only got the forward type) to invalid if the attempt to complete it fails for some reason (base class missing, for instance). But a not yet fully completed type will call itself valid, not invalid.

vsk updated this revision to Diff 136433.Feb 28 2018, 4:50 PM
vsk marked an inline comment as done.
vsk edited the summary of this revision. (Show Details)
  • Address some review feedback.
source/Plugins/ExpressionParser/Clang/ClangExpressionDeclMap.cpp
334–339

I think it'd be even more convenient to include the return into the macro, as Zachary suggested.

375–380

Thanks for the suggestion. I've introduced a macro which is pretty similar to what you've shown here.

vsk added a comment.Feb 28 2018, 4:55 PM

Going forward, we should transition to a model in which CompilerTypes are either valid or do not exist.

I don't understand very well how the LLDB type system works so excuse my naive questions: Does this account for lazyness? I.e., could there be value in having an unverified type that might be sufficient for what LLDB is trying to do with it, where validating it (which may involve recursively materializing all of its children) might fail? I could imagine that for some use-cases just knowing the size of a type would be sufficient.
I guess what I'm trying to say is: Are there code paths in LLDB that do something useful with a type where type.IsValid()==false ?

If we discover code like this, transitioning the code into a model where types are force-checked will require caution, but would still be possible/desirable. For example, we could refactor the operations on invalid type s.t they use a different type, i.e one without the same validity requirement as CompilerType.

No, I don't think so. Laziness might make a type go from valid (we only got the forward type) to invalid if the attempt to complete it fails for some reason (base class missing, for instance). But a not yet fully completed type will call itself valid, not invalid.

Thanks for the explanation Jim!

The problem with your RETURN_IF_UNEXPECTED macro is that it make it impossible to put a breakpoint only on the "type was invalid case." To catch that happening while debugging you'd have to write a conditional breakpoint that also checks the error.

That seems like something you would really want to be able to do. If you leave the return outside the macro, then you can break on the return. IMHO that's worth having to write

if (CHECK_IF_ERROR)

return;
labath added inline comments.Feb 28 2018, 5:17 PM
include/lldb/Utility/Log.h
277–282

it looks like this could be just handled by delegating to the LLDB_LOG_ERROR macro.

source/Plugins/ExpressionParser/Clang/ClangExpressionDeclMap.cpp
334–339

Yeah, I'm fine with the new macro as well. I see it mostly as a transitionary measure. Once most of the code uses llvm::Error, we should be able to just propagate that (or do something interesting with the error).

The phase where we are converting everything over seems exactly when we want to be able to easily stop on the error cases when debugging, which using a macro that contains the return makes impossible.

vsk updated this revision to Diff 136450.Feb 28 2018, 5:55 PM

The problem with your RETURN_IF_UNEXPECTED macro is that it make it impossible to put a breakpoint only on the "type was invalid case." To catch that happening while debugging you'd have to write a conditional breakpoint that also checks the error.

Yes, we'd need a conditional breakpoint if more than one InvalidTypeError instance is created (otherwise we'd just break on the constructor). While I hope we never have to deal with that, I'm happy to keep things more explicit for now.

include/lldb/Utility/Log.h
277–282

I missed this earlier because I mistakenly named two variables error_private. At any rate, there's an objection to having a return_if macro so I'm setting it aside.

Asking the person trying to catch a bad CompilerType to understand how the error mechanism works to effect this goal seems a little much, especially 'cause they are going to be focused on the return which your macro would make tantalizingly out of my reach. OTOH, if you put a comment before the macro saying "To catch this error return case, do X" I have no objections to having the macro.

I'm also ok with not having the macro fwiw, just an idea to reduce boilerplate.

vsk updated this revision to Diff 136640.Mar 1 2018, 4:44 PM
vsk retitled this revision from [Symbol] Add InvalidType, a force-checked recoverable error to [Symbol] Add InvalidTypeError, a force-checked recoverable error.
  • While playing around with InvalidTypeError, I found that it's useful to be able to pass a log category mask to the error-logging macros. I've implemented support for that.
  • I also found that the RETURN_IF_UNEXPECTED macro is very convenient in practice. There seems to be no objection to it provided that I document how to set breakpoints for failure cases. I've brought the macro back.

Thanks for your feedback! PTAL.

labath added inline comments.Mar 1 2018, 5:18 PM
include/lldb/Utility/Log.h
259–260

The thing to remember here is that we have multiple log channels. So the unsigned value must be a category from the "lldb" channel. That is by far the most prevalent channel, so it's probably fine that we don't have a shorthand way of specifying the other channels, but it should be called out explicitly.

vsk updated this revision to Diff 136656.Mar 1 2018, 5:43 PM
  • Clarify that the log_categories macro argument is for logging to the "lldb" channel.

Thanks. The logging stuff looks good to me. I know very little about the TypeFromUser class and friends, so someone else should probably ack that.

labath resigned from this revision.Aug 9 2019, 1:56 AM
vsk abandoned this revision.Aug 12 2019, 12:59 PM
vsk added a subscriber: shafik.

I don't think this got a lot of buy-in. Let's revisit this if the need to revamp CompilerType becomes more pressing.