diff --git a/internal/exec/runtime/check_moves.go b/internal/exec/runtime/check_moves.go index 0dcc27aaed..8baf91f593 100644 --- a/internal/exec/runtime/check_moves.go +++ b/internal/exec/runtime/check_moves.go @@ -216,7 +216,8 @@ func (e *StateExecutor) enabledMoves() []enabledMove { } picks = []int{pick} } - moves = append(moves, enabledMove{Owner: e, Node: item.owner, Label: e.entryLabel(item.owner), Kind: moveEntry, Picks: picks}) + owner := e.stateDecl(item.owner) + moves = append(moves, enabledMove{Owner: e, Node: owner, Label: e.entryLabel(owner), Kind: moveEntry, Picks: picks}) } return moves } @@ -243,9 +244,9 @@ func (e *StateExecutor) enabledMoves() []enabledMove { // where the unit draws an order among them. func (e *StateExecutor) doMoves(due []*doAction, picked bool) []enabledMove { moves := make([]enabledMove, 0, len(due)) - names := e.stateNames(statesOf(due)) + names := e.stateNames(e.statesOf(due)) for i, act := range due { - m := enabledMove{Owner: e, Node: act.state, Kind: moveDoStep, Label: "do " + names[i]} + m := enabledMove{Owner: e, Node: e.stateDecl(act.state), Kind: moveDoStep, Label: "do " + names[i]} if picked { m.Picks = []int{i} } @@ -254,10 +255,10 @@ func (e *StateExecutor) doMoves(due []*doAction, picked bool) []enabledMove { return moves } -func statesOf(acts []*doAction) []*ast.StateNode { +func (e *StateExecutor) statesOf(acts []*doAction) []*ast.StateNode { states := make([]*ast.StateNode, len(acts)) for i, act := range acts { - states[i] = act.state + states[i] = e.stateDecl(act.state) } return states } diff --git a/internal/exec/runtime/check_reduce.go b/internal/exec/runtime/check_reduce.go index 4ee02f3750..8446590640 100644 --- a/internal/exec/runtime/check_reduce.go +++ b/internal/exec/runtime/check_reduce.go @@ -52,7 +52,7 @@ func standing(exec *ActionExecutor, t Token) lower.Footprint { func machineStanding(e *StateExecutor) lower.Footprint { fp := dispatchFootprint(e) for _, act := range e.doActions { - fp = unionFootprints(fp, doStepFootprint(e, act.state)) + fp = unionFootprints(fp, doStepFootprint(e, e.stateDecl(act.state))) } return fp } @@ -114,7 +114,7 @@ func triggerChannel(trans *lower.Transition) (lower.Channel, bool) { // by that behavior's, or the next one pending. func doStepFootprint(e *StateExecutor, state ast.Node) lower.Footprint { for _, act := range e.doActions { - if act.state != state { + if e.stateDecl(act.state) != state { continue } if run := act.run; run != nil { diff --git a/internal/exec/runtime/check_state.go b/internal/exec/runtime/check_state.go index ed895c55ef..d116e2912f 100644 --- a/internal/exec/runtime/check_state.go +++ b/internal/exec/runtime/check_state.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -268,16 +269,16 @@ func (s *stateSpeller) machine(e *StateExecutor) { s.out.WriteString(" exited") } if e.activeConfig != nil { - if e.activeConfig.simpleState != nil { - fmt.Fprintf(&s.out, " active{%s}", e.statePath(e.activeConfig.simpleState)) + if e.activeConfig.simpleState != 0 { + fmt.Fprintf(&s.out, " active{%s}", e.statePath(e.simpleState())) } for _, region := range e.orderedActiveRegions() { - fmt.Fprintf(&s.out, " active{%s = %s}", regionKey(region), e.statePath(e.activeConfig.regionStates[region])) + fmt.Fprintf(&s.out, " active{%s = %s}", regionKey(region), e.statePath(e.regionActiveState(region))) } complete := make([]*ast.StateRegion, 0, len(e.activeConfig.defaultComplete)) for region, isComplete := range e.activeConfig.defaultComplete { if isComplete { - complete = append(complete, region) + complete = append(complete, e.regionDecl(region)) } } sort.Slice(complete, func(i, j int) bool { return regionKey(complete[i]) < regionKey(complete[j]) }) @@ -286,13 +287,13 @@ func (s *stateSpeller) machine(e *StateExecutor) { } } for _, state := range e.stateStack { - fmt.Fprintf(&s.out, " stack{%s}", e.statePath(state)) + fmt.Fprintf(&s.out, " stack{%s}", e.statePath(e.stateDecl(state))) } s.history(e) s.joinsArrived(e) fmt.Fprintf(&s.out, " data{%s}", s.values(e.stateData)) - for _, state := range sortedStates(e.stateAttrs) { - fmt.Fprintf(&s.out, " attrs{%s: %s}", e.statePath(state), s.values(e.stateAttrs[state])) + for _, state := range sortedStates(e, e.stateAttrs) { + fmt.Fprintf(&s.out, " attrs{%s: %s}", e.statePath(state), s.values(e.stateAttrs[e.stateID(state)])) } fmt.Fprintf(&s.out, " visits{%s}", strings.Join(e.stateVisits, ", ")) if e.eventQueue != nil { @@ -302,20 +303,20 @@ func (s *stateSpeller) machine(e *StateExecutor) { fmt.Fprintf(&s.out, " event{%s}", s.event(e, event)) } } - for _, trans := range sortedTransitions(e.timerScheduled) { + for _, trans := range sortedTransitions(e, e.timerScheduled) { fmt.Fprintf(&s.out, " timer{%s}", s.transition(e, trans)) } - for _, trans := range sortedTransitions(e.changeFired) { + for _, trans := range sortedTransitions(e, e.changeFired) { fmt.Fprintf(&s.out, " latched{%s}", s.transition(e, trans)) } - for _, trans := range sortedTransitions(e.changePending) { + for _, trans := range sortedTransitions(e, e.changePending) { fmt.Fprintf(&s.out, " pending{%s}", s.transition(e, trans)) } - for _, trans := range sortedTransitionKeys(e.changeObserved) { - fmt.Fprintf(&s.out, " observed{%s=%t}", s.transition(e, trans), e.changeObserved[trans]) + for _, trans := range sortedTransitionKeys(e, e.changeObserved) { + fmt.Fprintf(&s.out, " observed{%s=%t}", s.transition(e, trans), e.changeObserved[e.transID(trans)]) } for _, act := range e.doActions { - fmt.Fprintf(&s.out, " do{%s: %d pending", e.statePath(act.state), len(act.pending)) + fmt.Fprintf(&s.out, " do{%s: %d pending", e.statePath(e.stateDecl(act.state)), len(act.pending)) if act.run != nil { fmt.Fprintf(&s.out, ", paused{%s}", s.body(act.run.body)) } @@ -327,11 +328,11 @@ func (s *stateSpeller) machine(e *StateExecutor) { // history spells the last active child, and region children, of each state // the machine remembers. func (s *stateSpeller) history(e *StateExecutor) { - for _, state := range sortedStates(e.history) { - record := e.history[state] - fmt.Fprintf(&s.out, " history{%s = %s", e.statePath(state), s.stateName(e, record.child)) - for _, region := range sortedRegions(record.regions) { - fmt.Fprintf(&s.out, ", %s = %s", regionKey(region), s.stateName(e, record.regions[region])) + for _, state := range sortedStates(e, e.history) { + record := e.history[e.stateID(state)] + fmt.Fprintf(&s.out, " history{%s = %s", e.statePath(state), s.stateName(e, e.stateDecl(record.child))) + for _, region := range sortedRegions(e, record.regions) { + fmt.Fprintf(&s.out, ", %s = %s", regionKey(region), s.stateName(e, e.stateDecl(record.regions[e.regionID(region)]))) } s.out.WriteString("}") } @@ -340,14 +341,14 @@ func (s *stateSpeller) history(e *StateExecutor) { // joinsArrived spells the incoming segments that have arrived at each join. func (s *stateSpeller) joinsArrived(e *StateExecutor) { for _, join := range e.graph.Pseudostates { - arrived := e.joinArrived[join] + arrived := e.joinArrived[e.pseudoID(join)] if len(arrived) == 0 { continue } fmt.Fprintf(&s.out, " arrived{%s: ", join.Name) first := true for _, segment := range e.joinIncoming(join) { - if !slices.Contains(arrived, segment) { + if !slices.Contains(arrived, e.transID(segment)) { continue } if !first { @@ -363,8 +364,8 @@ func (s *stateSpeller) joinsArrived(e *StateExecutor) { s.out.WriteString("}") } fmt.Fprintf(&s.out, " data{%s}", s.bodyValues(e.stateData, e.stateCells)) - for _, state := range sortedStates(e.stateAttrs) { - fmt.Fprintf(&s.out, " attrs{%s: %s}", e.statePath(state), s.bodyValues(e.stateAttrs[state], e.stateAttrCells[state])) + for _, state := range sortedStates(e, e.stateAttrs) { + fmt.Fprintf(&s.out, " attrs{%s: %s}", e.statePath(state), s.bodyValues(e.stateAttrs[e.stateID(state)], e.stateAttrCells[e.stateID(state)])) } fmt.Fprintf(&s.out, " visits{%s}", strings.Join(e.stateVisits, ", ")) if e.eventQueue != nil { @@ -374,14 +375,14 @@ func (s *stateSpeller) joinsArrived(e *StateExecutor) { fmt.Fprintf(&s.out, " event{%s}", s.event(e, event)) } } - for _, trans := range sortedTransitions(e.timerScheduled) { + for _, trans := range sortedTransitions(e, e.timerScheduled) { fmt.Fprintf(&s.out, " timer{%s}", s.transition(e, trans)) } - for _, trans := range sortedTransitions(e.changeFired) { + for _, trans := range sortedTransitions(e, e.changeFired) { fmt.Fprintf(&s.out, " latched{%s}", s.transition(e, trans)) } for _, act := range e.doActions { - fmt.Fprintf(&s.out, " do{%s: %d pending", e.statePath(act.state), len(act.pending)) + fmt.Fprintf(&s.out, " do{%s: %d pending", e.statePath(e.stateDecl(act.state)), len(act.pending)) if act.run != nil { fmt.Fprintf(&s.out, ", paused{%s}", s.body(act.run.body)) } @@ -405,41 +406,41 @@ func regionKey(region *ast.StateRegion) string { } // sortedStates orders a map's state keys by identity, so the form is independent of map order. -func sortedStates[V any](m map[*ast.StateNode]V) []*ast.StateNode { +func sortedStates[V any](e *StateExecutor, m map[program.StateID]V) []*ast.StateNode { states := make([]*ast.StateNode, 0, len(m)) for state := range m { - states = append(states, state) + states = append(states, e.stateDecl(state)) } sort.Slice(states, func(i, j int) bool { return nodeKey(states[i]) < nodeKey(states[j]) }) return states } // sortedRegions orders a map's region keys by identity. -func sortedRegions[V any](m map[*ast.StateRegion]V) []*ast.StateRegion { +func sortedRegions[V any](e *StateExecutor, m map[program.RegionID]V) []*ast.StateRegion { regions := make([]*ast.StateRegion, 0, len(m)) for region := range m { - regions = append(regions, region) + regions = append(regions, e.regionDecl(region)) } sort.Slice(regions, func(i, j int) bool { return regionKey(regions[i]) < regionKey(regions[j]) }) return regions } // sortedTransitions orders a set's transitions by source and position. -func sortedTransitions(m map[*lower.Transition]bool) []*lower.Transition { +func sortedTransitions(e *StateExecutor, m map[program.TransID]bool) []*lower.Transition { transitions := make([]*lower.Transition, 0, len(m)) for trans, set := range m { if set { - transitions = append(transitions, trans) + transitions = append(transitions, e.transDecl(trans)) } } sort.Slice(transitions, func(i, j int) bool { return transitionKey(transitions[i]) < transitionKey(transitions[j]) }) return transitions } -func sortedTransitionKeys(m map[*lower.Transition]bool) []*lower.Transition { +func sortedTransitionKeys(e *StateExecutor, m map[program.TransID]bool) []*lower.Transition { transitions := make([]*lower.Transition, 0, len(m)) for trans := range m { - transitions = append(transitions, trans) + transitions = append(transitions, e.transDecl(trans)) } sort.Slice(transitions, func(i, j int) bool { return transitionKey(transitions[i]) < transitionKey(transitions[j]) }) return transitions diff --git a/internal/exec/runtime/debug_api_test.go b/internal/exec/runtime/debug_api_test.go index 4d8fc9c3c6..067f53544a 100644 --- a/internal/exec/runtime/debug_api_test.go +++ b/internal/exec/runtime/debug_api_test.go @@ -1296,11 +1296,8 @@ func activeStateNames(exec *StateExecutor) string { func firedNames(exec *StateExecutor) []string { out := make([]string, 0, exec.FiredCount()) for _, fired := range exec.FiredTransitions() { - source := "" - if fired.Source != nil { - source = StateVertexName(fired.Source) - } - out = append(out, source+"->"+StateVertexName(fired.Target)) + _, from, to, _ := exec.FiredNodes(fired) + out = append(out, StateVertexName(from)+"->"+StateVertexName(to)) } return out } @@ -1365,7 +1362,7 @@ func TestFiredSinceReleasesTheFiringsRead(t *testing.T) { if got := exec.FiredSince(0); len(got) != 2 || got[0].Target != exec.FiredTransitions()[0].Target { t.Errorf("FiredSince(0) = %v, want the two kept firings, the released one gone", got) } - if got := exec.FiredSince(2); len(got) != 1 || StateVertexName(got[0].Target) != "working" { + if got := exec.FiredSince(2); len(got) != 1 || StateVertexName(exec.vertexDecl(got[0].Target)) != "working" { t.Errorf("FiredSince(2) = %v, want waiting->working alone", got) } if got := firedNames(exec); !slices.Equal(got, []string{"waiting->working"}) || exec.FiredCount() != 3 { @@ -1380,7 +1377,7 @@ func TestFiredSinceReleasesTheFiringsRead(t *testing.T) { if err := exec.ProcessNextEvent(); err != nil { t.Fatalf("ProcessNextEvent: %v", err) } - if got := exec.FiredSince(3); len(got) != 1 || StateVertexName(got[0].Target) != "done" || exec.FiredCount() != 4 { + if got := exec.FiredSince(3); len(got) != 1 || StateVertexName(exec.vertexDecl(got[0].Target)) != "done" || exec.FiredCount() != 4 { t.Errorf("FiredSince(3) = %v of %d, want working->done of 4", got, exec.FiredCount()) } exec.KeepFired(false) diff --git a/internal/exec/runtime/held_image_behavior.go b/internal/exec/runtime/held_image_behavior.go index 370628d05c..6dbf91fcae 100644 --- a/internal/exec/runtime/held_image_behavior.go +++ b/internal/exec/runtime/held_image_behavior.go @@ -6,6 +6,7 @@ import ( "slices" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -123,38 +124,38 @@ type imagedState struct { events []Event stateData map[string]Value stateCells []imagedBodyCell - stateAttrs map[*ast.StateNode]map[string]Value - stateAttrCells map[*ast.StateNode][]imagedBodyCell + stateAttrs map[program.StateID]map[string]Value + stateAttrCells map[program.StateID][]imagedBodyCell stateVisits []string - stateStack []*ast.StateNode + stateStack []program.StateID fired []FiredTransition firedBase int keepFired bool - breakpointNodes map[ast.Node]bool - breakpointHit *ast.StateNode - pausedAt ast.Node + breakpointNodes map[program.VertexRef]bool + breakpointHit program.StateID + pausedAt program.VertexRef completionDue bool - history map[*ast.StateNode]historyRecord - joinArrived map[*ast.PseudostateNode][]*lower.Transition + history map[program.StateID]historyRecord + joinArrived map[program.PseudoID][]program.TransID lastDispatch *Dispatch lastEventAt float64 doActions []doActionCapture machineExited bool run, under int inRun, moved bool - timerScheduled map[*lower.Transition]bool - timeTriggerVerdict map[*lower.Transition]error - changeFired map[*lower.Transition]bool - changeObserved map[*lower.Transition]bool - changePending map[*lower.Transition]bool - firingChange *lower.Transition + timerScheduled map[program.TransID]bool + timeTriggerVerdict map[program.TransID]error + changeFired map[program.TransID]bool + changeObserved map[program.TransID]bool + changePending map[program.TransID]bool + firingChange program.TransID firingNotes []RunNote - changeRearmed map[*lower.Transition]bool + changeRearmed map[program.TransID]bool changeWaits []changeWait held []heldEntry - entering map[*ast.StateNode]bool + entering map[program.StateID]bool enteringMachine bool - activeAtEntry map[*ast.StateNode]bool + activeAtEntry map[program.StateID]bool pendingCall *pendingCall } @@ -426,8 +427,8 @@ func (t *imaging) stateExecutor(e *StateExecutor) (*imagedState, error) { graph: e.graph, state: e.state, activeConfig: cloneConfiguration(e.activeConfig), nextEventID: e.nextEventID, stateData: maps.Clone(e.stateData), - stateAttrCells: make(map[*ast.StateNode][]imagedBodyCell, len(e.stateAttrCells)), - stateAttrs: make(map[*ast.StateNode]map[string]Value, len(e.stateAttrs)), + stateAttrCells: make(map[program.StateID][]imagedBodyCell, len(e.stateAttrCells)), + stateAttrs: make(map[program.StateID]map[string]Value, len(e.stateAttrs)), stateVisits: slices.Clone(e.stateVisits), stateStack: slices.Clone(e.stateStack), fired: slices.Clone(e.fired), @@ -437,7 +438,7 @@ func (t *imaging) stateExecutor(e *StateExecutor) (*imagedState, error) { breakpointHit: e.breakpointHit, pausedAt: e.pausedAt, completionDue: e.completionDue, - history: make(map[*ast.StateNode]historyRecord, len(e.history)), + history: make(map[program.StateID]historyRecord, len(e.history)), joinArrived: cloneJoinArrivals(e.joinArrived), lastDispatch: cloneDispatch(e.lastDispatch), lastEventAt: e.lastEventAt, @@ -482,7 +483,7 @@ func (t *imaging) stateExecutor(e *StateExecutor) (*imagedState, error) { cloned := maps.Clone(attrs) img.stateAttrCells[node] = imageBodyCells(e.stateAttrCells[node], cloned) if err := t.values(cloned); err != nil { - return nil, fmt.Errorf("state %s: %w", StateVertexName(node), err) + return nil, fmt.Errorf("state %s: %w", StateVertexName(e.stateDecl(node)), err) } img.stateAttrs[node] = cloned } @@ -505,16 +506,16 @@ func (t *imaging) stateExecutor(e *StateExecutor) (*imagedState, error) { for _, act := range e.doActions { if act.run != nil { return nil, fmt.Errorf("%w: do behavior of state %s of %s", ErrSnapshotPausedBody, - StateVertexName(act.state), SymbolText(e.stateMachine)) + StateVertexName(e.stateDecl(act.state)), SymbolText(e.stateMachine)) } if err := t.firing(act.firing); err != nil { - return nil, fmt.Errorf("do behavior of state %s: %w", StateVertexName(act.state), err) + return nil, fmt.Errorf("do behavior of state %s: %w", StateVertexName(e.stateDecl(act.state)), err) } img.doActions = append(img.doActions, doActionCapture{act: &doAction{state: act.state}, pending: slices.Clone(act.pending), firing: act.firing.snapshot()}) } for _, item := range img.held { if err := t.firing(item.firing); err != nil { - return nil, fmt.Errorf("held entry of %s: %w", StateVertexName(item.owner), err) + return nil, fmt.Errorf("held entry of %s: %w", StateVertexName(e.stateDecl(item.owner)), err) } } return img, nil @@ -1020,24 +1021,25 @@ func (m *materializing) stateExecutor(e *StateExecutor, img *imagedState) error restoreStateBodyCells(m.dst, e.stateCells, e.graph.Attributes, img.stateCells, "state machine "+SymbolText(e.stateMachine), e.graph.Scope) } - for node, attrs := range img.stateAttrs { - if e.stateAttrs[node], err = m.values(attrs); err != nil { + for id, attrs := range img.stateAttrs { + node := e.stateDecl(id) + if e.stateAttrs[id], err = m.values(attrs); err != nil { return fmt.Errorf("state %s: %w", StateVertexName(node), err) } - if image := img.stateAttrCells[node]; len(image) != 0 { - cells := newBodyCells(e.stateAttrs[node], func(scope *symbols.Scope) *EvalContext { + if image := img.stateAttrCells[id]; len(image) != 0 { + cells := newBodyCells(e.stateAttrs[id], func(scope *symbols.Scope) *EvalContext { return e.stateAttributeContext(node, scope) }) restoreStateBodyCells(m.dst, cells, e.graph.StateAttributes[node], image, "state "+node.Name, e.graph.Scope) - e.stateAttrCells[node] = cells + e.stateAttrCells[id] = cells } } e.stateVisits, e.stateStack = slices.Clone(img.stateVisits), slices.Clone(img.stateStack) e.fired, e.firedBase, e.keepFired = slices.Clone(img.fired), img.firedBase, img.keepFired e.breakpointNodes = maps.Clone(img.breakpointNodes) if e.breakpointNodes == nil { - e.breakpointNodes = make(map[ast.Node]bool) + e.breakpointNodes = make(map[program.VertexRef]bool) } e.breakpointHit, e.pausedAt, e.completionDue = img.breakpointHit, img.pausedAt, img.completionDue e.joinArrived = cloneJoinArrivals(img.joinArrived) @@ -1063,7 +1065,7 @@ func (m *materializing) stateExecutor(e *StateExecutor, img *imagedState) error for _, act := range img.doActions { carried, err := m.firing(act.firing) if err != nil { - return fmt.Errorf("do behavior of state %s: %w", StateVertexName(act.act.state), err) + return fmt.Errorf("do behavior of state %s: %w", StateVertexName(e.stateDecl(act.act.state)), err) } e.doActions = append(e.doActions, &doAction{state: act.act.state, pending: slices.Clone(act.pending), firing: carried}) } @@ -1074,14 +1076,14 @@ func (m *materializing) stateExecutor(e *StateExecutor, img *imagedState) error e.changeFired = maps.Clone(img.changeFired) e.changeObserved = maps.Clone(img.changeObserved) e.changePending = maps.Clone(img.changePending) - e.changeReads = make(map[*lower.Transition][]*FeatureValue) + e.changeReads = make(map[program.TransID][]*FeatureValue) e.firingChange, e.firingNotes = img.firingChange, slices.Clone(img.firingNotes) e.changeRearmed = maps.Clone(img.changeRearmed) e.changeWaits = slices.Clone(img.changeWaits) e.held = cloneHeldEntries(img.held) for i := range e.held { if e.held[i].firing, err = m.firing(img.held[i].firing); err != nil { - return fmt.Errorf("held entry of %s: %w", StateVertexName(e.held[i].owner), err) + return fmt.Errorf("held entry of %s: %w", StateVertexName(e.stateDecl(e.held[i].owner)), err) } } clear(e.entering) diff --git a/internal/exec/runtime/history_test.go b/internal/exec/runtime/history_test.go index a0341f40cb..c05cb464b9 100644 --- a/internal/exec/runtime/history_test.go +++ b/internal/exec/runtime/history_test.go @@ -207,7 +207,7 @@ func regionHistoryMachine(kind ast.PseudostateKind, nestLeft bool) *ast.Usage { func advanceRegion(t *testing.T, exec *StateExecutor, regionName, source, target string) { t.Helper() for region := range exec.activeConfig.regionStates { - if region.Name != regionName { + if exec.regionDecl(region).Name != regionName { continue } trans := transitionBetween(t, exec, source, target) @@ -215,7 +215,7 @@ func advanceRegion(t *testing.T, exec *StateExecutor, regionName, source, target if err != nil { t.Fatalf("advance region %s: %v", regionName, err) } - fired, err := exec.fireTransitionInRegion(region, trans, route) + fired, err := exec.fireTransitionInRegion(exec.regionDecl(region), trans, route) if err != nil { t.Fatalf("advance region %s: %v", regionName, err) } @@ -245,7 +245,7 @@ func TestHistoryRestoresOrthogonalRegions(t *testing.T) { restored := map[string]bool{} for _, state := range exec.activeConfig.regionStates { - restored[state.Name] = true + restored[exec.stateDecl(state).Name] = true } if !restored["lwork"] || !restored["rwork"] { t.Errorf("restored regions = %v, want lwork and rwork", restored) @@ -310,7 +310,7 @@ func TestDeepHistoryRestoresARegionLeftFromInsideItsCompositeState(t *testing.T) restored := map[string]bool{} for _, state := range exec.activeConfig.regionStates { - restored[state.Name] = true + restored[exec.stateDecl(state).Name] = true } if !restored["lwork"] || !restored["wrapper"] || !restored["ideep"] { t.Errorf("restored regions = %v, want lwork, wrapper and ideep", restored) @@ -342,7 +342,7 @@ func TestDeepHistoryRestoresBelowRegion(t *testing.T) { restored := map[string]bool{} for _, state := range exec.activeConfig.regionStates { - restored[state.Name] = true + restored[exec.stateDecl(state).Name] = true } if !restored["ldeep"] || !restored["rwork"] { t.Errorf("restored regions = %v, want ldeep and rwork", restored) @@ -399,7 +399,7 @@ func TestExitingNestedRegionsKeepsSiblingRegions(t *testing.T) { active := map[string]bool{} for _, state := range exec.activeConfig.regionStates { - active[state.Name] = true + active[exec.stateDecl(state).Name] = true } if !active["rstart"] { t.Errorf("active configuration = %v, want the sibling region to still hold rstart", active) diff --git a/internal/exec/runtime/join_snapshot_test.go b/internal/exec/runtime/join_snapshot_test.go index 9ba9a982a3..cb49064c97 100644 --- a/internal/exec/runtime/join_snapshot_test.go +++ b/internal/exec/runtime/join_snapshot_test.go @@ -3,8 +3,6 @@ package runtime import ( "strings" "testing" - - "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) func TestCloneNilJoinArrivalsIsWritable(t *testing.T) { @@ -12,7 +10,7 @@ func TestCloneNilJoinArrivalsIsWritable(t *testing.T) { if arrived == nil { t.Fatal("cloneJoinArrivals(nil) = nil, want an initialized map") } - arrived[&ast.PseudostateNode{}] = nil + arrived[1] = nil } func TestSnapshotRestoresJoinArrivals(t *testing.T) { diff --git a/internal/exec/runtime/region_pseudostate_test.go b/internal/exec/runtime/region_pseudostate_test.go index 7b11e05e47..36a357b44f 100644 --- a/internal/exec/runtime/region_pseudostate_test.go +++ b/internal/exec/runtime/region_pseudostate_test.go @@ -29,7 +29,7 @@ func runStateMachine(t *testing.T, name, src string) *StateExecutor { func regionConfig(exec *StateExecutor) map[string]string { config := make(map[string]string, len(exec.activeConfig.regionStates)) for region, state := range exec.activeConfig.regionStates { - config[region.Name] = state.Name + config[exec.regionDecl(region).Name] = exec.stateDecl(state).Name } return config } diff --git a/internal/exec/runtime/robustness_junction_join_test.go b/internal/exec/runtime/robustness_junction_join_test.go index 15cf18c6f9..93970cfa4d 100644 --- a/internal/exec/runtime/robustness_junction_join_test.go +++ b/internal/exec/runtime/robustness_junction_join_test.go @@ -209,7 +209,7 @@ func TestRuntimeRobustnessJunctionJoin(t *testing.T) { if sync == nil { t.Fatal("sync join not found") } - if got := len(exec.joinArrived[sync]); got != 1 { + if got := len(exec.joinArrived[exec.pseudoID(sync)]); got != 1 { t.Fatalf("sync arrivals after first timer expiry = %d, want 1", got) } activeSources := 0 diff --git a/internal/exec/runtime/snapshot.go b/internal/exec/runtime/snapshot.go index 997908d1ff..3b23447e21 100644 --- a/internal/exec/runtime/snapshot.go +++ b/internal/exec/runtime/snapshot.go @@ -6,6 +6,7 @@ import ( "slices" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -822,47 +823,47 @@ type stateCapture struct { nextEventID int64 events eventHeap stateData mapState[string, Value] - stateAttrs map[*ast.StateNode]mapState[string, Value] + stateAttrs map[program.StateID]mapState[string, Value] stateCells bodyCellsCapture - stateAttrCells map[*ast.StateNode]bodyCellsCapture + stateAttrCells map[program.StateID]bodyCellsCapture stateVisits []string - stateStack []*ast.StateNode + stateStack []program.StateID fired []FiredTransition firedBase int - breakpointHit *ast.StateNode - pausedAt ast.Node + breakpointHit program.StateID + pausedAt program.VertexRef completionDue bool - history map[*ast.StateNode]historyRecord - joinArrived map[*ast.PseudostateNode][]*lower.Transition + history map[program.StateID]historyRecord + joinArrived map[program.PseudoID][]program.TransID lastDispatch *Dispatch lastEventAt float64 doActions []doActionCapture machineExited bool driven, under *runState inRun, moved bool - timerScheduled mapState[*lower.Transition, bool] - timeTriggerVerdict mapState[*lower.Transition, error] - changeFired mapState[*lower.Transition, bool] - changeObserved mapState[*lower.Transition, bool] - changePending mapState[*lower.Transition, bool] - changeReads map[*lower.Transition][]*FeatureValue - firingChange *lower.Transition + timerScheduled mapState[program.TransID, bool] + timeTriggerVerdict mapState[program.TransID, error] + changeFired mapState[program.TransID, bool] + changeObserved mapState[program.TransID, bool] + changePending mapState[program.TransID, bool] + changeReads map[program.TransID][]*FeatureValue + firingChange program.TransID firingNotes []RunNote - changeRearmed mapState[*lower.Transition, bool] + changeRearmed mapState[program.TransID, bool] changeWaits []changeWait // held contains entry cascades paused at RTC boundaries. held []heldEntry // entering marks states entered during the captured entry unit. - entering map[*ast.StateNode]bool + entering map[program.StateID]bool // enteringMachine reports whether the captured entry unit includes the machine. enteringMachine bool // activeAtEntry records states active before the captured entry unit. - activeAtEntry map[*ast.StateNode]bool + activeAtEntry map[program.StateID]bool pendingCall *pendingCall } // captureJoinArrivals copies each join's ordered arrivals for a snapshot. -func captureJoinArrivals(arrived map[*ast.PseudostateNode][]*lower.Transition) map[*ast.PseudostateNode][]*lower.Transition { +func captureJoinArrivals(arrived map[program.PseudoID][]program.TransID) map[program.PseudoID][]program.TransID { return cloneJoinArrivals(arrived) } @@ -887,9 +888,9 @@ func (e *StateExecutor) capture() stateCapture { exec: e, occurrence: e.occurrence, state: e.state, activeConfig: cloneConfiguration(e.activeConfig), nextEventID: e.nextEventID, stateData: captureMap(e.stateData), - stateAttrs: make(map[*ast.StateNode]mapState[string, Value], len(e.stateAttrs)), + stateAttrs: make(map[program.StateID]mapState[string, Value], len(e.stateAttrs)), stateCells: captureBodyCells(e.stateCells), - stateAttrCells: make(map[*ast.StateNode]bodyCellsCapture, len(e.stateAttrCells)), + stateAttrCells: make(map[program.StateID]bodyCellsCapture, len(e.stateAttrCells)), stateVisits: slices.Clone(e.stateVisits), stateStack: slices.Clone(e.stateStack), fired: slices.Clone(e.fired), @@ -897,7 +898,7 @@ func (e *StateExecutor) capture() stateCapture { breakpointHit: e.breakpointHit, pausedAt: e.pausedAt, completionDue: e.completionDue, - history: make(map[*ast.StateNode]historyRecord, len(e.history)), + history: make(map[program.StateID]historyRecord, len(e.history)), joinArrived: captureJoinArrivals(e.joinArrived), lastDispatch: cloneDispatch(e.lastDispatch), lastEventAt: e.lastEventAt, @@ -956,7 +957,7 @@ func (c stateCapture) restore() { e.stateAttrs[node] = attrs.restore() } } - e.stateAttrCells = make(map[*ast.StateNode]*bodyCells, len(c.stateAttrCells)) + e.stateAttrCells = make(map[program.StateID]*bodyCells, len(c.stateAttrCells)) for node, cells := range c.stateAttrCells { cells.restore() e.stateAttrCells[node] = cells.cells diff --git a/internal/exec/runtime/state_change_trigger.go b/internal/exec/runtime/state_change_trigger.go index 25f41645eb..c480ecd349 100644 --- a/internal/exec/runtime/state_change_trigger.go +++ b/internal/exec/runtime/state_change_trigger.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -45,18 +46,19 @@ type changeMark struct { func (e *StateExecutor) changeMarkOf(trans *lower.Transition) changeMark { var m changeMark - m.pending, m.hadPending = e.changePending[trans] - m.observed, m.hadObserved = e.changeObserved[trans] - m.fired, m.hadFired = e.changeFired[trans] + m.pending, m.hadPending = e.changePending[e.transID(trans)] + m.observed, m.hadObserved = e.changeObserved[e.transID(trans)] + m.fired, m.hadFired = e.changeFired[e.transID(trans)] return m } func (e *StateExecutor) restoreChangeMark(trans *lower.Transition, m changeMark) { - restore := func(values map[*lower.Transition]bool, value, present bool) { + id := e.transID(trans) + restore := func(values map[program.TransID]bool, value, present bool) { if present { - values[trans] = value + values[id] = value } else { - delete(values, trans) + delete(values, id) } } restore(e.changePending, m.pending, m.hadPending) @@ -74,7 +76,7 @@ func (e *StateExecutor) restoreChangeMark(trans *lower.Transition, m changeMark) // same edge again, and only a condition observed false re-arms it. func (e *StateExecutor) pollChangeEvents() (bool, error) { poll := newChangePoll() - e.changeRearmed = make(map[*lower.Transition]bool) + e.changeRearmed = make(map[program.TransID]bool) defer func() { e.changeRearmed = nil }() if err := e.observeChangeConditions(poll); err != nil { @@ -111,21 +113,21 @@ func (e *StateExecutor) pollChangeEvents() (bool, error) { } if !pass { poll.blocked[trans] = true - poll.wait(trans, candidate.source.Name, guardWaitReason(unvalued)) + poll.wait(trans, e.stateDecl(candidate.source).Name, guardWaitReason(unvalued)) return false, nil } // The edge is latched before it is taken: an effect that leaves the // condition true must not enable the same edge again, and the exit this // firing causes must not re-arm the edge that caused it. A join an earlier // candidate disarmed fires nothing, which is no move. - e.changeFired[trans] = true - e.firingChange = trans + e.changeFired[e.transID(trans)] = true + e.firingChange = e.transID(trans) fired, err := e.firingOn(occurrence, func() (bool, error) { - return e.fireFrom(candidate.source, trans, notes, candidate.route) + return e.fireFrom(e.stateDecl(candidate.source), trans, notes, candidate.route) }) - e.firingChange = nil + e.firingChange = 0 if err != nil { - return true, fmt.Errorf("fire transition out of %s: %w", candidate.source.Name, err) + return true, fmt.Errorf("fire transition out of %s: %w", e.stateDecl(candidate.source).Name, err) } return fired, nil }) @@ -166,7 +168,7 @@ func (e *StateExecutor) risenChanges() ([]*lower.Transition, bool, error) { e.changeObserved, e.changePending, e.changeReads = observed, pending, reads }() poll := newChangePoll() - e.changeRearmed = make(map[*lower.Transition]bool) + e.changeRearmed = make(map[program.TransID]bool) defer func() { e.changeRearmed = nil }() if err := e.observeChangeConditions(poll); err != nil { return nil, true, err @@ -201,7 +203,7 @@ func (e *StateExecutor) risenChange() (*lower.Transition, bool, error) { // blocked since, and its watch not re-armed by a state entry during the poll, // which belongs to an activation later than the observation. func (e *StateExecutor) riseEnables(poll *changePoll, trans *lower.Transition) bool { - return poll.condition[trans] && poll.guard[trans] && !poll.blocked[trans] && !e.changeRearmed[trans] + return poll.condition[trans] && poll.guard[trans] && !poll.blocked[trans] && !e.changeRearmed[e.transID(trans)] } // consumeRise latches every transition the rise enabled, not only the ones taken, @@ -212,7 +214,7 @@ func (e *StateExecutor) consumeRise(poll *changePoll) bool { consumed := false for trans := range poll.condition { if e.riseEnables(poll, trans) { - e.changeFired[trans] = true + e.changeFired[e.transID(trans)] = true consumed = true } } @@ -239,13 +241,13 @@ func (e *StateExecutor) observeChangeConditions(poll *changePoll) error { if err != nil { return fmt.Errorf("state %s: %w", source.Name, err) } - pending := e.changePending[trans] - delete(e.changePending, trans) + pending := e.changePending[e.transID(trans)] + delete(e.changePending, e.transID(trans)) poll.condition[trans] = holds || pending poll.observed = append(poll.observed, trans) - e.changeObserved[trans] = holds + e.changeObserved[e.transID(trans)] = holds if !holds { - delete(e.changeFired, trans) + delete(e.changeFired, e.transID(trans)) } if !poll.condition[trans] { if unvalued != nil { @@ -255,7 +257,7 @@ func (e *StateExecutor) observeChangeConditions(poll *changePoll) error { } continue } - if e.changeFired[trans] { + if e.changeFired[e.transID(trans)] { poll.wait(trans, source.Name, "condition has not changed since it fired") continue } @@ -333,7 +335,7 @@ func (e *StateExecutor) changeConditionHolds(changeEvent *ast.ChangeEvent, trans } // The reads are kept for a condition with no result too: the write that // gives the feature its value is the one that must re-poll it. - e.changeReads[trans] = reads + e.changeReads[e.transID(trans)] = reads return holds, unvalued, nil } @@ -386,11 +388,11 @@ func (e *StateExecutor) observeChangedValue(relevant func([]*FeatureValue) bool) } seen[trans] = true if previouslyWatched { - if _, watched := e.changeObserved[trans]; !watched { + if _, watched := e.changeObserved[e.transID(trans)]; !watched { continue } } - if !relevant(e.changeReads[trans]) { + if !relevant(e.changeReads[e.transID(trans)]) { continue } var holds bool @@ -400,15 +402,15 @@ func (e *StateExecutor) observeChangedValue(relevant func([]*FeatureValue) bool) continue } prior := e.changeMarkOf(trans) - observed := e.changeObserved[trans] + observed := e.changeObserved[e.transID(trans)] if !holds { - e.changeObserved[trans] = false - delete(e.changeFired, trans) + e.changeObserved[e.transID(trans)] = false + delete(e.changeFired, e.transID(trans)) } else { - if !observed && !e.changeFired[trans] { - e.changePending[trans] = true + if !observed && !e.changeFired[e.transID(trans)] { + e.changePending[e.transID(trans)] = true } - e.changeObserved[trans] = true + e.changeObserved[e.transID(trans)] = true } if e.changeMarkOf(trans) != prior { e.ctx.noteProbeUndo(func() { e.restoreChangeMark(trans, prior) }) @@ -439,7 +441,7 @@ func (e *StateExecutor) risenChangeTransitions(state *ast.StateNode, poll *chang if _, ok := trans.Trigger.(*ast.ChangeEvent); !ok { continue } - if poll.condition[trans] && !e.changeFired[trans] && poll.guard[trans] { + if poll.condition[trans] && !e.changeFired[e.transID(trans)] && poll.guard[trans] { available, err := e.routeAvailable(trans, occurrence) if err != nil { return nil, nil, err diff --git a/internal/exec/runtime/state_change_trigger_test.go b/internal/exec/runtime/state_change_trigger_test.go index 06a221c982..a9649104e8 100644 --- a/internal/exec/runtime/state_change_trigger_test.go +++ b/internal/exec/runtime/state_change_trigger_test.go @@ -718,7 +718,7 @@ func TestChangeTriggerArrivalCompletesOnLaterRise(t *testing.T) { if got := activeStateNames(exec); got != "b1" || len(exec.joinArrived) != 1 { t.Fatalf("after a's rise: configuration = %s, arrivals = %v; want b1 and a's waiting arrival", got, exec.joinArrived) } - if !exec.changeFired[exec.graph.Transitions[stateNamed(t, exec, "a1")][0]] { + if !exec.changeFired[exec.transID(exec.graph.Transitions[stateNamed(t, exec, "a1")][0])] { t.Error("a's rise is not latched: the next poll would offer the same occurrence again") } // b's later rise completes the join with a's earlier occurrence. diff --git a/internal/exec/runtime/state_executor.go b/internal/exec/runtime/state_executor.go index 310213258b..ff8c90db47 100644 --- a/internal/exec/runtime/state_executor.go +++ b/internal/exec/runtime/state_executor.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" @@ -18,14 +19,58 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/syntax/source" ) -// stateConfiguration represents the active state configuration (simple or multi-region). +// stateConfiguration is the active state configuration by graph-local id: the +// single active state, or the active state of each orthogonal region. type stateConfiguration struct { - // For simple states (no regions): single active state - simpleState *ast.StateNode + simpleState program.StateID - // For composite states with regions: map of region → active state in that region - regionStates map[*ast.StateRegion]*ast.StateNode - defaultComplete map[*ast.StateRegion]bool + regionStates map[program.RegionID]program.StateID + defaultComplete map[program.RegionID]bool +} + +// reset empties the configuration. +func (c *stateConfiguration) reset() { + c.simpleState = 0 + c.regionStates = make(map[program.RegionID]program.StateID) + c.defaultComplete = make(map[program.RegionID]bool) +} + +// deactivate drops the region from the configuration. +func (c *stateConfiguration) deactivate(region program.RegionID) { + delete(c.regionStates, region) + delete(c.defaultComplete, region) +} + +// regionIsActive reports whether region has an active state. +func (e *StateExecutor) regionIsActive(region *ast.StateRegion) bool { + _, active := e.activeConfig.regionStates[e.regionID(region)] + return active +} + +// regionActive is region's active state, and whether it has one. +func (e *StateExecutor) regionActive(region *ast.StateRegion) (*ast.StateNode, bool) { + id, active := e.activeConfig.regionStates[e.regionID(region)] + return e.stateDecl(id), active +} + +// regionActiveState is region's active state, nil when it has none. +func (e *StateExecutor) regionActiveState(region *ast.StateRegion) *ast.StateNode { + return e.stateDecl(e.activeConfig.regionStates[e.regionID(region)]) +} + +// deactivateRegion drops region from the configuration. +func (e *StateExecutor) deactivateRegion(region *ast.StateRegion) { + e.activeConfig.deactivate(e.regionID(region)) +} + +// simpleState is the single active state, nil when regions are active or none is. +func (e *StateExecutor) simpleState() *ast.StateNode { + return e.stateDecl(e.activeConfig.simpleState) +} + +// setSimpleState makes state the single active state. +func (e *StateExecutor) setSimpleState(state *ast.StateNode) { + e.activeConfig.simpleState = e.stateID(state) } // StateExecutor executes state machines using event-driven semantics. @@ -60,25 +105,25 @@ type StateExecutor struct { stateCells *bodyCells // stateAttrs holds the attributes each state owns, one map per state node, so // two usages of one state definition keep separate values. - stateAttrs map[*ast.StateNode]map[string]Value - stateAttrCells map[*ast.StateNode]*bodyCells + stateAttrs map[program.StateID]map[string]Value + stateAttrCells map[program.StateID]*bodyCells // shadowed caches holdsShadowed, nil until first asked. shadowed *bool // attrIndex indexes the machine's attributes by feature, stateAttrIndex each state's. attrIndex map[*symbols.Symbol]string - stateAttrIndex map[*ast.StateNode]map[*symbols.Symbol]string - stateVisits []string // Ordered list of visited state names - stateStack []*ast.StateNode // Active state configuration (for nested states) + stateAttrIndex map[program.StateID]map[*symbols.Symbol]string + stateVisits []string // Ordered list of visited state names + stateStack []program.StateID // Active state configuration (for nested states) // history records, per composite state, the configuration that state had when // it was last exited. A history pseudostate re-enters that configuration // instead of the composite state's initial one. - history map[*ast.StateNode]*historyRecord + history map[program.StateID]*historyRecord // joinArrived holds the join segments that have fired and are waiting on // their remaining incoming segments. - joinArrived map[*ast.PseudostateNode][]*lower.Transition + joinArrived map[program.PseudoID][]program.TransID // joinChosen holds the incoming segments selected by the current dispatch. - joinChosen map[*ast.PseudostateNode]map[*lower.Transition]bool + joinChosen map[program.PseudoID]map[program.TransID]bool // pendingCall is the synchronous Call the machine is running, if any. pendingCall *pendingCall @@ -98,9 +143,9 @@ type StateExecutor struct { // breakpointNodes are the vertices a run pauses on entering or passing through; // breakpointHit the first state the dispatch under way entered, pausedAt the // vertex the run paused at. - breakpointNodes map[ast.Node]bool - breakpointHit *ast.StateNode - pausedAt ast.Node + breakpointNodes map[program.VertexRef]bool + breakpointHit program.StateID + pausedAt program.VertexRef // dispatchMark is where the dispatch under way began in fired, -1 between // dispatches; completionDue holds a completion reached under a breakpoint. dispatchMark int @@ -128,23 +173,23 @@ type StateExecutor struct { // timerScheduled holds the time-triggered transitions whose timer is already // running, so a state's timer is not restarted while it stays active. - timerScheduled map[*lower.Transition]bool + timerScheduled map[program.TransID]bool // timeTriggerVerdict caches, per time-triggered transition, whether its // argument's static type was accepted (nil) or the error refusing it. - timeTriggerVerdict map[*lower.Transition]error + timeTriggerVerdict map[program.TransID]error // changeFired holds the change-triggered transitions already taken on a // condition that has stayed true, so an unchanged one does not re-fire. - changeFired map[*lower.Transition]bool - changeObserved map[*lower.Transition]bool - changePending map[*lower.Transition]bool - changeReads map[*lower.Transition][]*FeatureValue + changeFired map[program.TransID]bool + changeObserved map[program.TransID]bool + changePending map[program.TransID]bool + changeReads map[program.TransID][]*FeatureValue changeEvaluating bool hasChangeTriggers bool // firingChange is the change-triggered transition being taken, whose latch the // state entries it causes must leave alone. - firingChange *lower.Transition + firingChange program.TransID // firingNotes is what selecting the transition being taken noted, recorded // once its guard's final reading lets it fire (see transitionDecided). @@ -160,17 +205,17 @@ type StateExecutor struct { // leftAhead are the states a compound transition under way left before its // choice was resolved; exitingAhead is set while it leaves them. Both live // within one firing, inside a step, so no snapshot sees them. - leftAhead map[*ast.StateNode]bool + leftAhead map[program.StateID]bool exitingAhead bool // enteredAhead are the states a move activated ahead of entering them, so a // segment's effect could follow the entry of the state enclosing it. - enteredAhead map[*ast.StateNode]bool + enteredAhead map[program.StateID]bool // moving marks a compound transition under way that a refused witness may undo. moving *moveMark // front is the site under way whose regions' units are drawn one at a time; // it lives within one move, so no snapshot sees it. front *unitFront - pendingRouteEntryEffects map[*ast.StateRegion][]routeEffect + pendingRouteEntryEffects map[program.RegionID][]routeEffect // began are the do behaviors the move under way started, whose due steps its entry sites // draw against the entries left; path is the draw along an entry path no front orders. began []*doAction @@ -180,19 +225,19 @@ type StateExecutor struct { // held contains entry cascades paused at RTC boundaries. held []heldEntry // entering marks states entered during the current entry unit. - entering map[*ast.StateNode]bool + entering map[program.StateID]bool // enteringMachine marks an entry cascade that includes the machine. enteringMachine bool // activeAtEntry records states active before the current entry unit. - activeAtEntry map[*ast.StateNode]bool + activeAtEntry map[program.StateID]bool // performing counts the entry and exit behaviors of each state under way, so // the state reads as active to them before the configuration holds it // (entry) and after the configuration dropped it (exit). - performing map[*ast.StateNode]int + performing map[program.StateID]int // changeRearmed collects, while a poll runs, the watches a state entry armed // for a new activation, so the poll's earlier observation does not latch them. - changeRearmed map[*lower.Transition]bool + changeRearmed map[program.TransID]bool // changeWaits are the change conditions the last poll found could not fire, // telling a machine waiting on one from a quiesced machine. @@ -203,7 +248,7 @@ type StateExecutor struct { // behavior runs while its state is active rather than at entry, and is abandoned // when the state is exited. type doAction struct { - state *ast.StateNode + state program.StateID pending []lower.StateBehavior // firing is the transition that entered the state, whose payload the behavior // reads for its whole run (`StatePerformance::incomingTransitionTrigger`). @@ -235,10 +280,10 @@ func (act *doAction) finished() bool { type historyRecord struct { // child is the substate that was active, whether it was declared directly or // in one of the state's orthogonal regions. - child *ast.StateNode + child program.StateID // regions is the active state of each orthogonal region, empty for a state // that has none. - regions map[*ast.StateRegion]*ast.StateNode + regions map[program.RegionID]program.StateID } // newStateExecutor creates a state executor. self is the object performing the @@ -329,26 +374,26 @@ func newStateExecutorOn( nextEventID: 1, eventQueue: newEventQueue(), stateData: make(map[string]Value), - stateAttrs: make(map[*ast.StateNode]map[string]Value), - stateAttrCells: make(map[*ast.StateNode]*bodyCells), - stateAttrIndex: make(map[*ast.StateNode]map[*symbols.Symbol]string), + stateAttrs: make(map[program.StateID]map[string]Value), + stateAttrCells: make(map[program.StateID]*bodyCells), + stateAttrIndex: make(map[program.StateID]map[*symbols.Symbol]string), stateVisits: make([]string, 0), - stateStack: make([]*ast.StateNode, 0), - history: make(map[*ast.StateNode]*historyRecord), - joinArrived: make(map[*ast.PseudostateNode][]*lower.Transition), - timerScheduled: make(map[*lower.Transition]bool), - timeTriggerVerdict: make(map[*lower.Transition]error), - changeFired: make(map[*lower.Transition]bool), - changeObserved: make(map[*lower.Transition]bool), - changePending: make(map[*lower.Transition]bool), - changeReads: make(map[*lower.Transition][]*FeatureValue), - breakpointNodes: make(map[ast.Node]bool), + stateStack: make([]program.StateID, 0), + history: make(map[program.StateID]*historyRecord), + joinArrived: make(map[program.PseudoID][]program.TransID), + timerScheduled: make(map[program.TransID]bool), + timeTriggerVerdict: make(map[program.TransID]error), + changeFired: make(map[program.TransID]bool), + changeObserved: make(map[program.TransID]bool), + changePending: make(map[program.TransID]bool), + changeReads: make(map[program.TransID][]*FeatureValue), + breakpointNodes: make(map[program.VertexRef]bool), dispatchMark: -1, activeConfig: &stateConfiguration{ - regionStates: make(map[*ast.StateRegion]*ast.StateNode), - defaultComplete: make(map[*ast.StateRegion]bool), + regionStates: make(map[program.RegionID]program.StateID), + defaultComplete: make(map[program.RegionID]bool), }, - entering: make(map[*ast.StateNode]bool), + entering: make(map[program.StateID]bool), } exec.driven.exec = exec for _, attr := range graph.Attributes { @@ -488,7 +533,7 @@ func (e *StateExecutor) heldKey(state *ast.StateNode, scope *symbols.Scope, name return name } for _, ancestor := range e.getParentChain(state) { - if key, held := e.stateAttrIndex[ancestor][sym]; held { + if key, held := e.stateAttrIndex[e.stateID(ancestor)][sym]; held { return key } } @@ -571,12 +616,12 @@ func (e *StateExecutor) initializeStateAttributes() error { continue } data := make(map[string]Value, len(attrs)) - e.stateAttrs[state] = data + e.stateAttrs[e.stateID(state)] = data var cells *bodyCells for _, attr := range attrs { - index := e.stateAttrIndex[state] + index := e.stateAttrIndex[e.stateID(state)] e.ctx.holdFeature(&index, attr.Symbol, attr.Name) - e.stateAttrIndex[state] = index + e.stateAttrIndex[e.stateID(state)] = index } for _, attr := range attrs { if attr.Binding && attr.Value != nil { @@ -584,7 +629,7 @@ func (e *StateExecutor) initializeStateAttributes() error { cells = newBodyCells(data, func(scope *symbols.Scope) *EvalContext { return e.stateAttributeContext(state, scope) }) - e.stateAttrCells[state] = cells + e.stateAttrCells[e.stateID(state)] = cells } scope := attr.Scope if scope == nil { @@ -633,8 +678,8 @@ func (e *StateExecutor) attrFramesFor(state *ast.StateNode) []frame { var frames []frame chain := e.getParentChain(state) for i := len(chain) - 1; i >= 0; i-- { - if data := e.stateAttrs[chain[i]]; data != nil { - frames = append(frames, frame{vars: data, cells: e.stateAttrCells[chain[i]], held: e.stateAttrIndex[chain[i]]}) + if data := e.stateAttrs[e.stateID(chain[i])]; data != nil { + frames = append(frames, frame{vars: data, cells: e.stateAttrCells[e.stateID(chain[i])], held: e.stateAttrIndex[e.stateID(chain[i])]}) } } return frames @@ -648,7 +693,7 @@ func (e *StateExecutor) stateAttributeValues(state *ast.StateNode, name string) return nil, nil, nil, false } for _, ancestor := range e.getParentChain(state) { - data, ok := e.stateAttrs[ancestor] + data, ok := e.stateAttrs[e.stateID(ancestor)] if !ok { continue } @@ -663,7 +708,7 @@ func (e *StateExecutor) stateAttributeValues(state *ast.StateNode, name string) if attr.Shadowed && attr.Symbol != nil && attr.Symbol.OwnerScope != nil { scope = attr.Symbol.OwnerScope } - return data, scope, e.stateAttrCells[ancestor], true + return data, scope, e.stateAttrCells[e.stateID(ancestor)], true } } return nil, nil, nil, false @@ -986,7 +1031,7 @@ func (e *StateExecutor) scheduleTimeTransitions(state *ast.StateNode) error { for _, group := range lower.TransitionGroups(state, transitions) { scheduled := false for _, index := range group { - scheduled = scheduled || e.timerScheduled[transitions[index]] + scheduled = scheduled || e.timerScheduled[e.transID(transitions[index])] } if scheduled { continue @@ -1028,7 +1073,7 @@ func (e *StateExecutor) scheduleTimeTransitions(state *ast.StateNode) error { }) e.nextEventID++ for _, index := range group { - e.timerScheduled[transitions[index]] = true + e.timerScheduled[e.transID(transitions[index])] = true } } @@ -1038,11 +1083,11 @@ func (e *StateExecutor) scheduleTimeTransitions(state *ast.StateNode) error { // checkTimeTriggerType refuses, before evaluating it, the trigger argument // validation refuses. The verdict is static, so it is judged once per transition. func (e *StateExecutor) checkTimeTriggerType(trans *lower.Transition, t *ast.TimeEvent) error { - if err, ok := e.timeTriggerVerdict[trans]; ok { + if err, ok := e.timeTriggerVerdict[e.transID(trans)]; ok { return err } err := e.ctx.judgeTimeTriggerType(trans.Scope, t) - e.timeTriggerVerdict[trans] = err + e.timeTriggerVerdict[e.transID(trans)] = err return err } @@ -1101,7 +1146,7 @@ func (e *StateExecutor) recordAccept(event Event, mark int, at float64) { // markDispatch opens a dispatch's account: where its fired transitions begin, // with no state hit left staged by a dispatch that failed before pausing. func (e *StateExecutor) markDispatch() { - e.breakpointHit, e.dispatchMark = nil, -1 + e.breakpointHit, e.dispatchMark = 0, -1 if !e.keepFired { e.releaseFired(len(e.fired)) } @@ -1111,19 +1156,16 @@ func (e *StateExecutor) markDispatch() { // stagedBreakpoint is the breakpoint vertex the dispatch under way passed or // entered, nil when none or no dispatch is under way. A pseudostate passed is // read from the transitions the dispatch fired, so a failed firing stages none. -func (e *StateExecutor) stagedBreakpoint() ast.Node { +func (e *StateExecutor) stagedBreakpoint() program.VertexRef { if e.dispatchMark < 0 { - return nil + return program.VertexRef{} } for _, fired := range e.fired[min(e.dispatchMark, len(e.fired)):] { - if _, ok := fired.Target.(*ast.PseudostateNode); ok && e.breakpointNodes[fired.Target] { + if fired.Target.Kind == program.VertexPseudostate && e.breakpointNodes[fired.Target] { return fired.Target } } - if e.breakpointHit != nil { - return e.breakpointHit - } - return nil + return program.StateVertex(e.breakpointHit) } // pauseAtBreakpoint closes the dispatch just done, suspending the machine at the @@ -1131,8 +1173,8 @@ func (e *StateExecutor) stagedBreakpoint() ast.Node { // the machine's own completion included — to the next run. func (e *StateExecutor) pauseAtBreakpoint() { hit := e.stagedBreakpoint() - e.breakpointHit, e.dispatchMark = nil, -1 - if hit == nil || e.state != StateRunning { + e.breakpointHit, e.dispatchMark = 0, -1 + if hit.IsZero() || e.state != StateRunning { return } e.pausedAt = hit @@ -1222,16 +1264,27 @@ func (e *StateExecutor) LastDispatch() (Dispatch, bool) { return *e.lastDispatch, true } -// FiredTransition is one transition taken: where it was written and the vertices -// it joined. An entry transition has no Source; it leaves the start of Owner's -// body, the one keying it in the graph's EntryTransitions (nil: the machine's own). +// FiredTransition is a transition the machine took, by graph-local identity: its id +// and origin and the vertices it left and reached; an entry transition by Entry instead. type FiredTransition struct { - Decl ast.Node - Source ast.Node - Target ast.Node - Owner ast.Node + Transition program.TransID + Source program.VertexRef + Target program.VertexRef + Entry EntryRef + Origin program.Origin } +// EntryRef names an entry transition by the body it is written in (a state, a region, +// or neither for the machine's own) and its 1-based position there; the zero EntryRef names none. +type EntryRef struct { + Owner program.StateID + Region program.RegionID + Index int +} + +// IsZero reports whether the reference names no entry transition. +func (r EntryRef) IsZero() bool { return r.Index == 0 } + // KeepFired has the machine keep its firings until FiredSince releases them, as // a debugger reads them; off (the default), a dispatch keeps only its own. func (e *StateExecutor) KeepFired(keep bool) { @@ -1283,11 +1336,61 @@ func (e *StateExecutor) releaseFired(n int) { func (e *StateExecutor) noteFired(transitions ...*lower.Transition) { for _, trans := range transitions { if trans != nil && trans.Decl != nil { - e.fired = append(e.fired, FiredTransition{Decl: trans.Decl, Source: trans.Source, Target: trans.Target}) + e.fired = append(e.fired, e.firedTransition(trans)) } } } +// firedTransition is trans as the firing log records it. +func (e *StateExecutor) firedTransition(trans *lower.Transition) FiredTransition { + id := e.transID(trans) + return FiredTransition{ + Transition: id, + Source: e.vertexRef(trans.Source), + Target: e.vertexRef(trans.Target), + Origin: e.graph.TransOrigins[id], + } +} + +// firedEntry is the entry transition at position index of owner's body as the firing +// log records it; lowering gives it no id or origin yet, so the origin is the declaration's span. +func (e *StateExecutor) firedEntry(owner ast.Node, index int, entry *lower.EntryTransition) FiredTransition { + ref := EntryRef{Index: index} + switch owner := e.graph.EntryOwner(owner).(type) { + case *ast.StateNode: + ref.Owner = e.stateID(owner) + case *ast.StateRegion: + ref.Region = e.regionID(owner) + } + return FiredTransition{ + Target: e.vertexRef(entryTarget(entry)), + Entry: ref, + Origin: program.Origin{Doc: e.graph.DocOf(entry.Decl), Span: entry.Decl.Span(), Kind: "entry transition"}, + } +} + +// FiredNodes resolves a fired transition to the declarations the graph keeps for it: +// its own, the vertices it left and reached, and an entry transition's body (nil for the machine's). +func (e *StateExecutor) FiredNodes(f FiredTransition) (decl, source, target, owner ast.Node) { + if f.Entry.IsZero() { + if trans := e.transDecl(f.Transition); trans != nil { + decl = trans.Decl + } + return decl, e.vertexDecl(f.Source), e.vertexDecl(f.Target), nil + } + switch { + case f.Entry.Region != 0: + owner = e.regionDecl(f.Entry.Region) + case f.Entry.Owner != 0: + owner = e.stateDecl(f.Entry.Owner) + } + entries := e.graph.EntryTransitions[owner] + if i := f.Entry.Index - 1; i >= 0 && i < len(entries) { + decl = entries[i].Decl + } + return decl, nil, e.vertexDecl(f.Target), owner +} + // unfireOnError drops the transitions logged since mark when *err is set: a // firing that failed midway took none of them. func (e *StateExecutor) unfireOnError(mark int, err *error) { @@ -1306,7 +1409,7 @@ func (e *StateExecutor) dispatchEvent(event Event) (Dispatch, error) { if lowerTrans, ok := event.Payload.(*lower.Transition); ok { // The timer has expired, so it is no longer running: a transition that // does not leave its source state re-arms it for the next round. - delete(e.timerScheduled, lowerTrans) + delete(e.timerScheduled, e.transID(lowerTrans)) sourceState, _ := lowerTrans.Source.(*ast.StateNode) if sourceState != nil && !e.inActiveConfiguration(sourceState) { // The source was left before this event came up, so the transition @@ -1326,7 +1429,7 @@ func (e *StateExecutor) dispatchEvent(event Event) (Dispatch, error) { // signal dispatch would. for _, groupMember := range e.graph.Transitions[sourceState] { if sameTimerGroup(groupMember, lowerTrans) { - delete(e.timerScheduled, groupMember) + delete(e.timerScheduled, e.transID(groupMember)) } } enabled, probeNotes, err := e.enabledTransitions(sourceState, &event) @@ -1335,7 +1438,7 @@ func (e *StateExecutor) dispatchEvent(event Event) (Dispatch, error) { } notes = probeNotes chosen, choiceNotes, err := e.chooseTransition(dispatchCandidate{ - leaf: sourceState, source: sourceState, + leaf: e.stateID(sourceState), source: e.stateID(sourceState), enabled: enabled, notes: notes, }, &event) if err != nil || chosen == nil { @@ -1383,7 +1486,7 @@ func (e *StateExecutor) dispatchEvent(event Event) (Dispatch, error) { Effect: lower.LowerBehaviors(edge.Effect, nil, e.stateMachine.Scope, e.ctx.Resolver()), } var err error - dispatch.Fired, err = e.fireTransition(lowerTrans, route{segments: []*lower.Transition{lowerTrans}, target: targetState}) + dispatch.Fired, err = e.fireTransition(lowerTrans, route{segments: []*lower.Transition{lowerTrans}, target: e.stateID(targetState)}) return dispatch, err } @@ -1446,13 +1549,13 @@ func (e *StateExecutor) broadcastEvent(event *Event) (bool, []string, error) { unbind, err := e.bindTriggerArguments(trans, event) if err != nil { unbind() - return false, fmt.Errorf("state %s: %w", candidate.source.Name, err) + return false, fmt.Errorf("state %s: %w", e.stateDecl(candidate.source).Name, err) } fired, err := e.firingOn(event, func() (bool, error) { - return e.fireFrom(candidate.source, trans, notes, candidate.route) + return e.fireFrom(e.stateDecl(candidate.source), trans, notes, candidate.route) }) if err != nil { - return false, fmt.Errorf("fire transition out of %s: %w", candidate.source.Name, err) + return false, fmt.Errorf("fire transition out of %s: %w", e.stateDecl(candidate.source).Name, err) } return fired, nil }) @@ -1479,13 +1582,14 @@ func (e *StateExecutor) dispatchInOrder( armed func(dispatchCandidate) (bool, error), fire func(dispatchCandidate, *lower.Transition, []RunNote) (bool, error), ) (bool, error) { - chosen := make(map[*ast.PseudostateNode]map[*lower.Transition]bool) + chosen := make(map[program.PseudoID]map[program.TransID]bool) for _, candidate := range candidates { if join, ok := candidate.chosen.Target.(*ast.PseudostateNode); ok && join.Kind == ast.PseudostateJoin { - if chosen[join] == nil { - chosen[join] = make(map[*lower.Transition]bool) + id := e.pseudoID(join) + if chosen[id] == nil { + chosen[id] = make(map[program.TransID]bool) } - chosen[join][candidate.chosen] = true + chosen[id][e.transID(candidate.chosen)] = true } } saved := e.joinChosen @@ -1494,7 +1598,9 @@ func (e *StateExecutor) dispatchInOrder( acted := false var previewErr error - gone := func(candidate dispatchCandidate) bool { return !e.isActive(candidate.leaf) || e.state.Ended() } + gone := func(candidate dispatchCandidate) bool { + return !e.isActive(e.stateDecl(candidate.leaf)) || e.state.Ended() + } // A guard that cannot be read is left to the firing, which reports the error. // An order-dependent verdict instead fails the dispatch as not covered. void := func(candidate dispatchCandidate) bool { @@ -1534,8 +1640,8 @@ func (e *StateExecutor) dispatchInOrder( f := e.openFront(ChoiceRegionOrder, where) for _, candidate := range firings { head := unitHead{ - label: e.exitLabel(candidate.leaf), at: candidate.leaf, void: func() bool { return void(candidate) }, - silent: e.exitIsUnit(candidate.leaf) && e.silentExit(candidate.leaf), + label: e.exitLabel(e.stateDecl(candidate.leaf)), at: e.stateDecl(candidate.leaf), void: func() bool { return void(candidate) }, + silent: e.exitIsUnit(e.stateDecl(candidate.leaf)) && e.silentExit(e.stateDecl(candidate.leaf)), } if join, ok := candidate.chosen.Target.(*ast.PseudostateNode); ok && join.Kind == ast.PseudostateJoin { head.label, head.at, head.silent = join.Name+"(join)", join, false @@ -1574,8 +1680,8 @@ func joinFirings(candidates []dispatchCandidate) []dispatchCandidate { // (chooseTransitions); what selecting and choosing found worth noting is recorded // only if it fires. type dispatchCandidate struct { - leaf *ast.StateNode - source *ast.StateNode + leaf program.StateID + source program.StateID enabled []int notes []RunNote chosen *lower.Transition @@ -1620,7 +1726,7 @@ func (e *StateExecutor) selectCandidates( if len(positions) == 0 { continue } - candidates = append(candidates, dispatchCandidate{leaf: leaf, source: source, enabled: positions, notes: notes}) + candidates = append(candidates, dispatchCandidate{leaf: e.stateID(leaf), source: e.stateID(source), enabled: positions, notes: notes}) break } } @@ -1647,7 +1753,7 @@ func (e *StateExecutor) chooseTransitions(candidates []dispatchCandidate, event var route route route, err = e.resolveRouteFor(candidate.chosen, event) if err != nil { - return nil, fmt.Errorf("transition out of %s: %w", candidate.source.Name, err) + return nil, fmt.Errorf("transition out of %s: %w", e.stateDecl(candidate.source).Name, err) } candidate.route = route candidate.event = event @@ -1676,22 +1782,24 @@ func (e *StateExecutor) resolveRouteFor(trans *lower.Transition, event *Event) ( // arguments bound for them as they are for a guard. event is nil for a // completion, a change poll or a route's branch. func (e *StateExecutor) chooseTransition(candidate dispatchCandidate, event *Event) (*lower.Transition, []RunNote, error) { - transitions := e.graph.Transitions[candidate.source] + transitions := e.graph.Transitions[e.stateDecl(candidate.source)] notes := candidate.notes pick := 0 // Weights are validated for a lone enabled transition too, even though it // records no choice point and fires with probability 1. - weights, err := e.transitionWeights(candidate.source, transitions, candidate.enabled, event) + weights, err := e.transitionWeights(e.stateDecl(candidate.source), transitions, candidate.enabled, event) if err != nil { return nil, nil, err } - if choice, ok := e.transitionChoice(candidate.source, transitions, candidate.enabled); ok { - whereOf := func(i int) string { return transitionWhere(candidate.source, transitions[candidate.enabled[i]]) } + if choice, ok := e.transitionChoice(e.stateDecl(candidate.source), transitions, candidate.enabled); ok { + whereOf := func(i int) string { + return transitionWhere(e.stateDecl(candidate.source), transitions[candidate.enabled[i]]) + } pick, err = e.drawTransition(&choice, whereOf, weights) if err != nil { return nil, nil, err } - choice.File, choice.Span = e.transitionLocation(candidate.source, transitions[candidate.enabled[pick]]) + choice.File, choice.Span = e.transitionLocation(e.stateDecl(candidate.source), transitions[candidate.enabled[pick]]) notes = append([]RunNote{choice}, notes...) } return transitions[candidate.enabled[pick]], notes, nil @@ -1757,7 +1865,7 @@ func (e *StateExecutor) stateNames(states []*ast.StateNode) []string { // same state are in conflict, and the innermost one wins. func (e *StateExecutor) losesToNestedTransition(candidates []dispatchCandidate, candidate dispatchCandidate) bool { for _, other := range candidates { - if other.source != candidate.source && e.nestedIn(other.source, candidate.source) { + if other.source != candidate.source && e.nestedIn(e.stateDecl(other.source), e.stateDecl(candidate.source)) { return true } } @@ -2066,7 +2174,7 @@ func (e *StateExecutor) activeRegionOf(state *ast.StateNode) *ast.StateRegion { if !inRegion { continue } - if _, active := e.activeConfig.regionStates[region]; active { + if e.regionIsActive(region) { return region } } @@ -2077,7 +2185,7 @@ func (e *StateExecutor) activeRegionOf(state *ast.StateNode) *ast.StateRegion { // an active state below it or one waiting at a join. func (e *StateExecutor) enclosesActiveRegion(state *ast.StateNode) bool { for _, region := range e.graph.CompositeStates[state] { - active, ok := e.activeConfig.regionStates[region] + active, ok := e.regionActive(region) if ok && active != state { return true } @@ -2473,7 +2581,7 @@ func (e *StateExecutor) matchesEvent(trans *lower.Transition, event *Event) (boo // A change occurrence is the poll that observed the rise: it takes the // change-triggered transitions whose condition rose in it, not yet latched. poll, ok := event.Payload.(*changePoll) - return ok && e.triggerMatches(trans.Trigger, trans.Scope, event) && poll.condition[trans] && !e.changeFired[trans], nil + return ok && e.triggerMatches(trans.Trigger, trans.Scope, event) && poll.condition[trans] && !e.changeFired[e.transID(trans)], nil case eventAccept, eventCall: if !e.triggerMatches(trans.Trigger, trans.Scope, event) { @@ -2653,10 +2761,10 @@ func (e *StateExecutor) moveTo(trans *lower.Transition, currentState *ast.StateN previousEffects := e.pendingRouteEntryEffects e.pendingRouteEntryEffects = cloneRouteEntryEffects(previousEffects) if e.pendingRouteEntryEffects == nil && len(entryEffects) > 0 { - e.pendingRouteEntryEffects = make(map[*ast.StateRegion][]routeEffect) + e.pendingRouteEntryEffects = make(map[program.RegionID][]routeEffect) } for region, routeEffects := range entryEffects { - e.pendingRouteEntryEffects[region] = append(e.pendingRouteEntryEffects[region], routeEffects...) + e.pendingRouteEntryEffects[e.regionID(region)] = append(e.pendingRouteEntryEffects[e.regionID(region)], routeEffects...) } defer func() { e.pendingRouteEntryEffects = previousEffects }() @@ -2741,7 +2849,7 @@ func (e *StateExecutor) completeIfDone(target *ast.StateNode) error { } // A dispatch pausing at a breakpoint shows the completion vertex it reached; // the machine completes once resumed. - if e.stagedBreakpoint() != nil { + if !e.stagedBreakpoint().IsZero() { e.completionDue = true return nil } @@ -2793,7 +2901,7 @@ func (e *StateExecutor) terminateMachine(fromName string, trans *lower.Transitio func (e *StateExecutor) abandonMachine() []string { var abandoned []string for _, act := range e.doActions { - abandoned = append(abandoned, act.state.Name) + abandoned = append(abandoned, e.stateDecl(act.state).Name) if act.run != nil { e.endDoRun(act.run) act.run = nil @@ -2802,9 +2910,7 @@ func (e *StateExecutor) abandonMachine() []string { clear(e.doActions) e.doActions = e.doActions[:0] e.clearEntryState() - e.activeConfig.simpleState = nil - e.activeConfig.regionStates = make(map[*ast.StateRegion]*ast.StateNode) - e.activeConfig.defaultComplete = make(map[*ast.StateRegion]bool) + e.activeConfig.reset() e.stateStack = nil e.completionDue = false // Nothing dispatches on an ended machine: what it queued is discarded. @@ -2875,7 +2981,7 @@ func (e *StateExecutor) machineComplete() bool { // completeInto finishes a transition into a still active ancestor: the target is // not re-entered and the region the source left completes (PSSM 8.5.8). func (e *StateExecutor) completeInto(trans *lower.Transition, fromName string, target *ast.StateNode) error { - e.stateStack = e.rootToLeaf(target) + e.stateStack = e.stateIDs(e.rootToLeaf(target)) if _, orthogonal := e.graph.CompositeStates[target]; orthogonal { if e.stateComplete(target) { if err := e.scheduleCompletionTransitions(target); err != nil { @@ -2884,15 +2990,15 @@ func (e *StateExecutor) completeInto(trans *lower.Transition, fromName string, t } } else { // The body completed: its history keeps no substate to restore. - if record := e.history[target]; record != nil { - record.child = nil + if record := e.history[e.stateID(target)]; record != nil { + record.child = 0 } onPath := e.branchesTo(nil, target) for region, state := range onPath { e.setRegionState(region, state) } if len(onPath) == 0 && len(e.activeConfig.regionStates) == 0 { - e.activeConfig.simpleState = target + e.setSimpleState(target) } if err := e.scheduleCompletionTransitions(target); err != nil { return fmt.Errorf("schedule completion of state %s: %w", target.Name, err) @@ -2907,11 +3013,11 @@ func (e *StateExecutor) completeInto(trans *lower.Transition, fromName string, t // regionComplete reports whether region reached its completion vertex, was left empty by default entry, or was left at its owner. func (e *StateExecutor) regionComplete(region *ast.StateRegion) bool { - if e.activeConfig.defaultComplete[region] { + if e.activeConfig.defaultComplete[e.regionID(region)] { standIn := e.graph.RegionState[region] return standIn == nil || !e.hasRunningDoAction(standIn) } - active, ok := e.activeConfig.regionStates[region] + active, ok := e.regionActive(region) if !ok { return false } @@ -2927,7 +3033,7 @@ func (e *StateExecutor) allRegionsDefaultComplete(state *ast.StateNode) bool { return false } for _, region := range regions { - if !e.activeConfig.defaultComplete[region] || !e.regionComplete(region) { + if !e.activeConfig.defaultComplete[e.regionID(region)] || !e.regionComplete(region) { return false } } @@ -2962,7 +3068,7 @@ func (e *StateExecutor) stateComplete(state *ast.StateNode) bool { // bodyAhead reports whether the move entering state has yet to enter its body. func (e *StateExecutor) bodyAhead(state *ast.StateNode) bool { - return e.entering[state] && e.hasBody(state) && !e.bodyRunning(state) + return e.entering[e.stateID(state)] && e.hasBody(state) && !e.bodyRunning(state) } // bodyRunning reports whether a state nested in state is active. @@ -3040,10 +3146,11 @@ func transitionDescription(trans *lower.Transition) string { // recordHistory returns state's history record, creating it on first use. func (e *StateExecutor) recordHistory(state *ast.StateNode) *historyRecord { - record, ok := e.history[state] + id := e.stateID(state) + record, ok := e.history[id] if !ok { record = &historyRecord{} - e.history[state] = record + e.history[id] = record } return record } @@ -3051,20 +3158,20 @@ func (e *StateExecutor) recordHistory(state *ast.StateNode) *historyRecord { // historyRecorded reports whether state was left in a configuration its history // restores; a record emptied by completion holds none. func (e *StateExecutor) historyRecorded(state *ast.StateNode) bool { - record := e.history[state] - return record != nil && (record.child != nil || len(record.regions) > 0) + record := e.history[e.stateID(state)] + return record != nil && (record.child != 0 || len(record.regions) > 0) } // recordChildHistory remembers the substate parent was left in for its history; // a body left at `done` completed, and a completed configuration leaves none. func (e *StateExecutor) recordChildHistory(parent, state *ast.StateNode) { if e.graph.Completes(state) { - if record := e.history[parent]; record != nil { - record.child = nil + if record := e.history[e.stateID(parent)]; record != nil { + record.child = 0 } return } - e.recordHistory(parent).child = state + e.recordHistory(parent).child = e.stateID(state) } // recordRegionHistory remembers the state a region was left in for the owning @@ -3080,9 +3187,9 @@ func (e *StateExecutor) recordRegionHistory(region *ast.StateRegion, state *ast. } record := e.recordHistory(owner) if record.regions == nil { - record.regions = make(map[*ast.StateRegion]*ast.StateNode) + record.regions = make(map[program.RegionID]program.StateID) } - record.regions[region] = state + record.regions[e.regionID(region)] = e.stateID(state) } // forgetRegionHistory drops a region's recorded state, for a region left with no @@ -3092,8 +3199,8 @@ func (e *StateExecutor) forgetRegionHistory(region *ast.StateRegion) { if owner == nil { return } - if record := e.history[owner]; record != nil { - delete(record.regions, region) + if record := e.history[e.stateID(owner)]; record != nil { + delete(record.regions, e.regionID(region)) } } @@ -3190,13 +3297,13 @@ func (e *StateExecutor) moveToHistory(trans *lower.Transition, currentState *ast return err } e.noteFired(r.segments...) - if r.terminate != nil { - return e.terminateAt(trans, fromName, r, r.effects(e.graph), e.descendantChain(below, e.graph.TerminateOwner[r.terminate])) + if stop := e.terminateDecl(r.terminate); stop != nil { + return e.terminateAt(trans, fromName, r, r.effects(e), e.descendantChain(below, e.graph.TerminateOwner[stop])) } - if err := e.runEffects(r.effects(e.graph), e.descendantChain(below, r.target)); err != nil { + if err := e.runEffects(r.effects(e), e.descendantChain(below, e.stateDecl(r.target))); err != nil { return err } - return e.enterBelow(trans, fromName, below, r.target, nil) + return e.enterBelow(trans, fromName, below, e.stateDecl(r.target), nil) } // defaultHistoryRoute takes a history's default transition from inside its @@ -3215,7 +3322,7 @@ func (e *StateExecutor) defaultHistoryRoute(hist *ast.PseudostateNode, owner *as if err != nil { return route{}, fmt.Errorf("default transition of history %s: %w", hist.Name, err) } - for r.choice != nil { + for r.choice != 0 { targets, stops, err := e.reachable(r) if err != nil { return route{}, err @@ -3228,10 +3335,10 @@ func (e *StateExecutor) defaultHistoryRoute(hist *ast.PseudostateNode, owner *as ends.entries = append(ends.entries, e.descendantChain(owner, e.graph.TerminateOwner[stop])) } certain := ends.certainEntries() - if err := e.runEffects(r.effects(e.graph), certain); err != nil { + if err := e.runEffects(r.effects(e), certain); err != nil { return route{}, err } - if err := e.enterOwnerOf(r.choice, certain); err != nil { + if err := e.enterOwnerOf(e.pseudoDecl(r.choice), certain); err != nil { return route{}, err } e.noteFired(r.segments...) @@ -3256,7 +3363,7 @@ func (e *StateExecutor) historyEntry(hist *ast.PseudostateNode, owner *ast.State return owner, nil, nil } - record := e.history[owner] + record := e.history[e.stateID(owner)] deep := hist.Kind == ast.PseudostateDeepHistory branches := make(map[*ast.StateRegion]*ast.StateNode) @@ -3264,10 +3371,11 @@ func (e *StateExecutor) historyEntry(hist *ast.PseudostateNode, owner *ast.State // The owner keeps its configuration in its regions, so it is re-entered // with one branch per region rather than moved to a single state. for _, region := range e.graph.CompositeStates[owner] { - active, recorded := record.regions[region] - if !recorded { + recorded, has := record.regions[e.regionID(region)] + if !has { continue } + active := e.stateDecl(recorded) if deep { active = e.deepestRecorded(active, branches) } @@ -3276,7 +3384,7 @@ func (e *StateExecutor) historyEntry(hist *ast.PseudostateNode, owner *ast.State return owner, branches, nil } - target := record.child + target := e.stateDecl(record.child) if deep { target = e.deepestRecorded(target, branches) } @@ -3306,22 +3414,22 @@ func (e *StateExecutor) hasDefaultEntry(state *ast.StateNode) bool { // carry the rest. func (e *StateExecutor) deepestRecorded(state *ast.StateNode, branches map[*ast.StateRegion]*ast.StateNode) *ast.StateNode { for { - record := e.history[state] + record := e.history[e.stateID(state)] if record == nil { return state } if len(record.regions) > 0 { for _, region := range e.graph.CompositeStates[state] { - if active, recorded := record.regions[region]; recorded { - branches[region] = e.deepestRecorded(active, branches) + if active, recorded := record.regions[e.regionID(region)]; recorded { + branches[region] = e.deepestRecorded(e.stateDecl(active), branches) } } return state } - if record.child == nil { + if record.child == 0 { return state } - state = record.child + state = e.stateDecl(record.child) } } @@ -3351,7 +3459,7 @@ func (e *StateExecutor) fireForkTransition(trans *lower.Transition, fork *ast.Ps // enter the rest of the way down to the owner and their targets, bypassing // the initial states of the regions they enter (PSSM §8.5.7). above := e.forkEntry(boundary, owner) - if i := slices.Index(above.chain, e.graph.PseudostateOwner[fork]); i >= 0 { + if i := slices.Index(above.chain, e.stateID(e.graph.PseudostateOwner[fork])); i >= 0 { if err := e.enterLazily(above, i+1); err != nil { return err } @@ -3361,7 +3469,7 @@ func (e *StateExecutor) fireForkTransition(trans *lower.Transition, fork *ast.Ps } // A region the move re-entered on its way down keeps the state of that path. for region, state := range e.branchesTo(boundary, owner) { - if _, active := e.activeConfig.regionStates[region]; !active { + if !e.regionIsActive(region) { e.setRegionState(region, state) } } @@ -3417,7 +3525,7 @@ func (e *StateExecutor) leaveForFork(trans *lower.Transition, owner *ast.StateNo func (e *StateExecutor) exitRegionsOf(owner *ast.StateNode) error { var units []unit for _, region := range e.graph.CompositeStates[owner] { - if _, active := e.activeConfig.regionStates[region]; active { + if e.regionIsActive(region) { units = append(units, unit{ run: func() error { return e.exitRegionTo(region, owner) }, footprint: e.exitFootprint(e.regionExitPath(region, owner)), @@ -3463,7 +3571,7 @@ func (e *StateExecutor) fireJoinTransition(trans *lower.Transition, join *ast.Ps if err != nil { return false, err } - if (r.arrival == join) == completes { + if (r.arrival == e.pseudoID(join)) == completes { r, err = e.resolveRouteFor(trans, event) if errors.Is(err, errNoWayThrough) { return false, nil @@ -3481,13 +3589,13 @@ func (e *StateExecutor) fireJoinTransition(trans *lower.Transition, join *ast.Ps return false, err } - if r.arrival == join { + if r.arrival == e.pseudoID(join) { return true, e.moveWhole(func() error { r = e.transitionDecided(r) if err := e.fireJoinIncoming(join, plan, firingNow); err != nil { return err } - e.joinArrived[join] = append(e.joinArrived[join], firingNow...) + e.joinArrived[e.pseudoID(join)] = append(e.joinArrived[e.pseudoID(join)], e.transIDs(firingNow)...) return nil }) } @@ -3501,7 +3609,7 @@ func (e *StateExecutor) fireJoinTransition(trans *lower.Transition, join *ast.Ps if err := e.fireJoinIncoming(join, plan, firingNow); err != nil { return err } - delete(e.joinArrived, join) + delete(e.joinArrived, e.pseudoID(join)) return e.leaveJoinOwner(plan.Owner, trans, r) }) } @@ -3526,7 +3634,7 @@ func (e *StateExecutor) leaveJoinOwner(owner *ast.StateNode, trans *lower.Transi return err } } - e.activeConfig.simpleState = nil + e.activeConfig.simpleState = 0 return e.transitionTo(trans, r) } if region := e.activeRegionOf(owner); region != nil { @@ -3537,7 +3645,7 @@ func (e *StateExecutor) leaveJoinOwner(owner *ast.StateNode, trans *lower.Transi return e.moveInRegion(region, owner, trans, effects, target) }) } - e.activeConfig.simpleState = owner + e.setSimpleState(owner) return e.transitionTo(trans, r) } @@ -3620,10 +3728,9 @@ func (e *StateExecutor) fireJoinSegment(trans *lower.Transition, plan *lower.Joi // The region is left whole: its configuration is what a history of the owner // restores, and the entry goes before the exits or the owner's exit walks it again. if region := plan.Regions[trans]; region != nil { - if active, isActive := e.activeConfig.regionStates[region]; isActive { + if active, isActive := e.regionActive(region); isActive { e.recordRegionHistory(region, active) - delete(e.activeConfig.regionStates, region) - delete(e.activeConfig.defaultComplete, region) + e.deactivateRegion(region) } } if err := e.exitStates(e.exitPath(leaving, plan.Owner, nil)); err != nil { @@ -3641,7 +3748,7 @@ func (e *StateExecutor) fireJoinSegment(trans *lower.Transition, plan *lower.Joi // active state when that lies below the segment's composite source, else the source. func (e *StateExecutor) joinSegmentLeaves(segment *lower.Transition, plan *lower.JoinPlan) *ast.StateNode { source := segment.Source.(*ast.StateNode) - if active, ok := e.activeConfig.regionStates[plan.Regions[segment]]; ok && e.isBelowOrEqual(active, source) { + if active, ok := e.regionActive(plan.Regions[segment]); ok && e.isBelowOrEqual(active, source) { return active } return source @@ -3664,16 +3771,17 @@ func (e *StateExecutor) joinIncoming(join *ast.PseudostateNode) []*lower.Transit // setRegionState clears arrivals from a region before its active state changes. func (e *StateExecutor) setRegionState(region *ast.StateRegion, state *ast.StateNode) { e.clearJoinArrivalsForRegion(region) - delete(e.activeConfig.defaultComplete, region) - e.activeConfig.regionStates[region] = state + id := e.regionID(region) + delete(e.activeConfig.defaultComplete, id) + e.activeConfig.regionStates[id] = e.stateID(state) } // regionWaitingAtJoin reports whether an arrived segment leaves this region parked. func (e *StateExecutor) regionWaitingAtJoin(region *ast.StateRegion) bool { for join, arrived := range e.joinArrived { - plan := e.graph.JoinPlans[join] + plan := e.graph.JoinPlans[e.pseudoDecl(join)] for _, segment := range arrived { - if plan != nil && plan.Regions[segment] == region { + if plan != nil && plan.Regions[e.transDecl(segment)] == region { return true } } @@ -3687,10 +3795,10 @@ func (e *StateExecutor) clearJoinArrivalsForRegion(region *ast.StateRegion) { return } for join, arrived := range e.joinArrived { - plan := e.graph.JoinPlans[join] + plan := e.graph.JoinPlans[e.pseudoDecl(join)] remaining := arrived[:0] for _, segment := range arrived { - if plan == nil || plan.Regions[segment] != region { + if plan == nil || plan.Regions[e.transDecl(segment)] != region { remaining = append(remaining, segment) } } @@ -3705,7 +3813,7 @@ func (e *StateExecutor) clearJoinArrivalsForRegion(region *ast.StateRegion) { // clearJoinArrivalsOwnedBy drops arrivals when their join owner exits. func (e *StateExecutor) clearJoinArrivalsOwnedBy(owner *ast.StateNode) { for join := range e.joinArrived { - if plan := e.graph.JoinPlans[join]; plan != nil && plan.Owner == owner { + if plan := e.graph.JoinPlans[e.pseudoDecl(join)]; plan != nil && plan.Owner == owner { delete(e.joinArrived, join) } } @@ -3717,8 +3825,8 @@ func (e *StateExecutor) clearJoinArrivals() { } // cloneJoinArrivals returns a non-nil copy of each join's ordered arrivals. -func cloneJoinArrivals(arrived map[*ast.PseudostateNode][]*lower.Transition) map[*ast.PseudostateNode][]*lower.Transition { - cloned := make(map[*ast.PseudostateNode][]*lower.Transition, len(arrived)) +func cloneJoinArrivals(arrived map[program.PseudoID][]program.TransID) map[program.PseudoID][]program.TransID { + cloned := make(map[program.PseudoID][]program.TransID, len(arrived)) for join, segments := range arrived { cloned[join] = slices.Clone(segments) } @@ -3735,8 +3843,8 @@ func (e *StateExecutor) joinFiringNow(trans *lower.Transition, event *Event) ([] if _, err := e.joinSources(join); err != nil { return nil, false, err } - arrived := make(map[*lower.Transition]bool, len(e.joinArrived[join])) - for _, segment := range e.joinArrived[join] { + arrived := make(map[program.TransID]bool, len(e.joinArrived[e.pseudoID(join)])) + for _, segment := range e.joinArrived[e.pseudoID(join)] { arrived[segment] = true } firing := map[*lower.Transition]bool{trans: true} @@ -3746,10 +3854,10 @@ func (e *StateExecutor) joinFiringNow(trans *lower.Transition, event *Event) ([] firingNow = append(firingNow, segment) continue } - if arrived[segment] { + if arrived[e.transID(segment)] { continue } - if !e.joinChosen[join][segment] { + if !e.joinChosen[e.pseudoID(join)][e.transID(segment)] { continue } source := segment.Source.(*ast.StateNode) @@ -3773,7 +3881,7 @@ func (e *StateExecutor) joinFiringNow(trans *lower.Transition, event *Event) ([] } } for _, segment := range e.joinIncoming(join) { - if !arrived[segment] && !firing[segment] { + if !arrived[e.transID(segment)] && !firing[segment] { return firingNow, false, nil } } @@ -3788,7 +3896,7 @@ func (e *StateExecutor) segmentTakes(segment *lower.Transition, event *Event) (b } if event.Type == eventChange { poll, ok := event.Payload.(*changePoll) - return ok && poll.condition[segment] && !e.changeFired[segment], nil + return ok && poll.condition[segment] && !e.changeFired[e.transID(segment)], nil } if _, isTime := segment.Trigger.(*ast.TimeEvent); isTime { return false, nil @@ -3835,11 +3943,12 @@ func (e *StateExecutor) joinSources(join *ast.PseudostateNode) ([]*ast.StateNode // isActive reports whether state is part of the active configuration. func (e *StateExecutor) isActive(state *ast.StateNode) bool { - if e.activeConfig.simpleState == state { + id := e.stateID(state) + if e.activeConfig.simpleState == id { return true } for _, active := range e.activeConfig.regionStates { - if active == state { + if active == id { return true } } @@ -3854,7 +3963,7 @@ func (e *StateExecutor) activeCompositeOwner() *ast.StateNode { for _, state := range e.graph.CompositeStateOrder { regions := e.graph.CompositeStates[state] for _, region := range regions { - if _, active := e.activeConfig.regionStates[region]; active { + if e.regionIsActive(region) { if d := len(e.getParentChain(state)); d > depth { deepest, depth = state, d } @@ -3871,7 +3980,7 @@ func (e *StateExecutor) orderedRegionStates() []*ast.StateNode { regions := e.orderedActiveRegions() states := make([]*ast.StateNode, 0, len(regions)) for _, region := range regions { - states = append(states, e.activeConfig.regionStates[region]) + states = append(states, e.regionActiveState(region)) } return states } @@ -3934,7 +4043,7 @@ func (e *StateExecutor) runCounting(atCurrentTime, single bool, progress *duePro // Suspension is derived at quiescence, so re-running is allowed: a run that // finds nothing to do suspends again. if e.state == StateSuspended { - e.state, e.pausedAt = StateRunning, nil + e.state, e.pausedAt = StateRunning, program.VertexRef{} } for e.state == StateRunning { @@ -4054,7 +4163,7 @@ func (e *StateExecutor) watchesChange() bool { // drivable is a machine the clock may run: initialized, not completed, and not // paused at a breakpoint, which holds its work until its driver resumes it. func (e *StateExecutor) drivable() bool { - return (e.state == StateRunning || e.state == StateSuspended) && e.pausedAt == nil + return (e.state == StateRunning || e.state == StateSuspended) && e.pausedAt.IsZero() } // runDue runs the machine to quiescence at the current instant. @@ -4085,7 +4194,7 @@ func (e *StateExecutor) runOne(progress *dueProgress) (moved bool, err error) { e.inRun = true defer func() { e.inRun = wasRunning }() if e.state == StateSuspended { - e.state, e.pausedAt = StateRunning, nil + e.state, e.pausedAt = StateRunning, program.VertexRef{} } if e.state != StateRunning { return false, nil @@ -4327,7 +4436,7 @@ func (e *StateExecutor) chooseStepOrder(due []*doAction, dispatch string) (bool, return false, err } if choice.Taken == 0 && len(due) == 1 { - choice.Span = due[0].state.Span() + choice.Span = e.stateDecl(due[0].state).Span() } e.noteChoice(choice) return choice.Taken == 1, nil @@ -4339,13 +4448,13 @@ func (e *StateExecutor) stepOrderChoice(due []*doAction, dispatch string) Choice choice := ChoicePoint{ Kind: choiceStepOrder, Where: stepWherePrefix + semantics.FormatReal(e.ctx.clock.now), - Alternatives: []string{doStepLabel(e.stateNames(statesOf(due))), dispatch}, + Alternatives: []string{doStepLabel(e.stateNames(e.statesOf(due))), dispatch}, File: e.stateMachine.DocName, Span: e.stateMachine.DeclSpan, } var steps lower.Footprint for _, act := range due { - steps = unionFootprints(steps, doStepFootprint(e, act.state)) + steps = unionFootprints(steps, doStepFootprint(e, e.stateDecl(act.state))) } choice.alike = e.ctx.heldAlike classifyChoice(&choice, []lower.Footprint{steps, dispatchFootprint(e)}, nil) @@ -4364,7 +4473,7 @@ func (e *StateExecutor) noteRoundStepOrder(due []*doAction) { } choice := e.stepOrderChoice(due, dispatch.step) if len(due) == 1 { - choice.Span = due[0].state.Span() + choice.Span = e.stateDecl(due[0].state).Span() } e.noteChoice(choice) } @@ -4487,7 +4596,7 @@ func (e *StateExecutor) startDoAction(state *ast.StateNode) { return } e.registerDoAction(&doAction{ - state: state, + state: e.stateID(state), pending: slices.Clone(pending), firing: e.currentFiring(), }) @@ -4497,7 +4606,7 @@ func (e *StateExecutor) startDoAction(state *ast.StateNode) { // the behaviors rest after it) as its do action, ahead of its do behaviors. func (e *StateExecutor) parkEntry(state *ast.StateNode, run *doRun, rest []lower.StateBehavior) { e.registerDoAction(&doAction{ - state: state, + state: e.stateID(state), pending: slices.Concat(rest, e.behaviorsOf(state).Do), firing: e.currentFiring(), run: run, @@ -4509,7 +4618,7 @@ func (e *StateExecutor) parkEntry(state *ast.StateNode, run *doRun, rest []lower // registerDoAction makes act the running do action of its state, replacing the one // abandoned when the state was last exited, and offers it to the entry site under way. func (e *StateExecutor) registerDoAction(act *doAction) { - e.stopDoAction(act.state) + e.stopDoAction(e.stateDecl(act.state)) e.doActions = append(e.doActions, act) e.began = append(e.began, act) if e.inFront(choiceEntryOrder) { @@ -4521,7 +4630,7 @@ func (e *StateExecutor) registerDoAction(act *doAction) { // entry has run to its end or where it ran at the instant. func (e *StateExecutor) parkedEntryOf(state *ast.StateNode) *doAction { for _, act := range e.doActions { - if act.state == state && act.entering { + if act.state == e.stateID(state) && act.entering { return act } } @@ -4532,7 +4641,7 @@ func (e *StateExecutor) parkedEntryOf(state *ast.StateNode) *doAction { // goes on entering below them. func (e *StateExecutor) resumeEntering(states []*ast.StateNode) { for _, act := range e.doActions { - if slices.Contains(states, act.state) && !slices.Contains(e.began, act) { + if slices.Contains(states, e.stateDecl(act.state)) && !slices.Contains(e.began, act) { e.began = append(e.began, act) } } @@ -4552,7 +4661,7 @@ func (e *StateExecutor) behaviorsOf(state *ast.StateNode) *lower.StateBehaviors func (e *StateExecutor) stopDoAction(state *ast.StateNode) { kept := e.doActions[:0] for _, act := range e.doActions { - if act.state != state { + if act.state != e.stateID(state) { kept = append(kept, act) continue } @@ -4627,15 +4736,15 @@ func (e *StateExecutor) stepDoAction(act *doAction, goOn func(*doRun) (*doRun, e behavior = run.host.behavior } if err := e.validateBehaviorMultiplicity(behavior); err != nil { - return fmt.Errorf("do action in state %s: %w", act.state.Name, err) + return fmt.Errorf("do action in state %s: %w", e.stateDecl(act.state).Name, err) } e.moved = true if e.trace() != nil { if act.entering { - e.trace().recordEntryStep(e.traceOrigin(), act.state.Name, e.StatePath(act.state), e.regionPathName(act.state)) + e.trace().recordEntryStep(e.traceOrigin(), e.stateDecl(act.state).Name, e.StatePath(e.stateDecl(act.state)), e.regionPathName(e.stateDecl(act.state))) } else { - e.trace().recordDoStep(e.traceOrigin(), act.state.Name, e.StatePath(act.state), e.regionPathName(act.state)) + e.trace().recordDoStep(e.traceOrigin(), e.stateDecl(act.state).Name, e.StatePath(e.stateDecl(act.state)), e.regionPathName(e.stateDecl(act.state))) } } if run == nil { @@ -4656,7 +4765,7 @@ func (e *StateExecutor) stepDoAction(act *doAction, goOn func(*doRun) (*doRun, e var err error act.run, err = goOn(run) if err != nil { - return fmt.Errorf("do action in state %s: %w", act.state.Name, err) + return fmt.Errorf("do action in state %s: %w", e.stateDecl(act.state).Name, err) } act.settleEntering() return nil @@ -4678,12 +4787,12 @@ func (act *doAction) settleEntering() { func (e *StateExecutor) doBehaviorsTaking(m Message, candidates []dispatchCandidate) ([]*doAction, error) { var taking []*doAction for _, act := range e.doActions { - if act.run == nil || e.takenByChosen(act.state, candidates) { + if act.run == nil || e.takenByChosen(e.stateDecl(act.state), candidates) { continue } accepted, err := act.run.acceptsMessage(m) if err != nil { - return nil, fmt.Errorf("do action in state %s: %w", act.state.Name, err) + return nil, fmt.Errorf("do action in state %s: %w", e.stateDecl(act.state).Name, err) } if accepted { taking = append(taking, act) @@ -4698,7 +4807,7 @@ func (e *StateExecutor) doBehaviorsTaking(m Message, candidates []dispatchCandid // would fail counts as one. func (e *StateExecutor) takenByChosen(state *ast.StateNode, candidates []dispatchCandidate) bool { for _, candidate := range candidates { - if e.isBelowOrEqual(candidate.source, state) { + if e.isBelowOrEqual(e.stateDecl(candidate.source), state) { return true } exited, ok := e.exitedBy(candidate) @@ -4725,8 +4834,8 @@ func (e *StateExecutor) exitedBy(candidate dispatchCandidate) ([]*ast.StateNode, if !r.settled() { return nil, false } - if region := e.activeRegionOf(candidate.source); region != nil { - source := e.activeConfig.regionStates[region] + if region := e.activeRegionOf(e.stateDecl(candidate.source)); region != nil { + source := e.regionActiveState(region) return e.mayExit(r, trans, source, func(target *ast.StateNode) []*ast.StateNode { return e.exitedInRegion(region, trans, target) }) } origin := e.moveOrigin() @@ -4742,7 +4851,7 @@ func (e *StateExecutor) exitedInRegion(region *ast.StateRegion, trans *lower.Tra if owner == nil { var exited []*ast.StateNode for _, top := range e.graph.TopRegions { - if active, ok := e.activeConfig.regionStates[top]; ok { + if active, ok := e.regionActive(top); ok { exited = append(exited, e.exitPath(active, nil, nil)...) } } @@ -4844,7 +4953,7 @@ func (e *StateExecutor) resumeDoBehaviors(taking []*doAction, m Message) ([]stri if err := e.stepDoAction(act, func(run *doRun) (*doRun, error) { return run.offer(e.ctx, m) }); err != nil { return resumed, err } - resumed = append(resumed, doBehaviorDescription(act.state)) + resumed = append(resumed, doBehaviorDescription(e.stateDecl(act.state))) } if len(resumed) == 0 { return nil, nil @@ -4873,7 +4982,7 @@ func (e *StateExecutor) settleDoActions() error { kept = append(kept, act) continue } - finished = append(finished, act.state) + finished = append(finished, e.stateDecl(act.state)) } for i := len(kept); i < len(e.doActions); i++ { e.doActions[i] = nil @@ -4918,7 +5027,7 @@ func (e *StateExecutor) chooseDoAction(due []*doAction) (int, error) { } states := make([]*ast.StateNode, len(due)) for i, act := range due { - states[i] = act.state + states[i] = e.stateDecl(act.state) } choice := e.regionOrderChoice("do round at t="+semantics.FormatReal(e.ctx.clock.now), states, func(state *ast.StateNode) lower.Footprint { return doStepFootprint(e, state) @@ -4944,7 +5053,7 @@ func (e *StateExecutor) isRunningDoAction(act *doAction) bool { // hasRunningDoAction reports whether a state's do behavior is still running. func (e *StateExecutor) hasRunningDoAction(state *ast.StateNode) bool { for _, act := range e.doActions { - if act.state == state { + if act.state == e.stateID(state) { return true } } @@ -5296,7 +5405,7 @@ func (e *StateExecutor) decide(m Message) (Decision, []*lower.Transition, error) transitions = append(transitions, candidate.chosen) } for _, act := range taking { - decision.Resumes = append(decision.Resumes, doBehaviorDescription(act.state)) + decision.Resumes = append(decision.Resumes, doBehaviorDescription(e.stateDecl(act.state))) } return decision, transitions, nil } @@ -5539,9 +5648,7 @@ func (e *StateExecutor) initialize() (err error) { } e.state = StateRunning - e.activeConfig.regionStates = make(map[*ast.StateRegion]*ast.StateNode) - e.activeConfig.defaultComplete = make(map[*ast.StateRegion]bool) - e.activeConfig.simpleState = nil + e.activeConfig.reset() if act := e.parkedEntryOf(e.graph.Machine); act != nil { e.holdParkedEntry(nil, act, nil, nil) return nil @@ -5573,8 +5680,8 @@ func (e *StateExecutor) enterMachineStart() error { return nil } e.setCurrentState(start) - e.stateStack = e.rootToLeaf(start) - for _, state := range e.stateStack { + e.stateStack = e.stateIDs(e.rootToLeaf(start)) + for _, state := range e.rootToLeaf(start) { if err := e.enterStateInto(state, nil, state == start); err != nil { return fmt.Errorf("enter state %s: %w", state.Name, err) } @@ -5604,7 +5711,7 @@ func (e *StateExecutor) enterMachineRegions() error { return fmt.Errorf("schedule events: %w", err) } for _, region := range e.graph.TopRegions { - if err := e.completeIfDone(e.activeConfig.regionStates[region]); err != nil { + if err := e.completeIfDone(e.regionActiveState(region)); err != nil { return fmt.Errorf("complete state machine: %w", err) } if e.state.Ended() { @@ -5617,7 +5724,7 @@ func (e *StateExecutor) enterMachineRegions() error { // startIn chooses where the owner's body starts and performs its entry route. func (e *StateExecutor) startIn(owner ast.Node) (*ast.StateNode, error) { transitions := e.graph.StartOf(owner) - for _, entry := range transitions { + for i, entry := range transitions { holds, err := e.entryGuardHolds(owner, entry) if err != nil { return nil, err @@ -5644,7 +5751,7 @@ func (e *StateExecutor) startIn(owner ast.Node) (*ast.StateNode, error) { return nil, nil } if entry.Decl != nil { - e.fired = append(e.fired, FiredTransition{Decl: entry.Decl, Target: entryTarget(entry), Owner: e.graph.EntryOwner(owner)}) + e.fired = append(e.fired, e.firedEntry(owner, i+1, entry)) } return target, nil } @@ -5711,23 +5818,23 @@ func (e *StateExecutor) continueEntryRoute(fromName string, via *ast.Pseudostate return nil, fmt.Errorf("entry transition through %s %s: %w", via.Kind, via.Name, err) } } - if err := e.runEntryRouteEffects(route.effects(e.graph)); err != nil { + if err := e.runEntryRouteEffects(route.effects(e)); err != nil { return nil, err } e.noteFired(route.segments...) e.noteAll(route.notes) route.notes = nil - if route.terminate != nil { - if err := e.terminateMachine(fromName, nil, route.terminate); err != nil { + if stop := e.terminateDecl(route.terminate); stop != nil { + if err := e.terminateMachine(fromName, nil, stop); err != nil { return nil, err } return nil, nil } - if route.choice == nil { - if route.target == nil { + if route.choice == 0 { + if route.target == 0 { return nil, fmt.Errorf("entry transition through %s %s has no state target", via.Kind, via.Name) } - return route.target, nil + return e.stateDecl(route.target), nil } route, err = e.resolveChoice(route) if err != nil { @@ -5910,7 +6017,7 @@ func (e *StateExecutor) enterStateInto(state *ast.StateNode, branches map[*ast.S return nil } leaf := last && e.completesAtEntry(state) - if e.entryIsUnit(state) && !e.enteredAhead[state] { + if e.entryIsUnit(state) && !e.enteredAhead[e.stateID(state)] { if _, err := e.unit(choiceEntryOrder, e.entryHead(state, leaf)); err != nil { return err } @@ -5972,49 +6079,49 @@ func (e *StateExecutor) activateState(state *ast.StateNode) error { // entries of other composites' regions are left alone. A simple state is the // single active state only outside any region. if _, isComposite := e.graph.CompositeStates[state]; isComposite { - e.activeConfig.simpleState = nil + e.activeConfig.simpleState = 0 } else if len(e.activeConfig.regionStates) == 0 { - e.activeConfig.simpleState = state + e.setSimpleState(state) } return nil } // resetEntering starts an entry unit and snapshots the configuration active before it. func (e *StateExecutor) resetEntering() { - e.entering = make(map[*ast.StateNode]bool) + e.entering = make(map[program.StateID]bool) e.enteringMachine = false e.began, e.path = nil, pathDraw{} - e.activeAtEntry = make(map[*ast.StateNode]bool) + e.activeAtEntry = make(map[program.StateID]bool) for _, active := range e.activeStates() { for _, state := range e.getParentChain(active) { - e.activeAtEntry[state] = true + e.activeAtEntry[e.stateID(state)] = true } } } // performEntry records the entry of state and performs its entry behaviors. func (e *StateExecutor) performEntry(state *ast.StateNode) error { - e.ctx.restartBodyCells(e.stateAttrCells[state]) - if !e.activeAtEntry[state] { - e.entering[state] = true + e.ctx.restartBodyCells(e.stateAttrCells[e.stateID(state)]) + if !e.activeAtEntry[e.stateID(state)] { + e.entering[e.stateID(state)] = true } // Change watches are created fresh per activation, so a condition that stayed // true rises again; the firing transition keeps its latch so the entry it // caused does not re-enable it. for _, trans := range e.graph.Transitions[state] { - if trans != e.firingChange { - delete(e.changeFired, trans) + if e.transID(trans) != e.firingChange { + delete(e.changeFired, e.transID(trans)) if e.changeRearmed != nil { - e.changeRearmed[trans] = true + e.changeRearmed[e.transID(trans)] = true } } - delete(e.changeObserved, trans) - delete(e.changePending, trans) - delete(e.changeReads, trans) + delete(e.changeObserved, e.transID(trans)) + delete(e.changePending, e.transID(trans)) + delete(e.changeReads, e.transID(trans)) } - if e.breakpointNodes[state] && e.breakpointHit == nil { - e.breakpointHit = state + if e.breakpointNodes[e.vertexRef(state)] && e.breakpointHit == 0 { + e.breakpointHit = e.stateID(state) } if !e.graph.HiddenStates[state] { @@ -6049,12 +6156,12 @@ func (e *StateExecutor) performingBehaviors(state *ast.StateNode, behaviors []lo return nil } if e.performing == nil { - e.performing = make(map[*ast.StateNode]int) + e.performing = make(map[program.StateID]int) } - e.performing[state]++ + e.performing[e.stateID(state)]++ defer func() { - if e.performing[state]--; e.performing[state] == 0 { - delete(e.performing, state) + if e.performing[e.stateID(state)]--; e.performing[e.stateID(state)] == 0 { + delete(e.performing, e.stateID(state)) } }() return e.executeBehaviors(behaviors) @@ -6067,7 +6174,7 @@ func (e *StateExecutor) activity(state *ast.StateNode) bool { if e == nil || state == nil || e.state.Ended() { return false } - return e.performing[state] > 0 || e.inActiveConfiguration(state) + return e.performing[e.stateID(state)] > 0 || e.inActiveConfiguration(state) } // exitState executes exit behaviors when leaving a state. @@ -6077,11 +6184,11 @@ func (e *StateExecutor) exitState(state *ast.StateNode) error { } e.clearJoinArrivalsOwnedBy(state) e.held = slices.DeleteFunc(e.held, func(item heldEntry) bool { - if item.owner == state { + if item.owner == e.stateID(state) { delete(e.entering, item.owner) return true } - for current := item.owner; current != nil; current = e.graph.ParentState[current] { + for current := e.stateDecl(item.owner); current != nil; current = e.graph.ParentState[current] { if current == state { delete(e.entering, item.owner) return true @@ -6094,10 +6201,10 @@ func (e *StateExecutor) exitState(state *ast.StateNode) error { // is withdrawn, so re-entering the state times a fresh interval. timed := make(map[*lower.Transition]bool) for _, trans := range e.graph.Transitions[state] { - delete(e.timerScheduled, trans) - delete(e.changeObserved, trans) - delete(e.changePending, trans) - delete(e.changeReads, trans) + delete(e.timerScheduled, e.transID(trans)) + delete(e.changeObserved, e.transID(trans)) + delete(e.changePending, e.transID(trans)) + delete(e.changeReads, e.transID(trans)) if _, isTime := trans.Trigger.(*ast.TimeEvent); isTime { timed[trans] = true } @@ -6117,7 +6224,7 @@ func (e *StateExecutor) exitState(state *ast.StateNode) error { // whole map would stop the regions of an enclosing composite state too. active := make(map[*ast.StateRegion]*ast.StateNode, len(regions)) for _, region := range regions { - if regionState, isActive := e.activeConfig.regionStates[region]; isActive { + if regionState, isActive := e.regionActive(region); isActive { active[region] = regionState } } @@ -6142,12 +6249,12 @@ func (e *StateExecutor) exitState(state *ast.StateNode) error { } } - if e.leftAhead[state] { - e.activeConfig.simpleState = nil + if e.leftAhead[e.stateID(state)] { + e.activeConfig.simpleState = 0 return nil } if e.exitingAhead { - e.leftAhead[state] = true + e.leftAhead[e.stateID(state)] = true } if e.exitIsUnit(state) { if _, err := e.unit(choiceExitOrder, unitHead{label: e.exitLabel(state), at: state, silent: e.silentExit(state)}); err != nil { @@ -6171,13 +6278,13 @@ func (e *StateExecutor) exitState(state *ast.StateNode) error { if err := e.performingBehaviors(state, e.behaviorsOf(state).Exit); err != nil { return fmt.Errorf("exit action: %w", err) } - if err := e.ctx.freezeBodyCells(e.stateAttrCells[state]); err != nil { + if err := e.ctx.freezeBodyCells(e.stateAttrCells[e.stateID(state)]); err != nil { return fmt.Errorf("freeze state %s attributes: %w", state.Name, err) } - e.ctx.exitBodyCells(e.stateAttrCells[state]) + e.ctx.exitBodyCells(e.stateAttrCells[e.stateID(state)]) // Clear simple state - e.activeConfig.simpleState = nil + e.activeConfig.simpleState = 0 return nil } @@ -6193,12 +6300,11 @@ func (e *StateExecutor) exitRegionsBelow(owner *ast.StateNode, regions []*ast.St } // Clear the entry first: the recursive exit walks the same map, and the // region still pointing at regionState would exit it a second time. - delete(e.activeConfig.regionStates, region) + e.deactivateRegion(region) var leaving []*ast.StateNode for current := regionState; current != nil && current != owner; current = e.graph.ParentState[current] { leaving = append(leaving, current) } - delete(e.activeConfig.defaultComplete, region) units = append(units, unit{ run: func() error { for _, current := range leaving { @@ -6273,8 +6379,8 @@ func stateActionName(u *ast.Usage) string { // CurrentState returns the current active state node. func (e *StateExecutor) CurrentState() ast.Node { // For backward compatibility: return simple state if no regions active - if e.activeConfig.simpleState != nil { - return e.activeConfig.simpleState + if e.activeConfig.simpleState != 0 { + return e.simpleState() } // If multi-region, return nil (caller should check regions individually) return nil @@ -6324,7 +6430,7 @@ func (e *StateExecutor) finalStateName() string { sort.SliceStable(regions, func(i, j int) bool { return regions[i].Name < regions[j].Name }) names := make([]string, len(regions)) for i, region := range regions { - names[i] = e.activeConfig.regionStates[region].Name + names[i] = e.regionActiveState(region).Name } return strings.Join(names, "+") } @@ -6336,20 +6442,18 @@ func (e *StateExecutor) finalStateName() string { // getCurrentState returns the active simple state (nil if multi-region). func (e *StateExecutor) getCurrentState() *ast.StateNode { - return e.activeConfig.simpleState + return e.simpleState() } // setCurrentState sets the active simple state (for non-region states). func (e *StateExecutor) setCurrentState(state *ast.StateNode) { - e.activeConfig.simpleState = state - e.activeConfig.regionStates = make(map[*ast.StateRegion]*ast.StateNode) // Clear regions - e.activeConfig.defaultComplete = make(map[*ast.StateRegion]bool) + e.activeConfig.reset() + e.setSimpleState(state) } // StateStack returns a copy of the state stack (active configuration). func (e *StateExecutor) StateStack() []*ast.StateNode { - stack := make([]*ast.StateNode, len(e.stateStack)) - copy(stack, e.stateStack) + stack := e.stateDecls(e.stateStack) return stack } @@ -6374,7 +6478,7 @@ func (e *StateExecutor) StateDataWithError() (map[string]Value, error) { if err := e.ctx.deriveBodyCells(cells); err != nil && deriveErr == nil { deriveErr = err } - data = copyStateValuesInto(data, attrs, cells, e.statePath(state)+".") + data = copyStateValuesInto(data, attrs, cells, e.statePath(e.stateDecl(state))+".") } return data, deriveErr } @@ -6585,7 +6689,7 @@ func (e *StateExecutor) Resume() bool { if e.state != StateSuspended { return false } - e.state, e.pausedAt = StateRunning, nil + e.state, e.pausedAt = StateRunning, program.VertexRef{} return true } @@ -6596,24 +6700,24 @@ func (e *StateExecutor) SetBreakpointAt(vertex ast.Node) { switch v := vertex.(type) { case *ast.StateNode: if v != nil { - e.breakpointNodes[v] = true + e.breakpointNodes[e.vertexRef(v)] = true } case *ast.PseudostateNode: if v != nil { - e.breakpointNodes[v] = true + e.breakpointNodes[e.vertexRef(v)] = true } } } // ClearBreakpoints removes every breakpoint; a pause already reached stands. func (e *StateExecutor) ClearBreakpoints() { - e.breakpointNodes = make(map[ast.Node]bool) + e.breakpointNodes = make(map[program.VertexRef]bool) } // PausedAt is the breakpoint state or pseudostate the last run paused at, nil // when none did. func (e *StateExecutor) PausedAt() ast.Node { - return e.pausedAt + return e.vertexDecl(e.pausedAt) } // CompletionDue reports a machine paused on its completion vertex by a @@ -6756,7 +6860,7 @@ func (e *StateExecutor) hasPendingChange() bool { for _, leaf := range e.activeLeaves() { for _, source := range e.getParentChain(leaf) { for _, trans := range e.graph.Transitions[source] { - if e.changePending[trans] { + if e.changePending[e.transID(trans)] { return true } } diff --git a/internal/exec/runtime/state_executor_test.go b/internal/exec/runtime/state_executor_test.go index 8b6dcabe5f..2f34cb8039 100644 --- a/internal/exec/runtime/state_executor_test.go +++ b/internal/exec/runtime/state_executor_test.go @@ -881,8 +881,8 @@ func TestStateExecutor_StateStackTracking(t *testing.T) { } for i, expected := range expectedStack { - if exec.stateStack[i] != expected { - t.Errorf("stateStack[%d]: expected %s, got %s", i, expected.Name, exec.stateStack[i].Name) + if got := exec.stateDecl(exec.stateStack[i]); got != expected { + t.Errorf("stateStack[%d]: expected %s, got %s", i, expected.Name, got.Name) } } @@ -897,8 +897,8 @@ func TestStateExecutor_StateStackTracking(t *testing.T) { t.Errorf("expected stateStack length 1, got %d", len(exec.stateStack)) } - if len(exec.stateStack) > 0 && exec.stateStack[0] != standalone { - t.Errorf("expected stateStack[0] = standalone, got %s", exec.stateStack[0].Name) + if len(exec.stateStack) > 0 && exec.stateDecl(exec.stateStack[0]) != standalone { + t.Errorf("expected stateStack[0] = standalone, got %s", exec.stateDecl(exec.stateStack[0]).Name) } if exec.getCurrentState() != standalone { diff --git a/internal/exec/runtime/state_ids.go b/internal/exec/runtime/state_ids.go new file mode 100644 index 0000000000..be3d1b2b67 --- /dev/null +++ b/internal/exec/runtime/state_ids.go @@ -0,0 +1,201 @@ +package runtime + +import ( + "slices" + + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// The executor's own records name states, regions, pseudostates and transitions by +// the graph-local ids lowering assigns; these translate to the declarations the graph's tables key. + +// machineID is the id the executor gives the machine state, which lowering keeps +// out of the state table: one past the last numbered state. +func (e *StateExecutor) machineID() program.StateID { + return program.StateID(len(e.graph.Origins)) // #nosec G115 -- a graph has far fewer than 2^32 states +} + +// stateID is the graph-local id of state, 0 for nil. +func (e *StateExecutor) stateID(state *ast.StateNode) program.StateID { + if state == nil { + return 0 + } + if state == e.graph.Machine { + return e.machineID() + } + return e.graph.ID[state] +} + +// stateDecl is the state declared under id, nil for 0. +func (e *StateExecutor) stateDecl(id program.StateID) *ast.StateNode { + if id == e.machineID() && e.graph.Machine != nil { + return e.graph.Machine + } + return e.graph.Provenance.State(id) +} + +// stateIDs maps states to their ids, in order. +func (e *StateExecutor) stateIDs(states []*ast.StateNode) []program.StateID { + if states == nil { + return nil + } + ids := make([]program.StateID, len(states)) + for i, state := range states { + ids[i] = e.stateID(state) + } + return ids +} + +// stateDecls maps ids to the states declared under them, in order. +func (e *StateExecutor) stateDecls(ids []program.StateID) []*ast.StateNode { + if ids == nil { + return nil + } + states := make([]*ast.StateNode, len(ids)) + for i, id := range ids { + states[i] = e.stateDecl(id) + } + return states +} + +// regionID is the graph-local id of region, 0 for nil. +func (e *StateExecutor) regionID(region *ast.StateRegion) program.RegionID { + if region == nil { + return 0 + } + return e.graph.RegionID[region] +} + +// regionDecl is the region declared under id, nil for 0. +func (e *StateExecutor) regionDecl(id program.RegionID) *ast.StateRegion { + return e.graph.Provenance.Region(id) +} + +// regionIDs maps regions to their ids, in order. +func (e *StateExecutor) regionIDs(regions []*ast.StateRegion) []program.RegionID { + if regions == nil { + return nil + } + ids := make([]program.RegionID, len(regions)) + for i, region := range regions { + ids[i] = e.regionID(region) + } + return ids +} + +// regionDecls maps ids to the regions declared under them, in order. +func (e *StateExecutor) regionDecls(ids []program.RegionID) []*ast.StateRegion { + if ids == nil { + return nil + } + regions := make([]*ast.StateRegion, len(ids)) + for i, id := range ids { + regions[i] = e.regionDecl(id) + } + return regions +} + +// pseudoID is the graph-local id of ps, 0 for nil. +func (e *StateExecutor) pseudoID(ps *ast.PseudostateNode) program.PseudoID { + if ps == nil { + return 0 + } + return e.graph.PseudoID[ps] +} + +// pseudoDecl is the pseudostate declared under id, nil for 0. +func (e *StateExecutor) pseudoDecl(id program.PseudoID) *ast.PseudostateNode { + return e.graph.Provenance.Pseudostate(id) +} + +// transID is the graph-local id of trans, 0 for nil. +func (e *StateExecutor) transID(trans *lower.Transition) program.TransID { + if trans == nil { + return 0 + } + return e.graph.TransID[trans] +} + +// transDecl is the transition lowered under id, nil for 0. +func (e *StateExecutor) transDecl(id program.TransID) *lower.Transition { + return e.graph.Provenance.Transition(id) +} + +// transIDs maps transitions to their ids, in order. +func (e *StateExecutor) transIDs(transitions []*lower.Transition) []program.TransID { + if transitions == nil { + return nil + } + ids := make([]program.TransID, len(transitions)) + for i, trans := range transitions { + ids[i] = e.transID(trans) + } + return ids +} + +// vertexRef names a state, pseudostate or terminate action vertex by id; the zero +// reference for nil or any other node. +func (e *StateExecutor) vertexRef(node ast.Node) program.VertexRef { + switch n := node.(type) { + case *ast.StateNode: + return program.StateVertex(e.stateID(n)) + case *ast.PseudostateNode: + return program.PseudoVertex(e.pseudoID(n)) + case *ast.Usage: + if i := slices.Index(e.graph.Terminates, n); i >= 0 { + return program.TerminateVertex(uint32(i + 1)) // #nosec G115 -- a graph has far fewer than 2^32 terminates + } + } + return program.VertexRef{} +} + +// vertexDecl is the node declared under ref, nil for the zero reference. +func (e *StateExecutor) vertexDecl(ref program.VertexRef) ast.Node { + switch ref.Kind { + case program.VertexState: + if state := e.stateDecl(ref.State()); state != nil { + return state + } + case program.VertexPseudostate: + if ps := e.pseudoDecl(ref.Pseudo()); ps != nil { + return ps + } + case program.VertexTerminate: + if i := int(ref.Index) - 1; i >= 0 && i < len(e.graph.Terminates) { + return e.graph.Terminates[i] + } + } + return nil +} + +// terminateDecl is the terminate action usage under ref, nil for any other reference. +func (e *StateExecutor) terminateDecl(ref program.VertexRef) *ast.Usage { + stop, _ := e.vertexDecl(ref).(*ast.Usage) + return stop +} + +// regionStateIDs maps a region → state table to ids. +func (e *StateExecutor) regionStateIDs(states map[*ast.StateRegion]*ast.StateNode) map[program.RegionID]program.StateID { + if states == nil { + return nil + } + ids := make(map[program.RegionID]program.StateID, len(states)) + for region, state := range states { + ids[e.regionID(region)] = e.stateID(state) + } + return ids +} + +// regionStateDecls maps a region → state table of ids back to declarations. +func (e *StateExecutor) regionStateDecls(ids map[program.RegionID]program.StateID) map[*ast.StateRegion]*ast.StateNode { + if ids == nil { + return nil + } + states := make(map[*ast.StateRegion]*ast.StateNode, len(ids)) + for region, state := range ids { + states[e.regionDecl(region)] = e.stateDecl(state) + } + return states +} diff --git a/internal/exec/runtime/state_region_entry.go b/internal/exec/runtime/state_region_entry.go index 1cee317f70..01d6ad0cca 100644 --- a/internal/exec/runtime/state_region_entry.go +++ b/internal/exec/runtime/state_region_entry.go @@ -5,34 +5,35 @@ import ( "slices" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) // regionEntry is one orthogonal region's share of entering its container. type regionEntry struct { - region *ast.StateRegion - container *ast.StateNode - branches map[*ast.StateRegion]*ast.StateNode - target *ast.StateNode // where the region starts instead of its own start, if anywhere + region program.RegionID + container program.StateID + branches map[program.RegionID]program.StateID + target program.StateID // where the region starts instead of its own start, if anywhere effects []routeEffect } // lazyEntry is the chain of states a fork's branches still have to enter down to // owner, the composite whose regions they enter. type lazyEntry struct { - chain []*ast.StateNode + chain []program.StateID next int - owner *ast.StateNode - branches map[*ast.StateRegion]*ast.StateNode // region the chain passes through → the state it passes into + owner program.StateID + branches map[program.RegionID]program.StateID // region the chain passes through → the state it passes into } // forkEntry describes the way from boundary, which stays active, down to the // composite whose regions a fork's branches enter. func (e *StateExecutor) forkEntry(boundary, owner *ast.StateNode) *lazyEntry { return &lazyEntry{ - chain: e.descendantChain(boundary, owner), - owner: owner, - branches: e.branchesTo(boundary, owner), + chain: e.stateIDs(e.descendantChain(boundary, owner)), + owner: e.stateID(owner), + branches: e.regionStateIDs(e.branchesTo(boundary, owner)), } } @@ -42,10 +43,10 @@ func (e *StateExecutor) forkEntry(boundary, owner *ast.StateNode) *lazyEntry { func (e *StateExecutor) enterRegionsInto(container *ast.StateNode, regions []*ast.StateRegion, branches map[*ast.StateRegion]*ast.StateNode) error { entries := make([]*regionEntry, 0, len(regions)) for _, region := range regions { - entry := ®ionEntry{region: region, container: container, branches: branches, target: branches[region]} - if effects := e.pendingRouteEntryEffects[region]; len(effects) > 0 { + entry := ®ionEntry{region: e.regionID(region), container: e.stateID(container), branches: e.regionStateIDs(branches), target: e.stateID(branches[region])} + if effects := e.pendingRouteEntryEffects[e.regionID(region)]; len(effects) > 0 { entry.effects = slices.Clone(effects) - delete(e.pendingRouteEntryEffects, region) + delete(e.pendingRouteEntryEffects, e.regionID(region)) } entries = append(entries, entry) } @@ -82,14 +83,14 @@ func (e *StateExecutor) enterForkBranches(fork *ast.PseudostateNode, plan *lower units := make([]unit, 0, len(regions)) shared := lower.Footprint{} for _, state := range above.chain[above.next:] { - shared = unionFootprints(shared, e.enterFootprint(state)) + shared = unionFootprints(shared, e.enterFootprint(e.stateDecl(state))) } for _, region := range regions { - entry := ®ionEntry{region: region, container: plan.Owner, branches: targets} + entry := ®ionEntry{region: e.regionID(region), container: e.stateID(plan.Owner), branches: e.regionStateIDs(targets)} branch := plan.Branches[region] footprint := unionFootprints(shared, e.regionEntryFootprint(entry)) if branch != nil { - entry.target = branch.Target.(*ast.StateNode) + entry.target = e.stateID(branch.Target.(*ast.StateNode)) footprint = unionFootprints(footprint, e.behaviorsFootprint(branch.Effect)) } units = append(units, unit{ @@ -102,7 +103,7 @@ func (e *StateExecutor) enterForkBranches(fork *ast.PseudostateNode, plan *lower if err := e.runBranchEffect(branch); err != nil { return err } - e.ctx.noteTransitionTaken(e.self, e.stateMachine, e.vertexPathNames(fork), e.statePathNames(entry.target), branch) + e.ctx.noteTransitionTaken(e.self, e.stateMachine, e.vertexPathNames(fork), e.statePathNames(e.stateDecl(entry.target)), branch) } if err := e.enterShared(above); err != nil { return err @@ -119,11 +120,11 @@ func (e *StateExecutor) enterForkBranches(fork *ast.PseudostateNode, plan *lower // state a unit every branch heads until one of them enters it. func (e *StateExecutor) enterShared(l *lazyEntry) error { for i := l.next; i < len(l.chain); i++ { - state := l.chain[i] + state := e.stateDecl(l.chain[i]) perform := true if e.entryIsUnit(state) { head := e.entryHead(state, false) - head.shared, head.dropped = state, func() bool { return l.next > i } + head.shared, head.dropped = e.stateID(state), func() bool { return l.next > i } var err error if perform, err = e.unit(choiceEntryOrder, head); err != nil { return err @@ -164,8 +165,8 @@ func (e *StateExecutor) enterRegion(w *regionEntry) error { if e.state == StateTerminated { return nil } - if w.target == nil && e.graph.RegionState[w.region] == nil && len(e.graph.StartOf(w.region)) > 0 { - if err := e.unitAhead(choiceEntryOrder, e.startHead(w.region, w.container)); err != nil { + if w.target == 0 && e.graph.RegionState[e.regionDecl(w.region)] == nil && len(e.graph.StartOf(e.regionDecl(w.region))) > 0 { + if err := e.unitAhead(choiceEntryOrder, e.startHead(e.regionDecl(w.region), e.stateDecl(w.container))); err != nil { return err } } @@ -177,25 +178,25 @@ func (e *StateExecutor) enterRegion(w *regionEntry) error { return nil } if entry == nil { - if w.target == nil { + if w.target == 0 { e.activeConfig.defaultComplete[w.region] = true } return nil } - e.setRegionState(w.region, entry) - _, deepest, err := e.enterToward(w.container, entry, w.branches) + e.setRegionState(e.regionDecl(w.region), entry) + _, deepest, err := e.enterToward(e.stateDecl(w.container), entry, e.regionStateDecls(w.branches)) if err != nil { - return fmt.Errorf("enter starting state in region %s: %w", w.region.Name, err) + return fmt.Errorf("enter starting state in region %s: %w", e.regionDecl(w.region).Name, err) } if e.state == StateTerminated { return nil } // The deepest state the region keeps active is the one on the way down that // its own substates declare. - if branch, ok := e.branchesTo(nil, deepest)[w.region]; ok { - e.setRegionState(w.region, branch) + if branch, ok := e.branchesTo(nil, deepest)[e.regionDecl(w.region)]; ok { + e.setRegionState(e.regionDecl(w.region), branch) } - if w.target == nil && entry == e.graph.RegionState[w.region] && deepest == entry && + if w.target == 0 && entry == e.graph.RegionState[e.regionDecl(w.region)] && deepest == entry && len(e.graph.StartOf(entry)) == 0 { e.activeConfig.defaultComplete[w.region] = true } @@ -226,13 +227,13 @@ func (e *StateExecutor) startHead(body ast.Node, above *ast.StateNode) unitHead // regionStart is the state a region starts in: the target it was given, the // substate of a parallel state it stands for, or its own entry transitions' choice. func (e *StateExecutor) regionStart(w *regionEntry) (*ast.StateNode, error) { - if w.target != nil { - return w.target, nil + if w.target != 0 { + return e.stateDecl(w.target), nil } - if owner := e.graph.RegionState[w.region]; owner != nil { + if owner := e.graph.RegionState[e.regionDecl(w.region)]; owner != nil { return owner, nil } - entry, err := e.startIn(w.region) + entry, err := e.startIn(e.regionDecl(w.region)) if err != nil { return nil, err } @@ -246,20 +247,20 @@ func (e *StateExecutor) regionStart(w *regionEntry) (*ast.StateNode, error) { // behavior begun at its state's entry; the regions off the chain start as usual, on a front. func (e *StateExecutor) enterLazily(l *lazyEntry, upto int) error { for ; l.next < upto; l.next++ { - state := l.chain[l.next] + state := e.stateDecl(l.chain[l.next]) if err := e.activateState(state); err != nil { return fmt.Errorf("enter state %s: %w", state.Name, err) } e.startDoAction(state) - if state == l.owner { + if state == e.stateDecl(l.owner) { continue } var others []*regionEntry for _, region := range e.graph.CompositeStates[state] { - if _, onWay := l.branches[region]; onWay { + if _, onWay := l.branches[e.regionID(region)]; onWay { continue } - others = append(others, ®ionEntry{region: region, container: state, branches: l.branches}) + others = append(others, ®ionEntry{region: e.regionID(region), container: e.stateID(state), branches: l.branches}) } if err := e.enterRegions(state, others, false); err != nil { return fmt.Errorf("enter state %s: %w", state.Name, err) diff --git a/internal/exec/runtime/state_region_transition.go b/internal/exec/runtime/state_region_transition.go index 0948e161fb..3a25219a3f 100644 --- a/internal/exec/runtime/state_region_transition.go +++ b/internal/exec/runtime/state_region_transition.go @@ -86,7 +86,7 @@ func (e *StateExecutor) enterToward(lca, target *ast.StateNode, branches map[*as return enter, enter, nil } // Entering enter's regions entered target and its start; record the deepest. - return enter, e.activeConfig.regionStates[e.enclosingRegion(target)], nil + return enter, e.regionActiveState(e.enclosingRegion(target)), nil } deepest, err = e.enterStartOf(target) return deepest, deepest, err @@ -103,13 +103,13 @@ func (e *StateExecutor) activeLeavesBelow(state *ast.StateNode) []*ast.StateNode leaves := make([]*ast.StateNode, 0, len(regions)) waiting := false for _, region := range regions { - if _, active := e.activeConfig.regionStates[region]; !active && e.regionWaitingAtJoin(region) { + if !e.regionIsActive(region) && e.regionWaitingAtJoin(region) { waiting = true break } } for _, region := range regions { - active, ok := e.activeConfig.regionStates[region] + active, ok := e.regionActive(region) if !ok { if waiting { continue @@ -163,7 +163,7 @@ func (e *StateExecutor) fireTransitionInRegion(region *ast.StateRegion, trans *l return false, fmt.Errorf("transition out of region %s has no target state", region.Name) } - source := e.activeConfig.regionStates[region] + source := e.regionActiveState(region) return true, e.travel(trans, source, r, func(target *ast.StateNode) []*ast.StateNode { return e.exitedInRegion(region, trans, target) }, func(target *ast.StateNode) []*ast.StateNode { return e.enteredInRegion(region, trans, target) }, @@ -254,7 +254,7 @@ func (e *StateExecutor) siblingRegionContaining(region *ast.StateRegion, state * // the path to target, which stay active, and returns the region the move happens in. func (e *StateExecutor) innermostActiveRegion(region *ast.StateRegion, target *ast.StateNode) *ast.StateRegion { for { - active, ok := e.activeConfig.regionStates[region] + active, ok := e.regionActive(region) if !ok || active == target { return region } @@ -353,7 +353,7 @@ func (e *StateExecutor) moveBetweenRegions( // of a source enclosing the target (an external transition even inside a region), // or the region's owner when nothing inside it is shared. func (e *StateExecutor) regionKeep(targetRegion *ast.StateRegion, trans *lower.Transition, target *ast.StateNode) *ast.StateNode { - keep := e.getLCA(e.activeConfig.regionStates[targetRegion], target) + keep := e.getLCA(e.regionActiveState(targetRegion), target) if declared, isState := trans.Source.(*ast.StateNode); isState && e.encloses(declared, target) { keep = e.graph.ParentState[declared] } @@ -375,7 +375,7 @@ func (e *StateExecutor) regionKeeps(region *ast.StateRegion, state *ast.StateNod // regionExitPath lists the states exitRegionTo exits, innermost first: region's // active state and its ancestors up to stop, within the region. func (e *StateExecutor) regionExitPath(region *ast.StateRegion, stop *ast.StateNode) []*ast.StateNode { - active, ok := e.activeConfig.regionStates[region] + active, ok := e.regionActive(region) if !ok { return nil } @@ -386,12 +386,11 @@ func (e *StateExecutor) regionExitPath(region *ast.StateRegion, stop *ast.StateN // which stays active; a nil stop leaves the region entirely, up to its own // boundary. The region is left without an active state. func (e *StateExecutor) exitRegionTo(region *ast.StateRegion, stop *ast.StateNode) error { - if _, ok := e.activeConfig.regionStates[region]; !ok { + if !e.regionIsActive(region) { return nil } path := e.regionExitPath(region, stop) - delete(e.activeConfig.regionStates, region) - delete(e.activeConfig.defaultComplete, region) + e.deactivateRegion(region) if stop == nil { // The region keeps no active state, so it has none to restore either. e.forgetRegionHistory(region) @@ -409,7 +408,7 @@ func (e *StateExecutor) exitRegionTo(region *ast.StateRegion, stop *ast.StateNod // whole set is left: every sibling region is exited, recording its configuration // for history, before the target is entered. func (e *StateExecutor) leaveRegion(region *ast.StateRegion, trans *lower.Transition, effects []routeEffect, target *ast.StateNode) error { - source := e.activeConfig.regionStates[region] + source := e.regionActiveState(region) owner := e.graph.RegionOwner[region] if owner == nil { return e.leaveTopRegions(trans, effects, source, target) @@ -455,10 +454,9 @@ func (e *StateExecutor) exitRegionOwnerTo(owner, lca *ast.StateNode) error { // Clear the region current is active in first — a region's active state may // be nested below current — or an enclosing state exits current again. if declaring := e.enclosingRegion(current); declaring != nil { - if active, isActive := e.activeConfig.regionStates[declaring]; isActive && e.isBelowOrEqual(active, current) { + if active, isActive := e.regionActive(declaring); isActive && e.isBelowOrEqual(active, current) { e.recordRegionHistory(declaring, active) - delete(e.activeConfig.regionStates, declaring) - delete(e.activeConfig.defaultComplete, declaring) + e.deactivateRegion(declaring) } } if err := e.exitState(current); err != nil { @@ -474,12 +472,11 @@ func (e *StateExecutor) exitRegionOwnerTo(owner, lca *ast.StateNode) error { func (e *StateExecutor) leaveTopRegions(trans *lower.Transition, effects []routeEffect, source, target *ast.StateNode) error { var units []unit for _, region := range e.graph.TopRegions { - active, ok := e.activeConfig.regionStates[region] + active, ok := e.regionActive(region) if !ok { continue } - delete(e.activeConfig.regionStates, region) - delete(e.activeConfig.defaultComplete, region) + e.deactivateRegion(region) leaving := make([]*ast.StateNode, 0) for current := active; current != nil; current = e.graph.ParentState[current] { leaving = append(leaving, current) @@ -489,9 +486,7 @@ func (e *StateExecutor) leaveTopRegions(trans *lower.Transition, effects []route if err := e.performUnits(choiceExitOrder, exitingWherePrefix+e.stateMachine.Name, units, true); err != nil { return err } - e.activeConfig.regionStates = make(map[*ast.StateRegion]*ast.StateNode) - e.activeConfig.defaultComplete = make(map[*ast.StateRegion]bool) - e.activeConfig.simpleState = nil + e.activeConfig.reset() if err := e.runEffects(effects, e.descendantChain(nil, target)); err != nil { return err @@ -519,10 +514,10 @@ func (e *StateExecutor) enterOutside(trans *lower.Transition, source, lca, targe e.setRegionState(region, leaf) } if len(onPath) == 0 && len(e.activeConfig.regionStates) == 0 { - e.activeConfig.simpleState = deepest + e.setSimpleState(deepest) } - e.stateStack = e.rootToLeaf(deepest) + e.stateStack = e.stateIDs(e.rootToLeaf(deepest)) if err := e.scheduleFromEntered(entered); err != nil { return err } @@ -564,13 +559,13 @@ func (e *StateExecutor) recordTransitionTrace(trans *lower.Transition, source, t func (e *StateExecutor) orderedActiveRegions() []*ast.StateRegion { regions := make([]*ast.StateRegion, 0, len(e.activeConfig.regionStates)) for _, region := range e.graph.TopRegions { - if _, active := e.activeConfig.regionStates[region]; active { + if e.regionIsActive(region) { regions = append(regions, region) } } for _, state := range e.graph.States { for _, region := range e.graph.CompositeStates[state] { - if _, active := e.activeConfig.regionStates[region]; active { + if e.regionIsActive(region) { regions = append(regions, region) } } diff --git a/internal/exec/runtime/state_route.go b/internal/exec/runtime/state_route.go index 9c9c7b958d..ea10d08708 100644 --- a/internal/exec/runtime/state_route.go +++ b/internal/exec/runtime/state_route.go @@ -7,6 +7,7 @@ import ( "slices" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -29,20 +30,20 @@ func transientPseudostate(kind ast.PseudostateKind) bool { // history restoring. type route struct { segments []*lower.Transition - target *ast.StateNode - choice *ast.PseudostateNode + target program.StateID + choice program.PseudoID draw *junctionDraw - arrival *ast.PseudostateNode + arrival program.PseudoID // crossed are the pseudostates passed, so a path back into one is a cycle. - crossed []*ast.PseudostateNode - // origin is the state the transition routed left, nil when none (an entry). - origin *ast.StateNode + crossed []program.PseudoID + // origin is the state the transition routed left, 0 when none (an entry). + origin program.StateID // notes is what settling the route noted — a branch guard it could not read, // a junction drawn among several enabled — recorded once the transition is taken. notes []RunNote // terminate is the terminate action usage the route ends at, which ends the // machine's performance in place of entering a state (SysML v2 §7.18.3). - terminate *ast.Usage + terminate program.VertexRef // performs are the behaviors of the terminate action usages naming an // occurrence the route ends at, run after the segments' effects before the // completion vertex target is entered (SysML v2 §8.3.17.16). @@ -55,10 +56,10 @@ type route struct { // committed to fire, so a transition another region's reaction leaves behind // draws nothing. type junctionDraw struct { - at *ast.PseudostateNode + at program.PseudoID outgoing []*lower.Transition enabled []int - origin *ast.StateNode + origin program.StateID // beyond is the route on from each enabled branch, in enabled's order. beyond []branchBeyond } @@ -72,22 +73,22 @@ type branchBeyond struct { // settled reports whether the route has somewhere to move to. func (r route) settled() bool { - return r.target != nil || r.choice != nil || r.draw != nil || r.arrival != nil || r.terminate != nil + return r.target != 0 || r.choice != 0 || r.draw != nil || r.arrival != 0 || !r.terminate.IsZero() } // routeEffect is one effect of a compound transition and the state declaring the // pseudostate its segment leaves; nil for a segment out of a state or the machine's body. type routeEffect struct { behavior lower.StateBehavior - within *ast.StateNode + within program.StateID segment *lower.Transition } -func cloneRouteEntryEffects(effects map[*ast.StateRegion][]routeEffect) map[*ast.StateRegion][]routeEffect { +func cloneRouteEntryEffects(effects map[program.RegionID][]routeEffect) map[program.RegionID][]routeEffect { if effects == nil { return nil } - cloned := make(map[*ast.StateRegion][]routeEffect, len(effects)) + cloned := make(map[program.RegionID][]routeEffect, len(effects)) for region, routeEffects := range effects { cloned[region] = slices.Clone(routeEffects) } @@ -96,12 +97,12 @@ func cloneRouteEntryEffects(effects map[*ast.StateRegion][]routeEffect) map[*ast // effects are the behaviors the route's segments perform, in path order, each // with the state enclosing it. -func (r route) effects(g *lower.StateGraph) []routeEffect { +func (r route) effects(e *StateExecutor) []routeEffect { var effects []routeEffect for _, seg := range r.segments { - var within *ast.StateNode + var within program.StateID if ps, isPseudostate := seg.Source.(*ast.PseudostateNode); isPseudostate { - within = g.PseudostateOwner[ps] + within = e.stateID(e.graph.PseudostateOwner[ps]) } for _, behavior := range seg.Effect { effects = append(effects, routeEffect{behavior: behavior, within: within, segment: seg}) @@ -120,10 +121,11 @@ func (r route) effects(g *lower.StateGraph) []routeEffect { func (e *StateExecutor) resolveRoute(trans *lower.Transition, event *Event) (route, error) { defer e.taking(trans, e.firingNotes).restore() r := route{segments: []*lower.Transition{trans}} - r.origin, _ = trans.Source.(*ast.StateNode) + origin, _ := trans.Source.(*ast.StateNode) + r.origin = e.stateID(origin) switch target := trans.Target.(type) { case *ast.StateNode: - r.target = target + r.target = e.stateID(target) return r, nil case *ast.PseudostateNode: switch target.Kind { @@ -135,7 +137,7 @@ func (e *StateExecutor) resolveRoute(trans *lower.Transition, event *Event) (rou return route{}, err } if !completes { - r.arrival = target + r.arrival = e.pseudoID(target) return r, nil } } @@ -159,12 +161,12 @@ func (e *StateExecutor) resolveRoute(trans *lower.Transition, event *Event) (rou // as KerML `DecisionPerformance::outgoingHBLink: HappensBefore[1]` requires. // A route that cannot be settled is returned as far as it got, with its notes. func (e *StateExecutor) followOut(ps *ast.PseudostateNode, r route) (route, error) { - if slices.Contains(r.crossed, ps) { + if slices.Contains(r.crossed, e.pseudoID(ps)) { return r, fmt.Errorf("%s %s: outgoing transitions form a cycle between pseudostates", ps.Kind, ps.Name) } - r.crossed = append(r.crossed, ps) + r.crossed = append(r.crossed, e.pseudoID(ps)) if ps.Kind == ast.PseudostateChoice { - r.choice = ps + r.choice = e.pseudoID(ps) return r, nil } outgoing := e.graph.Transitions[ps] @@ -180,7 +182,7 @@ func (e *StateExecutor) followOut(ps *ast.PseudostateNode, r route) (route, erro return r, fmt.Errorf("%w: %s %s: no guard evaluated to true", errNoWayThrough, ps.Kind, ps.Name) } if len(enabled) > 1 { - draw := &junctionDraw{at: ps, outgoing: outgoing, origin: r.origin} + draw := &junctionDraw{at: e.pseudoID(ps), outgoing: outgoing, origin: r.origin} for _, pos := range enabled { beyond, err := e.follow(ps, outgoing[pos], route{crossed: slices.Clone(r.crossed), origin: r.origin}) if errors.Is(err, errNoWayThrough) { @@ -256,7 +258,7 @@ func (e *StateExecutor) settleDraws(r route) (route, error) { for r.draw != nil { draw := r.draw r.draw = nil - pick, err := e.pickBranch(draw.at, draw.outgoing, draw.enabled, draw.origin, func(n RunNote) { r.notes = append(r.notes, n) }) + pick, err := e.pickBranch(e.pseudoDecl(draw.at), draw.outgoing, draw.enabled, e.stateDecl(draw.origin), func(n RunNote) { r.notes = append(r.notes, n) }) if err != nil { return route{}, err } @@ -276,10 +278,10 @@ func (e *StateExecutor) settleDraws(r route) (route, error) { func (e *StateExecutor) terminateRoute(r route, stop *ast.Usage) route { if behavior, named := e.graph.TerminateBehaviors[stop]; named { r.performs = append(r.performs, behavior) - r.target = e.graph.TerminateCompletion[stop] + r.target = e.stateID(e.graph.TerminateCompletion[stop]) return r } - r.terminate = stop + r.terminate = e.vertexRef(stop) return r } @@ -299,7 +301,7 @@ func (e *StateExecutor) follow(from *ast.PseudostateNode, seg *lower.Transition, r.segments = append(r.segments, seg) switch target := seg.Target.(type) { case *ast.StateNode: - r.target = target + r.target = e.stateID(target) return r, nil case *ast.PseudostateNode: if !transientPseudostate(target.Kind) { @@ -318,7 +320,7 @@ func (e *StateExecutor) follow(from *ast.PseudostateNode, seg *lower.Transition, // data as it now stands and goes on from the branch taken: the policy draws among // several enabled, as a recorded choice point; none enabled is the typed error. func (e *StateExecutor) resolveChoice(r route) (route, error) { - choice := r.choice + choice := e.pseudoDecl(r.choice) outgoing := e.graph.Transitions[choice] enabled, notes, err := e.enabledBranches(choice, outgoing) if err != nil { @@ -328,7 +330,7 @@ func (e *StateExecutor) resolveChoice(r route) (route, error) { if len(enabled) == 0 { return route{}, fmt.Errorf("%w: choice %s: no guard evaluated to true", errChoiceWithoutBranch, choice.Name) } - pick, err := e.pickBranch(choice, outgoing, enabled, r.origin, e.ctx.note) + pick, err := e.pickBranch(choice, outgoing, enabled, e.stateDecl(r.origin), e.ctx.note) if err != nil { return route{}, err } @@ -488,9 +490,9 @@ func pseudostateWhere(ps *ast.PseudostateNode) string { // junction enabled (one that could not be settled ends nowhere), or through every // branch of the choice and whatever pseudostates lie beyond, each once. func (e *StateExecutor) reachable(r route) (states []*ast.StateNode, stops []*ast.Usage, err error) { - reach := &reachSet{graph: e.graph, seen: make(map[*ast.PseudostateNode]bool)} + reach := &reachSet{exec: e, graph: e.graph, seen: make(map[*ast.PseudostateNode]bool)} for _, ps := range r.crossed { - reach.seen[ps] = true + reach.seen[e.pseudoDecl(ps)] = true } err = reach.settled(r) return reach.states, reach.stops, err @@ -499,6 +501,7 @@ func (e *StateExecutor) reachable(r route) (states []*ast.StateNode, stops []*as // reachSet gathers the states and terminate actions a route can end at, each // once, crossing each transient pseudostate once. type reachSet struct { + exec *StateExecutor graph *lower.StateGraph seen map[*ast.PseudostateNode]bool states []*ast.StateNode @@ -549,12 +552,12 @@ func (s *reachSet) visit(ps *ast.PseudostateNode, branches []*lower.Transition) // settled follows the route where it is settled, and every branch of the // choice where it is not. func (s *reachSet) settled(r route) error { - if r.target != nil { - s.add(r.target) + if r.target != 0 { + s.add(s.exec.stateDecl(r.target)) return nil } - if r.terminate != nil { - s.stop(r.terminate) + if !r.terminate.IsZero() { + s.stop(s.exec.terminateDecl(r.terminate)) return nil } if r.draw != nil { @@ -568,7 +571,8 @@ func (s *reachSet) settled(r route) error { } return nil } - return s.visit(r.choice, s.graph.Transitions[r.choice]) + choice := s.exec.pseudoDecl(r.choice) + return s.visit(choice, s.graph.Transitions[choice]) } // terminateBoundary is the state a move from `from` into stop's owner stays inside @@ -665,7 +669,7 @@ func (e *StateExecutor) runEffects(effects []routeEffect, chain []*ast.StateNode if e.endedBefore(ended, effect.behavior) { continue } - if upto := e.enclosingIndex(chain, effect.within); upto >= 0 { + if upto := e.enclosingIndex(chain, e.stateDecl(effect.within)); upto >= 0 { if err := e.enterAhead(chain[:upto+1]); err != nil { return err } @@ -718,7 +722,7 @@ func (e *StateExecutor) enterAhead(chain []*ast.StateNode) error { simple := e.activeConfig.simpleState defer func() { e.activeConfig.simpleState = simple }() for _, state := range chain { - if _, ahead := e.enteredAhead[state]; ahead { + if _, ahead := e.enteredAhead[e.stateID(state)]; ahead { continue } if e.entryIsUnit(state) { @@ -730,19 +734,19 @@ func (e *StateExecutor) enterAhead(chain []*ast.StateNode) error { return fmt.Errorf("enter state %s: %w", state.Name, err) } if e.enteredAhead == nil { - e.enteredAhead = make(map[*ast.StateNode]bool) + e.enteredAhead = make(map[program.StateID]bool) } - e.enteredAhead[state] = true + e.enteredAhead[e.stateID(state)] = true } return nil } // activatedAhead reports, and takes, an activation the move already made ahead. func (e *StateExecutor) activatedAhead(state *ast.StateNode) bool { - if !e.enteredAhead[state] { + if !e.enteredAhead[e.stateID(state)] { return false } - e.enteredAhead[state] = false + e.enteredAhead[e.stateID(state)] = false return true } @@ -750,7 +754,7 @@ func (e *StateExecutor) activatedAhead(state *ast.StateNode) bool { func (e *StateExecutor) entriesSettled() error { for state, waiting := range e.enteredAhead { if waiting { - return fmt.Errorf("state %s was entered ahead of the transition but the transition does not enter it", state.Name) + return fmt.Errorf("state %s was entered ahead of the transition but the transition does not enter it", e.stateDecl(state).Name) } } return nil @@ -784,13 +788,10 @@ func (e *StateExecutor) exitAhead(states []*ast.StateNode) error { return nil } simple, regions, complete := e.activeConfig.simpleState, e.activeConfig.regionStates, e.activeConfig.defaultComplete - e.activeConfig.regionStates = make(map[*ast.StateRegion]*ast.StateNode, len(regions)) + e.activeConfig.regionStates = maps.Clone(regions) e.activeConfig.defaultComplete = maps.Clone(complete) - for region, active := range regions { - e.activeConfig.regionStates[region] = active - } if e.leftAhead == nil { - e.leftAhead = make(map[*ast.StateNode]bool) + e.leftAhead = make(map[program.StateID]bool) } e.exitingAhead = true err := e.exitStates(states) @@ -846,7 +847,7 @@ func (e *StateExecutor) travelResolving(trans *lower.Transition, from *ast.State return err } } - for r.choice != nil { + for r.choice != 0 { ends, err := e.endsOf(r, trans, from, exits, enters) if err != nil { return err @@ -858,7 +859,7 @@ func (e *StateExecutor) travelResolving(trans *lower.Transition, from *ast.State if err := e.exitAhead(certainExits); err != nil { return err } - if err := e.enterOwnerOf(r.choice, certainEntries); err != nil { + if err := e.enterOwnerOf(e.pseudoDecl(r.choice), certainEntries); err != nil { return err } e.noteFired(r.segments...) @@ -867,27 +868,28 @@ func (e *StateExecutor) travelResolving(trans *lower.Transition, from *ast.State } } var leaving, entering []*ast.StateNode - if r.terminate != nil { - leaving = e.expandExits(e.terminateExits(from, trans, r.terminate)) - entering = e.terminateEntries(from, trans, r.terminate) + if stop := e.terminateDecl(r.terminate); stop != nil { + leaving = e.expandExits(e.terminateExits(from, trans, stop)) + entering = e.terminateEntries(from, trans, stop) } else { - leaving, entering = e.expandExits(exits(r.target)), enters(r.target) + target := e.stateDecl(r.target) + leaving, entering = e.expandExits(exits(target)), enters(target) } effects, err := e.leaveAlong(r, leaving, entering, r.segments[len(r.segments)-1]) if err != nil { return err } e.noteFired(r.segments...) - if r.terminate != nil { + if !r.terminate.IsZero() { return e.terminateAlong(trans, from, r, effects) } - return move(effects, r.target) + return move(effects, e.stateDecl(r.target)) } // leaveAlong runs the segments before upto, exiting what each has left before its effects // (UML 14.2.3.8.4), and returns the effects from upto on, which run after the move's exits. func (e *StateExecutor) leaveAlong(r route, leaving, entering []*ast.StateNode, upto *lower.Transition) ([]routeEffect, error) { - effects := r.effects(e.graph) + effects := r.effects(e) var left []*ast.StateNode for _, seg := range r.segments { if seg == upto { @@ -964,10 +966,10 @@ func (e *StateExecutor) vertexState(end ast.Node) *ast.StateNode { // (SysML v2 §7.18.3): the states the move leaves are exited and the ones down to // the action's owner entered, as for a state beside it, then the machine ends. func (e *StateExecutor) terminateAlong(trans *lower.Transition, from *ast.StateNode, r route, effects []routeEffect) error { - if err := e.exitStates(e.terminateExits(from, trans, r.terminate)); err != nil { + if err := e.exitStates(e.terminateExits(from, trans, e.terminateDecl(r.terminate))); err != nil { return err } - return e.terminateAt(trans, StateVertexName(from), r, effects, e.terminateEntries(from, trans, r.terminate)) + return e.terminateAt(trans, StateVertexName(from), r, effects, e.terminateEntries(from, trans, e.terminateDecl(r.terminate))) } // terminateAt ends the machine's performance at the terminate action r reaches, @@ -983,7 +985,7 @@ func (e *StateExecutor) terminateAt(trans *lower.Transition, fromName string, r } // The machine ends here: the entries made ahead are the move's own. clear(e.enteredAhead) - return e.terminateMachine(fromName, trans, r.terminate) + return e.terminateMachine(fromName, trans, e.terminateDecl(r.terminate)) } // runBehaviors performs a transition's effects, in order. @@ -997,11 +999,11 @@ func (e *StateExecutor) runBehaviors(effects []lower.StateBehavior) error { // mayExit lists the states a compound transition along r from `from` may leave, // whichever end it can reach; false where the route cannot be followed. func (e *StateExecutor) mayExit(r route, trans *lower.Transition, from *ast.StateNode, exits exitPlan) ([]*ast.StateNode, bool) { - if r.target != nil { - return exits(r.target), true + if r.target != 0 { + return exits(e.stateDecl(r.target)), true } - if r.terminate != nil { - return e.terminateExits(from, trans, r.terminate), true + if stop := e.terminateDecl(r.terminate); stop != nil { + return e.terminateExits(from, trans, stop), true } targets, stops, err := e.reachable(r) if err != nil { diff --git a/internal/exec/runtime/state_run_to_completion.go b/internal/exec/runtime/state_run_to_completion.go index d4777cdcb8..eb8e8d5421 100644 --- a/internal/exec/runtime/state_run_to_completion.go +++ b/internal/exec/runtime/state_run_to_completion.go @@ -5,6 +5,7 @@ import ( "slices" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -18,21 +19,21 @@ func (e *StateExecutor) HoldsEntry() bool { return len(e.held) > 0 } // heldEntry is the unfinished portion of a state entry cascade. type heldEntry struct { // owner is the composite body whose entry is paused. - owner *ast.StateNode + owner program.StateID // regions are orthogonal regions still awaiting entry. - regions []*ast.StateRegion + regions []program.RegionID // branches maps each held region to its selected branch. - branches map[*ast.StateRegion]*ast.StateNode + branches map[program.RegionID]program.StateID // chain lists states entered by this cascade, outermost first. - chain []*ast.StateNode + chain []program.StateID // scopes are the RTC scopes guarding dispatch while this cascade is held. - scopes []*ast.StateNode + scopes []program.StateID // machine reports that the machine body is part of this cascade. machine bool // firing is the transition whose entry the cascade is, with the payload it // bound; the entries and do behaviors performed on resumption read it. firing *firing - routeEffects map[*ast.StateRegion][]routeEffect + routeEffects map[program.RegionID][]routeEffect // awaits is the owner's entry behavior parked for a message, after whose end the // cascade goes on below the owner; nil for one paused at a run-to-completion boundary. awaits *doAction @@ -59,7 +60,7 @@ func (e *StateExecutor) readyHeld() []heldEntry { // for a message, its body yet to be entered. func (e *StateExecutor) entryParked(state *ast.StateNode) bool { for _, item := range e.held { - if item.awaits != nil && item.owner == state { + if item.awaits != nil && item.owner == e.stateID(state) { return true } } @@ -70,8 +71,8 @@ func (e *StateExecutor) entryParked(state *ast.StateNode) bool { // is parked for a message; resumeParkedEntries enters owner's body after its end. func (e *StateExecutor) holdParkedEntry(owner *ast.StateNode, act *doAction, regions []*ast.StateRegion, branches map[*ast.StateRegion]*ast.StateNode) { e.held = append(e.held, heldEntry{ - owner: owner, regions: regions, branches: branches, - chain: e.enteringChain(owner), scopes: []*ast.StateNode{owner}, machine: owner == nil && e.enteringMachine, + owner: e.stateID(owner), regions: e.regionIDs(regions), branches: e.regionStateIDs(branches), + chain: e.stateIDs(e.enteringChain(owner)), scopes: []program.StateID{e.stateID(owner)}, machine: owner == nil && e.enteringMachine, firing: e.currentFiring(), routeEffects: cloneRouteEntryEffects(e.pendingRouteEntryEffects), awaits: act, }) @@ -139,7 +140,7 @@ func (e *StateExecutor) enteringChain(owner *ast.StateNode) []*ast.StateNode { } var chain []*ast.StateNode for _, state := range e.rootToLeaf(owner) { - if e.entering[state] { + if e.entering[e.stateID(state)] { chain = append(chain, state) } } @@ -153,7 +154,7 @@ func (e *StateExecutor) holdEntry(owner *ast.StateNode, regions []*ast.StateRegi if !machine && len(chain) == 0 { return false, nil } - if owner != nil && e.activeAtEntry[owner] && e.entering[owner] { + if owner != nil && e.activeAtEntry[e.stateID(owner)] && e.entering[e.stateID(owner)] { return false, nil } members := slices.Clone(chain) @@ -177,8 +178,8 @@ func (e *StateExecutor) holdEntry(owner *ast.StateNode, regions []*ast.StateRegi } } e.held = append(e.held, heldEntry{ - owner: owner, regions: regions, branches: branches, - chain: chain, scopes: scopes, machine: machine, + owner: e.stateID(owner), regions: e.regionIDs(regions), branches: e.regionStateIDs(branches), + chain: e.stateIDs(chain), scopes: e.stateIDs(scopes), machine: machine, firing: e.currentFiring(), routeEffects: cloneRouteEntryEffects(e.pendingRouteEntryEffects), }) return true, nil @@ -186,7 +187,7 @@ func (e *StateExecutor) holdEntry(owner *ast.StateNode, regions []*ast.StateRegi func (e *StateExecutor) heldOwner(state *ast.StateNode) *heldEntry { for i := range e.held { - if e.held[i].owner == state { + if e.held[i].owner == e.stateID(state) { return &e.held[i] } } @@ -204,13 +205,14 @@ func (e *StateExecutor) performHeld(item heldEntry) (err error) { e.entering[state] = true } e.enteringMachine = item.machine - begun := append(slices.Clone(item.chain), item.owner) + owner := e.stateDecl(item.owner) + begun := append(e.stateDecls(item.chain), owner) if item.machine { begun = append(begun, e.graph.Machine) } e.resumeEntering(begun) defer e.unfireOnError(len(e.fired), &err) - if item.owner == nil && item.awaits != nil { + if owner == nil && item.awaits != nil { // The machine's own entry was parked: its body starts now. if len(e.graph.TopRegions) == 0 { return e.enterMachineStart() @@ -219,14 +221,14 @@ func (e *StateExecutor) performHeld(item heldEntry) (err error) { } if item.regions != nil { if e.activeConfig.simpleState == item.owner { - e.activeConfig.simpleState = nil + e.activeConfig.simpleState = 0 } - if err = e.enterRegionsInto(item.owner, item.regions, item.branches); err == nil { - err = e.settleEntered(item.owner) + if err = e.enterRegionsInto(owner, e.regionDecls(item.regions), e.regionStateDecls(item.branches)); err == nil { + err = e.settleEntered(owner) } } else { var leaf *ast.StateNode - leaf, err = e.enterStartOf(item.owner) + leaf, err = e.enterStartOf(owner) if err == nil && e.state != StateTerminated { err = e.settleEntered(leaf) } @@ -263,9 +265,9 @@ func (e *StateExecutor) settleEntered(leaf *ast.StateNode) error { e.setRegionState(region, state) } if len(onPath) == 0 && len(e.activeConfig.regionStates) == 0 { - e.activeConfig.simpleState = leaf + e.setSimpleState(leaf) } - e.stateStack = e.rootToLeaf(leaf) + e.stateStack = e.stateIDs(e.rootToLeaf(leaf)) if err := e.scheduleTransitionEvents(); err != nil { return fmt.Errorf("schedule events: %w", err) } @@ -290,7 +292,7 @@ func (e *StateExecutor) entryStep(progress *dueProgress) (bool, error) { fps = append(fps, dispatchFootprint(e)) } for _, item := range ready { - alternatives = append(alternatives, e.entryLabel(item.owner)) + alternatives = append(alternatives, e.entryLabel(e.stateDecl(item.owner))) fps = append(fps, e.heldEntryFootprint(item)) } if len(alternatives) == 1 { @@ -342,7 +344,7 @@ func (e *StateExecutor) takeHeld(item heldEntry) heldEntry { func (e *StateExecutor) heldScopes() []*ast.StateNode { var scopes []*ast.StateNode for _, item := range e.held { - scopes = append(scopes, item.scopes...) + scopes = append(scopes, e.stateDecls(item.scopes)...) } return scopes } @@ -434,7 +436,7 @@ func (e *StateExecutor) eventHeld(scopes []*ast.StateNode, event Event) (bool, e } for _, candidate := range candidates { for _, index := range candidate.enabled { - if scopeContains(e.graph, scopes, e.graph.Transitions[candidate.source][index].Owner) { + if scopeContains(e.graph, scopes, e.graph.Transitions[e.stateDecl(candidate.source)][index].Owner) { return true, nil } } diff --git a/internal/exec/runtime/state_statements.go b/internal/exec/runtime/state_statements.go index 3ab16be4db..540d2a2f07 100644 --- a/internal/exec/runtime/state_statements.go +++ b/internal/exec/runtime/state_statements.go @@ -5,6 +5,7 @@ import ( "slices" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -74,12 +75,12 @@ func (e *StateExecutor) executeEntryBehaviors(state *ast.StateNode, behaviors [] return nil, nil, nil } if e.performing == nil { - e.performing = make(map[*ast.StateNode]int) + e.performing = make(map[program.StateID]int) } - e.performing[state]++ + e.performing[e.stateID(state)]++ defer func() { - if e.performing[state]--; e.performing[state] == 0 { - delete(e.performing, state) + if e.performing[e.stateID(state)]--; e.performing[e.stateID(state)] == 0 { + delete(e.performing, e.stateID(state)) } }() var ended []ast.Node @@ -483,8 +484,8 @@ func (h *stateStmtHost) assignStateAttribute(name string, value Value) (bool, er declared := name if h.exec.holdsShadowed() { for _, ancestor := range h.exec.getParentChain(h.behavior.Owner) { - if _, held := h.exec.stateAttrs[ancestor]; held { - _, declared = declarationHeldUnder(h.exec.stateAttrIndex[ancestor], scope, name) + if _, held := h.exec.stateAttrs[h.exec.stateID(ancestor)]; held { + _, declared = declarationHeldUnder(h.exec.stateAttrIndex[h.exec.stateID(ancestor)], scope, name) if declared != name { break } diff --git a/internal/exec/runtime/state_unit_footprint.go b/internal/exec/runtime/state_unit_footprint.go index fdffcf0936..a7490e8d13 100644 --- a/internal/exec/runtime/state_unit_footprint.go +++ b/internal/exec/runtime/state_unit_footprint.go @@ -59,7 +59,7 @@ func (e *StateExecutor) regionEntryFootprint(entry *regionEntry) lower.Footprint } transitions := e.graph.TransitionFootprints() var regions []*ast.StateRegion - for regions = append(regions, entry.region); len(regions) > 0; { + for regions = append(regions, e.regionDecl(entry.region)); len(regions) > 0; { region := regions[len(regions)-1] regions = regions[:len(regions)-1] if owner := e.graph.RegionState[region]; owner != nil { diff --git a/internal/exec/runtime/state_unit_front.go b/internal/exec/runtime/state_unit_front.go index c636fb1cc2..b7e0e7d12e 100644 --- a/internal/exec/runtime/state_unit_front.go +++ b/internal/exec/runtime/state_unit_front.go @@ -7,6 +7,7 @@ import ( "slices" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -157,7 +158,7 @@ type unitHead struct { label string at ast.Node site string - shared *ast.StateNode + shared program.StateID dropped func() bool until func() bool void func() bool @@ -179,11 +180,11 @@ type pathDraw struct { type firingScope struct { event *Event taken *lower.Transition - change *lower.Transition + change program.TransID notes []RunNote - leftAhead map[*ast.StateNode]bool + leftAhead map[program.StateID]bool exitingAhead bool - enteredAhead map[*ast.StateNode]bool + enteredAhead map[program.StateID]bool moving *moveMark // bound are the machine's data the firing bound, installed while its units // run; shadowed what the data held under each while installed. @@ -392,13 +393,13 @@ func (f *unitFront) offer(act *doAction) { return } var q *unitQueue - q = f.spawnAt(f.doStepHead(act), unit{run: func() error { return f.offerDoSteps(q, act) }, footprint: doStepFootprint(f.exec, act.state)}) + q = f.spawnAt(f.doStepHead(act), unit{run: func() error { return f.offerDoSteps(q, act) }, footprint: doStepFootprint(f.exec, f.exec.stateDecl(act.state))}) q.head.void = func() bool { return !f.offering(q, act) } } // doStepHead is the unit moving one token of the do behavior. func (f *unitFront) doStepHead(act *doAction) unitHead { - return unitHead{label: doStepLabel([]string{f.exec.stateName(act.state)}), at: act.state, doStep: true} + return unitHead{label: doStepLabel([]string{f.exec.stateName(f.exec.stateDecl(act.state))}), at: f.exec.stateDecl(act.state), doStep: true} } // add places a queue for body in the front, before the running queue. @@ -516,8 +517,8 @@ func (e *StateExecutor) drawOnPath(head unitHead) error { alternatives := make([]string, 0, len(due)+1) fps := make([]lower.Footprint, 0, len(due)+1) for _, act := range due { - alternatives = append(alternatives, doStepLabel([]string{e.stateName(act.state)})) - fps = append(fps, doStepFootprint(e, act.state)) + alternatives = append(alternatives, doStepLabel([]string{e.stateName(e.stateDecl(act.state))})) + fps = append(fps, doStepFootprint(e, e.stateDecl(act.state))) } alternatives = append(alternatives, head.label) fps = append(fps, e.headFootprint(head)) @@ -531,7 +532,7 @@ func (e *StateExecutor) drawOnPath(head unitHead) error { choice.Taken = pick at := head.at if pick < len(due) { - at = due[pick].state + at = e.stateDecl(due[pick].state) } if at != nil { choice.Span = at.Span() @@ -714,7 +715,7 @@ func (f *unitFront) ready() []*unitQueue { if q.done || q.head.until != nil || (q.head.void != nil && q.head.void()) { continue } - if q.head.shared != nil && slices.ContainsFunc(ready, func(r *unitQueue) bool { return r.head.shared == q.head.shared }) { + if q.head.shared != 0 && slices.ContainsFunc(ready, func(r *unitQueue) bool { return r.head.shared == q.head.shared }) { continue } ready = append(ready, q) diff --git a/internal/frontend/lsp/debug.go b/internal/frontend/lsp/debug.go index c644dbbe4d..cc34cecea9 100644 --- a/internal/frontend/lsp/debug.go +++ b/internal/frontend/lsp/debug.go @@ -1363,10 +1363,11 @@ func (sess *debugSession) machineSnapshot(snap *debugSnapshot) { for _, f := range exec.FiredSince(sess.fired) { var index int var ok bool - if f.Source == nil { - index, ok = sess.states.EntryTransition(f.Decl, f.Owner, f.Target) + decl, source, target, owner := exec.FiredNodes(f) + if !f.Entry.IsZero() { + index, ok = sess.states.EntryTransition(decl, owner, target) } else { - index, ok = sess.states.Transition(f.Decl, f.Source, f.Target) + index, ok = sess.states.Transition(decl, source, target) } if ok { snap.Taken = append(snap.Taken, sess.edgeAt(index)) diff --git a/internal/ir/lower/identity.go b/internal/ir/lower/identity.go index 41bbc6af13..90a54dff58 100644 --- a/internal/ir/lower/identity.go +++ b/internal/ir/lower/identity.go @@ -221,12 +221,41 @@ func (g *StateGraph) assignIDs() { g.assignTransIDs(paths) } +// statesInOrder lists the graph's states once each: States in order, then the +// graph-only states of a parallel body's usages, which parent chains and region tables reach. +func (g *StateGraph) statesInOrder() []*ast.StateNode { + seen := make(map[*ast.StateNode]bool, len(g.States)) + out := make([]*ast.StateNode, 0, len(g.States)) + add := func(state *ast.StateNode) { + if state != nil && !seen[state] { + seen[state] = true + out = append(out, state) + } + } + for _, state := range g.States { + add(state) + } + for _, region := range g.TopRegions { + add(g.RegionState[region]) + } + for i := 0; i < len(out); i++ { + for parent := g.ParentState[out[i]]; parent != nil && !seen[parent]; parent = g.ParentState[parent] { + add(parent) + } + for _, region := range g.CompositeStates[out[i]] { + add(g.RegionState[region]) + } + } + return out +} + func (g *StateGraph) assignStateIDs(paths originPaths) { - n := len(g.States) + 1 - g.ID = make(map[*ast.StateNode]program.StateID, len(g.States)) + states := g.statesInOrder() + n := len(states) + 1 + g.ID = make(map[*ast.StateNode]program.StateID, len(states)) g.Origins = make([]program.Origin, 1, n) g.Provenance.states = make([]*ast.StateNode, 1, n) - for _, state := range g.States { + for _, state := range states { if _, seen := g.ID[state]; seen { continue } @@ -260,7 +289,7 @@ func (g *StateGraph) regionsInOrder() []*ast.StateRegion { } } add(g.TopRegions) - for _, state := range g.States { + for _, state := range g.statesInOrder() { add(g.CompositeStates[state]) } return out diff --git a/internal/ir/lower/identity_test.go b/internal/ir/lower/identity_test.go index cb0c2e110d..5b864ad056 100644 --- a/internal/ir/lower/identity_test.go +++ b/internal/ir/lower/identity_test.go @@ -182,7 +182,7 @@ func TestAssignIDs_StateTablesDenseInDeclarationOrder(t *testing.T) { t.Fatalf("fixture lowers %d states, %d pseudostates, %d regions, %d transitions", len(graph.States), len(graph.Pseudostates), len(graph.RegionID), len(graph.TransID)) } - if len(graph.Origins) != len(graph.States)+1 || len(graph.PseudoOrigins) != len(graph.Pseudostates)+1 || + if len(graph.Origins) != len(graph.ID)+1 || len(graph.PseudoOrigins) != len(graph.Pseudostates)+1 || len(graph.RegionOrigins) != len(graph.RegionID)+1 || len(graph.TransOrigins) != len(graph.TransID)+1 { t.Fatal("every origin table is one longer than its records") } @@ -258,6 +258,54 @@ func TestAssignIDs_StateDeterministicAcrossLowerings(t *testing.T) { } } +// The states a parallel body's usages stand for are graph-only (States leaves +// them out) but head parent chains and region tables, so they are numbered too. +func TestAssignIDs_StateHiddenRegionOwnersNumbered(t *testing.T) { + graph, err := ToStateGraph(stateDefinitionIn(t, ` + package test { + state def Machine { + entry; then outer; + state outer parallel { + state a { + entry; then a1; + state a1; + } + state b { + entry; then b1; + state b1; + } + } + } + } + `), nil) + if err != nil { + t.Fatalf("ToStateGraph: %v", err) + } + if len(graph.Origins) != len(graph.ID)+1 || len(graph.ID) != len(graph.States)+2 { + t.Fatalf("%d ids for %d states and two hidden region owners, %d origins", len(graph.ID), len(graph.States), len(graph.Origins)) + } + for i, state := range graph.States { + if graph.ID[state] != program.StateID(i+1) { + t.Errorf("state %s: id %d at %d; want States numbered first", state.Name, graph.ID[state], i) + } + } + for _, name := range []string{"a1", "b1"} { + parent := graph.ParentState[stateNamed(graph, name)] + if parent == nil || !graph.HiddenStates[parent] { + t.Fatalf("%s parent = %v, want hidden region owner", name, parent) + } + id := graph.ID[parent] + if id == 0 || graph.Provenance.State(id) != parent || graph.Origins[id].Name != parent.Name { + t.Errorf("hidden %s: id %d, provenance %v, origin %+v", parent.Name, id, graph.Provenance.State(id), graph.Origins[id]) + } + } + for region, owner := range graph.RegionState { + if graph.ID[owner] == 0 { + t.Errorf("region %s owner %s has no id", region.Name, owner.Name) + } + } +} + func TestAssignIDs_CaseFlowCoversNodesAddedAfterLowering(t *testing.T) { body := caseBodyOf(t, ` analysis def Among { diff --git a/internal/ir/program/vertex.go b/internal/ir/program/vertex.go new file mode 100644 index 0000000000..704acab372 --- /dev/null +++ b/internal/ir/program/vertex.go @@ -0,0 +1,83 @@ +package program + +// VertexKind tells which table of a state graph a VertexRef indexes. +type VertexKind uint8 + +const ( + VertexNone VertexKind = iota + VertexState + VertexPseudostate + VertexTerminate +) + +// String spells the kind the way messages name vertices. +func (k VertexKind) String() string { + switch k { + case VertexState: + return "state" + case VertexPseudostate: + return "pseudostate" + case VertexTerminate: + return "terminate" + } + return "none" +} + +// VertexRef names a vertex of a lowered state graph: a StateID, a PseudoID or a terminate +// action's 1-based ordinal in the graph's terminate table; the zero VertexRef names no vertex. +type VertexRef struct { + Kind VertexKind + Index uint32 +} + +// StateVertex refers to the state id, or to no vertex when id is 0. +func StateVertex(id StateID) VertexRef { + if id == 0 { + return VertexRef{} + } + return VertexRef{Kind: VertexState, Index: uint32(id)} +} + +// PseudoVertex refers to the pseudostate id, or to no vertex when id is 0. +func PseudoVertex(id PseudoID) VertexRef { + if id == 0 { + return VertexRef{} + } + return VertexRef{Kind: VertexPseudostate, Index: uint32(id)} +} + +// TerminateVertex refers to the terminate action at ordinal, or to no vertex when ordinal is 0. +func TerminateVertex(ordinal uint32) VertexRef { + if ordinal == 0 { + return VertexRef{} + } + return VertexRef{Kind: VertexTerminate, Index: ordinal} +} + +// IsZero reports whether the reference names no vertex. +func (r VertexRef) IsZero() bool { return r == VertexRef{} } + +// State is the state the reference names, 0 when it names another kind of vertex. +func (r VertexRef) State() StateID { + if r.Kind != VertexState { + return 0 + } + return StateID(r.Index) +} + +// Pseudo is the pseudostate the reference names, 0 when it names another kind of vertex. +func (r VertexRef) Pseudo() PseudoID { + if r.Kind != VertexPseudostate { + return 0 + } + return PseudoID(r.Index) +} + +// Terminate is the ordinal of the terminate action the reference names, 0 when +// it names another kind of vertex. +func (r VertexRef) Terminate() uint32 { + if r.Kind != VertexTerminate { + return 0 + } + return r.Index +} diff --git a/internal/ir/program/vertex_test.go b/internal/ir/program/vertex_test.go new file mode 100644 index 0000000000..49edc79581 --- /dev/null +++ b/internal/ir/program/vertex_test.go @@ -0,0 +1,34 @@ +package program + +import "testing" + +func TestVertexRefNamesOneTable(t *testing.T) { + state := StateVertex(3) + if state.State() != 3 || state.Pseudo() != 0 || state.Terminate() != 0 || state.IsZero() { + t.Fatalf("StateVertex(3) = %+v; want a state reference to 3 and nothing else", state) + } + pseudo := PseudoVertex(2) + if pseudo.Pseudo() != 2 || pseudo.State() != 0 || pseudo == state { + t.Fatalf("PseudoVertex(2) = %+v; want a pseudostate reference distinct from a state's", pseudo) + } + stop := TerminateVertex(1) + if stop.Terminate() != 1 || stop.State() != 0 || stop.Pseudo() != 0 { + t.Fatalf("TerminateVertex(1) = %+v; want a terminate reference", stop) + } + if StateVertex(2) == PseudoVertex(2) { + t.Fatal("a state and a pseudostate at the same index compare equal; want the kind to tell them apart") + } +} + +func TestVertexRefZeroNamesNothing(t *testing.T) { + for _, ref := range []VertexRef{{}, StateVertex(0), PseudoVertex(0), TerminateVertex(0)} { + if !ref.IsZero() || ref.Kind != VertexNone { + t.Errorf("%+v is not the zero reference; want id 0 of every kind to name no vertex", ref) + } + } + for kind, want := range map[VertexKind]string{VertexNone: "none", VertexState: "state", VertexPseudostate: "pseudostate", VertexTerminate: "terminate"} { + if got := kind.String(); got != want { + t.Errorf("%d.String() = %q, want %q", kind, got, want) + } + } +} diff --git a/tests/wasm/browser_repl_test.go b/tests/wasm/browser_repl_test.go index 70002fc32f..74a8cfc1e8 100644 --- a/tests/wasm/browser_repl_test.go +++ b/tests/wasm/browser_repl_test.go @@ -12,7 +12,7 @@ import ( // replGzipBudget bounds the gzipped RE (-tags sysml_re) js sysml the CLI page downloads for its in-browser REPL, as the gate links and // `make build-release-wasm` ships it (-trimpath, stripped, library snapshot embedded): 16,009,964 bytes under go1.27.3 on 2026-11-09 at develop ddc4a547fe, 14,324,422 without the snapshot. -const replGzipBudget = 16100000 +const replGzipBudget = 16200000 // TestBrowserREPLWalkthroughs runs every walkthrough the CLI page offers // (docs/assets/repl-walkthroughs.json) through the page's own host, diff --git a/tests/wasm/engine_test.go b/tests/wasm/engine_test.go index 5d72c4d754..0a6160bbf4 100644 --- a/tests/wasm/engine_test.go +++ b/tests/wasm/engine_test.go @@ -24,7 +24,7 @@ import ( // engineGzipBudget bounds the gzipped js sysml-engine the gate links (-trimpath, stripped, the library snapshot embedded, as // every Makefile build target does): 9,451,750 bytes under go1.27.3 on 2026-11-09 at develop ddc4a547fe, 7,764,359 without the snapshot. -const engineGzipBudget = 9500000 +const engineGzipBudget = 9600000 func TestEngineDependencies(t *testing.T) { const module = "github.com/Open-MBEE/OpenSysML"