Retain overflow checks when compiled with `-Ounchecked`

No, it isn’t: preconditions aren’t assertions. If it’s possible to “thoroughly test and audit” that a precondition isn’t needed, as you stipulate in your hypothetical scenario, then being wrong about that in the future is a violation of your own internal invariants that went into the audit. Thus, it would be an assertion failure, not a precondition failure. By construction, you’ve proved by your thoroughness that it’s an unnecessary precondition in any compiler mode.

Nowhere is anyone arguing that you don’t need to have a debug and release mode.

Um? You don’t have the source code for proprietary Apple frameworks and yet Swift can and does emit and specialize inlinable functions from those frameworks into your product.

Anyway, I think I’ve said enough. As others have pointed out, this is a “works as intended” situation from my perspective as well.

2 Likes

Would preconditions in inlined code be removed? If so, how?

That is today. Are you testing all your code on 32-bit architectures? Are you even thinking about 32-bit architectures? I’m usually not. If I wanted to make demands about what size of integer I was using, I wouldn’t be using Int.

Yes, as I said, they are. Swift modules export serialized SIL for inlining.

1 Like

You specifically mentioned Int8 before... But I will help you out here: even it is Int32 - I don't. The day I encounter the need to use those platforms (if ever comes) - I would. That's what YAGNI is all about.

Coming from C/C++ it's a bit strange Swift doesn't have some min guarantees about Int types (e.g. "Int shall be at least 32 bits") - but as per above this is not my concern today.

That's what I do (Int8, etc) when I'm in need of specific size.

That’s what fatal errors are about. If you are going to need it, it’ll safely crash and you deal with it then.

If you want that guarantee, demand that guarantee. The compiler is currently unable to check that at function boundaries, so you do it by documenting a precondition then checking it.

Note that you cannot satisfy protocol requirements that don’t possess all of the preconditions you’ve added. Again, the compiler isn’t capable of stopping you right now, but that doesn’t mean it’s allowed.

It's theoretical for me as I follow YAGNI, but for those who don't it might be important to know (today) "whether Swift Array is capable of holding 100K elements on all "hypothetical future" platforms as this answer influences their today's decision whether to use an array or, say, a dictionary (along with a completely different algorithm) to hold required number of elements, so that their code written today works correctly in the future without any modifications."

For me it's not a matter of practical concern.

as any of these can happen, we just can't know (and thus care) today:

  • Swift never supports a platform where Int is less than 32 bits
  • Swift dies before this change happens
  • Swift may adjust array API to use Int64 count/indices on those platforms where Int is 16 bits
  • You won't need that in your projects by the time it happens
  • Your code is so obsolete you need to redo it anyway
  • Your code is already rewritten a few times and that doesn't matter anymore
  • You stop being developer by the time this change happens
  • You die by the time this change happens
  • World ends before this change can happen
  • Anything else
2 Likes

I feel like you’re missing the point here:

  • The go-to defaults for arithmetic trigger fatal runtime errors in the event of overflow, which means you don’t need to worry about it until that becomes a problem.
  • Not doing that in -Ounchecked means that -Ounchecked is unusable.
  • Overflow is not a precondition. If it was, those operators would document it as a precondition and all usage of it would have to propagate that precondition or otherwise ensure it is not violated. That is not how people use these operators.
  • Fatal errors (with the current exception of overflow) have defined behavior in all compilation modes: they crash. -Ounchecked does not make forced-unwrapping unsafe, for instance.
  • assert(_:_:file:line:) and precondition(_:_:file:line:) have defined behavior in all compilation modes: when skipped, they are assumed to evaluate as true. If that results in the enclosing function behaving in undocumented ways, that function has undefined behavior.
  • To the best of my knowledge, what happens if a non-wrapping arithmetic operator overflows in -Ounchecked is currently undefined. Since it is not documented as a precondition anywhere, that means that literally any function call in -Ounchecked could produce undefined behavior. The caller wouldn’t know if the callee uses these operators, after all.

Please don’t quote me then edit the quote. I did not say you should demand an explicit number of bits. In fact, I’d usually recommend against it: you could accept any FixedWidthInteger with that precondition in place.

Or just accept Int without a precondition, use the standard operators, and be done. Just don’t assume anything you didn’t ask for.

The edited fragment in brackets should have been "minimal number of bits" and if you mean something else you lost me. It is obvious we are talking past each other.. My example was about an algorithm choice that works on future platforms and can't be resolved with preconditions (unless you want to implement several versions of algorithm for "future proveness"), your examples were replies to something I wasn't even talking about. This clearly leads nowhere constructive, so it is best for me to step aside and leave this space for others. One final note though on the point of order - your wording like exemplified here:

maybe that's just me, but that sound like direct orders about what other people must do and must not do... I assure you I can and will assume anything I want... along with taking full responsibility for my today's assumptions that might be proved incorrect tomorrow, if/when that happens I will responsibly revisit them.

1 Like

Defined behavior is how you define your code to behave. If your code behaves differently when an assumption you didn’t tell anyone about is violated, that is undefined behavior.

Undefined behavior is to be avoided at all costs. That is a fundamental principle of Swift, and I don’t think there is any debate on the matter.

Preconditions say “If this isn’t true, undefined behavior occurs”. That means that undefined behavior occurs when a precondition is violated, and doesn’t occur when one isn’t. Ergo, precondition violations are to be avoided at all costs, and doing so avoids undefined behavior.

Another way to avoid undefined behavior is to define the behavior: if you use a wrapping operator, and say your function wraps, there’s no problem.

According to the documentation and the source code, it does return Never at all optimization levels. Perhaps you were thinking of precondition(_:_:file:line:)? Am I understanding you correctly?


Here's an example of a simple algorithm that increments an Int without the possibility of an overflow:

func printContents<Element>(of array: Array<Element>) {
    var index = 0
    while index < array.count {
        print(array[index])
        index += 1
    }
}

I think it's safe for most Swift programmers to assume they're working with more than 256 bytes of memory, seeing as Int can hold a pointer address.


This is pretty hyperbolic; if you want your program to stop on an overflow, you can still do that, even in -Ounchecked mode:

let (result, overflow) = lhs.addingReportingOverflow(rhs)
guard !overflow else {
    fatalError("integer overflow")
}

If you aren't sure your code will work properly in -Ounchecked mode, then you shouldn't use -Ounchecked mode.


The entire point of preconditions is to prevent undefined behavior. If a precondition check fails in -Onone or -O builds, then it will stop program execution instead of continuing on with undefined behavior. The only exception to this is when using the -Ounchecked mode, which is unrecommended unless you're sure your code doesn't violate preconditions and you want the extra speed.

3 Likes

You’re correct: I’m clearly mistaken on that point, my apologies.

If you audit all of the code to ensure that arithmetic operators aren’t used, even though they are allowed, then sure. You can do that. You’d probably have to avoid using any Swift packages you aren’t willing to fork, since they’re unlikely to do so.

On a related note, I feel the argument that "failing preconditions is correct unless you use -Ounchecked" has no merit. At the point you are programming, you do not know what the optimization mode is going to be. If you are including code in a Swift package, you aren’t even allowed to set it. You could, I suppose, document the precondition “Must not be compiled in -Ounchecked mode”, but that brings me to my main issue with that interpretation.

It doesn’t make any sense in practice: why would you use precondition for that? fatalError does exactly the same thing, and never has undefined behavior. There is literally no reason to use precondition if you don’t want to allow the check to be skipped.

If you think that correct code can violate preconditions, invariants, etc. and only becomes retroactively incorrect when fed to a compiler with -Ounchecked, why would these constructs even exist? Why would -Ounchecked exist?

1 Like

-Ounchecked exists to compile code without precondition checks so that it can be compared to -O builds or, with extreme caution, put into production. If you aren't confident in a package you're using, then you shouldn't compile it with -Ounchecked. It isn't meant to be used with all code, just code that you are confident won't fail any preconditions.

A precondition tells the compiler that you don't want to allow a check to be skipped; -Ounchecked overrides that, telling the compiler to skip all checks, as you've verified that the code satisfies all preconditions. Fatal errors indicate that there is no precondition at a point; the program should just crash.

2 Likes

Preconditions are requirements to avoid undefined behavior, as documented in the program. precondition(_:_:file:line:) is simply a check to make sure that someone did not accidentally violate a requirement.

If violating a requirement does not cause undefined behavior (in any optimization mode!), it is not a precondition. Similarly, violating a precondition should be presumed to cause undefined behavior in the same way that Optional.none.unsafelyUnwrapped should be presumed to. The fact that the outcome is defined in some compilation modes is irrelevant.

Preconditions are not supposed to be enforced by the precondition(_:_:file:line:) check. They are supposed to be enforced by the caller. The check is just a redundancy for safety purposes. It is never supposed to be relied upon.

This is not what undefined behaviour means in the context of the Swift programming language. Undefined behaviour is a technical term of art. Undefined behaviour occurs when a program performs an operation that is forbidden by the Swift language: that is, that the Swift language asserts (axiomatically) may never occur. A good example of undefined behaviour in Swift is concurrent unsynchronized writes to the same memory location: this triggers undefined behaviour.

No they don't. The documentation is quite clear:

Use this function to stop the program when control flow can only reach the call if your API was improperly used. This function’s effects vary depending on the build flag used:

  • In playgrounds and -Onone builds (the default for Xcode’s Debug configuration), stops program execution in a debuggable state after printing message .
  • In -O builds (the default for Xcode’s Release configuration), stops program execution.
  • In -Ounchecked builds, the optimizer may assume that this function is never called. Failure to satisfy that assumption is a serious programming error.

It is very clear that preconditionFailure refers to correct use of the API, and does not imply that violating the precondition triggers undefined behaviour except in one specific case: namely, in -Ounchecked. That is, adding preconditionFailure adds a source of UB into a program when run in -Ounchecked.

As noted above, this is false, explicitly in -Ounchecked.

As noted above, this is false.

11 Likes

I'm afraid it's not just about the overflow checks... -Ounchecked is seriously problematic, a few examples:

struct S { let x = 42 }
var s: S?
guard let s else { preconditionFailure() }
print(s) // S(x: 0) with -Ounchecked

struct S {
    var x = 42
    var y = 42
    init(x: Int) { self.x = x; self.y = 24}
}
var s: S?
guard let s else { preconditionFailure() }
print(s) // S(x: 0, y: 0) with -Ounchecked

class C { func foo() {} }
var c: C?
guard let c else { preconditionFailure() }
c.foo() // crash with -Ounchecked

// let's create a variable of type Never
var never: Never?
guard let never: Never = never else { preconditionFailure() }
print(never) // crash with -Ounchecked

// let's create a variable of type empty enum
enum EmptyEnum {}
var v: EmptyEnum?
guard let emptyEnumValue = v else { preconditionFailure() }
print(emptyEnumValue) // crash with -Ounchecked

precondition(importantCondition)
if !importantCondition {
    // go berserk and corrupt user data with -Ounchecked
}

func transfer(amount: Money, toOtherPersonAccount: AccountNo) {
    // at this point the app all checks are already performed.
    // this is the last frontier:
    var balance: UInt64 = currentBalance() // £1
    precondition(amount <= balance) // £100 <= £1 --> false. Should crash the app!
    balance &-= amount // just got 18 million trillion pounds into my account with -Ounchecked
    proceedWithTransfer(amount)
}

The most dangerous are things that don't crash immediately but cause some other serious damage (data corruption, money loss, etc).

If we change just the overflow checks, to, say:

subscript(index: Int) -> Element {
    get {
        guard index >= 0 && index < count else { fatalError("crash for sure") }
        return elements[index]
    }
    set {
        guard index >= 0 && index < count else { fatalError("crash for sure") }
        elements[index] = newValue
    }
}

The whole class of undefined behaviour illustrated above is still possible by merely using -Ounchecked. Deprecating "precondition" family of calls (along with introducing some "stricter" checks that terminate the app regardless of -Ounchecked") is another possible option but I guess it's a harder sell compared to getting rid of -Ounchecked itself.

2 Likes

I agree with this pitch in principle. -Ounchecked should be usable for code without programmer errors, such as code that has been extensively reviewed for bugs, or code where performance can be prioritized over safety. I know those cases are rare, but they exist.

An overflow isn't a programmer error. It's a dynamic error that can't be prevented statically, and is rare enough that we don't care if our program crashes when it occurs. The closest analogue to overflow I can think of is a try! expression, in which a dynamic error may reasonably occur, but we decide to just crash when it does. try! expressions still check for errors in -Ounchecked mode.

But if -Ounchecked keeps overflow checks, I think there should be a new compiler option to disable them too, so people can still measure the performance cost of overflow checking. To be fair, I can see this can make things overcomplicated. Overflow checks are rare in systems programming languages - even Rust disables them by default in release mode. It might be that anyone who cares about performance enough to use -Ounchecked won't care about checking for overflows.

I think Swift disagrees. There are no failable versions of most of the relevant operators. If Swift felt numeric overflow were a dynamic error, it would provide them, just like it does for e.g. trying to decode a [Unicode] String from raw bytes.

Swift's opinion seems to be that it's a programmer error to even attempt arithmetic that overflows; that the code should either be written so that overflow is statically impossible, or the programmer should have manually checked before invoking the relevant arithmetic operator.

I don't know if that's why Swift behaves as it does regarding overflow and -Ounchecked, but in any case the result is consistent.

It's basically impossible to statically prove that overflow won't occur. For example, if a user inputs the decimal representation of Int.max and it's parsed by the program, and then the program increments it, there'll be an overflow. And I know this might not practically matter, but almost any program will eventually overflow if running for long enough.

I view the crashing behavior as a practical concession, because arithmetic is so ubiquitous and it'd be troublesome to always handle overflows, which would be rare anyway. After reading ErrorHandlingRationale.rst, I believe integer overflow can be classified as a "universal error" akin to running out of memory or overflowing the stack.

The OptimizationTips.rst file suggests to use the overflow operators when you can prove that overflow won't occur. Though I guess it also says that "Swift eliminates integer overflow bugs by checking for overflow".

2 Likes

It means the app won't see incorrect results and won't experience an undefined behaviour due to an overflow as the app will be terminated before it could proceed (making any harm like corrupting user data, etc). Unless it is -Ounchecked mode being discussed...

2 Likes