tabular-center

tabular-center Home · Rust · Kotlin · Swift · Migrating · Compatibility · Cells · Diagnostics · Matrix files · Source

Swift

The runtime (TabularCenter) and the generator’s logic (TabularCenterCodegen) are done and checked on Linux and macOS. The generator does not yet run inside a build: the @Machine macro that will call it needs a SwiftPM that can declare a macro target, which the pinned toolchain cannot. Until it lands you write the dispatcher’s shape by hand – shown below – and the guarantee holds the same way, because it comes from protocol conformance, not from the macro.

The surface the macro will read

This block is not an illustration. It is the first swift block of macros/SURFACE.md, and a check parses it on every run with the macro’s own syntax reader, so it cannot drift from what the macro will accept:

@Machine
enum Turnstile {
    enum S { case locked, unlocked }
    enum A { case coin, push }
    enum F { case click }

    final class Ctx { var admitted = 0 }

    static let initial = S.locked

    //                        Coin                        Push
    @Row(.locked)   static let l = [ .go(.unlocked, [.click]),  .ignore ]
    @Row(.unlocked) static let u = [ .ignore,                   .handle ]

    // Carries the color and the return type. Every required member the
    // generator emits is stamped from this one line.
    func handle(_ ctx: Ctx, _ state: S, _ action: A) -> Step<S, F> { fatalError() }
}

From tabular-center-swift/macros/SURFACE.md.

What you write today

The matrix as a Table literal in a *.tb.swift file (why):

extension Timer {
    static let TABLE = Table(
        machine: "Timer",
        states: ["Idle", "Running", "Done"],
        actions: ["Start", "Tick", "Cancel"],
        cells: [
            [.handle,                                         .ignore, .ignore],
            [.ignore,                                         .handle, .go(target: "Idle", effects: ["StopClock"])],
            [.go(target: "Running", effects: ["StartClock"]), .ignore, .ignore],
        ],
        initial: "Idle"
    )
}

From tabular-center-swift/examples/Sources/Timer/Timer.tb.swift.

One protocol requirement per HANDLE cell and per effect, then the dispatcher – the shape TabularCenterCodegen emits and the macro will generate. The switch has no default:, so a new state or action breaks it:

    protocol Cells {
        func idleStart(_ ctx: Ctx) -> Step<S, F>
        func runningTick(_ ctx: Ctx, _ state: Running, _ action: Tick) -> Step<S, F>
        func startClock(_ ctx: Ctx) -> A?
        func stopClock(_ ctx: Ctx, _ reason: Reason) -> A?
    }

From tabular-center-swift/examples/Sources/Timer/Timer.swift.

    static func step(_ c: Cells, _ ctx: Ctx, _ s: S, _ a: A) -> Step<S, F> {
        switch (s, a) {
        case (.idle, .start): return c.idleStart(ctx)
        case (.idle, .tick): return .ignored
        case (.idle, .cancel): return .ignored
        case (.running, .start): return .ignored
        case let (.running(since), .tick(now)):
            return c.runningTick(ctx, Running(since: since), Tick(now: now))
        case (.running, .cancel):
            return .go(.idle, effects: [.stopClock(reason: .cancelled)])
        case (.done, .start): return .go(.running(since: 0), effects: [.startClock])
        case (.done, .tick): return .ignored
        case (.done, .cancel): return .ignored
        }
    }

From tabular-center-swift/examples/Sources/Timer/Timer.swift.

Then the implementation, with narrowed arguments – state.since and action.now are plain fields:

    struct Impl: Cells {
        func idleStart(_ ctx: Ctx) -> Step<S, F> {
            .go(.running(since: 0), effects: [.startClock])
        }

        func runningTick(_ ctx: Ctx, _ state: Running, _ action: Tick) -> Step<S, F> {
            if action.now - state.since >= ctx.limit {
                return .go(.done, effects: [.stopClock(reason: .elapsed)])
            }
            return .stay(effects: [])
        }

        func startClock(_ ctx: Ctx) -> A? {
            ctx.log.append("start")
            return nil
        }

        func stopClock(_ ctx: Ctx, _ reason: Reason) -> A? {
            ctx.log.append("stop:\(reason)")
            return nil
        }
    }

From tabular-center-swift/examples/Sources/Timer/Timer.swift.

When a cell is missing

error: does not conform to protocol

swiftc’s own conformance error, required by a fixture on every run. The dispatcher has no default:, and neither does your code, which is where one would otherwise creep in.

Composing steps

A Step is a value, so a cell can be written from small decisions rather than assembled in one expression: map(_:) relabels a target, flatMap(_:) chains a decision onto a transition (its effects follow the first step’s), and zip(_:) / zip(_:with:) combine two. Every closure may throw; the operations rethrow.

func enter(_ door: Door) -> Step<Door, Signal> {
    door == .open ? .go(.open, effects: [.chime]) : .go(door, effects: [])
}

From tabular-center-swift/Sources/TabularCenterCheck/StepAlgebra.swift.

func unlock(codeOk: Bool) -> Step<Door, Signal> {
    let decided: Step<Door, Signal> = codeOk ? .go(.open, effects: []) : .stay(effects: [.buzz])
    return decided.flatMap(enter)
}

From tabular-center-swift/Sources/TabularCenterCheck/StepAlgebra.swift.

func openBoth(left: Bool, right: Bool) -> Step<(Door, Door), Signal> {
    unlock(codeOk: left).zip(unlock(codeOk: right))
}

From tabular-center-swift/Sources/TabularCenterCheck/StepAlgebra.swift.

.stay and .ignored short-circuit: unlock(codeOk: false) stays and buzzes without calling enter, and zip drops the right side’s effects when the left does not move. An .ignored anywhere absorbs, and the whole step emits nothing. The rules and the laws are in Cells, section 6, and all three implementations replay the same cases.

Colors, SwiftUI, and pinning a matrix

    static let FIXTURE = """
        machine Turnstile
        initial Locked
        states  Locked Unlocked
        actions Coin Push

        Locked   | GO(Unlocked) | IGNORE
        Unlocked | IGNORE       | HANDLE
        """

From tabular-center-swift/examples/Sources/SpecCheck/Turnstile.swift.

do {
    let spec = try parseSpec(Turnstile.FIXTURE, "Turnstile.FIXTURE")
    Check.eq(
        checkTable(Turnstile.TABLE, spec),
        [],
        "spec: the matrix matches the fixture a reviewer reads")

    let drifted = try parseSpec(Turnstile.DRIFTED, "Turnstile.DRIFTED")
    let differences = checkTable(Turnstile.TABLE, drifted)
    Check.ok(!differences.isEmpty, "spec: a changed cell is reported, not shrugged at")
    Check.ok(
        differences.count == 1,
        "spec: one changed cell is one difference, not a cascade")
} catch {
    Check.ok(false, "spec: the fixture failed to parse: \(error)")
}

From tabular-center-swift/examples/Sources/SpecCheck/main.swift.

The package declares swift-tools-version: 5.9 and no platforms: clause, so it asks nothing of a deployment target; only ObservableStore carries an @available.

Examples

In tabular-center-swift/examples, one executable target each:

    adds
1-4 TrafficLight, Timer, Retry, Login the four core machines
6 ObservableCounter ObservableStore; skips, saying so, where Observation is absent
7 SpecCheck TabularCenterTesting, matching and then deliberately mismatching