From 6f9afe9386bf40b24b74a49e04913a9712e098c6 Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Thu, 4 Jun 2026 22:56:12 -0400 Subject: [PATCH 1/2] refactor: make ServiceRunner.Tick return a slice like the other drivers Signed-off-by: Joshua Temple --- durable/service.go | 5 +++-- examples/fooddelivery/rig.go | 20 ++++++++++++-------- state/CHANGELOG.md | 7 +++++++ state/invoke_test.go | 5 +++-- state/runner.go | 23 ++++++++++++++--------- 5 files changed, 39 insertions(+), 21 deletions(-) diff --git a/durable/service.go b/durable/service.go index 5f5ed2d..fedfe31 100644 --- a/durable/service.go +++ b/durable/service.go @@ -95,10 +95,11 @@ func (h *Handle[S, E, C]) RunService(ctx context.Context, id string) (state.Fire return state.FireResult[S]{}, false, nil } - res, ok := h.svc.Tick(ctx, id) - if !ok { + results := h.svc.Tick(ctx, id) + if len(results) == 0 { return state.FireResult[S]{}, false, nil } + res := results[0] // The settle re-fired the invocation's onDone / onError, which may enter a state // that arms a timer, starts another service, or spawns an actor. Absorb those diff --git a/examples/fooddelivery/rig.go b/examples/fooddelivery/rig.go index aa79c52..0647eaf 100644 --- a/examples/fooddelivery/rig.go +++ b/examples/fooddelivery/rig.go @@ -162,22 +162,26 @@ func (r *Rig) SettleRefund(ctx context.Context, amount int64) state.FireResult[S // (executing authorizeFn) and routes its outcome, rather than settling it with a // fixed token. It returns the resulting FireResult and whether a service ran. func (r *Rig) RunAuthorization(ctx context.Context) (state.FireResult[Stage], bool) { - fr, ok := r.run.Tick(ctx, r.authorizeID()) - if ok { - r.absorb(ctx, fr.Effects) + results := r.run.Tick(ctx, r.authorizeID()) + if len(results) == 0 { + return state.FireResult[Stage]{}, false } - return fr, ok + fr := results[0] + r.absorb(ctx, fr.Effects) + return fr, true } // RunRefund runs the real refund service synchronously through the runner (executing // refundFn) and routes its outcome. It returns the resulting FireResult and whether a // service ran. func (r *Rig) RunRefund(ctx context.Context) (state.FireResult[Stage], bool) { - fr, ok := r.run.Tick(ctx, r.refundID()) - if ok { - r.absorb(ctx, fr.Effects) + results := r.run.Tick(ctx, r.refundID()) + if len(results) == 0 { + return state.FireResult[Stage]{}, false } - return fr, ok + fr := results[0] + r.absorb(ctx, fr.Effects) + return fr, true } // RunKitchen steps the kitchen actor to its final state, so its completion re-fires diff --git a/state/CHANGELOG.md b/state/CHANGELOG.md index eece708..0af6341 100644 --- a/state/CHANGELOG.md +++ b/state/CHANGELOG.md @@ -453,6 +453,13 @@ representative hot-path numbers. option that makes `Verify` collect all failing requirements in one pass instead of failing fast is now `Aggregate()`. Replace `WithAggregate()` with `Aggregate()` at the call site. +- **BREAKING: `ServiceRunner.Tick` now returns `[]FireResult[S]` instead of + `(FireResult[S], bool)`.** The three host drivers now share one advance-verb + shape: `Scheduler.Tick`, `ActorSystem.Tick`, and `ServiceRunner.Tick` all + return `[]FireResult[S]`. `ServiceRunner.Tick` returns a one-element slice when + the service settled and an empty slice when the id is not in flight. A caller + that checked the old `ok` bool now checks `len(results) > 0` and reads + `results[0]` for the routed result. - The determinism and ordering contract is now explicit and frozen: emission order is exit → transition → entry across the cascade, declaration order within a set, fixed parallel-region order, and the run-to-completion interleave for diff --git a/state/invoke_test.go b/state/invoke_test.go index a907cd3..e36ad55 100644 --- a/state/invoke_test.go +++ b/state/invoke_test.go @@ -282,10 +282,11 @@ func TestInvoke_RunResolvesService(t *testing.T) { run.Absorb(ctx, inst.Fire(ctx, "start").Effects) id := state.InvokeID("loader", "loading", 0) - fr, ok := run.Tick(ctx, id) - if !ok { + results := run.Tick(ctx, id) + if len(results) == 0 { t.Fatalf("Run reported no in-flight service %q", id) } + fr := results[0] if fr.NewState != "ready" { t.Fatalf("after Run, want ready, got %q", fr.NewState) } diff --git a/state/runner.go b/state/runner.go index e8c57e0..79552bd 100644 --- a/state/runner.go +++ b/state/runner.go @@ -222,26 +222,31 @@ func (r *ServiceRunner[S, E, C]) settle(ctx context.Context, id string, result a // advance verb — the host-driver counterpart of Scheduler.Tick and // ActorSystem.Tick — coupling resolve + run + settle: a host that arms services // from Absorb and wants the runner to execute them calls Tick(ctx, id) (typically -// from its own goroutine). It returns the routed FireResult and true, or false -// when id is not in flight or no registry / ServiceFn resolves it (in which case -// the service is settled as an error so the machine still routes onError rather -// than hanging). -func (r *ServiceRunner[S, E, C]) Tick(ctx context.Context, id string) (FireResult[S], bool) { +// from its own goroutine). It returns a one-element slice holding the routed +// FireResult when the service settled, sharing the []FireResult[S] shape of +// Scheduler.Tick and ActorSystem.Tick, or an empty slice when id is not in flight. +// A missing registry / unresolved ServiceFn still settles the service as an error +// (so the machine routes onError rather than hanging) and that routed result is +// returned in the slice. +func (r *ServiceRunner[S, E, C]) Tick(ctx context.Context, id string) []FireResult[S] { r.mu.Lock() rs, ok := r.running[id] r.mu.Unlock() if !ok { - return FireResult[S]{}, false + return nil } fn := r.resolve(rs.src.Name) if fn == nil { - return r.SettleError(ctx, id, &ErrUnboundRef{Kind: "service", Name: rs.src.Name}) + res, _ := r.SettleError(ctx, id, &ErrUnboundRef{Kind: "service", Name: rs.src.Name}) + return []FireResult[S]{res} } out, err := fn(ctx, ServiceCtx[C]{Entity: r.inst.entity, Params: rs.src.Params, Input: rs.input}) if err != nil { - return r.SettleError(ctx, id, err) + res, _ := r.SettleError(ctx, id, err) + return []FireResult[S]{res} } - return r.SettleDone(ctx, id, out) + res, _ := r.SettleDone(ctx, id, out) + return []FireResult[S]{res} } // resolve returns the bound ServiceFn for name, or nil when no registry was wired From 0bc697380cf65c0279215550cd0cc2fd75a1bf7e Mon Sep 17 00:00:00 2001 From: Joshua Temple Date: Thu, 4 Jun 2026 22:58:12 -0400 Subject: [PATCH 2/2] refactor: name error types with the idiomatic *Error suffix Signed-off-by: Joshua Temple --- source/statemachine/drive.go | 4 +- source/statemachine/drivefunc.go | 2 +- state/CHANGELOG.md | 30 +++++++--- state/actor.go | 2 +- state/actor_comms.go | 2 +- state/actor_escalation_test.go | 4 +- state/actor_system.go | 2 +- state/assign_test.go | 16 ++--- state/binding.go | 2 +- state/binding_test.go | 6 +- state/coreexpr.go | 4 +- state/coverage_test.go | 38 ++++++------ state/effect.go | 6 +- state/effect_test.go | 4 +- state/envelope.go | 4 +- state/envelope_test.go | 4 +- state/errors.go | 100 +++++++++++++++---------------- state/evolution/evolution.go | 4 +- state/fire.go | 46 +++++++------- state/guard.go | 4 +- state/guard_test.go | 12 ++-- state/hardening_test.go | 18 +++--- state/hsm_test.go | 16 ++--- state/invoke.go | 2 +- state/invoke_test.go | 6 +- state/ir.go | 2 +- state/kernel.go | 8 +-- state/kernel_test.go | 18 +++--- state/parallel.go | 12 ++-- state/plan.go | 4 +- state/quench.go | 8 +-- state/roundtrip_test.go | 6 +- state/runner.go | 2 +- state/scheduler.go | 2 +- state/transitions_test.go | 4 +- state/verify.go | 2 +- 36 files changed, 211 insertions(+), 195 deletions(-) diff --git a/source/statemachine/drive.go b/source/statemachine/drive.go index ce479ee..f7911b2 100644 --- a/source/statemachine/drive.go +++ b/source/statemachine/drive.go @@ -167,7 +167,7 @@ func emitEffects(ctx context.Context, sink Sink, effects []state.Effect) error { // errors.Is(err, source.ErrInvalidForState); otherwise it reports false and the // caller treats the error as transient. func classifyFire[K comparable, E any](err error, event E, from K) (*source.GuardRejection, bool) { - var invalid *state.ErrInvalidTransition + var invalid *state.InvalidTransitionError if errors.As(err, &invalid) { return &source.GuardRejection{ Event: fmt.Sprint(event), @@ -175,7 +175,7 @@ func classifyFire[K comparable, E any](err error, event E, from K) (*source.Guar Err: err, }, true } - var guard *state.ErrGuardFailed + var guard *state.GuardFailedError if errors.As(err, &guard) { return &source.GuardRejection{ Event: fmt.Sprint(event), diff --git a/source/statemachine/drivefunc.go b/source/statemachine/drivefunc.go index 96ef43b..1dbc2d8 100644 --- a/source/statemachine/drivefunc.go +++ b/source/statemachine/drivefunc.go @@ -18,7 +18,7 @@ import ( // flows back to the binding. // // A nil error and a result whose Err is nil is a successful transition; a result -// carrying a [state.ErrInvalidTransition] or [state.ErrGuardFailed] is the +// carrying a [state.InvalidTransitionError] or [state.GuardFailedError] is the // state-aware rejection. Returning a non-nil error (not the FireResult.Err) is a // transient failure resolving the instance — a nak. type FireFunc[K comparable, E comparable] func(ctx context.Context, key K, event E) (state.FireResult[K], error) diff --git a/state/CHANGELOG.md b/state/CHANGELOG.md index 0af6341..a55f019 100644 --- a/state/CHANGELOG.md +++ b/state/CHANGELOG.md @@ -29,7 +29,7 @@ representative hot-path numbers. preserves unknown fields on nested nodes (machine, state, transition, `Ref`, `GuardNode`) so a document written by a newer build round-trips losslessly through an older one, and rejects only a higher *major* schema version (the - typed `*ErrUnsupportedSchema`). IR encoding is deterministic (stable key order) + typed `*UnsupportedSchemaError`). IR encoding is deterministic (stable key order) so a definition hashes and diffs reproducibly. - Closed-enum extension policy. Every IR enum that may grow (guard op, state kind, param type, descriptor kind, effect kind) has a documented @@ -96,7 +96,7 @@ representative hot-path numbers. on `NewEffectRegistry`) decodes it back to a concrete effect. Per the closed-enum extension policy, an unrecognized effect kind is preserved verbatim on load (surfaced as `UnknownEffect`) and rejected only at dispatch - (`EffectRegistry.Dispatchable` returns the typed `*ErrUnknownEffectKind`), never + (`EffectRegistry.Dispatchable` returns the typed `*UnknownEffectKindError`), never silently dropped or applied. Effects remain data the host applies; the kernel does not execute them. The `Effect` alias stays `any`, so bare domain effects are unaffected. @@ -220,7 +220,7 @@ representative hot-path numbers. - **Declarative invoke + service registry.** A state declares `Invocation{ID, Src, Input, OnDone, OnError}`; service implementations bind by name through `Registry.Service` / `Builder.Service`, parallel to guards and - actions. An unbound service ref fails `Quench` with the typed `*ErrUnboundRef` + actions. An unbound service ref fails `Quench` with the typed `*UnboundRefError` (`Kind: "service"`), consistent with unbound guards/actions. Authored via the DSL `Invoke(src, ...InvokeOption)` whose outcomes are options — `WithInvokeOnDone` / `WithInvokeOnError` — alongside `WithInput`, @@ -333,8 +333,8 @@ representative hot-path numbers. (e.g. `And(Or(g1, g2), Not(g3))`). Evaluation short-circuits exactly like a plain multi-guard transition: `And` stops at the first false, `Or` at the first true. A failing composite reports the failing leaf(s) when cheap, else - the composite, preserving the typed `ErrGuardFailed`; a leaf panic still - surfaces as `ErrGuardPanic`. + the composite, preserving the typed `GuardFailedError`; a leaf panic still + surfaces as `GuardPanicError`. - **`stateIn(state)`.** A first-class, config-aware built-in guard, true when the instance's active configuration includes the named state (its active leaves and their ancestor spine), so it is correct for atomic, compound, and @@ -370,7 +370,7 @@ representative hot-path numbers. transitions until the configuration is stable, recording each as a Trace microstep. The internal queue is macrostep-local, so `Fire` stays pure. An unhandled raised event is ignored; a non-settling raise/eventless cycle fails fast - with the typed `ErrMicrostepOverflow`. + with the typed `MicrostepOverflowError`. - DSL also gains `Always()` to author eventless transitions directly (previously IR-only). The wildcard target, forbidden marker, reenter flag, and raised-event list serialize in the IR and round-trip losslessly through JSON; `raise` is @@ -460,6 +460,22 @@ representative hot-path numbers. the service settled and an empty slice when the id is not in flight. A caller that checked the old `ok` bool now checks `len(results) > 0` and reads `results[0]` for the routed result. +- **BREAKING: the struct error types are renamed from the `Err*` prefix to the + idiomatic `*Error` suffix.** The `Err*` prefix is the Go convention for sentinel + error values, not for struct types a caller inspects with `errors.As`; these are + all struct types with no sentinel vars. `ErrInvalidTransition`, `ErrGuardFailed`, + `ErrGuardPanic`, `ErrAssignPanic`, `ErrPolicyDenied`, `ErrUndeclaredState`, + `ErrUnboundRef`, `ErrActionFailed`, `ErrMicrostepOverflow`, `ErrNoPath`, + `ErrNoInitialState`, `ErrUnknownBuiltin`, `ErrUnboundActor`, `ErrUnsupportedSchema`, + `ErrUnknownEffectKind`, and `MultiRegionErr` become `InvalidTransitionError`, + `GuardFailedError`, `GuardPanicError`, `AssignPanicError`, `PolicyDeniedError`, + `UndeclaredStateError`, `UnboundRefError`, `ActionFailedError`, + `MicrostepOverflowError`, `NoPathError`, `NoInitialStateError`, + `UnknownBuiltinError`, `UnboundActorError`, `UnsupportedSchemaError`, + `UnknownEffectKindError`, and `MultiRegionError`, matching the already-correct + `WaitTimeoutError`, `SnapshotError`, `SnapshotVersionError`, and `VerifyError`. + Behavior and fields are unchanged; update the type name at each `errors.As` + target, type switch, and struct literal. - The determinism and ordering contract is now explicit and frozen: emission order is exit → transition → entry across the cascade, declaration order within a set, fixed parallel-region order, and the run-to-completion interleave for @@ -468,7 +484,7 @@ representative hot-path numbers. ### Fixed -- `Cast` returns the typed `*ErrInvalidTransition` consistently for an event that +- `Cast` returns the typed `*InvalidTransitionError` consistently for an event that matches no transition, including inside parallel regions, so a caller can distinguish "no transition" from other failures uniformly. - On-entry lifecycle effects (`after` / `invoke` / actor `spawn`) are now emitted diff --git a/state/actor.go b/state/actor.go index ce14792..add2d26 100644 --- a/state/actor.go +++ b/state/actor.go @@ -176,7 +176,7 @@ func evalActorBuiltinAction(a Ref) (Effect, error) { id, _ := a.Params[stopActorIDParam].(string) return StopActor{ID: id}, nil default: - return nil, &ErrUnknownBuiltin{Name: a.Name} + return nil, &UnknownBuiltinError{Name: a.Name} } } diff --git a/state/actor_comms.go b/state/actor_comms.go index d923639..4e83403 100644 --- a/state/actor_comms.go +++ b/state/actor_comms.go @@ -167,6 +167,6 @@ func evalCommBuiltinAction(a Ref) (Effect, error) { systemID, _ := a.Params[sendToSystemIDParam].(string) return ForwardEvent{TargetID: target, SystemID: systemID}, nil default: - return nil, &ErrUnknownBuiltin{Name: a.Name} + return nil, &UnknownBuiltinError{Name: a.Name} } } diff --git a/state/actor_escalation_test.go b/state/actor_escalation_test.go index ebfb43f..261fea1 100644 --- a/state/actor_escalation_test.go +++ b/state/actor_escalation_test.go @@ -409,8 +409,8 @@ func TestActorEscalation_UnboundSrc_NoOnError_Escalates(t *testing.T) { if esc == nil { t.Fatal("unbound-src spawn failure was swallowed; LastEscalation = nil") } - var unbound *state.ErrUnboundActor + var unbound *state.UnboundActorError if !errors.As(esc, &unbound) { - t.Fatalf("escalation cause is not *ErrUnboundActor: %v", esc) + t.Fatalf("escalation cause is not *UnboundActorError: %v", esc) } } diff --git a/state/actor_system.go b/state/actor_system.go index 4ec72f7..5c5e476 100644 --- a/state/actor_system.go +++ b/state/actor_system.go @@ -313,7 +313,7 @@ func (s *ActorSystem[S, E, C]) spawn(ctx context.Context, e SpawnActor) { s.mu.Unlock() if !ok { - s.routeError(ctx, e, &ErrUnboundActor{Name: e.Src.Name}) + s.routeError(ctx, e, &UnboundActorError{Name: e.Src.Name}) return } inst, err := behavior(e.Input) diff --git a/state/assign_test.go b/state/assign_test.go index 015d742..a6ccc4a 100644 --- a/state/assign_test.go +++ b/state/assign_test.go @@ -283,7 +283,7 @@ func TestAssign_ParallelRegionFolds(t *testing.T) { } // TestAssign_ParallelRegionPanicStopsCommit asserts a region reducer that panics -// surfaces as OutcomeAssignFailed / *ErrAssignPanic and stops the commit — it +// surfaces as OutcomeAssignFailed / *AssignPanicError and stops the commit — it // must not silently no-op, which was the prior behavior on the parallel path. func TestAssign_ParallelRegionPanicStopsCommit(t *testing.T) { m := state.Forge[string, string, acct]("par-assign-panic"). @@ -309,9 +309,9 @@ func TestAssign_ParallelRegionPanicStopsCommit(t *testing.T) { if res.Err == nil { t.Fatal("panicking region reducer should fail the fire") } - var ap *state.ErrAssignPanic + var ap *state.AssignPanicError if !errors.As(res.Err, &ap) { - t.Fatalf("error = %v, want *ErrAssignPanic", res.Err) + t.Fatalf("error = %v, want *AssignPanicError", res.Err) } if ap.AssignName != "boom" { t.Fatalf("assign name = %q, want boom", ap.AssignName) @@ -386,9 +386,9 @@ func TestAssign_PanicStopsCommit(t *testing.T) { if res.Err == nil { t.Fatal("panicking reducer should fail the fire") } - var ap *state.ErrAssignPanic + var ap *state.AssignPanicError if !errors.As(res.Err, &ap) { - t.Fatalf("error = %v, want *ErrAssignPanic", res.Err) + t.Fatalf("error = %v, want *AssignPanicError", res.Err) } if ap.AssignName != "boom" { t.Fatalf("assign name = %q, want boom", ap.AssignName) @@ -399,7 +399,7 @@ func TestAssign_PanicStopsCommit(t *testing.T) { } // TestAssign_UnboundRefFailsQuench asserts a transition that wires an unregistered -// assign fails Quench with the typed *ErrUnboundRef (Kind "assign"), exactly like +// assign fails Quench with the typed *UnboundRefError (Kind "assign"), exactly like // an unbound guard, action, or service. func TestAssign_UnboundRefFailsQuench(t *testing.T) { defer func() { @@ -411,9 +411,9 @@ func TestAssign_UnboundRefFailsQuench(t *testing.T) { if !ok { t.Fatalf("panic value is not an error: %T", r) } - var ub *state.ErrUnboundRef + var ub *state.UnboundRefError if !errors.As(err, &ub) { - t.Fatalf("panic = %v, want *ErrUnboundRef", err) + t.Fatalf("panic = %v, want *UnboundRefError", err) } if ub.Kind != "assign" || ub.Name != "ghost" { t.Fatalf("unbound ref = {%q, %q}, want {assign, ghost}", ub.Kind, ub.Name) diff --git a/state/binding.go b/state/binding.go index eee7db0..983a4ce 100644 --- a/state/binding.go +++ b/state/binding.go @@ -171,7 +171,7 @@ func (r *Registry[C]) bindGuard(name string, b GuardBinding[C]) { // The binding is wired into the same name path Guard uses, so a guard registered // this way is indistinguishable to the kernel from a Go-func guard — it resolves // by name at Provide/Quench, evaluates synchronously inside the pure Fire step, and -// surfaces a panic as the same typed ErrGuardPanic. The binding's EvalGuard is +// surfaces a panic as the same typed GuardPanicError. The binding's EvalGuard is // adapted to a GuardFn over the in-process context view so the fire-time fast path // (which reads r.guards) finds it; the binding is also recorded on the parallel // binding seam so a future out-of-process transport can swap it under the same name. diff --git a/state/binding_test.go b/state/binding_test.go index 8ba5570..8a474f5 100644 --- a/state/binding_test.go +++ b/state/binding_test.go @@ -173,7 +173,7 @@ func TestRegistry_AssignBindingRecorded(t *testing.T) { } // TestEvalAssign_UnboundRefFailsClosed asserts the defensive fire-time guard: -// an assign ref with no bound reducer surfaces as a typed *ErrAssignPanic and +// an assign ref with no bound reducer surfaces as a typed *AssignPanicError and // leaves the context unchanged, rather than silently dropping the fold. func TestEvalAssign_UnboundRefFailsClosed(t *testing.T) { m := Forge[string, string, bindOrder]("x").State("a").Initial("a").Quench() @@ -181,9 +181,9 @@ func TestEvalAssign_UnboundRefFailsClosed(t *testing.T) { if err == nil { t.Fatal("unbound assign ref should fail") } - var ap *ErrAssignPanic + var ap *AssignPanicError if !errors.As(err, &ap) || ap.AssignName != "ghost" { - t.Fatalf("error = %v, want *ErrAssignPanic{ghost}", err) + t.Fatalf("error = %v, want *AssignPanicError{ghost}", err) } if next.Amount != 3 { t.Fatalf("context changed on unbound assign: %+v", next) diff --git a/state/coreexpr.go b/state/coreexpr.go index 1f3244c..e8f2f93 100644 --- a/state/coreexpr.go +++ b/state/coreexpr.go @@ -322,12 +322,12 @@ func appendErr(errs []error, err error) []error { // node) against the live context value. It reads each operand — a field-ref // resolves the dotted path against the entity by reflection; a literal yields its // constant — then compares them with the op's typed comparison. An unresolvable -// field path or an incomparable pair surfaces a typed ErrGuardPanic, matching how +// field path or an incomparable pair surfaces a typed GuardPanicError, matching how // a named guard's failure surfaces, so a malformed Core guard fails the firing // deterministically rather than silently passing. func evalCorePredicate[S comparable, C any](g *GuardNode[S], entity C) (bool, error) { guardErr := func(reason string) error { - return &ErrGuardPanic{GuardName: renderGuardExpr(g), Recovered: reason} + return &GuardPanicError{GuardName: renderGuardExpr(g), Recovered: reason} } switch g.Op { diff --git a/state/coverage_test.go b/state/coverage_test.go index 5d7c8b8..3324028 100644 --- a/state/coverage_test.go +++ b/state/coverage_test.go @@ -16,17 +16,17 @@ func TestErrorMessages(t *testing.T) { err error want string }{ - {&state.ErrInvalidTransition{From: "Draft", Event: "Submit", Reason: "no match"}, "invalid transition"}, - {&state.ErrInvalidTransition{From: "Draft", To: "Approved", Event: "Approve", Reason: "guards failed"}, "from \"Draft\" to \"Approved\""}, - {&state.ErrGuardFailed{GuardName: "hasReviewer", Reason: "nil"}, "guard \"hasReviewer\" failed"}, - {&state.ErrGuardPanic{GuardName: "g", Recovered: "boom"}, "panicked"}, - {&state.ErrPolicyDenied{PolicyName: "p", Reason: "denied"}, "policy \"p\" denied"}, - {&state.ErrUndeclaredState{State: "Ghost"}, "undeclared state \"Ghost\""}, - {&state.ErrUnboundRef{Kind: "guard", Name: "g"}, "unbound guard ref \"g\""}, - {&state.ErrActionFailed{ActionName: "a", TransitionName: "t", Cause: errors.New("x")}, "action \"a\""}, - {&state.ErrNoPath{From: "A", To: "B"}, "no path from \"A\" to \"B\""}, - {&state.ErrNoInitialState{Machine: "m"}, "no CurrentStateFn"}, - {&state.MultiRegionErr{Errors: []error{errors.New("r1"), errors.New("r2")}}, "2 regions errored"}, + {&state.InvalidTransitionError{From: "Draft", Event: "Submit", Reason: "no match"}, "invalid transition"}, + {&state.InvalidTransitionError{From: "Draft", To: "Approved", Event: "Approve", Reason: "guards failed"}, "from \"Draft\" to \"Approved\""}, + {&state.GuardFailedError{GuardName: "hasReviewer", Reason: "nil"}, "guard \"hasReviewer\" failed"}, + {&state.GuardPanicError{GuardName: "g", Recovered: "boom"}, "panicked"}, + {&state.PolicyDeniedError{PolicyName: "p", Reason: "denied"}, "policy \"p\" denied"}, + {&state.UndeclaredStateError{State: "Ghost"}, "undeclared state \"Ghost\""}, + {&state.UnboundRefError{Kind: "guard", Name: "g"}, "unbound guard ref \"g\""}, + {&state.ActionFailedError{ActionName: "a", TransitionName: "t", Cause: errors.New("x")}, "action \"a\""}, + {&state.NoPathError{From: "A", To: "B"}, "no path from \"A\" to \"B\""}, + {&state.NoInitialStateError{Machine: "m"}, "no CurrentStateFn"}, + {&state.MultiRegionError{Errors: []error{errors.New("r1"), errors.New("r2")}}, "2 regions errored"}, {&state.VerifyError{Failures: []state.RequirementFailure{{Name: "req"}}}, "verify failed"}, } for _, c := range cases { @@ -64,20 +64,20 @@ func TestOutcomeString(t *testing.T) { // errors.As/Unwrap. func TestErrActionFailed_Unwrap(t *testing.T) { cause := errors.New("root") - err := error(&state.ErrActionFailed{ActionName: "a", Cause: cause}) + err := error(&state.ActionFailedError{ActionName: "a", Cause: cause}) if !errors.Is(err, cause) { - t.Fatal("ErrActionFailed should unwrap to its cause") + t.Fatal("ActionFailedError should unwrap to its cause") } } // TestMultiRegionErr_Unwrap asserts a typed region error is reachable through // the aggregate via errors.As. func TestMultiRegionErr_Unwrap(t *testing.T) { - inner := &state.ErrGuardFailed{GuardName: "g", Reason: "no"} - agg := error(&state.MultiRegionErr{Errors: []error{inner}}) - var gf *state.ErrGuardFailed + inner := &state.GuardFailedError{GuardName: "g", Reason: "no"} + agg := error(&state.MultiRegionError{Errors: []error{inner}}) + var gf *state.GuardFailedError if !errors.As(agg, &gf) { - t.Fatal("MultiRegionErr should expose region errors to errors.As") + t.Fatal("MultiRegionError should expose region errors to errors.As") } } @@ -157,9 +157,9 @@ func TestQuenchError_Message(t *testing.T) { func TestVerify_UndeclaredState(t *testing.T) { m := buildDocMachine() err := m.Verify(DocState(99), &Document{}) - var us *state.ErrUndeclaredState + var us *state.UndeclaredStateError if !errors.As(err, &us) { - t.Fatalf("err = %v, want *ErrUndeclaredState", err) + t.Fatalf("err = %v, want *UndeclaredStateError", err) } } diff --git a/state/effect.go b/state/effect.go index 1813460..69e3c81 100644 --- a/state/effect.go +++ b/state/effect.go @@ -141,7 +141,7 @@ func (e *EffectEnvelope) UnmarshalJSON(data []byte) error { // unknown effect survives a load -> save cycle byte-for-byte (forward-compat, // per the closed-enum extension policy). It implements KindedEffect, so it can be // re-marshaled, but it is never dispatchable — EffectRegistry.Dispatchable -// rejects it with a typed *ErrUnknownEffectKind. The kernel never produces an +// rejects it with a typed *UnknownEffectKindError. The kernel never produces an // UnknownEffect; only deserialization of a foreign envelope yields one. type UnknownEffect struct { // EffectKind is the unrecognized discriminant, preserved verbatim. @@ -252,7 +252,7 @@ func (r *EffectRegistry) Unmarshal(env EffectEnvelope) (Effect, error) { // means the effect carries a kind the registry recognizes (or is not kinded at // all — a bare domain effect the kernel never gated). An UnknownEffect, or any // KindedEffect whose kind the registry does not know, is rejected with a typed -// *ErrUnknownEffectKind, completing the preserve-on-load, reject-on-dispatch +// *UnknownEffectKindError, completing the preserve-on-load, reject-on-dispatch // policy: a foreign effect is never silently applied. func (r *EffectRegistry) Dispatchable(eff Effect) error { ke, ok := eff.(KindedEffect) @@ -262,7 +262,7 @@ func (r *EffectRegistry) Dispatchable(eff Effect) error { return nil } if _, known := r.factories[ke.Kind()]; !known { - return &ErrUnknownEffectKind{Kind: ke.Kind()} + return &UnknownEffectKindError{Kind: ke.Kind()} } return nil } diff --git a/state/effect_test.go b/state/effect_test.go index d7e5fd7..6cb3ee3 100644 --- a/state/effect_test.go +++ b/state/effect_test.go @@ -176,9 +176,9 @@ func TestEffectRegistry_UnknownKind_RejectedOnDispatch(t *testing.T) { if derr := reg.Dispatchable(eff); derr == nil { t.Fatal("Dispatchable(unknown) = nil, want typed rejection") } else { - var ue *ErrUnknownEffectKind + var ue *UnknownEffectKindError if !errors.As(derr, &ue) { - t.Fatalf("Dispatchable error = %T, want *ErrUnknownEffectKind", derr) + t.Fatalf("Dispatchable error = %T, want *UnknownEffectKindError", derr) } if ue.Kind != "vendor.future" { t.Fatalf("rejected kind = %q", ue.Kind) diff --git a/state/envelope.go b/state/envelope.go index 4300889..1da3890 100644 --- a/state/envelope.go +++ b/state/envelope.go @@ -9,7 +9,7 @@ import ( // CurrentSchemaVersion is the IR wire-format version this build emits and // accepts. It is a major.minor string: a higher minor (same major) loads with // unknown fields preserved for forward-compat, while a higher major is refused by -// LoadFromJSON as *ErrUnsupportedSchema. Every document ToJSON emits is stamped +// LoadFromJSON as *UnsupportedSchemaError. Every document ToJSON emits is stamped // with this version, so an IR on the wire is self-describing. const CurrentSchemaVersion = "1.0" @@ -157,5 +157,5 @@ func cloneMeta(in map[string]any) map[string]any { // schemaError builds the typed rejection for a document whose schema major // exceeds the loader's. Centralized so the message stays consistent. func schemaError(got string) error { - return &ErrUnsupportedSchema{Got: got, Supported: CurrentSchemaVersion} + return &UnsupportedSchemaError{Got: got, Supported: CurrentSchemaVersion} } diff --git a/state/envelope_test.go b/state/envelope_test.go index 74c3a88..5424da3 100644 --- a/state/envelope_test.go +++ b/state/envelope_test.go @@ -222,9 +222,9 @@ func TestLoadFromJSON_RejectHigherMajor(t *testing.T) { t.Fatalf("LoadFromJSON(%q) err = %v, want nil", ver, err) } if tc.wantError { - var ue *state.ErrUnsupportedSchema + var ue *state.UnsupportedSchemaError if !errors.As(err, &ue) { - t.Fatalf("err = %v, want *ErrUnsupportedSchema", err) + t.Fatalf("err = %v, want *UnsupportedSchemaError", err) } } }) diff --git a/state/errors.go b/state/errors.go index f550218..478e8a6 100644 --- a/state/errors.go +++ b/state/errors.go @@ -6,123 +6,123 @@ import ( "time" ) -// ErrInvalidTransition is returned when no transition matched (current, event), +// InvalidTransitionError is returned when no transition matched (current, event), // or all matching transitions had failing guards. From names the state the event // was fired in, Event the rejected event, and Reason the specific cause (no // declared transition, a final-state exit, an undeclared current state, ...). To // names the intended target when the rejected transition had one (a targeted // transition whose guards all failed); it is empty for an unmatched event with no // candidate target. -type ErrInvalidTransition struct { +type InvalidTransitionError struct { From string To string Event string Reason string } -func (e *ErrInvalidTransition) Error() string { +func (e *InvalidTransitionError) Error() string { if e.To != "" { return fmt.Sprintf("crucible/state: invalid transition from %q to %q on %q: %s", e.From, e.To, e.Event, e.Reason) } return fmt.Sprintf("crucible/state: invalid transition from %q on %q: %s", e.From, e.Event, e.Reason) } -// ErrGuardFailed is returned when a named guard returned false. -type ErrGuardFailed struct { +// GuardFailedError is returned when a named guard returned false. +type GuardFailedError struct { GuardName string Reason string } -func (e *ErrGuardFailed) Error() string { +func (e *GuardFailedError) Error() string { return fmt.Sprintf("crucible/state: guard %q failed: %s", e.GuardName, e.Reason) } -// ErrGuardPanic is returned when a guard panicked and was recovered. -type ErrGuardPanic struct { +// GuardPanicError is returned when a guard panicked and was recovered. +type GuardPanicError struct { GuardName string Recovered any } -func (e *ErrGuardPanic) Error() string { +func (e *GuardPanicError) Error() string { return fmt.Sprintf("crucible/state: guard %q panicked: %v", e.GuardName, e.Recovered) } -// ErrAssignPanic is returned when an assign reducer panicked and was recovered, +// AssignPanicError is returned when an assign reducer panicked and was recovered, // or when an assign ref did not resolve at fire time. An assign is a total reducer, // so a panic is a programmer error the kernel surfaces as a typed failure that // stops the commit rather than leaving context partly folded. -type ErrAssignPanic struct { +type AssignPanicError struct { AssignName string Recovered any } -func (e *ErrAssignPanic) Error() string { +func (e *AssignPanicError) Error() string { return fmt.Sprintf("crucible/state: assign %q panicked: %v", e.AssignName, e.Recovered) } -// ErrPolicyDenied is returned when a policy returned Deny. -type ErrPolicyDenied struct { +// PolicyDeniedError is returned when a policy returned Deny. +type PolicyDeniedError struct { PolicyName string Reason string } -func (e *ErrPolicyDenied) Error() string { +func (e *PolicyDeniedError) Error() string { return fmt.Sprintf("crucible/state: policy %q denied: %s", e.PolicyName, e.Reason) } -// ErrUndeclaredState is returned when a state value was never declared. -type ErrUndeclaredState struct { +// UndeclaredStateError is returned when a state value was never declared. +type UndeclaredStateError struct { State string } -func (e *ErrUndeclaredState) Error() string { +func (e *UndeclaredStateError) Error() string { return fmt.Sprintf("crucible/state: undeclared state %q", e.State) } -// ErrUnboundRef is returned when a guard/action/effect ref in the IR did not +// UnboundRefError is returned when a guard/action/effect ref in the IR did not // resolve against the registry (raised at Quench / Provide). -type ErrUnboundRef struct { +type UnboundRefError struct { Kind string // "guard" | "action" | "assign" | "service" Name string } -func (e *ErrUnboundRef) Error() string { +func (e *UnboundRefError) Error() string { return fmt.Sprintf("crucible/state: unbound %s ref %q", e.Kind, e.Name) } -// ErrActionFailed wraps a bound action that returned an error during emission. -type ErrActionFailed struct { +// ActionFailedError wraps a bound action that returned an error during emission. +type ActionFailedError struct { TransitionName string ActionName string Cause error } -func (e *ErrActionFailed) Error() string { +func (e *ActionFailedError) Error() string { return fmt.Sprintf("crucible/state: action %q on transition %q failed: %v", e.ActionName, e.TransitionName, e.Cause) } -func (e *ErrActionFailed) Unwrap() error { return e.Cause } +func (e *ActionFailedError) Unwrap() error { return e.Cause } -// ErrMicrostepOverflow is returned when a single Fire macrostep does not reach a +// MicrostepOverflowError is returned when a single Fire macrostep does not reach a // stable configuration within the run-to-completion step budget. It indicates a // cycle of raised internal events or eventless ("always") transitions that never // settles. -type ErrMicrostepOverflow struct { +type MicrostepOverflowError struct { Limit int State string } -func (e *ErrMicrostepOverflow) Error() string { +func (e *MicrostepOverflowError) Error() string { return fmt.Sprintf("crucible/state: run-to-completion did not stabilize within %d microsteps (at %q): a raise/eventless cycle", e.Limit, e.State) } -// ErrNoPath is returned by PlanPath when no event sequence connects from->to. -type ErrNoPath struct { +// NoPathError is returned by PlanPath when no event sequence connects from->to. +type NoPathError struct { From string To string } -func (e *ErrNoPath) Error() string { +func (e *NoPathError) Error() string { return fmt.Sprintf("crucible/state: no path from %q to %q", e.From, e.To) } @@ -141,39 +141,39 @@ func (e *WaitTimeoutError) Error() string { return fmt.Sprintf("crucible/state: WaitFor on machine %q timed out after %s in state %q", e.Machine, e.Timeout, e.Last) } -// ErrNoInitialState is returned/panicked by Cast when neither a CurrentStateFn +// NoInitialStateError is returned/panicked by Cast when neither a CurrentStateFn // is declared on the machine nor an explicit initial state is supplied via // WithInitialState — there is no way to derive the instance's starting state. // This is a programmer error, consistent with Quench's panic-on-misuse posture. -type ErrNoInitialState struct { +type NoInitialStateError struct { Machine string } -func (e *ErrNoInitialState) Error() string { +func (e *NoInitialStateError) Error() string { return fmt.Sprintf("crucible/state: cannot Cast machine %q: no CurrentStateFn declared and no WithInitialState supplied", e.Machine) } -// ErrUnknownBuiltin is returned when a ref names a kernel built-in action the +// UnknownBuiltinError is returned when a ref names a kernel built-in action the // kernel does not recognize. It is a defensive programmer-error signal: the DSL // and lint only ever produce known built-in names, so this surfaces only a // hand-constructed or corrupted ref. -type ErrUnknownBuiltin struct { +type UnknownBuiltinError struct { Name string } -func (e *ErrUnknownBuiltin) Error() string { +func (e *UnknownBuiltinError) Error() string { return fmt.Sprintf("crucible/state: unknown built-in action %q", e.Name) } -// ErrUnboundActor is returned by an ActorSystem when a SpawnActor's Src does not +// UnboundActorError is returned by an ActorSystem when a SpawnActor's Src does not // resolve against the system's actor palette — no child-machine factory was // registered under that name. The actor is settled as an error so the parent // still routes its onError rather than hanging. -type ErrUnboundActor struct { +type UnboundActorError struct { Name string } -func (e *ErrUnboundActor) Error() string { +func (e *UnboundActorError) Error() string { return fmt.Sprintf("crucible/state: unbound actor ref %q", e.Name) } @@ -218,14 +218,14 @@ func (e *SnapshotVersionError) Error() string { e.Kind, e.Machine, e.Got, e.Want, e.Reason) } -// MultiRegionErr aggregates the errors raised by more than one orthogonal +// MultiRegionError aggregates the errors raised by more than one orthogonal // region firing on a single event. Its Unwrap returns each region's error so // errors.As finds any region's typed error. -type MultiRegionErr struct { +type MultiRegionError struct { Errors []error } -func (e *MultiRegionErr) Error() string { +func (e *MultiRegionError) Error() string { msgs := make([]string, 0, len(e.Errors)) for _, err := range e.Errors { msgs = append(msgs, err.Error()) @@ -234,7 +234,7 @@ func (e *MultiRegionErr) Error() string { } // Unwrap exposes the per-region errors for errors.As / errors.Is traversal. -func (e *MultiRegionErr) Unwrap() []error { return e.Errors } +func (e *MultiRegionError) Unwrap() []error { return e.Errors } // VerifyError aggregates one or more failing requirements found by Verify. type VerifyError struct { @@ -249,33 +249,33 @@ func (e *VerifyError) Error() string { return fmt.Sprintf("crucible/state: verify failed: %s", strings.Join(names, ", ")) } -// ErrUnsupportedSchema is returned by LoadFromJSON when an IR document declares a +// UnsupportedSchemaError is returned by LoadFromJSON when an IR document declares a // schema major version newer than the loader supports. The reject-higher-major // policy is the reserved compatibility seam: a higher minor (same major) loads, // preserving unknown fields for forward-compat, but a higher major signals a wire // form this build cannot safely interpret and is refused rather than guessed at. -type ErrUnsupportedSchema struct { +type UnsupportedSchemaError struct { // Got is the schemaVersion declared in the document. Got string // Supported is the loader's own schema version. Supported string } -func (e *ErrUnsupportedSchema) Error() string { +func (e *UnsupportedSchemaError) Error() string { return fmt.Sprintf("crucible/state: unsupported schema version %q (loader supports %q)", e.Got, e.Supported) } -// ErrUnknownEffectKind is returned by EffectRegistry.Dispatchable when an effect +// UnknownEffectKindError is returned by EffectRegistry.Dispatchable when an effect // carries a kind the registry does not recognize. It realizes the reject half of // the closed-enum extension policy for effect kinds: an unknown kind is preserved // on load (as an UnknownEffect) so a foreign effect round-trips losslessly, but // it is refused at dispatch rather than silently applied — the host must register // the kind (RegisterEffect) or drop the effect deliberately. -type ErrUnknownEffectKind struct { +type UnknownEffectKindError struct { // Kind is the unrecognized effect discriminant. Kind string } -func (e *ErrUnknownEffectKind) Error() string { +func (e *UnknownEffectKindError) Error() string { return fmt.Sprintf("crucible/state: unknown effect kind %q (not registered for dispatch)", e.Kind) } diff --git a/state/evolution/evolution.go b/state/evolution/evolution.go index 0a315c8..038f852 100644 --- a/state/evolution/evolution.go +++ b/state/evolution/evolution.go @@ -207,7 +207,7 @@ func (d *differ[S, E, C]) diffStates(prefix string, oldStates, newStates []state d.r.add(Change{ Kind: KindStateRemoved, Path: path, - Description: fmt.Sprintf("state %q removed; entities persisted in it hit ErrInvalidTransition (a rename appears as remove+add)", name), + Description: fmt.Sprintf("state %q removed; entities persisted in it hit InvalidTransitionError (a rename appears as remove+add)", name), Breaking: true, }) continue @@ -313,7 +313,7 @@ func (d *differ[S, E, C]) diffTransitions(statePath string, oldTr, newTr []state d.r.add(Change{ Kind: KindTransitionRemoved, Path: tpath, - Description: fmt.Sprintf("transition on %q removed; paths relying on it become ErrNoPath", key.on), + Description: fmt.Sprintf("transition on %q removed; paths relying on it become NoPathError", key.on), Breaking: true, }) continue diff --git a/state/fire.go b/state/fire.go index 5f09814..818da3c 100644 --- a/state/fire.go +++ b/state/fire.go @@ -256,7 +256,7 @@ func absorbMicrosteps(dst *Trace, sub Trace) { // microstepOverflow returns the macrostep result annotated with the typed // run-to-completion overflow error. func microstepOverflow[S comparable](res FireResult[S], state S) FireResult[S] { - res.Err = &ErrMicrostepOverflow{Limit: maxMicrosteps, State: fmtState(state)} + res.Err = &MicrostepOverflowError{Limit: maxMicrosteps, State: fmtState(state)} res.Trace.Outcome = OutcomeInvalidTransition res.NewState = state return res @@ -265,7 +265,7 @@ func microstepOverflow[S comparable](res FireResult[S], state S) FireResult[S] { // isNoTransition reports whether err is the "no transition declared" outcome — // the benign result of a raised event the current configuration does not handle. func isNoTransition(err error) bool { - var it *ErrInvalidTransition + var it *InvalidTransitionError if as(err, &it) { return it.Reason == "no transition declared for this state and event" } @@ -296,7 +296,7 @@ func (i *Instance[S, E, C]) fireOnce(ctx context.Context, event E) FireResult[S] if _, ok := m.stateByName(from); !ok { if _, ok := m.resolveNode(from); !ok { - err := &ErrInvalidTransition{ + err := &InvalidTransitionError{ From: fmtState(from), Event: fmt.Sprint(event), Reason: "current state is not declared", @@ -316,7 +316,7 @@ func (i *Instance[S, E, C]) fireOnce(ctx context.Context, event E) FireResult[S] // A transition out of a final leaf is rejected (runtime guard mirroring the // builder lint, for machines loaded from JSON). if n, ok := m.resolveNode(from); ok && n.state.IsFinal { - err := &ErrInvalidTransition{ + err := &InvalidTransitionError{ From: fmtState(from), Event: fmt.Sprint(event), Reason: "state is final", @@ -368,7 +368,7 @@ func (i *Instance[S, E, C]) fireSpine(ctx context.Context, event E, tr Trace) Fi if !ok { passed = false sawGuardFail = true - lastGuardErr = &ErrGuardFailed{GuardName: g.Name, Reason: "predicate returned false"} + lastGuardErr = &GuardFailedError{GuardName: g.Name, Reason: "predicate returned false"} break } } @@ -385,7 +385,7 @@ func (i *Instance[S, E, C]) fireSpine(ctx context.Context, event E, tr Trace) Fi if !res.ok { passed = false sawGuardFail = true - lastGuardErr = &ErrGuardFailed{ + lastGuardErr = &GuardFailedError{ GuardName: joinLeafs(res.failedLeafs), Reason: "composite guard failed", } @@ -401,12 +401,12 @@ func (i *Instance[S, E, C]) fireSpine(ctx context.Context, event E, tr Trace) Fi if sawGuardFail { tr.Outcome = OutcomeGuardFailed if lastGuardErr == nil { - lastGuardErr = &ErrGuardFailed{Reason: "all candidate transitions failed their guards"} + lastGuardErr = &GuardFailedError{Reason: "all candidate transitions failed their guards"} } return FireResult[S]{NewState: from, Trace: tr, Err: lastGuardErr} } - err := &ErrInvalidTransition{ + err := &InvalidTransitionError{ From: fmtState(from), Event: fmt.Sprint(event), Reason: "no transition declared for this state and event", @@ -492,7 +492,7 @@ func (i *Instance[S, E, C]) commit( tr.Outcome = OutcomeEffectError return FireResult[S]{ NewState: i.current, Effects: effects, Trace: tr, - Err: &ErrActionFailed{ + Err: &ActionFailedError{ TransitionName: fmt.Sprintf("%s->%s", fmtState(from), fmtState(from)), ActionName: errName, Cause: err, }, @@ -503,7 +503,7 @@ func (i *Instance[S, E, C]) commit( tr.Outcome = OutcomeAssignFailed return FireResult[S]{ NewState: i.current, Effects: effects, Trace: tr, - Err: &ErrActionFailed{ + Err: &ActionFailedError{ TransitionName: fmt.Sprintf("%s->%s", fmtState(from), fmtState(from)), ActionName: aName, Cause: aErr, }, @@ -568,7 +568,7 @@ func (i *Instance[S, E, C]) commit( tr.Outcome = OutcomeEffectError return FireResult[S]{ NewState: i.current, Effects: effects, Trace: tr, - Err: &ErrActionFailed{TransitionName: transName(from, to), ActionName: errName, Cause: err}, + Err: &ActionFailedError{TransitionName: transName(from, to), ActionName: errName, Cause: err}, } } next, aName, aErr := i.applyAssigns(n.state.OnExitAssign, cur, eventData, &tr) @@ -576,7 +576,7 @@ func (i *Instance[S, E, C]) commit( tr.Outcome = OutcomeAssignFailed return FireResult[S]{ NewState: i.current, Effects: effects, Trace: tr, - Err: &ErrActionFailed{TransitionName: transName(from, to), ActionName: aName, Cause: aErr}, + Err: &ActionFailedError{TransitionName: transName(from, to), ActionName: aName, Cause: aErr}, } } cur = next @@ -625,7 +625,7 @@ func (i *Instance[S, E, C]) commit( tr.Outcome = OutcomeEffectError return FireResult[S]{ NewState: i.current, Effects: effects, Trace: tr, - Err: &ErrActionFailed{TransitionName: transName(from, to), ActionName: errName, Cause: err}, + Err: &ActionFailedError{TransitionName: transName(from, to), ActionName: errName, Cause: err}, } } tnext, taName, taErr := i.applyAssigns(t.Assigns, cur, eventData, &tr) @@ -633,7 +633,7 @@ func (i *Instance[S, E, C]) commit( tr.Outcome = OutcomeAssignFailed return FireResult[S]{ NewState: i.current, Effects: effects, Trace: tr, - Err: &ErrActionFailed{TransitionName: transName(from, to), ActionName: taName, Cause: taErr}, + Err: &ActionFailedError{TransitionName: transName(from, to), ActionName: taName, Cause: taErr}, } } cur = tnext @@ -652,7 +652,7 @@ func (i *Instance[S, E, C]) commit( tr.Outcome = OutcomeEffectError return FireResult[S]{ NewState: i.current, Effects: effects, Trace: tr, - Err: &ErrActionFailed{TransitionName: transName(from, to), ActionName: errName, Cause: err}, + Err: &ActionFailedError{TransitionName: transName(from, to), ActionName: errName, Cause: err}, } } next, aName, aErr := i.applyAssigns(n.state.OnEntryAssign, cur, eventData, &tr) @@ -660,7 +660,7 @@ func (i *Instance[S, E, C]) commit( tr.Outcome = OutcomeAssignFailed return FireResult[S]{ NewState: i.current, Effects: effects, Trace: tr, - Err: &ErrActionFailed{TransitionName: transName(from, to), ActionName: aName, Cause: aErr}, + Err: &ActionFailedError{TransitionName: transName(from, to), ActionName: aName, Cause: aErr}, } } cur = next @@ -687,7 +687,7 @@ func (i *Instance[S, E, C]) commit( tr.Outcome = OutcomeEffectError return FireResult[S]{ NewState: i.current, Effects: effects, Trace: tr, - Err: &ErrActionFailed{TransitionName: transName(from, to), ActionName: dname, Cause: derr}, + Err: &ActionFailedError{TransitionName: transName(from, to), ActionName: dname, Cause: derr}, } } @@ -764,17 +764,17 @@ func (i *Instance[S, E, C]) runActions(refs []Ref, entity C, tr *Trace) (effects return effects, "", nil } -// evalGuard resolves and runs a guard ref, recovering panics into ErrGuardPanic. +// evalGuard resolves and runs a guard ref, recovering panics into GuardPanicError. func (m *Machine[S, E, C]) evalGuard(g Ref, entity C) (ok bool, err error) { fn, found := m.guards[g.Name] if !found { // Unbound refs are caught at Quench; defensively treat as a guard panic. - return false, &ErrGuardPanic{GuardName: g.Name, Recovered: "unbound guard at fire time"} + return false, &GuardPanicError{GuardName: g.Name, Recovered: "unbound guard at fire time"} } defer func() { if r := recover(); r != nil { ok = false - err = &ErrGuardPanic{GuardName: g.Name, Recovered: r} + err = &GuardPanicError{GuardName: g.Name, Recovered: r} } }() return fn(GuardCtx[C]{Entity: entity, Params: g.Params}), nil @@ -812,17 +812,17 @@ func (i *Instance[S, E, C]) applyAssigns(refs []Ref, cur C, eventData any, tr *T } // evalAssign resolves and runs an assign ref, folding the prior context into the -// next. A reducer panic is recovered into a typed ErrAssignPanic so a faulty +// next. A reducer panic is recovered into a typed AssignPanicError so a faulty // reducer fails the commit deterministically rather than corrupting context. func (m *Machine[S, E, C]) evalAssign(a Ref, cur C, eventData any) (next C, err error) { fn, found := m.assigns[a.Name] if !found { - return cur, &ErrAssignPanic{AssignName: a.Name, Recovered: "unbound assign at fire time"} + return cur, &AssignPanicError{AssignName: a.Name, Recovered: "unbound assign at fire time"} } defer func() { if r := recover(); r != nil { next = cur - err = &ErrAssignPanic{AssignName: a.Name, Recovered: r} + err = &AssignPanicError{AssignName: a.Name, Recovered: r} } }() return fn(AssignCtx[C]{Entity: cur, Event: eventData, Params: a.Params}), nil diff --git a/state/guard.go b/state/guard.go index bdbcf8b..b95883e 100644 --- a/state/guard.go +++ b/state/guard.go @@ -358,7 +358,7 @@ type guardEval struct { // instance's live active configuration, with the same short-circuit semantics // short-circuits: And stops at the first false, Or stops at the first true, Not // inverts. A leaf guard that panics or fails to bind stops evaluation and -// surfaces the typed error (ErrGuardPanic), exactly like a plain transition +// surfaces the typed error (GuardPanicError), exactly like a plain transition // guard. The stateIn built-in reads the active spine, so it is correct for // atomic, compound, and parallel configurations. func (i *Instance[S, E, C]) evalGuardExpr(g *GuardNode[S], entity C, tr *Trace) guardEval { @@ -451,7 +451,7 @@ func (i *Instance[S, E, C]) evalGuardExpr(g *GuardNode[S], entity C, tr *Trace) return guardEval{ok: true} default: - return guardEval{err: &ErrGuardPanic{GuardName: string(g.Op), Recovered: "unknown guard op"}} + return guardEval{err: &GuardPanicError{GuardName: string(g.Op), Recovered: "unknown guard op"}} } } diff --git a/state/guard_test.go b/state/guard_test.go index 8ecf2b8..94b4a9d 100644 --- a/state/guard_test.go +++ b/state/guard_test.go @@ -199,9 +199,9 @@ func TestCompositeGuard_FailureReportsLeaf(t *testing.T) { if enabled { t.Fatalf("expected guard failure") } - var gf *state.ErrGuardFailed + var gf *state.GuardFailedError if !errors.As(res.Err, &gf) { - t.Fatalf("want *ErrGuardFailed, got %T: %v", res.Err, res.Err) + t.Fatalf("want *GuardFailedError, got %T: %v", res.Err, res.Err) } if !strings.Contains(gf.GuardName, "b") { t.Fatalf("failure should name leaf b, got %q", gf.GuardName) @@ -222,9 +222,9 @@ func TestCompositeGuard_PanicSurfacesTyped(t *testing.T) { Quench() inst := m.Cast(gctx{}, state.WithInitialState("from")) res := inst.Fire(context.Background(), "go") - var gp *state.ErrGuardPanic + var gp *state.GuardPanicError if !errors.As(res.Err, &gp) { - t.Fatalf("want *ErrGuardPanic, got %T: %v", res.Err, res.Err) + t.Fatalf("want *GuardPanicError, got %T: %v", res.Err, res.Err) } } @@ -437,9 +437,9 @@ func TestGuardExpr_UnboundLeafPanicsAtQuench(t *testing.T) { if r == nil { t.Fatalf("expected Quench panic for unbound composite leaf") } - var ub *state.ErrUnboundRef + var ub *state.UnboundRefError if !errors.As(r.(error), &ub) { - t.Fatalf("want *ErrUnboundRef, got %T: %v", r, r) + t.Fatalf("want *UnboundRefError, got %T: %v", r, r) } }() state.Forge[string, string, gctx]("u"). diff --git a/state/hardening_test.go b/state/hardening_test.go index ef5a598..6e64683 100644 --- a/state/hardening_test.go +++ b/state/hardening_test.go @@ -165,7 +165,7 @@ func TestSelfTransition(t *testing.T) { // TestActionError_AdvancesStateRecordsEffectError asserts the locked decision: // state advances before actions run; a failing action records OutcomeEffectError -// and the typed *ErrActionFailed, with the state already advanced. +// and the typed *ActionFailedError, with the state already advanced. func TestActionError_AdvancesStateRecordsEffectError(t *testing.T) { type s = int type e = int @@ -186,9 +186,9 @@ func TestActionError_AdvancesStateRecordsEffectError(t *testing.T) { Quench() res := m.Cast(nil).Fire(context.Background(), go0) - var af *state.ErrActionFailed + var af *state.ActionFailedError if !errors.As(res.Err, &af) { - t.Fatalf("err = %v, want *ErrActionFailed", res.Err) + t.Fatalf("err = %v, want *ActionFailedError", res.Err) } if !errors.Is(res.Err, boom) { t.Fatalf("err does not unwrap to boom: %v", res.Err) @@ -202,7 +202,7 @@ func TestActionError_AdvancesStateRecordsEffectError(t *testing.T) { } // TestProvide_UnboundActionRef asserts an unbound action ref also panics with -// *ErrUnboundRef (kind "action"). +// *UnboundRefError (kind "action"). func TestProvide_UnboundActionRef(t *testing.T) { defer func() { r := recover() @@ -213,9 +213,9 @@ func TestProvide_UnboundActionRef(t *testing.T) { if !ok { t.Fatalf("recovered non-error: %v", r) } - var ub *state.ErrUnboundRef + var ub *state.UnboundRefError if !errors.As(err, &ub) { - t.Fatalf("err = %v, want *ErrUnboundRef", err) + t.Fatalf("err = %v, want *UnboundRefError", err) } if ub.Kind != "action" { t.Fatalf("Kind = %q, want action", ub.Kind) @@ -230,7 +230,7 @@ func TestProvide_UnboundActionRef(t *testing.T) { } // TestGuardPanic_RecoveredAsTypedError asserts a panicking guard is recovered -// into *ErrGuardPanic rather than crashing the process. +// into *GuardPanicError rather than crashing the process. func TestGuardPanic_RecoveredAsTypedError(t *testing.T) { type s = int type e = int @@ -241,9 +241,9 @@ func TestGuardPanic_RecoveredAsTypedError(t *testing.T) { Quench() res := m.Cast(nil).Fire(context.Background(), 0) - var gp *state.ErrGuardPanic + var gp *state.GuardPanicError if !errors.As(res.Err, &gp) { - t.Fatalf("err = %v, want *ErrGuardPanic", res.Err) + t.Fatalf("err = %v, want *GuardPanicError", res.Err) } if res.Trace.Outcome != state.OutcomeGuardPanic { t.Fatalf("outcome = %v, want OutcomeGuardPanic", res.Trace.Outcome) diff --git a/state/hsm_test.go b/state/hsm_test.go index 2463939..df67207 100644 --- a/state/hsm_test.go +++ b/state/hsm_test.go @@ -128,9 +128,9 @@ func TestFinal_RejectsOutgoing(t *testing.T) { m := buildJobMachine() inst := m.Cast(&Job{Status: JobDone}) res := inst.Fire(context.Background(), Cancel) - var it *state.ErrInvalidTransition + var it *state.InvalidTransitionError if !errors.As(res.Err, &it) { - t.Fatalf("err = %v, want *ErrInvalidTransition", res.Err) + t.Fatalf("err = %v, want *InvalidTransitionError", res.Err) } } @@ -275,7 +275,7 @@ func TestHSM_JobRoundTrip(t *testing.T) { } // TestParallel_MultiRegionErr asserts that when two regions both match an event -// but fail their guards, the result is a MultiRegionErr unwrapping each region's +// but fail their guards, the result is a MultiRegionError unwrapping each region's // typed error. func TestParallel_MultiRegionErr(t *testing.T) { type s = int @@ -316,16 +316,16 @@ func TestParallel_MultiRegionErr(t *testing.T) { inst := m.Cast(nil) inst.Fire(context.Background(), on) res := inst.Fire(context.Background(), step) - var multi *state.MultiRegionErr + var multi *state.MultiRegionError if !errors.As(res.Err, &multi) { - t.Fatalf("err = %v, want *MultiRegionErr", res.Err) + t.Fatalf("err = %v, want *MultiRegionError", res.Err) } if len(multi.Errors) != 2 { - t.Fatalf("MultiRegionErr.Errors = %d, want 2", len(multi.Errors)) + t.Fatalf("MultiRegionError.Errors = %d, want 2", len(multi.Errors)) } - var gf *state.ErrGuardFailed + var gf *state.GuardFailedError if !errors.As(res.Err, &gf) { - t.Fatalf("MultiRegionErr does not unwrap to *ErrGuardFailed: %v", res.Err) + t.Fatalf("MultiRegionError does not unwrap to *GuardFailedError: %v", res.Err) } } diff --git a/state/invoke.go b/state/invoke.go index fdde40a..6c31c53 100644 --- a/state/invoke.go +++ b/state/invoke.go @@ -37,7 +37,7 @@ type Invocation[S comparable, E comparable, C any] struct { // Src is the named reference (plus serializable params) to the host-provided // service implementation, bound from the service registry at Provide/Quench // time exactly like a guard or action ref. An unbound Src fails Quench with - // the typed *ErrUnboundRef (Kind "service"). + // the typed *UnboundRefError (Kind "service"). Src Ref `json:"src"` // Input is the serializable input passed to the service when it starts, // surfaced on the StartService effect as input. It is data only; diff --git a/state/invoke_test.go b/state/invoke_test.go index e36ad55..73778b1 100644 --- a/state/invoke_test.go +++ b/state/invoke_test.go @@ -157,7 +157,7 @@ func TestInvoke_StoppedOnExit(t *testing.T) { } // TestInvoke_UnboundServiceQuench asserts an invoke whose Src is not registered -// fails Quench with the typed *ErrUnboundRef (Kind "service"), exactly like an +// fails Quench with the typed *UnboundRefError (Kind "service"), exactly like an // unbound guard or action. func TestInvoke_UnboundServiceQuench(t *testing.T) { defer func() { @@ -169,9 +169,9 @@ func TestInvoke_UnboundServiceQuench(t *testing.T) { if !ok { t.Fatalf("panic value is not an error: %T", r) } - var ub *state.ErrUnboundRef + var ub *state.UnboundRefError if !errors.As(err, &ub) { - t.Fatalf("panic = %v, want *ErrUnboundRef", err) + t.Fatalf("panic = %v, want *UnboundRefError", err) } if ub.Kind != "service" || ub.Name != "missing" { t.Fatalf("unbound ref = {%q, %q}, want {service, missing}", ub.Kind, ub.Name) diff --git a/state/ir.go b/state/ir.go index fe1d0e1..d986262 100644 --- a/state/ir.go +++ b/state/ir.go @@ -188,7 +188,7 @@ func LoadFromJSON[S comparable, E comparable, C any](b []byte, opts ...LoadOptio // Provide binds every Ref in the IR against the host registry and returns a // Builder ready to Quench. Refs that do not resolve are surfaced at Quench as -// the typed *ErrUnboundRef (the same failure the DSL raises for an unregistered +// the typed *UnboundRefError (the same failure the DSL raises for an unregistered // ref), so a UI/JSON-authored machine and a DSL-authored machine fail // identically. func (ir *IR[S, E, C]) Provide(reg *Registry[C], opts ...ProvideOption) *Builder[S, E, C] { diff --git a/state/kernel.go b/state/kernel.go index 035ee0e..7ff70d3 100644 --- a/state/kernel.go +++ b/state/kernel.go @@ -608,7 +608,7 @@ func (r *Registry[C]) Reducer(name string, fn AssignFn[C], opts ...DescribeOptio // Service registers a named invoked-service implementation. An invoke's Src ref // binds to it at Provide/Quench time exactly like a guard or action ref; an -// unbound service ref fails Quench with the typed *ErrUnboundRef (Kind +// unbound service ref fails Quench with the typed *UnboundRefError (Kind // "service"). The runner resolves and runs it when the owning state is entered. // An optional Describe option adds palette metadata; registering without one // still works and yields a minimal palette descriptor. @@ -790,7 +790,7 @@ func (b *Builder[S, E, C]) Reducer(name string, fn AssignFn[C], opts ...Describe // Service registers a named invoked-service implementation into the builder's // palette, bound by an invoke's Src ref. An unbound service ref fails Quench with -// the typed *ErrUnboundRef, mirroring guards and actions. An optional Describe +// the typed *UnboundRefError, mirroring guards and actions. An optional Describe // option attaches palette metadata. func (b *Builder[S, E, C]) Service(name string, fn ServiceFn[C], opts ...DescribeOption) *Builder[S, E, C] { b.reg.Service(name, fn, opts...) @@ -1537,7 +1537,7 @@ func (m *Machine[S, E, C]) stateByName(name S) (*State[S, E, C], bool) { // given entity. The instance's starting state is derived from the entity via // the machine's CurrentStateFn; if no CurrentStateFn was declared, an explicit // initial state must be supplied via WithInitialState. When both are present, -// WithInitialState wins. With neither, Cast panics with *ErrNoInitialState — a +// WithInitialState wins. With neither, Cast panics with *NoInitialStateError — a // programmer error, consistent with Quench's panic-on-misuse posture. // // The entity value is held on the Instance and supplied to guards and actions @@ -1555,7 +1555,7 @@ func (m *Machine[S, E, C]) Cast(entity C, opts ...CastOption[S]) *Instance[S, E, case m.currentStateFn != nil: current = m.currentStateFn(entity) default: - panic(&ErrNoInitialState{Machine: m.name}) + panic(&NoInitialStateError{Machine: m.name}) } clock := cfg.clock diff --git a/state/kernel_test.go b/state/kernel_test.go index 1bca2c8..708de4b 100644 --- a/state/kernel_test.go +++ b/state/kernel_test.go @@ -79,7 +79,7 @@ func TestFire_TraceAlwaysNonNil(t *testing.T) { } } -// TestFire_InvalidTransition asserts the typed ErrInvalidTransition. +// TestFire_InvalidTransition asserts the typed InvalidTransitionError. func TestFire_InvalidTransition(t *testing.T) { m, rec := safeBuild(t) if rec != nil { @@ -87,13 +87,13 @@ func TestFire_InvalidTransition(t *testing.T) { } inst := m.Cast(&Document{Status: Published}) res := inst.Fire(context.Background(), Submit) - var it *state.ErrInvalidTransition + var it *state.InvalidTransitionError if !errors.As(res.Err, &it) { - t.Fatalf("err = %v, want *ErrInvalidTransition", res.Err) + t.Fatalf("err = %v, want *InvalidTransitionError", res.Err) } } -// TestFire_GuardFailed asserts the typed ErrGuardFailed when a guard returns +// TestFire_GuardFailed asserts the typed GuardFailedError when a guard returns // false (no reviewer on the document). func TestFire_GuardFailed(t *testing.T) { m, rec := safeBuild(t) @@ -102,9 +102,9 @@ func TestFire_GuardFailed(t *testing.T) { } inst := m.Cast(&Document{Status: Submitted}) // no reviewer set on this entity res := inst.Fire(context.Background(), Approve) - var gf *state.ErrGuardFailed + var gf *state.GuardFailedError if !errors.As(res.Err, &gf) { - t.Fatalf("err = %v, want *ErrGuardFailed", res.Err) + t.Fatalf("err = %v, want *GuardFailedError", res.Err) } if gf.GuardName != "hasReviewer" { t.Fatalf("GuardName = %q, want %q", gf.GuardName, "hasReviewer") @@ -133,7 +133,7 @@ func TestPlanPath(t *testing.T) { } } -// TestPlanPath_NoPath asserts the typed ErrNoPath when no sequence connects. +// TestPlanPath_NoPath asserts the typed NoPathError when no sequence connects. func TestPlanPath_NoPath(t *testing.T) { m, rec := safeBuild(t) if rec != nil { @@ -141,9 +141,9 @@ func TestPlanPath_NoPath(t *testing.T) { } doc := &Document{} _, err := m.PlanPath(Archived, Draft, doc) - var np *state.ErrNoPath + var np *state.NoPathError if !errors.As(err, &np) { - t.Fatalf("err = %v, want *ErrNoPath", err) + t.Fatalf("err = %v, want *NoPathError", err) } } diff --git a/state/parallel.go b/state/parallel.go index 9a8860e..fa531cb 100644 --- a/state/parallel.go +++ b/state/parallel.go @@ -98,7 +98,7 @@ func (i *Instance[S, E, C]) fireParallel(ctx context.Context, parallel S, event tr.Outcome = OutcomeEffectError return FireResult[S]{ NewState: i.current, Effects: effects, Trace: tr, - Err: &ErrActionFailed{TransitionName: "onDone:" + fmtState(parallel), ActionName: dname, Cause: derr}, + Err: &ActionFailedError{TransitionName: "onDone:" + fmtState(parallel), ActionName: dname, Cause: derr}, } } tr.Outcome = OutcomeSuccess @@ -108,7 +108,7 @@ func (i *Instance[S, E, C]) fireParallel(ctx context.Context, parallel S, event return FireResult[S]{NewState: i.current, Effects: effects, Trace: tr, Err: regionErrs[0]} default: tr.Outcome = regionErrOutcome(regionErrs[0]) - return FireResult[S]{NewState: i.current, Effects: effects, Trace: tr, Err: &MultiRegionErr{Errors: regionErrs}} + return FireResult[S]{NewState: i.current, Effects: effects, Trace: tr, Err: &MultiRegionError{Errors: regionErrs}} } } @@ -116,7 +116,7 @@ func (i *Instance[S, E, C]) fireParallel(ctx context.Context, parallel S, event // mirroring the main commit path: assign reducer failures use // OutcomeAssignFailed while guard failures use OutcomeGuardFailed. func regionErrOutcome(err error) Outcome { - var ap *ErrAssignPanic + var ap *AssignPanicError if errors.As(err, &ap) { return OutcomeAssignFailed } @@ -152,7 +152,7 @@ func (i *Instance[S, E, C]) fireRegion( } if !okg { pass = false - err = &ErrGuardFailed{GuardName: g.Name, Reason: "predicate returned false"} + err = &GuardFailedError{GuardName: g.Name, Reason: "predicate returned false"} break } } @@ -163,7 +163,7 @@ func (i *Instance[S, E, C]) fireRegion( } if !res.ok { pass = false - err = &ErrGuardFailed{GuardName: joinLeafs(res.failedLeafs), Reason: "composite guard failed"} + err = &GuardFailedError{GuardName: joinLeafs(res.failedLeafs), Reason: "composite guard failed"} } } if pass { @@ -346,7 +346,7 @@ func (i *Instance[S, E, C]) fireFromState(ctx context.Context, start S, event E, } } - err := &ErrInvalidTransition{ + err := &InvalidTransitionError{ From: fmtState(from), Event: fmt.Sprint(event), Reason: "no transition declared for this state and event", diff --git a/state/plan.go b/state/plan.go index bce9465..35f1f39 100644 --- a/state/plan.go +++ b/state/plan.go @@ -4,7 +4,7 @@ package state // `from` state to the `to` state, found by breadth-first search over the static // transition graph. Guards are honored against the supplied entity, so the // returned path is one the entity can actually traverse. The entity is never -// mutated. ErrNoPath is returned when no sequence connects from->to. +// mutated. NoPathError is returned when no sequence connects from->to. func (m *Machine[S, E, C]) PlanPath(from, to S, entity C, opts ...PlanOption) ([]E, error) { cfg := planConfig{} for _, o := range opts { @@ -60,7 +60,7 @@ func (m *Machine[S, E, C]) PlanPath(from, to S, entity C, opts ...PlanOption) ([ } } - return nil, &ErrNoPath{From: fmtState(from), To: fmtState(to)} + return nil, &NoPathError{From: fmtState(from), To: fmtState(to)} } // guardsPass reports whether every guard on a transition passes for the entity. diff --git a/state/quench.go b/state/quench.go index 13bf56c..5d9d853 100644 --- a/state/quench.go +++ b/state/quench.go @@ -20,7 +20,7 @@ const ( // exact errors.As-able value. type diagnostic struct { Diagnostic - unboundRef *ErrUnboundRef + unboundRef *UnboundRefError } // quenchError wraps a non-ref lint finding so Quench panics with an error value @@ -132,7 +132,7 @@ func (b *Builder[S, E, C]) lint() []diagnostic { b.checkRefs(&diags, "assign", sd.state.OnExitAssign, sd.state.OwnedBy, 0) // Validate every invoked service's Src ref against the service registry, - // surfacing an unbound service as the same typed *ErrUnboundRef the DSL and + // surfacing an unbound service as the same typed *UnboundRefError the DSL and // IR paths raise for guards and actions. Child-MACHINE actor invocations // (ActorKindMachine) are exempt: their Src binds at the host ActorSystem's // actor palette, not the service registry, and an unbound actor src is @@ -323,7 +323,7 @@ type refSrc struct { line int } -// checkRefs appends an ErrUnboundRef diagnostic for every ref that does not +// checkRefs appends an UnboundRefError diagnostic for every ref that does not // resolve in the builder's registry. The trailing src is optional. func (b *Builder[S, E, C]) checkRefs(diags *[]diagnostic, kind string, refs []Ref, _ string, _ int, src ...refSrc) { var file string @@ -346,7 +346,7 @@ func (b *Builder[S, E, C]) checkRefs(diags *[]diagnostic, kind string, refs []Re _, ok = b.reg.assigns[r.Name] } if !ok { - ub := &ErrUnboundRef{Kind: kind, Name: r.Name} + ub := &UnboundRefError{Kind: kind, Name: r.Name} *diags = append(*diags, diagnostic{ Diagnostic: Diagnostic{ Severity: diagError, diff --git a/state/roundtrip_test.go b/state/roundtrip_test.go index 7c83a28..0531e04 100644 --- a/state/roundtrip_test.go +++ b/state/roundtrip_test.go @@ -58,7 +58,7 @@ func TestRoundTrip_Identity(t *testing.T) { } } -// TestProvide_UnboundRef asserts Provide fails with *ErrUnboundRef when a ref +// TestProvide_UnboundRef asserts Provide fails with *UnboundRefError when a ref // name in the IR has no registry binding. func TestProvide_UnboundRef(t *testing.T) { m, rec := safeBuild(t) @@ -83,9 +83,9 @@ func TestProvide_UnboundRef(t *testing.T) { t.Fatal("expected Provide/Quench to fail on unbound ref") } if err, ok := r.(error); ok { - var ub *state.ErrUnboundRef + var ub *state.UnboundRefError if !errors.As(err, &ub) { - t.Fatalf("recovered err = %v, want *ErrUnboundRef", err) + t.Fatalf("recovered err = %v, want *UnboundRefError", err) } } }() diff --git a/state/runner.go b/state/runner.go index 79552bd..699ebeb 100644 --- a/state/runner.go +++ b/state/runner.go @@ -237,7 +237,7 @@ func (r *ServiceRunner[S, E, C]) Tick(ctx context.Context, id string) []FireResu } fn := r.resolve(rs.src.Name) if fn == nil { - res, _ := r.SettleError(ctx, id, &ErrUnboundRef{Kind: "service", Name: rs.src.Name}) + res, _ := r.SettleError(ctx, id, &UnboundRefError{Kind: "service", Name: rs.src.Name}) return []FireResult[S]{res} } out, err := fn(ctx, ServiceCtx[C]{Entity: r.inst.entity, Params: rs.src.Params, Input: rs.input}) diff --git a/state/scheduler.go b/state/scheduler.go index 9c7eb14..b074069 100644 --- a/state/scheduler.go +++ b/state/scheduler.go @@ -79,7 +79,7 @@ func evalBuiltinAction(a Ref) (Effect, error) { case isCommBuiltinAction(a.Name): return evalCommBuiltinAction(a) default: - return nil, &ErrUnknownBuiltin{Name: a.Name} + return nil, &UnknownBuiltinError{Name: a.Name} } } diff --git a/state/transitions_test.go b/state/transitions_test.go index 4678d39..6f43076 100644 --- a/state/transitions_test.go +++ b/state/transitions_test.go @@ -271,9 +271,9 @@ func TestRaise_CycleOverflowsTyped(t *testing.T) { inst := m.Cast(&trec{}, state.WithInitialState("a")) res := inst.Fire(context.Background(), "go") - var overflow *state.ErrMicrostepOverflow + var overflow *state.MicrostepOverflowError if !errors.As(res.Err, &overflow) { - t.Fatalf("err = %v, want *ErrMicrostepOverflow", res.Err) + t.Fatalf("err = %v, want *MicrostepOverflowError", res.Err) } } diff --git a/state/verify.go b/state/verify.go index 7820394..ccffb16 100644 --- a/state/verify.go +++ b/state/verify.go @@ -23,7 +23,7 @@ func (m *Machine[S, E, C]) Verify(s S, entity C, opts ...VerifyOption) error { } if _, ok := m.stateByName(s); !ok { - return &ErrUndeclaredState{State: fmtState(s)} + return &UndeclaredStateError{State: fmtState(s)} } var failures []RequirementFailure