feat(lsp): run a drawn behavior and report it in render IDs - #294
Merged
Merged
Conversation
Add the opensysml/debug/* requests (start, step, continue, send, advance, breakpoints, stop) and the opensysml/debugChanged notification. A session runs the state machine or action a state or action view draws, with the REPL's executors, in a runtime built over the workspace, and answers every request with a snapshot keyed by the rendering's node and edge IDs: active states with the composite states and regions enclosing them, tokens with the node they sit at and the edges they took or await, queued events, notes, results and the run's status. Breakpoints are set by render ID. Sessions survive edits that leave the target's declaration as it was, moving to the fresh IDs, and end when the target, performer or view is rewritten or removed. To place a run on a rendering, the runtime records the transitions a state machine fires and the successions each token travels, and Token.Within names the nested flows a token runs in; view.LocateStates/LocateActions map lowered vertices and edges to render IDs by declaration-relative position; model.Workspace.NewRuntime builds a runtime over a workspace. 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:
|
…tates, attach to the performer's behavior A debug continue on a state machine runs it to quiescence at the current instant, so an event due later leaves it waiting for an advance rather than moving the clock to it; the snapshot reports the wait as waiting. Breakpoints on states live in the runtime: StateExecutor.SetBreakpointAt pauses the machine as the dispatch entering the state completes, a state left again at the same instant included, and Context.AdvanceUntil stops an advance at that instant so the breakpoint is reported, not run past. A session started on an object that already exhibits or performs the target attaches to that running behavior instead of creating a second executor beside it, so timed effects happen once; an object running it under several usages is refused as ambiguous, as the REPL refuses it. Closing a document ends the sessions started on it directly, whether or not the workspace keeps the file's text from disk. Co-Authored-By: jason.han <hanhuijun@gmail.com>
A session compared only the target's and performer's declarations with what the runtime lowered, so an edit to a definition the target specializes or is typed by left a frozen run reported under a fresh rendering, and a rewritten view that still drew the target relocated the session the documentation said ends. Lowered graphs now list the declarations they took content from (StateGraph.Inherited, ActionGraph.Inherited), the root of a state or action rendering carries where they were written (view.Node.Inherited), and reconciliation ends the session when any of them is edited or removed, when the fresh drawing takes content from a declaration the runtime did not, or when the declared view's text changes; a pseudo-view has no declaration to compare. A machine's queue now lists every message pending on the bus rather than only those a state accepts now. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nested entry transitions where written Co-Authored-By: jason.han <hanhuijun@gmail.com>
…tests Co-Authored-By: jason.han <hanhuijun@gmail.com>
A session recorded the target, its performer and the definitions the target took content from, so an edit of an invoked action, an accepted signal, a feature's type or a value a guard named left a stale runtime under fresh IDs. The runtime now lists every declaration a run reads by following references from the roots through the workspace's documents (model.Runtime.Dependencies, Reading.Dependencies), a bare `accept Halt` included (resolve.RunReferences), and a session ends when one is edited, removed, resolved elsewhere, or newly read. A send whose signal or arguments name a declaration the run had not read checks it the same way before posting. Failed fork and route firings no longer leave the transitions never completed in the fired log; pseudostate breakpoints pause where they are set; and a standalone runtime index keeps the caller-supplied indexed documents. Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ted content by its own document `opensysml/debug/step` and the REPL's `%step` step an action through `ActionExecutor.StepToBreakpoint`, which suspends the run on a node a token has just reached, or already sits on, when a breakpoint is set there, as `RunToCompletion` does; the next step resumes past it. A lowered `StateGraph`/`ActionGraph` now answers `DocOf(decl)`, the document an inherited vertex, node, region, transition or succession was written in, from the scope of the general's body it came from. Renderings draw inherited content with that document as its origin, and the locators measure every span against the innermost enclosing declaration of its own document — the behavior drawn or one it inherits — so an unrelated edit in the document of a general no longer leaves inherited nodes and edges of a running session without a fresh render ID. `LocateActions` takes the lowered graph it places. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…am-execution Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
…me it on advance Setting a debug session's breakpoints replaces the executor's identity breakpoints without forgetting the stop already made at one kept, so a run resumed afterwards passes the node it paused at rather than pausing there again; a breakpoint removed and set again stops the run once more. An advance resumes an action paused at a breakpoint before moving the clock, as it does a machine, so the run reaches what is due by then. An identity breakpoint now names the occurrence of a node in one nested flow (NodeBreakpoint), so a breakpoint on one drawing of a declaration that several nested flows run stops only the token in that flow. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…itten in A workspace renderer read notation from the viewed document alone, so a trigger or guard a machine inherits from a definition in another document lost its text and drew as a placeholder. The renderer now reads any of the workspace's documents and the library behind them, as the REPL's does. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…am-execution Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
A compound transition through a choice logged only the branch out of it, since resolving the choice starts a fresh route; the segments into it are now logged once their effects have run, under the same rollback a failed firing already has. Co-Authored-By: jason.han <hanhuijun@gmail.com>
A breakpoint state's hit is staged as the state is entered, before its entry behavior runs; a dispatch failing after that returned without consuming it, so the next dispatch to succeed paused there, even once the breakpoint was cleared. Each dispatch now begins with no staged hit, alongside the fired-log mark. Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…am-execution Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
Entering the state that completes the machine completed it inside the dispatch, before the dispatch checked its breakpoints, so a breakpoint on `done` never suspended anything. A dispatch that staged a breakpoint now holds the completion (`completionDue`): the machine suspends standing on the completion vertex, and the next run, step or ProcessNextEvent after a resume finishes it — exit behaviors, StateCompleted and the end of its performance happen once, then. Snapshots and held images carry the held completion; the LSP step finishes it. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…am-execution Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…am-execution Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md # internal/core/runtime/state_change_trigger.go # internal/core/runtime/state_executor.go
…am-execution Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
The advance request's time was a plain float64, so a request that omitted it or sent null decoded as 0 and was accepted as a zero-duration advance. The field is now a pointer: omitted and null are refused with InvalidParams while an explicit 0 remains a valid advance that leaves the clock where it stands. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…am-execution Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
A debug request could run between a document update and the rebinding of the sessions that follow it, driving the old runtime and answering render IDs of the version the edit replaced. Every workspace mutation the server makes (open, change, close, watched files, folders, the initial walk) now lands under the lock debug requests hold, and the sessions are rebound before it is released; notifications still follow the unlock. Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
A machine or action kept every transition fired and succession taken for its lifetime. Now a dispatch keeps only its own firings and a run keeps no successions unless a debugger asks (KeepFired, KeepTraversals); FiredSince and TraversalsSince release the records before the mark they read from, while FiredCount and TraversalCount go on counting the released ones so marks stay valid. Snapshots and held images carry the kept records with their released count. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…am-execution Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
…am-execution Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md # internal/core/runtime/classifier_behavior.go
…am-execution Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md # internal/core/runtime/state_executor.go # internal/core/runtime/state_route.go
A view usually exposes a behavior another document declares, so debug/start looks the target and the object up in the view's document first and then among the other documents the runtime holds, refusing a qualified name two of them declare. A session remembers the declaring documents and rebinds through them, ending when any is closed. Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…is closed A close keeps a document whose file is on disk, so rebinding still found the target's and the object's documents and a cross-document session went on after either was closed. The close now ends every session standing on the closed document: the view's, the target's and the object's. 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
The VS Code diagram panel and the CLI draw state machines and action flows as static pictures, while the REPL can step the same behaviors textually; nothing joins the two. This PR adds the server half of live execution on a diagram: an LSP-hosted debug service that runs the behavior a view draws and reports where the run stands in that rendering's node and edge IDs, so a client can overlay it on the drawing it already has. The panel's toolbar and animation come in a follow-up that consumes this protocol.
New requests, advertised as the experimental capability
openSysmlDebug:A snapshot carries
state(ready | running | waiting | suspended | completed | failed | ended), the virtualtime, and everything in render IDs:activeStates(each active leaf with the composite states and regions enclosing it, outermost first),tokens({id, node, awaiting[]}),takenedges since the previous request,queue,breakpoints,pausedAt,notes,results,root/versionof the rendering it is keyed against, and a per-sessionrevision— adebugChangednotification is captured under the session's lock but sent outside it, so it can reach the client after the answer to a later request; the client keeps the highest revision it has seen.objectnames a performer — a part, item or occurrence definition or usage; anything else is refused withInvalidParams— so a machine or action runs with that instance's attributes: when the instantiated object already exhibits or performs the target, the session attaches to that running behavior (ExhibitedStatesOf/PerformedActionsOf, as%statedoes) rather than starting a second executor beside it, and an object running it under several usages is refused as ambiguous; otherwise a fresh executor is made viaCreateStateExecutorFor/CreateActionExecutorFor.Semantics follow the REPL:
stepis one dispatch (change events, then the next queued event, then a do-activity round) for a machine and oneStep()for an action;continueruns until the run waits, completes, fails or hits a breakpoint, holding the clock (a machine whose only pending event is due later iswaitinguntiladvance);advancemoves the runtime clock throughContext.AdvanceUntil, stopping at the instant a breakpoint is reached;sendparses the arguments as expressions and refuses a signal no active state accepts. Breakpoints are set by render ID and mapped to graph nodes by identity, so unnamed or duplicate-named action nodes work too; state breakpoints live in the runtime (StateExecutor.SetBreakpointAt), pausing as the dispatch entering the state completes so a state left again at the same instant is paused on too.Sessions run over a
model.Workspace.NewRuntime()— a runtime on its own index over the documents as they stood, so the workspace's later reindexing does not move under a live executor. Closing a document ends the sessions started on it outright, whether or not the workspace keeps the file's text from disk. After each document open/change, watched-file or workspace-folder event, sessions are reconciled against every declaration the run reads.model.Runtime.Dependencieswalks the references reachable from the target, the performer and anything asendlater named (resolve.RunReferences, which also counts a bareaccept Haltas a read of the signal) and records each declaration by(Doc, FQN)with its source text, trailing trivia trimmed; the closure covers the definitions typing or specializing a machine or its states, the actions an action invokes or specializes, accepted signals and their schemas, feature types, and whatever a guard, effect or argument names. Reconciliation recomputes the closure from the current workspace (Reading.Dependencies) and compares (dependencyChange): a session whose declarations are all unchanged is re-rendered and remapped to the fresh IDs, a pause at a breakpoint included (announced bydebugChanged); one where a read declaration was edited or is no longer declared, no longer read, or where the run would now read a declaration it did not (a nearer definition now shadows the specialized one, so the name resolves elsewhere), or whose declared view was rewritten even while still drawing the target, is ended and released. Library declarations are not watched, since workspace edits cannot reach them. A pseudo-view has no declaration to compare, so a session on one ends only for the other reasons. A machine'squeuelists every message pending on the bus, accepted by an active state or not.A session's rendering and runtime come from one reading of the workspace:
Workspace.Readruns a callback under a single read lock over amodel.Reading(RenderView,NewRuntime,Declared,DeclaredView,DeclarationText), and reconciliation compares and re-renders within one reading too. The workspace keeps agenerationbumped on every invalidation and the runtime records the one it was built at; every workspace mutation the server makes (didOpen,didChange,didClose, watched files, workspace folders, the initial folder walk) is applied and the sessions rebound under the session-store lock debug requests hold (debugEdit), with diagnostics and thedebugChangednotifications following the unlock, so a request in flight completes at the documents it began with and the next finds the session already moved; registration of a new session happens under that lock too and reconciles the session first when the generation moved since its reading — so an edit that lands while a start is in flight either answers the start under the fresh IDs or refuses it when it rewrote the behavior, and never leaves a session mapped to stale IDs unannounced.To make the runtime→render join possible:
runtime:StateExecutor.FiredTransitions()/FiredCount()log every transition taken (compound segments, fork/join branches, entry transitions — an entry transition carries theOwnerbody it is written in, so one into a nested state is drawn from the start marker of that body, not of the state's parent);ActionExecutor.Traversals()/TraversalCount()log every succession, with all token movement funnelled throughmove;Token.Within()names the nested flows a token runs in;ActionExecutor.ReplaceBreakpointsAt([]NodeBreakpoint{Within, Node})sets breakpoints by identity on one occurrence of a node in a nested flow, keeping the stop already made at a breakpoint kept so a resumed run passes it;ActionExecutor.Resumereturns a run a breakpoint suspended to running, foradvance;EventQueue.Events()lists pending events in dispatch order. Both logs are captured/restored with runtime snapshots so a rolled-back speculative step leaves no trace.view:LocateStates/LocateActionsmap lowered vertices and edges onto a rendering by declaration-relative position: kind + the declaring document + span offset within the innermost enclosing anchor of that document (the drawn declaration or one it inherits from,Node.Inherited) + ordinal among same-span siblings. A graph lowered from earlier text thus still locates after unrelated declarations moved every absolute offset, in the target's document or in the document of a general it inherits states, regions, transitions, nodes or successions from.Origin.Docof inherited content is the document it is written in, which the lowered graphs now supply (StateGraph.DocOf/ActionGraph.DocOf, from the scope of the general's body);LocateActionstakes the lowered graph.model:Workspace.Read/Reading,Workspace.NewRuntime,Runtime.Declared/Named/Lookup/FQN/Dependencies/DeclarationOf/Generation—Namedresolvestargetandobjectin the view's document first, then in the one other workspace document declaring the qualified name (a view usually exposes a behavior another file declares), refusing a name two documents declare; the session records the declaring documents and rebinds through them, ending when any is closed,Reading.Dependencies,Workspace.Declared/DeclarationText/TextAt/DeclaredView/Generation.lower:StateGraph.EntryOwnernames the body keying a state's entry transitions.resolve:RunReferences/ExpressionReferencescollect the references a run resolves, bare signal triggers included.runtime: a failed fork or compound route leaves none of its uncompleted segments in the fired log (unfireOnError); breakpoints may be set on pseudostates (SetBreakpointAt,PausedAt), pausing as the route passes through them.model: a runtime built over a caller-supplied index keeps that index's documents the workspace does not manage.runtime:ActionExecutor.StepToBreakpointisStephonoring breakpoints the wayRunToCompletiondoes — a token stepping onto a breakpoint node suspends the run there, one already on it pauses without moving, the next step resumes past it;opensysml/debug/stepand the REPL's%stepuse it.How it was verified
internal/lsp/debug_test.go(new): JSON round-trip of every request/snapshot shape; typedInvalidParamsrefusals (undrawn behavior, wrong-kind target, a performer that is no object, unknown session/node/signal, negative duration); a state-machine session over a fixture with orthogonal regions, a composite state, a timed transition, a guarded signal transition and a final state — initial configuration, step,sendwith expression args,advance,continueto completion, breakpoint pause and resume,takenper request; an action session over a fork/join with a nested own-flow, timed and signal waits, a decision and a final — every token and its awaited edges, traversal ordering, breakpoint by render ID on an unnamed node, failure and budget exhaustion; sessions surviving an unrelated edit (remapped IDs,debugChanged, a pause moved to the fresh ID, consecutiverevisions across answers and notifications), ending on an edit to the target/performer/view, and being released on stop and shutdown; staticopensysml/renderunaffected.internal/core/view/locate_test.go(new): vertex/transition and node/edge mapping, entry transitions, nested flows, undrawn-node fallback, stale-symbol rejection, and shifted-offset resilience for both locators.locate_inherited_test.go(new): a state machine specializing one in another document and an action specializing one declared earlier in the same document both keep locating inherited and own nodes, transitions and successions after an unrelated declaration is inserted ahead of the general.internal/core/runtime/debug_api_test.go,internal/lsp/debug_test.go,internal/repl/runtime_commands_test.go: a stepped action pauses on a breakpoint node before executing it and resumes on the next step, through the executor,opensysml/debug/stepand%step.internal/lsp/debug_test.goalso coverscontinueholding the clock on a machine with an event due later (statewaiting, clock unchanged,advancethen firing it), anadvancepausing on a breakpoint state left again at the same instant with the clock held there, a session on an object exhibiting the target attaching to its machine (its timed effect happens once; an object exhibiting it twice is refused),didCloseending a session when the document's text remains on disk, a session ending when a specialized or typing definition is edited, removed or shadowed (state and action, including a definition typing a nested state) while unrelated edits still relocate, a rewritten view that still draws the target ending the session while a pseudo-view session outlives edits beside its target, and a machine's queue reporting a pending message no active state accepts; an edit landing between a session's preparation and its registration (relocated to the new IDs, or refused when it rewrote the machine by a same-length edit); a machine-level entry transition into a nested state reported from the machine's start marker.internal/lsp/debug_test.gofurther covers the dependency lifecycle: a session ends when a performer supertype's default, an invoked action's body, a signal's schema or a feature's type is edited, when a read declaration is removed or a nearer one shadows it, and when asendnames a signal the run had not read whose declaration then changes, while an edit beside those declarations only relocates; a pseudostate breakpoint pauses and clears on resume.internal/lsp/debug_test.go,internal/core/runtime/debug_api_test.go: setting the same breakpoints again while paused keeps the pause and the next step resumes past it, removing and re-setting one stops the held token again,advancefrom a pause resumes the action and reaches the due work, and a breakpoint on one drawing of a declaration that two nested flows run stops only the token in that flow.internal/core/model/render_test.go: a transition inherited from a definition in another document is labelled with its trigger and guard as written there (the workspace renderer reads every workspace document, not the viewed one alone).internal/core/runtime/debug_api_test.go: a compound transition through a choice logs the segments into the choice as well as the branch taken, and a firing that fails past the choice logs none of them.internal/core/resolve/references_test.go:Referencesleaves a bareaccept Haltto the event resolver whileRunReferencesreports it.internal/core/runtime/debug_api_test.go: a failed fork and a failed route leave the fired log as it was; a pseudostate breakpoint pauses after the route through it.internal/core/model/runtime_test.go(new): a runtime keeps a definition that exists only in the caller's index;Namedprefers the document's own declaration, finds a unique one in another workspace document, ignores a library's, and refuses an ambiguous name naming both documents.internal/lsp/debug_test.go: a session on a view, machine and performer split across three documents survives an unrelated edit to the machine's document, ends when the machine is rewritten or its document closed, and a duplicate declaration is refused withInvalidParams.internal/core/model/reading_test.go(new): a reading's rendering, runtime, dependencies and view agree, with no trailing trivia in dependency text; an equal-length edit moves the generation while an earlier runtime keeps its own.internal/core/runtime/debug_api_test.go: fired-transition and traversal ordering, fork/join segments, rollback leaving no trace, identity breakpoints,Token.Within, a state breakpoint pausing on a transient state underAdvance,ClearBreakpoints.developmerged in and the corpus gates required:gofmt -l .clean,go build ./...,go vet ./...,go test ./...,make lint(staticcheck + gosec),scripts/changelog.py check,scripts/check-doc-ids.py.Not in this PR: any VS Code / webview change. The panel still renders statically; the follow-up wires Step/Continue/Send/Advance/Stop, token animation and click-to-set breakpoints onto these requests.
Checklist
make testandmake lintpass locallydocs/reference/lsp.md)changes/unreleased/lsp-debug-sessions.added.mdmake docs-countsrun if a gate count moved — the generated test-suite figures are regeneratedLink to Devin session: https://nasa-jpl-demo.devinenterprise.com/sessions/e0936f79d51248529de87f141b7807e3
Open in Devin Desktop: https://nasa-jpl-demo.devinenterprise.com/desktop/session/e0936f79d51248529de87f141b7807e3?variant=devin
Requested by: @HuiJun