Home · Rust · Kotlin · Swift · Migrating · Compatibility · Cells · Diagnostics · Matrix files · Source
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.
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.
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.
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.
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.
async, throws, @MainActor or any other
attribute is copied onto every generated member, and callers get
try await where the color demands it. A colored child under an uncolored
parent does not compile.ObservableStore is a @MainActor store SwiftUI can observe (macOS 14,
iOS 17), written without the @Observable macro so the runtime asks nothing
of macro plugins. Store and AsyncStore work everywhere.TabularCenterTesting parses a .tbl fixture and diffs it against a machine’s
TABLE – a reviewable, one-line-per-row pin of the 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.
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 |