Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/unreleased/terminate-nested-flow-target.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **A `terminate` inside a loop body names its own loop performance's node only.** `terminate slow;` in a loop body reached by several tokens ended the paused `slow` performances of the other loop performances too, and ran on as if it had ended its own. Each step of a statement node now owns what its body performs, so the terminate ends that body's `slow`; one that has already ended is reported as `performance already ended`, since `TerminateAction` ends an occurrence during its own performance.
2 changes: 1 addition & 1 deletion docs/project/spec-compliance.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions internal/exec/runtime/action_body_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,17 +484,20 @@ type statementWork struct {
token int64
frame *actionFrame
node ast.Node
done bool
// step is the node's performance, owning the nodes its body performs.
step *actionFrame
done bool
}

func (w *statementWork) clone() bodyWork { c := *w; return &c }

func (w *statementWork) perform() error {
e := w.exec
if !w.done {
if err := e.executeBody(w.frame, w.frame.graph, w.node); err != nil {
if err := e.executeStatementBody(w.step, w.frame.graph); err != nil {
return err
}
w.step.ended = true
w.done = true
}
idx, err := e.workToken(w.token)
Expand Down
27 changes: 26 additions & 1 deletion internal/exec/runtime/action_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3065,7 +3065,32 @@ 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]
return e.runBody(tokenIdx, &statementWork{exec: e, token: token.ID, frame: token.frame, node: token.Location})
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})
}

// beginStatementStep is a token's step of a statement node in frame: a transparent performance
// owning what the step's body performs, so a `terminate` there names the step's own.
func (e *ActionExecutor) beginStatementStep(frame *actionFrame, node ast.Node) *actionFrame {
scope := frame.graph.Scopes[node]
if scope == nil {
scope = frame.scope
}
return &actionFrame{
node: node,
flow: frame.graph,
scope: scope,
parent: frame,
connections: frame.connections,
data: make(map[string]Value),
features: make(map[string]ast.FeatureDirection),
subactions: make(map[ast.Node]*actionFrame),
perfs: &e.performances,
run: frame.run,
began: e.ctx.newActivation(),
label: frame.describe(),
body: true,
}
}

// leaveStatementNode takes the token at tokenIdx on from node, retiring it where
Expand Down
28 changes: 25 additions & 3 deletions internal/exec/runtime/action_statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type actionStmtHost struct {
perf *actionFrame
// graph is the flow node is a node of.
graph *lower.ActionGraph
// step is the performance of a statement node's step, owning what its body performs.
step *actionFrame
}

// executeBody runs the lowered statements graph records for node in perf, the
Expand All @@ -31,6 +33,26 @@ func (e *performances) executeBody(perf *actionFrame, graph *lower.ActionGraph,
return err
}

// executeStatementBody runs the statements of the statement node step performs, in the
// performance around it, with what they perform owned by step.
func (e *performances) executeStatementBody(step *actionFrame, graph *lower.ActionGraph) error {
perf := step.parent
_, err := e.ctx.runStatements(func() *stmtEngine {
host := &actionStmtHost{exec: e, node: step.node, perf: perf, graph: graph, step: step}
lexical := perf.lexicalFrames()
return newStmtEngineIn(e.ctx, host, lexical[len(lexical)-1], lexical[:len(lexical)-1])
}, graph.Bodies[step.node])
return err
}

// around is the performance the body's nodes are performed in and its terminates resolve from.
func (h *actionStmtHost) around() *actionFrame {
if h.step != nil {
return h.step
}
return h.perf
}

// runNodeBody runs the statements a control or initial node's body declares,
// which the token passing through the node performs.
func (e *ActionExecutor) runNodeBody(frame *actionFrame, node ast.Node) error {
Expand Down Expand Up @@ -160,7 +182,7 @@ func (h *actionStmtHost) acceptReturn(Value, lower.Return) error {
func (h *actionStmtHost) effect(engine *stmtEngine, s lower.Effect) error {
env := engine.env
if s.Kind == lower.EffectTerminate {
return h.exec.terminate(engine, h.perf, s)
return h.exec.terminate(engine, h.around(), s)
}
if s.Kind == lower.EffectStart {
if err := h.exec.ctx.startEffect(engine.evalIn(s.Scope), s, h.exec.self); err != nil {
Expand Down Expand Up @@ -204,7 +226,7 @@ func (h *actionStmtHost) effect(engine *stmtEngine, s lower.Effect) error {
// performNode performs a nested action a block of the body declares as a
// subperformance of the body's.
func (h *actionStmtHost) performNode(engine *stmtEngine, graph *lower.ActionGraph, node *ast.Usage) (stmtFlow, error) {
return h.exec.performNode(h.perf, engine, graph, node)
return h.exec.performNode(h.around(), engine, graph, node)
}

// runFlow rejects a stated flow among statements: an action's own flow is the
Expand All @@ -215,7 +237,7 @@ func (h *actionStmtHost) runFlow(lower.Block) (stmtFlow, error) {
}

func (h *actionStmtHost) runBlockFlow(engine *stmtEngine, block lower.Block) (stmtFlow, error) {
return h.exec.performBlockFlow(h.perf, engine, block)
return h.exec.performBlockFlow(h.around(), engine, block)
}

// performNode performs node, which a block of parent's body declares, as a subperformance
Expand Down
134 changes: 134 additions & 0 deletions internal/exec/runtime/robustness_terminate_nested_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package runtime

import (
"errors"
"strings"
"testing"

"github.com/Open-MBEE/OpenSysML/internal/syntax/ast"
)

// TestRuntimeRobustnessTerminateNestedFlowTarget pins which performances a `terminate`
// in a loop body names: those of its own loop body's performance, never another's.
func TestRuntimeRobustnessTerminateNestedFlowTarget(t *testing.T) {
t.Run("terminate_in_a_loop_body_of_its_own_ended_performance", testTerminateInALoopBodyOfItsOwnEndedPerformance)
t.Run("terminate_in_a_loop_body_leaves_other_loop_performances", testTerminateInALoopBodyLeavesOtherLoopPerformances)
t.Run("terminate_in_a_loop_body_before_its_node_began", testTerminateInALoopBodyBeforeItsNodeBegan)
}

// executeLibraryActionSource executes the named action of src over the standard library.
func executeLibraryActionSource(t *testing.T, name, src string) (map[string]Value, error) {
t.Helper()
idx, _, ctx := buildRuntimeWithLibraries(t, "<test>", parseAndBuild(t, src))
sym := findSymbolByName(idx.DocumentRoot("<test>"), name, ast.DefAction)
if sym == nil {
t.Fatalf("action %s not found", name)
}
return ctx.ExecuteAction(sym)
}

// loopPerformancesHost reaches the loop node with three tokens a step apart, each
// performance of its body running slow (and the first two napping in it) before body.
func loopPerformancesHost(slow, body string) string {
return `package test {
private import ScalarValues::*;
private import ISQ::*;
private import SI::*;
action host {
out attribute entered : Integer = 0;
out attribute napped : Integer = 0;
out attribute later : Integer = 0;
fork split;
action pre;
action pre1;
action pre2;
succession first start then split;
succession first split then gate;
succession first split then pre;
succession first pre then gate;
succession first split then pre1;
succession first pre1 then pre2;
succession first pre2 then gate;
merge gate;
then loop {
action slow {
assign entered := entered + 1;
` + slow + `
}
` + body + `
} until true;
then done;
}
}`
}

const nappingSlow = `if entered <= 2 {
action inner {
first start;
then action nap accept after 10 [s];
then done;
}
assign napped := napped + 1;
}`

// testTerminateInALoopBodyOfItsOwnEndedPerformance: the third loop performance's
// slow has ended when its body runs `terminate slow;`, while the other two loop
// performances' slows are still paused. Those are not its own, so it names an ended
// performance, which TerminateAction cannot end during its performance.
func testTerminateInALoopBodyOfItsOwnEndedPerformance(t *testing.T) {
_, err := executeLibraryActionSource(t, "host", loopPerformancesHost(nappingSlow, `if entered == 3 {
terminate slow;
}
assign later := later + 1;`))
if !errors.Is(err, ErrPerformanceEnded) {
t.Fatalf("error = %v, want ErrPerformanceEnded", err)
}
if !strings.Contains(err.Error(), "slow") {
t.Fatalf("error = %v, want it to name slow", err)
}
}

// testTerminateInALoopBodyLeavesOtherLoopPerformances: the third loop performance's
// slow ends itself from a watch due before the other two naps end; those two are
// another loop performance's and nap on to completion.
func testTerminateInALoopBodyLeavesOtherLoopPerformances(t *testing.T) {
values, err := executeLibraryActionSource(t, "host", loopPerformancesHost(nappingSlow+` else {
action guard {
first start;
then fork f;
succession first f then nap;
succession first f then watch;
action nap accept after 10 [s];
action watch accept after 5 [s];
succession first watch then stop;
action stop { terminate slow; }
}
assign napped := napped + 10;
}`, `assign later := later + 1;`))
if err != nil {
t.Fatalf("execute: %v", err)
}
assertIntOutput(t, values, "entered", 3)
assertIntOutput(t, values, "napped", 2)
assertIntOutput(t, values, "later", 3)
}

// testTerminateInALoopBodyBeforeItsNodeBegan: `terminate slow;` ordered before slow
// in a loop body has no performance of slow to end, its own or another's.
func testTerminateInALoopBodyBeforeItsNodeBegan(t *testing.T) {
_, err := executeActionSource(t, "host", `package test {
private import ScalarValues::*;
action host {
out attribute x : Integer = 0;
first start;
then loop {
terminate slow;
then action slow { assign x := 1; }
} until true;
then done;
}
}`)
if !errors.Is(err, ErrTerminateTarget) {
t.Fatalf("error = %v, want ErrTerminateTarget", err)
}
}
5 changes: 5 additions & 0 deletions internal/exec/runtime/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,11 @@ func (e *ActionExecutor) reachableFrames() []*actionFrame {
visit(e.root)
for _, token := range e.tokens {
visit(token.frame)
if token.body != nil {
if w, ok := token.body.work.(*statementWork); ok {
visit(w.step)
}
}
for _, perf := range token.performed() {
visit(perf)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"type": "action",
"libraries": true,
"trace": true,
"outputs": {
"entered": {"type": "Integer", "value": 3},
"napped": {"type": "Integer", "value": 2},
"passed": {"type": "Integer", "value": 3},
"later": {"type": "Integer", "value": 3}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package test {
private import ScalarValues::*;
private import ISQ::*;
private import SI::*;

// The third loop performance's slow terminates itself while the first two nap: it names
// its own loop body's slow only, so the other two nap on and complete.
action host {
out attribute entered : Integer = 0;
out attribute napped : Integer = 0;
out attribute passed : Integer = 0;
out attribute later : Integer = 0;

fork split;
action pre;
action pre1;
action pre2;
succession first start then split;
succession first split then gate;
succession first split then pre;
succession first pre then gate;
succession first split then pre1;
succession first pre1 then pre2;
succession first pre2 then gate;
merge gate;
then loop {
action slow {
assign entered := entered + 1;
if entered <= 2 {
action inner {
first start;
then action nap accept after 10 [s];
then done;
}
assign napped := napped + 1;
} else {
action guard {
first start;
then fork f;
succession first f then nap;
succession first f then watch;
action nap accept after 10 [s];
action watch accept after 5 [s];
succession first watch then stop;
action stop { terminate slow; }
}
assign napped := napped + 1;
}
}
assign passed := passed + 1;
assign later := later + 1;
} until true;
then done;
}
}
Loading
Loading