feat(runtime): interrupt an inline do body between its statements - #362
Merged
Merged
Conversation
…its rest at exit Co-Authored-By: jason.han <hanhuijun@gmail.com>
…e's checkpoints Co-Authored-By: jason.han <hanhuijun@gmail.com>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
A state's inline
do action { s1; s2; s3; }was lowered to onelower.StateBehaviorwhose blockexecuteBehaviorran to its end in one call, so a do round advanced the whole body as one action and a transition out of the state could interrupt it only between rounds — never betweens1ands2. The one-action-per-statementdo { action a; action b; }form already interleaved per statement;state_anonymous_do_atomicpinned the difference (123456against124356), andspec-compliance.mdcarried it as a Known Limitation.Now a do behavior's statement block yields after each statement it performs, and the do round resumes it one statement at a time:
bodyRungainsyields(set bystartDoRunfor do behaviors only) andperformed;bodyPausegainsyielded.Context.yieldBodypauses the body before its next statement once one has performed since the last resume;Context.bodyPerformedmarks one.bodyRun.resumeclearsperformed, so a paused run always makes progress when resumed.stmtEngine.runcallsyieldBodybefore each statement after the first,loop/forLoopbefore each iteration,blockFlowbefore each node, and marks the body performed after every statement that completes — a simple one, or a compound one (if, a loop) whose branch or iterations performed nothing themselves, so anifselecting an empty branch is a round too. The statements inside a compound one are rounds of their own, so an iteration of two statements is two rounds, and a transition triggered between them leaves the iteration's second statement unrun with the iterations after it. ThestmtListFrame/loopFrame/flowNodeFramecursors already on the continuation stack are what the pause keeps, so locals, nested blocks and loop collections resume exactly where they left off.lowerBehaviorBody→stateStmtHost.runFlow→ActionExecutor.driveSubflow) yields after a step in which a token performed a node (an action, a statement node), before the next step that would perform one — control nodes (initial, fork, join, merge, decision) and the end of a wait are routed through without spending a round. With several tokens (after a fork) each steps one node in the round.doRun.resumeresumes a yielded pause as it does a wait's;doRun.resumablereports a yielded body due at once, sorunDoRoundpicks it through the existingchooseDoAction(entry order by default,seed:<n>draws,exploresweeps). No new choice kind, andchoicelines are spelled as before.bodyRun.end:stopDoAction/endDoRunend a yielded run as they end one paused on a wait, so leaving the state (a transition from it or an ancestor, the region left, the machine terminated, the executor released) drops the pending statements with the behavior, and the exit behavior runs after, as before.checkspells such a body asyielded between statements.internal/core/loweris unchanged: theBlockthe behavior is lowered to already indexes its statements.Two showcases moved with the granularity, both reproduced by hand against the built binary:
examples/runtime-showcase/spacecraft-comms.sysml: thetransmitDataandrechargedo behaviors are token flows. At the instant the drain, frame-send and charge timers expire together, the drain and the charge each run in that round (in the schedule's order) and the drain's decision reads the battery in the round after, when the charge's 1 % is in whichever way the round went — soBatteryLowgoes out at t=80 with 50 frames sent under every schedule, where the schedules used to differ (t=79/49 frames underreverse/declared, t=80/50 underseed:7/seed:42). The end is the same:transmitted | notRechargingat t=241, 100 frames, battery 100. The summary reads 592 choice points and 808 do behavior actions. README andinternal/repl/spacecraft_showcase_test.gofollow the observed run.state_terminate_entry_do_exit_behaviors: with thegosignal queued before the run, the do body is now interrupted afterassign later := 5and never reaches theterminatethe fixture is about. The transition out is timed (accept after 1) so the body reaches it; what the fixture proves —terminatein an entry, do or exit body ends that behavior at the statement, the state stays active — is unchanged.robustness_test.go:testStateDoBodyAcceptRunsBeforeTheChoiceReadskeeps its contract (the do behavior goes on with the signal before the transition fires, and the choice reads the rewritten guard); it now also checks that the node afterflipruns in the following round rather than the same one.state_anonymous_do_atomic.trace.golden, before and afterBefore,
lwork's body ran whole in its round (1,2,3), thenrwork's (4,5,6):123456. After (entry order, the default policy):124356is whatstate_concurrent_do's one-action-per-statement form reaches in entry order; the fixture's.expected.jsonnow admits the same four interleavings (124356,124536,142356,142536), derived indocs/project/behavior-semantic-oracle.md§ "Do behaviors of sibling regions active at one instant".Specification basis
SysML v2 §7.18.3 (state transition semantics), step 1: the source state's do action, "if it is still being performed, is interrupted" when the transition is triggered — now also between the statements of a body written as one inline action. KerML
StatePerformances.kerml: the do behavior is amiddleperformance no succession joins to another region's, so orthogonal regions' bodies interleave.docs/project/spec-compliance.md: the Known Limitations bullet ("an inline entry/do/exit body is one action…") is removed and the State Machine map row "An inline body is one action, so a do round runs it to its end" is restated as the interruption/interleaving row, ✅ Faithful.docs/guide/06-behavior.mddescribes the do body as one statement per round. Changelog fragmentchanges/unreleased/resumable-inline-do-body.changed.md.How it was verified
New coverage:
state_do_body_interrupted_by_signal.sysml+.expected.json+ trace golden: three statements,Stopaccepted after the first — the trace showss1, the exit behavior and the accept;s2ands3never run.state_concurrent_inline_do_bodies.sysml+.expected.json+.check.expected.json+ default/declared/seed:1trace goldens: aforloop and anifblock in two regions' inline bodies interleave one iteration / one nested statement a round and reach exactly the four interleavings ofstate_concurrent_do.state_anonymous_do_atomicregenerated with-update-traces(above), its.expected.jsonnow an admissible set,.check.expected.jsonand policy goldens added.robustness_resumable_inline_do_body_test.go(TestRuntimeRobustnessResumableInlineDoBody): aforbody left mid-loop drops its pending iterations with no frame, do work, clock wait or goroutine left; one left after the first statement of an iteration drops the iteration's second statement too; anifwith an empty branch and awhileended at once each spend a round; a body left at a clock wait after a loop leaves the clock empty; a non-terminating loop and a non-terminating stated flow each end withErrStepLimitExceeded.state_do_action_successions_first_start(nodesaandbin two rounds),state_terminate_this_ends_performer(theterminatein the round after the assignment),state_terminate_entry_do_exit_behaviors(see above).Unchanged:
state_concurrent_do.*, thestate_terminate_*/action_terminate_*fixtures other than the two named,state_do_action_*sources and expectations, everyinternal/core/lowertest.Gates:
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