feat(runtime): execute stated flows in nested loop and branch bodies - #469
devin-ai-integration[bot] wants to merge 6 commits into
Conversation
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
… bodies Co-Authored-By: jason.han <hanhuijun@gmail.com>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
| func (h *stateStmtHost) runBlockFlow(engine *stmtEngine, block lower.Block) (stmtFlow, error) { | ||
| return h.perfs.performBlockFlow(h.perfs.root, engine, block) |
There was a problem hiding this comment.
🟡 State flow errors surface late
A malformed stated flow is validated only when runBlockFlow reaches it. Earlier statements can mutate state, while untaken branches hide the error.
Learn more
Action execution validates every nested graph before spawning its initial token in initialize. State behavior graphs receive no equivalent initialization pass. This guard therefore validates only a block selected during statement execution, after entry behavior or preceding statements can already run. A malformed flow in an untaken conditional is never validated.
Example: A do behavior assigns n := 1 before entering a loop containing an invalid stated flow. The assignment persists before ErrInvalidActionFlow is returned. If the invalid flow sits in an if false branch, the machine can complete without any error.
Recommended fix: Traverse every entry, do, exit, and transition-effect StateBehavior during StateExecutor.initialize(). Validate all stated block graphs recursively before entering the machine or starting behaviors, without mutating the behavior executor's active graph.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed at the layer both hosts share: enterBodyFlow/enterSubflow now refuse a graph whose Invalid is set with ErrInvalidActionFlow before any token is placed, so a partial stated graph never runs from a state behavior (test state_body_flow_unsequenced_statement). The standalone action path is unchanged and still reports at initialize().
Not taken: an eager pass over every state behavior's body at StateExecutor.initialize(). State behaviors validate their inline flow when they start (runFlow) today, before this PR; moving that to executor initialization is a timing change to state_executor.go that belongs with the resumable-inline-body work on that file, not to nested body nodes.
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
What and why
A
for/while/loopbody or anifbranch that states its own token flow — nested action nodes joined bysuccession a then b;,first, forks, joins, athen terminate;— previously lowered those nodes asUnsupportedand reported them when reached; only statements ran in such bodies. This makes a stated body flow a first-class execution path:internal/ir/lower):lowerActionFlow(members, scope, resolver)is the member-list coreToActionGraphWithnow calls;lowerStatedBlockruns it over a body whose members state a flow (statesOwnFlow) and returnsBlock{Graph, Stated: true}(notOwn, which stays reserved for the host's own flow). A malformed stated body keeps its graph withActionGraph.Invalidset so the error surfaces atinitialize()asErrInvalidActionFlow, not at construction and not when reached. Declaration-order bodies keep the existingblockNeedsFlowpath untouched.internal/exec/runtime):stmtEngine.runBlockdistinguishesperformBlockFlowcreates a transparent body performance per iteration (actionFrame.body = true) with its own data/features maps, the statement engine's locals, and inherited connections; body-local attributes are seeded per performance; the flow is driven by the samerunOwnFlow/runSubflowmachinery a node-owned flow uses, and pauses/resumes throughblockFlowFrame.within()andterminateTargetswalk past body frames, soterminate;in a body ends the enclosing action node, and a node's ownterminate;still ends only that node. Action, state and calc hosts implementrunBlockFlow; a calc host with no performances returns the typedErrStatementNotExecutable.internal/syntax/parser/behavior.go): loop andif-branch bodies appended members directly instead of going throughbodyBuilder, so a member-attachedthen …there kept an unnamed source (andthen <statement>was lost). They now usebodyBuilderlike an action body. This changes the parse of the standard library itself (Actions::ForLoopAction::whileLoop): the stdlib snapshot and three parser goldens are regenerated, and the RDF back-to-notation writer learned to spell a positional succession to an unnamed statement target as thethen <statement>it came from (export/behavior.gopositionalSuccessions), symmetrically with unnamed sources.Remaining typed refusals, named in the compliance row:
acceptin a declaration-order body (the block-flow executor cannot park a token), a stated flow in a calculation body, and body succession probability metadata when lowering has no resolver.Shared executor plumbing touched
Kept to the seams other work on
developis not on:stmtHostgains one method (runBlockFlow),stmtEngine.runBlockgains one case,actionFramegains thebodyflag read bywithin()andterminateTargets,enterBodyFlowfalls back toperf.describe()for an unnamed node, andvalidateSubflowschecksInvalidfor nested graphs.runFlow,applyDataFlows,state_executor.goand inlinedobodies are untouched.Specification basis
SysML v2 1.0 §7.16 (action successions and control nodes apply to the nodes of any action body, including one written inside a loop or conditional node); KerML §7.4 (each performance is its own feature space). Moves the "A block has a token flow of its own" row of the Actions map in
docs/project/spec-compliance.mdand removes the nested-node bullet from Known Limitations.How it was verified
action_body_flow_succession,action_body_flow_fork_join,action_body_flow_branch_in_loop,action_body_flow_while_attribute,action_body_flow_terminate_statement,action_body_flow_terminate_node,action_body_node_pins_from_loop_variable,action_body_node_terminate,state_body_flow_succession.robustness_nested_node_body_test.go:TestRuntimeRobustnessNestedNodeInBody(malformed stated flow atinitialize(), unsequenced statement, non-terminating body flow, calc body).lower/block_graph_test.go; export round-trip testTestThenBeforeUnnamedAssignmentRoundTrips.go build ./...,go vet ./...,gofmt -l .empty,go test -count=1 ./...green; training and pilot corpus gates andTestCorpusRoundTripwith the require variables set (no ratchet movement); pilot-library XMI identity gate;scripts/check-doc-ids.py,scripts/changelog.py check.Checklist
make testandmake lintpass locallychanges/unreleased/<slug>.<section>.md, not as an edit toCHANGELOG.mdmake docs-countsrun if a gate count moved (compliance rows need nothing: the census is counted at docs build)F4,K5) in the body, docs, or changelog