Skip to content

Instantly share code, notes, and snippets.

@erica erica/invariant.swift
Last active May 20, 2019

Embed
What would you like to do?
extension Never {
/// A developer facing statement explaining how program correctness has failed.
public struct Reason: CustomStringConvertible {
public let description: String
public init(_ rationale: String) {
self.description = rationale
}
}
}
extension Never {
/// A policy for checking or bypassing invariants at runtime
public enum Policy {
/// The invariant will always fire
case alwaysFire
/// Compiled such that historical asserts normally fire, and
/// `_isDebugAssertConfiguration` returns true
case debugConfiguration
/// Compiled with -O, -Ounchecked, excluding -Onone, -Oplayground. This
/// test is currently uncheckable and using this policy will always fail
case optimizedConfiguration // FIXME:
/// Compiled with -Onone, -Oplayground, excluding -O, -Ounchecked. This
/// test is currently uncheckable and using this policy will always fail
case unoptimizedConfiguration // FIXME:
public func shouldFire() -> Bool {
switch self {
case .alwaysFire:
return true
case .debugConfiguration:
return _isDebugAssertConfiguration()
// FIXME: This needs configuration support
case .optimizedConfiguration:
return _isFastAssertConfiguration() // _isReleaseAssertConfiguration()
// FIXME: This needs configuration support
case .unoptimizedConfiguration:
return !_isFastAssertConfiguration()
}
}
}
}
// Universal reasons why code should die at runtime.
extension Never.Reason {
/// Die because this code branch should be unreachable.
public static let unreachable = Never.Reason("Should never be reached during execution.")
/// Die because this method or function has not yet been implemented.
public static let notYetImplemented = Never.Reason("Not yet implemented.")
/// Die because a default method must be overriden by a
/// subtype or extension.
public static let subtypeMustOverride = Never.Reason("Must be overriden in subtype.")
/// Die because this functionality should never be called,
/// typically to silence requirements.
public static let mustNotBeCalled = Never.Reason("Should never be called.")
}
/// A developer-facing typealias that provides a narrated end (`Abort.because`,
/// `Abort.if`) to program execution.
public typealias Abort = Invariant.Trap
/// A truth that holds during all or some portion of program execution.
public enum Invariant {
/// A type motivating the end of program execution due to a failed invariant.
public struct Trap {
/// Stops execution and prints a reason why the program execution trapped.
///
/// This method always succeeds. It does not rely on policies to determine
/// whether the call should execute or not.
///
///
/// - Parameters:
/// - reason: a developer-facing printable reason
/// - file: The file name to print with `reason`. The default is the file
/// where `Trap.because(_:file:line:function:)` is called.
/// - line: The line number to print along with `reason`. The default is the
/// line number where `Trap.because(_:file:line:function:)` is called.
/// - function: The source function to print along with `reason`. The default
/// is the function where `Trap.because(_:file:line:function:)` is called.
public static func because(
_ reason: @autoclosure () -> CustomStringConvertible,
file: StaticString = #file,
line: UInt = #line,
function: StaticString = #function
) -> Never {
let message = "Failed assertion in \(function) - \(reason())"
// FIXME: This should call _assertionFailure as the original does
fatalError(message, file: file, line: line)
}
/// Conditionally stops execution, printing a reason why the program execution
/// trapped.
///
/// - Parameters:
/// - condition: a Boolean test
/// - reason: a developer-facing printable reason
/// - policy: a runtime `Never.Policy` policy that determines whether
/// the test should be executed under the current compilation conditions.
/// This defaults to always fire, where the test ignores build configuration.
/// - file: The file name to print with `reason`. The default is the file
/// where `Trap.if(_:because:file:line:function:)` is called.
/// - line: The line number to print along with `reason`. The default is the
/// line number where `Trap.if(_:because:file:line:function:)` is called.
/// - function: The source function to print along with `reason`. The default
/// is the function where `Trap.if(_:because:file:line:function:)` is called.
public static func `if`(
_ condition: @autoclosure () -> Bool,
because reason: @autoclosure () -> CustomStringConvertible,
policy: Never.Policy = .alwaysFire,
file: StaticString = #file,
line: UInt = #line,
function: StaticString = #function
) {
guard policy.shouldFire() else { return }
guard condition() else { return}
Abort.because(reason(), file: file, line: line, function: function)
}
}
}
// Provides entry points to access `TrappingReason` namespacing and IDE-supported
// auto completion.
extension Abort {
/// A developer-facing typealias that provides a rationale for trapping.
public typealias TrappingReason = Never.Reason
/// Stops execution and prints a reason why the program execution trapped.
///
/// This method always succeeds. It does not rely on policies to determine
/// whether the call should execute or not.
///
/// For example:
///
/// ```
/// Abort.because(.mustNotBeCalled)
/// Abort.because(.notYetImplemented)
/// Abort.because(.subtypeMustOverride)
/// Abort.because(.unreachable)
/// Abort.because(.missingOutlet("tosAgreementCheckbox"))
/// ```
///
/// - Parameters:
/// - reason: a developer-facing printable reason
/// - file: The file name to print with `reason`. The default is the file
/// where `Trap.because(_:file:line:function:)` is called.
/// - line: The line number to print along with `reason`. The default is the
/// line number where `Trap.because(_:file:line:function:)` is called.
/// - function: The source function to print along with `reason`. The default
/// is the function where `Trap.because(_:file:line:function:)` is called.
public static func because(
_ reason: TrappingReason,
file: StaticString = #file,
line: UInt = #line,
function: StaticString = #function
) -> Never {
let message = "Failed assertion in \(function) - \(reason)"
// FIXME: This should call _assertionFailure as the original does
fatalError(message, file: file, line: line)
}
/// Conditionally stops execution, printing a reason why the program execution
/// trapped.
///
/// For example:
///
/// ```swift
/// Abort.if(data == nil, because: "data must not be nil")
/// Abort.if(size < 0, because: "size must be non-negative")
/// Abort.if(capacity < 0, because: "capacity must be non-negative")
/// Abort.if(size > capacity, because: "size must fit within capacity")
/// ```
///
/// - Parameters:
/// - condition: a Boolean test
/// - reason: a developer-facing printable reason
/// - policy: a runtime `Never.Policy` policy that determines whether
/// the test should be executed under the current compilation conditions.
/// This defaults to always fire, where the test ignores build configuration.
/// - file: The file name to print with `reason`. The default is the file
/// where `Trap.if(_:because:file:line:function:)` is called.
/// - line: The line number to print along with `reason`. The default is the
/// line number where `Trap.if(_:because:file:line:function:)` is called.
/// - function: The source function to print along with `reason`. The default
/// is the function where `Trap.if(_:because:file:line:function:)` is called.
public static func `if`(
_ condition: @autoclosure () -> Bool,
because reason: TrappingReason,
policy: Never.Policy = .alwaysFire,
file: StaticString = #file,
line: UInt = #line,
function: StaticString = #function
) {
guard policy.shouldFire() else { return }
guard condition() else { return }
Abort.because(reason, file: file, line: line, function: function)
}
}
/// A developer-facing typealias that provides invariant testing
public typealias Assert = Invariant
// An alternate entry point to `Trap.if`, expressed as `Assert.that`, using
// negative logic. Assertions default to using debug condition policies
extension Assert {
/// A developer-facing typealias that provides a rationale for why the
/// invariant cannot fail. These developer-facing rationales explain why
/// the program traps should an invariant fail to hold
public typealias InvariantRationale = Never.Reason
/// Conditionally stops execution, printing a reason why the program execution
/// trapped.
///
/// For example,
///
/// ```swift
/// Assert.that(data != nil, because: "data must not be nil")
/// Assert.that(size >= 0, because: "size must be non-negative")
/// Assert.that(capacity >= 0, because: "capacity must be non-negative")
/// Assert.that(size <= capacity, because: "size must fit within capacity")
/// ```
///
/// - Parameters:
/// - condition: a Boolean test
/// - reason: a developer-facing printable reason
/// - policy: a runtime `Never.Policy` policy that determines whether
/// the test should be executed under the current compilation conditions.
/// This defaults to debug configuration, where the test is ignored for
/// release builds.
/// - file: The file name to print with `reason`. The default is the file
/// where `Trap.if(_:because:file:line:function:)` is called.
/// - line: The line number to print along with `reason`. The default is the
/// line number where `Trap.if(_:because:file:line:function:)` is called.
/// - function: The source function to print along with `reason`. The default
/// is the function where `Trap.if(_:because:file:line:function:)` is called.
public static func that(
_ condition: @autoclosure () -> Bool,
because reason: @autoclosure () -> CustomStringConvertible,
policy: Never.Policy = .debugConfiguration,
file: StaticString = #file,
line: UInt = #line,
function: StaticString = #function
) {
guard policy.shouldFire() else { return }
guard !condition() else { return }
Abort.because(reason(), file: file, line: line, function: function)
}
}
// Provide entry points to access InvariantRationale namespacing and auto completion
extension Assert {
/// Conditionally stops execution, printing a reason why the program execution
/// trapped.
///
/// For example:
///
/// ```swift
/// extension InvariantRationale {
/// public static func missingOutlet(_ name: String) -> InvariantRationale {
/// return InvariantRationale("Outlet '\(name)' is not connected")
/// }
/// }
///
/// // In use
/// Assert.that(false, because: .missingOutlet("Bob"))
/// ```
///
/// - Parameters:
/// - condition: a Boolean test
/// - reason: a developer-facing printable reason
/// - policy: a runtime `Never.Policy` policy that determines whether
/// the test should be executed under the current compilation conditions.
/// This defaults to debug configuration, where the test is ignored for
/// release builds.
/// - file: The file name to print with `reason`. The default is the file
/// where `Trap.if(_:because:file:line:function:)` is called.
/// - line: The line number to print along with `reason`. The default is the
/// line number where `Trap.if(_:because:file:line:function:)` is called.
/// - function: The source function to print along with `reason`. The default
/// is the function where `Trap.if(_:because:file:line:function:)` is called.
public static func that(
_ condition: @autoclosure () -> Bool,
because reason: InvariantRationale,
policy: Never.Policy = .debugConfiguration,
file: StaticString = #file,
line: UInt = #line,
function: StaticString = #function
) {
guard policy.shouldFire() else { return }
guard !condition() else { return }
Abort.because(reason, file: file, line: line, function: function)
}
}
/// Unconditionally prints a given message and stops execution.
///
/// - Parameters:
/// - reason: The string to print.
/// - function: The name of the calling function to print with `reason`. The
/// default is the calling scope where `fatalError(because:, function:, file:, line:)`
/// is called.
/// - file: The file name to print with `reason`. The default is the file
/// where `fatalError(because:, function:, file:, line:)` is called.
/// - line: The line number to print along with `reason`. The default is the
/// line number where `fatalError(because:, function:, file:, line:)` is called.
@inlinable // FIXME(sil-serialize-all)
@_transparent
public func fatalError(
because reason: @autoclosure () -> CustomStringConvertible,
file: StaticString = #file,
line: UInt = #line,
function: StaticString = #function
) -> Never {
// FIXME: This includes the name of the calling function. This should probably
// be redesigned to use _assertionFailure()
Abort.because(reason(), file: file, line: line, function: function)
}
// Even after redesign, none of the following C-style calls currently uses
// policies to determine whether they fire in optimized and debug configurations.
/// Performs a traditional C-style assert with an optional message.
///
/// Use this function for internal sanity checks that are active during testing
/// but do not impact performance of shipping code. To check for invalid usage
/// in Release builds, see `precondition(_:_:file:line:)`.
///
/// * In playgrounds and `-Onone` builds (the default for Xcode's Debug
/// configuration): If `condition` evaluates to `false`, stop program
/// execution in a debuggable state after printing `reason`.
///
/// * In `-O` builds (the default for Xcode's Release configuration),
/// `condition` is not evaluated, and there are no effects.
///
/// * In `-Ounchecked` builds, `condition` is not evaluated, but the optimizer
/// may assume that it *always* evaluates to `true`. Failure to satisfy that
/// assumption is a serious programming error.
///
/// - Parameters:
/// - condition: The condition to test. `condition` is only evaluated in
/// playgrounds and `-Onone` builds.
/// - reason: A reason to print if `condition` is `false`.
/// - file: The file name to print with `reason` if the assertion fails. The
/// default is the file where `assert(_:_:file:line:)` is called.
/// - line: The line number to print along with `reason` if the assertion
/// fails. The default is the line number where `assert(_:_:file:line:)`
/// is called.
@_transparent
public func assert(
that condition: @autoclosure () -> Bool,
because reason: @autoclosure () -> CustomStringConvertible,
file: StaticString = #file,
line: UInt = #line,
function: StaticString = #function
) {
// Only assert in debug mode.
if _isDebugAssertConfiguration() {
Assert.that(condition(),
because: reason(),
file: file, line: line, function: function)
// if !_branchHint(condition(), expected: true) {
// _assertionFailure("Assertion failed", message(), file: file, line: line,
// flags: _fatalErrorFlags())
// }
}
}
/// Checks a necessary condition for making forward progress.
///
/// Use this function to detect conditions that must prevent the program from
/// proceeding, even in shipping code.
///
/// * In playgrounds and `-Onone` builds (the default for Xcode's Debug
/// configuration): If `condition` evaluates to `false`, stop program
/// execution in a debuggable state after printing `reason`.
///
/// * In `-O` builds (the default for Xcode's Release configuration): If
/// `condition` evaluates to `false`, stop program execution.
///
/// * In `-Ounchecked` builds, `condition` is not evaluated, but the optimizer
/// may assume that it *always* evaluates to `true`. Failure to satisfy that
/// assumption is a serious programming error.
///
/// - Parameters:
/// - condition: The condition to test. `condition` is not evaluated in
/// `-Ounchecked` builds.
/// - reason: A reason to print in a playground or `-Onone` build if the
/// `condition` is false.
/// - file: The file name to print with `reason` if the precondition fails.
/// The default is the file where `precondition(_:_:file:line:)` is
/// called.
/// - line: The line number to print along with `reason` if the assertion
/// fails. The default is the line number where
/// `precondition(_:_:file:line:)` is called.
@_transparent
public func precondition(
that condition: @autoclosure () -> Bool,
because reason: @autoclosure () -> CustomStringConvertible,
file: StaticString = #file,
line: UInt = #line,
function: StaticString = #function
) {
// Only check in debug and release mode. In release mode just trap.
if _isDebugAssertConfiguration() {
Assert.that(condition(),
because: reason(),
file: file, line: line, function: function)
}
// The following would have to be fixed
// if !_branchHint(condition(), expected: true) {
// _assertionFailure("Precondition failed", message(), file: file, line: line,
// flags: _fatalErrorFlags())
// }
// } else if _isReleaseAssertConfiguration() {
// let error = !condition()
// Builtin.condfail(error._value)
// }
}
/// Indicates that a precondition was violated.
///
/// 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 `reason`.
///
/// * 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.
///
/// - Parameters:
/// - reason: A reason to print in a playground or `-Onone` build.
/// - file: The file name to print with `reason`. The default is the file
/// where `preconditionFailure(_:file:line:)` is called.
/// - line: The line number to print along with `reason`. The default is the
/// line number where `preconditionFailure(_:file:line:)` is called.
@_transparent
public func preconditionFailure(
because reason: @autoclosure () -> CustomStringConvertible,
file: StaticString = #file,
line: UInt = #line,
function: StaticString = #function
) -> Never {
// Only check in debug and release mode. In release mode just trap.
if _isDebugAssertConfiguration() {
Abort.because(reason(),
file: file, line: line, function: function)
// _assertionFailure("Fatal error", reason(), file: file, line: line,
// flags: _fatalErrorFlags())
// } else if _isReleaseAssertConfiguration() {
// Builtin.int_trap()
}
// _conditionallyUnreachable()
}
/// Indicates that an internal sanity check failed.
///
/// Use this function to stop the program, without impacting the performance of
/// shipping code, when control flow is not expected to reach the call---for
/// example, in the `default` case of a `switch` where you have knowledge that
/// one of the other cases must be satisfied. To protect code from invalid
/// usage in Release builds, see `preconditionFailure(_:file:line:)`.
///
/// * In playgrounds and -Onone builds (the default for Xcode's Debug
/// configuration), stop program execution in a debuggable state after
/// printing `reason`.
///
/// * In -O builds, has no effect.
///
/// * In -Ounchecked builds, the optimizer may assume that this function is
/// never called. Failure to satisfy that assumption is a serious
/// programming error.
///
/// - Parameters:
/// - reason: A reason to print in a playground or `-Onone` build.
/// - file: The file name to print with `reason`. The default is the file
/// where `assertionFailure(_:file:line:)` is called.
/// - line: The line number to print along with `reason`. The default is the
/// line number where `assertionFailure(_:file:line:)` is called.
@inlinable
@inline(__always)
public func assertionFailure(
because reason: @autoclosure () -> CustomStringConvertible,
file: StaticString = #file,
line: UInt = #line,
function: StaticString = #function
) {
if _isDebugAssertConfiguration() {
Abort.because(reason(),
file: file, line: line, function: function)
}
// _assertionFailure("Fatal error", message(), file: file, line: line,
// flags: _fatalErrorFlags())
// }
// else if _isFastAssertConfiguration() {
// _conditionallyUnreachable()
// }
}
// MARK: Thought Experiments
// A thought experiment that treats unwrapping as an invariant
extension Optional {
/// Returns the value wrapped within an `Optional` for instances that are
/// guaranteed to be non-nil
public func unwrap(because reason: CustomStringConvertible) -> Wrapped {
guard let wrapped = self else {
Abort.because(reason)
}
return wrapped
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.