From d0325fed2fc84c550ae913331d94218108ebc05c Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Mon, 13 Jul 2026 12:13:15 -0600 Subject: [PATCH] Add control-flow support to definition builders DefinitionBuilder and EventHandlerArrayBuilder previously accepted only a fixed-arity variadic block, so state machines whose states come from runtime data (e.g. a parsed specification) had to pad a hand-written list of component expressions to a fixed length. This adds buildExpression / buildArray / buildOptional / buildEither overloads so definition blocks support for-loops and conditionals: StateMachine { initialState(first) for state in states { state(state) { ... } } } The existing variadic buildBlock entry points are retained for source compatibility; all existing call sites compile unchanged. Also qualifies the test helper's Nimble.Predicate explicitly: modern Foundation exports its own Predicate type, which made the bare name ambiguous and cascaded into unrelated overload-resolution errors across the suite on current toolchains. [Human-Directed AI Assistance] Co-Authored-By: Claude Fable 5 --- Swift/Sources/StateMachine/StateMachine.swift | 92 ++++++++++++++ .../StateMachineTests/StateMachineTests.swift | 4 +- .../StateMachine_ControlFlow_Tests.swift | 115 ++++++++++++++++++ 3 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 Swift/Tests/StateMachineTests/StateMachine_ControlFlow_Tests.swift diff --git a/Swift/Sources/StateMachine/StateMachine.swift b/Swift/Sources/StateMachine/StateMachine.swift index 3bb1072..63f15d0 100644 --- a/Swift/Sources/StateMachine/StateMachine.swift +++ b/Swift/Sources/StateMachine/StateMachine.swift @@ -273,6 +273,70 @@ public enum StateMachineTypes { ) -> Definition { Definition(initialState: initialState, components: components) } + + // The overloads below add control-flow support (`for`, `if`, `if`/`else`) + // to definition blocks, so components can be produced from runtime data: + // + // StateMachine { + // initialState(first) + // for state in states { + // state(state) { ... } + // } + // } + // + // `buildExpression` lifts each component expression to `[Component]`, + // giving every statement (single components, loops, and conditionals) a + // uniform partial-result type. The variadic `buildBlock` above is + // retained for source compatibility with direct callers. + + public static func buildExpression( + _ initialState: InitialState + ) -> InitialState { + initialState + } + + public static func buildExpression( + _ component: Component + ) -> [Component] { + [component] + } + + public static func buildBlock( + _ initialState: InitialState, + _ componentGroups: [Component]... + ) -> Definition { + Definition(initialState: initialState, components: Array(componentGroups.joined())) + } + + public static func buildBlock( + _ componentGroups: [Component]... + ) -> [Component] { + Array(componentGroups.joined()) + } + + public static func buildArray( + _ componentGroups: [[Component]] + ) -> [Component] { + Array(componentGroups.joined()) + } + + public static func buildOptional( + _ componentGroup: [Component]? + ) -> [Component] { + componentGroup ?? [] + } + + public static func buildEither( + first componentGroup: [Component] + ) -> [Component] { + componentGroup + } + + public static func buildEither( + second componentGroup: [Component] + ) -> [Component] { + componentGroup + } } public struct InitialState { @@ -294,6 +358,34 @@ public enum StateMachineTypes { ) -> [EventHandler] { Array(events.joined()) } + + // Control-flow support (`for`, `if`, `if`/`else`) inside `state { ... }` + // blocks, mirroring `DefinitionBuilder`. No `buildExpression` is needed + // here: `on(...)` already produces `[EventHandler]`. + + public static func buildArray( + _ eventGroups: [[EventHandler]] + ) -> [EventHandler] { + Array(eventGroups.joined()) + } + + public static func buildOptional( + _ events: [EventHandler]? + ) -> [EventHandler] { + events ?? [] + } + + public static func buildEither( + first events: [EventHandler] + ) -> [EventHandler] { + events + } + + public static func buildEither( + second events: [EventHandler] + ) -> [EventHandler] { + events + } } public struct EventHandler { diff --git a/Swift/Tests/StateMachineTests/StateMachineTests.swift b/Swift/Tests/StateMachineTests/StateMachineTests.swift index 9813b34..1c44cf9 100644 --- a/Swift/Tests/StateMachineTests/StateMachineTests.swift +++ b/Swift/Tests/StateMachineTests/StateMachineTests.swift @@ -202,9 +202,9 @@ final class Logger { } } -func log(_ expectedMessages: String...) -> Predicate { +func log(_ expectedMessages: String...) -> Nimble.Predicate { let expectedString: String = stringify(expectedMessages.joined(separator: "\\n")) - return Predicate { + return Nimble.Predicate { let actualMessages: [String]? = try $0.evaluate()?.messages let actualString: String = stringify(actualMessages?.joined(separator: "\\n")) let message: ExpectationMessage = .expectedCustomValueTo("log <\(expectedString)>", diff --git a/Swift/Tests/StateMachineTests/StateMachine_ControlFlow_Tests.swift b/Swift/Tests/StateMachineTests/StateMachine_ControlFlow_Tests.swift new file mode 100644 index 0000000..6a13c92 --- /dev/null +++ b/Swift/Tests/StateMachineTests/StateMachine_ControlFlow_Tests.swift @@ -0,0 +1,115 @@ +// +// Copyright © 2026 PassiveLogic, Inc. All rights reserved. +// + +import Nimble +@testable import StateMachine +import XCTest + +/// Exercises the control-flow support (`for`, `if`, `if`/`else`) in +/// `DefinitionBuilder` and `EventHandlerArrayBuilder`, which allows states and +/// event handlers to be produced from runtime data instead of a fixed-arity +/// literal block. +final class StateMachine_ControlFlow_Tests: XCTestCase, StateMachineBuilder { + + enum State: Hashable { + + case step0, step1, step2, step3, maintenance + } + + enum Event: Hashable { + + case advance, reset, service + } + + enum SideEffect: Equatable { + + case reachedEnd + } + + typealias ChainStateMachine = StateMachine + typealias ValidTransition = ChainStateMachine.Transition.Valid + + /// Builds a linear chain over `steps` with a `for` loop, an optional + /// maintenance state behind `if`, and per-state handlers chosen with + /// `if`/`else` inside the `state { ... }` block. + static func chainStateMachine( + steps: [State], + includeMaintenance: Bool + ) -> ChainStateMachine { + ChainStateMachine { + initialState(steps[0]) + for (index, step) in steps.enumerated() { + state(step) { + if index + 1 < steps.count { + on(.advance) { + transition(to: steps[index + 1]) + } + } else { + on(.advance) { + dontTransition(emit: .reachedEnd) + } + } + if includeMaintenance { + on(.service) { + transition(to: .maintenance) + } + } + on(.reset) { + transition(to: steps[0]) + } + } + } + if includeMaintenance { + state(.maintenance) { + on(.reset) { + transition(to: steps[0]) + } + } + } + } + } + + func testForLoopStatesAreAllReachable() throws { + let steps: [State] = [.step0, .step1, .step2, .step3] + let stateMachine: ChainStateMachine = Self.chainStateMachine(steps: steps, includeMaintenance: false) + for expected in steps.dropFirst() { + let transition: ValidTransition = try stateMachine.transition(.advance) + expect(transition.toState) == expected + } + expect(stateMachine.state) == .step3 + } + + func testLastLoopStateKeepsItsOwnHandlers() throws { + let steps: [State] = [.step0, .step1] + let stateMachine: ChainStateMachine = Self.chainStateMachine(steps: steps, includeMaintenance: false) + try stateMachine.transition(.advance) + let transition: ValidTransition = try stateMachine.transition(.advance) + expect(transition.toState) == .step1 + expect(transition.sideEffect) == .reachedEnd + } + + func testOptionalStateIsIncludedWhenConditionHolds() throws { + let stateMachine: ChainStateMachine = Self.chainStateMachine( + steps: [.step0, .step1], + includeMaintenance: true + ) + try stateMachine.transition(.service) + expect(stateMachine.state) == .maintenance + let transition: ValidTransition = try stateMachine.transition(.reset) + expect(transition.toState) == .step0 + } + + func testOptionalStateIsOmittedWhenConditionFails() throws { + let stateMachine: ChainStateMachine = Self.chainStateMachine( + steps: [.step0, .step1], + includeMaintenance: false + ) + expect(try stateMachine.transition(.service)) + .to(throwError(ChainStateMachine.Transition.Invalid())) + } +} + +extension StateMachine_ControlFlow_Tests.State: StateMachineHashable {} + +extension StateMachine_ControlFlow_Tests.Event: StateMachineHashable {}