Last active
October 2, 2019 17:50
-
-
Save erica/4a66a1e384f3516663dd0dc239aadedb to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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