9 ms·
The Use of assert() in SQLite
- draw_down 8y ago“The SQLite developers believe that the lack of assert() disqualifies Go as a language for serious development work.“ Spicy!
- kalecserk 8y ago> In SQLite, the presence of assert(X) means that the developers have a proof that X is always true. Almost impossible to prove anything in a sufficiently complex system such as SQLite. The difference between assert and always seems arbitrary IMHO
- saagarjha 8y agoThe difference is that assert has been “proved” to be true, while ALWAYS is merely thought to be true.
- chris_wot 8y agoI’m still uncertain what they mean by “prove” though.
- kevingadd 8y agosqlite has extremely thorough test coverage with something approaching 100% branch coverage and such wide production usage it's basically proven that a lot of this stuff works.
- nn3 8y agoMost (all?) of the production usage probably doesn't enable the assert though.
- hcs 8y agoProbably the same mechanism by which most proofs work, not fully formal but it seems to follow. Without the proofs in comments (or transitively through other asserts) it's up to the reader to recreate the argument, though. The purpose is to be more explicit than a comment, I suspect, using the same notation as the code.
- viraptor 8y agoYou're only using it locally for specific invariants. That doesn't have to depend on the system's complexity. For example assert(2+2==4) works the same, regardless of it's only a one line app, or in the middle of an OS kernel.
- shaklee3 8y ago> The SQLite developers believe that the lack of assert() disqualifies Go as a language for serious development work. Great comment.
- rileymat2 8y agoI did not understand this comment, as they also wrote ALWAYS, NEVER and testcase. Why could you not write your own assert in Go? (I have not programmed in Go)
- drb91 8y agoYou can always log to stderr with a stack trace and exit with a non zero static code. I’m guessing they’re referring to the fact that the built in panic keyword can be “caught” further up the stack a la an exception.
- hermitdev 8y agoIMO, far better to log, then abort/core. I want to be able to inspect the state of the failed case, not just know that it failed.
- catlifeonmars 8y agoIt’s far better to let the caller decide. As a user of a library, I absolutely do not want a library terminating my process. Sure, some programs can benefit from this, but some programs need to guarantee that they keep running. Being able to recover (Go’s counterpart to panic()) allows the caller to decide which behavior they want.
- hermitdev 8y agoI'm not sure I agree. There's a very, very small limited set of conditions that a client can recover from a condition that would trigger an assertion failure. Dont want an abort? Then use the release libs where those have been preprocesed out. Conversely, at a former employer we used a set of 3rd party database libs for ODBC on Linux that for the smallest config error would emit nothing to stderr and call exit(1) from a shared lib. that was beyond annoying
- klodolph 8y agoI’d like to mention Go. This seems to me like a pointless quibble about how to catch and handle errors, the kind of argument you see buried in the HN comments and gloss over because you’ve heard it a bunch of times before. The assert() macro in C is roughly just if ... { panic() } in Go. You want that, use it. So the idea that getting rid of assert is “essentially telling developers that they are not allowed to do run-time verification of invariants” is completely nonsensical. The Go philosophy against assert() and assertion libraries is largely that the error messages produced by assert() are low quality. The idea is that you should not just have a stack trace, you should have a good error message. The assert() macro can give you a stack trace with some effort, but rarely gives you a good message. Good use of panic() is easy and gives you both. It’s also used mainly for runtime invariant checking, although there are some other ways it can be used (since it can be caught).
- hermitdev 8y agoI dont kbow about go, but in C or C++, it's a common idiom to do something like: asset(condition && "static reason why we expected this condition to be true"); Unfortunately, this doesn't allow for a dynamic string, but does generally present a message about the assert. Also, if properly configured, a failed assert results in an abort and should generate a core/crash dump. Pretty useful to be able to load in a debugger for further inspection. But to the point about provide a good message: that's on the developer, and its going to be true no matter the language.
- htfy96 8y agoif (argc != 2) throw std::runtime_error("Expect 1 argument. Actual "s + std::to_string(argc - 1)); Don't get terrified by exception. If you don't catch any exception, then it is mostly abort() with dynamic string
- Asooka 8y agoIt still incurs a nontrivial runtime cost, unless you've ifdef'd it away in release builds, but even so it is a heavy price to pay in debug or semi-debug^ builds for things like container operator[] bounds checking. The best implementation of assert I've seen raises SIGTRAP on error, which neatly lets you attach a debugger and break when the error occurs, at which point you have the full stack trace and program state. ^ semi-debug in our case means minimal optimisations and debug asserts enabled.
- pornel 8y agoOoh, I love the idea of `testcase(X)` to instrument test coverage.
- repsilat 8y agoInteresting how it's different to branch coverage. If you have if(X) {} else {} if(Y) {} else {} "normal test coverage" is weaker than SQLite tests with the following lines added: testcase(X && Y); testcase(X && !Y); testcase(!X && Y); testcase(!X && !Y); I wasn't sure when reading the article, to be honest, but making sure you test things you don't explicitly branch on is kinda cool.
- gshrikant 8y agoI'm probably being a blockhead but I didn't quite get how the testcase macro is used in instrumenting coverage. Can you explain that?
- nairboon 8y agoThe testcase macro is the following: # define testcase(X) if( X ){ sqlite3Coverage(__LINE__); } If the testcase(condition) always evaluates to true, the code coverage analyzer would complain that the if ( X ) {sqlite3...} statement never evaluates to false and the branch coverage drops below 100%, the converse holds conversely if it was never evaluated to true in the first place. Only if the code using the testcase macro gets repeatedly called in a way so that the testcase macro evaluates sometimes to true and sometimes to false, the coverage stays at 100%. sqlite3Coverage() does some dummy work so that the call will not be optimized away by the compiler.
- AndyKelley 8y agoNot only does zig embrace this philosophy, it introduces the concept of "language-level assertions". For example, integer operations, both signed and unsigned, assert that no overflow occured. Likewise many of the casts, such as non truncating integer conversion, assert that the numerical value is unchanged. In safety-protected build modes assertion failures trigger a panic, which is globally overridable, but the default behavior is to print a full stack trace. Finally, the assertion behavior can be modified at any scope. So one can identify the performance bottlenecks and turn assertion failures into undefined behavior.
- FrozenVoid 8y agoFor reference, they use this code instead of assert: #if defined(SQLITE_COVERAGE_TEST) || defined(SQLITE_MUTATION_TEST) # define ALWAYS(X) (1) # define NEVER(X) (0) #elif !defined(NDEBUG) # define ALWAYS(X) ((X)?1:(assert(0),0)) # define NEVER(X) ((X)?(assert(0),1):0) #else # define ALWAYS(X) (X) # define NEVER(X) (X) #endif
- bayareanative 8y agoALWAYS and NEVER don't pass-through their values consistently as claimed in debug builds, they pass through truthiness instead, with actual values in release builds. Perhaps this would be better for debug builds: #define ALWAYS(X) (assert(X), (X)) #define NEVER(X) (assert(!(X)), (X))
- pawelmurias 8y agoThat would duplicate X
- ahaferburg 8y agoIn addition to, not instead of.
- nh2 8y agoI've always found the name `assert()` to be particularly bad. Many people do not know that their compilers optimise away assertions in `-O` ("release") builds. They use assertions for control flow and input validation, which is very wrong. I believe that could be trivially fixed by giving functions proper names that indicate what they do or what they are intended for. For example, calling such an optimised-away assertion function `debug_only_assert()` would immediately rule out such unintentional misuse.
- pmorici 8y agoHa, reminds me of the time I was working on a project that worked great in debug mode and then crashed and burned when they went to compile it in release mode. Turned out someone had put the call to the initialization code inside an assert function. The bug took some time to figure out because all the crashes happened in code that was nowhere near the actual problem and of course when you turned on debugging the problem disappeared.
- strmpnk 8y agoWhile I'm still a fan of the name `assert()`, one alternative that I like is `invariant()`. It seems to capture the idea a little better. The debug vs release build is a bit of a problem still but I think part of the issue is around the idea that debug builds are too slow to use at all or take so long to build anyway so you might as well use release optimizations. This is something to address with faster compilers and better teaching (and perhaps encouraging folks to try learning how to use debuggers effectively).
- senderista 8y agoThat’s exactly what Rust does (debug_assert() is stripped from release builds, assert() stays).
- masklinn 8y ago> I believe that could be trivially fixed by giving functions proper names that indicate what they do or what they are intended for. For example, calling such an optimised-away assertion function `debug_only_assert()` would immediately rule out such unintentional misuse. That is Rust's tack: > [assert!] assertions are always checked in both debug and release builds, and cannot be disabled. > Unlike assert!, debug_assert! statements are only enabled in non optimized builds by default. An optimized build will omit all debug_assert! statements unless -C debug-assertions is passed to the compiler.
- chiph 8y ago> The CORRUPT_DB macro is used in many assert() statements. In functional testing builds, CORRUPT_DB references a global variable that is true if the database file might contain corruption. This variable is true by default, since we do not normally know whether or not a database is corrupt, but during testing while working on databases that are known to be well-formed, that global variable can be set to false. This is interesting - this really reveals their philosophy where they assume that until proven to be good, stuff is considered to be broken.
- macintux 8y agoI haven’t worked with a lot of code bases, but am I correct in assuming this level of precision with error handling is fairly rare? Impressive stuff. Orthogonal: one of my favorite talking points about Erlang is the fact that nearly every line of code is an assertion, and that those assertions are always enabled, even in production. You’d not want to use Erlang for truly high performance code, granted.
- vemv 8y agoInteresting point about Erlang! Where can I find more info on that specific aspect?
- macintux 8y agoIf you can tolerate a video, my talk from Midwest.io covers it to some degree. It’s really just a happy byproduct of the overall design of the language and VM. The two primary features that make it happen are immutability and ubiquitous pattern matching. https://youtu.be/E18shi1qIHU https://youtu.be/E18shi1qIHU