Skip to content

Improved invariants #1063

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

Conversation

smowton
Copy link
Contributor

@smowton smowton commented Jun 27, 2017

Based on recent discussions about the shortcomings of the INVARIANT family of macros, here are some improvements:

  1. Most importantly, invariants can now be structured. INVARIANT(false, "Hello world") still works and produces an invariant_failedt as before, but one can also derive from invariant_failedt, add fields to describe an error in a more useful way, and override pretty_print to render them into a human-readable error message. This means that even before catching invariants becomes allowed practice we can be producing structured invariants ready for if and when that changes. The update to tests demonstrates this feature in use.
  2. For consistency, the throw / abort behaviour of invariants is no longer dependent on compile-time conditional definitions; it will now function one way and do that dependably on both development and production builds, hopefully reducing the chance of a surprise in production. Currently it always aborts owing to @martin-cs concerns that a catch(...) may accidentally swallow in invariant it didn't mean to.
  3. (Incidental) the reason expression, whether structured or a simple string, is now lazily evaluated (e.g. INVARIANT(x==y, str1+str2) will only perform the string concatenation if the test fails).

Please get in there now with your design concerns rather than objecting to down-the-line PRs that use these features!

@smowton
Copy link
Contributor Author

smowton commented Jun 27, 2017

@NathanJPhillips requested review also (can't do it formally for some reason)

@smowton smowton requested review from reuk and martin-cs June 27, 2017 10:05
@smowton smowton force-pushed the smowton/feature/improved_invariants branch from c0fea89 to f199e65 Compare June 27, 2017 10:08
/// \param reason : C string giving the reason why the invariant should be true.
void check_invariant(
/// \param out: Stream to print backtrace
void print_backtrace(
Copy link
Contributor

Choose a reason for hiding this comment

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

I appreciate this should have gone in a previous review - but I notice the backtrace only happens under glibc. Given that we currently run the DB platform on musl based platform this seems like not a good limitation

Copy link
Member

Choose a reason for hiding this comment

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

afaik, all images have been moved to debian in the meantime

Copy link
Contributor

Choose a reason for hiding this comment

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

This solution works on gcc and Windows: http://stacktrace.sourceforge.net/
Even if you don't use it it might be worth adding a comment mentioning it in the #else section below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The eternal question of external deps. I'll leave this for subsequent work.

@@ -97,6 +97,31 @@ void cbmc_parse_optionst::eval_verbosity()
ui_message_handler.set_verbosity(v);
}

/// An example of structured invariants-- this contains fields to
Copy link
Member

Choose a reason for hiding this comment

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

I'd suggest to put these tests into a unit test instead of cluttering the tool.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Was just copying the existing clutter :) but I agree will move

/// \param reason : C string giving the reason why the invariant should be true.
void check_invariant(
/// \param out: Stream to print backtrace
void print_backtrace(
Copy link
Member

Choose a reason for hiding this comment

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

afaik, all images have been moved to debian in the meantime

explicit invariant_failedt(const std::string& what) : logic_error(what) {}
explicit invariant_failedt(const char* what) : logic_error(what) {}
invariant_failedt(
const std::string& what):
Copy link
Member

Choose a reason for hiding this comment

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

&what

explicit invariant_failedt(const char* what) : logic_error(what) {}
invariant_failedt(
const std::string& what):
logic_error(what),
Copy link
Member

Choose a reason for hiding this comment

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

indent

}

/// Stores the backtrace set by `invariant_violated`.
std::string backtrace;
Copy link
Member

Choose a reason for hiding this comment

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

maybe make protected?

}

/// Temporary storage for `what`'s return value
mutable std::string what_str;
Copy link
Member

Choose a reason for hiding this comment

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

why can't this be declared locally?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The string needs to live beyond the lifespan of what(). It's a bad interface; the docs for std::logic_error admit as much, but I'd rather produce this on demand than inevitably have some derived class mutator forget to update it.

Copy link
Member

Choose a reason for hiding this comment

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

Ah. I see. exception::what returns a pointer.

@@ -10,6 +10,8 @@ Author: Martin Brain, [email protected]
#define CPROVER_UTIL_INVARIANT_H

#include <stdexcept>
#include <type_traits>
#include <iostream>

/*
** Invariants document conditions that the programmer believes to
Copy link
Member

Choose a reason for hiding this comment

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

/* */ should not be used

@smowton smowton force-pushed the smowton/feature/improved_invariants branch from f199e65 to 1352369 Compare June 27, 2017 15:40
@smowton
Copy link
Contributor Author

smowton commented Jun 27, 2017

Applied @peterschrammel changes except for moving the tests, which needs me to understand how unit tests are being done...

@thk123
Copy link
Contributor

thk123 commented Jun 28, 2017

@smowton as discussed, since you want to check the output of stderr you probably are best using a driver program inside the regressions folder.

However, if you would also like to write a unit test (e.g. to check that under default builds, INVARIANT(false, "hello") throws an exception):

Example unit tests from variable sensitivity domain
Documentation from coding standards
Catch (the unit testing framework) documentation

/// \param reason : (derived from invariant_failedt) error report structure;
/// its backtrace member will be set before it is used.
template<class T>
typename std::enable_if<std::is_base_of<invariant_failedt, T>::value>::type
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason to use templates here rather than just const invariant_failedt & Unless I'm not understanding something, this will only compile with children of invariant_failedt

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see - there is some magic to make different methods exist if the type is a string or a invariant_failedt. Can this not be done through regular method overloading, one that takes an const invariant_failedt & and one that takes const std::string &?

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually I think it probably could work like that in this case. Either taking a const invariant_failedt & and then copying it to a temporary or else taking an invariant_failedt & and having it in the contract that it will be updated with the stacktrace.

Copy link
Contributor Author

@smowton smowton Jun 28, 2017

Choose a reason for hiding this comment

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

I took the true type because when we throw them we'll want to throw the actual type. If I throw an invariant_failedt & then I will attempt to copy-construct an invariant_failedt at the throw site (throws are by value). Such a copy constructor would discard information.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh I see - I didn't know that. I suppose this is cleaner than a virtual clone method on invariant_failedt

/// \param reason : C string giving the reason why the invariant should be true.
void check_invariant(
/// \param out: Stream to print backtrace
void print_backtrace(
Copy link
Contributor

Choose a reason for hiding this comment

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

This solution works on gcc and Windows: http://stacktrace.sourceforge.net/
Even if you don't use it it might be worth adding a comment mentioning it in the #else section below.

class invariant_failedt: public std::logic_error
{
/// Stores the backtrace set by `invariant_violated`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Normally I would explicitly write private: rather than relying on the default in case someone adds something public above.

invariant_failedt(
const std::string &what):
logic_error(what),
backtrace()
Copy link
Contributor

Choose a reason for hiding this comment

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

Unnecessary call to default constructor.

{ \
if(!(CONDITION)) \
invariant_violated(__FILE__, __FUNCTION__, __LINE__, (REASON)); \
} while(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Presumably this is to make a statement that requires a trailing semicolon to make INVARIANT behave syntactically as much like a normal function as possible. If so, a comment saying so would be helpful.

/// \param reason : (derived from invariant_failedt) error report structure;
/// its backtrace member will be set before it is used.
template<class T>
typename std::enable_if<std::is_base_of<invariant_failedt, T>::value>::type
Copy link
Contributor

Choose a reason for hiding this comment

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

Actually I think it probably could work like that in this case. Either taking a const invariant_failedt & and then copying it to a temporary or else taking an invariant_failedt & and having it in the contract that it will be updated with the stacktrace.

/// Set the backtrace stored against this error report.
/// This is overwritten by `invariant_violated`, which is called
/// from the `INVARIANT` family of macros.
void set_backtrace(const std::string &_backtrace)
Copy link
Contributor

@NathanJPhillips NathanJPhillips Jun 28, 2017

Choose a reason for hiding this comment

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

I would consider having this private (probably actually directly accessing the private field) and making invariant_violated a friend or a static member of this class.

@smowton
Copy link
Contributor Author

smowton commented Jun 28, 2017

@thk123 under every build they abort with this patch, but if we change that I will use Catch as you recommend

@smowton smowton force-pushed the smowton/feature/improved_invariants branch from 1352369 to ad188a1 Compare June 28, 2017 10:42
@smowton
Copy link
Contributor Author

smowton commented Jun 28, 2017

@NathanJPhillips @peterschrammel all changes applied except as noted

@smowton smowton force-pushed the smowton/feature/improved_invariants branch from ad188a1 to e449526 Compare June 28, 2017 11:11
@smowton
Copy link
Contributor Author

smowton commented Jun 28, 2017

All changes applied, including moving the tests. @thk123 @peterschrammel @NathanJPhillips ready for re-review.

include ../../src/common

test: driver$(EXEEXT)
@if ! ../test.pl -c ../../../src/cbmc/cbmc ; then \
Copy link
Contributor

Choose a reason for hiding this comment

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

How does this work? Don't you need some shell script that compiles and runs driver.cpp rather than running cbmc?

Copy link
Collaborator

@tautschnig tautschnig left a comment

Choose a reason for hiding this comment

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

I can no longer claim I understand what's going on here. Files being compiled but seemingly never used (driver.cpp), options seemingly nowhere handled (test-invariant-failure). Can we keep it simple, please?


/// Causes an invariant failure dependent on first argument value.
/// Returns 1 on unexpected arguments.
int main(int argc, char** argv) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit picking: Place { on next line

@@ -106,26 +106,6 @@ void cbmc_parse_optionst::get_command_line_options(optionst &options)
}

// Test only; do not use for input validation
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe that comment should be removed as well

@@ -63,7 +63,7 @@ class optionst;
"(java-cp-include-files):" \
"(localize-faults)(localize-faults-method):" \
"(lazy-methods)" \
"(test-invariant-failure)" \
"(test-invariant-failure):" \
Copy link
Collaborator

Choose a reason for hiding this comment

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

I admit I have no idea how this works: all code handling "test-invariant-failure" has been removed. So where is it being processed?

@smowton smowton force-pushed the smowton/feature/improved_invariants branch from e449526 to a7f31e2 Compare June 28, 2017 12:13
@smowton
Copy link
Contributor Author

smowton commented Jun 28, 2017

@thk123 @tautschnig oops I had not committed some changes but was getting away with it due to a stale cbmc. Changed now, but in summary:

  1. Makefile builds driver as a dependency of the test target
  2. driver passed to test.pl as the program under test.

@smowton smowton force-pushed the smowton/feature/improved_invariants branch from a7f31e2 to a61e740 Compare June 28, 2017 12:19
@smowton
Copy link
Contributor Author

smowton commented Jun 28, 2017

@tautschnig that option should have been removed; all your changes now applied.

@smowton smowton force-pushed the smowton/feature/improved_invariants branch from a61e740 to 3871f8f Compare June 28, 2017 12:23
/// \return backtrace with a file / function / line header.
std::string get_backtrace(
const char * const file,
const char * const function,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why are C strings being used here instead of std::string?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Constant strings introduced by the preprocessor I guess.

Copy link
Collaborator

Choose a reason for hiding this comment

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

But then the compiler would happily build const std::string objects from those?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I was just saying I guess @martin-cs presumably did that cos he knew they were always plain old string constants. I can make them std::string if you prefer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am aware that this implies further changes; but given the effort by @reuk to get rid of plain pointers we should stick with that direction.

@@ -10,6 +10,8 @@ Author: Martin Brain, [email protected]
#define CPROVER_UTIL_INVARIANT_H

#include <stdexcept>
#include <type_traits>
#include <iostream>
Copy link
Collaborator

Choose a reason for hiding this comment

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

We must avoid #include <iostream> in a widely used header file to keep compilation times reasonable.

Copy link
Contributor

Choose a reason for hiding this comment

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

If it's just for forward declarations #include <iosfwd> might be acceptable?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Just to follow-up on that: the implementation of the function using std::cerr would thus have to go in the cpp file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've moved the std::cerr stuff into the cpp file

@smowton smowton force-pushed the smowton/feature/improved_invariants branch 2 times, most recently from 80944ef to 9c8a2be Compare June 28, 2017 12:44
@peterschrammel
Copy link
Member

@smowton smowton force-pushed the smowton/feature/improved_invariants branch from b1a89d9 to 83d9b04 Compare June 30, 2017 10:11
@smowton
Copy link
Contributor Author

smowton commented Jul 3, 2017

(fix applied)

@peterschrammel
Copy link
Member

@kroening, this is ready to go.

Copy link
Contributor

@reuk reuk left a comment

Choose a reason for hiding this comment

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

I have a couple of issues with the structure of invariant_failedt, sorry to revive this PR after so long.


/// Describes this error using `backtrace` and `pretty_print`.
/// Cannot be overridden; override `pretty_print` instead.
#ifdef _MSC_VER
Copy link
Contributor

Choose a reason for hiding this comment

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

We should have a macro in util called CBMC_NOEXCEPT or similar which expands to noexcept on platforms which support it, and nothing on those which don't. We could do the same thing with constexpr too.
That way, this function declaration would become

const char *what() const CBMC_NOEXCEPT final

which is a bit cleaner.

/// structured error description. `std::logic_error::what` is
/// overridden to use this and the backtrace to describe the
/// error.
virtual std::string pretty_print() const
Copy link
Contributor

@reuk reuk Jul 10, 2017

Choose a reason for hiding this comment

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

Exceptions already have a virtual const function which returns a string, so I'm not convinced there's a need for another. I would restructure this code like so:

class invariant_failedt : public std::logic_error {
  // We don't need a mutable string holding the return of `what`, so the
  // object is smaller *and* more const-safe
  std::string backtrace;
public:
  // This is not virtual! It will call `what` and add a stack trace
  std::string pretty_print() const {
    try {
      std::ostringstream ss;
      ss << what() << '\n' + backtrace;
      return ss.str();
    } catch (...) {
      return "Exception describing invariant failure";
    }
  }

  // We don't even need to override `what` here, but derived classes are free
  // to do so if they need more structured info.
};

A possible downfall of this approach is that clients need to know to call pretty_print when they catch the exception, rather than what, but I believe this is a reasonable trade-off, as the invariant_failedt no longer needs to store the what_str member, making it smaller and more const-safe.

Copy link
Contributor

@NathanJPhillips NathanJPhillips Jul 11, 2017

Choose a reason for hiding this comment

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

I believe various standard things do call what() - in fact isn't the standard behaviour on uncaught exceptions to print what() and terminate?

@tautschnig
Copy link
Collaborator

@smowton Could you please rebase and poke @kroening to get it merged? With current master, it's seemingly very cumbersome to pass a non-constant reason to INVARIANT (because it uses char* instead of std::string).

@smowton
Copy link
Contributor Author

smowton commented Jul 20, 2017

I've also got reuk's comments to account for, will do this tomorrow morning (on the road today)

@tautschnig
Copy link
Collaborator

Great, thanks!

@kroening
Copy link
Member

Looks ready to go after rebase.

@smowton smowton force-pushed the smowton/feature/improved_invariants branch 2 times, most recently from a4df2b3 to 0c29208 Compare July 21, 2017 14:56
Copy link
Contributor

@reuk reuk left a comment

Choose a reason for hiding this comment

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

LGTM

#define INVARIANT(CONDITION, REASON) do {} while(0)

#define INVARIANT(CONDITION, REASON, ...) do {} while(0)
#define INVARIANT(CONDITION, REASON, ...) do {} while(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this duplication deliberate?

const std::string reason;

invariant_failedt(
const std::string &_file,
Copy link
Contributor

Choose a reason for hiding this comment

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

Very minor point: names starting with an underscore are reserved in the global namespace, and names starting with _[A-Z] are reserved everywhere. Although the usage here is not in violation, I'd prefer to avoid underscore prefixes in general.

@smowton smowton force-pushed the smowton/feature/improved_invariants branch 2 times, most recently from 48781ac to 06f758e Compare July 21, 2017 15:26
@smowton
Copy link
Contributor Author

smowton commented Jul 21, 2017

@kroening ready when you are

@smowton smowton force-pushed the smowton/feature/improved_invariants branch from 06f758e to 493e9be Compare July 23, 2017 11:07
kroening pushed a commit that referenced this pull request Jul 24, 2017
[depends: #1063] Use nullptr to represent null pointers (targets master)
kroening pushed a commit that referenced this pull request Jul 24, 2017
@kroening kroening merged commit 747db78 into diffblue:master Jul 24, 2017
tautschnig added a commit that referenced this pull request Jul 24, 2017
Revert "[depends: #1063] Use nullptr to represent null pointers (targets master)"
theyoucheng pushed a commit to theyoucheng/cbmc that referenced this pull request Sep 15, 2017
Corrected simplify lhs in ai_domain_baset

The boolean flag was the wrong way round for ai_simplify_lhs - this
corrects this.

Added unit tests to validate the return meaning of ai_simplify_lhs

These tests use a mock implementation of the `ai_domain_baset` interface
just to validate that true means no simplification.

Fix nullptr

Fix linter errors, ignoring big-int and miniz

Allow invariants with structured exceptions

Revert "[depends: diffblue#1063] Use nullptr to represent null pointers (targets master)"

goto-symex^2: goto-symex for symbolic execution

Goto merged before jumping out the goto-symex operator loop, for handling GOTO and ASSERT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants