diff --git a/changes/unreleased/origin-node-descriptions.changed.md b/changes/unreleased/origin-node-descriptions.changed.md new file mode 100644 index 0000000000..c8d1a69d7e --- /dev/null +++ b/changes/unreleased/origin-node-descriptions.changed.md @@ -0,0 +1 @@ +- A run's diagnostics name an unnamed action node by its construct — `the if action`, `the decision node`, `the while loop`, `the fork node`, `the statement` — where they printed the syntax tree's type name (`the *ast.IfActionNode`) before. diff --git a/internal/doc/queryexec/states.go b/internal/doc/queryexec/states.go index 7c690ee652..3260a98728 100644 --- a/internal/doc/queryexec/states.go +++ b/internal/doc/queryexec/states.go @@ -6,6 +6,7 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/exec/runtime" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/ir/queryplan" "github.com/Open-MBEE/OpenSysML/internal/semantic/query" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" @@ -206,20 +207,22 @@ func stateMachinesOf(inst *runtime.Instance) []*runtime.ObjectBehavior { return out } -// activeState describes one active leaf of a machine as a row. +// activeState describes one active leaf of a machine as a row, by the origins +// the graph lowered for the leaf and the states enclosing it. func activeState(inst *runtime.Instance, label string, machine *runtime.ObjectBehavior, leaf *ast.StateNode) State { exec := machine.State + graph := exec.Graph() enclosing := exec.EnclosingStates(leaf) names := make([]string, 0, len(enclosing)) for _, s := range enclosing { - names = append(names, s.Name) + names = append(names, stateName(graph, s)) } return State{ object: inst, label: label, machine: machineName(machine), - symbol: stateSymbol(exec.Graph(), leaf), - name: leaf.Name, + symbol: stateSymbol(graph, graph.ID[leaf]), + name: stateName(graph, leaf), path: exec.StatePath(leaf), region: regionName(exec, leaf), enclosing: names, @@ -227,11 +230,17 @@ func activeState(inst *runtime.Instance, label string, machine *runtime.ObjectBe } } +// stateName is a state's name as its origin records it. +func stateName(graph *lower.StateGraph, state *ast.StateNode) string { + return graph.Origins[graph.ID[state]].Name +} + // regionName names the innermost orthogonal region state stands in, its own or an // ancestor's, "" for a state outside every region. func regionName(exec *runtime.StateExecutor, state *ast.StateNode) string { if region := exec.RegionOf(state); region != nil { - return region.Name + graph := exec.Graph() + return graph.RegionOrigins[graph.RegionID[region]].Name } return "" } @@ -248,9 +257,13 @@ func machineName(machine *runtime.ObjectBehavior) string { return "" } -// stateSymbol is the symbol declaring a state of the graph, nil for one -// lowering synthesized without a declaration. -func stateSymbol(graph *lower.StateGraph, state *ast.StateNode) *symbols.Symbol { +// stateSymbol is the symbol declaring the state with id in the graph, found +// through the graph's provenance; nil for one lowering synthesized without a declaration. +func stateSymbol(graph *lower.StateGraph, id program.StateID) *symbols.Symbol { + state := graph.Provenance.State(id) + if state == nil { + return nil + } decl := graph.DeclOf(state) scope := graph.StateScopes[state] if scope == nil { @@ -270,11 +283,13 @@ func stateSymbol(graph *lower.StateGraph, state *ast.StateNode) *symbols.Symbol // stateNamed reports whether name denotes leaf or a state enclosing it, by // the state's own name or by its path (`on.run`). func stateNamed(exec *runtime.StateExecutor, leaf *ast.StateNode, name string) bool { + graph := exec.Graph() chain := append(exec.EnclosingStates(leaf), leaf) parts := make([]string, 0, len(chain)) for _, state := range chain { - parts = append(parts, state.Name) - if state.Name == name || strings.Join(parts, ".") == name { + own := stateName(graph, state) + parts = append(parts, own) + if own == name || strings.Join(parts, ".") == name { return true } } diff --git a/internal/exec/runtime/action_control_objects.go b/internal/exec/runtime/action_control_objects.go index 5479ac4c12..ed78f61fc8 100644 --- a/internal/exec/runtime/action_control_objects.go +++ b/internal/exec/runtime/action_control_objects.go @@ -55,7 +55,7 @@ func (e *performances) carryObjects(frame *actionFrame, node ast.Node, via lower ec.pushFrame(mapFrame(inputs)) value, err := ec.Eval(f.Value) if err != nil { - return fmt.Errorf("%s: %s: %w", nodeDescription(node), f.Name, err) + return fmt.Errorf("%s: %s: %w", describeNode(graph, node), f.Name, err) } if len(elementsOf(value)) > 0 { produced[f.Name] = soleElement(value) @@ -111,7 +111,7 @@ func arrivingObject(frame *actionFrame, graph *lower.ActionGraph, node ast.Node, case len(held) == 1: return held[0], first[held[0]], nil case len(held) > 1: - return "", -1, fmt.Errorf("%w: %s holds values at %s", errAmbiguousMergeInput, nodeDescription(node), strings.Join(held, ", ")) + return "", -1, fmt.Errorf("%w: %s holds values at %s", errAmbiguousMergeInput, describeNode(graph, node), strings.Join(held, ", ")) case len(from) > 0 || via.Carries: return "", -1, nil } @@ -126,7 +126,7 @@ func arrivingObject(frame *actionFrame, graph *lower.ActionGraph, node ast.Node, case 1: return held[0], slices.IndexFunc(frame.held[refOf(graph, node)][held[0]], func(h nodeObject) bool { return !h.carried }), nil } - return "", -1, fmt.Errorf("%w: %s holds values at %s", errAmbiguousMergeInput, nodeDescription(node), strings.Join(held, ", ")) + return "", -1, fmt.Errorf("%w: %s holds values at %s", errAmbiguousMergeInput, describeNode(graph, node), strings.Join(held, ", ")) } // queueControlObject holds a value an object flow delivers at an input of a fork, join @@ -134,14 +134,14 @@ func arrivingObject(frame *actionFrame, graph *lower.ActionGraph, node ast.Node, func (e *performances) queueControlObject(frame *actionFrame, graph *lower.ActionGraph, flow lower.ObjectFlow, value Value) error { for _, f := range graph.Features[flow.Target] { if f.Name == flow.TargetPin && f.Direction == ast.DirIn { - if err := e.ctx.checkNamedWriteAs(graph.Scopes[flow.Target], func() string { return nodeDescription(flow.Target) }, f.Name, &value); err != nil { + if err := e.ctx.checkNamedWriteAs(graph.Scopes[flow.Target], func() string { return describeNode(graph, flow.Target) }, f.Name, &value); err != nil { return fmt.Errorf("%s: %w", flowDescription(flow), err) } frame.hold(refOf(graph, flow.Target), f.Name, nodeObject{value: value, flow: flow.Decl, carried: flow.Kind == lower.FlowSuccession}) return nil } } - return fmt.Errorf("%s: %w: %s declares no input %s", flowDescription(flow), errNodePin, nodeDescription(flow.Target), flow.TargetPin) + return fmt.Errorf("%s: %w: %s declares no input %s", flowDescription(flow), errNodePin, describeNode(graph, flow.Target), flow.TargetPin) } // hold appends a value to those node holds at pin. diff --git a/internal/exec/runtime/action_executor.go b/internal/exec/runtime/action_executor.go index e93f163ab9..ba1858c36b 100644 --- a/internal/exec/runtime/action_executor.go +++ b/internal/exec/runtime/action_executor.go @@ -10,6 +10,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" @@ -1343,7 +1344,7 @@ func (e *ActionExecutor) breakpointNameOf(within []NodeRef, node NodeRef) string if name := ActionNodeName(node.Node()); name != "" { return name } - return nodeDescription(node.Node()) + return node.Origin().String() } for _, name := range actionNodeNames(node.Node()) { if e.breakpoints[name] { @@ -2322,7 +2323,7 @@ func (e *ActionExecutor) enabledSuccessions(frame *actionFrame, node ast.Node) ( enabled := make([]lower.ActionEdge, 0, len(declared)) for _, edge := range declared { - holds, err := e.guardHolds(ec.evalIn(graph.DeclScope(edge.Decl)), node, edge.Guard) + holds, err := e.guardHolds(ec.evalIn(graph.DeclScope(edge.Decl)), graph, node, edge.Guard) if err != nil { return nil, err } @@ -2358,18 +2359,18 @@ func (e *ActionExecutor) falseGuardLeavesRepeated(graph *lower.ActionGraph, edge // guardHolds evaluates the guard a succession out of node carries; none is unconditional, and // one reading a feature with no value is not true, so it does not assert it (SysML v2 7.17.5). -func (e *ActionExecutor) guardHolds(ec *EvalContext, node, guard ast.Node) (bool, error) { +func (e *ActionExecutor) guardHolds(ec *EvalContext, graph *lower.ActionGraph, node, guard ast.Node) (bool, error) { return e.ctx.guardUnderStatementOrders(guard, e.stepCount+1, ec.scope, func() (bool, error) { result, err := guardResult(ec, guard) if readsNoValue(err) { return false, nil } if err != nil { - return false, fmt.Errorf("eval guard of %s: %w", nodeDescription(node), err) + return false, fmt.Errorf("eval guard of %s: %w", describeNode(graph, node), err) } if !result.isBool() { return false, fmt.Errorf("%w: %s: guard must evaluate to boolean, got %v", - ErrTypeMismatch, nodeDescription(node), result.Kind) + ErrTypeMismatch, describeNode(graph, node), result.Kind) } return result.Const.Bool, nil }) @@ -2822,7 +2823,7 @@ func (e *ActionExecutor) stepDecisionNode(tokenIdx int) error { } } else { var err error - if holds, err = e.guardHolds(ec.evalIn(graph.DeclScope(edge.Decl)), decisionNode, edge.Guard); err != nil { + if holds, err = e.guardHolds(ec.evalIn(graph.DeclScope(edge.Decl)), graph, decisionNode, edge.Guard); err != nil { return err } } @@ -2843,7 +2844,7 @@ func (e *ActionExecutor) stepDecisionNode(tokenIdx int) error { // is the run's own, so the run holds what evaluating it did. if pick > 0 && successors[holding[pick]].Guard != nil { picked := successors[holding[pick]] - holds, err := e.guardHolds(ec.evalIn(graph.DeclScope(picked.Decl)), decisionNode, picked.Guard) + holds, err := e.guardHolds(ec.evalIn(graph.DeclScope(picked.Decl)), graph, decisionNode, picked.Guard) if err != nil { return err } @@ -3508,7 +3509,7 @@ func (e *performances) applyDataFlows( } return fmt.Errorf( "%w: %s: %s produced no value at %s", - errFlowSource, flowDescription(flow), nodeDescription(sourceNode), orAnyPin(flow.SourcePin), + errFlowSource, flowDescription(flow), describeNode(graph, sourceNode), orAnyPin(flow.SourcePin), ) } var err error @@ -3572,13 +3573,14 @@ func orAnyPin(pin string) string { return pin } -// nodeDescription names an action node for a diagnostic, falling back to the -// kind of node it is when the notation gave it no name. -func nodeDescription(node ast.Node) string { - if name := ActionNodeName(node); name != "" { - return "node " + name +//go:noinline +func describeNode(graph *lower.ActionGraph, node ast.Node) string { + // Names a node for a diagnostic by its origin (`node a`, else `the if action`); kept out + // of line because inlined into every error path it cost 17 KB of the wasm size budget. + if graph == nil { + return program.Origin{}.String() } - return fmt.Sprintf("the %T", node) + return graph.OriginOf(node).String() } // --- Public accessor methods for REPL debugging --- diff --git a/internal/exec/runtime/action_frame.go b/internal/exec/runtime/action_frame.go index ced12a764a..a64e768713 100644 --- a/internal/exec/runtime/action_frame.go +++ b/internal/exec/runtime/action_frame.go @@ -758,7 +758,7 @@ func (e *performances) seedDeclaredValues(perf *actionFrame, features []lower.Fe } } if err != nil { - return fmt.Errorf("eval %s of %s: %w", feature.Name, nodeDescription(perf.node), err) + return fmt.Errorf("eval %s of %s: %w", feature.Name, describeNode(perf.flow, perf.node), err) } if err := e.streamFrom(perf, perf.key(feature.Name), value); err != nil { return err @@ -805,8 +805,8 @@ func checkStreamsReceived(frame *actionFrame) error { } stream := frame.unreceived[nodes[0]][0] return fmt.Errorf("%w: %s: %s completed before %s wrote %s", - errStreamUnreceived, flowDescription(stream.flow), nodeDescription(nodes[0].Node()), - nodeDescription(stream.source), orAnyPin(stream.flow.SourcePin)) + errStreamUnreceived, flowDescription(stream.flow), nodes[0].Origin(), + describeNode(nodes[0].Graph, stream.source), orAnyPin(stream.flow.SourcePin)) } // nodePins are the pins a performance of node holds (its own and its action's), @@ -1189,9 +1189,9 @@ func (e *performances) deliver(f *actionFrame, flow *lower.ActionGraph, node ast return err } if !pins.declares(pin) { - return fmt.Errorf("%w: %s declares no %s", errNodePin, nodeDescription(node), pin) + return fmt.Errorf("%w: %s declares no %s", errNodePin, describeNode(flow, node), pin) } - if err := e.ctx.checkNamedWriteAs(flow.Scopes[node], func() string { return nodeDescription(node) }, pin, &value); err != nil { + if err := e.ctx.checkNamedWriteAs(flow.Scopes[node], func() string { return describeNode(flow, node) }, pin, &value); err != nil { return err } f.queue(refOf(flow, node), canonical(pins.aliases, pin), value) @@ -1236,20 +1236,20 @@ func (f *actionFrame) stage(node NodeRef, pin string, source *actionFrame, from // node declaring pin, so a delivery waiting for a performance is known to have somewhere to go. func (e *performances) checkNestedDelivery(flow *lower.ActionGraph, node ast.Node, path []ast.Node, pin string, value *Value) error { for _, next := range path { - flow = lower.NestedFlow(flow, node, next) - if flow == nil { - return fmt.Errorf("%w: %s holds no nested action %s", errNodePin, nodeDescription(node), ActionNodeName(next)) + nested := lower.NestedFlow(flow, node, next) + if nested == nil { + return fmt.Errorf("%w: %s holds no nested action %s", errNodePin, describeNode(flow, node), ActionNodeName(next)) } - node = next + flow, node = nested, next } pins, err := e.nodePins(flow, node) if err != nil { return err } if !pins.declares(pin) { - return fmt.Errorf("%w: %s declares no %s", errNodePin, nodeDescription(node), pin) + return fmt.Errorf("%w: %s declares no %s", errNodePin, describeNode(flow, node), pin) } - return e.ctx.checkNamedWriteAs(flow.Scopes[node], func() string { return nodeDescription(node) }, pin, value) + return e.ctx.checkNamedWriteAs(flow.Scopes[node], func() string { return describeNode(flow, node) }, pin, value) } // takeDeliveries moves the oldest delivery at each pin of node into perf, so that @@ -1397,9 +1397,9 @@ func (e *performances) streamFlow( return fmt.Errorf("%s: %w", flowDescription(flow), err) } if !pins.declares(flow.TargetPin) { - return fmt.Errorf("%s: %w: %s declares no %s", flowDescription(flow), errNodePin, nodeDescription(flow.Target), flow.TargetPin) + return fmt.Errorf("%s: %w: %s declares no %s", flowDescription(flow), errNodePin, describeNode(graph, flow.Target), flow.TargetPin) } - if err := e.ctx.checkNamedWriteAs(graph.Scopes[flow.Target], func() string { return nodeDescription(flow.Target) }, flow.TargetPin, &value); err != nil { + if err := e.ctx.checkNamedWriteAs(graph.Scopes[flow.Target], func() string { return describeNode(graph, flow.Target) }, flow.TargetPin, &value); err != nil { return fmt.Errorf("%s: %w", flowDescription(flow), err) } pin := canonical(pins.aliases, flow.TargetPin) diff --git a/internal/exec/runtime/action_node_ref.go b/internal/exec/runtime/action_node_ref.go index 6176443d9e..7e647d1006 100644 --- a/internal/exec/runtime/action_node_ref.go +++ b/internal/exec/runtime/action_node_ref.go @@ -33,6 +33,14 @@ func refsOf(graph *lower.ActionGraph, nodes []ast.Node) []NodeRef { return refs } +// Origin is where the node the reference names was written; the zero Origin for none. +func (r NodeRef) Origin() program.Origin { + if r.Graph == nil || int(r.ID) >= len(r.Graph.Origins) { + return program.Origin{} + } + return r.Graph.Origins[r.ID] +} + // Node is the declaration the reference names, nil for none. func (r NodeRef) Node() ast.Node { if r.Graph == nil { diff --git a/internal/exec/runtime/binding.go b/internal/exec/runtime/binding.go index 76a2fcf825..6308df35fe 100644 --- a/internal/exec/runtime/binding.go +++ b/internal/exec/runtime/binding.go @@ -7,6 +7,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/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -438,11 +439,11 @@ func (ctx *Context) unmetBindingCounts(unmet []lower.Binding) error { return nil } -// claimBinding marks the binding as the one resolving key, so its own probe of the -// other end is not mistaken for a genuine cycle; the returned func releases the claim. -func (ctx *Context) claimBinding(key featureValueRef, decl *ast.Usage) func() { +// claimBinding marks the binding, by its origin, as the one resolving key, so its own +// probe of the other end is not mistaken for a genuine cycle; the returned func releases the claim. +func (ctx *Context) claimBinding(key featureValueRef, binding program.Origin) func() { previousOwner, hadOwner := ctx.bindingOwners[key] - ctx.bindingOwners[key] = decl + ctx.bindingOwners[key] = binding return func() { if hadOwner { ctx.bindingOwners[key] = previousOwner @@ -687,7 +688,7 @@ func (ctx *Context) sharedValues(a, b Value) int64 { // an end's feature holds, or one reaching the target across a collection. func (ctx *Context) partialBinding(owner, targetInst *Instance, target *FeatureValue, binding lower.Binding, key featureValueRef) (partial bool, err error) { - defer ctx.claimBinding(key, binding.Decl)() + defer ctx.claimBinding(key, binding.Origin)() for end := range binding.Ends { endpoint, err := ctx.resolveBindingEndpoint(owner, binding, end) if err != nil { @@ -865,7 +866,7 @@ func bindingEndForPath(binding lower.Binding, path string) int { func (ctx *Context) attemptBinding(owner, targetInst *Instance, target *FeatureValue, binding lower.Binding, key featureValueRef, carryingRead endRead) (attempt bindingAttempt) { - defer ctx.claimBinding(key, binding.Decl)() + defer ctx.claimBinding(key, binding.Origin)() attempt.defaulted = binding.Ends[0].Default || binding.Ends[1].Default left, err := ctx.resolveBindingEndpoint(owner, binding, 0) @@ -915,7 +916,7 @@ func (ctx *Context) attemptBinding(owner, targetInst *Instance, target *FeatureV } for _, endpoint := range []bindingEndpoint{left, right} { for _, loc := range endpoint.locations { - if ctx.genuineBindingCycle(featureValueRef{instance: loc.instance.ID, feature: loc.name}, binding.Decl) { + if ctx.genuineBindingCycle(featureValueRef{instance: loc.instance.ID, feature: loc.name}, binding.Origin) { attempt.cycle = true } } @@ -1070,7 +1071,7 @@ func (ctx *Context) resolvingBindingEnd(endpoint bindingEndpoint, carries bool) return false } -func (ctx *Context) genuineBindingCycle(ref featureValueRef, binding *ast.Usage) bool { +func (ctx *Context) genuineBindingCycle(ref featureValueRef, binding program.Origin) bool { return ctx.resolvingBindings[ref] && ctx.bindingOwners[ref] != binding } diff --git a/internal/exec/runtime/calc_statements.go b/internal/exec/runtime/calc_statements.go index 12016c0c55..3a3df918a2 100644 --- a/internal/exec/runtime/calc_statements.go +++ b/internal/exec/runtime/calc_statements.go @@ -334,7 +334,7 @@ func (h *calcStmtHost) performNode(engine *stmtEngine, graph *lower.ActionGraph, } if connectsPins(graph, node) { return flowNext, fmt.Errorf("%s: a binding or flow at a pin of %s in a body is not executable", - h.describe(), nodeDescription(node)) + h.describe(), describeNode(graph, node)) } var nestedSteps []lower.Statement var nestedOrder *lower.StatementOrder @@ -343,7 +343,7 @@ func (h *calcStmtHost) performNode(engine *stmtEngine, graph *lower.ActionGraph, nestedSteps, simple = sub.Graph.StatementList() if !simple { return flowNext, fmt.Errorf("%s: the flow %s states of its own in a body is not executable", - h.describe(), nodeDescription(node)) + h.describe(), describeNode(graph, node)) } nestedOrder = graph.StatementOrders[node] } @@ -357,7 +357,7 @@ func (h *calcStmtHost) performNode(engine *stmtEngine, graph *lower.ActionGraph, } value, err := engine.evalIn(feature.Scope).Eval(feature.Value) if err != nil { - return flowNext, fmt.Errorf("eval %s of %s: %w", feature.Name, nodeDescription(node), err) + return flowNext, fmt.Errorf("eval %s of %s: %w", feature.Name, describeNode(graph, node), err) } engine.env.declare(engine.ctx, feature.Name, value) } diff --git a/internal/exec/runtime/case_step.go b/internal/exec/runtime/case_step.go index 59d621e830..79372093d7 100644 --- a/internal/exec/runtime/case_step.go +++ b/internal/exec/runtime/case_step.go @@ -34,7 +34,7 @@ func stepSymbol(flow *lower.ActionGraph, node ast.Node) (*symbols.Symbol, *symbo func caseStepSymbol(flow *lower.ActionGraph, node ast.Node) (*symbols.Symbol, *symbols.Scope, error) { sym, declaring := stepSymbol(flow, node) if sym == nil || !isCalcUsageSymbol(sym) { - return nil, nil, fmt.Errorf("%w: %s is not resolved to a case", errNotACalcUsage, nodeDescription(node)) + return nil, nil, fmt.Errorf("%w: %s is not resolved to a case", errNotACalcUsage, describeNode(flow, node)) } return sym, declaring, nil } @@ -75,7 +75,7 @@ func (e *performances) performCase(perf *actionFrame) error { f = &caseStepFrame{endStep: endStep} if f.start, f.run, err = e.ctx.beginCalcUsage(reader, sym); err != nil { endStep() - return fmt.Errorf("%s: %w", nodeDescription(perf.node), err) + return fmt.Errorf("%s: %w", describeNode(perf.flow, perf.node), err) } } if f.run == nil { @@ -87,7 +87,7 @@ func (e *performances) performCase(perf *actionFrame) error { } if err != nil { f.endStep() - return fmt.Errorf("%s: %w", nodeDescription(perf.node), err) + return fmt.Errorf("%s: %w", describeNode(perf.flow, perf.node), err) } f.run = run } @@ -95,7 +95,7 @@ func (e *performances) performCase(perf *actionFrame) error { run := f.run outputs, err := run.outputValues(e.ctx) if err != nil { - return fmt.Errorf("%s: %w", nodeDescription(perf.node), err) + return fmt.Errorf("%s: %w", describeNode(perf.flow, perf.node), err) } for _, out := range outputs { perf.features[out.Name] = ast.DirOut diff --git a/internal/exec/runtime/check_moves.go b/internal/exec/runtime/check_moves.go index c77cc4e9e1..c44fa7f70e 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 d370689659..0927460222 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 4040878814..ba32f02b77 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/constraint_statements.go b/internal/exec/runtime/constraint_statements.go index f392168672..df054b3b27 100644 --- a/internal/exec/runtime/constraint_statements.go +++ b/internal/exec/runtime/constraint_statements.go @@ -104,7 +104,7 @@ func (h *constraintStmtHost) performNode(engine *stmtEngine, graph *lower.Action } if connectsPins(graph, node) { return flowNext, fmt.Errorf("%s: a binding or flow at a pin of %s in a body is not executable", - h.describe(), nodeDescription(node)) + h.describe(), describeNode(graph, node)) } var nestedSteps []lower.Statement var nestedOrder *lower.StatementOrder @@ -113,7 +113,7 @@ func (h *constraintStmtHost) performNode(engine *stmtEngine, graph *lower.Action nestedSteps, simple = sub.Graph.StatementList() if !simple { return flowNext, fmt.Errorf("%w: %s: the flow %s states of its own in a body is not executable", - errStatementNotExecutable, h.describe(), nodeDescription(node)) + errStatementNotExecutable, h.describe(), describeNode(graph, node)) } nestedOrder = graph.StatementOrders[node] } @@ -127,7 +127,7 @@ func (h *constraintStmtHost) performNode(engine *stmtEngine, graph *lower.Action } value, err := engine.evalIn(feature.Scope).Eval(feature.Value) if err != nil { - return flowNext, fmt.Errorf("eval %s of %s: %w", feature.Name, nodeDescription(node), err) + return flowNext, fmt.Errorf("eval %s of %s: %w", feature.Name, describeNode(graph, node), err) } engine.env.declare(engine.ctx, feature.Name, value) } diff --git a/internal/exec/runtime/context.go b/internal/exec/runtime/context.go index 62bef1f43e..8709119879 100644 --- a/internal/exec/runtime/context.go +++ b/internal/exec/runtime/context.go @@ -9,6 +9,7 @@ import ( "sync/atomic" "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" @@ -137,7 +138,7 @@ type Context struct { // resolvingBindings guards binding endpoint resolution for one instance // feature, so a valueless binding cycle is reported rather than recursed. resolvingBindings map[featureValueRef]bool - bindingOwners map[featureValueRef]*ast.Usage + bindingOwners map[featureValueRef]program.Origin // pendingBehaviors are the object behaviors attached but not yet run, drained // by the outermost materialization so a start reached from inside a running @@ -464,7 +465,7 @@ func NewContext(model *Model, maxSteps int64) *Context { materializingConnectors: make(map[connectorRef]bool), derivingFeatureValues: make(map[featureValueRef]bool), resolvingBindings: make(map[featureValueRef]bool), - bindingOwners: make(map[featureValueRef]*ast.Usage), + bindingOwners: make(map[featureValueRef]program.Origin), collectingSubsets: make(map[featureValueRef]bool), readingSubsetted: make(map[featureValueRef]bool), drawingSubsetters: make(map[featureValueRef]bool), @@ -494,11 +495,11 @@ func (ctx *Context) declaredSymbol(sym *symbols.Symbol) *symbols.Symbol { return ctx.model.declaredSymbol(sym) } -// collectDeclared records the symbol declared by each node under scope. -func collectDeclared(scope *symbols.Scope, into map[ast.Node]*symbols.Symbol) { +// collectDeclared records the symbol declaring each element under scope, by the element's key. +func collectDeclared(scope *symbols.Scope, into map[symbols.ElementKey]*symbols.Symbol) { scope.ForEachMember(func(sym *symbols.Symbol) bool { if sym.Decl != nil { - into[sym.Decl] = sym + into[symbols.KeyOf(sym)] = sym } return true }) diff --git a/internal/exec/runtime/debug_api_test.go b/internal/exec/runtime/debug_api_test.go index fae01e929a..7277aa853d 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/errors.go b/internal/exec/runtime/errors.go index 04bf62524c..ac619e3bac 100644 --- a/internal/exec/runtime/errors.go +++ b/internal/exec/runtime/errors.go @@ -6,7 +6,6 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" - "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" "github.com/Open-MBEE/OpenSysML/internal/syntax/source" ) @@ -586,9 +585,6 @@ func IsUninitializedFeatureValue(err error) bool { // naming the feature so a caller can tell which one is uninitialized. type NoValueError struct { Feature string - // Ref is the written name whose read found no value, so a caller can tell a - // read of its own expression from one made while evaluating a default. - Ref *ast.QualifiedName // Symbol is the feature declaration the read reached, when it is known. Symbol *symbols.Symbol } diff --git a/internal/exec/runtime/eval.go b/internal/exec/runtime/eval.go index 48ce59fad3..afcda68adf 100644 --- a/internal/exec/runtime/eval.go +++ b/internal/exec/runtime/eval.go @@ -750,7 +750,7 @@ func (ec *EvalContext) evalNameGeneral(qn *ast.QualifiedName, unset *symbols.Sym delete(ec.resolving, name) return val, err } - return Value{}, &NoValueError{Feature: name, Ref: qn} + return Value{}, &NoValueError{Feature: name} } } } @@ -908,7 +908,7 @@ func (ec *EvalContext) evalNameGeneral(qn *ast.QualifiedName, unset *symbols.Sym // A feature the element declares but nothing gives a value to is // uninitialized rather than unresolved. if _, declared := ec.features[name]; declared { - return Value{}, &NoValueError{Feature: name, Ref: qn} + return Value{}, &NoValueError{Feature: name} } if ec.ctx.model.resolver != nil && ec.scope != nil { return Value{}, fmt.Errorf("%w: %s", ErrUnresolvedReference, ec.ctx.model.resolver.UnresolvedName(ec.scope, name, qn)) @@ -1152,7 +1152,7 @@ func (ec *EvalContext) withoutValue(sym *symbols.Symbol, spelled string, qn *ast if ec.modelLevel() { return ec.undeterminedFeature(sym, spelled), nil } - return Value{}, &NoValueError{Feature: spelled, Ref: qn} + return Value{}, &NoValueError{Feature: spelled} } return Value{}, fmt.Errorf("cannot evaluate %s %s", sym.Kind, spelled) } diff --git a/internal/exec/runtime/held_image_behavior.go b/internal/exec/runtime/held_image_behavior.go index e85f6af51b..f187ceb06d 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/instance.go b/internal/exec/runtime/instance.go index 99081fa148..d43d2a9208 100644 --- a/internal/exec/runtime/instance.go +++ b/internal/exec/runtime/instance.go @@ -194,9 +194,6 @@ func usageSymbol(sym *symbols.Symbol) bool { // the feature that holds it. func (ctx *Context) noValueError(val Value, node ast.Node) *NoValueError { err := &NoValueError{Feature: conditionText(node)} - if ref, ok := node.(*ast.FeatureReference); ok { - err.Ref = ref.Name - } if inst, ok := ctx.instances[val.Instance]; ok && inst.owner != nil { if fv, ok := inst.owner.FeatureValues[inst.ownerFeature]; ok && fv.Feature != nil { err.Symbol = fv.Feature.Symbol 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/model.go b/internal/exec/runtime/model.go index daa7720a66..2c32f3f53f 100644 --- a/internal/exec/runtime/model.go +++ b/internal/exec/runtime/model.go @@ -211,9 +211,9 @@ type Model struct { sources map[string]*source.SourceFile // scopes holds the scope trees the caller resolves references in; declared - // maps each declaration node to the symbol they declare for it, built on first use. + // maps each element's key to the symbol they declare for it, built on first use. scopes []*symbols.Scope - declared map[ast.Node]*symbols.Symbol + declared map[symbols.ElementKey]*symbols.Symbol // parse reads the notation text a run receives as text: a witness file's input // values and the units a tool answers in; installed by SetExpressionParser. @@ -359,12 +359,12 @@ func (m *Model) declaredSymbol(sym *symbols.Symbol) *symbols.Symbol { return sym } if m.declared == nil { - m.declared = make(map[ast.Node]*symbols.Symbol) + m.declared = make(map[symbols.ElementKey]*symbols.Symbol) for _, scope := range m.scopes { collectDeclared(scope, m.declared) } } - if local, ok := m.declared[sym.Decl]; ok { + if local, ok := m.declared[symbols.KeyOf(sym)]; ok { return local } return sym diff --git a/internal/exec/runtime/origin_description_test.go b/internal/exec/runtime/origin_description_test.go index 8403024b7c..fa4f8e0e60 100644 --- a/internal/exec/runtime/origin_description_test.go +++ b/internal/exec/runtime/origin_description_test.go @@ -8,6 +8,7 @@ import ( "testing" "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/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -16,9 +17,9 @@ import ( "github.com/Open-MBEE/OpenSysML/tests/fixtures" ) -// TestOriginStringMatchesNodeDescription pins program.Origin.String() to what -// nodeDescription says of the same node, over every action the conformance fixtures lower. -func TestOriginStringMatchesNodeDescription(t *testing.T) { +// TestOriginDescriptions pins what a message calls an action node over every action the +// conformance fixtures lower: `node ` when named, else `the `. +func TestOriginDescriptions(t *testing.T) { conformanceDir := filepath.Join("testdata", "conformance") entries, err := os.ReadDir(conformanceDir) if err != nil { @@ -97,11 +98,31 @@ func checkOriginDescriptions(t *testing.T, caseName string, graph *lower.ActionG for _, node := range graph.Nodes { id, ok := graph.ID[node] if !ok { - t.Errorf("%s: %s has no id", caseName, nodeDescription(node)) + t.Errorf("%s: %T %s has no id", caseName, node, ActionNodeName(node)) continue } - if got, want := graph.Origins[id].String(), nodeDescription(node); got != want { - t.Errorf("%s: Origin.String() = %q, nodeDescription = %q", caseName, got, want) + origin, kind := graph.Origins[id], graph.Kinds[id] + if origin.Kind != kind.Word() { + t.Errorf("%s: Origin.Kind = %q, want %q", caseName, origin.Kind, kind.Word()) + } + if origin.Span != node.Span() { + t.Errorf("%s: Origin.Span = %v, want %v", caseName, origin.Span, node.Span()) + } + if origin.Doc == "" { + t.Errorf("%s: %s has no document", caseName, origin) + } + want := "the " + kind.Word() + if name := ActionNodeName(node); name != "" { + want = "node " + name + } + if got := origin.String(); got != want { + t.Errorf("%s: Origin.String() = %q, want %q", caseName, got, want) + } + if got := describeNode(graph, node); got != want { + t.Errorf("%s: describeNode = %q, want %q", caseName, got, want) + } + if got := refOf(graph, node).Origin(); got != origin { + t.Errorf("%s: NodeRef.Origin() = %v, want %v", caseName, got, origin) } checked++ } @@ -114,3 +135,13 @@ func checkOriginDescriptions(t *testing.T, caseName string, graph *lower.ActionG } return checked } + +// TestDescribeNodeOutsideGraph pins what a message calls a node no graph indexes. +func TestDescribeNodeOutsideGraph(t *testing.T) { + if got, want := describeNode(nil, nil), (program.Origin{}).String(); got != want { + t.Errorf("describeNode(nil, nil) = %q, want %q", got, want) + } + if got, want := (NodeRef{}).Origin(), (program.Origin{}); got != want { + t.Errorf("zero NodeRef.Origin() = %v, want the zero Origin", got) + } +} 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 79db2eebc8..1b8ca54a5c 100644 --- a/internal/exec/runtime/snapshot.go +++ b/internal/exec/runtime/snapshot.go @@ -6,8 +6,8 @@ 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" ) // ErrSnapshotMidRun reports a snapshot asked for from inside a step: a snapshot is @@ -822,47 +822,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 +887,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 +897,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 +956,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..ceee65d2a7 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,9 +1336,59 @@ 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 yet, so the record carries its origin alone. +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: entry.Origin, + } +} + +// 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 @@ -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 } @@ -5657,10 +5764,8 @@ func (e *StateExecutor) startIn(owner ast.Node) (*ast.StateNode, error) { } func entryFromName(owner ast.Node, entry *lower.EntryTransition) string { - if transition, ok := entry.Decl.(*ast.TransitionMember); ok { - if source := lower.EndpointText(transition.Source); source != "" { - return source - } + if entry.SourceText != "" { + return entry.SourceText } switch body := owner.(type) { case *ast.StateNode: @@ -5711,23 +5816,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 +6015,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 +6077,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 +6154,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 +6172,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 +6182,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 +6199,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 +6222,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 +6247,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 +6276,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 +6298,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 +6377,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 +6428,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 +6440,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 +6476,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 } @@ -6463,7 +6565,7 @@ func (e *StateExecutor) sourcePathNames(trans *lower.Transition, fromName string } func (e *StateExecutor) stateSourceOrigin(state *ast.StateNode) symbols.Origin { - doc := e.graph.DocOf(state) + doc := e.stateOrigin(state).Doc if doc == "" && e.stateMachine != nil { doc = e.stateMachine.DocName } @@ -6585,7 +6687,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 +6698,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 +6858,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..7a494e7f13 --- /dev/null +++ b/internal/exec/runtime/state_ids.go @@ -0,0 +1,210 @@ +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] +} + +// stateOrigin is the origin lowering recorded for state; the zero Origin for the +// machine state, which the table leaves out. +func (e *StateExecutor) stateOrigin(state *ast.StateNode) program.Origin { + if id := e.stateID(state); int(id) < len(e.graph.Origins) { + return e.graph.Origins[id] + } + return program.Origin{} +} + +// 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 2e992c99e9..a67d5a8f59 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..46807f09d5 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" ) @@ -41,10 +42,8 @@ func (e *StateExecutor) effectLabel(trans *lower.Transition) string { func (e *StateExecutor) entryTransitionEffectHead(owner ast.Node, entry *lower.EntryTransition) unitHead { label := e.describeBody(owner) + effectTag - if transition, ok := entry.Decl.(*ast.TransitionMember); ok { - if source := lower.EndpointText(transition.Source); source != "" { - label = source + "->" + StateVertexName(entryTarget(entry)) + effectTag - } + if entry.SourceText != "" { + label = entry.SourceText + "->" + StateVertexName(entryTarget(entry)) + effectTag } return unitHead{label: label, at: entry.Decl, site: e.bodySite(owner)} } @@ -157,7 +156,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 +178,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 +391,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 +515,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 +530,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 +713,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/grpc/session.go b/internal/frontend/grpc/session.go index af2b947720..a9d4c411f4 100644 --- a/internal/frontend/grpc/session.go +++ b/internal/frontend/grpc/session.go @@ -15,6 +15,7 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/exec/runtime" "github.com/Open-MBEE/OpenSysML/internal/frontend/protoconv" "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" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast/astcodec" @@ -617,28 +618,26 @@ func branchesOf(exec *runtime.ActionExecutor) []SessionBranch { if len(t.Within) > 0 { continue } - if _, ok := t.Edge.Source.(*ast.DecisionNode); !ok { + source := graph.ID[t.Edge.Source] + if graph.Kinds[source] != program.NodeDecision { continue } - branch := SessionBranch{ - Decision: runtime.ActionNodeName(t.Edge.Source), - Target: runtime.ActionNodeName(t.Edge.Target), - Opening: t.Edge.Source == gate, - } - if edge, ok := t.Edge.Decl.(*ast.ControlFlowEdge); ok { - branch.Else = edge.IsElse - } - out = append(out, branch) + out = append(out, SessionBranch{ + Decision: graph.Origins[source].Name, + Target: graph.OriginOf(t.Edge.Target).Name, + Opening: source == gate, + Else: t.Edge.Else, + }) } return out } // gateOf is the first decision the action's start leads to without a choice on -// the way, following each node's single succession; nil when there is none. -func gateOf(graph *lower.ActionGraph) ast.Node { +// the way, following each node's single succession; 0 when there is none. +func gateOf(graph *lower.ActionGraph) program.NodeID { var at ast.Node for _, node := range graph.Nodes { - if _, ok := node.(*ast.InitialNode); ok { + if graph.Kinds[graph.ID[node]] == program.NodeInitial { at = node break } @@ -646,16 +645,16 @@ func gateOf(graph *lower.ActionGraph) ast.Node { seen := map[ast.Node]bool{} for at != nil && !seen[at] { seen[at] = true - if _, ok := at.(*ast.DecisionNode); ok { - return at + if id := graph.ID[at]; graph.Kinds[id] == program.NodeDecision { + return id } edges := graph.Edges[at] if len(edges) != 1 { - return nil + return 0 } at = edges[0].Target } - return nil + return 0 } // choicesOf copies the choice points among a run's notes. diff --git a/internal/frontend/lsp/debug.go b/internal/frontend/lsp/debug.go index 57064a10ee..703dff4692 100644 --- a/internal/frontend/lsp/debug.go +++ b/internal/frontend/lsp/debug.go @@ -1365,10 +1365,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/binding.go b/internal/ir/lower/binding.go index cb47d1f0e1..311431e2fd 100644 --- a/internal/ir/lower/binding.go +++ b/internal/ir/lower/binding.go @@ -3,6 +3,7 @@ package lower import ( "strings" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -13,6 +14,8 @@ type Binding struct { Ends [2]BindingEnd Scope *symbols.Scope Decl *ast.Usage + // Origin is where the connector was written, Kind "binding"; it has no Path. + Origin program.Origin // Multiplicity is the connector's own as written (`binding [1] bind …`), nil when none. Multiplicity *ast.Multiplicity } @@ -119,7 +122,12 @@ func lowerBinding(u *ast.Usage, scope *symbols.Scope) (Binding, bool) { ends[i].Expr, ends[i].Index = end.AttachedSelection(), index } } - return Binding{Ends: ends, Scope: scope, Decl: u, Multiplicity: u.Multiplicity}, true + return Binding{Ends: ends, Scope: scope, Decl: u, Origin: bindingOrigin(u, scope), Multiplicity: u.Multiplicity}, true +} + +func bindingOrigin(u *ast.Usage, scope *symbols.Scope) program.Origin { + name, _ := ast.EffectiveName(u) + return program.Origin{Doc: symbols.DocNameOf(scope), Span: u.Span(), Kind: "binding", Name: name} } // EndValueBindings lowers each `=` or `default =` in a connector end's body as a binding of the end's @@ -153,7 +161,7 @@ func EndValueBindings(decl ast.Node, scope *symbols.Scope) []Binding { {Path: endName + "." + name, Expr: u.Value}, {Expr: u.Value, Default: u.ValueIsDefault}, }, - Scope: endScope, Decl: u, + Scope: endScope, Decl: u, Origin: bindingOrigin(u, endScope), }) } } diff --git a/internal/ir/lower/binding_test.go b/internal/ir/lower/binding_test.go index 7b77e50664..222e53e7dd 100644 --- a/internal/ir/lower/binding_test.go +++ b/internal/ir/lower/binding_test.go @@ -275,6 +275,12 @@ func TestEndValueBindingsLowerEndBodyValues(t *testing.T) { if got.Decl == nil || got.Decl.Value != got.Ends[1].Expr { t.Errorf("binding %d is declared by %v, want the end's attribute", i, got.Decl) } + if got.Origin.Doc != "ends.sysml" || got.Origin.Span != got.Decl.Span() || got.Origin.Kind != "binding" { + t.Errorf("binding %d origin = %#v, want the end attribute's document, span and kind binding", i, got.Origin) + } + } + if bindings[0].Origin == bindings[1].Origin { + t.Errorf("the two end value bindings share the origin %#v; a run tells bindings apart by it", bindings[0].Origin) } through, ok := bindings[0].ThroughEnds(map[string]string{"a": "s.src", "b": "k.dst"}, nil, Hop{Feature: "i"}) if !ok || through.Ends[0].Path != "k.dst.voltage" || !slices.Equal(through.Ends[1].Via, []Hop{{Feature: "i"}}) { diff --git a/internal/ir/lower/entry_transition.go b/internal/ir/lower/entry_transition.go index 261cad2839..71581a0bcc 100644 --- a/internal/ir/lower/entry_transition.go +++ b/internal/ir/lower/entry_transition.go @@ -3,6 +3,7 @@ package lower import ( "fmt" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -20,6 +21,10 @@ type EntryTransition struct { Effect []StateBehavior // Scope is the scope Guard resolves in. Scope *symbols.Scope + // Origin is where the member was written, Kind "entry transition"; it has no Path. + Origin program.Origin + // SourceText is the source a TransitionMember names as written (`boot`), "" for the other forms. + SourceText string } const ( diff --git a/internal/ir/lower/identity.go b/internal/ir/lower/identity.go index 41bbc6af13..4c5a9ffc04 100644 --- a/internal/ir/lower/identity.go +++ b/internal/ir/lower/identity.go @@ -1,7 +1,6 @@ package lower import ( - "fmt" "sort" "strconv" @@ -206,27 +205,76 @@ func (g *ActionGraph) nodeOrigin(node ast.Node, kind program.NodeKind, paths ori Doc: symbols.DocNameOf(scope), Span: node.Span(), Path: paths.unique(originPath(scope, sym, name, kind.String())), - Kind: fmt.Sprintf("%T", node), + Kind: kind.Word(), Name: name, } } -// assignIDs gives every state, region, pseudostate and transition of the graph -// its id, dense from 1 in declaration order, and the origin beside it. +// OriginOf is the origin of a node of the graph; the zero Origin for one the graph does not index. +func (g *ActionGraph) OriginOf(node ast.Node) program.Origin { + if id, ok := g.ID[node]; ok { + return g.Origins[id] + } + return program.Origin{} +} + func (g *StateGraph) assignIDs() { paths := originPaths{} g.assignStateIDs(paths) g.assignRegionIDs(paths) g.assignPseudoIDs(paths) g.assignTransIDs(paths) + g.assignEntryOrigins() +} + +// assignEntryOrigins gives each entry transition its origin and the text of the +// source it was written from, so a run names it without its declaration. +func (g *StateGraph) assignEntryOrigins() { + for _, transitions := range g.EntryTransitions { + for _, entry := range transitions { + entry.Origin = program.Origin{Doc: g.DocOf(entry.Decl), Span: entry.Decl.Span(), Kind: "entry transition"} + if member, ok := entry.Decl.(*ast.TransitionMember); ok && member.Source != nil { + entry.SourceText = EndpointText(member.Source) + } + } + } +} + +// 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 +308,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..fa66f62573 100644 --- a/internal/ir/lower/identity_test.go +++ b/internal/ir/lower/identity_test.go @@ -101,14 +101,14 @@ func TestAssignIDs_ActionOriginsNameWhatMessagesName(t *testing.T) { byName[graph.Origins[graph.ID[node]].Name] = graph.ID[node] } grind := graph.Origins[byName["grind"]] - if grind.String() != "node grind" || grind.Kind != "*ast.Usage" || grind.Path != "brew::grind" || grind.Doc != "test.sysml" { + if grind.String() != "node grind" || grind.Kind != program.NodeExecution.Word() || grind.Path != "brew::grind" || grind.Doc != "test.sysml" { t.Errorf("grind origin = %#v", grind) } if graph.Kinds[1] != program.NodeInitial || graph.Kinds[byName["f"]] != program.NodeFork || graph.Kinds[byName["grind"]] != program.NodeExecution { t.Errorf("kinds: initial %v, fork %v, grind %v", graph.Kinds[1], graph.Kinds[byName["f"]], graph.Kinds[byName["grind"]]) } done := graph.Origins[byName["done"]] - if done.String() != "node done" || done.Kind != "*ast.FinalNode" || graph.Kinds[byName["done"]] != program.NodeFinal { + if done.String() != "node done" || done.Kind != program.NodeFinal.Word() || graph.Kinds[byName["done"]] != program.NodeFinal { t.Errorf("done origin = %#v", done) } for id := 1; id < len(graph.Origins); id++ { @@ -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/kind.go b/internal/ir/program/kind.go index ca5bcef305..e1c90d7088 100644 --- a/internal/ir/program/kind.go +++ b/internal/ir/program/kind.go @@ -69,3 +69,32 @@ func (k NodeKind) String() string { } return "NodeKind(" + strconv.Itoa(int(k)) + ")" } + +var nodeKindWords = [...]string{ + NodeNone: "node", + NodeInitial: "initial node", + NodeFinal: "final node", + NodeFork: "fork node", + NodeJoin: "join node", + NodeMerge: "merge node", + NodeDecision: "decision node", + NodeExecution: "action", + NodePerform: "perform action", + NodeAccept: "accept action", + NodeSend: "send action", + NodeAssign: "assignment action", + NodeTerminate: "terminate action", + NodeIf: "if action", + NodeLoop: "while loop", + NodeBlock: "block", + NodeStatement: "statement", +} + +// Word is the kind's name as a message says it of an unnamed node (`the if action`): +// the SysML v2 name of the node's construct, which Origin.Kind carries. +func (k NodeKind) Word() string { + if int(k) < len(nodeKindWords) { + return nodeKindWords[k] + } + return nodeKindWords[NodeNone] +} diff --git a/internal/ir/program/origin.go b/internal/ir/program/origin.go index c58cabd5e1..ba6824630f 100644 --- a/internal/ir/program/origin.go +++ b/internal/ir/program/origin.go @@ -14,8 +14,8 @@ type Origin struct { Path string // Element is the declaration's element id when one is known, else "". Element string - // Kind is the word today's messages name the declaration by when it is unnamed: - // an action node's declaring type as %T renders it, a vertex's `state` or `pseudostate`. + // Kind is the word a message names the declaration by when it is unnamed: an + // action node's NodeKind.Word (`if action`), a vertex's `state` or `pseudostate`. Kind string // Name is the declaration's name as messages use it, "" when it has none. Name string @@ -24,10 +24,13 @@ type Origin struct { } // String names the record the way a message does: by name when it has one, -// else by kind. +// else by kind; the zero Origin is "the node". func (o Origin) String() string { if o.Name != "" { return "node " + o.Name } + if o.Kind == "" { + return "the node" + } return "the " + o.Kind } diff --git a/internal/ir/program/program_test.go b/internal/ir/program/program_test.go index 8b8685da34..56e6633c75 100644 --- a/internal/ir/program/program_test.go +++ b/internal/ir/program/program_test.go @@ -13,10 +13,11 @@ func TestOriginString(t *testing.T) { origin Origin want string }{ - {"named", Origin{Name: "step", Kind: "*ast.Usage"}, "node step"}, - {"final", Origin{Name: "done", Kind: "*ast.FinalNode"}, "node done"}, - {"unnamed", Origin{Kind: "*ast.IfActionNode"}, "the *ast.IfActionNode"}, + {"named", Origin{Name: "step", Kind: NodeExecution.Word()}, "node step"}, + {"final", Origin{Name: "done", Kind: NodeFinal.Word()}, "node done"}, + {"unnamed", Origin{Kind: NodeIf.Word()}, "the if action"}, {"vertex", Origin{Kind: "state"}, "the state"}, + {"zero", Origin{}, "the node"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -28,7 +29,7 @@ func TestOriginString(t *testing.T) { } func TestOriginIsPlainData(t *testing.T) { - a := Origin{Doc: "m.sysml", Span: source.Span{Offset: 3, Len: 4}, Path: "A::b", Name: "b", Kind: "*ast.Usage"} + a := Origin{Doc: "m.sysml", Span: source.Span{Offset: 3, Len: 4}, Path: "A::b", Name: "b", Kind: NodeExecution.Word()} b := a if a != b { t.Fatal("Origin must be comparable and copy by value") @@ -49,6 +50,26 @@ func TestNodeKindString(t *testing.T) { } } +func TestNodeKindWord(t *testing.T) { + words := map[string]NodeKind{} + for k := NodeNone; k <= NodeStatement; k++ { + word := k.Word() + if word == "" { + t.Errorf("NodeKind %s has no word", k) + } + if other, dup := words[word]; dup { + t.Errorf("NodeKind %s and %s share the word %q", k, other, word) + } + words[word] = k + } + if got := NodeKind(200).Word(); got != NodeNone.Word() { + t.Errorf("unknown kind's word is %q, want the zero kind's", got) + } + if got := (Origin{Kind: NodeDecision.Word()}).String(); got != "the decision node" { + t.Errorf("unnamed decision renders %q", got) + } +} + func TestGraphIDZero(t *testing.T) { var id GraphID if !id.IsZero() { 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"