[swift-evolution] Asserts should not cause undefined behaviour
Joseph Lord
joseph at human-friendly.com
Mon Dec 28 07:48:31 CST 2015
I propose that assert and assertionFailure should be no-ops (with branch
hints) in unchecked builds as they are in normal release builds rather
than resulting in undefined behaviour in the failure condition.
I would like to kick off a discussion of this. I found the proposal
template useful for setting out my thoughts clearly to trigger the
discussion but I'm not trying jump the discussion phase. I'm open to
radical rewrites or to not proceed if there is opposition. Also if my
understanding of the current code is wrong please let me know.
[Current Situation]
The documented behaviour of assert and assertionFailure in "disable
safety checks" builds (still documented as -Ounchecked) is that the
compiler "may assume that it would evaluate to true" or in the
assertionFailure case never be called.
This documented behaviour would allow the compiler to completely
eliminate tests and branches before or after the assertion and take the
operation deep into undefined behaviour.
https://github.com/apple/swift/blob/cf8baedee2b09c9dd2d9c5519bf61629d1f6ebc8/stdlib/public/core/Assert.swift
(latest commit to this file at time of writing)
[NOTE - Actual current behaviour]
It appears from the code as if the assumption is not currently applied
on the assert method although it is on the assertionFailure case by
means of the _conditionallyUnreachable() call. assert seems to be a
no-op in both normal release and disable safety checks build modes.
It also appears that precondition does not apply the permitted
assumption when in _isFastAssertConfiguration mode.
There also appears to be code in assert to hint the compiler that the
assert will likely be true. This was something that I was planning to
suggest and means that code containing asserts can be faster than the
same code without the assert in release mode.
[Proposed Change]
Change the documentation for assert and assertionFailure so that
behaviour in unchecked mode is the same as in normal release - no
evaluation and no effect.
Change the behaviour of assertionFailure to match the updated documentation.
[Motivation]
1) Expected behaviour based on other languages is for assert to have no
effect in release. (If current behaviour is highly desired another
function name should be used). Having potential dangerous behaviour from
a function that is familiar across languages and is regarded as a safety
feature is undesirable.
2) Adding asserts to code should not make the code more dangerous
whatever the build. Assuming the truth of the assert may lead to runtime
safety checks being skipped and undefined behaviour when a no-op would
be a safe behaviour.
3) "For highly robust code assert and then handle the error anyway"
[Code Complete 2nd Edition section 8.2] While the designed expectation
from the Swift team is to use them for checks against internal
programming errors within a module in a number of cases I use assertions
and assertionFailure calls while processing input data. The code without
the assertion would simply fail to read the input gracefully but as the
developer I want to know immediately if there are realistic cases that
have not been handled so use assertions to flag it to myself that a case
may need better handling. As such there are assertions in my code that
are in error cases that I expect not to occur. I do not want the runtime
safety checks being optimised out. [Being aware of this issue I use
custom assertions but am therefore missing out on the branch hinting of
the stdlib version and others may want similar behaviour and may not
fully read the documentation].
[Impact on existing code]
Loss of performance in cases where assertionFailure is used. Loss of
potential performance improvement option in assert usage. In most cases
this performance loss will be small but there is potential where the
assumption of the value allows large code blocks (to evaluate the
condition) to be eliminated for significant effects but I suspect that
these cases are rare and preconditionFailure could be used instead to
get the performance in unchecked builds (at the cost of release
performance) or an additional method could be added.
[Alternatives]
Renaming assert so that the behaviour is not assumed by users familiar
with other languages. Function name suggestion:
assume/assumeUnreachable
This could either behaviour as the current assert/assertionFailure
documentation or possibly allow the assumption to be made in normal
release mode not just the unchecked. It should be a fatal error in debug
builds if assumption is incorrect.
[Note about precondition]
precondition/preconditionFailure also have undefined behaviour in
unchecked mode but this is not a problem for me for a couple of reasons:
1) I don't have the same prior understanding about what they do.
2) It is clear from release build behaviour that hitting the condition
is always an error.
More information about the swift-evolution
mailing list