Skip to content

feat(runtime): execute stated flows in nested loop and branch bodies - #469

Open
devin-ai-integration[bot] wants to merge 6 commits into
developfrom
feature/nested-action-node-in-body
Open

devin-ai-integration[bot] wants to merge 6 commits into
developfrom
feature/nested-action-node-in-body

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

What and why

A for/while/loop body or an if branch that states its own token flow — nested action nodes joined by succession a then b;, first, forks, joins, a then terminate; — previously lowered those nodes as Unsupported and reported them when reached; only statements ran in such bodies. This makes a stated body flow a first-class execution path:

  • Lowering (internal/ir/lower): lowerActionFlow(members, scope, resolver) is the member-list core ToActionGraphWith now calls; lowerStatedBlock runs it over a body whose members state a flow (statesOwnFlow) and returns Block{Graph, Stated: true} (not Own, which stays reserved for the host's own flow). A malformed stated body keeps its graph with ActionGraph.Invalid set so the error surfaces at initialize() as ErrInvalidActionFlow, not at construction and not when reached. Declaration-order bodies keep the existing blockNeedsFlow path untouched.
  • Runtime (internal/exec/runtime): stmtEngine.runBlock distinguishes
    case block.Stated && block.Own: return e.host.runFlow(block)      // host's own flow, unchanged
    case block.Stated:              return e.host.runBlockFlow(e, block)
    performBlockFlow creates 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 same runOwnFlow/runSubflow machinery a node-owned flow uses, and pauses/resumes through blockFlowFrame. within() and terminateTargets walk past body frames, so terminate; in a body ends the enclosing action node, and a node's own terminate; still ends only that node. Action, state and calc hosts implement runBlockFlow; a calc host with no performances returns the typed ErrStatementNotExecutable.
  • Parser (internal/syntax/parser/behavior.go): loop and if-branch bodies appended members directly instead of going through bodyBuilder, so a member-attached then … there kept an unnamed source (and then <statement> was lost). They now use bodyBuilder like 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 the then <statement> it came from (export/behavior.go positionalSuccessions), symmetrically with unnamed sources.

Remaining typed refusals, named in the compliance row: accept in 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 develop is not on: stmtHost gains one method (runBlockFlow), stmtEngine.runBlock gains one case, actionFrame gains the body flag read by within() and terminateTargets, enterBodyFlow falls back to perf.describe() for an unnamed node, and validateSubflows checks Invalid for nested graphs. runFlow, applyDataFlows, state_executor.go and inline do bodies 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.md and removes the nested-node bullet from Known Limitations.

How it was verified

  • Conformance fixtures + expected JSON, with trace goldens where ordering matters: 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 at initialize(), unsequenced statement, non-terminating body flow, calc body).
  • Lowering tests in lower/block_graph_test.go; export round-trip test TestThenBeforeUnnamedAssignmentRoundTrips.
  • go build ./..., go vet ./..., gofmt -l . empty, go test -count=1 ./... green; training and pilot corpus gates and TestCorpusRoundTrip with the require variables set (no ratchet movement); pilot-library XMI identity gate; scripts/check-doc-ids.py, scripts/changelog.py check.

Checklist

  • make test and make lint pass locally
  • Tests added or updated for the change
  • Documentation extended where it already covers the surface (see CONTRIBUTING.md)
  • Changelog entry added as changes/unreleased/<slug>.<section>.md, not as an edit to CHANGELOG.md
  • baselines regenerated and make docs-counts run if a gate count moved (compliance rows need nothing: the census is counted at docs build)
  • No internal work-item labels (waves, slices, F4, K5) in the body, docs, or changelog

devin-ai-integration Bot and others added 3 commits September 20, 2026 17:14
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>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration
devin-ai-integration Bot marked this pull request as ready for review September 20, 2026 20:31

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 1 potential issue.

Devin Review

Comment on lines +335 to +336
func (h *stateStmtHost) runBlockFlow(engine *stmtEngine, block lower.Block) (stmtFlow, error) {
return h.perfs.performBlockFlow(h.perfs.root, engine, block)

@devin-ai-integration devin-ai-integration Bot Sep 20, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

devin-ai-integration Bot and others added 2 commits September 20, 2026 20:54
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant