feat(pssm): return call results, drive tester traces in order and read standalone machines - #486
devin-ai-integration[bot] wants to merge 10 commits into
Conversation
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
StateExecutor.Call queues the call event, runs the machine to completion and releases the caller with the outputs the triggered behaviors returned, by parameter name; a call left queued or deferred reports ErrCallNotReturned. Outputs of entry, exit and effect actions performed for the firing event reach the machine through the performance frames' return path. WriteAttribute lets a driver write a declared attribute between steps. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ndalone machines as targets The driver performs the tester's stimulation step by step: a synchronous call returns the operation's outputs once its run-to-completion step is done, and a tester trace of a returned value is appended to the machine's log there, before the next stimulus. The suite's library behaviors the traces apply (Concat, ToString, formatParameterValue) are read from the model's library references and evaluated generically. Stimulation reading moves out of the emitter. A standalone state machine is already read as the target itself, so the classifier no longer refuses it as a construct; Standalone 001/002 keep their entry and exit point reasons. Event 019 A moves from not-expressible to pass. Co-Authored-By: jason.han <hanhuijun@gmail.com>
… machines in the referee records Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ster reads A tester trace that reads two output pins of one call action (Event 019 E's result and return of or(true, true)) is one call of the machine; the tracer memoises each call action's outputs for the trace it evaluates. Co-Authored-By: jason.han <hanhuijun@gmail.com>
9e6aa5e to
e085f02
Compare
…e declared operation's outputs Call stops the run once the call event has left the queue, so a completion event the step queued or a timer it armed waits for the machine's next run instead of running (or failing) under the caller. The outputs released are the out and inout parameters the operation declares as a member of the machine's owner when it declares one; a trigger naming no declared operation still releases every output the step returned. Co-Authored-By: jason.han <hanhuijun@gmail.com>
… to be recalled A call a state defers has not been dispatched, so the run continues through the machine's later steps until the deferred event is recalled and taken; only then is the caller released with the step's outputs. A run that ends with the call still deferred keeps reporting ErrCallNotReturned. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…he caller passed it Co-Authored-By: jason.han <hanhuijun@gmail.com>
…all-operation alignment row Co-Authored-By: jason.han <hanhuijun@gmail.com>
… arguments select A call of an operation the owner declares under several same-named members read the first action member's parameters, so a call the arguments routed to another overload returned that other declaration's outputs under the wrong names. The pending call now resolves the declaration through the selection InvokeOperation makes (memberCalled), and a call the arguments cannot tell apart is refused as ErrAmbiguousInvocation before it is queued. Co-Authored-By: jason.han <hanhuijun@gmail.com>
| member, err := e.ctx.memberCalled(owner, e.self, operation, OperationArguments{Named: args}) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if !isActionSymbol(member) { | ||
| return call, nil | ||
| } |
There was a problem hiding this comment.
🟡 Declared call inputs are not bound
Call accepts wrong-typed values and omits defaults for a declared operation before dispatch. operationInputs checks names only, while InvokeOperation validates types and initializes defaults. Invalid calls can fire; valid defaulted calls fail when bindTriggerArguments finds no value.
Learn more
A synchronous call first selects the owner’s operation declaration, then queues a Call payload for the state machine. The selected declaration’s input contract includes type conformance and default values. Here, operationInputs only maps supplied values to parameter names and checks required or unknown names. Normal action invocation later validates those values and initializes missing defaults through input binding, but this state-machine path never executes that declaration. The original args map is then queued, so bindTriggerArguments cannot bind an omitted defaulted parameter.
Example: Suppose compute declares in x : Integer = 3. Call("compute", {"x": "bad"}) can fire a transition that ignores x, despite the type mismatch. Call("compute", nil) passes this check, but accept compute(x) fails because the queued event contains no x; it must bind x = 3.
Recommended fix: Build a complete, validated input map for the selected member before queuing the event. Reuse the runtime’s existing parameter type checks and default evaluation rather than duplicating signature logic. Queue that normalized map and use it when initializing unchanged inout results.
Was this helpful? React with 👍 or 👎 to provide feedback.
| // pendingCall is the synchronous Call the machine is running, if any. | ||
| pendingCall *pendingCall |
There was a problem hiding this comment.
🟡 Snapshots drop pending calls
Call stores caller state only in pendingCall. Snapshot and held-image captures omit it, so restored executions lose synchronous-call results.
Learn more
A pending call contains its event identity, result accumulator, declared outputs, inout defaults, and taken status. stateCapture and imagedState in held behavior images capture the surrounding executor but not this field. Restoring either representation therefore produces an executor whose call event may still exist while no caller state tracks it.
Example: A deferred synchronous call is captured while waiting. After restore, the event can be recalled and dispatched, but recordCallOutput sees no pendingCall; the original caller cannot receive the output.
Recommended fix: Add a deep-copied pending-call representation to both snapshot and held-image capture/restore paths. Preserve the event ID and clone all maps so branches cannot share mutable result state.
Was this helpful? React with 👍 or 👎 to provide feedback.
…ed inputs before it is queued Co-Authored-By: jason.han <hanhuijun@gmail.com>
| if !isActionSymbol(member) { | ||
| return call, nil |
There was a problem hiding this comment.
🟡 Calc calls return unrelated outputs
For a matching calc, Call leaves returns nil after validating inputs. Nil admits every step output instead of the calc’s declared result. Callers can receive helper outputs that InvokeOperation omits.
Learn more
memberCalled can select calc members because operation invocation supports actions, calcs, and constraints. A nil pendingCall.returns means the machine has no declaration and every recorded output is returned. This branch uses that same nil state for a declared calc, although InvokeOperationWith returns only the calc’s declared result. Any action performed by the transition can therefore leak its other outputs into the call result map.
Example: A calc named compute declares only result. Its call-triggered transition performs a helper producing result and log. Call("compute", ...) returns both keys, while invoking the calc directly returns only result.
Recommended fix: Populate pendingCall.returns from the selected calc’s output shape before returning from newPendingCall. Apply the same declared-result contract to every member kind accepted by memberCalled, including the synthetic result returned for constraints, while preserving nil only for genuinely undeclared calls.
Was this helpful? React with 👍 or 👎 to provide feedback.
What and why
The PSSM referee refused four constructs not because SysML v2 has no spelling for them but because the runtime, the driver or the reader did not carry them: a call trigger whose operation returns a value the tester observes, the tester's own
trace(...)after a call returns, a standaloneStateMachinethat is itself the class under test, and entry/exit/do behaviors with parameters bound from the triggering event's data. This PR closes the runtime, driver and reader side of that class and lifts the classifier only where the current translation already reaches the suite's traces; it depends on no new emitter spelling.Runtime — call results (alignment row A14).
StateExecutor.Call(operation, args)queues the call event, runs the machine to completion and returns the operation's outputs by name: whatever the behaviors that step fired — the transition's effect, an entry or an exit —returned or assigned to the operation'sout/result parameters. A nested action'sreturnor output assignment reaches the enclosing behavior's parameter of that name (performanceOwner.returnAround,assignEnclosingBy; calc and state hosts route the same way). The result set is theout/inoutparameters the operation declares as a member of the machine's owner (every output the step returned when it declares none), aninoutno behavior wrote going back as passed; a call a state defers holds its caller until it is recalled, one the run leaves queued or deferred isErrCallNotReturned, and a call no transition accepts is discarded, as PSSM does. The change is confined to the call/perform path (perform.go,state_executor.go,action_frame.go,action_executor.go,calc_statements.go,state_statements.go); the do-step scheduling site is untouched.Driver — tester order.
run.go:drivereplaces the batchedPerformStatewith the tester's own step order: a send is queued (the tester does not wait for a signal; the pool is FIFO, so every send before a call is dispatched before the call event), a call isStateExecutor.Call, and atrace(...)embedding a call is evaluated when that call returns and appended to the target'slogbefore the next stimulus — the point PSSM §8.5.9 resumes a synchronous caller. Stimulation reading moved out ofemit.gointostimulation.go(names and typed literals preserved; repeated target calls generic). The suite's test library (Concat,ToString,formatParameterValue) is read into the model (activity.go,model.go) and evaluated from a metadata registry (library.go) — no per-test strings.Reader — standalone machines.
reader.goreads auml:StateMachinewith no owning class as the target itself (attributes, operations, methods, constructor/factory behavior,Machine.Owner == ""), soConstructStandaloneis no longer a blanket refusal.Classifier.
ConstructTesterTraceis lifted where the ordered driver reaches it.ConstructBehaviorParameterandConstructOperationResultstay refused with their reasons unchanged: the runtime now transports a result, but the current translation spells the producing effect/entry/exit as parameterless, and no faithful emitter-independent spelling of a behavior parameter bound from the accepted payload exists (a v2acceptbinds on the transition;entryAction/exitAction/doActionhave no reference to the triggering transfer). Both readings, the candidate spellings and what each has to reach are recorded in the alignment note.Referee movement, adjudicated in
docs/project/pssm-referee.md:not-expressible → pass, reaching its one admitted trace (source exit, call transition's effect,End, next state's entry).not-expressibleonoperation resultand/orbehavior parameter.not-expressibleon their entry/exit-point (and, for 002, behavior-parameter) reasons, byte-identical.TestSuiteNoTranslationReasonspins the exact reasons.Of the eight tests predicted to move if all four constructs translated, one moved; the remaining seven need the emitter to spell behavior parameters and returning effects, which a follow-up PR stacked on this one carries (or records as a refusal).
Specification basis
CallEventOccurrence— the synchronous caller is released with the operation's outputs after the run-to-completion step the call event triggers; §8.5.10 run-to-completion; Clause 9.2 test-suite tester/target structure.StatePerformances.kerml,TransitionPerformances.kerml— readings for each construct indocs/internals/design/precise-semantics-alignment.md(rows for behavior parameters, operation results, tester traces and standalone machines; A14 updated).docs/project/spec-compliance.md: synchronous call-result transport, ✅ faithful.How it was verified
New tests: conformance case
state_call_trigger_results(.sysml,.expected.json, trace golden),robustness_call_results_test.go(TestRuntimeRobustnessCallResults: held, queued, untaken, empty, repeated and erroring calls),TestDriveTesterTraceAfterCallReturns,TestDriveRefusesATraceWhileTheMachineMayRun,TestDriveCallNotReturnedFails,standalone_test.go(synthetic standalone XMI read, classified, emitted and run),TestSuiteNoTranslationReasons,TestClassifyRedefinedMachine.Gates run locally, all green:
Checked by hand:
-checkwith the suite absent reports and exits 0; with a tampered suite it fails on the checksum before parsing; with a baseline whosepasscount is edited it reports the movement and asks for adjudication;-filter "Deferred 006"still reportsdiffers-by-design.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