From d3424c1ec35a668c6f9f4d43dbd06f19435e962e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:15:12 +0000 Subject: [PATCH 1/2] feat(runtime): key action executor state by node id The action executor's frames, tokens, breakpoints, traversals, future footprints and their snapshot and held-image copies hold the graph-local program.NodeID step 1 assigns, as runtime.NodeRef, in place of ast.Node; consumers read the declaration back through the graph's provenance. Co-Authored-By: jason.han --- internal/exec/runtime/action_body_run.go | 2 +- internal/exec/runtime/action_choice.go | 12 +- .../exec/runtime/action_control_objects.go | 24 +-- internal/exec/runtime/action_executor.go | 114 ++++++------ internal/exec/runtime/action_executor_test.go | 60 +++---- internal/exec/runtime/action_frame.go | 169 +++++++++++------- internal/exec/runtime/action_node_ref.go | 158 ++++++++++++++++ internal/exec/runtime/action_statements.go | 4 +- .../exec/runtime/action_step_multiplicity.go | 13 +- internal/exec/runtime/action_subflow.go | 22 +-- internal/exec/runtime/action_terminate.go | 10 +- internal/exec/runtime/advance.go | 2 +- internal/exec/runtime/calc_statements.go | 6 +- internal/exec/runtime/check.go | 6 +- internal/exec/runtime/check_body.go | 2 +- internal/exec/runtime/check_moves.go | 4 +- internal/exec/runtime/check_reduce.go | 2 +- internal/exec/runtime/check_state.go | 36 ++-- .../exec/runtime/check_state_stream_test.go | 15 +- internal/exec/runtime/choice_test.go | 2 +- internal/exec/runtime/debug_api_test.go | 46 ++--- .../exec/runtime/debug_breakpoint_ids_test.go | 55 ++++++ internal/exec/runtime/executor_common.go | 22 ++- internal/exec/runtime/executor_common_test.go | 5 +- internal/exec/runtime/future_footprint.go | 13 +- internal/exec/runtime/held_image_behavior.go | 48 ++--- internal/exec/runtime/held_image_test.go | 10 +- internal/exec/runtime/pristine_test.go | 2 +- internal/exec/runtime/replay_test.go | 4 +- .../runtime/robustness_action_flow_test.go | 4 +- internal/exec/runtime/snapshot.go | 20 +-- internal/exec/runtime/snapshot_test.go | 2 +- internal/exec/runtime/state_space.go | 2 +- internal/exec/runtime/state_statements.go | 6 +- internal/exec/runtime/stream_stage_test.go | 2 +- internal/exec/runtime/trace.go | 4 +- internal/frontend/lsp/debug.go | 12 +- internal/frontend/repl/debugger.go | 2 +- internal/frontend/repl/meta.go | 2 +- 39 files changed, 593 insertions(+), 331 deletions(-) create mode 100644 internal/exec/runtime/action_node_ref.go create mode 100644 internal/exec/runtime/debug_breakpoint_ids_test.go diff --git a/internal/exec/runtime/action_body_run.go b/internal/exec/runtime/action_body_run.go index 23c1015740..ff9f31b743 100644 --- a/internal/exec/runtime/action_body_run.go +++ b/internal/exec/runtime/action_body_run.go @@ -713,7 +713,7 @@ func (e *ActionExecutor) resumeBody(tokenIdx int) error { // pauseAt pauses the run before a node a breakpoint is set on performs, as the run // pauses before a token steps such a node; a run not made pausable goes on. -func (e *ActionExecutor) pauseAt(within []ast.Node, node ast.Node) error { +func (e *ActionExecutor) pauseAt(within []NodeRef, node NodeRef) error { if stop, set := e.stopAt(within, node); set { return e.ctx.pauseBody(bodyPause{breakpoint: stop}) } diff --git a/internal/exec/runtime/action_choice.go b/internal/exec/runtime/action_choice.go index ba02cf1542..c0d9174806 100644 --- a/internal/exec/runtime/action_choice.go +++ b/internal/exec/runtime/action_choice.go @@ -253,7 +253,7 @@ func (e *ActionExecutor) keeps(t Token) bool { // this step, so the keeping accept does not yield to it. func (e *ActionExecutor) yieldsKeeping(k Token, pending []Message, unready map[int64]bool) bool { accept, _ := e.messageAccept(k) - keeps, _ := e.acceptMatch(k.frame, accept, k.Location.(*ast.Usage)) + keeps, _ := e.acceptMatch(k.frame, accept, k.Node().(*ast.Usage)) for _, m := range pending { if !keeps(m) { continue @@ -263,7 +263,7 @@ func (e *ActionExecutor) yieldsKeeping(k Token, pending []Message, unready map[i continue } if other, ok := e.messageAccept(t); ok { - matches, _ := e.acceptMatch(t.frame, other, t.Location.(*ast.Usage)) + matches, _ := e.acceptMatch(t.frame, other, t.Node().(*ast.Usage)) if matches(m) { return true } @@ -286,14 +286,14 @@ func (e *ActionExecutor) offeredMessage(t Token, pending []Message) bool { if !ok { return false } - matches, _ := e.acceptMatch(t.frame, accept, t.Location.(*ast.Usage)) + matches, _ := e.acceptMatch(t.frame, accept, t.Node().(*ast.Usage)) return slices.ContainsFunc(pending, matches) } // messageAccept returns the accept the token sits at when it is one a message, // not a trigger, answers. func (e *ActionExecutor) messageAccept(t Token) (lower.Accept, bool) { - usage, ok := t.Location.(*ast.Usage) + usage, ok := t.Node().(*ast.Usage) if !ok || t.body != nil { return lower.Accept{}, false } @@ -367,7 +367,7 @@ func (e *ActionExecutor) noteTokenOrder(step int, order stepOrder, schedule *tok alts = make([]string, len(tokens)) fps = make([]lower.Footprint, len(tokens)) for i, t := range tokens { - alts[i] = TokenLabel(t.ID, t.Location) + alts[i] = TokenLabel(t.ID, t.Node()) fps[i] = standing(e, t) if t.ID == first { taken = i @@ -395,7 +395,7 @@ func (e *ActionExecutor) labeledFootprints(labels []string, able []Token) []lowe for i, label := range labels { fps[i] = unknownFootprint() for _, t := range able { - if TokenLabel(t.ID, t.Location) == label { + if TokenLabel(t.ID, t.Node()) == label { fps[i] = standing(e, t) break } diff --git a/internal/exec/runtime/action_control_objects.go b/internal/exec/runtime/action_control_objects.go index 0a8f77915f..5479ac4c12 100644 --- a/internal/exec/runtime/action_control_objects.go +++ b/internal/exec/runtime/action_control_objects.go @@ -37,12 +37,12 @@ func (e *performances) carryObjects(frame *actionFrame, node ast.Node, via lower return err } if at >= 0 { - inputs[pin] = frame.takeHeld(node, pin, at) + inputs[pin] = frame.takeHeld(refOf(graph, node), pin, at) } } else { for _, f := range features { - if f.Direction == ast.DirIn && len(frame.held[node][f.Name]) > 0 { - inputs[f.Name] = frame.takeHeld(node, f.Name, 0) + if f.Direction == ast.DirIn && len(frame.held[refOf(graph, node)][f.Name]) > 0 { + inputs[f.Name] = frame.takeHeld(refOf(graph, node), f.Name, 0) } } } @@ -84,7 +84,7 @@ func arrivingObject(frame *actionFrame, graph *lower.ActionGraph, node ast.Node, continue } if via.Carries && flow.Kind == lower.FlowSuccession && flow.Decl == via.Decl { - if at := frame.heldFrom(node, flow.TargetPin, flow.Decl); at >= 0 { + if at := frame.heldFrom(refOf(graph, node), flow.TargetPin, flow.Decl); at >= 0 { return flow.TargetPin, at, nil } continue @@ -96,7 +96,7 @@ func arrivingObject(frame *actionFrame, graph *lower.ActionGraph, node ast.Node, var held []string first := make(map[string]int) for _, flow := range from { - at := frame.heldFrom(node, flow.TargetPin, flow.Decl) + at := frame.heldFrom(refOf(graph, node), flow.TargetPin, flow.Decl) if at < 0 { continue } @@ -116,7 +116,7 @@ func arrivingObject(frame *actionFrame, graph *lower.ActionGraph, node ast.Node, return "", -1, nil } for _, f := range graph.Features[node] { - if f.Direction == ast.DirIn && slices.ContainsFunc(frame.held[node][f.Name], func(h nodeObject) bool { return !h.carried }) { + if f.Direction == ast.DirIn && slices.ContainsFunc(frame.held[refOf(graph, node)][f.Name], func(h nodeObject) bool { return !h.carried }) { held = append(held, f.Name) } } @@ -124,7 +124,7 @@ func arrivingObject(frame *actionFrame, graph *lower.ActionGraph, node ast.Node, case 0: return "", -1, nil case 1: - return held[0], slices.IndexFunc(frame.held[node][held[0]], func(h nodeObject) bool { return !h.carried }), nil + 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, ", ")) } @@ -137,7 +137,7 @@ func (e *performances) queueControlObject(frame *actionFrame, graph *lower.Actio if err := e.ctx.checkNamedWriteAs(graph.Scopes[flow.Target], func() string { return nodeDescription(flow.Target) }, f.Name, &value); err != nil { return fmt.Errorf("%s: %w", flowDescription(flow), err) } - frame.hold(flow.Target, f.Name, nodeObject{value: value, flow: flow.Decl, carried: flow.Kind == lower.FlowSuccession}) + frame.hold(refOf(graph, flow.Target), f.Name, nodeObject{value: value, flow: flow.Decl, carried: flow.Kind == lower.FlowSuccession}) return nil } } @@ -145,9 +145,9 @@ func (e *performances) queueControlObject(frame *actionFrame, graph *lower.Actio } // hold appends a value to those node holds at pin. -func (f *actionFrame) hold(node ast.Node, pin string, object nodeObject) { +func (f *actionFrame) hold(node NodeRef, pin string, object nodeObject) { if f.held == nil { - f.held = make(map[ast.Node]map[string][]nodeObject) + f.held = make(map[NodeRef]map[string][]nodeObject) } if f.held[node] == nil { f.held[node] = make(map[string][]nodeObject) @@ -156,12 +156,12 @@ func (f *actionFrame) hold(node ast.Node, pin string, object nodeObject) { } // heldFrom is the position of the oldest value flow brought that node holds at pin; -1 if none. -func (f *actionFrame) heldFrom(node ast.Node, pin string, flow ast.Node) int { +func (f *actionFrame) heldFrom(node NodeRef, pin string, flow ast.Node) int { return slices.IndexFunc(f.held[node][pin], func(h nodeObject) bool { return h.flow == flow }) } // takeHeld removes and returns the value node holds at pin's position at. -func (f *actionFrame) takeHeld(node ast.Node, pin string, at int) Value { +func (f *actionFrame) takeHeld(node NodeRef, pin string, at int) Value { objects := f.held[node][pin] value := objects[at].value if len(objects) > 1 { diff --git a/internal/exec/runtime/action_executor.go b/internal/exec/runtime/action_executor.go index 7955d83b0a..e93f163ab9 100644 --- a/internal/exec/runtime/action_executor.go +++ b/internal/exec/runtime/action_executor.go @@ -31,7 +31,7 @@ type ActionExecutor struct { // performedAt is the node of another action's graph whose performance this // executor runs, nil for an action run on its own: the body's own places are // that performance's, as its pins are. - performedAt ast.Node + performedAt NodeRef // action states the body performed; performed is the action as named, a usage // stating no body of its own or the action itself, whose metadata binds the performance. action, performed *symbols.Symbol @@ -132,18 +132,18 @@ func (e *ActionExecutor) beginSweep() func() { // breakpointVisit identifies one token's stay at one node. type breakpointVisit struct { token int64 - node ast.Node + node NodeRef } // NodeBreakpoint is a breakpoint set by identity: on Node, in the flow of the nested // action nodes Within (outermost first, as Token.Within lists them; none for the action's own). type NodeBreakpoint struct { - Within []ast.Node - Node ast.Node + Within []NodeRef + Node NodeRef } // at reports whether the breakpoint is set on node in the flow of within. -func (bp NodeBreakpoint) at(within []ast.Node, node ast.Node) bool { +func (bp NodeBreakpoint) at(within []NodeRef, node NodeRef) bool { return bp.Node == node && slices.Equal(bp.Within, within) } @@ -168,7 +168,7 @@ type breakpointStop struct { // stopAt is the stop at a breakpoint set on node in the flow of within, if one is; // false otherwise. -func (e *ActionExecutor) stopAt(within []ast.Node, node ast.Node) (breakpointStop, bool) { +func (e *ActionExecutor) stopAt(within []NodeRef, node NodeRef) (breakpointStop, bool) { name := e.breakpointNameOf(within, node) if name == "" { return breakpointStop{}, false @@ -483,7 +483,7 @@ func (e *ActionExecutor) holdVisible(held map[*symbols.Symbol]bool, attr lower.A // made and nothing is waiting for a message). func (e *ActionExecutor) Step() error { defer e.ctx.beginExecutorRun(&e.driven)() - defer e.ctx.performingNode(e.performedAt, e.performedParams())() + defer e.ctx.performingNode(e.performedAt.Node(), e.performedParams())() if e.released { return fmt.Errorf("%w: its run ended when it was let go of", ErrExecutorReleased) @@ -517,7 +517,7 @@ func (e *ActionExecutor) Step() error { tokenCountBefore := len(e.tokens) tokenLocationsBefore := make([]ast.Node, len(e.tokens)) for i, t := range e.tokens { - tokenLocationsBefore[i] = t.Location + tokenLocationsBefore[i] = t.Node() } // Under a sweep the tokens whose work paused resume last, the longest paused @@ -610,7 +610,7 @@ func (e *ActionExecutor) tokensProgressed(countBefore int, locationsBefore []ast return true } for i := 0; i < len(e.tokens) && i < len(locationsBefore); i++ { - if e.tokens[i].Location != locationsBefore[i] || e.moving(e.tokens[i]) { + if e.tokens[i].Node() != locationsBefore[i] || e.moving(e.tokens[i]) { return true } } @@ -774,7 +774,7 @@ func (e *ActionExecutor) describePausedWait(token Token, seen map[waitToken]bool if len(descriptions) == 0 { return nil, blocked } - caller := ActionNodeName(token.Location) + caller := ActionNodeName(token.Node()) for i := range descriptions { context := make([]string, 0, 2) if performance != "" { @@ -836,7 +836,7 @@ func (e *ActionExecutor) run(atCurrentTime bool) error { // runSteps is run, stopping after one step where single is set. func (e *ActionExecutor) runSteps(atCurrentTime, single bool) error { defer e.ctx.beginExecutorRun(&e.driven)() - defer e.ctx.performingNode(e.performedAt, e.performedParams())() + defer e.ctx.performingNode(e.performedAt.Node(), e.performedParams())() if e.released { return fmt.Errorf("%w: its run ended when it was let go of", ErrExecutorReleased) @@ -1049,7 +1049,7 @@ func (e *ActionExecutor) changeWaitHoldsIn(perf *actionFrame, seen map[waitTarge continue } if token.Wait != nil && !token.Wait.Timed && token.Wait.Trigger != "" { - usage, ok := token.Location.(*ast.Usage) + usage, ok := token.Node().(*ast.Usage) if ok { accept, ok := e.graphOf(token.frame).Accepts[usage] if _, isChange := accept.Trigger.(*ast.ChangeEvent); ok && isChange { @@ -1195,7 +1195,7 @@ func (e *ActionExecutor) acceptTaking(m Message) ([]TakingAccept, error) { if matches(m) { taking = append(taking, TakingAccept{ Param: token.Wait.ParamName, - Node: ActionNodeName(token.Location), + Node: ActionNodeName(token.Node()), Within: token.frame.path(), }) } @@ -1234,7 +1234,7 @@ func (e *ActionExecutor) parkedAcceptTakes(perf *actionFrame, takes func(token T if token.Wait == nil || token.Wait.Timed || !token.inFlowOf(perf) { continue } - usage, ok := token.Location.(*ast.Usage) + usage, ok := token.Node().(*ast.Usage) if !ok { continue } @@ -1335,17 +1335,17 @@ func (e *ActionExecutor) tokenByID(id int64) (Token, bool) { // breakpointNameOf returns the name a breakpoint is set on for node in the flow of // within, or "" when none is. A node answers to its short name as well as its name; // one a breakpoint is set on by identity answers to its description. -func (e *ActionExecutor) breakpointNameOf(within []ast.Node, node ast.Node) string { +func (e *ActionExecutor) breakpointNameOf(within []NodeRef, node NodeRef) string { for _, bp := range e.breakpointNodes { if !bp.at(within, node) { continue } - if name := ActionNodeName(node); name != "" { + if name := ActionNodeName(node.Node()); name != "" { return name } - return nodeDescription(node) + return nodeDescription(node.Node()) } - for _, name := range actionNodeNames(node) { + for _, name := range actionNodeNames(node.Node()) { if e.breakpoints[name] { return name } @@ -1996,7 +1996,7 @@ func (e *ActionExecutor) checkInputNames() error { // initialize spawns initial token at InitialNode. func (e *ActionExecutor) initialize() error { defer e.ctx.beginExecutorRun(&e.driven)() - defer e.ctx.performingNode(e.performedAt, e.performedParams())() + defer e.ctx.performingNode(e.performedAt.Node(), e.performedParams())() if e.dynamicsKind != lower.NotStateSpace { return e.initializeDynamics() @@ -2065,7 +2065,7 @@ func (e *ActionExecutor) stepTokenAt(tokenIdx int) error { } token := &e.tokens[tokenIdx] - switch node := token.Location.(type) { + switch node := token.Node().(type) { case *ast.InitialNode: return e.stepInitialNode(tokenIdx) case *ast.FinalNode: @@ -2140,20 +2140,20 @@ func (e *ActionExecutor) synchronize(tokenIdx int) (int, bool) { // the earliest per awaited succession, or nil while one has yet to deliver; held is false // for a token no synchronization holds. func (e *ActionExecutor) arrivals(token Token) (consumed []int, held bool) { - if token.Via == (lower.ActionEdge{}) || token.body != nil || !synchronizes(token.Location) { + if token.Via == (lower.ActionEdge{}) || token.body != nil || !synchronizes(token.Node()) { return nil, false } - if e.starvedPin(token.frame, token.Location) { + if e.starvedPin(token.frame, token.Node()) { return nil, true } - _, join := token.Location.(*ast.JoinNode) - incoming := e.awaitedSuccessions(token.frame, token.Location) + _, join := token.Node().(*ast.JoinNode) + incoming := e.awaitedSuccessions(token.frame, token.Node()) if len(incoming) < 2 && !join { return nil, false } consumed = make([]int, 0, len(incoming)) for _, edge := range incoming { - idx, ok := e.arrival(token.frame, token.Location, edge, true) + idx, ok := e.arrival(token.frame, token.Node(), edge, true) if !ok { return nil, true } @@ -2190,7 +2190,7 @@ func (e *ActionExecutor) starvedPin(frame *actionFrame, node ast.Node) bool { var live map[ast.Node]bool for _, feature := range graph.Features[node] { edges := feeds[feature.Name] - if feature.Direction != ast.DirIn || len(edges) == 0 || len(frame.pending[node][feature.Name]) > 0 { + if feature.Direction != ast.DirIn || len(edges) == 0 || len(frame.pending[refOf(graph, node)][feature.Name]) > 0 { continue } if sym := memberSymbol(feature.Scope, feature.Node); sym != nil && e.ctx.admitsNoValue(sym) { @@ -2220,7 +2220,7 @@ func synchronizes(node ast.Node) bool { // passes over those the sweep in progress moved there, which arrive once it ends. func (e *ActionExecutor) arrival(frame *actionFrame, node ast.Node, edge lower.ActionEdge, settled bool) (int, bool) { for i, t := range e.tokens { - if t.Location == node && t.frame == frame && t.body == nil && t.Via == edge && + if t.Node() == node && t.frame == frame && t.body == nil && t.Via == edge && (!settled || !e.moving(t)) { return i, true } @@ -2295,12 +2295,12 @@ func (e *ActionExecutor) leaves(frame *actionFrame, node ast.Node, reached map[a // Awaiting returns the successions into the node token is held at that no token has // arrived over yet, nil for a token not held there. func (e *ActionExecutor) Awaiting(token Token) []lower.ActionEdge { - if token.Via == (lower.ActionEdge{}) || token.body != nil || !synchronizes(token.Location) { + if token.Via == (lower.ActionEdge{}) || token.body != nil || !synchronizes(token.Node()) { return nil } var awaited []lower.ActionEdge - for _, edge := range e.awaitedSuccessions(token.frame, token.Location) { - if _, delivered := e.arrival(token.frame, token.Location, edge, false); !delivered { + for _, edge := range e.awaitedSuccessions(token.frame, token.Node()) { + if _, delivered := e.arrival(token.frame, token.Node(), edge, false); !delivered { awaited = append(awaited, edge) } } @@ -2485,7 +2485,7 @@ func (e *ActionExecutor) readiness(id int64, eligible func(Token) bool) (ready b if e.dynamics != nil { return e.dynamicsDue(t), nil } - usage, ok := t.Location.(*ast.Usage) + usage, ok := t.Node().(*ast.Usage) if !ok || t.body != nil { return true, nil } @@ -2510,7 +2510,7 @@ func (e *ActionExecutor) readiness(id int64, eligible func(Token) bool) (ready b // tokenLabel names a token as the trace does, by ID and node. func (e *ActionExecutor) tokenLabel(id int64) string { if tok, ok := e.tokenByID(id); ok { - return fmt.Sprintf("%d@%s", id, nodeIdentifier(tok.Location)) + return fmt.Sprintf("%d@%s", id, nodeIdentifier(tok.Node())) } return fmt.Sprintf("%d", id) } @@ -2565,14 +2565,14 @@ func (e *ActionExecutor) stepTokens(schedule *tokenSchedule, paused []int64, ord func (e *ActionExecutor) stepInitialNode(tokenIdx int) error { token := &e.tokens[tokenIdx] graph := e.tokenGraph(tokenIdx) - if len(graph.Edges[token.Location]) == 0 { + if len(graph.Edges[token.Node()]) == 0 { return fmt.Errorf("%w: initial node has no successors", errInvalidActionFlow) } - if err := e.runNodeBody(token.frame, token.Location); err != nil { + if err := e.runNodeBody(token.frame, token.Node()); err != nil { return err } - successors, err := e.enabledSuccessions(token.frame, token.Location) + successors, err := e.enabledSuccessions(token.frame, token.Node()) if err != nil { return err } @@ -2621,7 +2621,7 @@ func (e *ActionExecutor) retireToken(tokenIdx int) error { // only: its branches go on reading and writing the action's own features. func (e *ActionExecutor) stepForkNode(tokenIdx int) error { token := &e.tokens[tokenIdx] - node := token.Location.(*ast.ForkNode) + node := token.Node().(*ast.ForkNode) frame := token.frame graph := e.graphOf(frame) @@ -2671,7 +2671,7 @@ func (e *ActionExecutor) split(tokenIdx int, successors []lower.ActionEdge) { for _, edge := range successors { newToken := Token{ ID: e.nextTokenID, - Location: edge.Target, + Location: refOf(e.graphOf(frame), edge.Target), Via: edge, moved: e.sweep, frame: frame, @@ -2691,7 +2691,7 @@ func (e *ActionExecutor) split(tokenIdx int, successors []lower.ActionEdge) { // already waited for its incoming successions — on to its successor. func (e *ActionExecutor) stepJoinNode(tokenIdx int) error { token := &e.tokens[tokenIdx] - node := token.Location.(*ast.JoinNode) + node := token.Node().(*ast.JoinNode) frame := token.frame graph := e.graphOf(frame) @@ -2729,9 +2729,9 @@ func (e *ActionExecutor) stepJoinNode(tokenIdx int) error { // arrival (Actions::MergeAction), so a loop re-enters it and a fork's branches each traverse it. func (e *ActionExecutor) stepMergeNode(tokenIdx int) error { token := &e.tokens[tokenIdx] - mergeNode, ok := token.Location.(*ast.MergeNode) + mergeNode, ok := token.Node().(*ast.MergeNode) if !ok { - return fmt.Errorf("expected MergeNode, got %T", token.Location) + return fmt.Errorf("expected MergeNode, got %T", token.Node()) } graph := e.graphOf(token.frame) @@ -2767,9 +2767,9 @@ func (e *ActionExecutor) stepMergeNode(tokenIdx int) error { // stepDecisionNode evaluates guards and routes token to matching branch. func (e *ActionExecutor) stepDecisionNode(tokenIdx int) error { token := &e.tokens[tokenIdx] - decisionNode, ok := token.Location.(*ast.DecisionNode) + decisionNode, ok := token.Node().(*ast.DecisionNode) if !ok { - return fmt.Errorf("expected DecisionNode, got %T", token.Location) + return fmt.Errorf("expected DecisionNode, got %T", token.Node()) } // Get successors (outgoing edges from decision) @@ -2895,7 +2895,7 @@ func (e *ActionExecutor) guardsStanding(graph *lower.ActionGraph, successors []l // performedParams is the action's own parameters where another action's node // performs it, nil where it runs on its own: the places the node's pins are. func (e *ActionExecutor) performedParams() map[*symbols.Symbol]bool { - if e.performedAt == nil || e.graph == nil { + if e.performedAt == (NodeRef{}) || e.graph == nil { return nil } params := make(map[*symbols.Symbol]bool, len(e.graph.Parameters)) @@ -2910,9 +2910,9 @@ func (e *ActionExecutor) performedParams() map[*symbols.Symbol]bool { // stepActionExecutionNode evaluates inline expression or invokes nested action. func (e *ActionExecutor) stepActionExecutionNode(tokenIdx int) error { token := &e.tokens[tokenIdx] - node, ok := token.Location.(*ast.ActionExecutionNode) + node, ok := token.Node().(*ast.ActionExecutionNode) if !ok { - return fmt.Errorf("expected ActionExecutionNode, got %T", token.Location) + return fmt.Errorf("expected ActionExecutionNode, got %T", token.Node()) } frame := token.frame @@ -2963,9 +2963,9 @@ func (e *ActionExecutor) leaveExecutionNode(tokenIdx int, frame *actionFrame, no // stepNestedAction performs a nested action usage in a frame of its own. func (e *ActionExecutor) stepNestedAction(tokenIdx int) error { token := &e.tokens[tokenIdx] - usage, ok := token.Location.(*ast.Usage) + usage, ok := token.Node().(*ast.Usage) if !ok { - return fmt.Errorf("expected Usage, got %T", token.Location) + return fmt.Errorf("expected Usage, got %T", token.Node()) } // An accept node waits for a message of its parameter's type. Until one @@ -3076,7 +3076,7 @@ func (e *ActionExecutor) awaitSignal(token *Token, accept lower.Accept, usage *a } token.Wait = nil if tr := e.trace(); tr != nil { - tr.recordActionAccept(e.traceOrigin(), token.ID, e.stepCount+1, nodeIdentifier(token.Location), + tr.recordActionAccept(e.traceOrigin(), token.ID, e.stepCount+1, nodeIdentifier(token.Node()), msg.Serial, acceptedEventName(msg), msg.Payload) } if accepting, err := e.materializeOccurrence(); err != nil { @@ -3425,7 +3425,7 @@ func (e *ActionExecutor) runDueSteps(progress *dueProgress, single bool) (bool, func (e *ActionExecutor) tokenPositions() map[int64]ast.Node { positions := make(map[int64]ast.Node, len(e.tokens)) for _, token := range e.tokens { - positions[token.ID] = token.Location + positions[token.ID] = token.Node() } return positions } @@ -3446,8 +3446,8 @@ func performerSuffix(self *Instance) string { // runs the statements lowering recorded for it, then leaves for its successor. func (e *ActionExecutor) stepStatementNode(tokenIdx int) error { token := e.tokens[tokenIdx] - step := e.beginStatementStep(token.frame, token.Location) - return e.runBody(tokenIdx, &statementWork{exec: e, token: token.ID, frame: token.frame, node: token.Location, step: step}) + step := e.beginStatementStep(token.frame, token.Node()) + return e.runBody(tokenIdx, &statementWork{exec: e, token: token.ID, frame: token.frame, node: token.Node(), step: step}) } // beginStatementStep is a token's step of a statement node in frame: a transparent performance @@ -3465,7 +3465,7 @@ func (e *ActionExecutor) beginStatementStep(frame *actionFrame, node ast.Node) * connections: frame.connections, data: make(map[string]Value), features: make(map[string]ast.FeatureDirection), - subactions: make(map[ast.Node]*actionFrame), + subactions: make(map[NodeRef]*actionFrame), perfs: &e.performances, run: frame.run, began: e.ctx.newActivation(), @@ -3595,7 +3595,7 @@ func (e *ActionExecutor) Tokens() []Token { type Traversal struct { Token int64 Edge lower.ActionEdge - Within []ast.Node + Within []NodeRef } // cloneTraversals copies ts, each path with it, so neither copy can alias the other. @@ -3801,7 +3801,7 @@ func (e *ActionExecutor) Breakpoints() []string { func (e *ActionExecutor) ReplaceBreakpointsAt(bps []NodeBreakpoint) { e.breakpointNodes = e.breakpointNodes[:0] for _, bp := range bps { - if bp.Node != nil { + if bp.Node != (NodeRef{}) { e.breakpointNodes = append(e.breakpointNodes, NodeBreakpoint{Within: slices.Clone(bp.Within), Node: bp.Node}) } } @@ -3861,13 +3861,13 @@ func (e *ActionExecutor) recordTokenAt(token *Token, edge *lower.ActionEdge) { if edge != nil { from = nodeIdentifier(edge.Source) } - tr.recordTokenStep(e.traceOrigin(), token.ID, e.stepCount+1, from, nodeIdentifier(token.Location)) + tr.recordTokenStep(e.traceOrigin(), token.ID, e.stepCount+1, from, nodeIdentifier(token.Node())) } // recordTokenWait records token parking where it is for awaited. func (e *ActionExecutor) recordTokenWait(token *Token, awaited string) { if tr := e.trace(); tr != nil { - tr.recordTokenWait(e.traceOrigin(), token.ID, e.stepCount+1, nodeIdentifier(token.Location), awaited) + tr.recordTokenWait(e.traceOrigin(), token.ID, e.stepCount+1, nodeIdentifier(token.Node()), awaited) } } @@ -3881,7 +3881,7 @@ func (e *ActionExecutor) recordDecide(token *Token, successors []lower.ActionEdg for i := range successors { alternatives[i] = branchName(successors, i) } - tr.recordDecide(e.traceOrigin(), token.ID, e.stepCount+1, nodeIdentifier(token.Location), alternatives, branchName(successors, pick)) + tr.recordDecide(e.traceOrigin(), token.ID, e.stepCount+1, nodeIdentifier(token.Node()), alternatives, branchName(successors, pick)) } // SetTrace sets the trace recorder for this executor and the context it diff --git a/internal/exec/runtime/action_executor_test.go b/internal/exec/runtime/action_executor_test.go index 41bd271f6c..a7cb237110 100644 --- a/internal/exec/runtime/action_executor_test.go +++ b/internal/exec/runtime/action_executor_test.go @@ -81,14 +81,14 @@ func assertDecisionRoutes(t *testing.T, ctx *Context, action *symbols.Symbol, de exec.initialize() exec.root.data["x"] = integerValue(x) exec.stepToken(0) // initial → decision - if exec.tokens[0].Location != decision { + if exec.tokens[0].Node() != decision { t.Fatal("token should be at decision node") } if err := exec.stepToken(0); err != nil { t.Fatalf("step decision: %v", err) } - if exec.tokens[0].Location != want { - t.Errorf("expected token at %s, got %T", wantName, exec.tokens[0].Location) + if exec.tokens[0].Node() != want { + t.Errorf("expected token at %s, got %T", wantName, exec.tokens[0].Node()) } } @@ -163,7 +163,7 @@ func TestActionExecutor_InitialNode(t *testing.T) { } token := exec.tokens[0] - if token.Location != initial { + if token.Node() != initial { t.Error("expected token at initial node") } @@ -199,7 +199,7 @@ func TestActionExecutor_FinalNode(t *testing.T) { t.Fatalf("expected 1 token after step, got %d", len(exec.tokens)) } - if exec.tokens[0].Location != final { + if exec.tokens[0].Node() != final { t.Error("expected token at final node") } @@ -261,7 +261,7 @@ func TestActionExecutor_ActionExecutionNode(t *testing.T) { } // Token should move to final node - if exec.tokens[0].Location != final { + if exec.tokens[0].Node() != final { t.Error("expected token at final node") } } @@ -302,7 +302,7 @@ func TestActionExecutor_ForkNode(t *testing.T) { // Verify all tokens at correct locations locations := make(map[ast.Node]bool) for _, token := range exec.tokens { - locations[token.Location] = true + locations[token.Node()] = true } if !locations[action1] || !locations[action2] || !locations[action3] { @@ -482,7 +482,7 @@ func TestActionExecutor_JoinNode(t *testing.T) { } for i, token := range exec.tokens { - if token.Location != join { + if token.Node() != join { t.Errorf("token %d not at join node", i) } } @@ -499,7 +499,7 @@ func TestActionExecutor_JoinNode(t *testing.T) { } // Token should be at final node - if exec.tokens[0].Location != final { + if exec.tokens[0].Node() != final { t.Error("expected token at final node") } @@ -552,7 +552,7 @@ func TestActionExecutor_JoinNode_PartialArrival(t *testing.T) { // Now 2 tokens at join, 1 still at task3 tokensAtJoin := 0 for _, tok := range exec.tokens { - if _, ok := tok.Location.(*ast.JoinNode); ok { + if _, ok := tok.Node().(*ast.JoinNode); ok { tokensAtJoin++ } } @@ -577,7 +577,7 @@ func TestActionExecutor_JoinNode_PartialArrival(t *testing.T) { // Now all 3 tokens at join tokensAtJoin = 0 for _, tok := range exec.tokens { - if _, ok := tok.Location.(*ast.JoinNode); ok { + if _, ok := tok.Node().(*ast.JoinNode); ok { tokensAtJoin++ } } @@ -596,7 +596,7 @@ func TestActionExecutor_JoinNode_PartialArrival(t *testing.T) { } // Token should be at final node - if exec.tokens[0].Location != final { + if exec.tokens[0].Node() != final { t.Error("expected token at final node after join fires") } } @@ -633,7 +633,7 @@ func TestActionExecutor_MergeNode(t *testing.T) { // Both tokens should be at merge for i, tok := range exec.tokens { - if tok.Location != merge { + if tok.Node() != merge { t.Errorf("token %d not at merge node", i) } } @@ -648,10 +648,10 @@ func TestActionExecutor_MergeNode(t *testing.T) { mergeIdx := -1 tokenAtFinal := false for i, tok := range exec.tokens { - if tok.Location == final { + if tok.Node() == final { tokenAtFinal = true } - if tok.Location == merge { + if tok.Node() == merge { mergeIdx = i } } @@ -667,8 +667,8 @@ func TestActionExecutor_MergeNode(t *testing.T) { t.Fatalf("expected 2 tokens after both traversed the merge, got %d", len(exec.tokens)) } for i, tok := range exec.tokens { - if tok.Location != final { - t.Errorf("token %d at %T, want the final node", i, tok.Location) + if tok.Node() != final { + t.Errorf("token %d at %T, want the final node", i, tok.Node()) } } } @@ -718,8 +718,8 @@ func TestActionExecutor_MergeNode_ControlOnly(t *testing.T) { // Verify both at merge for i, tok := range exec.tokens { - if _, ok := tok.Location.(*ast.MergeNode); !ok { - t.Fatalf("token %d not at merge: %T", i, tok.Location) + if _, ok := tok.Node().(*ast.MergeNode); !ok { + t.Fatalf("token %d not at merge: %T", i, tok.Node()) } } @@ -738,7 +738,7 @@ func TestActionExecutor_MergeNode_ControlOnly(t *testing.T) { } mergeIdx := -1 for i, tok := range exec.tokens { - if _, ok := tok.Location.(*ast.MergeNode); ok { + if _, ok := tok.Node().(*ast.MergeNode); ok { mergeIdx = i } } @@ -752,8 +752,8 @@ func TestActionExecutor_MergeNode_ControlOnly(t *testing.T) { t.Fatalf("expected 2 tokens at final after both traversed, got %d", len(exec.tokens)) } for i, tok := range exec.tokens { - if _, ok := tok.Location.(*ast.FinalNode); !ok { - t.Errorf("token %d at %T, want the final node", i, tok.Location) + if _, ok := tok.Node().(*ast.FinalNode); !ok { + t.Errorf("token %d at %T, want the final node", i, tok.Node()) } } @@ -787,7 +787,7 @@ func TestActionExecutor_MergeNode_SingleParent(t *testing.T) { t.Fatalf("expected 1 token, got %d", len(exec.tokens)) } - if exec.tokens[0].Location != final { + if exec.tokens[0].Node() != final { t.Error("token should pass through merge to final") } } @@ -1065,7 +1065,7 @@ func TestActionExecutor_Step(t *testing.T) { if err != nil { t.Fatalf("step 1: %v", err) } - if exec.tokens[0].Location != action { + if exec.tokens[0].Node() != action { t.Error("expected token at action node after step 1") } @@ -1074,7 +1074,7 @@ func TestActionExecutor_Step(t *testing.T) { if err != nil { t.Fatalf("step 2: %v", err) } - if exec.tokens[0].Location != final { + if exec.tokens[0].Node() != final { t.Error("expected token at final node after step 2") } @@ -1176,7 +1176,7 @@ func TestActionExecutor_Deadlock_JoinStarvation(t *testing.T) { // Find path2 token path2Idx := -1 for i, tok := range exec.tokens { - if n, ok := tok.Location.(*ast.ActionExecutionNode); ok && n.Name == "path2" { + if n, ok := tok.Node().(*ast.ActionExecutionNode); ok && n.Name == "path2" { path2Idx = i break } @@ -1599,7 +1599,7 @@ func TestActionExecutor_GuardedSuccession_GuardHolds(t *testing.T) { if len(exec.tokens) != 1 { t.Fatalf("expected the token to live on, %d left", len(exec.tokens)) } - if name := ActionNodeName(exec.tokens[0].Location); name != "s2" { + if name := ActionNodeName(exec.tokens[0].Node()); name != "s2" { t.Errorf("token at %s, want s2", name) } } @@ -1724,7 +1724,7 @@ func TestActionExecutor_GuardedSuccession_PrunesAForkBranch(t *testing.T) { locations := make(map[ast.Node]bool, len(exec.tokens)) for _, token := range exec.tokens { - locations[token.Location] = true + locations[token.Node()] = true } if len(exec.tokens) != 2 || !locations[task1] || !locations[task3] { t.Errorf("fork produced %d tokens at %v, want one each at task1 and task3", len(exec.tokens), locations) @@ -1759,7 +1759,7 @@ func TestActionExecutor_GuardedSuccession_TwoGuardsHold(t *testing.T) { } locations := make(map[ast.Node]bool, len(exec.tokens)) for _, token := range exec.tokens { - locations[token.Location] = true + locations[token.Node()] = true } if len(exec.tokens) != 2 || !locations[s2] || !locations[s3] { t.Errorf("s1 left %d tokens at %v, want one each at s2 and s3", len(exec.tokens), locations) @@ -1803,7 +1803,7 @@ func TestActionExecutor_GuardedSuccession_PrunedMergeStaysOpen(t *testing.T) { step := func(at ast.Node) { t.Helper() for i := range exec.tokens { - if exec.tokens[i].Location == at { + if exec.tokens[i].Node() == at { if err := exec.stepToken(i); err != nil { t.Fatalf("step %s: %v", ActionNodeName(at), err) } diff --git a/internal/exec/runtime/action_frame.go b/internal/exec/runtime/action_frame.go index 0f9325c456..ced12a764a 100644 --- a/internal/exec/runtime/action_frame.go +++ b/internal/exec/runtime/action_frame.go @@ -69,7 +69,7 @@ type performanceOwner interface { // every frame: what is around the root may also keep it for a caller. returnAround(name string, value Value) (bool, error) // pauseAt pauses the run before node, in the flow of within, performs where a breakpoint is set on it. - pauseAt(within []ast.Node, node ast.Node) error + pauseAt(within []NodeRef, node NodeRef) error // runOwnFlow runs the flow perf's node states of its own to completion. runOwnFlow(perf *actionFrame) error // endsOwn reports whether a terminate may end the root performance: an action's @@ -161,22 +161,22 @@ type actionFrame struct { outputs []string // subactions holds the latest performance of each node of graph, which is // what a read of the node's pins by name sees. - subactions map[ast.Node]*actionFrame + subactions map[NodeRef]*actionFrame // repeatedPerfs holds every performance of a repeated node, by repetition // index, so a read of its features from outside sees them all. - repeatedPerfs map[ast.Node][]*actionFrame + repeatedPerfs map[NodeRef][]*actionFrame // pending queues what flows and bindings delivered to a node's pins ahead of // its performances, each of which takes the oldest delivery at each pin. - pending map[ast.Node]map[string][]Value + pending map[NodeRef]map[string][]Value // held queues what object flows delivered to the inputs of a fork, join or merge // until a token passes it, each value with the flow that brought it. - held map[ast.Node]map[string][]nodeObject + held map[NodeRef]map[string][]nodeObject // staged locates, per target node and pin, the queued value each streaming source // performance's latest write left, which the source's next write replaces. - staged map[ast.Node]map[string][]stagedStream + staged map[NodeRef]map[string][]stagedStream // nested queues deliveries to pins of the nodes under a node (`leg.inner.v`) ahead // of its next performance, which forwards them to its own nodes. - nested map[ast.Node][]nestedDelivery + nested map[NodeRef][]nestedDelivery // ended marks a performance that has completed, so a delivery to a node under it // waits for the next performance rather than reaching one that is over. ended bool @@ -185,10 +185,10 @@ type actionFrame struct { streamed map[string]bool // unreceived holds, per target node, the values streaming flows carried after the // node's latest performance ended; a later performance takes each, else it is reported. - unreceived map[ast.Node][]unreceivedStream + unreceived map[NodeRef][]unreceivedStream // nodes are the action nodes a state behavior's performance runs, which its body's // blocks declare; the frames of a state machine and its states hold none. - nodes []ast.Node + nodes []NodeRef // label names a performance that is no action node's in a diagnostic: a state // machine's, a state's or a state behavior's (state_statements.go). label string @@ -196,17 +196,62 @@ type actionFrame struct { // within lists the nested action nodes whose performances enclose this one, itself // included, outermost first; empty for the action's own and for no frame at all. -func (f *actionFrame) within() []ast.Node { - var chain []ast.Node +func (f *actionFrame) within() []NodeRef { + var chain []NodeRef for ; f != nil; f = f.parent { if f.node != nil && !f.body { - chain = append(chain, f.node) + chain = append(chain, f.ref()) } } slices.Reverse(chain) return chain } +// flowOf is the flow the performance's node is a node of: its own flow field, else +// the flow of the performance around it; nil for the action's own. +func (f *actionFrame) flowOf() *lower.ActionGraph { + if f.flow != nil || f.parent == nil { + return f.flow + } + return f.parent.graph +} + +// ref references the performance's node in the flow it is a node of; the zero +// reference for the action's own. +func (f *actionFrame) ref() NodeRef { + if f.node == nil { + return NodeRef{} + } + return refOf(f.flowOf(), f.node) +} + +// refUnder references node among those a performance of f holds as its own (see +// nodesNamed); the zero reference for one it does not. +func (f *actionFrame) refUnder(node ast.Node) NodeRef { + switch { + case f.graph != nil: + if ref, ok := locateNode(f.graph, node); ok { + return ref + } + case f.flow != nil: + if flow := lower.NestedFlow(f.flow, f.node, node); flow != nil { + return refOf(flow, node) + } + default: + for _, ref := range f.nodes { + if ref.Node() == node { + return ref + } + } + } + if f.performs != nil { + if ref, ok := locateNode(f.performs, node); ok { + return ref + } + } + return NodeRef{} +} + // unreceivedStream is a value a streaming flow carried to a target whose performances // were all over: it waits at position at in the pending queue of the target's pin. type unreceivedStream struct { @@ -266,7 +311,7 @@ func (e *ActionExecutor) newRootFrame(shape *performanceShape) *actionFrame { unvalued: shape.unvalued, shadowed: shape.shadowed, sharedShape: true, - subactions: make(map[ast.Node]*actionFrame), + subactions: make(map[NodeRef]*actionFrame), perfs: &e.performances, run: e.ctx.newRun(), } @@ -511,7 +556,7 @@ func (e *performances) beginPerformance( perf.graph = graph perf.live = 1 perf.connections = joinConnections(parent.connections, graph.Connections) - perf.subactions = make(map[ast.Node]*actionFrame) + perf.subactions = make(map[NodeRef]*actionFrame) } } pins, err := e.nodePins(flow, node) @@ -537,15 +582,16 @@ func (e *performances) beginPerformance( // The performance is ongoing before anything seeding it streams, so a value carried // back to its node reaches it rather than waiting for a later performance. if parent.subactions == nil { - parent.subactions = make(map[ast.Node]*actionFrame) + parent.subactions = make(map[NodeRef]*actionFrame) } - previous, hadPrevious := parent.subactions[node] - parent.subactions[node] = perf + at := refOf(flow, node) + previous, hadPrevious := parent.subactions[at] + parent.subactions[at] = perf if err := e.seedPerformance(parent, flow, node, perf); err != nil { if hadPrevious { - parent.subactions[node] = previous + parent.subactions[at] = previous } else { - delete(parent.subactions, node) + delete(parent.subactions, at) } return nil, err } @@ -585,7 +631,7 @@ func (e *performances) seedPerformance(parent *actionFrame, flow *lower.ActionGr cell.binding.masked = feature.Name cell.binding.place = featurePlace(feature) } - if err := e.takeDeliveries(parent, node, perf); err != nil { + if err := e.takeDeliveries(parent, refOf(flow, node), perf); err != nil { return err } // The arguments, bindings and defaults are one evaluation: a calc usage two of them @@ -753,13 +799,13 @@ func (e *performances) endPerformance(perf *actionFrame) error { // checkStreamsReceived reports a value a streaming flow of frame's nodes carried after // its target's last performance ended, which no later performance of the target took. func checkStreamsReceived(frame *actionFrame) error { - nodes := sortedNodes(frame.unreceived) + nodes := sortedRefs(frame.unreceived) if len(nodes) == 0 { return nil } stream := frame.unreceived[nodes[0]][0] return fmt.Errorf("%w: %s: %s completed before %s wrote %s", - errStreamUnreceived, flowDescription(stream.flow), nodeDescription(nodes[0]), + errStreamUnreceived, flowDescription(stream.flow), nodeDescription(nodes[0].Node()), nodeDescription(stream.source), orAnyPin(stream.flow.SourcePin)) } @@ -940,15 +986,15 @@ func (f *actionFrame) lexicalFrames() []frame { // nodesNamed returns the nodes named name among the performance's subactions: its // flow's nodes, those its bodies' blocks declare, and those of the action it performed. -func (f *actionFrame) nodesNamed(name string) []ast.Node { - var named []ast.Node +func (f *actionFrame) nodesNamed(name string) []NodeRef { + var named []NodeRef switch { case f.graph != nil: named = flowNodesNamed(f.graph, name) case f.flow != nil: - named = usagesNamed(f.flow.BlockNodes[f.node], name) + named = usageRefsNamed(blockNodeRefs(f.flow.Bodies[f.node]), name) default: - named = usagesNamed(f.nodes, name) + named = usageRefsNamed(f.nodes, name) } if f.performs != nil { named = append(named, flowNodesNamed(f.performs, name)...) @@ -958,15 +1004,8 @@ func (f *actionFrame) nodesNamed(name string) []ast.Node { // flowNodesNamed returns the nodes of graph named name: its own, then those the // blocks of its statement nodes declare. -func flowNodesNamed(graph *lower.ActionGraph, name string) []ast.Node { - named := usagesNamed(graph.Nodes, name) - for _, node := range graph.Nodes { - if _, isUsage := node.(*ast.Usage); isUsage { - continue - } - named = append(named, usagesNamed(graph.BlockNodes[node], name)...) - } - return named +func flowNodesNamed(graph *lower.ActionGraph, name string) []NodeRef { + return usageRefsNamed(flowNodeRefs(graph), name) } // usagesNamed returns the usages among nodes named name. @@ -990,8 +1029,8 @@ func (f *actionFrame) subaction(name string, decl ast.Node) (perf *actionFrame, return nil, false, nil } node := named[0] - if slices.Contains(named, decl) { - node = decl + if i := slices.IndexFunc(named, func(ref NodeRef) bool { return ref.Node() == decl }); i >= 0 { + node = named[i] } else { for _, candidate := range named { performed, ok := f.subactions[candidate] @@ -1003,7 +1042,7 @@ func (f *actionFrame) subaction(name string, decl ast.Node) (perf *actionFrame, perf, performed := f.subactions[node] if !performed { return nil, true, fmt.Errorf("%w: action node %s has not been performed yet", - errNodeNotPerformed, ActionNodeName(node)) + errNodeNotPerformed, ActionNodeName(node.Node())) } return perf, true, nil } @@ -1014,7 +1053,7 @@ func (f *actionFrame) repetitionSiblings() []*actionFrame { if f == nil || f.parent == nil { return nil } - return f.parent.repeatedPerfs[f.node] + return f.parent.repeatedPerfs[f.ref()] } // repeatedStep reports whether the performance is one of a step declared to run @@ -1135,13 +1174,14 @@ func (e *performances) deliver(f *actionFrame, flow *lower.ActionGraph, node ast if err := e.checkNestedDelivery(flow, node, path, pin, &value); err != nil { return err } - if sub, performed := f.subactions[node]; performed && !sub.ended { + at := refOf(flow, node) + if sub, performed := f.subactions[at]; performed && !sub.ended { return e.deliver(sub, lower.NestedFlow(flow, node, path[0]), path[0], path[1:], pin, value) } if f.nested == nil { - f.nested = make(map[ast.Node][]nestedDelivery) + f.nested = make(map[NodeRef][]nestedDelivery) } - f.nested[node] = append(f.nested[node], nestedDelivery{path: path, pin: pin, value: value}) + f.nested[at] = append(f.nested[at], nestedDelivery{path: path, pin: pin, value: value}) return nil } pins, err := e.nodePins(flow, node) @@ -1154,14 +1194,14 @@ func (e *performances) deliver(f *actionFrame, flow *lower.ActionGraph, node ast if err := e.ctx.checkNamedWriteAs(flow.Scopes[node], func() string { return nodeDescription(node) }, pin, &value); err != nil { return err } - f.queue(node, canonical(pins.aliases, pin), value) + f.queue(refOf(flow, node), canonical(pins.aliases, pin), value) return nil } // queue appends a value to the pending queue of node's pin, returning its position. -func (f *actionFrame) queue(node ast.Node, pin string, value Value) int { +func (f *actionFrame) queue(node NodeRef, pin string, value Value) int { if f.pending == nil { - f.pending = make(map[ast.Node]map[string][]Value) + f.pending = make(map[NodeRef]map[string][]Value) } if f.pending[node] == nil { f.pending[node] = make(map[string][]Value) @@ -1173,7 +1213,7 @@ func (f *actionFrame) queue(node ast.Node, pin string, value Value) int { // stage queues a value flow streamed from source's from pin ahead of node's next // performance, or replaces the one an earlier write to from left waiting there through // the same flow; two flows out of one pin each keep a place. Reports whether it appended. -func (f *actionFrame) stage(node ast.Node, pin string, source *actionFrame, from string, flow ast.Node, value Value) bool { +func (f *actionFrame) stage(node NodeRef, pin string, source *actionFrame, from string, flow ast.Node, value Value) bool { queue := f.pending[node][pin] for _, s := range f.staged[node][pin] { if source != nil && s.source == source && s.pin == from && s.flow == flow && s.at < len(queue) { @@ -1183,7 +1223,7 @@ func (f *actionFrame) stage(node ast.Node, pin string, source *actionFrame, from } at := f.queue(node, pin, value) if f.staged == nil { - f.staged = make(map[ast.Node]map[string][]stagedStream) + f.staged = make(map[NodeRef]map[string][]stagedStream) } if f.staged[node] == nil { f.staged[node] = make(map[string][]stagedStream) @@ -1215,7 +1255,7 @@ func (e *performances) checkNestedDelivery(flow *lower.ActionGraph, node ast.Nod // takeDeliveries moves the oldest delivery at each pin of node into perf, so that // performances of one node begun in turn each start with their own inputs, streams // each on along the pin's flows, and forwards what waits for the nodes under it. -func (e *performances) takeDeliveries(f *actionFrame, node ast.Node, perf *actionFrame) error { +func (e *performances) takeDeliveries(f *actionFrame, node NodeRef, perf *actionFrame) error { queues := f.pending[node] taken := make(map[string]Value, len(queues)) for pin, values := range queues { @@ -1240,7 +1280,7 @@ func (e *performances) takeDeliveries(f *actionFrame, node ast.Node, perf *actio nested := f.nested[node] delete(f.nested, node) for _, d := range nested { - if err := e.deliver(perf, lower.NestedFlow(perf.flow, node, d.path[0]), d.path[0], d.path[1:], d.pin, d.value); err != nil { + if err := e.deliver(perf, lower.NestedFlow(perf.flow, perf.node, d.path[0]), d.path[0], d.path[1:], d.pin, d.value); err != nil { return err } } @@ -1249,7 +1289,7 @@ func (e *performances) takeDeliveries(f *actionFrame, node ast.Node, perf *actio // shiftStaged moves what is staged at node's pin one place toward the front of the // pin's queue, as a performance takes the oldest delivery, dropping what it took. -func (f *actionFrame) shiftStaged(node ast.Node, pin string) { +func (f *actionFrame) shiftStaged(node NodeRef, pin string) { var kept []stagedStream for _, s := range f.staged[node][pin] { if s.at--; s.at >= 0 { @@ -1265,7 +1305,7 @@ func (f *actionFrame) shiftStaged(node ast.Node, pin string) { // receiveStream notes that a performance of node took the oldest value at pin: the // streamed values behind it move up, and the one taken is received. -func (f *actionFrame) receiveStream(node ast.Node, pin string) { +func (f *actionFrame) receiveStream(node NodeRef, pin string) { var kept []unreceivedStream for _, s := range f.unreceived[node] { if s.pin == pin { @@ -1367,13 +1407,14 @@ func (e *performances) streamFlow( if perf != nil { from = perf.key(from) } - appended := frame.stage(flow.Target, pin, perf, from, flow.Decl, value) - if latest := frame.subactions[flow.Target]; appended && latest != nil && latest.ended { + target := refOf(graph, flow.Target) + appended := frame.stage(target, pin, perf, from, flow.Decl, value) + if latest := frame.subactions[target]; appended && latest != nil && latest.ended { if frame.unreceived == nil { - frame.unreceived = make(map[ast.Node][]unreceivedStream) + frame.unreceived = make(map[NodeRef][]unreceivedStream) } - frame.unreceived[flow.Target] = append(frame.unreceived[flow.Target], unreceivedStream{ - flow: flow, source: source, pin: pin, at: len(frame.pending[flow.Target][pin]) - 1, + frame.unreceived[target] = append(frame.unreceived[target], unreceivedStream{ + flow: flow, source: source, pin: pin, at: len(frame.pending[target][pin]) - 1, }) } return nil @@ -1625,7 +1666,7 @@ func (f *actionFrame) collect(prefix string, into map[string]Value) error { func (f *actionFrame) latestSubactions() map[string]*actionFrame { latest := make(map[string]*actionFrame) for node, sub := range f.subactions { - name := ActionNodeName(node) + name := ActionNodeName(node.Node()) if name == "" { continue } @@ -1966,12 +2007,12 @@ func (e *performances) otherPerformance(perf *actionFrame, end boundEnd) (other if node == nil { return holder, true } - other, performed = holder.subactions[node] + other, performed = holder.subactions[holder.refUnder(node)] for _, node := range path { if !performed { return nil, false } - other, performed = other.subactions[node] + other, performed = other.subactions[other.refUnder(node)] } return other, performed } @@ -2197,8 +2238,8 @@ func (e *performances) beginInvocation(perf *actionFrame, inv actionInvocation) return nil, fmt.Errorf("invoke action %s: %w", inv.name(), err) } callee.name, callee.out, callee.performer = inv.name(), out, perf - if callee.exec.performedAt == nil { - callee.exec.performedAt = perf.node + if callee.exec.performedAt == (NodeRef{}) { + callee.exec.performedAt = perf.ref() } return callee, nil } @@ -2267,7 +2308,7 @@ func (f *actionFrame) adopt(callee *ActionExecutor) { } f.performs = callee.graph if len(callee.root.subactions) > 0 && f.subactions == nil { - f.subactions = make(map[ast.Node]*actionFrame, len(callee.root.subactions)) + f.subactions = make(map[NodeRef]*actionFrame, len(callee.root.subactions)) } for node, sub := range callee.root.subactions { sub.parent = f @@ -2317,11 +2358,11 @@ func (e *performances) bodyCells(perf *actionFrame) *bodyCells { } // cloneUnreceived copies the unreceived streams of a frame, queues included. -func cloneUnreceived(m map[ast.Node][]unreceivedStream) map[ast.Node][]unreceivedStream { +func cloneUnreceived(m map[NodeRef][]unreceivedStream) map[NodeRef][]unreceivedStream { if m == nil { return nil } - out := make(map[ast.Node][]unreceivedStream, len(m)) + out := make(map[NodeRef][]unreceivedStream, len(m)) for node, streams := range m { out[node] = slices.Clone(streams) } diff --git a/internal/exec/runtime/action_node_ref.go b/internal/exec/runtime/action_node_ref.go new file mode 100644 index 0000000000..fd62bdce02 --- /dev/null +++ b/internal/exec/runtime/action_node_ref.go @@ -0,0 +1,158 @@ +package runtime + +import ( + "slices" + "strings" + + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/ir/program" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// NodeRef names one node of an action graph by the id the graph gives it: what the +// executor's records hold in place of the node, which Node reads back through the graph. +type NodeRef struct { + Graph *lower.ActionGraph + ID program.NodeID +} + +// refOf is the reference to node in graph; the zero id where graph does not index node. +func refOf(graph *lower.ActionGraph, node ast.Node) NodeRef { + if graph == nil { + return NodeRef{} + } + return NodeRef{Graph: graph, ID: graph.ID[node]} +} + +// refsOf references each of nodes in graph, in order. +func refsOf(graph *lower.ActionGraph, nodes []ast.Node) []NodeRef { + refs := make([]NodeRef, len(nodes)) + for i, node := range nodes { + refs[i] = refOf(graph, node) + } + return refs +} + +// Node is the declaration the reference names, nil for none. +func (r NodeRef) Node() ast.Node { + if r.Graph == nil { + return nil + } + return r.Graph.Provenance.Node(r.ID) +} + +// NodesOf is the declarations the references name, in order. +func NodesOf(refs []NodeRef) []ast.Node { + if refs == nil { + return nil + } + nodes := make([]ast.Node, len(refs)) + for i, ref := range refs { + nodes[i] = ref.Node() + } + return nodes +} + +// BreakpointAt resolves a breakpoint on node, in the flow of the nested action nodes within +// (outermost first), to ids; false where a node is not one a performance of the one before holds. +func (e *ActionExecutor) BreakpointAt(within []ast.Node, node ast.Node) (NodeBreakpoint, bool) { + refs := make([]NodeRef, 0, len(within)+1) + for i, n := range append(slices.Clone(within), node) { + var ref NodeRef + var ok bool + if i == 0 { + ref, ok = locateNode(e.graph, n) + } else { + ref, ok = nestedRef(refs[i-1].Graph, within[i-1], n) + } + if !ok { + return NodeBreakpoint{}, false + } + refs = append(refs, ref) + } + return NodeBreakpoint{Within: refs[:len(within)], Node: refs[len(within)]}, true +} + +// nestedRef references node among those a performance of owner, a node of flow, holds +// as its own: the nodes of the flow owner owns, or of the block flows of its body. +func nestedRef(flow *lower.ActionGraph, owner, node ast.Node) (NodeRef, bool) { + if sub := flow.Subflows[owner]; sub != nil && sub.Graph != nil { + if ref, ok := locateNode(sub.Graph, node); ok { + return ref, true + } + } + if block := lower.NestedFlow(flow, owner, node); block != nil { + return refOf(block, node), true + } + return NodeRef{}, false +} + +// locateNode references node where a performance of flow runs it: as a node of flow's +// own, or of a block flow the body of one of its statement nodes states. +func locateNode(flow *lower.ActionGraph, node ast.Node) (NodeRef, bool) { + if flow == nil { + return NodeRef{}, false + } + if id, ok := flow.ID[node]; ok { + return NodeRef{Graph: flow, ID: id}, true + } + for _, owner := range flow.Nodes { + if _, isUsage := owner.(*ast.Usage); isUsage || !slices.Contains(flow.BlockNodes[owner], node) { + continue + } + if block := lower.NestedFlow(flow, owner, node); block != nil { + return refOf(block, node), true + } + } + return NodeRef{}, false +} + +// blockNodeRefs references the action nodes the blocks among stmts declare, each in +// the block flow it is a node of: lower.BlockNodes(stmts) by reference. +func blockNodeRefs(stmts []lower.Statement) []NodeRef { + var refs []NodeRef + for _, flow := range lower.BlockFlows(stmts) { + for _, node := range flow.Nodes { + if _, isUsage := node.(*ast.Usage); isUsage && !flow.StatementRuns[node] { + refs = append(refs, refOf(flow, node)) + continue + } + refs = append(refs, blockNodeRefs(flow.Bodies[node])...) + } + } + return refs +} + +// flowNodeRefs references the nodes of graph: its own, then those the blocks of its +// statement nodes declare. +func flowNodeRefs(graph *lower.ActionGraph) []NodeRef { + refs := refsOf(graph, graph.Nodes) + for _, node := range graph.Nodes { + if _, isUsage := node.(*ast.Usage); isUsage { + continue + } + refs = append(refs, blockNodeRefs(graph.Bodies[node])...) + } + return refs +} + +// usageRefsNamed returns the usages among refs named name. +func usageRefsNamed(refs []NodeRef, name string) []NodeRef { + var named []NodeRef + for _, ref := range refs { + if _, isUsage := ref.Node().(*ast.Usage); isUsage && slices.Contains(actionNodeNames(ref.Node()), name) { + named = append(named, ref) + } + } + return named +} + +// sortedRefs orders the keys of a table by the nodes they name, as a spelling lists them. +func sortedRefs[V any](m map[NodeRef]V) []NodeRef { + refs := make([]NodeRef, 0, len(m)) + for ref := range m { + refs = append(refs, ref) + } + slices.SortFunc(refs, func(a, b NodeRef) int { return strings.Compare(nodeKey(a.Node()), nodeKey(b.Node())) }) + return refs +} diff --git a/internal/exec/runtime/action_statements.go b/internal/exec/runtime/action_statements.go index 38a26d8585..f1e6c0a5a2 100644 --- a/internal/exec/runtime/action_statements.go +++ b/internal/exec/runtime/action_statements.go @@ -315,7 +315,7 @@ func (e *performances) performNode(parent *actionFrame, engine *stmtEngine, grap } if !resumed || f.recheck { f.recheck = false - if err := e.owner.pauseAt(parent.within(), node); err != nil { + if err := e.owner.pauseAt(parent.within(), refOf(graph, node)); err != nil { return flowNext, e.ctx.pausing(f, err) } } @@ -458,7 +458,7 @@ func (e *performances) performBlockFlow(parent *actionFrame, engine *stmtEngine, connections: joinConnections(parent.connections, block.Graph.Connections), data: make(map[string]Value), features: make(map[string]ast.FeatureDirection), - subactions: make(map[ast.Node]*actionFrame), + subactions: make(map[NodeRef]*actionFrame), perfs: e, run: e.ctx.newRun(), live: 1, diff --git a/internal/exec/runtime/action_step_multiplicity.go b/internal/exec/runtime/action_step_multiplicity.go index ed6b0eec18..0b52e5e4e2 100644 --- a/internal/exec/runtime/action_step_multiplicity.go +++ b/internal/exec/runtime/action_step_multiplicity.go @@ -111,23 +111,24 @@ func (e *ActionExecutor) trackRepeated(tokenID int64, perf *actionFrame) { } frame := token.frame if frame.repeatedPerfs == nil { - frame.repeatedPerfs = make(map[ast.Node][]*actionFrame) + frame.repeatedPerfs = make(map[NodeRef][]*actionFrame) } - perfs := frame.repeatedPerfs[perf.node] + perfs := frame.repeatedPerfs[perf.ref()] for int64(len(perfs)) < token.repetition { perfs = append(perfs, nil) } perfs[token.repetition-1] = perf - frame.repeatedPerfs[perf.node] = perfs + frame.repeatedPerfs[perf.ref()] = perfs } // recordRepetition notes perf as the next performance of the repeated node it // performs, where performances begin sequentially rather than on sibling tokens. func (e *performances) recordRepetition(parent *actionFrame, node ast.Node, perf *actionFrame) { if parent.repeatedPerfs == nil { - parent.repeatedPerfs = make(map[ast.Node][]*actionFrame) + parent.repeatedPerfs = make(map[NodeRef][]*actionFrame) } - perfs := parent.repeatedPerfs[node] + at := perf.ref() + perfs := parent.repeatedPerfs[at] perf.repetition = int64(len(perfs)) + 1 - parent.repeatedPerfs[node] = append(perfs, perf) + parent.repeatedPerfs[at] = append(perfs, perf) } diff --git a/internal/exec/runtime/action_subflow.go b/internal/exec/runtime/action_subflow.go index ba87596ed9..fb8cf70077 100644 --- a/internal/exec/runtime/action_subflow.go +++ b/internal/exec/runtime/action_subflow.go @@ -58,7 +58,7 @@ func unfoldSubflow(sub *lower.Subflow, node ast.Node) (*lower.ActionGraph, error // initialize(), so an unbuildable one is an error here. func (e *ActionExecutor) enterSubflow(tokenIdx int, perf *actionFrame) error { token := &e.tokens[tokenIdx] - node := token.Location + node := token.Node() if perf.graph != nil && perf.graph.Invalid != nil { return fmt.Errorf("%w: action node %s: %w", errInvalidActionFlow, ActionNodeName(node), perf.graph.Invalid) @@ -86,7 +86,7 @@ func (e *ActionExecutor) enterSubflow(tokenIdx int, perf *actionFrame) error { if len(starts) == 0 { return e.leaveSubflow(tokenIdx) } - token.Location = starts[0] + token.Location = refOf(perf.graph, starts[0]) e.recordTokenAt(token, nil) e.seedTokens(perf, starts[1:], e.sweep) return nil @@ -96,7 +96,7 @@ func (e *ActionExecutor) enterSubflow(tokenIdx int, perf *actionFrame) error { // performance of it starts at concurrently (lower.StartFlow). func (e *ActionExecutor) seedTokens(frame *actionFrame, starts []ast.Node, moved uint64) { for _, start := range starts { - e.tokens = append(e.tokens, Token{ID: e.nextTokenID, Location: start, frame: frame, moved: moved}) + e.tokens = append(e.tokens, Token{ID: e.nextTokenID, Location: refOf(frame.graph, start), frame: frame, moved: moved}) e.nextTokenID++ e.recordTokenAt(&e.tokens[len(e.tokens)-1], nil) } @@ -387,7 +387,7 @@ func (e *ActionExecutor) drawOneMove(perf *actionFrame) (acted, performed bool, did, err = e.stepTokenNoting(i, &order) schedule.Acted(id, did) if did { - acted, performed = true, e.performs(t.frame, t.Location) + acted, performed = true, e.performs(t.frame, t.Node()) } if err != nil || did { break @@ -457,16 +457,16 @@ func (e *ActionExecutor) silentMove(t Token) bool { if t.body != nil { return false } - switch t.Location.(type) { + switch t.Node().(type) { case *ast.InitialNode, *ast.ForkNode, *ast.JoinNode, *ast.MergeNode: default: return false } graph := e.graphOf(t.frame) - if len(graph.Bodies[t.Location]) > 0 { + if len(graph.Bodies[t.Node()]) > 0 { return false } - for _, edge := range graph.Edges[t.Location] { + for _, edge := range graph.Edges[t.Node()] { if edge.Guard != nil || edge.Probability != nil { return false } @@ -507,7 +507,7 @@ func (e *ActionExecutor) yieldedIn(perf *actionFrame, pauses int64) bool { func (e *ActionExecutor) subflowLocations(perf *actionFrame) map[int64]ast.Node { locations := make(map[int64]ast.Node) for _, idx := range e.tokensIn(perf) { - locations[e.tokens[idx].ID] = e.tokens[idx].Location + locations[e.tokens[idx].ID] = e.tokens[idx].Node() } return locations } @@ -517,7 +517,7 @@ func (e *ActionExecutor) subflowLocations(perf *actionFrame) map[int64]ast.Node func (e *ActionExecutor) performingTokens(perf *actionFrame) map[int64]bool { performing := make(map[int64]bool) for _, idx := range e.tokensIn(perf) { - if t := e.tokens[idx]; e.performs(t.frame, t.Location) { + if t := e.tokens[idx]; e.performs(t.frame, t.Node()) { performing[t.ID] = true } } @@ -585,7 +585,7 @@ func (t Token) drivenUnder(perf *actionFrame) bool { // owning the flow nested under perf it runs in; false for a token outside perf's flow. func (t Token) positionIn(perf *actionFrame) (ast.Node, bool) { if t.frame == perf { - return t.Location, true + return t.Node(), true } for f := t.frame; f != nil; f = f.parent { if f.parent == perf { @@ -601,7 +601,7 @@ func (e *ActionExecutor) leaveSubflow(tokenIdx int) error { token := &e.tokens[tokenIdx] frame := token.frame token.frame = frame.parent - token.Location = frame.node + token.Location = frame.ref() token.Via = lower.ActionEdge{} token.Wait = nil token.repetition = frame.repetition diff --git a/internal/exec/runtime/action_terminate.go b/internal/exec/runtime/action_terminate.go index bf68666a00..19acdf8741 100644 --- a/internal/exec/runtime/action_terminate.go +++ b/internal/exec/runtime/action_terminate.go @@ -123,7 +123,7 @@ func (e *performances) terminateTargets(perf *actionFrame, s lower.Effect) ([]*a pending, err := e.flow.beginPending(f.parent, s.Target) return append(ongoing, pending...), err } - latest, performed := f.subactions[s.Target] + latest, performed := f.subactions[f.refUnder(s.Target)] ongoing := e.flow.ongoing(f, s.Target) pending, err := e.flow.beginPending(f, s.Target) if err != nil { @@ -150,7 +150,7 @@ func (e *performances) terminateTargets(perf *actionFrame, s lower.Effect) ([]*a func (f *actionFrame) declaresAround(node ast.Node) bool { name := ActionNodeName(node) for ; f != nil; f = f.parent { - if slices.Contains(f.nodesNamed(name), node) { + if slices.Contains(NodesOf(f.nodesNamed(name)), node) { return true } } @@ -187,7 +187,7 @@ func (e *ActionExecutor) ongoing(parent *actionFrame, node ast.Node) []*actionFr found = append(found, f) } } - add(parent.subactions[node], math.MaxInt64) + add(parent.subactions[parent.refUnder(node)], math.MaxInt64) for _, state := range parent.repeats { if state.node != node { continue @@ -219,7 +219,7 @@ func (e *ActionExecutor) beginPending(parent *actionFrame, node ast.Node) ([]*ac } var parked []int64 for _, token := range e.tokens { - if token.frame == parent && token.Location == node && token.body == nil { + if token.frame == parent && token.Node() == node && token.body == nil { parked = append(parked, token.ID) } } @@ -416,7 +416,7 @@ func (e *ActionExecutor) leaveTerminated(tokenIdx int, perf *actionFrame) error } perf.live = 0 token.frame = perf.parent - token.Location = perf.node + token.Location = perf.ref() token.Via = lower.ActionEdge{} token.Wait = nil token.repetition = perf.repetition diff --git a/internal/exec/runtime/advance.go b/internal/exec/runtime/advance.go index e315d87bb0..85c81d208f 100644 --- a/internal/exec/runtime/advance.go +++ b/internal/exec/runtime/advance.go @@ -326,7 +326,7 @@ func tokenMayEnd(exec *ActionExecutor, t Token) bool { if t.frame != nil && t.frame != exec.root || standing(exec, t).Dynamic { return true } - edges := tokenGraphOf(exec, t).Edges[t.Location] + edges := tokenGraphOf(exec, t).Edges[t.Node()] return len(edges) == 0 || slices.ContainsFunc(edges, func(e lower.ActionEdge) bool { return e.Guard != nil }) } diff --git a/internal/exec/runtime/calc_statements.go b/internal/exec/runtime/calc_statements.go index 4318ca23e7..12016c0c55 100644 --- a/internal/exec/runtime/calc_statements.go +++ b/internal/exec/runtime/calc_statements.go @@ -68,8 +68,8 @@ func (h *calcStmtHost) attachPerformances(engine *stmtEngine) { scope: h.shape.bodyScope(), data: make(map[string]Value), features: make(map[string]ast.FeatureDirection), - subactions: make(map[ast.Node]*actionFrame), - nodes: h.shape.Nodes, + subactions: make(map[NodeRef]*actionFrame), + nodes: blockNodeRefs(h.shape.Steps), label: h.shape.Label, outer: append(append(append([]frame{}, engine.env.enclosing...), engine.env.data), engine.env.localFrame()), run: h.ctx.newRun(), @@ -417,7 +417,7 @@ func (h *calcStmtHost) returnAround(name string, value Value) (bool, error) { } // pauseAt sets no breakpoint: a case's steps are not stepped interactively. -func (h *calcStmtHost) pauseAt([]ast.Node, ast.Node) error { +func (h *calcStmtHost) pauseAt([]NodeRef, NodeRef) error { return nil } diff --git a/internal/exec/runtime/check.go b/internal/exec/runtime/check.go index 1dfadcbd00..c8f093b847 100644 --- a/internal/exec/runtime/check.go +++ b/internal/exec/runtime/check.go @@ -1339,7 +1339,7 @@ func (c *checker) resolveActionDiverge(exec *ActionExecutor, name, rest string) if !nested || len(named) == 0 { return &unknownCheckFeatureError{Name: name, Reason: "the action holds no such feature and performs no such node"} } - held, told := c.pathUnder(exec, exec.graph, named, path) + held, told := c.pathUnder(exec, exec.graph, NodesOf(named), path) if !held { return &unknownCheckFeatureError{Name: name, Reason: "action node " + node + " holds no feature " + path} } @@ -1376,7 +1376,7 @@ func (c *checker) pathUnder(exec *ActionExecutor, graph *lower.ActionGraph, node } } if sub, owns := exec.subflowOf(graph, node); owns && sub.Graph != nil { - if h, t := c.pathUnder(exec, sub.Graph, flowNodesNamed(sub.Graph, name), rest); h { + if h, t := c.pathUnder(exec, sub.Graph, NodesOf(flowNodesNamed(sub.Graph, name)), rest); h { return true, t } } @@ -1385,7 +1385,7 @@ func (c *checker) pathUnder(exec *ActionExecutor, graph *lower.ActionGraph, node if err != nil { return true, false } - if h, t := c.pathUnder(exec, flow, flowNodesNamed(flow, name), rest); h { + if h, t := c.pathUnder(exec, flow, NodesOf(flowNodesNamed(flow, name)), rest); h { return true, t && callee == settled } } diff --git a/internal/exec/runtime/check_body.go b/internal/exec/runtime/check_body.go index 642e312ed4..319f7ed037 100644 --- a/internal/exec/runtime/check_body.go +++ b/internal/exec/runtime/check_body.go @@ -36,7 +36,7 @@ func (s *stateSpeller) pause(p bodyPause) string { return "paused between two moves of its flow" } if !p.onWait { - return fmt.Sprintf("at breakpoint %q", p.breakpoint) + return fmt.Sprintf("at breakpoint %q", p.breakpoint.name) } if p.wait.held != nil { return "holds " + s.nested(p.wait.held) diff --git a/internal/exec/runtime/check_moves.go b/internal/exec/runtime/check_moves.go index 0dcc27aaed..c77cc4e9e1 100644 --- a/internal/exec/runtime/check_moves.go +++ b/internal/exec/runtime/check_moves.go @@ -138,7 +138,7 @@ func (e *ActionExecutor) enabledMoves() []enabledMove { moves = append(moves, enabledMove{ Owner: e, Token: id, - Node: t.Location, + Node: t.Node(), Label: tokens.label(id), Kind: e.moveKindOf(t), Fails: fails, @@ -156,7 +156,7 @@ func (e *ActionExecutor) moveKindOf(t Token) moveKind { if t.body != nil { return moveResume } - switch node := t.Location.(type) { + switch node := t.Node().(type) { case *ast.JoinNode: return moveJoin case *ast.DecisionNode: diff --git a/internal/exec/runtime/check_reduce.go b/internal/exec/runtime/check_reduce.go index 4ee02f3750..d370689659 100644 --- a/internal/exec/runtime/check_reduce.go +++ b/internal/exec/runtime/check_reduce.go @@ -44,7 +44,7 @@ func standing(exec *ActionExecutor, t Token) lower.Footprint { if exec.dynamics != nil { return dynamicsFootprint() } - return tokenGraphOf(exec, t).Footprints()[t.Location] + return tokenGraphOf(exec, t).Footprints()[t.Node()] } // machineStanding is what the machine's next unit may touch: a dispatch out of diff --git a/internal/exec/runtime/check_state.go b/internal/exec/runtime/check_state.go index ed895c55ef..4040878814 100644 --- a/internal/exec/runtime/check_state.go +++ b/internal/exec/runtime/check_state.go @@ -581,25 +581,25 @@ func (s *stateSpeller) frame(perf *actionFrame) string { fmt.Fprintf(&b, " local{%s}", s.bodyValues(local, cells)) } s.deliveries(&b, perf) - for _, node := range sortedNodes(perf.subactions) { - fmt.Fprintf(&b, " latest{%s = %s}", s.node(perf.graph, node), s.frameLabel(perf.subactions[node])) + for _, node := range sortedRefs(perf.subactions) { + fmt.Fprintf(&b, " latest{%s = %s}", s.node(perf.graph, node.Node()), s.frameLabel(perf.subactions[node])) } s.repeats(&b, perf) streamed := slices.Sorted(maps.Keys(perf.streamed)) if len(streamed) > 0 { fmt.Fprintf(&b, " streamed{%s}", strings.Join(streamed, ",")) } - for _, node := range sortedNodes(perf.staged) { + for _, node := range sortedRefs(perf.staged) { pins := perf.staged[node] for _, pin := range slices.Sorted(maps.Keys(pins)) { for _, staged := range pins[pin] { - fmt.Fprintf(&b, " staged{%s.%s[%d] by %s}", s.node(perf.graph, node), pin, staged.at, s.frameLabel(staged.source)) + fmt.Fprintf(&b, " staged{%s.%s[%d] by %s}", s.node(perf.graph, node.Node()), pin, staged.at, s.frameLabel(staged.source)) } } } - for _, node := range sortedNodes(perf.unreceived) { + for _, node := range sortedRefs(perf.unreceived) { for _, stream := range perf.unreceived[node] { - fmt.Fprintf(&b, " unreceived{%s.%s[%d] from %s.%s}", s.node(perf.graph, node), stream.pin, stream.at, + fmt.Fprintf(&b, " unreceived{%s.%s[%d] from %s.%s}", s.node(perf.graph, node.Node()), stream.pin, stream.at, nodeKey(stream.source), orAnyPin(stream.flow.SourcePin)) } } @@ -609,24 +609,24 @@ func (s *stateSpeller) frame(perf *actionFrame) string { // deliveries spells the values pending on a frame's pins and those delivered // to the pins of its nested nodes. func (s *stateSpeller) deliveries(b *strings.Builder, perf *actionFrame) { - for _, node := range sortedNodes(perf.pending) { + for _, node := range sortedRefs(perf.pending) { pins := perf.pending[node] for _, pin := range slices.Sorted(maps.Keys(pins)) { - fmt.Fprintf(b, " pending{%s.%s = (%s)}", s.node(perf.graph, node), pin, s.elements(pins[pin])) + fmt.Fprintf(b, " pending{%s.%s = (%s)}", s.node(perf.graph, node.Node()), pin, s.elements(pins[pin])) } } - for _, node := range sortedNodes(perf.held) { + for _, node := range sortedRefs(perf.held) { pins := perf.held[node] for _, pin := range slices.Sorted(maps.Keys(pins)) { for _, h := range pins[pin] { - fmt.Fprintf(b, " held{%s.%s = %s by %s}", s.node(perf.graph, node), pin, s.value(h.value), nodeKey(h.flow)) + fmt.Fprintf(b, " held{%s.%s = %s by %s}", s.node(perf.graph, node.Node()), pin, s.value(h.value), nodeKey(h.flow)) } } } - for _, node := range sortedNodes(perf.nested) { + for _, node := range sortedRefs(perf.nested) { for _, delivery := range perf.nested[node] { path := make([]string, 0, len(delivery.path)+1) - path = append(path, s.node(perf.graph, node)) + path = append(path, s.node(perf.graph, node.Node())) for _, step := range delivery.path { path = append(path, nodeIdentifier(step)) } @@ -650,16 +650,6 @@ func (s *stateSpeller) repeats(b *strings.Builder, perf *actionFrame) { } } -// sortedNodes orders a map's node keys by identity, so the form is independent of map order. -func sortedNodes[V any](m map[ast.Node]V) []ast.Node { - nodes := make([]ast.Node, 0, len(m)) - for node := range m { - nodes = append(nodes, node) - } - sort.Slice(nodes, func(i, j int) bool { return nodeKey(nodes[i]) < nodeKey(nodes[j]) }) - return nodes -} - // nodeKey identifies a node by its name and where it was written. func nodeKey(node ast.Node) string { span := node.Span() @@ -693,7 +683,7 @@ func (s *stateSpeller) tokenCore(t Token) string { graph = t.frame.graph } var b strings.Builder - fmt.Fprintf(&b, "token %s in %s", s.node(graph, t.Location), s.frameLabel(t.frame)) + fmt.Fprintf(&b, "token %s in %s", s.node(graph, t.Node()), s.frameLabel(t.frame)) if t.repetition > 0 { fmt.Fprintf(&b, " repetition=%d", t.repetition) } diff --git a/internal/exec/runtime/check_state_stream_test.go b/internal/exec/runtime/check_state_stream_test.go index 5d6ec777a5..8230b370e7 100644 --- a/internal/exec/runtime/check_state_stream_test.go +++ b/internal/exec/runtime/check_state_stream_test.go @@ -5,18 +5,23 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" - "github.com/Open-MBEE/OpenSysML/internal/syntax/source" ) // TestCanonicalStateTellsUnreceivedStreamsApart: two performances whose late streamed // values differ only in pin, queue position, source or count spell differently. func TestCanonicalStateTellsUnreceivedStreamsApart(t *testing.T) { - node := func(offset int) ast.Node { - return &ast.Usage{NodeBase: ast.NodeBase{NodeSpan: source.Span{Offset: offset, Len: 1}}, Kind: ast.UsageAction} + ctx, sym := loadAction(t, `package test { + action def A { + action target { in v : Integer; in w : Integer; } + action srcA { out v : Integer; out w : Integer; } + action srcB { out v : Integer; } } - target, srcA, srcB := node(10), node(20), node(30) +}`, "A") + exec := mustActionExecutor(t, ctx, sym) + graph := exec.Graph() + target, srcA, srcB := actionNodeNamed(t, exec, "target"), actionNodeNamed(t, exec, "srcA"), actionNodeNamed(t, exec, "srcB") frameWith := func(streams ...unreceivedStream) *actionFrame { - return &actionFrame{unreceived: map[ast.Node][]unreceivedStream{target: streams}} + return &actionFrame{graph: graph, unreceived: map[NodeRef][]unreceivedStream{refOf(graph, target): streams}} } mark := func(source ast.Node, pin string, at int) unreceivedStream { return unreceivedStream{flow: lower.ObjectFlow{SourcePin: pin, Target: target, TargetPin: pin}, source: source, pin: pin, at: at} diff --git a/internal/exec/runtime/choice_test.go b/internal/exec/runtime/choice_test.go index f901dd9169..0782fe72a0 100644 --- a/internal/exec/runtime/choice_test.go +++ b/internal/exec/runtime/choice_test.go @@ -652,7 +652,7 @@ func TestSharedMessageAcceptIsAChoice(t *testing.T) { } at := func(node string) (Token, bool) { for _, tok := range exec.Tokens() { - if nodeIdentifier(tok.Location) == node { + if nodeIdentifier(tok.Node()) == node { return tok, true } } diff --git a/internal/exec/runtime/debug_api_test.go b/internal/exec/runtime/debug_api_test.go index 4d8fc9c3c6..fae01e929a 100644 --- a/internal/exec/runtime/debug_api_test.go +++ b/internal/exec/runtime/debug_api_test.go @@ -65,7 +65,7 @@ func TestActionExecutorDebugAccessors(t *testing.T) { if len(tokens) != 1 { t.Fatalf("Tokens() = %d tokens, want 1", len(tokens)) } - if name := ActionNodeName(tokens[0].Location); name != "start" { + if name := ActionNodeName(tokens[0].Node()); name != "start" { t.Errorf("token sits at %q, want start", name) } @@ -109,7 +109,7 @@ func TestSetBreakpointStopsRun(t *testing.T) { t.Errorf("State() = %v, want %v", got, StateSuspended) } tokens := exec.Tokens() - if len(tokens) != 1 || ActionNodeName(tokens[0].Location) != "accumulate" { + if len(tokens) != 1 || ActionNodeName(tokens[0].Node()) != "accumulate" { t.Fatalf("expected one token at accumulate, got %v", tokens) } @@ -252,13 +252,13 @@ func TestStepToBreakpointPausesWhereARunWould(t *testing.T) { if got := exec.PausedAt(); got != "start" { t.Errorf("PausedAt() = %q, want start", got) } - if tokens := exec.Tokens(); len(tokens) != 1 || ActionNodeName(tokens[0].Location) != "start" { + if tokens := exec.Tokens(); len(tokens) != 1 || ActionNodeName(tokens[0].Node()) != "start" { t.Errorf("tokens = %v, want the one still at start", tokens) } if err := exec.StepToBreakpoint(); err != nil { t.Fatalf("resuming from start: %v", err) } - if tokens := exec.Tokens(); len(tokens) != 1 || ActionNodeName(tokens[0].Location) != "accumulate" { + if tokens := exec.Tokens(); len(tokens) != 1 || ActionNodeName(tokens[0].Node()) != "accumulate" { t.Errorf("tokens = %v, want the one moved on to accumulate", tokens) } } @@ -279,7 +279,7 @@ func actionNodeNamed(t *testing.T, exec *ActionExecutor, name string) ast.Node { // so the next step resumes past it; one removed stops the run again once re-set. func TestReplaceBreakpointsAtKeepsAStopAlreadyMade(t *testing.T) { exec := debugActionExecutor(t) - at := []NodeBreakpoint{{Node: actionNodeNamed(t, exec, "accumulate")}} + at := []NodeBreakpoint{breakpointAt(t, exec, nil, actionNodeNamed(t, exec, "accumulate"))} exec.ReplaceBreakpointsAt(at) if err := exec.StepToBreakpoint(); err != nil { t.Fatalf("StepToBreakpoint: %v", err) @@ -300,7 +300,7 @@ func TestReplaceBreakpointsAtKeepsAStopAlreadyMade(t *testing.T) { } exec = debugActionExecutor(t) - at = []NodeBreakpoint{{Node: actionNodeNamed(t, exec, "accumulate")}} + at = []NodeBreakpoint{breakpointAt(t, exec, nil, actionNodeNamed(t, exec, "accumulate"))} exec.ReplaceBreakpointsAt(at) if err := exec.StepToBreakpoint(); err != nil { t.Fatalf("StepToBreakpoint: %v", err) @@ -334,7 +334,7 @@ func TestReplaceBreakpointsAtResetsABodyStopRemoved(t *testing.T) { if q == nil { t.Fatalf("choose's blocks declare no q: %v", exec.Graph().BlockNodes[choose]) } - at := []NodeBreakpoint{{Within: []ast.Node{choose}, Node: q}} + at := []NodeBreakpoint{breakpointAt(t, exec, []ast.Node{choose}, q)} exec.ReplaceBreakpointsAt(at) if err := exec.RunToCompletion(); err != nil { t.Fatalf("RunToCompletion: %v", err) @@ -398,7 +398,7 @@ func TestResumeReturnsAPausedRunToTheClock(t *testing.T) { if exec.Resume() { t.Error("Resume() = true for a run no breakpoint suspended") } - exec.ReplaceBreakpointsAt([]NodeBreakpoint{{Node: actionNodeNamed(t, exec, "accumulate")}}) + exec.ReplaceBreakpointsAt([]NodeBreakpoint{breakpointAt(t, exec, nil, actionNodeNamed(t, exec, "accumulate"))}) if err := exec.StepToBreakpoint(); err != nil { t.Fatalf("StepToBreakpoint: %v", err) } @@ -463,7 +463,7 @@ func TestBreakpointsAtDistinguishReusedNestedFlows(t *testing.T) { t.Fatalf("look is not one node both flows run: %v", look) } - exec.ReplaceBreakpointsAt([]NodeBreakpoint{{Within: []ast.Node{b}, Node: look}}) + exec.ReplaceBreakpointsAt([]NodeBreakpoint{breakpointAt(t, exec, []ast.Node{b}, look)}) if err := exec.RunToCompletion(); err != nil { t.Fatalf("RunToCompletion: %v", err) } @@ -471,7 +471,7 @@ func TestBreakpointsAtDistinguishReusedNestedFlows(t *testing.T) { t.Fatalf("PausedAt() = %q, want look", got) } tokens := exec.Tokens() - if len(tokens) != 1 || tokens[0].Location != look || !slices.Equal(tokens[0].Within(), []ast.Node{b}) { + if len(tokens) != 1 || tokens[0].Node() != look || !slices.Equal(NodesOf(tokens[0].Within()), []ast.Node{b}) { t.Fatalf("tokens = %v, want the one at look within b", tokens) } if err := exec.RunToCompletion(); err != nil { @@ -539,7 +539,7 @@ func TestBreakpointPausesBeforeABranchNode(t *testing.T) { if got := exec.State(); got != StateSuspended { t.Errorf("State() = %v, want %v", got, StateSuspended) } - if tokens := exec.Tokens(); len(tokens) != 1 || ActionNodeName(tokens[0].Location) != "choose" { + if tokens := exec.Tokens(); len(tokens) != 1 || ActionNodeName(tokens[0].Node()) != "choose" { t.Fatalf("expected one token at choose, got %v", tokens) } results := exec.Results() @@ -578,7 +578,7 @@ func TestPausedBreakpointIdentifiesABranchNodeInItsFlow(t *testing.T) { if q == nil { t.Fatalf("choose's blocks declare no q: %v", exec.Graph().BlockNodes[choose]) } - exec.ReplaceBreakpointsAt([]NodeBreakpoint{{Within: []ast.Node{choose}, Node: q}}) + exec.ReplaceBreakpointsAt([]NodeBreakpoint{breakpointAt(t, exec, []ast.Node{choose}, q)}) if err := exec.RunToCompletion(); err != nil { t.Fatalf("RunToCompletion: %v", err) @@ -587,15 +587,15 @@ func TestPausedBreakpointIdentifiesABranchNodeInItsFlow(t *testing.T) { t.Fatalf("PausedAt() = %q, want q", got) } bp, ok := exec.PausedBreakpoint() - if !ok || bp.Node != q || !slices.Equal(bp.Within, []ast.Node{choose}) { + if !ok || bp.Node.Node() != q || !slices.Equal(NodesOf(bp.Within), []ast.Node{choose}) { t.Errorf("PausedBreakpoint() = %+v, %v, want q within choose", bp, ok) } - if tokens := exec.Tokens(); len(tokens) != 1 || tokens[0].Location != choose { + if tokens := exec.Tokens(); len(tokens) != 1 || tokens[0].Node() != choose { t.Errorf("tokens = %v, want the one still on choose", tokens) } // The path reported is the caller's own: writing to it leaves the stop where it was. - bp.Within[0] = q - if again, ok := exec.PausedBreakpoint(); !ok || !slices.Equal(again.Within, []ast.Node{choose}) { + bp.Within[0] = bp.Node + if again, ok := exec.PausedBreakpoint(); !ok || !slices.Equal(NodesOf(again.Within), []ast.Node{choose}) { t.Errorf("PausedBreakpoint() = %+v, %v after writing to the path reported, want q still within choose", again, ok) } @@ -712,7 +712,7 @@ func TestBreakpointPausesInsideABlockNodesOwnFlow(t *testing.T) { } var at []string for _, token := range exec.Tokens() { - at = append(at, ActionNodeName(token.Location)) + at = append(at, ActionNodeName(token.Node())) } if got := strings.Join(at, ","); got != "choose,gather" { t.Errorf("tokens at %s, want choose,gather", got) @@ -894,8 +894,8 @@ func TestBreakpointPausesOncePerSynchronizedPerformance(t *testing.T) { t.Fatalf("%d tokens while paused, want the 2 arrivals at %s", got, tc.node) } for _, tok := range exec.Tokens() { - if ActionNodeName(tok.Location) != tc.node { - t.Errorf("token %d @ %s while paused, want %s", tok.ID, ActionNodeName(tok.Location), tc.node) + if ActionNodeName(tok.Node()) != tc.node { + t.Errorf("token %d @ %s while paused, want %s", tok.ID, ActionNodeName(tok.Node()), tc.node) } if awaiting := exec.Awaiting(tok); len(awaiting) > 0 { t.Errorf("paused at %s while token %d still awaits %d successions", tc.node, tok.ID, len(awaiting)) @@ -966,7 +966,7 @@ func TestBreakpointOnANestedFirstNodePausesWhileOthersSynchronize(t *testing.T) } var atInner int for _, tok := range exec.Tokens() { - if ActionNodeName(tok.Location) == "inner" { + if ActionNodeName(tok.Node()) == "inner" { atInner++ } } @@ -1912,7 +1912,7 @@ func traversalNames(exec *ActionExecutor) []string { for _, tr := range exec.Traversals() { prefix := "" for _, owner := range tr.Within { - prefix += ActionNodeName(owner) + "/" + prefix += ActionNodeName(owner.Node()) + "/" } out = append(out, prefix+ActionNodeName(tr.Edge.Source)+"->"+ActionNodeName(tr.Edge.Target)) } @@ -1979,8 +1979,8 @@ func TestTraversalsLogEachSuccessionInOrder(t *testing.T) { if nested < 0 { t.Fatal("no traversal ran in a nested flow") } - exec.Traversals()[nested].Within[0] = nil - exec.TraversalsSince(0)[nested].Within[0] = nil + exec.Traversals()[nested].Within[0] = NodeRef{} + exec.TraversalsSince(0)[nested].Within[0] = NodeRef{} if got := traversalNames(exec); !slices.Equal(got[mark:], want) { t.Errorf("after writing into copies, traversals = %v, want %v unchanged", got[mark:], want) } diff --git a/internal/exec/runtime/debug_breakpoint_ids_test.go b/internal/exec/runtime/debug_breakpoint_ids_test.go new file mode 100644 index 0000000000..71cd72f3ed --- /dev/null +++ b/internal/exec/runtime/debug_breakpoint_ids_test.go @@ -0,0 +1,55 @@ +package runtime + +import ( + "slices" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +// breakpointAt is the breakpoint on node in the flow of within, which exec must resolve. +func breakpointAt(t *testing.T, exec *ActionExecutor, within []ast.Node, node ast.Node) NodeBreakpoint { + t.Helper() + bp, ok := exec.BreakpointAt(within, node) + if !ok { + t.Fatalf("BreakpointAt(%v, %v) resolves no node", within, node) + } + return bp +} + +// A breakpoint set by identity is keyed by the ids of the nodes it names, each in the flow +// the node before it performs them in; a node named under a path not leading to it resolves to nothing. +func TestBreakpointAtResolvesNodesInTheirFlows(t *testing.T) { + exec := blockDebugExecutor(t) + choose := actionNodeNamed(t, exec, "choose") + var q ast.Node + for _, node := range exec.Graph().BlockNodes[choose] { + if ActionNodeName(node) == "q" { + q = node + } + } + at := breakpointAt(t, exec, []ast.Node{choose}, q) + if want := (NodeRef{Graph: exec.Graph(), ID: exec.Graph().ID[choose]}); len(at.Within) != 1 || at.Within[0] != want { + t.Errorf("Within = %v, want choose by id in the action's own flow", at.Within) + } + if at.Node.Graph == exec.Graph() || at.Node.Graph == nil || at.Node.Node() != q || at.Node.Graph.ID[q] != at.Node.ID { + t.Errorf("Node = %+v, want q by id in the block flow of choose's body", at.Node) + } + if bp, ok := exec.BreakpointAt(nil, q); ok { + t.Errorf("BreakpointAt(nil, q) = %+v, want q unresolved in the action's own flow", bp) + } + if bp, ok := exec.BreakpointAt([]ast.Node{q}, choose); ok { + t.Errorf("BreakpointAt([q], choose) = %+v, want choose unresolved under q", bp) + } + + exec.ReplaceBreakpointsAt([]NodeBreakpoint{at}) + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion: %v", err) + } + if bp, ok := exec.PausedBreakpoint(); !ok || bp.Node != at.Node || !slices.Equal(bp.Within, at.Within) { + t.Errorf("PausedBreakpoint() = %+v, %v, want the breakpoint set, by the same ids", bp, ok) + } + if tokens := exec.Tokens(); len(tokens) != 1 || tokens[0].Location != at.Within[0] { + t.Errorf("tokens = %v, want the one at choose, keyed by its id", tokens) + } +} diff --git a/internal/exec/runtime/executor_common.go b/internal/exec/runtime/executor_common.go index 34c8740098..a1fb5ad300 100644 --- a/internal/exec/runtime/executor_common.go +++ b/internal/exec/runtime/executor_common.go @@ -16,9 +16,9 @@ import ( // Token represents a control token in action execution. It carries no values of // its own: the action's features are one space every token shares (see ActionExecutor.Data). type Token struct { - ID int64 // Unique token ID - Location ast.Node // Current node position - repetition int64 // Nonzero when this token is one performance of a repeated step. + ID int64 // Unique token ID + Location NodeRef // Current node position, by id in its flow + repetition int64 // Nonzero when this token is one performance of a repeated step. repetitionGroup repetitionGroupID // Via is the succession the token travelled to reach Location; the zero edge for a @@ -91,13 +91,25 @@ func (t *Token) underControl(by []int64, ends map[ast.Node]bool) { // Within lists the nested action nodes whose own flows the token runs in, // outermost first; empty for a token in the action's own flow. -func (t Token) Within() []ast.Node { +func (t Token) Within() []NodeRef { return t.frame.within() } +// Node is the declaration the token stands at: its node, or the action itself for +// the token of a state-space run, which stands at no node of the flow. +func (t Token) Node() ast.Node { + if node := t.Location.Node(); node != nil { + return node + } + if t.frame != nil && t.frame.perfs != nil && t.frame.perfs.flow != nil && t.frame.perfs.flow.dynamics != nil { + return t.frame.perfs.flow.dynamicsNode() + } + return nil +} + // travel moves the token along a succession in the given sweep, recording the one it arrived over. func (t *Token) travel(edge lower.ActionEdge, sweep uint64) { - t.Location = edge.Target + t.Location = refOf(t.Location.Graph, edge.Target) t.Via = edge t.moved = sweep if slices.ContainsFunc(t.control, func(c tokenControl) bool { return c.ends[edge.Target] }) { diff --git a/internal/exec/runtime/executor_common_test.go b/internal/exec/runtime/executor_common_test.go index 08877fcce2..7acee47f88 100644 --- a/internal/exec/runtime/executor_common_test.go +++ b/internal/exec/runtime/executor_common_test.go @@ -5,10 +5,7 @@ import ( ) func TestToken_Creation(t *testing.T) { - token := Token{ - ID: 1, - Location: nil, // Will use actual AST node in real usage - } + token := Token{ID: 1} if token.ID != 1 { t.Errorf("expected ID 1, got %d", token.ID) diff --git a/internal/exec/runtime/future_footprint.go b/internal/exec/runtime/future_footprint.go index 1340aa07c9..018e4a7e5a 100644 --- a/internal/exec/runtime/future_footprint.go +++ b/internal/exec/runtime/future_footprint.go @@ -2,15 +2,16 @@ package runtime import ( "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" ) -// futureKey identifies what a token may still do at this instant: its node in its flow. +// futureKey identifies what a token may still do at this instant: its node, by id, in its flow. type futureKey struct { graph *lower.ActionGraph - node ast.Node + node program.NodeID } // futureFootprints memoizes what executors may still touch at the clock's instant: @@ -157,12 +158,12 @@ func (f *futureFootprints) tokenFuture(exec *ActionExecutor, t Token) lower.Foot if t.Wait.Due > f.ctx.clock.now { return standing(exec, t) } - future = graph.Footprints()[t.Location] - for _, edge := range graph.Edges[t.Location] { + future = graph.Footprints()[t.Node()] + for _, edge := range graph.Edges[t.Node()] { future = unionFootprints(future, f.reach(graph, edge.Target)) } } else { - future = f.reach(graph, t.Location) + future = f.reach(graph, t.Node()) } for frame := t.frame; frame != nil && frame.node != nil; frame = frame.parent { flow := frame.flow @@ -188,7 +189,7 @@ func tokenGraphOf(exec *ActionExecutor, t Token) *lower.ActionGraph { // graph over its successions, the flows those nodes own included, going on past // no accept whose delay ends the instant; memoized for the instant. func (f *futureFootprints) reach(graph *lower.ActionGraph, node ast.Node) lower.Footprint { - key := futureKey{graph: graph, node: node} + key := futureKey{graph: graph, node: graph.ID[node]} if fp, ok := f.nodes[key]; ok { return fp } diff --git a/internal/exec/runtime/held_image_behavior.go b/internal/exec/runtime/held_image_behavior.go index 370628d05c..e85f6af51b 100644 --- a/internal/exec/runtime/held_image_behavior.go +++ b/internal/exec/runtime/held_image_behavior.go @@ -68,13 +68,13 @@ type imagedFrame struct { data map[string]Value cells []imagedBodyCell outer []imagedOuter - subactions map[ast.Node]int + subactions map[NodeRef]int repeats map[repetitionGroupID]imagedRepetition - repeatedPerfs map[ast.Node][]int - pending map[ast.Node]map[string][]Value - held map[ast.Node]map[string][]nodeObject - staged map[ast.Node]map[string][]imagedStaged - nested map[ast.Node][]nestedDelivery + repeatedPerfs map[NodeRef][]int + pending map[NodeRef]map[string][]Value + held map[NodeRef]map[string][]nodeObject + staged map[NodeRef]map[string][]imagedStaged + nested map[NodeRef][]nestedDelivery } // imagedBodyCell stores a body binding's value and tracking state for an image. @@ -237,7 +237,7 @@ func (t *imaging) actionExecutor(e *ActionExecutor) (*imagedAction, error) { for _, token := range e.tokens { if token.body != nil { return nil, fmt.Errorf("%w: token %d of %s at %s", ErrSnapshotPausedBody, - token.ID, SymbolText(e.action), ActionNodeName(token.Location)) + token.ID, SymbolText(e.action), ActionNodeName(token.Node())) } copied := token if token.Wait != nil { @@ -298,7 +298,7 @@ func (t *imaging) frame(perf *actionFrame, at func(*actionFrame) int) (imagedFra } f.outer = outer if perf.subactions != nil { - f.subactions = make(map[ast.Node]int, len(perf.subactions)) + f.subactions = make(map[NodeRef]int, len(perf.subactions)) for node, sub := range perf.subactions { f.subactions[node] = at(sub) } @@ -314,7 +314,7 @@ func (t *imaging) frame(perf *actionFrame, at func(*actionFrame) int) (imagedFra } } if perf.repeatedPerfs != nil { - f.repeatedPerfs = make(map[ast.Node][]int, len(perf.repeatedPerfs)) + f.repeatedPerfs = make(map[NodeRef][]int, len(perf.repeatedPerfs)) for node, perfs := range perf.repeatedPerfs { indexed := make([]int, len(perfs)) for i, repeated := range perfs { @@ -361,7 +361,7 @@ func (t *imaging) outerFrames(perf *actionFrame, at func(*actionFrame) int) ([]i } // nestedValues checks every value a pending table holds. -func (t *imaging) nestedValues(pending map[ast.Node]map[string][]Value) error { +func (t *imaging) nestedValues(pending map[NodeRef]map[string][]Value) error { for _, pins := range pending { for _, values := range pins { for _, v := range values { @@ -375,7 +375,7 @@ func (t *imaging) nestedValues(pending map[ast.Node]map[string][]Value) error { } // deliveredValues checks every value a nested-delivery table holds. -func (t *imaging) deliveredValues(nested map[ast.Node][]nestedDelivery) error { +func (t *imaging) deliveredValues(nested map[NodeRef][]nestedDelivery) error { for _, deliveries := range nested { for _, d := range deliveries { if err := t.value(d.value); err != nil { @@ -387,7 +387,7 @@ func (t *imaging) deliveredValues(nested map[ast.Node][]nestedDelivery) error { } // heldValues checks every value a control node holds. -func (t *imaging) heldValues(held map[ast.Node]map[string][]nodeObject) error { +func (t *imaging) heldValues(held map[NodeRef]map[string][]nodeObject) error { for _, pins := range held { for _, objects := range pins { for _, h := range objects { @@ -401,11 +401,11 @@ func (t *imaging) heldValues(held map[ast.Node]map[string][]nodeObject) error { } // stagedImaged images staged's sources by position. -func stagedImaged(staged map[ast.Node]map[string][]stagedStream, at func(*actionFrame) int) map[ast.Node]map[string][]imagedStaged { +func stagedImaged(staged map[NodeRef]map[string][]stagedStream, at func(*actionFrame) int) map[NodeRef]map[string][]imagedStaged { if staged == nil { return nil } - imaged := make(map[ast.Node]map[string][]imagedStaged, len(staged)) + imaged := make(map[NodeRef]map[string][]imagedStaged, len(staged)) for node, pins := range staged { imaged[node] = make(map[string][]imagedStaged, len(pins)) for pin, entries := range pins { @@ -739,7 +739,7 @@ func (m *materializing) frame(perf *actionFrame, img imagedFrame, frameAt func(i }) } if img.subactions != nil { - perf.subactions = make(map[ast.Node]*actionFrame, len(img.subactions)) + perf.subactions = make(map[NodeRef]*actionFrame, len(img.subactions)) for node, at := range img.subactions { perf.subactions[node] = frameAt(at) } @@ -755,7 +755,7 @@ func (m *materializing) frame(perf *actionFrame, img imagedFrame, frameAt func(i } } if img.repeatedPerfs != nil { - perf.repeatedPerfs = make(map[ast.Node][]*actionFrame, len(img.repeatedPerfs)) + perf.repeatedPerfs = make(map[NodeRef][]*actionFrame, len(img.repeatedPerfs)) for node, indexed := range img.repeatedPerfs { perfs := make([]*actionFrame, len(indexed)) for i, at := range indexed { @@ -926,11 +926,11 @@ func (m *materializing) frameCells( } // pending fills perf's pending table from img's. -func (m *materializing) pending(perf *actionFrame, pending map[ast.Node]map[string][]Value) error { +func (m *materializing) pending(perf *actionFrame, pending map[NodeRef]map[string][]Value) error { if pending == nil { return nil } - perf.pending = make(map[ast.Node]map[string][]Value, len(pending)) + perf.pending = make(map[NodeRef]map[string][]Value, len(pending)) for node, pins := range pending { carriedPins := make(map[string][]Value, len(pins)) for pin, values := range pins { @@ -948,11 +948,11 @@ func (m *materializing) pending(perf *actionFrame, pending map[ast.Node]map[stri } // held fills what perf's control nodes hold from img's. -func (m *materializing) held(perf *actionFrame, held map[ast.Node]map[string][]nodeObject) error { +func (m *materializing) held(perf *actionFrame, held map[NodeRef]map[string][]nodeObject) error { if held == nil { return nil } - perf.held = make(map[ast.Node]map[string][]nodeObject, len(held)) + perf.held = make(map[NodeRef]map[string][]nodeObject, len(held)) for node, pins := range held { carriedPins := make(map[string][]nodeObject, len(pins)) for pin, objects := range pins { @@ -971,11 +971,11 @@ func (m *materializing) held(perf *actionFrame, held map[ast.Node]map[string][]n } // nested fills perf's nested-delivery table from img's. -func (m *materializing) nested(perf *actionFrame, nested map[ast.Node][]nestedDelivery) error { +func (m *materializing) nested(perf *actionFrame, nested map[NodeRef][]nestedDelivery) error { if nested == nil { return nil } - perf.nested = make(map[ast.Node][]nestedDelivery, len(nested)) + perf.nested = make(map[NodeRef][]nestedDelivery, len(nested)) for node, deliveries := range nested { for _, d := range deliveries { carried, err := m.value(d.value) @@ -989,11 +989,11 @@ func (m *materializing) nested(perf *actionFrame, nested map[ast.Node][]nestedDe } // stagedMaterialized materializes img's staged writes, their sources by position. -func stagedMaterialized(staged map[ast.Node]map[string][]imagedStaged, frameAt func(int) *actionFrame) map[ast.Node]map[string][]stagedStream { +func stagedMaterialized(staged map[NodeRef]map[string][]imagedStaged, frameAt func(int) *actionFrame) map[NodeRef]map[string][]stagedStream { if staged == nil { return nil } - materialized := make(map[ast.Node]map[string][]stagedStream, len(staged)) + materialized := make(map[NodeRef]map[string][]stagedStream, len(staged)) for node, pins := range staged { materialized[node] = make(map[string][]stagedStream, len(pins)) for pin, entries := range pins { diff --git a/internal/exec/runtime/held_image_test.go b/internal/exec/runtime/held_image_test.go index 5ba62ab1bf..735a721362 100644 --- a/internal/exec/runtime/held_image_test.go +++ b/internal/exec/runtime/held_image_test.go @@ -687,7 +687,7 @@ func TestHeldImageCarriesAPausedAction(t *testing.T) { nine := Value{Kind: ValConst, Const: semantics.Value{Kind: semantics.ValInt, Int: 9}} action := await(t, waiter) mark := actionNodeNamed(t, action, "mark") - action.ReplaceBreakpointsAt([]NodeBreakpoint{{Node: mark}}) + action.ReplaceBreakpointsAt([]NodeBreakpoint{breakpointAt(t, action, nil, mark)}) action.KeepTraversals(true) early := imageInto(t, src, waiter) @@ -696,7 +696,7 @@ func TestHeldImageCarriesAPausedAction(t *testing.T) { if err := await(t, earlyCopy).RunToCompletion(); err != nil { t.Fatalf("RunToCompletion(early copy): %v", err) } - if bp, ok := await(t, earlyCopy).PausedBreakpoint(); !ok || bp.Node != mark { + if bp, ok := await(t, earlyCopy).PausedBreakpoint(); !ok || bp.Node.Node() != mark { t.Errorf("the copy imaged before the breakpoint paused at %+v, %v; want mark: the breakpoint was not carried", bp, ok) } @@ -704,7 +704,7 @@ func TestHeldImageCarriesAPausedAction(t *testing.T) { if err := action.RunToCompletion(); err != nil { t.Fatalf("RunToCompletion: %v", err) } - if bp, ok := action.PausedBreakpoint(); !ok || bp.Node != mark || action.State() != StateSuspended { + if bp, ok := action.PausedBreakpoint(); !ok || bp.Node.Node() != mark || action.State() != StateSuspended { t.Fatalf("the source paused at %+v, %v in state %v; want mark, suspended", bp, ok, action.State()) } // start then heard was taken before the debugger asked for the record, so only @@ -720,7 +720,7 @@ func TestHeldImageCarriesAPausedAction(t *testing.T) { if imaged.State() != StateSuspended || imaged.PausedAt() != "mark" { t.Errorf("the copy is %v paused at %q, want suspended at mark as imaged", imaged.State(), imaged.PausedAt()) } - if bp, ok := imaged.PausedBreakpoint(); !ok || bp.Node != mark || len(bp.Within) != 0 { + if bp, ok := imaged.PausedBreakpoint(); !ok || bp.Node.Node() != mark || len(bp.Within) != 0 { t.Errorf("the copy's PausedBreakpoint() = %+v, %v, want mark in the action's own flow", bp, ok) } if got := imaged.Traversals(); imaged.TraversalCount() != taken || !traversalsEqual(got, traversals) { @@ -1563,7 +1563,7 @@ func heldDigest(ctx *Context, held func(id int64) bool) string { if exec := behavior.Action; exec != nil { fmt.Fprintf(&b, " action state=%v inputs=%s\n", exec.State(), formatValues(exec.inputs)) for _, token := range exec.Tokens() { - fmt.Fprintf(&b, " token %d at %s wait=%+v\n", token.ID, ActionNodeName(token.Location), token.Wait) + fmt.Fprintf(&b, " token %d at %s wait=%+v\n", token.ID, ActionNodeName(token.Node()), token.Wait) } } if exec := behavior.State; exec != nil { diff --git a/internal/exec/runtime/pristine_test.go b/internal/exec/runtime/pristine_test.go index 1af9f5bfdf..b37cedfe43 100644 --- a/internal/exec/runtime/pristine_test.go +++ b/internal/exec/runtime/pristine_test.go @@ -429,7 +429,7 @@ func TestActionBodyFailingAfterAWriteRecordsTheMove(t *testing.T) { func tokenNodes(exec *ActionExecutor) string { names := make([]string, 0, len(exec.tokens)) for _, token := range exec.tokens { - names = append(names, ActionNodeName(token.Location)) + names = append(names, ActionNodeName(token.Node())) } slices.Sort(names) return strings.Join(names, " ") diff --git a/internal/exec/runtime/replay_test.go b/internal/exec/runtime/replay_test.go index 26eea52cc7..0a1c29f487 100644 --- a/internal/exec/runtime/replay_test.go +++ b/internal/exec/runtime/replay_test.go @@ -1747,8 +1747,8 @@ func TestReplayRefusedDecisionTakesNoBranch(t *testing.T) { if len(tokens) != 1 { t.Fatalf("%d tokens after the refusal, want the one at the decision", len(tokens)) } - if decision, ok := tokens[0].Location.(*ast.DecisionNode); !ok || decision.Name != "select" { - t.Errorf("token at %T, want decision select", tokens[0].Location) + if decision, ok := tokens[0].Node().(*ast.DecisionNode); !ok || decision.Name != "select" { + t.Errorf("token at %T, want decision select", tokens[0].Node()) } if got := FormatTraceValue(exec.Data()["handler"]); got != "0" { t.Errorf("handler = %s, want 0: no branch ran", got) diff --git a/internal/exec/runtime/robustness_action_flow_test.go b/internal/exec/runtime/robustness_action_flow_test.go index 0ba5a01d5d..1a39c3819a 100644 --- a/internal/exec/runtime/robustness_action_flow_test.go +++ b/internal/exec/runtime/robustness_action_flow_test.go @@ -357,9 +357,9 @@ func testUnguardedLoopThroughAMerge(t *testing.T) { if len(tokens) != 1 { t.Fatalf("after step %d: %d tokens, want 1 circling the loop", i, len(tokens)) } - _, atMerge := tokens[0].Location.(*ast.MergeNode) + _, atMerge := tokens[0].Node().(*ast.MergeNode) if atMerge != (i%2 == 0) { - t.Fatalf("after step %d: token at %T, want it alternating between m and a", i, tokens[0].Location) + t.Fatalf("after step %d: token at %T, want it alternating between m and a", i, tokens[0].Node()) } } err = stepped.RunToCompletion() diff --git a/internal/exec/runtime/snapshot.go b/internal/exec/runtime/snapshot.go index 997908d1ff..79db2eebc8 100644 --- a/internal/exec/runtime/snapshot.go +++ b/internal/exec/runtime/snapshot.go @@ -745,22 +745,22 @@ func cloneStepRepetitions(repeats map[repetitionGroupID]*stepRepetition) map[rep return cloned } -func cloneRepeatedPerfs(repeated map[ast.Node][]*actionFrame) map[ast.Node][]*actionFrame { +func cloneRepeatedPerfs(repeated map[NodeRef][]*actionFrame) map[NodeRef][]*actionFrame { if repeated == nil { return nil } - cloned := make(map[ast.Node][]*actionFrame, len(repeated)) + cloned := make(map[NodeRef][]*actionFrame, len(repeated)) for node, perfs := range repeated { cloned[node] = slices.Clone(perfs) } return cloned } -func clonePending(pending map[ast.Node]map[string][]Value) map[ast.Node]map[string][]Value { +func clonePending(pending map[NodeRef]map[string][]Value) map[NodeRef]map[string][]Value { if pending == nil { return nil } - cloned := make(map[ast.Node]map[string][]Value, len(pending)) + cloned := make(map[NodeRef]map[string][]Value, len(pending)) for node, pins := range pending { clonedPins := make(map[string][]Value, len(pins)) for pin, values := range pins { @@ -771,11 +771,11 @@ func clonePending(pending map[ast.Node]map[string][]Value) map[ast.Node]map[stri return cloned } -func cloneHeld(held map[ast.Node]map[string][]nodeObject) map[ast.Node]map[string][]nodeObject { +func cloneHeld(held map[NodeRef]map[string][]nodeObject) map[NodeRef]map[string][]nodeObject { if held == nil { return nil } - cloned := make(map[ast.Node]map[string][]nodeObject, len(held)) + cloned := make(map[NodeRef]map[string][]nodeObject, len(held)) for node, pins := range held { clonedPins := make(map[string][]nodeObject, len(pins)) for pin, objects := range pins { @@ -786,11 +786,11 @@ func cloneHeld(held map[ast.Node]map[string][]nodeObject) map[ast.Node]map[strin return cloned } -func cloneStaged(staged map[ast.Node]map[string][]stagedStream) map[ast.Node]map[string][]stagedStream { +func cloneStaged(staged map[NodeRef]map[string][]stagedStream) map[NodeRef]map[string][]stagedStream { if staged == nil { return nil } - cloned := make(map[ast.Node]map[string][]stagedStream, len(staged)) + cloned := make(map[NodeRef]map[string][]stagedStream, len(staged)) for node, pins := range staged { clonedPins := make(map[string][]stagedStream, len(pins)) for pin, entries := range pins { @@ -801,11 +801,11 @@ func cloneStaged(staged map[ast.Node]map[string][]stagedStream) map[ast.Node]map return cloned } -func cloneNested(nested map[ast.Node][]nestedDelivery) map[ast.Node][]nestedDelivery { +func cloneNested(nested map[NodeRef][]nestedDelivery) map[NodeRef][]nestedDelivery { if nested == nil { return nil } - cloned := make(map[ast.Node][]nestedDelivery, len(nested)) + cloned := make(map[NodeRef][]nestedDelivery, len(nested)) for node, deliveries := range nested { cloned[node] = slices.Clone(deliveries) } diff --git a/internal/exec/runtime/snapshot_test.go b/internal/exec/runtime/snapshot_test.go index 848867e7e4..342ce93841 100644 --- a/internal/exec/runtime/snapshot_test.go +++ b/internal/exec/runtime/snapshot_test.go @@ -119,7 +119,7 @@ func (r *steppedRun) digest() string { for _, exec := range r.actions { fmt.Fprintf(&b, "action %s state=%v tokens=%d results=%s\n", SymbolText(exec.action), exec.State(), len(exec.tokens), formatValues(exec.Results())) for _, token := range exec.tokens { - fmt.Fprintf(&b, " token %d at %s\n", token.ID, ActionNodeName(token.Location)) + fmt.Fprintf(&b, " token %d at %s\n", token.ID, ActionNodeName(token.Node())) } } for _, exec := range r.states { diff --git a/internal/exec/runtime/state_space.go b/internal/exec/runtime/state_space.go index fdacd428b3..3cfd9cb6b8 100644 --- a/internal/exec/runtime/state_space.go +++ b/internal/exec/runtime/state_space.go @@ -131,7 +131,7 @@ func (e *ActionExecutor) initializeDynamics() error { return err } run.start = e.ctx.clock.now - e.tokens = append(e.tokens, Token{ID: e.nextTokenID, Location: e.dynamicsNode(), frame: e.root}) + e.tokens = append(e.tokens, Token{ID: e.nextTokenID, Location: NodeRef{Graph: e.graph}, frame: e.root}) e.nextTokenID++ e.state = StateRunning return nil diff --git a/internal/exec/runtime/state_statements.go b/internal/exec/runtime/state_statements.go index 3ab16be4db..2e992c99e9 100644 --- a/internal/exec/runtime/state_statements.go +++ b/internal/exec/runtime/state_statements.go @@ -389,8 +389,8 @@ func (h *stateStmtHost) rootFrame(attrs []frame) *actionFrame { connections: h.exec.graph.Connections, data: make(map[string]Value), features: make(map[string]ast.FeatureDirection), - subactions: make(map[ast.Node]*actionFrame), - nodes: h.behavior.Nodes, + subactions: make(map[NodeRef]*actionFrame), + nodes: blockNodeRefs(h.behavior.Body), multiplicities: h.behavior.Multiplicities, label: h.describe(), outer: []frame{h.dataFrame()}, @@ -663,7 +663,7 @@ func (h *stateStmtHost) returnAround(name string, value Value) (bool, error) { } // pauseAt sets no breakpoint: a state behavior's nodes are not stepped. -func (h *stateStmtHost) pauseAt([]ast.Node, ast.Node) error { +func (h *stateStmtHost) pauseAt([]NodeRef, NodeRef) error { return nil } diff --git a/internal/exec/runtime/stream_stage_test.go b/internal/exec/runtime/stream_stage_test.go index cb305f8e55..ad48d1beaf 100644 --- a/internal/exec/runtime/stream_stage_test.go +++ b/internal/exec/runtime/stream_stage_test.go @@ -17,7 +17,7 @@ func intConst(i int64) Value { // flow: a source's next write to a pin replaces its own earlier value however many other // sources wrote in between, and taking a delivery shifts what every source left waiting. func TestStagedStreamsAreKeptPerSource(t *testing.T) { - target := &ast.Usage{NodeBase: ast.NodeBase{NodeSpan: source.Span{Offset: 10, Len: 1}}, Kind: ast.UsageAction} + target := NodeRef{ID: 1} flow := &ast.Usage{NodeBase: ast.NodeBase{NodeSpan: source.Span{Offset: 20, Len: 1}}, Kind: ast.UsageFlow} frame, a, b := &actionFrame{}, &actionFrame{}, &actionFrame{} queue := func() []int64 { diff --git a/internal/exec/runtime/trace.go b/internal/exec/runtime/trace.go index 15fb6f73c0..84f84cade9 100644 --- a/internal/exec/runtime/trace.go +++ b/internal/exec/runtime/trace.go @@ -321,7 +321,7 @@ func (tr *TraceRecorder) recordActionStep(step int, tokens []Token) { // Format: step N: token T1@node1, token T2@node2 var parts []string for _, t := range sorted { - nodeName := nodeIdentifier(t.Location) + nodeName := nodeIdentifier(t.Node()) parts = append(parts, fmt.Sprintf("token %d@%s", t.ID, nodeName)) } @@ -488,7 +488,7 @@ func (tr *TraceRecorder) recordActionTerminate(perf string, dropped []Token) { } parts := make([]string, 0, len(dropped)) for _, t := range dropped { - parts = append(parts, fmt.Sprintf("token %d@%s", t.ID, nodeIdentifier(t.Location))) + parts = append(parts, fmt.Sprintf("token %d@%s", t.ID, nodeIdentifier(t.Node()))) } tr.line(fmt.Sprintf("terminate %s: dropped %s", perf, strings.Join(parts, ", "))) } diff --git a/internal/frontend/lsp/debug.go b/internal/frontend/lsp/debug.go index c644dbbe4d..57064a10ee 100644 --- a/internal/frontend/lsp/debug.go +++ b/internal/frontend/lsp/debug.go @@ -605,7 +605,9 @@ func (sess *debugSession) applyBreakpoints() { if sess.action != nil { bps := make([]runtime.NodeBreakpoint, 0, len(sess.breakpoints)) for _, bp := range sess.breakpoints { - bps = append(bps, runtime.NodeBreakpoint{Within: bp.within, Node: bp.node}) + if at, ok := sess.action.BreakpointAt(bp.within, bp.node); ok { + bps = append(bps, at) + } } sess.action.ReplaceBreakpointsAt(bps) return @@ -707,7 +709,7 @@ func (sess *debugSession) pause() { return } if bp, ok := sess.action.PausedBreakpoint(); ok { - sess.paused, _ = sess.actions.Node(bp.Within, bp.Node) + sess.paused, _ = sess.actions.Node(runtime.NodesOf(bp.Within), bp.Node.Node()) sess.pausedName = sess.action.PausedAt() } } @@ -1241,14 +1243,14 @@ func (sess *debugSession) breakpointIDs() []string { func (sess *debugSession) actionSnapshot(snap *debugSnapshot) { exec := sess.action for _, tr := range exec.TraversalsSince(sess.fired) { - if edge, ok := sess.actionEdge(tr.Within, tr.Edge); ok { + if edge, ok := sess.actionEdge(runtime.NodesOf(tr.Within), tr.Edge); ok { snap.Taken = append(snap.Taken, edge) } } sess.fired = exec.TraversalCount() for _, tok := range exec.Tokens() { - within := tok.Within() - id, drawn := sess.actions.Node(within, tok.Location) + within := runtime.NodesOf(tok.Within()) + id, drawn := sess.actions.Node(within, tok.Node()) t := debugToken{ID: tok.ID, Node: id, Placed: drawn, Waiting: debugWait(tok)} if tok.Wait != nil && tok.Wait.Timed { due := tok.Wait.Due diff --git a/internal/frontend/repl/debugger.go b/internal/frontend/repl/debugger.go index 8259ef6407..62e4808f2b 100644 --- a/internal/frontend/repl/debugger.go +++ b/internal/frontend/repl/debugger.go @@ -231,7 +231,7 @@ func (s *Session) debugReport(lines []string) DebugReport { report.PausedAt = exec.PausedAt() report.Breakpoints = exec.Breakpoints() for _, tok := range exec.Tokens() { - t := TokenReport{ID: tok.ID, Node: nodeLabel(tok.Location)} + t := TokenReport{ID: tok.ID, Node: nodeLabel(tok.Node())} if awaiting := exec.Awaiting(tok); len(awaiting) > 0 { t.ArrivedFrom = nodeLabel(tok.Via.Source) for _, edge := range awaiting { diff --git a/internal/frontend/repl/meta.go b/internal/frontend/repl/meta.go index 0dbfa88764..0520537e2a 100644 --- a/internal/frontend/repl/meta.go +++ b/internal/frontend/repl/meta.go @@ -3167,7 +3167,7 @@ func (s *Session) doTokens() ([]string, bool, error) { out := []string{fmt.Sprintf("Active tokens (%d):", len(tokens))} for _, tok := range tokens { - line := fmt.Sprintf(" Token %d @ %s", tok.ID, nodeLabel(tok.Location)) + line := fmt.Sprintf(" Token %d @ %s", tok.ID, nodeLabel(tok.Node())) if awaiting := exec.Awaiting(tok); len(awaiting) > 0 { line += fmt.Sprintf(" (arrived from %s; awaiting %s)", nodeLabel(tok.Via.Source), sourceLabels(awaiting)) } From c9899ca04a6eccb9fdb636452e155682025b9ea9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:33:42 +0000 Subject: [PATCH 2/2] fix(runtime): resolve identity breakpoints in deferred flows BreakpointAt unfolds a deferred subflow (a typed body nested in its own type) as the performance running it does, so a node of that flow is one a breakpoint can name; Subflow.Graph stays nil for such a flow. Co-Authored-By: jason.han --- internal/exec/runtime/action_node_ref.go | 12 ++-- .../exec/runtime/debug_breakpoint_ids_test.go | 62 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/internal/exec/runtime/action_node_ref.go b/internal/exec/runtime/action_node_ref.go index fd62bdce02..6176443d9e 100644 --- a/internal/exec/runtime/action_node_ref.go +++ b/internal/exec/runtime/action_node_ref.go @@ -73,12 +73,14 @@ func (e *ActionExecutor) BreakpointAt(within []ast.Node, node ast.Node) (NodeBre return NodeBreakpoint{Within: refs[:len(within)], Node: refs[len(within)]}, true } -// nestedRef references node among those a performance of owner, a node of flow, holds -// as its own: the nodes of the flow owner owns, or of the block flows of its body. +// nestedRef references node among those a performance of owner, a node of flow, holds as +// its own: the nodes of the flow owner owns (a deferred one unfolded), or of its body's block flows. func nestedRef(flow *lower.ActionGraph, owner, node ast.Node) (NodeRef, bool) { - if sub := flow.Subflows[owner]; sub != nil && sub.Graph != nil { - if ref, ok := locateNode(sub.Graph, node); ok { - return ref, true + if sub := flow.Subflows[owner]; sub != nil { + if graph, err := sub.Unfold(); err == nil { + if ref, ok := locateNode(graph, node); ok { + return ref, true + } } } if block := lower.NestedFlow(flow, owner, node); block != nil { diff --git a/internal/exec/runtime/debug_breakpoint_ids_test.go b/internal/exec/runtime/debug_breakpoint_ids_test.go index 71cd72f3ed..7bae6e33d5 100644 --- a/internal/exec/runtime/debug_breakpoint_ids_test.go +++ b/internal/exec/runtime/debug_breakpoint_ids_test.go @@ -53,3 +53,65 @@ func TestBreakpointAtResolvesNodesInTheirFlows(t *testing.T) { t.Errorf("tokens = %v, want the one at choose, keyed by its id", tokens) } } + +// A node of a deferred flow, the typed body of a step nested in its own type, is one a +// breakpoint can name: the path unfolds the flow, as the performance running it does. +func TestBreakpointAtUnfoldsADeferredFlow(t *testing.T) { + ctx, sym := loadAction(t, `package test { + action def Walk { + in depth = 3; + out reached = 0; + attribute visited = 1; + if depth > 0 { + action step : Walk { in depth = depth - 1; assign visited := visited + 1; } + then assign reached := step.reached + 1; + } else { + assign reached := 1; + } + } +}`, "Walk") + exec, err := ctx.CreateActionExecutor(sym) + if err != nil { + t.Fatalf("CreateActionExecutor: %v", err) + } + graph := exec.Graph() + var step ast.Node + for _, node := range graph.Nodes { + for _, nested := range graph.BlockNodes[node] { + if ActionNodeName(nested) == "step" { + step = nested + } + } + } + if step == nil { + t.Fatalf("Walk's flow declares no step: %v", exec.NodeNames()) + } + block, ok := locateNode(graph, step) + if !ok { + t.Fatalf("step is not located in a block flow of Walk's") + } + if inner := block.Graph.Subflows[step]; inner == nil || inner.Deferred == nil || inner.Graph != nil { + t.Fatalf("step's flow is not deferred: %+v", inner) + } + + at := breakpointAt(t, exec, []ast.Node{step}, step) + if len(at.Within) != 1 || at.Within[0].Node() != step || at.Node.Node() != step || at.Node.Graph == at.Within[0].Graph { + t.Fatalf("BreakpointAt = %+v, want step by id in the block flow of the unfolded body, under step in Walk's", at) + } + exec.ReplaceBreakpointsAt([]NodeBreakpoint{at}) + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion: %v", err) + } + if got := exec.PausedAt(); got != "step" { + t.Fatalf("PausedAt() = %q, want step one level down", got) + } + if bp, ok := exec.PausedBreakpoint(); !ok || bp.Node != at.Node || !slices.Equal(bp.Within, at.Within) { + t.Errorf("PausedBreakpoint() = %+v, %v, want the breakpoint set, by the same ids", bp, ok) + } + if err := exec.RunToCompletion(); err != nil { + t.Fatalf("RunToCompletion after the pause: %v", err) + } + if got := exec.PausedAt(); got != "" || !exec.State().Ended() { + t.Errorf("after resuming: PausedAt() = %q, state %v; want the run ended, the breakpoint firing once", got, exec.State()) + } +}