Go Target Documentation

Go Target Documentation

VSCXML generates Go 1.21+ code from W3C SCXML state machine specifications.

W3C Compliance Status

Datamodel Status Tests Passing
ECMAScript (Goja) 100% 183/183
Null 100% 6/6
Native-Go 100% 20/20

The Go target achieves full W3C SCXML compliance across all three datamodels, matching Java, JavaScript, C#, C, and Python targets.

Last Verified: 2026-02-01

Requirements

  • Go 1.21 or later
  • github.com/dop251/goja (for ECMAScript datamodel)
  • github.com/google/uuid (for session IDs)

Installation

The generator produces a ready-to-build Go module with the runtime bundled locally:

bash
scxml-gen input.scxml -t go -o output/MyMachine.go

cd output/
go mod tidy   # downloads goja and uuid
go build ./...
go run .

Generated project layout:

output/
├── MyMachine.go      # Generated state machine (package main)
├── main.go           # Ready-to-run demo entry point
├── go.mod            # Module definition (e.g. example.com/mymachine)
├── README.md
└── scxmlgen/         # Bundled runtime package
    ├── transpiled_state_machine.go
    ├── executor.go
    ├── trace.go
    └── ...

Code-only mode: Use --code-only to generate just the .go file when integrating into an existing module that already has the scxmlgen package.

Published Package

For projects that prefer a versioned dependency:

bash
go get github.com/scxmlgen/scxmlgen

Code Generation

Generate Go code using the CLI:

bash
# Full project (default) — generates go.mod, main.go, scxmlgen/ runtime, README
scxml-gen input.scxml -t go -o output/MyMachine.go

# Code only — single .go file, no project scaffold (for existing modules)
scxml-gen input.scxml -t go -o MyMachine.go --code-only

# Custom module name (used in go.mod and runtime import path)
scxml-gen input.scxml -t go -o output/MyMachine.go --package github.com/myorg/myproject

# Bundle invoke children
scxml-gen parent.scxml -t go -o output/Parent.go --bundle-auto

Usage

Basic Example

go
package main

import (
    "fmt"
    // Bundled runtime — use your module path (set by generator in go.mod):
    "example.com/mymachine/scxmlgen"
    // Or the published package when using `go get github.com/scxmlgen/scxmlgen`:
    // "github.com/scxmlgen/scxmlgen"
)

func main() {
    // Create and start
    machine := NewMyStateMachine()
    machine.Start()

    // Send events
    machine.Send(scxmlgen.NewEvent("go"))
    machine.Send(scxmlgen.NewEventBuilder().
        Name("complete").
        Data(map[string]interface{}{"result": 42}).
        Build())

    // Check current state
    if machine.IsInState("active") {
        fmt.Println("Machine is active")
    }

    // Check completion
    if machine.IsFinished() {
        fmt.Println("Machine has finished")
    }
}

Event Handling

go
import "example.com/mymachine/scxmlgen"

machine := NewMyStateMachine()
machine.Start()

// String-based (convenient) — Send() accepts *scxmlgen.Event
machine.Send(scxmlgen.NewEvent("start"))

// Event with data
data := map[string]interface{}{"count": 5, "name": "test"}
machine.Send(scxmlgen.NewEventBuilder().Name("update").Data(data).Build())

// Full event construction (via runtime context) — advanced use
event := scxmlgen.NewEventBuilder().
    Name("custom").
    Origin("#_session_123").
    OriginType("http://www.w3.org/TR/scxml/#SCXMLEventProcessor").
    Build()
machine.GetRuntimeContext().EnqueueExternal(event)
machine.Pump()

Executor (Thread-Safe, Autonomous Timers)

ContinuousExecutor runs the machine on a dedicated goroutine. When the machine implements IRuntimeAwareStateMachine (all generated machines do), the executor registers a wakeup listener so that timer-fired events (<send delay="..."/>) are processed automatically — no manual Pump() calls required.

go
import "example.com/mymachine/scxmlgen"

machine := NewMyStateMachine()
machine.Start()

executor := scxmlgen.NewContinuousExecutor(machine)

// Register callbacks (optional)
executor.OnFinished(func() {
    fmt.Println("State machine finished")
})

// Start the processing goroutine (also registers autonomous wakeup listener)
executor.Start()

// Send events (thread-safe, non-blocking)
executor.SendEventAsync(scxmlgen.NewEvent("event1"))
executor.SendAsync("event2")

// Wait for the machine to reach a <final> state
executor.Wait()

// Or stop manually
executor.Stop()

Data Models

ECMAScript (Default)

Uses Goja for JavaScript expression evaluation. Goja is a pure Go implementation of ECMAScript 5.1+.

xml
<scxml datamodel="ecmascript">
    <datamodel>
        <data id="counter" expr="0"/>
        <data id="items" expr="[]"/>
    </datamodel>
    <state id="counting">
        <onentry>
            <script>counter = counter + 1; items.push(counter);</script>
        </onentry>
        <transition cond="counter >= 10" target="done"/>
    </state>
</scxml>

Go usage:

go
machine := NewMyMachine()
machine.Start()
fmt.Println(machine.DataModel.Get("counter"))  // 10
fmt.Println(machine.DataModel.Get("items"))    // [1 2 3 ... 10]

Null Datamodel

Minimal datamodel with only In() predicate support. No variables.

xml
<scxml datamodel="null">
    <state id="s0">
        <transition cond="In('s1')" target="fail"/>
        <transition target="pass"/>
    </state>
</scxml>

Native Go

Type-safe Go variables with compile-time code generation. Expressions are transformed to Go syntax at generation time.

xml
<scxml datamodel="native-go">
    <datamodel>
        <data id="counter" type="int" expr="0"/>
        <data id="name" type="string" expr="&quot;default&quot;"/>
        <data id="items" type="[]int" expr="nil"/>
    </datamodel>
    <state id="s0">
        <transition cond="counter > 5" target="done"/>
        <onentry>
            <assign location="counter" expr="counter + 1"/>
        </onentry>
    </state>
</scxml>

Generated code:

go
type MyMachine struct {
    scxmlgen.TranspiledStateMachine
    counter int
    name    string
    items   []int
}

func NewMyMachine() *MyMachine {
    sm := &MyMachine{}
    sm.Init("MyMachine", scxmlgen.DataModelNativeGo, 2)
    sm.counter = 0
    sm.name = "default"
    sm.items = nil
    return sm
}

func (sm *MyMachine) enterS_S0() {
    sm.ActiveStates[S_S0] = struct{}{}
    sm.counter = sm.counter + 1  // Direct Go
}

func (sm *MyMachine) stateS_S0(eventID int, event *scxmlgen.Event) bool {
    if sm.counter > 5 {  // Direct Go condition
        sm.exitS_S0()
        sm.enterS_DONE()
        return true
    }
    return false
}

Supported Types

  • int, int64, float64, bool, string
  • []T (slices), map[K]V (maps)
  • interface{} for untyped variables

Generated Code Structure

The generator produces a single Go file with:

  1. State Constants: Integer indices for O(1) lookups

    go
    const (
        S_IDLE    = 0
        S_RUNNING = 1
    )
  2. Event Constants: Integer IDs for fast dispatch

    go
    const (
        EVT_UNKNOWN = 0
        EVT_START   = 1
        EVT_STOP    = 2
    )
  3. State Methods: Enter/exit/handler for each state

    go
    func (sm *MyMachine) enterS_IDLE()
    func (sm *MyMachine) exitS_IDLE()
    func (sm *MyMachine) stateS_IDLE(eventID int, event *scxmlgen.Event) bool
  4. Eventless Transitions: Three-phase execution for W3C compliance

    go
    func (sm *MyMachine) checkEventlessTransitions() bool

Advanced Features

Delayed Events

Delayed sends are scheduled via Go's time.AfterFunc in the runtime context:

xml
<send event="timeout" delay="5s"/>

With ContinuousExecutor (recommended): timer-fired events are processed automatically via the wakeup listener — no polling loop needed:

go
machine.Start()
executor := scxmlgen.NewContinuousExecutor(machine)
executor.Start()
executor.Wait()   // blocks until machine finishes or executor.Stop() is called

Manual usage (without executor): call Pump() periodically to drain timer-fired events:

go
machine.Start()
for !machine.IsFinished() {
    machine.Pump()
    time.Sleep(50 * time.Millisecond)
}

Invoke (Child State Machines)

Child state machines are bundled at generation time:

bash
scxml-gen parent.scxml -t go -o Parent.go --bundle-auto

The generated code includes an _invokeRegistry for child lookups:

go
var _invokeRegistry = map[string]func() interface{}{
    "file:child.scxml": func() interface{} { return NewParent_External_0() },
    "s0.invoke.0":      func() interface{} { return NewParent_Child_0() },
}

History States

Both shallow and deep history are supported:

go
// Shallow: remembers direct child
history_H0 int

// Deep: remembers all active descendants
history_DEEP_H map[int]struct{}

Parallel States

Parallel regions are entered/exited correctly:

go
func (sm *MyMachine) enterS_PARALLEL() {
    sm.ActiveStates[S_PARALLEL] = struct{}{}
    // Enter all parallel children
    sm.enterS_REGION1()
    sm.enterS_REGION2()
}

Testing

Run W3C conformance tests (when available):

bash
cd scxml-go/test

# All tests
go test -v -run TestW3C

# Specific test
go test -v -run TestW3C/test144

# Specific datamodel
go test -v -run TestW3C -datamodel ecmascript

Performance Considerations

  • State tracking: Uses Go map[int]struct{} for O(1) state checks
  • Event dispatch: Switch statement with integer constants
  • Memory: Generated code has minimal runtime overhead
  • Goja: JavaScript evaluation adds ~5-10MB memory, ~1ms per expression
  • Native-Go: Zero overhead for expressions (compiled directly into Go)

Troubleshooting

Import Error

If the scxmlgen package is not found:

bash
# If using go modules
go mod init myproject
go get github.com/scxmlgen/scxmlgen

# Or copy bundled runtime
scxml-gen input.scxml -t go -o output/
# This creates scxmlgen/ directory

Goja Not Found

bash
go get github.com/dop251/goja

Go Version Error

The generated code requires Go 1.21+. Check your version:

bash
go version

API Reference

TranspiledStateMachine

Method Description
Init(name, dmType, stateCount) Initialize the state machine
Start() Start the state machine
Send(event) Send an event
IsInState(stateID) Check if state is active
Finished Whether machine has terminated
RaiseInternal(name) Raise an internal event
SendToTarget(target, event) Send event to target

Event

Property Description
Name Event name
Type EventTypeExternal, EventTypeInternal, or EventTypePlatform
SendID Send ID for delayed events
Origin Origin URI
OriginType Origin type
InvokeID Invoke ID for child events
Data Event data map
RawData Raw event data
Factory Method Description
NewEvent(name) Simple event
Builder() Fluent builder

RuntimeContext

Method Description
EnqueueInternal(event) Add internal event to queue
EnqueueExternal(event) Add external event to queue
DequeueInternal() Get next internal event
DequeueExternal() Get next external event
ScheduleDelayedSend(...) Schedule a delayed send
CancelDelayedSend(sendID) Cancel a delayed send

ContinuousExecutor

Method Description
NewContinuousExecutor(sm) Create executor for state machine
Start() Start goroutine; registers autonomous wakeup listener if machine implements IRuntimeAwareStateMachine
Stop() Stop executor and deregister wakeup listener
Send(name string) Send named event and wait for processing (blocking)
SendEvent(event *Event) Send event and wait for processing (blocking)
SendAsync(name string) Send named event asynchronously (non-blocking)
SendEventAsync(event *Event) Send event asynchronously (non-blocking)
OnEvent(fn func(*Event)) Set callback invoked after each event is processed
OnFinished(fn func()) Set callback invoked when machine reaches a final state
Wait() Block until machine finishes or Stop() is called
IsRunning() bool Return true if event loop is active

RunToCompletionExecutor

Method Description
NewRunToCompletionExecutor(sm) Create synchronous executor
Send(name string) Send named event synchronously (blocking)
SendEvent(event *Event) Process event synchronously on caller's goroutine
OnEvent(fn func(*Event)) Set event callback
OnFinished(fn func()) Set completion callback
IsFinished() bool Check if state machine finished

Tracing

Type Description
ITraceListener Base interface for trace listeners
IInvokeAwareTraceListener Extended interface with invoke context
JsonlTraceWriter JSONL trace file output
ConsoleTraceListener Console debugging output
TraceAdapter Base implementation for selective overrides

Event Introspection

go
all := machine.GetAllEvents()                    // []string
enabled := machine.GetEnabledEvents()             // []string, guard-aware
forState := machine.GetEventsForState("s1")       // []string
enabledFor := machine.GetEnabledEventsForState("s1") // []string

Example: Tracing to JSONL

go
package main

import "example.com/mymachine/scxmlgen"  // or "github.com/scxmlgen/scxmlgen"

func main() {
    // Create trace writer
    trace, _ := scxmlgen.NewJsonlTraceWriter("trace.jsonl")
    defer trace.Close()

    // Create and configure state machine
    machine := NewMyStateMachine()
    machine.SetTraceListener(trace)

    // Start - trace will record all state changes
    machine.Start()
    machine.Send(scxmlgen.NewEvent("go"))
}