feat(migrate): write v1 interactions, pseudostates, receptions and ported calls executably - #385
devin-ai-integration[bot] wants to merge 50 commits into
Conversation
…e events executably Co-Authored-By: jason.han <hanhuijun@gmail.com>
…and connection points Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ecutable scenario steps Interactions resolve lifelines through the context block's part tree, write synchronous and asynchronous operation calls as typed performs of the part's operation usage with positional and named arguments, replies as assignments of the call's results, and alt/opt/loop/par fragments as if, for and fork structures; create and delete messages and unresolvable lifelines are reported. A test-case interaction becomes a verification def running on its subject. The runtime's nested usage performs the action its reference subsetting names before the one its typing names, so a typed perform runs as the part. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ration calls over ports A reception whose method is an activity becomes an action def that accepts the signal and runs the method with the signal's attributes bound to its same-named parameters; the resolver now finds accept payloads from nested action bodies so those bindings resolve. A CallOperationAction over the caller's port performs the operation's usage on the part its connector reaches; over the target's own port it performs it on the target. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…it records Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nd cover deep history Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ldens under testdata Co-Authored-By: jason.han <hanhuijun@gmail.com>
…r it names Co-Authored-By: jason.han <hanhuijun@gmail.com>
…and the report's skipped counts Co-Authored-By: jason.han <hanhuijun@gmail.com>
…delegated and untyped ports A via path rooted at a feature bound to another object leaves that object's port; a part's ports are known to inherited connectors under the name they were declared with before redefinition; reference usages hold what is bound to them rather than an object of their own; and an untyped port materializes as a Ports::Port so binding connectors can join it. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nd route arrivals over connectors A state whose entry or do behavior takes parameters that the incoming signal's properties match gets a state-owned carrier item the transitions assign and the behavior binds to; incompatible or unavailable cases are refused with the reason. Signal arrivals are indexed over the document's connectors and delegations so triggers accept via each port the signal reaches, calls resolve their receiver through the activity's context, and activities whose required inputs no flow values are reported instead of written starved. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…inputs and via routing through bound references Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ive, route data through control nodes and wait on values conservatively Co-Authored-By: jason.han <hanhuijun@gmail.com>
…l nodes and loop-aware value waits Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nteraction-fidelity
…itten A ValueSpecificationAction whose literal is no value of its result's type is a placeholder, so the pass finding nodes that produce no value now counts it too: a call requiring that value stands in for itself instead of being written with an unbound input. A value action's value specification is no longer declared as an input pin of the placeholder. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…from Co-Authored-By: jason.han <hanhuijun@gmail.com>
🤖 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:
|
…pes in scenarios A call message leaving an in parameter with lower bound 0 and no default unbound was refused as unbound; requiresValue now decides, as it does for activities. partPaths tracked visited types globally, so a second sibling part of a type already walked was never entered and a lifeline standing for a part below it resolved to the first sibling alone; the type guard is now per path, so both paths are found and the lifeline is reported as ambiguous. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nteraction-fidelity
…ar names and via bindings distinct A signal send or interaction message that binds no argument to a signal attribute with no default and a lower bound above zero, inherited attributes included, is refused with the attribute named instead of written executably. Parallel fragments reserve their join and operand names through the scenario's used-name set so a source message named like a generated name does not collide. A via path rooted at a name the behavior binds routes through that binding even when the performer owns a feature of the same name, as any expression's lookup does. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nd name only populated regions carrierMatch and the carrier's attribute list now come from signalAttributes, so a state's entry and do parameters bind attributes the signal inherits from its generals rather than being refused or left unbound. nameMachine names the same populatedRegions the writer emits, so an empty sibling region no longer produces a parallel wrapper name and cross-level transition paths through a state that is never written. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…rlier message A duration constraint between two messages with steps between them was written as the whole interval before the later step, so the steps between added to it. The wait is now forked after the earlier step and joined before the later one, and its bound drawn once there; a constraint whose ends lie in different fragments is reported instead of written. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nto feature/v1-state-interaction-fidelity Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # internal/translate/migrate/migrate.go
The runtime keeps a message delivered to a port apart from one addressed to the object, so a reception written as a single direct accept never ran its method for a signal sent over a connector. The reception's action def now forks after start into the accept from the object and one accept via each port the signal arrives at, each running the method and accepting again; ports nothing declares or sends the signal to are named in the report. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…arameter's type and multiplicity Co-Authored-By: jason.han <hanhuijun@gmail.com>
…on's action def A reception whose method is also the method of an operation of the block now performs that operation's action def, the one form the method is written in, binding the parameters it declares; the hidden activity is no longer referenced. An operation call binds only its in and inout parameters, stated explicitly rather than as everything not out. Co-Authored-By: jason.han <hanhuijun@gmail.com>
… a cycle together contextOf marked an activity as having no context while it was still being resolved, so a callee reached back through a cycle of calls read the caller as context-free and either settled on its owner or got no parameter, leaving the caller's ports unbound. Activities are now visited as a graph of calls: a cycle settles at once with the union of the ports its members name, and the report says why none is written. The migration reference now describes the context parameter. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nto feature/v1-state-interaction-fidelity
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…r referred state behaviors A send passing a pin of a type a required signal attribute cannot take is refused like one passing no pin, through one type test shared with the emitted argument list, instead of writing a send without the attribute. A state's entry or do behavior it refers to rather than owns takes part in carrier discovery and receives the accepted signal's attributes as in bindings of its action usage beside its context binding; where a required parameter has no such source the behavior is not run and the state says why. Co-Authored-By: jason.han <hanhuijun@gmail.com>
A call message's argument for an operation's inout parameter, a referenced entry/do behavior's inout parameter and a reception method's inout parameter were all written as in bindings, so the runtime never wrote the callee's value back. Bindings now carry the parameter's direction; in parameters are unchanged. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ided duration intervals An unnamed message argument now takes the next parameter or attribute no named argument claims, so `mixed(tag = 7, 4, 5)` binds 4 to the first unnamed target instead of the argument's own position. A duration interval lacking a usable bound (no max, a max of `*`, an unwritable bound) is reported unmapped with the missing side: no single wait stands for an interval open on one side. A min beside an expressionless max in a MagicDraw document keeps its fixed wait, as that is the tool's encoding of a constraint written with one value. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…rries no value calleeOutput returned nothing both for a pin of no call and for a call's pin that stands for no out parameter, so producesAt took the latter for an ordinary producer: a consumer whose required input only such a pin fed was written as the typed call with the input unbound. The callee is now returned alone in that case, producesAt treats it as no value, and the flow from it is kept as a comment naming the reason, like one from a parameter the callee never values. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nto feature/v1-state-interaction-fidelity Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # internal/translate/migrate/activity.go # internal/translate/migrate/states.go
…nto feature/v1-state-interaction-fidelity
There was a problem hiding this comment.
Note
This report is out of date. Scroll down for Devin Review's latest report on this PR.
Devin Review found 1 new potential issue.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| if viaSelf || len(segments) < 2 { | ||
| return self, path, nil |
There was a problem hiding this comment.
🟡 Bound port references misroute messages
A single-segment bound port in viaHolder bypasses its binding and stays rooted at the performer. Messages then fail or use a same-named performer port.
Learn more
A via target can itself be a reference whose value is a port object. Multi-segment paths such as context.command work because viaHolder evaluates context, but a single-segment path such as via command returns early before evaluating command. Routing then asks the performer for a feature named command, even when the behavior parameter holds another object's port.
Example: An action declares in ref port command : CommandPort; and sends via command. The caller binds command to device.command. The send routes from the action performer instead of the bound port, so it either reports an unknown port or uses an unrelated performer port named command.
Recommended fix: Resolve single-segment paths through EvalContext.Lookup when the binding holds a port instance. Preserve the current performer-rooted behavior for ordinary unbound port names and for paths explicitly rooted at this.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Confirmed, and deferred to a follow-up change. A single-segment via command whose root is the action's own in ref port parameter bound to another object's port is routed at the performer: with a same-named port there the send uses that port (port "command" is joined to no port that can receive it when it is unconnected), and without one it is an unknown port. This behavior predates this change — develop routes single-segment via targets the same way — and viaHolder here only adds the re-rooting of multi-segment paths (via ctx.p), which is the form the migrator writes; it never emits a single-segment via over a bound reference, so migrated models are not affected. Resolving a bound single-segment root that holds a port object to that port's owner and name, with a conformance fixture, will land separately rather than moving this PR's head.
A `send … via dev.p` whose `dev` the behavior binds to another object leaves that object's port, but the behavior's own `connect dev.p to snk.local` kept its ends as written and was matched against the re-rooted port, so it never carried the send. postVia now routes the behavior's connections under the written path, resolving ends rooted at a binding through the objects it holds, and the port holder's connections under the re-rooted one; both deliver. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ject its via path is re-rooted to An addressed send over a via path starting at a bound reference, `send Ping() via ctx.tx to listener`, was refused before the path was re-rooted: the receiver validation read the written path against the performer, where `ctx.tx` is no port of its own. postVia now re-roots first, checks the port on the object holding it and resolves the receiver among that object's machines; diagnostics keep the written path. The behavior's own connectors still route under the written path, the holder's under the re-rooted one. Co-Authored-By: jason.han <hanhuijun@gmail.com>
There was a problem hiding this comment.
Devin Review found 1 new potential issue.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| t := firstOwned(n, "target") | ||
| ins := slices.DeleteFunc(inputPins(n), func(p *sysmlv1.Element) bool { return p == t }) | ||
| if p, why := a.unarguedParameter(ins, op); p != nil { | ||
| return why, Approximated, true |
There was a problem hiding this comment.
🟡 Required method inputs stay unbound
When a method adds a required input, unarguedParameter checks only the operation signature. The generated call leaves that declared input unbound.
Learn more
An operation action definition contains the operation's parameters plus unmatched parameters from its local method. operationBody declares both groups, but call validation and pin mapping inspect only op.Owned("ownedParameter"). A required method-only input therefore receives no pin or binding, despite the migrator's rule that typed performs cannot leave required inputs unbound. Interaction calls have the same mismatch because they also bind only the operation signature.
Example: Operation Tilt(in amount) has method Tilting(out was, in extra). Migration declares action def Tilt { in amount; out was; in extra; }, but a call supplying only amount is emitted as executable and leaves required extra unset.
Recommended fix: Validate and map calls against actionParameters(op), preserving the exact parameter order and directions emitted by operationBody. If UML call pins cannot supply a required method-only input, emit the existing placeholder/refusal instead. Apply the same effective signature to activity calls, operation calls, output mapping, and interaction calls.
Was this helpful? React with 👍 or 👎 to provide feedback.
What and why
The SysML v1 migrator wrote state machines and signal-only interactions, and left the rest of a v1 behavior model as comments or report entries: call and reply messages, lifelines that are not a direct part, combined fragments, transitions across regions, junction/choice/fork/join/history pseudostates, entry/exit points of submachine states, internal transitions, absolute time events, receptions, and operation calls over ports. This change writes each of those in the v2 form the runtime executes, or refuses it with the specific reason, so a migrated model's scenarios and machines run under
-action/-state/-analysison the migrated system individual.internal/translate/migrate:interaction.go,context.go): asynchCall/asynchCallmessage isperform action x : Op ::> <lifeline path>.op { in p = arg; }with arguments bound to the operation'sinparameters by name or position; areplyassigns the call's result to the caller lifeline's attribute; create/delete messages and message-less timing traces are refused with the reason. A lifeline'srepresentsis resolved through the context block's parts, ports, references andinparameters; a lifeline that does not resolve makes the scenario a report entry.alt/opt/loop/parfragments becomeif/while/for/forkwhen their guards parse and resolve, otherwise the fragment is refused with the guard text.states.go,events.go): transitions whose source and target lie in different regions or nesting levels are written by path;junction,choice,fork,join,historyanddeep historypseudostates are written executably; entry/exit points of a submachine are addressed as states of itsstate def; internal transitions are self transitions; a referenced absoluteTimeEventisaccept at <instant>over aTime::TimeInstantValueattribute whenwhenis a literal or resolves; an event no trigger names is skipped as a model element nothing refers to, counted apart from profile/library content in the header, rather than reported as unmapped. A state entered only by transitions carrying one signal values its entry/do parameters from that signal (carriers.go).call_port.go,arrivals.go): a Reception with a method is anaction defthe block performs from creation, accepting the signal — from the object and via each port the document's connectors or declarations bring it to — and running the method with the signal's attributes bound to its parameters, then accepting again; aCallOperationActionover a port performs the operation on the part a connector of the caller's block joins to that port, resolved the way connector paths resolve; a trigger naming no port also accepts via each port the document's connectors carry its signal to.arguments.go,datawait.go): a call or send that v1 fires without a value for a parameter or attribute that must hold one — no argument, or a pin fed only by flows no value travels (a parameter nothing values, an unmigrated opaque or value specification action, or a callee whose own activity gives thatoutparameter no value, judged through nesting) — is written as an empty action carrying the token with the reason in its comment and report line, and the flow is kept as a comment; v2 admits no typed perform with an input unbound. Control and buffer nodes only object flows lead to route their values from source to pin; a control node no edge leaves ends the token; an action fed from outside its control path waits on the value only when the producer runs on every pass of the surrounding loop.Runtime and resolution, where the migrated forms needed them (
internal/exec/runtime,internal/ir/lower,internal/semantic/resolve):perform action x : Def ::> part.action { … }runs the part's action with the part as performer and binds the callee's inputs from the body; an accept payload is visible by simple name to other nodes of the same body, so a typed nested action can read the accepted message.viasends and accepts route through bound references (via ctx.p), delegated ports, ports of a redefined part under the name its connectors use, and untyped ports (materialized asPorts::Port) so a binding connector joins them and an inward send reaches the bound part's machine; arefusage holds what is bound to it rather than an object of its own.Documentation:
docs/reference/sysml-v1-migration.md(mapping rows, "Behaviors", "Interactions", "Receptions and operation calls", "Values that never arrive", "Control nodes carrying data", "The report"),docs/project/spec-compliance.mdrows for the resolution and runtime rules, changelog fragmentchanges/unreleased/v1-state-interaction-fidelity.added.md.Specification basis
SysML v2 1.0 / KerML 1.0: action usages with a feature chain (SysML 7.16.2, 7.16.4⚠️ approximate: the payload is contributed to the scope the accept node is declared in rather than made a member of the body).
PerformActionUsage), accept action payload scoping (KerML 8.2.3.5.3), state transitions andaccept at(SysML 7.17.4), interfaces and connector ends over ports (SysML 7.13.5, 7.14.4). PSSM (OMG formal/19-05-01) for pseudostate and cross-region transition semantics. Adds three rows todocs/project/spec-compliance.md:viarouting over bound references and port kinds (✅), typed perform on a chained part (✅, self-assessed), and accept payload name scoping (How it was verified
Gates:
gofmt -l .(empty),go vet ./...,make lint,go build ./...,go test ./...;make docs-check,python3 scripts/changelog.py check,python3 scripts/check-doc-ids.py; the corpus gates with the corpora downloaded andOPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 OPENSYSML_REQUIRE_PILOT_LIBRARY_XMI=1(no baseline moved).Tests added: migrate fixtures
rig_interactions,plant_states,heater_receptions,ported_calls(XMI + golden notation + golden report undertests/migrate/testdata/xmi), and unit tests per construct (interaction_test.go,states_test.go,reception_test.go,call_port_test.go,context_test.go,carriers_test.go,arrivals_test.go,arguments_test.go,datawait_test.go,sinks_test.go), each migrating, validating with no errors and running the result; runtime conformance fixtures (perform_typed_action_on_nested_part,accept_payload_bound_into_typed_action,accept_via_bound_context_port,send_via_this_port,send_delegated_port_nested_binding,send_redefined_part_port_end,send_untyped_port_inward_binding,instance_reference_usage_not_materialized) with traces where ordering matters; robustnessTestRuntimeRobustnessPerformTypedOnPart,TestRuntimeRobustnessUntypedPort; parser goldenperform_typed_subsetting;resolve/accept_payload_test.go.Proof on a 44k-element Cameo 2024x model (TMT), migrated on the base branch
feature/v1-simulation-configs(66f257f, which carriesdevelop) and on this branch; both outputs validate with 0 errors. The header figures move with the base (its applied-stereotype rendering shifts 626Dependencyelements from mapped to approximated on both sides, and its simulation-configuration and probability notes are on both sides too); the per-metaclass rows below are the ones this change moves:feature/v1-simulation-configs)Two of the model's duration scenarios (sequence-diagram interactions that inject signals into the system individual) were run end to end on the migrated file:
-instantiatethe scenario,-statethe fifteen component state machines,-analysisthe scenario verification,-advance, under-seed 1since the components drawuniformdelays. Both complete: the executive machine steps Initializing → StandBy → its working state (do behavior running) → StandBy, and in the off-axis scenario on todone, at simulation times 360 s and 3590 s; the verification verdict is "inconclusive — the case body bound no VerdictKind value", since the source verifications bind none.Known limitations, each reported by the migration rather than dropped:
tFinaland the othert*) are not computed by the migrated model, and steps depending on their values are placeholders that keep the token.Integer[100000]) reports a multiplicity violation at-instantiateand materialization continues bounded.Integerparameter is written as the source declares it and fails the typed assignment at run time.*) is reported unmapped, since no single wait stands for it; one such constraint in the model (Acquire and Lock TTF OIWFS NS Logical Actual) is refused, while the scenarios'{60s}-style constraints — MagicDraw's min beside an expressionless max — keep their fixed waits.Take PIT Exposure,Take SH Exposure,Request Single APT Exposure) declare onlyinparameters, yet thirteen calls of them carry aresultpin; such a pin stands for no parameter and carries no value, so the flows from it are kept as comments and the ten analysis calls they alone feed (and three more fed only from those) are reported as never firing rather than written as typed calls with an input unbound. Neither duration scenario reaches them.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