From b0d23060f6841b5dc4b063c3a81c7732bc29cb1e Mon Sep 17 00:00:00 2001 From: queen-1111 bee Date: Mon, 31 Aug 2026 15:25:55 +0000 Subject: [PATCH] feat(trios): the interface-divergence decision, pure and self-proving (#1111) Two bees ran in parallel with disjoint boundaries (#1110 owned rings/SR-02/QueenBranchCommitter.swift, #1109 owned rings/SR-02/ChatViewModel.swift and rings/SR-00/QueenReviewVerdictRequest.swift). #1110 made baseBranch() return String?, exactly as its criterion asked; the call site lived in the other bee's file. Both honoured the one-owner-per-path rule and the build broke: rings/SR-02/ChatViewModel.swift:4142: error: value of optional type 'String?' must be unwrapped to a value of type 'String' The boundary rule protects against a conflict of writes. It says nothing about a conflict of interfaces: a signature changed in one file breaks a different file, and only a build of the two states together can see it. The acceptance-side half landed with #1128 (runInterfaceDivergenceWatchdog compiles the combined state before any acceptance; transitionToAccepted takes the proof as a parameter, so no path to .accepted can skip it). What remained open was the issue's second criterion: drift-guard judges two worktrees it builds itself, not two states of this tree, so returning the former behaviour moved nothing. This file closes that gap with the decision itself, as a pure rule: - QueenInterfaceDivergence.verdict(between:) takes the interface facts of each lane (declarations whose shape changed, calls and the shapes they expect), overlays the lanes the way verifyCombinedBuild overlays branches, and reports every call that no longer matches, plus the two-changers-one- declaration case. Findings name both sides - who changed what, and who still calls the old shape - because a refusal that does not name its subject sends the reader hunting for the wrong problem. - The single-file suite rides in the same file behind QUEEN_INTERFACE_DIVERGENCE_SELFTEST and runs on the incident's own facts: the #1110/#1109 pair is refused, while a change whose call moved with it, a body-only change, and a new call written against the new shape all pass. The block holds declarations only: -emit-module typechecks even inactive #if branches (measured on the container), so a top-level entry would break the app build; the documented one-line driver keeps the file buildable and the suite runnable where no linker exists. - The revert proof is executable: formerIsolatedVerdict restates the pre-#1111 loop (each lane judged alone, nothing crosses a boundary) and the suite shows it accepts the incident pair that the rule refuses. Revert the rule to that judgement and the incident scenario fails its first check - the check breaks. Verified live against two mutants (the isolated revert and a blanket-refuse revert): both fail the suite loudly and non-zero. Nothing is imported, not even Foundation, so the rule compiles anywhere the Swift standard library does and the suite runs interpreted on the Linux container: 17 checks, 0 failures, parse/typecheck/emit-module green with the flag both off and on. --- .../SR-00/QueenInterfaceDivergence.swift | 664 ++++++++++++++++++ 1 file changed, 664 insertions(+) create mode 100644 trios/rings/SR-00/QueenInterfaceDivergence.swift diff --git a/trios/rings/SR-00/QueenInterfaceDivergence.swift b/trios/rings/SR-00/QueenInterfaceDivergence.swift new file mode 100644 index 0000000000..12d721e738 --- /dev/null +++ b/trios/rings/SR-00/QueenInterfaceDivergence.swift @@ -0,0 +1,664 @@ +// Decides whether parallel lanes diverge by interface: one changed a public +// declaration inside its own boundary, and another owns a call still written +// against the old shape. +// +// Measured on #1110/#1109 (2026-08): two bees ran side by side with disjoint +// boundaries. #1110 owned `rings/SR-02/QueenBranchCommitter.swift` and made +// `baseBranch()` return `String?` - exactly what its criterion asked for. The +// call site lived in `rings/SR-02/ChatViewModel.swift`, which belongs to +// #1109. Both bees honoured the one-owner-per-path rule; neither wrote one +// byte outside its boundary; the build still broke: +// +// rings/SR-02/ChatViewModel.swift:4142: error: value of optional type +// 'String?' must be unwrapped to a value of type 'String' +// +// The boundary rule protects against a conflict of WRITES. It says nothing +// about a conflict of INTERFACES: a signature changed in one file breaks a +// different file, and the only instrument that can see that is a build of the +// two states TOGETHER - which no bee in the loop runs, because each builds +// its own branch alone. +// +// What already exists (#1128): `runInterfaceDivergenceWatchdog` in +// `ChatViewModel` compiles the combined state before any acceptance, and +// `transitionToAccepted` takes the proof as a parameter, so no acceptance +// path can skip it. That closes the acceptance side. What it does not give is +// the decision itself: the watchdog is a compiler run - minutes long, and out +// of reach of any suite - so nothing could answer, cheaply and in words, +// "do these lanes diverge?". That decision is this rule, pure and alone: +// +// - it takes the interface FACTS of each lane (which declarations changed +// shape, which calls expect which shape), not source text and not a +// compiler; +// - it overlays the lanes the same way `verifyCombinedBuild` overlays +// branches, and asks the compiler's question about the result: does any +// call still expect a shape that no longer exists? +// - it answers in findings that name both sides - who changed what, and +// who still calls the old shape - because a refusal that does not name +// its subject sends the reader hunting for the wrong problem. +// +// The rule assumes what the parallel loop already guarantees: every lane +// builds alone against the base. It therefore judges what that guarantee +// cannot see - what happens when the lanes are put together. A stale call +// inside the lane that made the change cannot occur under the precondition +// (that lane would not build alone); if the facts carry one anyway, the rule +// reports it, because the combined tree breaks just the same. +// +// It lives in SR-00 as a separate file because there it needs nothing but +// the Swift standard library - not even Foundation - and is provable by a +// single-file suite, which rides in the same file behind +// `QUEEN_INTERFACE_DIVERGENCE_SELFTEST` (see the bottom). Nothing here may +// depend on `ChatViewModel` or any app type: those files are owned by other +// tasks, and a rule that cannot be compiled alone cannot be checked alone. + +/// The pure decision: given the interface facts of the parallel lanes, do +/// they diverge - would the overlaid tree still satisfy every call? +public enum QueenInterfaceDivergence { + + // MARK: - facts + + /// The part of a declaration a caller can see and a change can break: + /// its parameters and its return type, as canonical text. + /// + /// Whoever extracts the facts canonicalises (one spelling per parameter, + /// default values dropped - they do not change what a caller may pass); + /// this rule only compares, so two spellings of one parameter list must + /// be normalised before they reach it. `text` is the form findings quote. + public struct Signature: Equatable, Hashable { + public let parameters: [String] + public let returns: String + + public init(parameters: [String], returns: String) { + self.parameters = parameters + self.returns = returns + } + + /// The form a finding names the shape by. + public var text: String { + "(" + parameters.joined(separator: ", ") + ") -> " + returns + } + } + + /// One public declaration one lane changed, in a file that lane owns. + /// `path` is project-relative, the form boundaries are written in. + public struct DeclarationChange: Equatable { + public let symbol: String + public let path: String + public let before: Signature + public let after: Signature + + public init(symbol: String, path: String, before: Signature, after: Signature) { + self.symbol = symbol + self.path = path + self.before = before + self.after = after + } + + /// False when only the body changed: the shape a caller sees is the + /// same, so no caller can be broken by it. + public var changedShape: Bool { before != after } + } + + /// One use of a public declaration, as written in a lane's own file, + /// with the shape that use was written against. + public struct Reference: Equatable { + public let symbol: String + public let path: String + public let expects: Signature + + public init(symbol: String, path: String, expects: Signature) { + self.symbol = symbol + self.path = path + self.expects = expects + } + } + + /// One parallel lane: its identity plus the interface facts of its + /// state. No owned-path list, deliberately: boundaries decide who may + /// WRITE, and the write rule already had its say; the divergence this + /// rule judges lives in the facts themselves. + public struct Lane: Equatable { + public let id: String + public let changes: [DeclarationChange] + public let references: [Reference] + + public init(id: String, changes: [DeclarationChange] = [], references: [Reference] = []) { + self.id = id + self.changes = changes + self.references = references + } + } + + // MARK: - the verdict + + /// A named divergence. Both sides are carried, because a refusal that + /// does not name its subject sends the reader hunting for the wrong + /// problem. + public enum Finding: Equatable { + /// A call still expects the shape the declaration no longer has. + /// This is the incident of #1110/#1109: each lane alone is + /// consistent; overlaid, this one call does not compile. + case staleCall( + symbol: String, + changedBy: String, + declaredIn: String, + calledBy: String, + calledFrom: String, + expected: Signature, + found: Signature + ) + + /// Two lanes each changed the same declaration to different shapes. + /// One declaration cannot carry both; the overlay has no winner. + /// (Two lanes changing one file is the boundary rule's business; + /// the same SYMBOL reachable from two files is this one's.) + case twoLanesChangedOneDeclaration( + symbol: String, + path: String, + firstChangedBy: String, + secondChangedBy: String, + firstShape: Signature, + secondShape: Signature + ) + + /// The symbol the finding is about, so findings can be ordered. + public var symbol: String { + switch self { + case let .staleCall(symbol, _, _, _, _, _, _): return symbol + case let .twoLanesChangedOneDeclaration(symbol, _, _, _, _, _): return symbol + } + } + + /// One sentence a review can act on, naming both sides. + public var summary: String { + switch self { + case let .staleCall(symbol, changedBy, declaredIn, calledBy, calledFrom, + expected, found): + return "\(symbol): \(changedBy) changed its declaration in " + + "\(declaredIn) to \(found.text), while \(calledBy) still " + + "calls it from \(calledFrom) expecting \(expected.text). " + + "Each lane builds alone; overlaid, this call does not " + + "compile." + case let .twoLanesChangedOneDeclaration(symbol, path, first, second, + firstShape, secondShape): + return "\(symbol): changed by both \(first) and \(second) in " + + "\(path), to \(firstShape.text) and \(secondShape.text). " + + "One declaration cannot carry both shapes." + } + } + } + + public enum Verdict: Equatable { + /// Overlaid, every call still matches the declaration it calls. + case clear + /// The lanes do not compose. Each finding names one divergence. + case divergent(findings: [Finding]) + + public var isDivergent: Bool { + if case .divergent = self { return true } + return false + } + } + + // MARK: - the decision + + /// Decides whether the lanes diverge by interface. + /// + /// The overlay mirrors `verifyCombinedBuild`: every lane's changes land + /// on the shared base, one shape per symbol. A reference is then checked + /// against the overlaid shape of the symbol it names - from ANY lane, + /// because the combined tree breaks the same whether the stale call sits + /// in the changer's own files or in another lane's. References to + /// symbols nobody changed are not checked: each lane builds alone + /// against the base, so those already agree with it. + /// + /// Symbols two lanes changed to DIFFERENT shapes have no overlay: the + /// finding says so and the verdict is divergent regardless of callers. + public static func verdict(between lanes: [Lane]) -> Verdict { + // One claim per lane per changed shape: who changed what, where. + struct Claim: Equatable { + let lane: String + let change: DeclarationChange + } + var claimsBySymbol: [String: [Claim]] = [:] + for lane in lanes { + for change in lane.changes where change.changedShape { + claimsBySymbol[change.symbol, default: []] + .append(Claim(lane: lane.id, change: change)) + } + } + + var findings: [Finding] = [] + var overlay: [String: (shape: Signature, by: String, in: String)] = [:] + for (symbol, claims) in claimsBySymbol { + let shapes = Set(claims.map { $0.change.after }) + if shapes.count > 1 { + findings.append(.twoLanesChangedOneDeclaration( + symbol: symbol, + path: claims[0].change.path, + firstChangedBy: claims[0].lane, + secondChangedBy: claims[1].lane, + firstShape: claims[0].change.after, + secondShape: claims[1].change.after + )) + // No shape wins the overlay for this symbol; the verdict is + // already divergent and callers of it have nothing to be + // compared against. + continue + } + let first = claims[0] + overlay[symbol] = ( + shape: first.change.after, + by: claims.map { $0.lane }.joined(separator: " & "), + in: first.change.path + ) + } + + for lane in lanes { + for reference in lane.references { + guard let landed = overlay[reference.symbol] else { continue } + if reference.expects != landed.shape { + findings.append(.staleCall( + symbol: reference.symbol, + changedBy: landed.by, + declaredIn: landed.in, + calledBy: lane.id, + calledFrom: reference.path, + expected: reference.expects, + found: landed.shape + )) + } + } + } + + if findings.isEmpty { return .clear } + // Deterministic order, so a suite and a review read the same list. + findings.sort { ($0.symbol, $0.summary) < ($1.symbol, $1.summary) } + return .divergent(findings: findings) + } + + /// `true` when the lanes diverge and must not be accepted in parallel + /// without reconciliation. + public static func isDivergent(between lanes: [Lane]) -> Bool { + verdict(between: lanes).isDivergent + } +} + +// MARK: - the single-file suite +// +// The proof rides in the same file, behind a condition the app build never +// defines, so the rule needs no second file to be checked - and no reviewer +// has to take the rule's word for itself. The suite block contains +// declarations only, deliberately: `-emit-module` (which the app build runs) +// typechecks even INACTIVE `#if` branches, measured on this container, so a +// top-level entry here would break the app build the day the flag is off. +// The entry is a one-line driver instead. +// +// Run the suite, interpreted (no linker needed; this is how it runs on the +// Linux container): +// +// cd trios && ( cat rings/SR-00/QueenInterfaceDivergence.swift; \ +// echo 'QueenInterfaceDivergenceTests.main()' ) > /tmp/qid_suite.swift \ +// && swift -D QUEEN_INTERFACE_DIVERGENCE_SELFTEST /tmp/qid_suite.swift +// +// or compiled, where a toolchain can link, from the same generated file: +// +// swiftc -D QUEEN_INTERFACE_DIVERGENCE_SELFTEST /tmp/qid_suite.swift \ +// -o /tmp/trios_queen_interface_divergence_test \ +// && /tmp/trios_queen_interface_divergence_test +// +// A failing check ends in `fatalError`, which is loud and non-zero on every +// platform without reaching for a C library; the suite imports nothing and +// uses only the Swift standard library, so it runs anywhere the interpreter +// does. +#if QUEEN_INTERFACE_DIVERGENCE_SELFTEST + +enum QueenInterfaceDivergenceTests { + static var failures = 0 + static var checks = 0 + + static func check(_ condition: Bool, _ name: String) { + checks += 1 + if condition { print("ok - \(name)") } else { + failures += 1 + print("FAIL - \(name)") + } + } + + static func equal(_ got: T, _ want: T, _ name: String) { + check(got == want, name) + if got != want { + print(" got: \(got)") + print(" want: \(want)") + } + } + + static func scenario(_ name: String) { print("\n# Scenario: \(name)") } + + // MARK: the incident, as it happened + + /// The facts of #1110/#1109, taken from the tree and the issue text: + /// paths as they are, shapes as they were. #1110 changed the return of + /// `baseBranch` and adapted the one call inside its own file (the + /// `?? "HEAD"` fallback at QueenBranchCommitter.swift:282), so its lane + /// built alone. #1109 owned `ChatViewModel.swift`, whose call was + /// written against the old non-optional return, and owned + /// `QueenReviewVerdictRequest.swift`, where it changed nothing that + /// callers could see. + static func incidentLanes() -> [QueenInterfaceDivergence.Lane] { + typealias D = QueenInterfaceDivergence + + let returnsString = D.Signature( + parameters: ["projectRoot: String"], returns: "String") + let returnsOptional = D.Signature( + parameters: ["projectRoot: String"], returns: "String?") + let baseBranch = "QueenBranchCommitter.baseBranch(projectRoot:)" + let committer = "rings/SR-02/QueenBranchCommitter.swift" + let viewModel = "rings/SR-02/ChatViewModel.swift" + let verdictRequest = "rings/SR-00/QueenReviewVerdictRequest.swift" + + // A body-only change in #1109's other file: real to the incident, + // and invisible to the interface - callers see the same shape. + let parseShape = D.Signature(parameters: ["body: String"], returns: "[String]") + + return [ + D.Lane( + id: "queen-1110", + changes: [ + D.DeclarationChange( + symbol: baseBranch, path: committer, + before: returnsString, after: returnsOptional) + ], + references: [ + // Its own file's call, already adapted on this branch. + D.Reference(symbol: baseBranch, path: committer, + expects: returnsOptional) + ] + ), + D.Lane( + id: "queen-1109", + changes: [ + D.DeclarationChange( + symbol: "QueenReviewVerdictRequest.parse(criteria:)", + path: verdictRequest, + before: parseShape, after: parseShape) + ], + references: [ + // The call the build error pointed at. + D.Reference(symbol: baseBranch, path: viewModel, + expects: returnsString) + ] + ) + ] + } + + static func theIncidentItself() { + scenario("the incident itself: signature change in one lane, call in the other") + + let verdict = QueenInterfaceDivergence.verdict(between: incidentLanes()) + + // Criterion 1's pure half: the decision refuses this pair. Honoured + // by the loop (the acceptance-side watchdog compiles exactly this + // overlay before any lane may land), the parallel run cannot leave + // the tree unbuildable: it is either refused here, in words, or + // refused there, by the compiler. + check( + verdict.isDivergent, + "the incident pair is divergent, not clear" + ) + + guard case let .divergent(findings) = verdict else { + check(false, "the verdict carries findings") + return + } + equal(findings.count, 1, "exactly one finding, naming one crossing") + + guard case let .staleCall(symbol, changedBy, declaredIn, calledBy, calledFrom, + expected, found) = findings[0] else { + check(false, "the finding is a stale call") + return + } + equal(symbol, "QueenBranchCommitter.baseBranch(projectRoot:)", + "the finding names the symbol") + equal(changedBy, "queen-1110", "the finding names the lane that changed it") + equal(declaredIn, "rings/SR-02/QueenBranchCommitter.swift", + "the finding names the declaring path") + equal(calledBy, "queen-1109", "the finding names the lane that owns the call") + equal(calledFrom, "rings/SR-02/ChatViewModel.swift", + "the finding names the calling path") + equal(expected.returns, "String", "the call expected the old return") + equal(found.returns, "String?", "the declaration now returns the new one") + + let summary = findings[0].summary + check( + summary.contains("queen-1110") + && summary.contains("queen-1109") + && summary.contains("rings/SR-02/QueenBranchCommitter.swift") + && summary.contains("rings/SR-02/ChatViewModel.swift"), + "the summary names both lanes and both paths" + ) + } + + static func oneOwnerUpdatedBothSides() { + scenario("the same signature change with one owner of both sides is clear") + + typealias D = QueenInterfaceDivergence + let before = D.Signature(parameters: [], returns: "String") + let after = D.Signature(parameters: [], returns: "String?") + + // The rule does not refuse signature changes; it refuses the + // CROSSING. A lane that owns both the declaration and the call and + // moves them together leaves nothing stale behind. + let lanes = [ + D.Lane( + id: "queen-1", + changes: [ + D.DeclarationChange(symbol: "Lib.greet()", path: "Sources/Lib.swift", + before: before, after: after) + ], + references: [ + D.Reference(symbol: "Lib.greet()", path: "Sources/App.swift", + expects: after) + ] + ) + ] + check( + QueenInterfaceDivergence.verdict(between: lanes) == .clear, + "a change and its call moved together is clear" + ) + } + + static func bodyOnlyChangeIsClear() { + scenario("a body-only change to a referenced symbol is clear") + + typealias D = QueenInterfaceDivergence + let same = D.Signature(parameters: ["count: Int"], returns: "String") + let lanes = [ + D.Lane( + id: "queen-1", + changes: [ + D.DeclarationChange(symbol: "Lib.describe(count:)", + path: "Sources/Lib.swift", + before: same, after: same) + ], + references: [] + ), + D.Lane( + id: "queen-2", + changes: [], + references: [ + D.Reference(symbol: "Lib.describe(count:)", + path: "Sources/App.swift", expects: same) + ] + ) + ] + check( + QueenInterfaceDivergence.verdict(between: lanes) == .clear, + "no shape changed, so no call can be stale" + ) + } + + static func newCallAgainstNewShapeIsClear() { + scenario("a second lane's call written against the new shape is clear") + + typealias D = QueenInterfaceDivergence + let after = D.Signature(parameters: ["name: String"], returns: "String?") + let lanes = [ + D.Lane( + id: "queen-1", + changes: [ + D.DeclarationChange(symbol: "Lib.greet(name:)", + path: "Sources/Lib.swift", + before: D.Signature(parameters: ["name: String"], + returns: "String"), + after: after) + ], + references: [] + ), + D.Lane( + id: "queen-2", + changes: [], + references: [ + D.Reference(symbol: "Lib.greet(name:)", + path: "Sources/Caller.swift", expects: after) + ] + ) + ] + check( + QueenInterfaceDivergence.verdict(between: lanes) == .clear, + "the overlay matches the call; nothing is stale" + ) + } + + static func twoLanesChangedOneDeclaration() { + scenario("two lanes changing one declaration to different shapes") + + typealias D = QueenInterfaceDivergence + let optionalReturn = D.Signature(parameters: [], returns: "String?") + let arrayReturn = D.Signature(parameters: [], returns: "[String]") + let lanes = [ + D.Lane( + id: "queen-1", + changes: [ + D.DeclarationChange(symbol: "Lib.names()", path: "Sources/Lib.swift", + before: D.Signature(parameters: [], returns: "String"), + after: optionalReturn) + ], + references: [] + ), + D.Lane( + id: "queen-2", + changes: [ + D.DeclarationChange(symbol: "Lib.names()", path: "Sources/Lib.swift", + before: D.Signature(parameters: [], returns: "String"), + after: arrayReturn) + ], + references: [] + ) + ] + let verdict = QueenInterfaceDivergence.verdict(between: lanes) + check(verdict.isDivergent, "one declaration cannot carry both shapes") + guard case let .divergent(findings) = verdict, + case .twoLanesChangedOneDeclaration = findings[0] else { + check(false, "the finding is the two-owners kind") + return + } + check( + findings[0].summary.contains("queen-1") + && findings[0].summary.contains("queen-2"), + "the finding names both changers" + ) + } + + // MARK: the revert proof + + /// The pre-#1111 loop, restated on facts so the suite can run it: each + /// lane judged ALONE - its references against the base overlaid with + /// its OWN changes and nothing else. Nothing crosses a lane boundary, + /// which is exactly how #1110 and #1109 each looked correct while the + /// pair did not compose. + /// + /// This executable stand-in exists so the claim "the check breaks if + /// the former behaviour returns" is demonstrated rather than asserted: + /// the scenario below feeds the incident pair to BOTH decisions and + /// they disagree. Revert `verdict(between:)` to the judgement below - + /// the literal former behaviour - and the incident scenario above fails + /// its first check. That failure is the break; this function is the + /// proof the incident data is one the two decisions split on, which is + /// what mutation would have to erase to pass unnoticed. + static func formerIsolatedVerdict( + between lanes: [QueenInterfaceDivergence.Lane] + ) -> QueenInterfaceDivergence.Verdict { + typealias D = QueenInterfaceDivergence + var findings: [D.Finding] = [] + for lane in lanes { + let ownOverlay = Dictionary( + uniqueKeysWithValues: lane.changes.map { ($0.symbol, $0) }) + for reference in lane.references { + guard let change = ownOverlay[reference.symbol], + change.changedShape else { continue } + if reference.expects != change.after { + findings.append(.staleCall( + symbol: reference.symbol, + changedBy: lane.id, + declaredIn: change.path, + calledBy: lane.id, + calledFrom: reference.path, + expected: reference.expects, + found: change.after + )) + } + } + } + return findings.isEmpty ? .clear : .divergent(findings: findings) + } + + static func theRevertProof() { + scenario("returning the former behaviour breaks the check") + + // On the incident pair, the former behaviour says clear: every lane + // is internally consistent, which is all it ever asked. + check( + formerIsolatedVerdict(between: incidentLanes()) == .clear, + "the former behaviour (each lane alone) accepts the incident pair" + ) + + // And the rule says divergent. One pair of lanes, two decisions, + // one disagreement - so the incident assertion cannot survive the + // rule being reverted to the former judgement: with that judgement + // in place, the check above it fails. The check breaks. + check( + QueenInterfaceDivergence.verdict(between: incidentLanes()).isDivergent, + "the rule refuses the same pair the former behaviour accepted" + ) + + // The discrimination runs both ways: a rule that refused EVERYTHING + // would also "catch" the incident, so the compatible cases above + // (one owner, body-only, new call) are what keep that lie from + // passing. They already ran; this note says why they are here. + } + + // MARK: entry + + /// The driver's one call. Runs every scenario, prints the summary, and + /// ends in `fatalError` when anything failed, so a red run is loud and + /// non-zero on every platform. + static func main() { + theIncidentItself() + oneOwnerUpdatedBothSides() + bodyOnlyChangeIsClear() + newCallAgainstNewShapeIsClear() + twoLanesChangedOneDeclaration() + theRevertProof() + + print("\n\(checks) checks, \(failures) failures") + if failures > 0 { + fatalError( + "SELFTEST FAILED: \(failures) of \(checks) checks failed - " + + "see the FAIL lines above" + ) + } + print("SELFTEST PASSED") + } +} + +#endif