diff --git a/.circleci/config.yml b/.circleci/config.yml index 065d6d1a13..1c2d1b6b7e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,7 +17,7 @@ executors: # It carries Node LTS, 24 or later, which the gate needs. - image: cimg/go:1.25-node resource_class: medium - + python-executor: docker: - image: cimg/python:3.11 @@ -1047,7 +1047,7 @@ jobs: executor: python-executor steps: - checkout - + - attach_workspace: at: . @@ -1070,7 +1070,7 @@ jobs: mkdir -p ~/.opensysml/bin cp bin/sysml-grpc ~/.opensysml/bin/ chmod +x ~/.opensysml/bin/sysml-grpc - + - run: name: Install Python client command: | @@ -1083,7 +1083,7 @@ jobs: # its wheels with python -m build. pip install --only-binary :all: jupyter_client==8.10.0 build==1.6.1 make jupyter-kernel-install - + # The integration tests connect to a service on the standard port with # auto_start=False, which is the explicit opt-in to one the client does not # manage; without it they skip, exercising nothing, so one runs here for @@ -2251,7 +2251,7 @@ jobs: - go/load-cache - go/mod-download - go/save-cache - + - run: name: Build release binaries command: | @@ -2259,58 +2259,58 @@ jobs: COMMIT=${CIRCLE_SHA1} BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') GO_VERSION=$(go version | awk '{print $3}') - + # Build for multiple platforms using make mkdir -p dist - + GOOS=linux GOARCH=amd64 make build-sysml VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml dist/sysml-linux-amd64 - + GOOS=linux GOARCH=arm64 make build-sysml VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml dist/sysml-linux-arm64 - + GOOS=darwin GOARCH=amd64 make build-sysml VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml dist/sysml-darwin-amd64 - + GOOS=darwin GOARCH=arm64 make build-sysml VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml dist/sysml-darwin-arm64 - + GOOS=windows GOARCH=amd64 make build-sysml VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml dist/sysml-windows-amd64.exe - + GOOS=linux GOARCH=amd64 make build-lsp VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml-lsp dist/sysml-lsp-linux-amd64 - + GOOS=linux GOARCH=arm64 make build-lsp VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml-lsp dist/sysml-lsp-linux-arm64 - + GOOS=darwin GOARCH=amd64 make build-lsp VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml-lsp dist/sysml-lsp-darwin-amd64 - + GOOS=darwin GOARCH=arm64 make build-lsp VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml-lsp dist/sysml-lsp-darwin-arm64 - + GOOS=windows GOARCH=amd64 make build-lsp VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml-lsp dist/sysml-lsp-windows-amd64.exe - + # The gRPC service backs the Python client, which downloads one # file per platform and verifies it against a .sha256 sidecar, so # these are published raw rather than archived. They are built into # a subdirectory to keep them out of the archive globs below. mkdir -p dist/grpc - + GOOS=linux GOARCH=amd64 make build-grpc VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml-grpc dist/grpc/sysml-grpc-linux-amd64 - + GOOS=linux GOARCH=arm64 make build-grpc VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml-grpc dist/grpc/sysml-grpc-linux-arm64 - + GOOS=darwin GOARCH=amd64 make build-grpc VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml-grpc dist/grpc/sysml-grpc-darwin-amd64 - + GOOS=darwin GOARCH=arm64 make build-grpc VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml-grpc dist/grpc/sysml-grpc-darwin-arm64 - + GOOS=windows GOARCH=amd64 make build-grpc VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/sysml-grpc dist/grpc/sysml-grpc-windows-amd64.exe @@ -2345,7 +2345,7 @@ jobs: mkdir -p dist/wasm make build-release-wasm VERSION="$VERSION" COMMIT="$COMMIT" BUILD_TIME="$BUILD_TIME" GO_VERSION="$GO_VERSION" mv bin/wasm/release/sysml-wasm.wasm bin/wasm/release/wasm_exec.js dist/wasm/ - + # Create tarballs cd dist for binary in sysml-*; do @@ -2355,7 +2355,7 @@ jobs: tar czf "${binary}.tar.gz" "$binary" fi done - + # Bundle archives: both binaries under their plain names # (sysml, sysml-lsp), which is the layout a Homebrew formula or a # PATH install expects, with their manual pages beside them under @@ -2376,7 +2376,7 @@ jobs: cp sysml-lsp-windows-amd64.exe stage/windows-amd64/sysml-lsp.exe (cd stage/windows-amd64 && zip -q ../../opensysml-windows-amd64.zip sysml.exe sysml-lsp.exe) rm -rf stage - + # The checksum manifest and the per-file sidecars are # build-release's, once the Python distributions exist to list. ls -l . grpc jupyter @@ -2457,17 +2457,17 @@ jobs: name: Assemble the release and write its checksum manifest command: | cd dist - + # The opensysml wheel and sdist, released with the binaries they drive, # and the jupyter-opensysml-kernel platform wheels and sdist likewise. cp ../client/python/dist/opensysml-*.whl ../client/python/dist/opensysml-*.tar.gz . cp ../client/jupyter-kernel/dist/jupyter_opensysml_kernel-*.whl \ ../client/jupyter-kernel/dist/jupyter_opensysml_kernel-*.tar.gz . - + # Checksums over every published archive, so a Homebrew formula # (or a careful user) can verify a download. sha256sum ./*.tar.gz ./*.zip ./*.whl | sed 's|\./||' > SHA256SUMS.txt - + # Same for the raw gRPC binaries, through the per-file sidecars # written beside them when they were built, the only checksum # opensysml reads: checked against the bytes here, then folded in. @@ -2623,7 +2623,7 @@ jobs: <(jq -r '.dsseEnvelope.payload' provenance.intoto.json.bundle | base64 -d \ | jq -r '.subject[] | "\(.name) \(.digest.sha256)"' | sort) - + - rehearsal-skip: what: "cosign keyless signing and attestation (they write to the public Rekor log)" - run: @@ -3301,7 +3301,7 @@ jobs: - rehearsal-tag - attach_workspace: at: . - + - unless: condition: << pipeline.parameters.release_rehearsal >> steps: @@ -3315,7 +3315,7 @@ jobs: fi echo "Publishing release $VERSION" ls -la dist/ - + # Move only tarballs to a release directory mkdir -p dist/release mv dist/*.tar.gz dist/*.zip dist/release/ 2>/dev/null || true @@ -3333,14 +3333,14 @@ jobs: mv dist/provenance.intoto.json dist/provenance.intoto.json.bundle dist/release/ echo "Release artifacts:" ls -la dist/release/ - + # Try different token variable names TOKEN="${GITHUB_TOKEN:-${GH_TOKEN:-${CIRCLE_TOKEN}}}" if [ -z "$TOKEN" ]; then echo "Error: No GitHub token found. Tried GITHUB_TOKEN, GH_TOKEN, CIRCLE_TOKEN" exit 1 fi - + # '-replace' reuses an existing release and only re-uploads assets of # the same name, so hand-written notes, title and prerelease/latest # flags survive a re-run. Never '-delete': that is an alias of diff --git a/changes/unreleased/case-step-order.fixed.md b/changes/unreleased/case-step-order.fixed.md index 2054f681d4..4a850591b8 100644 --- a/changes/unreleased/case-step-order.fixed.md +++ b/changes/unreleased/case-step-order.fixed.md @@ -3,4 +3,5 @@ interleaving of them instead of only declaration order, so a case such as `action s1 { assign x := x * 10; } action s2 { assign x := x + 2; }` reports both `12` and `30`. `declared` and the default `reverse` schedule still perform the steps in declaration order, so - default results do not change. + default results do not change. A case inherited as an action step keeps its case-body lowering and + declaring scope. diff --git a/changes/unreleased/inherited-action-steps.fixed.md b/changes/unreleased/inherited-action-steps.fixed.md new file mode 100644 index 0000000000..6ae75acd45 --- /dev/null +++ b/changes/unreleased/inherited-action-steps.fixed.md @@ -0,0 +1,6 @@ +- **An action definition that specializes another now performs the inherited steps, successions, flows and statements.** Previously `action def P :> B` silently ran only its own members, so `action def Plain :> Base;` completed with no results and `action def Extra :> Base { action b { ... } }` ran `b` but not `Base::a`. Inherited content is lowered into the specialization's flow (nearest general first, a diamond's shared general once), a step redefined by name replaces the inherited one and inherited successions retarget to it, and cyclic specialization, a redefinition naming a missing step, an incompatible replacement and an ambiguous retarget are reported as errors, and a specialization's own flows may connect inherited steps. +- **A specialization preserves gates on inherited succession flows.** A guarded succession targeting an inherited succession flow retains its guard and delivery, so the specialized action follows the same guarded path as its general. +- **A redefining action step with no multiplicity of its own now repeats as often as the step it redefines.** `action :>> a { … }` over `action a[3]` performs three times (it performed once), and `run`, `explore`, `check`, `validate` and the SMT refusal all use the same effective count; a redefining step also keeps the redefined step's body alongside its own. +- **An inherited assertion stays unchecked unless a succession or flow connection in its declaring body or elsewhere in the specializing action's inheritance chain names it.** A specialization therefore carries an assertion ordered by an intermediate general while leaving an unsequenced assertion unchecked. +- **A typed action usage, entry/do/exit behavior or performed action that states a body of its own now merges that body with the typed or performed action.** Its own members run alongside the action's inherited content as one flow, unordered unless a succession orders them, and its redefinitions replace inherited steps; an entry/do/exit behavior doing this is no longer reported at execution. A body that only binds pins still performs the action as before. An executable typed body nested in its own type, directly or through inheritance, is refused at execution rather than recursively unfolded; feature-only self-typed bodies remain lazy. +- **A specialization's body binding on an inherited action node now overrides the inherited binding of that pin.** diff --git a/changes/unreleased/terminate-usage-body-flow.fixed.md b/changes/unreleased/terminate-usage-body-flow.fixed.md index 4bfd1e8240..0b699ccc46 100644 --- a/changes/unreleased/terminate-usage-body-flow.fixed.md +++ b/changes/unreleased/terminate-usage-body-flow.fixed.md @@ -1 +1 @@ -- **A terminate action usage's body may state a flow.** `action stop terminate { assign code := 42; then terminate; then assign other := 7; }` now runs its `then` chain, where it was refused as stating no flow of its own. The terminate the usage stands for is unordered against its body's statements: `explore` and `-engine check` reach it falling before, between or after them, an output pin left unwritten binding no value, while `reverse` and `declared` still perform it after the body. A `terminate` naming the usage inside its own body ends the usage's performance rather than the body's. +- **A terminate action usage's body may state a flow.** `action stop terminate { assign code := 42; then terminate; then assign other := 7; }` now runs its `then` chain, where it was refused as stating no flow of its own. The terminate the usage stands for is unordered against its body's statements: `explore` and `-engine check` reach it falling before, between or after them, an output pin left unwritten binding no value, while `reverse` and `declared` still perform it after the body. A `terminate` naming the usage inside its own body ends the usage's performance rather than the body's. A terminate usage inherited into a specialized action retains its stated body flow. diff --git a/docs/project/behavior-semantic-oracle.md b/docs/project/behavior-semantic-oracle.md index 64baf5367d..df36f95939 100644 --- a/docs/project/behavior-semantic-oracle.md +++ b/docs/project/behavior-semantic-oracle.md @@ -77,6 +77,11 @@ derivation fixes is met — not whether the golden is the only correct trace. | `Performances.kerml` `Performance::enclosedPerformances`, `subperformances` | `step enclosedPerformances: Performance[0..*] subsets performances, timeEnclosedOccurrences` — "timeEnclosedOccurrences of this Performance that are also Performances"; `composite step subperformances: Performance[0..*] subsets enclosedPerformances, suboccurrences` — "enclosedPerformances that are composite" | A composite step's performances start no earlier and end no later than the performance owning them, whether or not a succession orders them | | `Occurrences.kerml` `Occurrence::timeEnclosedOccurrences` | "Occurrences that start no earlier than and end no later than this occurrence" | The owner's performance ends only after every subperformance has; its own successors follow them all | | `Actions.sysml` `Action::subactions` | `action subactions: Action[0..*] :> actions, subperformances` — "The subperformances of this Action that are Actions" | Every composite action usage of an action (a `send`, `accept`, `assign`, `if`, `while` or `for` among them) is one of its subperformances; a `ref` action usage is not composite and is not one | +| KerML 1.0 §7.3.2, §8.3.3.1.10 `Type::inheritedMembership` | `derived var feature inheritedMembership : Membership[0..*] ordered subsets membership` | A type's effective memberships include its owned memberships and those inherited from its generals; action steps, successions, flows and statements therefore carry into a specialization | +| KerML 1.0 §7.3.4.3, §8.3.3.3 `FeatureTyping` | Feature typing is a specialization | A body-stating typed usage inherits the action content of its type; its own redefinitions mask inherited members | +| SysML v2 §7.17 `PerformActionUsage` | A perform usage reference-subsets the action it performs | A body-stating perform usage merges the referenced action's members into its one lowered flow | +| `Systems Library/States.sysml` `StateAction` | `entry action entryAction :>> 'entry'; do action doAction : Action :>> 'do'; exit action exitAction : Action :>> 'exit'` | Entry, do and exit action usages are behavior members whose body-stating typed usages specialize the action they perform | +| KerML 1.0 §8.3.3.1 `Type::multiplicity` | "If there is no such ownedMember, then the cardinality of this Type is constrained by all the Multiplicity constraints applicable to any direct supertypes" | A step that declares no multiplicity takes the multiplicity of its redefined feature; subsetting alone does not transfer that multiplicity | | `Occurrences.kerml` `Occurrence::startShot` | `portion feature startShot: Occurrence[1] subsets snapshots` — "The snapshot representing the start of the occurrence in time" | A feature's initial value is bound at its performance's start shot, before any of its subperformances writes | | KerML 1.0 `FeatureValue` (`isInitial`) | An initial feature value (`:=`) gives the feature its value at the start of the featuring occurrence; a bound one (`=`) holds throughout | `attribute t : Integer := c` snapshots `c` once, when its performance starts | | `Actions.sysml` `Action::assignments`, `AssignmentAction`; `FeatureReferencingPerformances.kerml` `FeatureWritePerformance` | `abstract action assignments : AssignmentAction[0..*] :> subactions, assignmentActions`; `action def AssignmentAction :> FeatureWritePerformance, Action`; "assigns the values of a feature on an occurrence to the given replacementValues at time its performance ends" | An `assign` is a subperformance of its owner whose write happens when it ends, so it is a separate time from the owner's start shot | @@ -258,6 +263,94 @@ the owning action: two performances of an action holding such a node each perfor that a flow-owning node reached from both branches of a fork is likewise one performance, holding at each pin the one delivery the flow into that pin carried. +### A specialized action performs its inherited steps; a redefining step keeps the redefined step's members + +Fixtures: `inherited_action_steps_p0`, `_p2`, `_p3`, `_usage_typed`, +`_usage_typed_with_body`, `_multi_level`, `_diamond`, `_narrow`, `_keep`, +`_redefine_two_levels`, `_direct_assign`, `_direct_send`, `_typed_node`, `_perform`, +`_state_entry`, `_state_do`, `_state_exit`, `_redefined_typed_step`; the ordering-sensitive +cases carry trace goldens. The body-stating cases are `_usage_typed_with_body`, +`_nested_typed_usage_with_body`, `_state_entry_typed_body`, `_state_do_typed_body`, +`_state_exit_typed_body`, `_state_entry_perform_body` and +`_classifier_perform_typed_body`; state and classifier observations use explicit `inout` pins +bound to `hits`. + +Derived constraints: + +1. KerML 1.0 §7.3.2 and §8.3.3.1.10 `Type::inheritedMembership` derives a type's inherited + memberships from its generals. Redefinition is a Subsetting that is a Specialization + (§7.3.4.5), and FeatureTyping is also a Specialization, so a specialized action definition + inherits action nodes, statements, successions and flows, and performing a typed action usage + performs the action definition's body. +2. The pilot's `TypeAdapter.getInheritedMemberships` passes inherited memberships through + `removeRedefinedFeatures`: a member disappears only when an owned or nearer inherited feature + redefines it, directly or indirectly. An inherited succession therefore maps its endpoint to + the redefining node, and inherited start/done markers are the specialized flow's own markers. +3. For `Keep::a`, its own `assign c := c + 10` does not redefine `Base::a`'s unnamed assignment + `assign c := c + 1`. The pilot's `FeatureAdapter.addRedefinitions` / + `FeatureAdapter.getRelevantFeatures` add implicit redefinitions for end features, + constructor-result features and parameters by position; `ActionUsageAdapter.getRelevantFeatures` + does so for state- and transition-action members, not arbitrary assignments. Both statements + remain members of `Keep::a`, and each performance applies both. Keep owns no multiplicity, so + the redefined `Base::a[3]` governs it: `3 × (1 + 10) = 33`. The two additions are unordered + within each leaf performance, but their order does not change the result. +4. `Narrow::a[2]` owns its count, so it performs twice and inherits Base's `+1` body: + `2 × 1 = 2`. In the two-level case, `Mid::a[2]` governs `Leaf::a`, whose own `+10` statement + and inherited Base `+1` statement run twice: `2 × (10 + 1) = 22`. +5. Base's `first start then a` and `then done` constrain the specialized graph too. They target + the replacement for `a`, never a duplicate inherited node. Nearest-first generalization makes + each inherited body run once through a diamond. +6. A feature's type includes the types of features it redefines, so `Narrow2::a` inherits `Foo` + as its performed action. Its two performances update their shared `hits` feature twice; the + repeated node's frame is not reported as an output. + +The fixed outcomes are P0 `c = 1`, P2 `c = 1, z = 0`, P3 `c = 101`, typed usage `u1.c = 1`, +typed usage with its own `b` body `c = 101`, multi-level `c = 11`, diamond `c = 111`, +Narrow `c = 2`, Keep `c = 33`, two-level redefinition `c = 22`, direct assignment `c = 5`, +direct send `number = 5`, typed node `x.c = 1`, `perform` node `p.c = 1`, and one inherited +step each for state entry, do and exit. Body-stating typed state entry/do/exit each add the +inherited step and own member once (`hits = 11`); the `entry perform` form has the same outcome, +the empty-`Bump` entry body leaves `c = 1`, and the part-level typed classifier body leaves +`hits = 101`. `Narrow2` performs `a` twice as `Foo` and reports only the outer `hits = 2`. +Subsetting alone does not carry multiplicity: only the own-or- +redefined source used by `GoverningMultiplicityOf` does. + +Known refusals are cyclic specialization (`action specializes itself`), a missing redefined action +step (`redefined action step not found`), a succession reaching a non-action replacement +(`redefining feature is not an action step`), and an inherited succession mapping to multiple +replacements (`inherited succession reaches more than one redefining step`). + +### Body-stating typed usages and mixed state behaviors + +7. KerML 1.0 §7.3.4.3 / §8.3.3.3 makes FeatureTyping a Specialization, so a typed usage inherits + its type's memberships as well as stating its own. The pilot's `TypeAdapter.getInheritedMemberships` + and `removeRedefinedFeatures` preserve the typed action's steps, successions, flows and statements, + dropping only inherited members its own body redefines. Thus `action y : B1 { action b { … } }` + performs B1's ordered `a` and its own unordered `b` in one flow. This applies to top-level typed + usages and nested action nodes. A nested typed node with a real body is lowered with typing + included and marked in the graph so the runtime does not also invoke that type; a node whose body + only binds features or pins retains its invocation path. The nested and top-level fixtures each + leave `c = 101`, not `102`. +8. `Systems Library/States.sysml` `StateAction` declares the entry, do and exit behavior features. + The pilot's `ActionUsageAdapter.getRelevantFeatures` and `getRedefinedFeature` identify those + state behavior usages as redefinitions of `entryAction`, `doAction` and `exitAction`. A body-stating + typed entry/do/exit usage therefore specializes the performed action's body: its own members and + inherited ordered flow run once as a single started flow. SysML v2 §7.17 `PerformActionUsage` + gives the `perform action p : Bump { … }` form the same result because its ReferenceSubsetting + is a Subsetting, hence a Specialization. The same rule applies to a part-level classifier + `perform` body. The inherited action's step and the own action body each add once in the state + fixtures; the chosen trace order is a linearization, while the relative order of unordered + members is not fixed by the library. + In `action_invoked_node_body_writes_output`, the usage's own `assign y := y + 1` is an + unordered subaction beside Scale's inherited `start → scaling → done` flow. The inherited + `scaling` step sets `y` to 30; the assignment leaves 31 if it runs after `scaling`, while + `scaling` leaves 30 if the assignment runs first. The following `check` reads that final value, + so the oracle admits exactly `{30, 31}`. + A typed node whose own statements read the callee's outputs is order-dependent under this merge + rule: an order before the producing step reads the unset output and fails. `state_block_flow_typed_node` + therefore reads `scaled.y` after the node. `action_invoked_node_body_writes_output` and + `state_block_flow_typed_node_body_writes_output` carry the `{30, 31}` sets. + ### Repeated action steps and shared writes Fixtures: `action_step_multiplicity_exact`, `_reverse`, `_explore`, `_range`, `_named_bound`, @@ -270,8 +363,9 @@ Fixtures: `action_step_multiplicity_exact`, `_reverse`, `_explore`, `_range`, `_ Derived constraints: -- A missing multiplicity keeps the historical one performance. An action-node usage's own - `[n]`, `[n..n]`, or named exact bounds that evaluate to an exact count perform `n` times, +- A missing own multiplicity inherits the first declared multiplicity of a redefined feature; + where neither declares one, the action-node usage is one performance. An own or inherited exact + finite count `[n]`, `[n..n]`, or named exact bound that evaluates to `n` performs `n` times, including unordered subactions that start concurrently without an incoming succession; `[0]` performs no body, trace event, flow or data transfer. Other ranges and bounds the model cannot evaluate do not identify a fixed number and are refused. diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md index c5de98c04d..1667f51c4a 100644 --- a/docs/project/spec-compliance.md +++ b/docs/project/spec-compliance.md @@ -141,7 +141,7 @@ what cannot be checked by anything is in **Test Coverage:** counted from the tree when the documentation site is built and published at [redk.opensysml.org](https://redk.opensysml.org/project/spec-compliance/); no figure is typed in or committed here, so a branch adding a test or fixture leaves this page as it is. - Execution conformance: the execution conformance cases, by prefix — the calc cases include the fixed-step RK4 lunar descent whose stages are body-local usages read over a range, the one-binding output case, the library complex/vector/trig cases, and recursion — factorial, fibonacci, a descent over a sequence, a mutually recursive pair and one whose result is its last expression; the action and state cases include a decision and a transition guard reading a calc usage; the new `f62_send_body_payload`, `f62_transition_body_dotted_target`, `f63_control_node_body`, `f63_for_typed_variable`, and `f63_merge_body_runs_on_traversal` fixtures cover node bodies, dotted transition targets, typed `for` variables, and merge traversal, and the `action_merge_loop_reenters`, `action_merge_loop_three_passes`, `action_merge_fork_branch_and_loop` and `action_merge_body_flips_own_guard` fixtures a loop re-entering a merge, a merge fed by a fork and a loop at once, and a merge body write read by the merge's own outgoing guard; the `assign_chain_*` fixtures write through a feature chain at depth two and three, through a port, through an inherited feature, through two features holding one occurrence, from a state's entry, `do` and `exit` behaviors and from a transition effect, with the write read back by a later node and by a guard, and a calculation body's chained target rejected; the `assign_write_*` fixtures write a subtype value and a widening numeric value legally, and refuse a wrong-typed write, a wrong-typed chained write, a collection the target's multiplicity cannot hold and an empty write where one value is required; the `instance_quantity_coherent_units`, `calc_quantity_coherent_result` and `calc_quantity_coherent_mismatch` fixtures report a product, quotient, fractional power, prefixed input and user-declared derived unit in the coherent unit of the declared quantity kind, keep a dimensionless ratio a number and a non-numeric exponent an error, and keep the dimension mismatch of a speed written to an acceleration) -- Runtime robustness: the runtime robustness cases (first-level subtests across the `TestRuntimeRobustness*` functions), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; a write to a constant or a derived feature, directly, through a chain, through a redefinition and through `SetFeatureValue`, each refused and leaving the value; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, a behavior both performing an action and stating a body, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, an Integer result beyond the Integer size budget, an Integer beyond int64 used as an index, offset or size, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) +- Runtime robustness: the runtime robustness cases (first-level subtests across the `TestRuntimeRobustness*` functions), among them: a write of a wrong-typed value, of too many values and of none where one is required, each leaving the feature as it was, and such a write from a state entry behavior, through a feature chain, to a calc output, to a body-local, to an output, to a performer feature and to a performance occurrence; a write to a constant or a derived feature, directly, through a chain, through a redefinition and through `SetFeatureValue`, each refused and leaving the value; deadlock, an accept payload read by a node that runs before the accept binds it, a default whose element count does not conform to its feature's multiplicity, a calc output the body never assigns or only a branch that did not run would assign, an output bound both by its declaration and by an assignment or by two assignments, empty entry/do/exit bodies, a do body that never finishes, cyclic specialization, a redefinition naming a missing step, an inherited succession targeting an incompatible replacement, ambiguous inherited redefinitions, an assignment to a qualified target, a chained assignment target whose final segment the object reached does not hold, whose step is not an object, holds several objects or holds no value, whose base the body cannot reach, whose value the target's multiplicity refuses, or that is written in a calculation body, a body-local usage typed by something that is not a calc, a body-local declaration with no execution, a range bound that is not an Integer, a range spending the step budget, a collection spending the element budget, a chain through a part stopping at a calc usage, an index naming no position, a collection operand of the wrong kind, a collection body of the wrong arity, a `select` predicate that is not a condition, a collection operation spending the step budget, a non-terminating loop, a calc usage leaving an input unbound, reading an output it does not declare or one with no value, a usage nested in a calc leaving an input unbound, reading an output it does not declare, an input default naming only itself, a nested usage chain reaching the recursion limit or spending the step budget, outputs valued from each other, a usage typed by something that is not a calc, a usage body spending the step budget, an invocation of a calc that computes several outputs and designates no result, a non-terminating calc loop, a calc body that never returns, a send or a `terminate` inside a calc, an assignment outside a calc body, a non-Boolean calc condition, a body-local declaration that must not leak, a body member that is not executable, accept suspension that can never end, an accept standing as a statement of a loop body that nothing can end, guards, budgets, sourceless accept, fork/join misuse, pseudostate dead ends and cycles, non-numeric time trigger, a time trigger argument of the type validation refuses, misaddressed send, accept of an unsent type, send through an unconnected port, history misuse, non-deferrable deferred trigger, non-terminating do behavior, calc binding/arity/recursion failures, unhandled call, call argument of the wrong type, missing and cyclic `perform` references, a library function outside its domain or with the wrong arity, an extension library function outside its domain, an Integer result beyond the Integer size budget, an Integer beyond int64 used as an index, offset or size, a flow end that names no action node, a flow from a node that produced no value, an action accept waiting on the clock — `after` and `at` fired by advancing it, `at` an instant already past fired at once, a negative `after`, a duration of another dimension — and the clock advanced by zero, by a negative amount, with nothing waiting, past a wait that stays queued and into a machine the event budget stops, a non-Boolean change trigger, a variation with no variant selected, a selection that is not one of a variation's variants, two variants selected at once, a variation read through its declaration, a chain through an unselected variation part, two variation points selecting one variant without an owning object, a `variant` declared outside a variation, a variant under a redefined variation, a deep chain of redefinitions, conflicting redefinitions at several levels, one feature valued under two of its names, a feature both valued and restated in a body, a flow that names no feature to carry, an accepted message carrying no single value to bind, a transition that names no target, a transition endpoint that names nothing, a transition endpoint naming a state of a different machine, a transition endpoint lowered with no name-resolution pass, whose edge is left out, a connector end naming no reachable feature, a connector holding more than one object, a connector attached to itself or to one that names it back, a write into a pair of values derived from each other) - Runtime tests: the runtime test functions (the top-level tests `go test -v ./internal/exec/runtime` reports), the conformance, trace and robustness gates above among them - Golden ASTs: the golden AST fixtures (SysML and KerML), including the implicitly typed connector forms and the standard behavioral notation — a named flow with `from`, accept trigger expressions, an accept subsetting an event, sends, a succession to a loop with `until`, `then done`, a decision `else` branch, a bodied `exhibit state`, a transition with its trigger on its own line, a qualified namespace-level succession — body-local calc usages and ranges, the three loop forms, pseudostate, timed-trigger, call-trigger, calc default/invocation, calc statement bodies, n-ary connector-end parsing, prefix metadata, keyword-less members, node bodies and dotted transition targets, a chained assignment target, and occurrence typing) - Golden traces: the golden execution traces under the default schedule, by prefix, and the `.trace.golden` files pinning a case under a named policy — entry/do/exit ordering of inline action bodies and a do body run to its end inside one round, the standard loop `until` with `then done`, a decision's guarded and `else` branches, a named flow carrying a value between action nodes, an accept with a `when` trigger, an accept subsetting an event, a send invocation through a port, a transition accepting through a port, loop and conditional bodies, one calc usage body run feeding several output reads, a usage whose outputs are read either side of an assignment to what its input named, a usage nested in a calc read for two of its outputs, calc statement bodies and their loop iterations, fork/join branch ordering, region entry/exit ordering, do behavior interleaving across orthogonal regions, send/accept, an accept parked until its message arrives, a payload read by a node declared before the accept that binds it, calc and constraint evaluation, library function invocation, the dotted-target transition, control-node and merge-body traces, and the merge loops re-entered on every pass) @@ -738,8 +738,12 @@ remain one-shot. | Subactions no succession orders are performed during the owner, unordered: every composite action usage of an action is one of its `subactions` (Systems Library `Actions.sysml`: `action subactions: Action[0..*] :> actions, subperformances`, "The subperformances of this Action that are Actions"), and a subperformance is an enclosed performance (Kernel Semantic Library `Performances.kerml`: `composite step subperformances … subsets enclosedPerformances`; `enclosedPerformances … subsets timeEnclosedOccurrences`), which `Occurrences.kerml` defines as "Occurrences that start no earlier than and end no later than this occurrence". So each subaction no succession reaches — an action node, and a `send`, `accept`, `assign`, `if`, `while`, `for` or `terminate` written among the members — starts as the owner's performance starts, beside the `first`-rooted flow when there is one, with nothing ordering it against the others; the owner ends, and its successors, `done` and output reads follow, only after every one has. A `ref` (referential), abstract, performed (`perform`, an event occurrence usage, which is referential: `validateEventOccurrenceUsageIsReference`) or body-parameter usage, and one only typing, subsetting or redefined by a sibling, is no composite subaction and is not performed. The same holds for a nested action node and for a loop or branch body stating a flow, the do body of a state included | `lower/action_starts.go` `ActionGraph.Starts`, `unorderedSubactions`, `startsConcurrently`, `specializedSiblings`, `FlowStartError`; `lower/case_body.go` `StartFlow` (`ActionGraph.Concurrent`), `CaseFlowStart` (a sole unpreceded `ref` or abstract usage is no start, `performedStep`), `caseSteps` (a case body's statements in its stated flow are nodes of it, not locals as well; a trailing result no succession sequences is the body's result, not a node); `lower/action_subflow.go` `runsOwnFlow`; `lower/action_nodes.go` (statements among the members are nodes of the flow); `runtime/action_executor.go` `initialize` (one token per start); `runtime/action_subflow.go` `seedTokens` (a nested performance retires once every start's token has) | conformance `action_unordered_subactions` (+ trace), `action_unordered_join_once`, `action_unordered_join_once_nested`, `state_do_body_unordered_join_once` (two unordered starts `a`, `b` succeeding into `c`: `c` performed once, after both, `total = 111` on every schedule; trace + `.trace.order` + `.check.expected.json`), `action_unordered_subactions_write_conflict` (`outcomes` + `.check.expected.json` + per-policy traces), `action_unordered_beside_first`, `action_unordered_nested_subactions`, `action_unordered_statements`, `action_unordered_send_accept`, `action_unordered_send_to_receiver`, `action_unordered_reference_not_performed`, `action_unordered_accept_holds_owner`, `action_body_flow_unordered_statement`, `state_do_body_unordered_statement`; `robustness_unordered_subactions_test.go:TestRuntimeRobustnessUnorderedSubactions` (successions cycling over every step: `ErrInvalidActionFlow`; an unordered accept never satisfied, also nested: `ErrAcceptDeadlock`, the successor not performed; two unpreceded steps joined into a third, at the root, in a nested node and in a state's do body; a `while`, `if` and `assign` among the members beside `first start; … done;`), `internal/frontend/grpc/runtime_test.go:TestExecuteAction_EmptyAction` (an action with no subaction completes), `:TestExecuteAction_NoStart`, gRPC conformance `execute_action_empty`, `execute_action_no_initial` (a cycle); `smt/support_test.go:TestAnalyzeRefusesUnorderedSubactions` | ✅ Faithful (the order among them is open and is the scheduler's choice point, enumerated by `-schedule explore`; the bounded model checker refuses such a flow as not encoded, `UnsupportedError` "unordered subaction". A nested node whose members are only statements, and a loop, branch or behavior body stating no flow, keep running them in declaration order — one linearization of what the library leaves open) | | Final node termination | `action_executor.go:512` stepFinalNode | `action_control_flow.sysml` | ✅ Faithful | | An `assert constraint` a succession orders among an action's steps (`first spin then fast;` — a `ConstraintUsage` is an `OccurrenceUsage`, so a succession may join it, SysML v2 §7.19.2, §8.3.19.2 `AssertConstraintUsage`) is a step of the flow: the token reaching it evaluates its condition in the action's scope, a false result (true, under `assert not`) ending the run with a typed `ViolationError`, and moves on; an assertion no succession names stays a member the flow does not check | `lower/action_graph.go` `orderedAssertions` and `lower.Assert`, collected as a node by `lower/action_nodes.go` `collectActionNodes`; `runtime/statements.go` `stmtEngine.assert` (the condition machinery `Holds` uses) | `conformance/action_sequenced_assertion_holds.sysml` + `.expected.json` + trace golden, `conformance/action_sequenced_assertion_violated.sysml`; `robustness_sequenced_assertion_test.go:TestRuntimeRobustnessSequencedAssertion`; parse golden `assert_sequenced.sysml`; `tests/migrate/interaction_use_test.go:TestInteractionUsesAndStateInvariants` | ✅ Faithful | -| An action-node usage with no declared multiplicity is one performance; an exact finite declared multiplicity `[n]` or `[n..n]` performs `n` times, including zero times for `[0]`, using the usage's own declaration rather than an inherited feature multiplicity. `Performances.kerml` encloses the performances; `Actions.sysml` declares `subactions : Action[0..*]`. A non-fixed or unevaluable count refuses execution and is reported by validation. | `lower/step_multiplicity.go` `ActionGraph.StepCount`/`CheckStep`; `runtime/action_step_multiplicity.go` `ActionExecutor.stepMultiplicity`/`splitRepeatedStep`; `runtime/action_executor.go` `stepNestedAction`/`completeNode` | `conformance/action_step_multiplicity_exact`, `_reverse`, `_explore`, `_range`, `_zero`, `_local_frames`, `_nested`, `_perform`; `lower/step_multiplicity_test.go`; `robustness_action_step_multiplicity_test.go`; `passes/behavior/action_step_multiplicity_test.go` | ✅ Faithful | -| Action usages in loop or conditional block flows execute once per pass; exact counts other than `[1]`, including `[0]`, are refused rather than expanded there | `runtime/statements.go` `stmtEngine.runBlock`; `passes/behavior/action_step_multiplicity.go` `checkBlockFlowSteps` | `robustness_action_step_multiplicity_test.go`; `passes/behavior/action_step_multiplicity_test.go` | ⚠️ Approximate (block-flow repetition is out of scope) | +| An action definition inherits its action nodes, statements, successions and flows from its generals; inherited endpoints follow direct or indirect redefinitions, start/done remain singular, diamonds contribute a member once, and a bodyless specialization executes assertions ordered by any body in its generalization chain at their inherited position and in their declaring scope; an assertion no body in the chain orders stays unchecked, as in its general; a specialization's own flow may connect inherited steps; inherited analysis and verification cases remain case steps lowered in their declaring scope, and inherited terminate usages retain their stated body flows | `resolve/transition.go` `ActionGeneralization`; `lower/action_inherited.go` `orderedAssertionsForGraph`/`mergeInheritedActionContent`; `lower/action_graph.go` `orderedAssertions`/`lowerActionFlowWithTyping`; `lower/action_nodes.go` `ensureDeclaredActionNode`/`collectInheritedActionNodes`; `lower/case_body.go` `caseSteps`/`StartFlow`; `lower/action_subflow.go` `lowerTerminateNode` | `lower/action_inherited_test.go:TestToActionGraphInheritedFlowStartsAlongsideUnorderedLocalNode`, `TestToActionGraphInheritsAssertionOrderedByIntermediateGeneral`, `TestToActionGraphOwnFlowConnectsInheritedActionNodes`, `TestToActionGraphInheritsTerminateUsageBodyFlow`, `TestToActionGraphInheritsCaseAsCaseStep`, `TestToActionGraphKeepsDifferentlyGuardedInheritedSuccession`, `TestToActionGraphDeduplicatesRestatedUnguardedSuccession`, `TestToActionGraphTypedBodyPreservesInheritedPinBinding`, `TestToActionGraphPreservesInheritedSuccessionMultiplicitiesWhenRestated`, `TestToActionGraphCompositeRedefinitionMergesInheritedContent`; conformance `inherited_action_steps_p0`, `_p2`, `_p3`, `_multi_level`, `_diamond`, `_keep`, `_redefine_two_levels`, `_direct_assign`, `_direct_send`, `_state_entry`, `_state_do`, `_state_exit`, `_state_entry_typed_body`, `_state_do_typed_body`, `_state_exit_typed_body`, `_state_entry_perform_body`, `_classifier_perform_typed_body`, `inherited_action_steps_sequenced_assertion_holds` (+ trace), `_sequenced_assertion_violated`, `inherited_action_steps_own_flow_inherited_steps`; `inherited_action_step_analysis_test.go:TestInheritedActionStepFixturesAnalyseCleanly`; `robustness_inherited_action_steps_test.go:TestRuntimeRobustnessInheritedActionSteps` (`inherited_guard_survives_differently_guarded_owned_succession`, `typed_usage_bodyless_inherited_pin_binding`, `typed_usage_body_preserves_inherited_pin_binding`, `inherited_succession_plain_restatement_is_refused`, `inherited_succession_matching_restatement_runs_repeated_step`, `composite_redefinition_keeps_inherited_statement_and_owned_subaction`, `composite_redefinition_keeps_inherited_subaction_and_owned_statement`) | ✅ Faithful | +| Unsupported inherited-action cases have stable, typed refusals | `lower/errors.go` `ErrCyclicSpecialization`, `ErrRedefinedStepMissing`, `ErrIncompatibleRedefinedStep`, `ErrAmbiguousInheritedStep`; `passes/behavior/action_step_multiplicity.go` `checkAction` reports graph-lowering failures | `robustness_inherited_action_steps_test.go`; `passes/behavior/action_step_multiplicity_test.go:TestActionStepMultiplicityPassReportsLoweringErrors` | ✅ Faithful (`action specializes itself`; `redefined action step not found`; `redefining feature is not an action step`; `inherited succession reaches more than one redefining step`) | +| A body-stating action usage typed by an action definition merges the definition's inherited nodes, statements, successions and flows with its own body; its own redefinitions replace inherited members. A nested typed usage with a merged body is recorded so runtime does not invoke the type again; a bodyless usage or one whose body only binds features or pins retains its existing invocation/binding path | `lower/action_graph.go` `lowerActionFlowWithTyping`; `lower/action_subflow.go` `lowerActionNode`; `runtime/invoke_action.go` `nestedInvocationInGraph` | `lower/action_inherited_test.go`; conformance `inherited_action_steps_usage_typed`, `_usage_typed_with_body` (top-level), `_nested_typed_usage_with_body` (nested), `_typed_node` (bodyless), `action_invoked_node_body_writes_output` (`{30, 31}`); `inherited_action_step_analysis_test.go:TestInheritedActionStepFixturesAnalyseCleanly` + trace goldens | ✅ Faithful, self-assessed | +| A redefining nested action node retains its own effective members followed by inherited members not redefined by them, with statements and expressions resolved in their declaration scopes; inherited action typing is performed only when the node has no merged typed body | `lower/action_subflow.go` `effectiveActionMembers`/`lowerActionNode`; `lower/action_inherited.go` `recordInheritedPerform`; `runtime/invoke_action.go` `nestedInvocationInGraph` | `lower/action_inherited_test.go`; conformance `inherited_action_steps_keep`, `_narrow`, `_redefine_two_levels`, `_typed_node`, `_perform`, `_redefined_typed_step` | ✅ Faithful | +| An action-node usage with no own multiplicity inherits the first declared multiplicity of the feature it redefines, including through multiple redefinitions; its own declaration wins. Subsetting alone (`:>`) does not carry multiplicity. A missing count defaults to one; an exact finite count `[n]` or `[n..n]` performs `n` times, including zero times for `[0]`. Non-fixed or unevaluable counts are refused and reported at their source declaration. | `semantics/collection.go` `Model.GoverningMultiplicitySource`; `lower/step_multiplicity.go` `ActionGraph.StepMultiplicity`/`StepCount`/`CheckStep`; `runtime/action_step_multiplicity.go` `ActionExecutor.stepMultiplicity`/`splitRepeatedStep`; `runtime/action_executor.go` `stepNestedAction`/`completeNode` | `conformance/inherited_action_steps_keep`, `_narrow`, `_redefine_two_levels`; `lower/action_inherited_test.go`; `lower/step_multiplicity_test.go`; `robustness_inherited_action_steps_test.go`; `passes/behavior/action_step_multiplicity_test.go`; `smt/support_test.go:TestAnalyzeRefusesInheritedRepeatedActionSteps` | ✅ Faithful | +| Action usages in loop or conditional block flows execute once per pass; exact counts other than `[1]`, including inherited `[2]` and `[0]`, are refused rather than expanded there | `runtime/statements.go` `stmtEngine.runBlock`; `passes/behavior/action_step_multiplicity.go` `checkBlockFlowSteps` | `robustness_action_step_multiplicity_test.go`; `passes/behavior/action_step_multiplicity_test.go` (including inherited `[2]` in a loop body) | ⚠️ Approximate (block-flow repetition is out of scope) | | When an endpoint has a count other than one, each incident succession must establish ordering for every source and target performance. The executor checks both unwritten-end readings — unconstrained `[0..*]` and exact-one `[1..1]` — and refuses an open or excluded count; control-node adjacency, guards, object flows, bindings or connections to repeated-node pins, and external feature reads are unsupported. This approximates the unwritten-end rule, which the library leaves open (OMG issue [KERML-29](https://issues.omg.org/issues/KERML-29), deferred); the library's `StatePerformances.kerml` and `TransitionPerformances.kerml` explicitly write the entry/effect/exit endpoint multiplicities. | `lower/step_multiplicity.go` `ActionGraph.CheckStep`/`checkEdge`/`crossingRange`; `runtime/action_step_multiplicity.go`; `passes/behavior/action_step_multiplicity.go` | `conformance/action_step_multiplicity_ordering`, `_order_target_only`; `lower/step_multiplicity_test.go`; `robustness_action_step_multiplicity_test.go`; `passes/behavior/action_step_multiplicity_test.go` | ⚠️ Approximate (the two readings deliberately refuse where the undeclared default is unresolved) | | State entry, do, and exit performances retain their library-declared `[1]`; part-level performed-action multiplicity is outside this feature and refuses when executed. | `lower/state_behavior.go` `LowerBehaviors`; `runtime/state_statements.go`; `runtime/classifier_behavior.go` `attachClassifierBehavior`; `passes/behavior/action_step_multiplicity.go` | `conformance/action_step_multiplicity_state_entry`, `_part_perform`; `robustness_action_step_multiplicity_test.go`; `passes/behavior/action_step_multiplicity_test.go` | ✅ Faithful | | An action's result parameter inherited from a function it specializes (a calc, case or use case def — KerML §7.4.7.2, §8.3.4.7.8; SysML §7.17.2, §7.19.2) is an output parameter the body writes and a performer reads; only a `return` whose owner is no function or expression (KerML `validateReturnParameterMembershipOwningType`) is refused with `ErrActionResultParameter` | `semantics/return_parameter.go` `ResultParameterOwnerValid`, `DeclaresFunction`; `runtime/action_subflow.go` `checkResultParameters`, `checkNodeResultParameters`; `passes/return_parameter.go` `checkReturnParameterOwner` | `action_result_inherited_from_calc_def`, `action_result_inherited_from_use_case_def`, `action_result_inherited_from_use_case_library_result`, `action_result_read_by_performer`; `robustness_action_result_parameter_test.go:TestRuntimeRobustnessActionResultParameter` | ✅ Faithful | @@ -808,8 +812,11 @@ remain one-shot. | Succession outside a behavior body (`first part1::action1 then requirement1;`, `first b.g then b.m;`, `first grip then move;` in a part def) orders the performances it relates: featured by its owning type, or by the innermost common featuring type of its ends (KerML §8.3.4.5.3 `deriveConnectorDefaultFeaturingType`, `checkConnectorTypeFeaturing`; §8.4.4.6.1 implied TypeFeaturing), each earlier-end performance happening before each later-end one (`Occurrences::HappensBefore`, SysML §8.4.9.4) | `parser/namespace.go`, `parser/succession.go`; `semantics/succession_featuring.go:BehaviorSuccessionFeaturingType`; `lower/behavior_order.go:BehaviorOrders`; `check/passes/behavior_order.go:BehaviorOrderPass`; `runtime/succession_order.go:shouldDeferBehavior`, `releaseDeferredBehavior`, `checkSuccessionOrderViolation`, `successionCycle`, `noteRefusedBehaviorOrders` | `parse/behavior_namespace_succession.golden`, `parse/namespace_succession_chain_ends.golden`, `parse/type_succession_performed_actions.golden`, conformance `namespace_succession_chain_ends`, `type_succession_performed_actions`, `namespace_succession_qualified_ends`, `namespace_succession_explicit_start_violated`, `namespace_succession_requirement_end` (the first three with trace goldens), `robustness_namespace_succession_test.go:TestRuntimeRobustnessNamespaceSuccession`, `succession_featuring_test.go`, `behavior_order_test.go` (lower and check), `lower/namespace_succession_test.go:TestNamespaceSuccessionHasNoTokenFlowOrNeighborGraphAttachment` | ✅ Implemented where one performance per end per featuring instance is ordered: a type-bound start is held until its predecessor ends (the scheduler is constrained, not fixed to one order); an explicit `perform x.beh.start` or `-action` start that would break the order fails with `succession-order-violated`; a cycle is `succession-order-cycle`. Refused with `succession-orders-nothing` and the reason when one end binds an object behavior and the order cannot be enforced (a requirement end, a missing or repeated performance, open under KERML-29); written end multiplicities follow the action-step rule. Ends with no common featuring type are `connector-type-featuring` errors. Derivation: [behavior-semantic-oracle.md](behavior-semantic-oracle.md#a-succession-outside-a-behavior-body-orders-the-performances-it-relates-wherever-they-run); the pinned pilot's rejection of qualified ends is [gap register #21](spec-pilot-gap-register.md#21-a-succession-whose-ends-are-qualified-names) | | A statement written directly among an action's own members (`send`, `assign`, `if`, `while`, `for`, `terminate`) is a subaction of the action and is performed, not ignored | `lower/action_graph.go` `BodyStatementMembers`; `lower/action_nodes.go` (each is a node of the flow); `lower/action_starts.go` `startsConcurrently` | `robustness_unordered_subactions_test.go:TestRuntimeRobustnessUnorderedSubactions/statement_directly_in_an_action_body_*`, conformance `action_unordered_statements`, `action_body_flow_unordered_statement`, `analysis_unordered_statements_beside_flow` (performed once in a case body stating a flow), `analysis_stated_flow_trailing_result` (a trailing result-returning `if` returns after the flow) | ✅ Faithful (no succession reaches it, so it starts with its owner's performance, as the unordered-subactions row above states) | | A body member that is not an executable statement is reported, not skipped | `lower/action_graph.go` `Unsupported`; `runtime/statements.go` `execute` (`lower.Unsupported`) | `lower/action_body_test.go:TestActionBodyUnexecutableMemberIsLowered`, `lower/block_graph_test.go:TestBlockStatingItsOwnEdgeIsLoweredToAStatedFlow`, `robustness_test.go:loop_body_of_unexecutable_statement`, `:block_flow_of_unexecutable_member` | ✅ Faithful (a member of a loop or branch body outside what a block's flow executes — a `part` declaration fails the execution instead of producing a wrong answer silently; an edge written in a block now states a flow) | -| An action node that states a flow of its own runs that flow: its nodes are its subperformances (Kernel Semantic Library `Performances.kerml` — `composite step subperformances: Performance[0..*] subsets enclosedPerformances, suboccurrences`; Systems Library `Actions.sysml` — `abstract action def Action :> Performance` and `action subactions: Action[0..*] :> actions, subperformances`), so the node completes only when its own flow does and its outgoing succession fires only then | `lower/action_subflow.go` `lowerActionNode`, `statesOwnFlow` (`ActionGraph.Subflows`); `runtime/action_subflow.go` `enterSubflow`, `leaveSubflow`, `validateSubflows`, `actionFrame`; `runtime/action_executor.go` `stepNestedAction`, `retireToken`, `completeNode` | `conformance/action_nested_flow_writes_value.sysml`, `action_nested_flow_two_levels.sysml` + trace golden, `action_nested_flow_in_fork_join.sysml` + trace golden, `action_nested_flow_guarded_inner_succession.sysml`, `action_nested_leaf_body_preserved.sysml`; `lower/action_subflow_test.go:TestActionNodeCarriesItsOwnFlow`, `:TestActionNodeSubflowsNest`, `:TestActionNodeWithoutFlowStaysALeaf`, `:TestActionNodeSubflowCarriesItsFailure`; `robustness_test.go:nested_flow_without_an_initial_node` (a cycle, so no node is unpreceded), `:nested_flow_with_a_dangling_succession` (both `ErrInvalidActionFlow` at `initialize()`), `:nested_flow_that_cannot_progress` (`ErrActionDeadlock`), `:nested_flow_that_never_ends` (`ErrActionStepLimitExceeded`) | ⚠️ Approximate, self-assessed (the pinned OMG pilot implementation executes no actions, so nothing external referees this. Nesting is unbounded and combines with fork/join, decision/merge and guards at either level; an inner flow is a performance of its own — the features its node declares live in that frame and are read as `node.pin` — while a name it does not declare resolves lexically to the enclosing action's feature, so what it writes there is visible to later nodes and to the performance occurrence where one applies. A node stating no flow keeps its leaf body. Not covered: a node that both states a flow of its own and is typed by an action definition stating one — the node's own flow is run and the definition's is not merged into it) | -| An action usage stating no body of its own performs the body of the action it names — the definition typing it (Systems Library `Actions.sysml` — `abstract action def Action :> Performance`; a usage's features are those of its type, KerML 1.0 §7.4.7) | `runtime/classifier_behavior.go` `actionBodySymbol` (used by `newActionExecutorForOccurrence`, so every entry point resolves it the same way `classifierBehaviorSymbol` already did for `perform`) | `repl/runtime_commands_test.go:TestActionDebuggerRunsAPerformUsage` (`%action ::`), `repl/testdata/action_perform_usage.sysml` | ⚠️ Approximate, self-assessed (the flow is sourced from the definition typing the usage, which is what makes `%action` on a `perform` usage runnable. A usage that both states members of its own and is typed by a definition does not merge the two: its own body wins, and the definition's content is not inherited) | +| An action node that states a flow of its own runs that flow: its nodes are its subperformances (Kernel Semantic Library `Performances.kerml` — `composite step subperformances: Performance[0..*] subsets enclosedPerformances, suboccurrences`; Systems Library `Actions.sysml` — `abstract action def Action :> Performance` and `action subactions: Action[0..*] :> actions, subperformances`), so the node completes only when its own flow does and its outgoing succession fires only then | `lower/action_subflow.go` `lowerActionNode`, `statesOwnFlow` (`ActionGraph.Subflows`); `runtime/action_subflow.go` `enterSubflow`, `leaveSubflow`, `validateSubflows`, `actionFrame`; `runtime/action_executor.go` `stepNestedAction`, `retireToken`, `completeNode` | `conformance/action_nested_flow_writes_value.sysml`, `action_nested_flow_two_levels.sysml` + trace golden, `action_nested_flow_in_fork_join.sysml` + trace golden, `action_nested_flow_guarded_inner_succession.sysml`, `action_nested_leaf_body_preserved.sysml`, `inherited_action_steps_nested_typed_usage_with_body.sysml`; `lower/action_subflow_test.go:TestActionNodeCarriesItsOwnFlow`, `:TestActionNodeSubflowsNest`, `:TestActionNodeWithoutFlowStaysALeaf`, `:TestActionNodeSubflowCarriesItsFailure`; `lower/action_inherited_test.go:TestToActionGraphMergesBodyStatingTypedActionNode`; `robustness_test.go:nested_flow_without_an_initial_node` (a cycle, so no node is unpreceded), `:nested_flow_with_a_dangling_succession` (both `ErrInvalidActionFlow` at `initialize()`), `:nested_flow_that_cannot_progress` (`ErrActionDeadlock`), `:nested_flow_that_never_ends` (`ErrActionStepLimitExceeded`) | ⚠️ Approximate, self-assessed (the pinned OMG pilot implementation executes no actions, so nothing external referees this. Nesting is unbounded and combines with fork/join, decision/merge and guards at either level; an inner flow is a performance of its own — the features its node declares live in that frame and are read as `node.pin` — while a name it does not declare resolves lexically to the enclosing action's feature, so what it writes there is visible to later nodes and to the performance occurrence where one applies. A node stating no flow keeps its leaf body. A body-stating typed node merges its own and typed flows into one subflow, and the graph marks it so the type is not invoked a second time) | +| A body-stating typed action usage specializes the action it types or performs: its owned members join inherited steps, successions, flows and statements in one flow, and its redefinitions replace inherited members; features- and pin-only bodies remain invocation bindings (KerML 1.0 §7.3.4.3 / §8.3.3.3 `FeatureTyping`; SysML v2 §7.17 `PerformActionUsage`) | `lower/action_graph.go` `ToActionGraphWith`, `lowerEffectiveActionNodeFeatures`; `lower/action_subflow.go` `mergedTypedActionBody`; `ActionGraph.MergedTypedSubflows`; `runtime/invoke_action.go` `nestedInvocationInGraph`; `runtime/action_frame.go` `seedPerformance`, `checkMergedTypedInputs`; `runtime/classifier_behavior.go` `actionBodyGraph` | conformance `inherited_action_steps_usage_typed`, `_usage_typed_with_body`, `_nested_typed_usage_with_body`, `_state_mixed_empty_bump`, `_classifier_perform_typed_body`, `state_block_flow_typed_node`, `state_block_flow_typed_node_body_writes_output`, `action_invoked_node_body_writes_output` (`{30, 31}`), `action_accept_nested_call_chain`, `action_accept_nested_chain_clock` (each sequences an output read in a `take` step); lower `TestToActionGraphMergesBodyStatingTypedActionNode`, `TestToActionGraphBodyStatingTypedNodeIncludesTypedPins`; `robustness_test.go:testStateBlockTypedNodeInputBoundByNothing`; `inherited_action_step_analysis_test.go:TestInheritedActionStepFixturesAnalyseCleanly` | ✅ Faithful, self-assessed (nested graph metadata prevents a second invocation of the same typed action; its parameters and pins remain available and required inputs are checked before execution) | +| Entry/do/exit usages that type or perform an action and state a body run the action content and their own body as one started behavior flow (Systems Library `States.sysml` `StateAction`; SysML v2 §7.17 `PerformActionUsage`) | `lower/state_behavior.go` `lowerStateBehavior`; `lower/action_graph.go` `lowerActionFlowWithTyping`; `lower/classifier_behavior.go` `StatesBody`/`NamesBehavior`; `runtime/state_statements.go` `stateStmtHost.runFlow`; `runtime/classifier_behavior.go` `actionBodyGraph` | conformance `inherited_action_steps_state_entry`, `_state_do`, `_state_exit`, `_state_entry_typed_body`, `_state_do_typed_body`, `_state_exit_typed_body`, `_state_entry_perform_body`, `_classifier_perform_typed_body`, `_state_mixed_empty_bump` + trace goldens; robustness `robustness_inherited_action_steps_test.go:TestRuntimeRobustnessInheritedActionSteps` (missing redefined step, incompatible replacement, specialization cycle); `inherited_action_step_analysis_test.go:TestInheritedActionStepFixturesAnalyseCleanly` | ✅ Faithful, self-assessed (the valid state and classifier fixtures bind observable values through explicit `inout` pins; feature-only pin binding remains unchanged) | +| Results include the action's feature values from its frame or performance occurrence and its latest non-repeated subaction performances; values held only by repeated nested nodes are intentionally omitted | `runtime/action_executor.go` `Results`; `runtime/action_frame.go` `collect`/`heldFeatures` | conformance `inherited_action_steps_redefined_typed_step` (outer `hits = 2`; repeated `a.c` is omitted) | ✅ Faithful | +| An action usage stating no body of its own performs the body of the action it names — the definition typing it (Systems Library `Actions.sysml` — `abstract action def Action :> Performance`; a usage's features are those of its type, KerML 1.0 §7.4.7) | `runtime/classifier_behavior.go` `actionBodySymbol` (used by `newActionExecutorForOccurrence`, so every entry point resolves it the same way `classifierBehaviorSymbol` already did for `perform`) | `repl/runtime_commands_test.go:TestActionDebuggerRunsAPerformUsage` (`%action ::`), `repl/testdata/action_perform_usage.sysml` | ⚠️ Approximate, self-assessed (the flow is sourced from the definition typing the usage, which is what makes `%action` on a `perform` usage runnable. When a usage states a body of its own, that body is merged with its typed action's inherited content, as documented in the preceding row.) | | A block has a token flow of its own: a nested action declaration, an `accept` or a `perform` in a loop or branch body is a node of it | `lower/block_graph.go` `blockNeedsFlow`, `lowerBlockFlow`, `lowerFlowNode` (an accept node lowers to `ActionGraph.Accepts`), `lowerStatedBlock`; `lower/action_graph.go` `lowerBlock` (a declaration-order flow holding an accept is `Block.Stated`); `runtime/statements.go` `runBlock`; `runtime/action_statements.go` `performNode`, `performBlockFlow`; `runtime/action_subflow.go` `runSubflow`; state and calculation hosts delegate stated body flows through the same transparent performance path | `action_block_flow_nested_action.sysml`, `action_block_flow_perform_in_loop.sysml`, `action_block_flow_if_branch.sysml`, `action_body_flow_succession.sysml`, `action_body_flow_fork_join.sysml`, `action_body_flow_branch_in_loop.sysml`, `action_body_flow_while_attribute.sysml`, `action_body_flow_terminate_statement.sysml`, `state_body_flow_succession.sysml`, all + trace goldens where marked; `lower/block_graph_test.go:TestBlockStatingItsOwnEdgeIsLoweredToAStatedFlow`, `TestBlockStatingACycleKeepsInitialUnset`, `TestStatedBlockWithUnsequencedStatementRecordsInvalidFlow`, `TestStatedFlowInIfBranchInsideLoop`; `robustness_nested_node_body_test.go:TestRuntimeRobustnessNestedNodeInBody`, `:accept_in_declaration_order_body_deadlocks` | ⚠️ Approximate (declaration-order blocks retain their existing sequential statement semantics; a body that states `first`, successions, forks, joins, branches or other control nodes is lowered to `Block.Graph` and run as a transparent performance, recreated for every loop iteration, with body-local attributes seeded per performance and `terminate;` resolved through the body frame to the enclosing action node. Member-attached `then` in loop and branch bodies is parsed into the same positional succession edges as an action body. A declaration-order body holding an `accept` runs as the same transparent performance, its nodes still in declaration order, so the accept parks the body and everything performing it. Remaining typed refusals are a stated flow in a calculation body and body succession probability metadata when lowering has no resolver.) | | Send statement (message passing) | `lower/action_graph.go` lowerBody (`FeaturePath` keeps the whole target path); `runtime/signal.go` buildMessage (carries the resolved signal symbol), `valueTypeName`, `objectSignalSymbol`, `messageMatches` (qualified identity plus subtype conformance over `semantics.Model.Conforms`), post, `postFor` (a target led by a name the sending performance binds is delivered to the objects it holds; a target that is neither a name nor a feature chain is evaluated by `EvalContext.valuedTargetAddresses`/`receiverObjects` to the objects it yields, `lower/action_graph.go` `Send.TargetExpr` carrying the expression), postTo, `postAt`, resolveAddresses, namedAddresses/qualifiedAddresses, featureAddresses, `addressesFrom`, `EvalContext.boundTargetAddresses`, addressOwner, `fvObjects`, `portAddress`/`receiverAddress`/`objectAddress`, `PostMessage` (`deliveryOf`), `Message.reaches`; `runtime/invoke_action.go` invokeAction (a performed action runs as the object performing its caller) | `action_send_accept.sysml`, `send_identity_same_named_ports.sysml`, `port_identity_own_port.sysml`, `send_identity_unroutable_target.sysml`, `lower/action_body_test.go:TestActionBodyLowering`, `lower/connection_test.go:TestLowerSendKeepsAddressedPath`, `signal_test.go:TestActionMessageReachesStateMachine`, `:TestAddressedSendStaysWithinTheSendingObject`, `:TestAddressedSendResolvesPortOfNamedObject`, `:TestAddressedSendDescendsToNestedPort`, `:TestAddressedSendToUnreachablePortIsTyped`, `send_identity_addressed_part.sysml`, `send_identity_performed_object.sysml`, `signal_test.go:TestDeliveryHoldsAConsumerToTheWholeDestination`, `:TestPerformedBehaviorRunsAsItsPerformer`, `:TestAddressedSendToQualifiedElementOfATwinObject`, `:TestInjectedMessageIsHeldToTheDestinationItNames`, `signal_test.go:TestSendOfAnItemObjectIsTypedByItsDefinition`, `:TestSendOfAnItemObjectIsNotTypedByTheUsageSent`, `:TestAddressedSendFansOutOverAMultiValuedFeature`, conformance `send_subtype_matches_supertype_accept` + `.expected.json`, `send_same_name_distinct_packages` + `.expected.json`, `send_to_bound_parameter` + `.expected.json` (a send to an `in` parameter of the sending action, and a chain from one, reaches the object the caller bound), `robustness_test.go:injected_message_names_a_receiver_no_accept_has`, `robustness_send_target_test.go:bound_target_holds_a_value`, `:bound_target_holds_nothing` (`ErrSendTargetNotObject`), `:bound_target_chain_names_no_feature`; `migrate/send_target_test.go:TestSendTargetFedByParameterReachesTheObjectItHolds` (a migrated v1 `SendSignalAction` whose target pin an object flow feeds); conformance `send_to_second_object_of_usage` + `.expected.json` + trace golden (two objects of one usage, only the one `cars#(2)` yields hears), `send_to_object_held_in_feature`, `send_to_object_through_chain` (`garage.cars#(2)`), `signal_test.go:TestSendToConstructedObjectReachesIt` (`to new Car()`), `parse/send_to_indexed_receiver.golden`, `robustness_send_to_object_identity_test.go:TestRuntimeRobustnessSendToObjectIdentity` (a receiver yielding no object or a data value is `ErrSendTargetNotObject`, a destroyed one `ErrOccurrenceDestroyed`) | ✅ Faithful (a message is typed by what was sent and delivered by object identity: a target is resolved through the instance graph to the object owning it and the port path within it, and the destination is built whole or refused with `UnroutableSendError`, so a consumer takes a message only by satisfying every part of it — a same-named port or receiver of another object, or a sibling of the sender, never sees it. A behavior an object performs, however deeply, presents that object's identity, so only a behavior no object performs at all has none. A qualified target takes its object from its qualifier, and a message injected from outside the model is held to the destination its fields name. An object sent as a message — an `item cmd : Command { … }` sent by name — is typed by the definition it materializes, not by the usage sent, so an accept of that definition takes it and reads its features. A target through a multi-valued feature denotes every element it holds (KerML 1.0 §7.3.4.6), so one send delivers one copy per element, each on that element's own identity and port path, duplicates delivered once; a path reaching no object is still `UnroutableSendError`. A target led by a parameter, pin or local the sending performance binds — `send new Go() to recipient` under `in recipient : Worker` — is read from the performance's frame, so the objects it holds are addressed, with any further segments walked through them; a binding holding no object is `SendTargetValueError`, and a name no frame binds is resolved as a feature of the sender or a name in scope as before. A receiver that is any other expression — an index `cars#(2)`, a chain through one, a `new T()` — is evaluated in the sending performance and the message is addressed to the identity of every live object it yields (SysML v2 §7.17.7: the receiver is the value the `to` expression supplies), so a second object of one usage is reached as itself; a plain name naming a usage that occurs once still reaches the occurrence this context holds for it. A message's type is its qualified semantic identity rather than its written name: an accept takes a message whose signal conforms to the type it names — a subtype satisfies a supertype accept — while two same-named definitions in different packages stay distinct) | | Accept action (message consumption suspends the action, and every performance it is a subperformance of: `Transfers::AcceptPerformance` — `succession acceptedTransfer then self.endShot`; `Performances::Performance::subperformances` — `subsets enclosedPerformances, timeEnclosedOccurrences`; derivation under [Accept suspension across call chains and block bodies](#accept-suspension-across-call-chains-and-block-bodies)) | `action_executor.go` stepNestedAction accept case (parks the token as `Token.Wait`), Step (StateWaiting), RunToCompletion, deadlockError/describeWaits (a wait held in a paused call chain is described through it); `action_body_run.go` runBody (a token's body pauses for a message), pauseForMessage; `invoke_action.go` runCallee (the paused callee is kept as `calleeFrame`); `executor_common.go` AcceptWait; `runtime/signal.go` TakeMessage | `action_accept_suspends_until_message.sysml` + trace golden, `action_accept_two_waiters.sysml` + trace golden, `action_accept_nested_call_chain.sysml` + trace golden, `action_accept_nested_chain_clock.sysml` + trace golden, `action_accept_loop_body.sysml` + trace golden, `action_accept_if_branch.sysml` + trace golden, `action_accept_sequential_body.sysml` + trace golden, `action_send_accept.sysml`, `action_accept_message.sysml`, `parse/accept_in_block_bodies.golden`, `signal_test.go:TestAcceptParksTokenUntilMessageArrives`, `:TestParkedAcceptTakesOnlyItsOwnMessage`, `classifier_behavior_test.go:TestPerformedActionParkedInACallChainIsWokenByASibling`, `repl/send_test.go:TestSendWakesAnAcceptParkedInACallChain`, `lower/action_body_test.go:TestAcceptInALoopBodyIsLoweredAsAnAcceptNode`, `robustness_accept_suspension_test.go:TestRuntimeRobustnessAcceptSuspension`, `robustness_test.go:accept_deadlock_never_satisfied`, `:accept_deadlock_reports_every_waiting_accept`, `:accept_statement_deadlock_in_a_loop`, `:send_reaches_only_its_addressee`, `:accept_of_unsent_type`, `:send_via_unconnected_port` | ✅ Faithful (an accept with no message it can take suspends the action at that node — at any depth of synchronously performed actions, and inside loop, branch and sequential bodies — and the whole chain resumes when one arrives, from a parallel branch, another object, `%send` or, for a time trigger, the clock; tool-defined: a run whose every remaining token is parked and that nothing in the run can still post to reports `ErrAcceptDeadlock` naming the innermost accept and its chain rather than hanging) | @@ -837,6 +844,8 @@ remain one-shot. | The action executor agrees with the fUML reference implementation on every fUML test activity that has a SysML v2 spelling and no fUML-only re-firing: the [fUML referee](fuml-referee.md) translates each activity of the pinned `fUML-Tests.uml` by rule into a `fuml::` action definition (an activity parameter node is a nested read or an appending collector of the parameter, an object flow is a `flow` with an enabling `succession` beside it where its target has no control predecessor, a called activity a nested `action n : fuml::Callee` whose same-named parameters are spelled apart, a class a `part def` with its generalizations and its attributes at their declared multiplicity, `CreateObjectAction` a `new ()`, the structural feature actions reads of and `assign`ments to the object's feature that leave what the reference computes, a signal an `attribute def` specializing its generals, `SendSignalAction` a `send new (…) to target` and `AcceptEventAction` an `accept` node whose result pin is the instance received, a class's owned behavior an `action def` nested in its `part def` and its classifier behavior an `action classifierBehavior : ;` member the object starts nothing of at creation, an activity instantiated as an object such a `part def` around its own body, `ReadSelfAction` in an owned behavior `this`, and `StartObjectBehaviorAction` a `perform object.classifierBehavior.start;` — an owned behavior's own row is refereed through the activity that starts an object of its owner), explores its schedules and requires the values left in the `out` parameters to be the reference's, as a multiset where the fUML parameter is unordered; the reference's firing sequence being among the reachable ones is reported, never a verdict. A fUML activity built from constructs SysML v2 has no spelling for (object identity and classifier tests, `ReadExtentAction`, `DestroyObjectAction`, association links, buffer nodes, operation calls and replies, unlimited naturals, `WriteLine`, the exception tests) is `not-expressible`; one the reference re-fires an action in, once per token on a multiplicity-1 pin, is `differs-by-design` (alignment note row A15: a SysML v2 node is one performance that follows all its successions); one the emitter has no rule for yet is `not-expressible` by the emitter, its `class` still `expressible` and the construct named after `not yet translated:` | `tools/referee/fuml/emit.go` `Emit`, `spellParameters`; `tools/referee/fuml/run.go` `Execute` over `runtime.ExploreWith`; `tools/referee/fuml/referee.go` `Referee`; `tools/referee/fuml/baseline.go` `Reproduces`; `tools/cmd/fuml-referee` `-check` | `tools/referee/fuml/emit_test.go`, `referee_test.go`, `tools/cmd/fuml-referee/main_test.go`; `docs/project/fuml-referee-baseline.json` against `docs/project/fuml-referee-expected.json` (`go run -C tools ./cmd/fuml-referee -check`, run in CI over the downloaded suite) | ⚠️ Approximate (23 of the 55 activities pass, none fails, 4 are `differs-by-design`, 28 `not-expressible` — 27 by the classifier and 1 by the emitter, `TestSignalReceiver`, whose `ReadSelfAction` in an activity performed on its own yields the performance, which fUML writes an attribute of as an object; an edge weight other than 1 and an object-flow cycle through control nodes are refusals no activity of the suite reaches — attributed to the emitter, not the executor; no pass is evidence of SysML v2 conformance) | | Step budget enforcement | `context.go` incrementStep; budget configured by `OPENSYSML_MAX_STEPS` (`budget.go` `BudgetsFromEnv`) | `robustness_test.go:testStepBudgetExceeded`, `budget_test.go:TestRaisedBudgetRunsLongerLoop` | ✅ Faithful (the reported limit is the effective one, and names the variable that raises it) | +| An executable typed-action body nested in its own type, directly or through inheritance, is refused at execution because eager lowering cannot unfold a declaration already on its path; feature- and annotation-only bodies remain lazy | `lower/action_subflow.go` `lowerActionNode`, `typedActionTargetIsAncestor`; `lower/errors.go` `ErrRecursiveActionTyping` | `lower/action_inherited_test.go:TestToActionGraphExecutableSelfTypedNodeRefusesRecursiveMerge`, `:TestToActionGraphExecutableMutuallyTypedNodesRefuseRecursiveMerge`; `runtime/robustness_inherited_action_steps_test.go:TestRuntimeRobustnessInheritedActionSteps/recursive_typed_action_body` | ⚠️ Known limitation | + ### Accept suspension across call chains and block bodies What the specification requires, from KerML 1.0 and SysML v2.0 and the Kernel Semantic and @@ -920,7 +929,7 @@ nothing above. | A state machine's own entry, do and exit behaviors (`state def M { entry action { … } then s; … exit action { … } }`) frame its run whether or not it has orthogonal regions of its own: the entry behavior runs when the machine starts, before its entry transitions are tried and the start state entered, the do behavior runs alongside its states, and the exit behavior runs once a completing transition has left its last state (SysML v2 §7.16 `StateDefinition`: a state definition is itself a `StateAction` with `entryAction`, `doAction`, `exitAction`) | `lower/state_graph.go` `StateGraph.Machine`, `machineState` (the graph-only root state, built for every machine); `runtime/state_executor.go` `enterMachine` (from `initialize`), `exitMachine` (from `completeIfDone`) | conformance `state_machine_own_behaviors.sysml` (a machine without regions; on the earlier reading its own entry and exit behaviors were skipped), `state_entry_transition_nested_regions.sysml` (an `entry assign` read by the machine's own guards), `state_parallel_entry_behavior.sysml` (a parallel machine) | ✅ Faithful | | An inline do body is interrupted where a transition out of its state is triggered (§7.18.3: the source state's do action, "if it is still being performed, is interrupted"), in every spelling of the body (`do action { … }`, `do action named { … }`, the braced `do { … }`): a do round runs one statement of the body — a statement of a `for`/`while`/`loop` iteration, of a nested block or of a branch taken is one of its own, one step of a token flow the body states (each of its tokens one node) is one, and a `for`, `while` or `if` node of that flow yields where the same statement in a statement list does, after each iteration and branch statement — then yields, so the pending statements of a body under way are dropped with the behavior when the state is left, and orthogonal regions' inline bodies interleave statement by statement, the order within a round the do round's choice | `runtime/state_statements.go` `doRun` (`startDoRun`, `resume`, `resumable`: a body run with `bodyRun.yields` pauses at the statement boundary after the statement it performed, `bodyPause.yielded`, and is due again in the next round); `runtime/statements.go` `stmtEngine.run`/`loop`/`forLoop`/`blockFlow` (`Context.yieldBody` before the next statement, iteration or node once one performed); `runtime/action_subflow.go` `driveSubflow` (a stated flow yields before the next node a token performs, control nodes and waits aside), `ActionExecutor.yieldedIn`, `Token.drivenUnder`; `runtime/action_body_run.go` `bodyRun.nodesYield`, `yieldsAsStatement`; `state_executor.go` `runDoRound`, `stopDoAction` → `bodyRun.end` (the frames of a body yielded between statements are abandoned as those of one paused on a wait are) | `state_do_body_interrupted_by_signal.sysml` + trace golden (`s1`, the accept, the exit behavior, neither `s2` nor `s3`); `state_concurrent_inline_do_bodies.sysml` + `.expected.json` + trace goldens (a `for` and an `if` written as nodes of two regions' stated do flows, `then` ordering the nodes and the branch statements: 124356 under every fixed policy and the full outcome set under explore and check); `state_do_flow_branch_fork_lost_update.sysml` + `.expected.json` + `.check.expected.json` + trace goldens (an `if` node of a stated do flow forking two leaf bodies: `c` 1 or 2 under explore and check, the witness replayed); `state_anonymous_do_atomic.sysml` + `.expected.json` + trace goldens (one inline action of three statements per region: 124356 in entry order, the same eight values as `state_concurrent_do`, whose braced `do { … }` is the same anonymous action); `state_do_action_successions_first_start.trace.golden` (one flow step a round); `state_terminate_entry_do_exit_behaviors.trace.golden` (`terminate` reached in the round after the statement before it); `robustness_resumable_inline_do_body_test.go` (a `for` body left mid-loop drops its pending iterations with no frame, do work, clock wait or goroutine left behind; 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 with `ErrStepLimitExceeded`; a `for`, `if` or `while` node of a stated flow yields after each iteration or branch statement and is dropped mid-node by the exit) | ✅ Faithful | | A statement of an inline body may perform an action (`entry action { assign c := c + 1; perform Bump; }`), the performed action being lowered as an effect rather than an unsupported usage | `lower/action_graph.go` `lowerStatement` (an action usage naming what it performs → `Effect{EffectPerform}`); `runtime/state_statements.go` `stateStmtHost.effect` | `state_anonymous_body_perform.sysml` conformance | ✅ Faithful | -| A typed do, entry or exit usage whose body declares the pins of the action it performs and nothing else (`do action poll : Poll { inout n = ticks; }`; SysML v2 §7.16: a `StateSubactionMembership` owns an `ActionUsage`, whose body may bind its parameters as any action node's does) performs that action as the one node of the behavior's flow, its `in` and `inout` pins read from the bound features when the performance starts and an `inout` pin written back to its feature when the performance ends — not before, so a performance the state's exit abandons writes nothing back. A behavior that both performs an action and states executable steps of its own is still reported rather than one of the two being chosen silently | `lower/state_behavior.go` `lowerStateBehavior` (a performing usage with `declaresOnlyFeatures` lowers to a one-node `Block` through `lowerBlockFlow`; one with steps to `Unsupported`), `lower/action_graph.go` `lowerFeatures`/`inoutValueBinding` (an `inout` pin valued by a feature name is a `PinBinding` to that feature, `FromValue` marking it as read from the pin's value); `runtime/action_frame.go` `writeOutputs` (an `inout` pin valued by a name the enclosing performance holds no feature of — an enumeration literal, `inout mode = Mode::idle` or an imported `idle` — was initialized by it and writes nothing back; one valued by a feature name that the performance holds writes back to it, and an `out` pin bound to a name nothing holds is still `ErrBindingEnd`); `runtime/state_statements.go` `stateStmtHost.runFlow` (the node performs through the behavior's `ActionExecutor`, its bindings checked as a standalone action's are: `ErrUnboundParameter` names an `in` pin nothing binds, `ErrUnresolvedReference` a pin bound to a feature the state does not declare) | conformance `state_do_action_typed_inout_writes_back.sysml` + trace golden (`ticks` counted once, written back at the performance's end), `state_do_action_typed_inout_cancelled_on_exit.sysml` + trace golden (the exit at 10 s ends a performance paused until 33 s; `ticks` keeps its 5), `state_do_action_typed_inout_valued_by_a_literal.sysml` + `.expected.json` (`inout mode = Mode::idle` beside `inout n = ticks`: `ticks` written back, the literal written nowhere), `robustness_test.go:testStateDoTypedActionInputUnbound`, `:testStateDoTypedActionPinBoundToMissingFeature`, `:testStateDoTypedActionInoutValuedByAnImportedLiteral` (`inout mode = idle` through `import Mode::*`), `:testBehaviorPerformingAnActionAndStatingABody` (the mixed form) | ✅ Faithful for the pin-binding form; ⚠️ Approximate for the mixed form (rejected at execution, not at parse: the pinned `SysML.xtext` reads `entry action mixed : Bump { … }` as a `PerformActionUsage` with a body (`StateActionUsage` → `PerformedActionUsage ActionBody`), and neither the pinned validator nor we report anything on either form, so what is unadjudicated is the *meaning* of executable steps beside a performed action, which the reference cannot execute. Whether an `inout` pin of an abandoned performance writes back is self-assessed from `Performances.kerml`: a binding of a parameter holds for the whole performance, and an abandoned one has no end to hand its value out at) | +| A typed do, entry or exit usage whose body only binds the action's pins (for example, `do action poll : Poll { inout n = ticks; }`; SysML v2 §7.16) performs the action through the existing one-node behavior flow; its inputs read at start and `inout` values write back when the performance ends. A feature/pin-only body is not merged as executable action content | `lower/state_behavior.go` `lowerStateBehavior`/`declaresOnlyFeatures`; `lower/action_graph.go` `lowerFeatures`/`inoutValueBinding`; `runtime/action_frame.go` `writeOutputs`; `runtime/state_statements.go` `stateStmtHost.runFlow` | conformance `state_do_action_typed_inout_writes_back.sysml` + trace golden, `state_do_action_typed_inout_cancelled_on_exit.sysml` + trace golden, `state_do_action_typed_inout_valued_by_a_literal.sysml` + `.expected.json`; `robustness_test.go:testStateDoTypedActionInputUnbound`, `:testStateDoTypedActionPinBoundToMissingFeature`, `:testStateDoTypedActionInoutValuedByAnImportedLiteral` | ✅ Faithful for the pin-binding form; abandoned-write timing is self-assessed from `Performances.kerml` | | Concurrently active states interleave their do behaviors one statement per round — a braced `do { … }` and a `do action { … }` alike, each one inline body; which of the states with an action due acts first in a round is a choice point (KerML `StatePerformances.kerml`: the do behavior is a `middle` performance of its state, ordered after its entry and before its exit, and no succession joins a step of one region's to a step of another's) | `state_executor.go` `runDoRound` (the behaviors with an action due — a next behavior, or a paused one whose wait has ended — each perform one action, in the order `chooseDoAction` draws from the scheduling policy: entry order under `declared` and `reverse`, a draw under `seed:`, every order under `explore`), `chooseDoAction`/`regionOrderChoice` (`choice do round at t=0.0: states lwork, rwork react (unordered; took lwork first)`; one due alone is no choice), `doAction.due` | `state_concurrent_do.sysml` + `.expected.json` (`outcomes`: the four values two rounds of two orders reach, derived in [the semantic oracle](behavior-semantic-oracle.md)) + trace goldens under the default, `declared` and `seed:1`; `state_concurrent_do_action_bodies_timed.sysml` + `.expected.json` + trace goldens (two action bodies each parked at `accept after 2 [s]`, due together at `t=2.0`); `state_do_action_test.go:TestDoBehaviorsOfOrthogonalRegionsInterleave` | ✅ Faithful | | Exiting a state abandons the rest of its do behavior, a behavior paused mid-flow included: its wait leaves the clock, a message sent afterwards wakes nothing, and no node after the wait runs (`StatePerformances.kerml`: `middle` performances end before the state's `exit`) | `state_executor.go` `stopDoAction` (from `exitState`; ends the `doRun` paused on the clock or for a message), `Release` (a machine withdrawn from the clock ends them too, the action a paused body performs included, through `action_body_run.go` `bodyRun.end`; `classifier_behavior.go` `ObjectBehavior.leaveClock` releases so, so a behavior dropped with its object's failed start leaves no paused work on the clock); `state_statements.go` `doRun.end` | `state_do_action_test.go:TestDoBehaviorIsCancelledWhenItsStateIsExited`, conformance `state_do_action_timed_accept_cancelled_on_exit.sysml` + trace golden (the exit at `t=10.0` and nothing at `t=12.0`), `state_do_action_signal_accept_cancelled_on_exit.sysml` + trace golden (a do body parked at an accept for a signal nothing sends), `state_do_action_typed_inout_cancelled_on_exit.sysml` + trace golden, `robustness_test.go:testStateDoBodyNestedAcceptCancelledOnExit` (the accept one node deep; no wait left on the clock, a message after the exit revives nothing), `:clock_advance` (a do behavior paused in a timed action leaves the clock with its dropped machine: after a failed start nothing waits and an advance runs nothing of it) | ✅ Faithful | | A state completes only once its do behavior has finished | `state_executor.go` scheduleCompletionTransitions | `state_do_activity_test.go:TestCompletionWaitsForTheDoBehavior` | ✅ Faithful | @@ -3036,9 +3045,10 @@ the `@type` mapping and the comparison choices. ### Known Limitations (Non-blocking) **Runtime:** -- an entry/do/exit behavior that both performs an action and states a body of its own is - reported at execution rather than at parse time: which of the two SysML means is - unadjudicated, so neither is chosen +- entering a composite state runs its own entry body before the region's initial + transition reaches the substate, so a parent's do body can run before a substate's + entry body (`state_anonymous_action_body.trace.golden`). Pre-existing region-entry + scheduling, not specific to inline bodies - a calc output only a branch that did not run would assign is unassigned for that activation; the body is not statically required to bind every output it declares - a calc whose only computation is rebinding an `inout` in its body, with no `out` and no diff --git a/internal/check/passes/behavior/action_step_multiplicity.go b/internal/check/passes/behavior/action_step_multiplicity.go index fe438aaaff..e98190fc51 100644 --- a/internal/check/passes/behavior/action_step_multiplicity.go +++ b/internal/check/passes/behavior/action_step_multiplicity.go @@ -1,6 +1,8 @@ package behavior import ( + "errors" + "github.com/Open-MBEE/OpenSysML/internal/check/passes/kit" "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" @@ -9,7 +11,10 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/syntax/diag" ) -const actionStepMultiplicitySource = "action-step-multiplicity" +const ( + actionStepMultiplicitySource = "action-step-multiplicity" + actionStepLoweringCode = "action-step-lowering" +) // ActionStepMultiplicityPass warns about action steps the runtime cannot // execute according to their declared multiplicity. @@ -155,6 +160,26 @@ func (c *actionStepMultiplicityChecker) checkAction(decl ast.Node, scope *symbol } graph, err := lower.ToActionGraphWith(decl, scope, c.ctx.Resolver()) if err != nil { + if !errors.Is(err, lower.ErrCyclicSpecialization) && + !errors.Is(err, lower.ErrRedefinedStepMissing) && + !errors.Is(err, lower.ErrIncompatibleRedefinedStep) && + !errors.Is(err, lower.ErrAmbiguousInheritedStep) { + return + } + if c.reported[decl] == nil { + c.reported[decl] = make(map[string]bool) + } + if c.reported[decl][actionStepLoweringCode] { + return + } + c.reported[decl][actionStepLoweringCode] = true + c.diags = append(c.diags, diag.Diagnostic{ + Severity: diag.SeverityError, + Span: decl.Span(), + Message: err.Error(), + Code: actionStepLoweringCode, + Source: actionStepMultiplicitySource, + }) return } c.checkGraph(graph) @@ -226,7 +251,7 @@ func (c *actionStepMultiplicityChecker) checkBlockFlowSteps(block lower.Block) { if c.ctx.DownstreamOfFailure(node) { continue } - if block.Graph.Multiplicities[node] == nil { + if !block.Graph.HasStepMultiplicity(node, c.model) { continue } count, err := block.Graph.StepCount(node, c.model) @@ -254,7 +279,7 @@ func (c *actionStepMultiplicityChecker) checkGraph(graph *lower.ActionGraph) { if c.ctx.DownstreamOfFailure(node) { continue } - if graph.Multiplicities[node] == nil || c.blockFlowSteps[node] { + if !graph.HasStepMultiplicity(node, c.model) || c.blockFlowSteps[node] { continue } if err := graph.CheckStep(node, c.model); err != nil { @@ -287,7 +312,13 @@ func (c *actionStepMultiplicityChecker) report(graph *lower.ActionGraph, err err c.reported[stepErr.Node][stepErr.Code] = true declaration := stepErr.Declaration if declaration == nil && graph != nil { - declaration = graph.Multiplicities[stepErr.Node] + multiplicity := graph.Multiplicities[stepErr.Node] + if multiplicity == nil { + multiplicity, _ = graph.StepMultiplicity(stepErr.Node, c.model) + } + if multiplicity != nil { + declaration = multiplicity + } } span := stepErr.Node.Span() if declaration != nil { diff --git a/internal/check/passes/behavior/action_step_multiplicity_test.go b/internal/check/passes/behavior/action_step_multiplicity_test.go index 89859e36c5..95a0f7c07a 100644 --- a/internal/check/passes/behavior/action_step_multiplicity_test.go +++ b/internal/check/passes/behavior/action_step_multiplicity_test.go @@ -24,6 +24,24 @@ func actionStepMultiplicityDiags(t *testing.T, text string) []diag.Diagnostic { return behavior.ActionStepMultiplicityPass{}.Run(ctx, "t.sysml", root) } +func TestActionStepMultiplicityPassReportsLoweringErrors(t *testing.T) { + model := `package test { + action def Base { + first start then a; + action a; + then done; + } + action def Inc :> Base { attribute :>> a : Integer; } + }` + for _, diagnostic := range actionStepMultiplicityDiags(t, model) { + if diagnostic.Code == "action-step-lowering" && + strings.Contains(diagnostic.Message, "redefining feature is not an action step") { + return + } + } + t.Fatal("diagnostics do not report the inherited action-graph lowering error") +} + func TestActionStepMultiplicityPassReportsRuntimeRefusals(t *testing.T) { tests := []struct { name, code, model, step, multiplicity, reason string @@ -422,6 +440,84 @@ func TestActionStepMultiplicityPassLeavesSupportedStepsAlone(t *testing.T) { } } +func TestActionStepMultiplicityPassUsesInheritedStepMultiplicity(t *testing.T) { + t.Run("non_fixed multiplicity points to base declaration", func(t *testing.T) { + model := `package test { + action def Base { action a[1..3]; } + action def Derived :> Base { action :>> a; } + }` + diags := actionStepMultiplicityDiags(t, model) + want := strings.Index(model, "[1..3]") + found := false + for _, diagnostic := range diags { + if diagnostic.Code == "action-step-multiplicity-not-fixed" && diagnostic.Span.Offset == want { + found = true + if strings.Contains(diagnostic.Message, "inherited from test::Base::a") { + return + } + } + } + t.Fatalf("diagnostics = %+v, want a non-fixed inherited multiplicity diagnostic at offset %d (found=%v)", + diags, want, found) + }) + + t.Run("plain then remains open for inherited repeated step", func(t *testing.T) { + diags := actionStepMultiplicityDiags(t, `package test { + action def Base { + action a[3]; + then action b; + } + action def Derived :> Base { action :>> a; } + }`) + for _, diagnostic := range diags { + if diagnostic.Code == "action-step-order-open" { + return + } + } + t.Fatalf("diagnostics = %+v, want strict plain-then order warning", diags) + }) + + t.Run("inherited repeated step in a loop body is unsupported", func(t *testing.T) { + diags := actionStepMultiplicityDiags(t, `package test { + private import ScalarValues::*; + action def Base { + action worker { + attribute i : Integer = 0; + while i < 1 { + action tick[2]; + assign i := i + 1; + } + } + } + action def Derived :> Base { action :>> worker; } + }`) + for _, diagnostic := range diags { + if diagnostic.Code == "action-step-multiplicity-unsupported" && + strings.Contains(diagnostic.Message, "tick") && + strings.Contains(diagnostic.Message, "[2]") { + return + } + } + t.Fatalf("diagnostics = %+v, want unsupported inherited [2] tick in loop body", diags) + }) + + t.Run("fixed repeated step with explicit first edges remains supported", func(t *testing.T) { + model := `package test { + action def Base { + action p[1]; + action a[3]; + action q[1]; + succession first [1] p then [3] a; + succession first [3] a then [1] q; + } + action def Derived :> Base { action :>> a; } + }` + if diags := actionStepMultiplicityDiags(t, model); len(diags) != 0 { + t.Fatalf("diagnostics = %+v, want none", diags) + } + }) +} + func TestActionStepMultiplicityPassChecksStateBehaviorAndPartPerformance(t *testing.T) { tests := []struct { name, model, step string diff --git a/internal/exec/runtime/action_body_run.go b/internal/exec/runtime/action_body_run.go index 5a6ede2502..a934318407 100644 --- a/internal/exec/runtime/action_body_run.go +++ b/internal/exec/runtime/action_body_run.go @@ -96,6 +96,8 @@ type bodyRun struct { // drives where a step is one move, its machine going on between the moves; // shared only in a flow two of whose moves may touch what another does. steps, shared bool + // holdsClock means a clock wait is the hold's refusal, not a body pause. + holdsClock bool // stepDraws has a seeded run draw whether to pause after a callee's start shot or a // move of its flow where two of its moves may touch what another does. stepDraws bool @@ -833,6 +835,9 @@ func (ctx *Context) yieldedHere() bool { // pauseForClock pauses the body on the stack while wait, a wait on the clock, // goes on; nil where none is on the stack. func (ctx *Context) pauseForClock(wait bodyWait) error { + if ctx.body != nil && ctx.body.holdsClock { + return nil + } return ctx.pauseBody(bodyPause{onWait: true, wait: wait}) } diff --git a/internal/exec/runtime/action_executor.go b/internal/exec/runtime/action_executor.go index 97d8abbe8e..aad232e15d 100644 --- a/internal/exec/runtime/action_executor.go +++ b/internal/exec/runtime/action_executor.go @@ -189,6 +189,16 @@ func newActionExecutorOf( performed, action *symbols.Symbol, self *Instance, occurrence *Instance, +) (*ActionExecutor, error) { + return newActionExecutorOfGraph(ctx, performed, action, self, occurrence, nil) +} + +func newActionExecutorOfGraph( + ctx *Context, + performed, action *symbols.Symbol, + self *Instance, + occurrence *Instance, + graph *lower.ActionGraph, ) (*ActionExecutor, error) { if action.Kind != symbols.SymbolActionUsage && action.Kind != symbols.SymbolActionDef { return nil, fmt.Errorf("symbol %s is not an action", action.Name) @@ -197,15 +207,25 @@ func newActionExecutorOf( return nil, err } - // A usage stating no body of its own performs the body of the action it names — the - // definition typing it — as a classifier behavior binding does; under a tool, none. - action, tool, err := ctx.performanceBody(performed, action) - if err != nil { - return nil, err - } - graph, err := lowerPerformance(action, tool, ctx.Resolver()) - if err != nil { - return nil, err + var tool *toolExecution + if graph == nil { + var err error + action, tool, graph, err = ctx.performanceBody(performed, action) + if err != nil { + return nil, err + } + } else { + held, found, err := ctx.toolPerformance(performed, action) + if err != nil { + return nil, err + } + if found != nil { + graph, err = lower.ToActionInterface(held.Decl, DeclScope(held)) + if err != nil { + return nil, fmt.Errorf("lower action interface: %w", err) + } + action, tool = held, found + } } return newActionExecutorOn(ctx, performed, action, tool, graph, self, occurrence), nil } @@ -246,24 +266,6 @@ func newActionExecutorOn( return exec } -// lowerPerformance lowers what a performance of action runs, in the scope it was written -// in: its token flow, or under a tool only its own interface, the body never running. -func lowerPerformance(action *symbols.Symbol, tool *toolExecution, resolver *resolve.Resolver) (*lower.ActionGraph, error) { - if tool != nil { - graph, err := lower.ToActionInterface(action.Decl, DeclScope(action)) - if err != nil { - return nil, fmt.Errorf("lower action interface: %w", err) - } - return graph, nil - } - graph, err := lower.ToActionGraphWith(action.Decl, DeclScope(action), resolver) - if err != nil { - return nil, fmt.Errorf("lower action graph: %w", err) - } - lower.StartFlow(graph) - return graph, nil -} - // performanceFeatures lists the graph's attributes, then the inherited ones none // redefines with their declaring scope; library-contributed features stay behind the seam. // An attribute valued by none of its own takes the default of the one it redefines. @@ -2689,7 +2691,7 @@ func (e *ActionExecutor) stepNestedAction(tokenIdx int) error { work := &usageWork{exec: e, token: token.ID, perf: perf, graph: graph, usage: usage} if isCaseStep(usage) { work.isCase = true - } else if inv, ok := nestedInvocation(usage); ok { + } else if inv, ok := nestedInvocationInGraph(graph, usage); ok { work.inv, work.performs = inv, true } return e.runBody(tokenIdx, work) @@ -3337,6 +3339,17 @@ func (e *ActionExecutor) Results() map[string]Value { func (e *ActionExecutor) ResultsWithError() (map[string]Value, error) { results := make(map[string]Value, len(e.root.data)) err := e.root.collect("", results) + if e.occurrence != nil { + for _, attr := range e.features { + fv, featureErr := e.occurrence.GetFeatureValue(e.ctx, attr.Name) + if featureErr != nil { + continue + } + if value := fv.HeldValue(); value.Kind != ValInvalid { + results[attr.Name] = value + } + } + } e.collectPartsHeld(results) return results, err } diff --git a/internal/exec/runtime/action_frame.go b/internal/exec/runtime/action_frame.go index 42e4d9d7d8..e16487b429 100644 --- a/internal/exec/runtime/action_frame.go +++ b/internal/exec/runtime/action_frame.go @@ -393,6 +393,16 @@ func (e *performances) beginPerformance( perf.aliases = pins.aliases perf.optional = pins.optional perf.result = pins.result + if usage, ok := node.(*ast.Usage); ok { + if _, merged := mergedTypedSubflowInvocation(flow, usage); merged { + for name, direction := range pins.directions { + if direction == ast.DirOut || direction == ast.DirInOut { + perf.outputs = append(perf.outputs, name) + } + } + sort.Strings(perf.outputs) + } + } // The performance is ongoing before anything seeding it streams, so a value carried // back to its node reaches it rather than waiting for a later performance. if parent.subactions == nil { @@ -443,7 +453,37 @@ func (e *performances) seedPerformance(parent *actionFrame, flow *lower.ActionGr if err := e.bindInputPins(perf, activation); err != nil { return err } - return e.seedDeclaredValues(perf, flow.Features[node], activation) + if err := e.seedDeclaredValues(perf, flow.Features[node], activation); err != nil { + return err + } + return e.checkMergedTypedInputs(perf) +} + +func (e *performances) checkMergedTypedInputs(perf *actionFrame) error { + usage, ok := perf.node.(*ast.Usage) + if !ok { + return nil + } + inv, merged := mergedTypedSubflowInvocation(perf.flow, usage) + if !merged { + return nil + } + callee, err := resolveActionSymbol(e.ctx, nodeScope(perf.flow, usage), inv) + if err != nil { + return err + } + perf.callee = callee + params := e.ctx.actionParametersOf(perf.callee) + inputs := make(map[string]Value) + for _, param := range params { + if param.Direction != ast.DirIn && param.Direction != ast.DirInOut { + continue + } + if value, held := perf.data[perf.key(param.Name)]; held { + inputs[param.Name] = value + } + } + return checkInputsBound(inv, params, inputs) } // bindArguments writes the arguments a node passes its callee (`F(a = 3)`) to its pins, @@ -453,7 +493,10 @@ func (e *performances) bindArguments(perf *actionFrame, activation int64) error if !ok { return nil } - inv, performs := nestedInvocation(usage) + inv, performs := nestedInvocationInGraph(perf.flow, usage) + if !performs { + inv, performs = mergedTypedSubflowInvocation(perf.flow, usage) + } if !performs || inv.expr == nil || lower.IsCaseNode(usage) { return nil } @@ -595,7 +638,10 @@ func (e *performances) nodePins(graph *lower.ActionGraph, node ast.Node) (nodePi if !ok { return nodePins{directions: make(map[string]ast.FeatureDirection)}, nil } - inv, performs := nestedInvocation(usage) + inv, performs := nestedInvocationInGraph(graph, usage) + if !performs { + inv, performs = mergedTypedSubflowInvocation(graph, usage) + } if !performs || lower.IsCaseNode(usage) { return e.pinsOf(graph, node, inv, nil, nil) } @@ -634,7 +680,7 @@ func (e *performances) pinsOf( inv.step, _ = stepSymbol(graph, node) } for _, callee := range callees { - held, _, err := e.ctx.performanceBody(inv.performed(callee), callee) + held, _, _, err := e.ctx.performanceBody(inv.performed(callee), callee) if err != nil { return nodePins{}, err } @@ -804,25 +850,25 @@ func (f *actionFrame) subaction(name string, decl ast.Node) (perf *actionFrame, func (f *actionFrame) unsupportedRepeatedRead(node ast.Node) error { var graph *lower.ActionGraph multiplicities := f.multiplicities + var model = (*semantics.Model)(nil) + if f.perfs != nil && f.perfs.ctx != nil { + model = f.perfs.ctx.Semantics() + } for _, candidate := range []*lower.ActionGraph{f.graph, f.flow} { if candidate == nil { continue } - if _, declared := candidate.Multiplicities[node]; declared { + if candidate.HasStepMultiplicity(node, model) { graph = candidate multiplicities = candidate.Multiplicities break } } - if _, declared := multiplicities[node]; !declared { - return nil - } if graph == nil { graph = &lower.ActionGraph{Multiplicities: multiplicities} } - var model = (*semantics.Model)(nil) - if f.perfs != nil && f.perfs.ctx != nil { - model = f.perfs.ctx.Semantics() + if !graph.HasStepMultiplicity(node, model) { + return nil } count, err := graph.StepCount(node, model) if err != nil { @@ -1394,6 +1440,9 @@ func (f *actionFrame) heldFeatures(prefix string, into map[string]bool) { into[prefix+name] = true } for name, sub := range f.latestSubactions() { + if sub.repetition > 0 { + continue + } sub.heldFeatures(prefix+name+".", into) } } @@ -1557,6 +1606,12 @@ func (e *performances) writeNamedEnd(end boundEnd, value Value) error { if err != nil { return err } + if !written && end.FromValue { + written, err = assignPerformerFeature(e.ctx, e.self, end.Scope, name, value) + if err != nil { + return err + } + } if !written && !end.FromValue { return fmt.Errorf("%w: %s is bound to %s, which no enclosing action holds", ErrBindingEnd, end.pinText(), name) diff --git a/internal/exec/runtime/action_statements.go b/internal/exec/runtime/action_statements.go index 9286468944..ad7da9c3eb 100644 --- a/internal/exec/runtime/action_statements.go +++ b/internal/exec/runtime/action_statements.go @@ -431,7 +431,7 @@ func (e *performances) performNodeBody(f *performFrame, graph *lower.ActionGraph return e.performCase(perf) } if f.phase == performInvoking { - if inv, ok := nestedInvocation(node); ok { + if inv, ok := nestedInvocationInGraph(graph, node); ok { if err := e.performInvocation(perf, inv); err != nil { return err } diff --git a/internal/exec/runtime/action_step_multiplicity.go b/internal/exec/runtime/action_step_multiplicity.go index da5bc84fc3..272ac57ee1 100644 --- a/internal/exec/runtime/action_step_multiplicity.go +++ b/internal/exec/runtime/action_step_multiplicity.go @@ -29,7 +29,7 @@ func (e *ActionExecutor) stepMultiplicity(graph *lower.ActionGraph, node ast.Nod if graph == nil { return 1, nil } - if _, declared := graph.Multiplicities[node]; !declared { + if !graph.HasStepMultiplicity(node, e.ctx.Semantics()) { return 1, nil } if e.stepCounts == nil { diff --git a/internal/exec/runtime/calc_statements.go b/internal/exec/runtime/calc_statements.go index e354f8888c..0ce9021841 100644 --- a/internal/exec/runtime/calc_statements.go +++ b/internal/exec/runtime/calc_statements.go @@ -325,7 +325,7 @@ func (h *calcStmtHost) performNode(engine *stmtEngine, graph *lower.ActionGraph, if h.perfs != nil { return h.perfs.performNode(h.perfs.root, engine, graph, node) } - if _, performs := nestedInvocation(node); performs { + if _, performs := nestedInvocationInGraph(graph, node); performs { return flowNext, fmt.Errorf("%w: a calculation cannot perform action %s", ErrCalcSideEffect, ActionNodeName(node)) } if connectsPins(graph, node) { diff --git a/internal/exec/runtime/check.go b/internal/exec/runtime/check.go index 68fafc4cd4..de35bf7c99 100644 --- a/internal/exec/runtime/check.go +++ b/internal/exec/runtime/check.go @@ -1366,7 +1366,7 @@ func (c *checker) performedBy(graph *lower.ActionGraph, node ast.Node) (settled if !ok { return nil, nil, false } - inv, performs := nestedInvocation(usage) + inv, performs := nestedInvocationInGraph(graph, usage) if !performs || lower.IsCaseNode(usage) { return nil, nil, false } @@ -1383,13 +1383,13 @@ func (c *checker) performedBy(graph *lower.ActionGraph, node ast.Node) (settled // calleeFlow lowers what node's performance of callee runs, as the executor will. func (c *checker) calleeFlow(graph *lower.ActionGraph, node ast.Node, callee *symbols.Symbol) (*lower.ActionGraph, error) { usage := node.(*ast.Usage) - inv, _ := nestedInvocation(usage) + inv, _ := nestedInvocationInGraph(graph, usage) inv.step, _ = stepSymbol(graph, node) - body, tool, err := c.ctx.performanceBody(inv.performed(callee), callee) + _, _, flow, err := c.ctx.performanceBody(inv.performed(callee), callee) if err != nil { return nil, err } - return lowerPerformance(body, tool, c.ctx.Resolver()) + return flow, nil } // divergeReached fails the check when a path only a performance could tell was held diff --git a/internal/exec/runtime/classifier_behavior.go b/internal/exec/runtime/classifier_behavior.go index c0ccc601b5..974e4d16f7 100644 --- a/internal/exec/runtime/classifier_behavior.go +++ b/internal/exec/runtime/classifier_behavior.go @@ -1078,21 +1078,26 @@ func (b *ObjectBehavior) hasPendingWork() bool { // type binds, seeded with the values the binding declaration supplies, and // initializes it so its start is reported where every other behavior's is. func (ctx *Context) attachClassifierBehavior(inst *Instance, decl classifierBehaviorDecl) (*ObjectBehavior, error) { - if usage := decl.behavior.Decl; lower.IsPerformedActionUsage(usage) && usage.Multiplicity != nil { + if usage := decl.behavior.Decl; lower.IsPerformedActionUsage(usage) { scope := decl.member.OwnerScope graph := &lower.ActionGraph{ Scope: scope, - Multiplicities: map[ast.Node]*ast.Multiplicity{usage: usage.Multiplicity}, + Multiplicities: map[ast.Node]*ast.Multiplicity{}, Scopes: map[ast.Node]*symbols.Scope{usage: scope}, } - count, err := graph.StepCount(usage, ctx.Semantics()) - if err != nil { - return nil, fmt.Errorf("%w: %w", ErrActionStepMultiplicity, err) + if usage.Multiplicity != nil { + graph.Multiplicities[usage] = usage.Multiplicity } - if count != 1 { - return nil, fmt.Errorf("%w: %w", ErrActionStepMultiplicity, graph.StepError( - usage, ctx.Semantics(), lower.StepMultiplicityUnsupportedCode, - "part-level performed actions cannot execute with multiplicity other than [1]", nil)) + if graph.HasStepMultiplicity(usage, ctx.Semantics()) { + count, err := graph.StepCount(usage, ctx.Semantics()) + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrActionStepMultiplicity, err) + } + if count != 1 { + return nil, fmt.Errorf("%w: %w", ErrActionStepMultiplicity, graph.StepError( + usage, ctx.Semantics(), lower.StepMultiplicityUnsupportedCode, + "part-level performed actions cannot execute with multiplicity other than [1]", nil)) + } } } behavior, occurrence, err := ctx.bindClassifierBehavior(inst, decl) @@ -1121,7 +1126,11 @@ func (ctx *Context) attachClassifierBehavior(inst *Instance, decl classifierBeha } behavior.State = exec case lower.PerformedAction: - exec, err := newActionExecutorOf(ctx, decl.member, sym, inst, occurrence) + var graph *lower.ActionGraph + if decl.behavior.StatesBody && decl.behavior.NamesBehavior { + graph = lower.ToActionNodeFlow(decl.member.Decl, DeclScope(decl.member), ctx.Resolver()) + } + exec, err := newActionExecutorOfGraph(ctx, decl.member, sym, inst, occurrence, graph) if err != nil { return nil, fmt.Errorf("performed action %s of %s: %w", decl.behavior.Name, symbolText(inst.Type), err) } @@ -1358,29 +1367,32 @@ func (ctx *Context) argumentParameter(scope *symbols.Scope, arg lower.Attribute) return arg.Name } -// actionBodySymbol resolves the element holding the body an action symbol -// performs: itself when it states one, otherwise the action it names — the -// definition typing it, or the feature it refers to. The symbol itself is -// returned when nothing it names states a body, so the missing flow is reported -// against the declaration that was asked for. -func (ctx *Context) actionBodySymbol(action *symbols.Symbol) *symbols.Symbol { +// actionBodyGraph selects the lowered flow a performance of action executes. +func (ctx *Context) actionBodyGraph(action *symbols.Symbol) (*symbols.Symbol, *lower.ActionGraph, error) { sym := action + var fallback *lower.ActionGraph for depth := 0; depth < maxBehaviorBindingDepth; depth++ { - if statesBehaviorBody(sym) { - return sym + graph, err := lower.ToActionGraphWith(sym.Decl, DeclScope(sym), ctx.Resolver()) + if err != nil { + return nil, nil, fmt.Errorf("lower action graph: %w", err) + } + lower.StartFlow(graph) + if depth == 0 { + fallback = graph + } + if len(graph.Nodes) > 0 || len(graph.Bodies) > 0 { + return sym, graph, nil } next := ctx.namedBehavior(sym) if next == nil || next == sym || (next.Kind != symbols.SymbolActionUsage && next.Kind != symbols.SymbolActionDef) { - return action + return action, fallback, nil } sym = next } - return action + return action, fallback, nil } -// statesBehaviorBody reports whether a symbol's declaration states a behavior -// body of its own rather than naming an element that holds one. func statesBehaviorBody(sym *symbols.Symbol) bool { if sym == nil || sym.Decl == nil { return false diff --git a/internal/exec/runtime/condition.go b/internal/exec/runtime/condition.go index e73bff1ae6..b3c28dabdd 100644 --- a/internal/exec/runtime/condition.go +++ b/internal/exec/runtime/condition.go @@ -425,7 +425,7 @@ func (ctx *Context) constraintBodyStep(node ast.Node, scope *symbols.Scope) (low if stmt, ok := ctx.model.constraintSteps[node]; ok { return stmt, true } - stmt, ok := lower.ConstraintStep(node, scope) + stmt, ok := lower.ConstraintStep(node, scope, ctx.model.Resolver()) if !ok { return nil, false } diff --git a/internal/exec/runtime/held_image_behavior.go b/internal/exec/runtime/held_image_behavior.go index 561be5b515..f5ff829973 100644 --- a/internal/exec/runtime/held_image_behavior.go +++ b/internal/exec/runtime/held_image_behavior.go @@ -559,7 +559,7 @@ func (m *materializing) behavior(b imagedBehavior) error { } behavior.State = exec case b.action != nil: - action, tool, err := dst.performanceBody(decl.member, behavior.Symbol) + action, tool, _, err := dst.performanceBody(decl.member, behavior.Symbol) if err != nil { return err } diff --git a/internal/exec/runtime/inherited_action_step_analysis_test.go b/internal/exec/runtime/inherited_action_step_analysis_test.go new file mode 100644 index 0000000000..dc4ddba6a5 --- /dev/null +++ b/internal/exec/runtime/inherited_action_step_analysis_test.go @@ -0,0 +1,48 @@ +package runtime + +import ( + "os" + "path/filepath" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/check/passes" + "github.com/Open-MBEE/OpenSysML/internal/syntax/diag" + "github.com/Open-MBEE/OpenSysML/internal/syntax/parser" + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" + "github.com/Open-MBEE/OpenSysML/internal/workspace/libs" +) + +func TestInheritedActionStepFixturesAnalyseCleanly(t *testing.T) { + paths, err := filepath.Glob(filepath.Join("testdata", "conformance", "inherited_action_steps_*.sysml")) + if err != nil { + t.Fatal(err) + } + if len(paths) == 0 { + t.Fatal("no inherited action step fixtures found") + } + paths = append(paths, filepath.Join("testdata", "conformance", "action_invoked_node_body_writes_output.sysml")) + + for _, path := range paths { + path := path + t.Run(filepath.Base(path), func(t *testing.T) { + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + sf := source.New(path, data) + p := parser.New(sf) + root := p.ParseFile() + + idx := libs.NewModelIndex() + idx.AddDocument(sf.Name(), root) + idx.ExpandWildcardImports() + + diags := passes.Analyze(sf.Name(), root, parser.AsDiagnostics(p.Diagnostics, p.Warnings), idx) + for _, d := range diags { + if d.Severity == diag.SeverityError { + t.Errorf("%s: %s", d.Code, d.Message) + } + } + }) + } +} diff --git a/internal/exec/runtime/invoke_action.go b/internal/exec/runtime/invoke_action.go index 40bdad65ae..afdb543aa4 100644 --- a/internal/exec/runtime/invoke_action.go +++ b/internal/exec/runtime/invoke_action.go @@ -39,6 +39,9 @@ type actionInvocation struct { // step is the usage declaring the invocation, whose metadata and parameters // bind the performance; nil for an invocation no usage of the body declares. step *symbols.Symbol + // scope is the declaration scope of an invocation inherited through step + // redefinition; nil uses the owning graph's node scope. + scope *symbols.Scope } // performed is what the invocation performs: the step declaring it, else the callee itself. @@ -90,6 +93,35 @@ func nestedInvocation(usage *ast.Usage) (actionInvocation, bool) { return actionInvocation{}, false } +func nestedInvocationInGraph(graph *lower.ActionGraph, usage *ast.Usage) (actionInvocation, bool) { + if graph != nil { + if _, merged := graph.MergedTypedSubflows[usage]; merged { + return actionInvocation{}, false + } + } + if invocation, ok := nestedInvocation(usage); ok { + return invocation, true + } + if graph == nil { + return actionInvocation{}, false + } + if performed, ok := graph.Performs[usage]; ok && performed.Target != nil { + return actionInvocation{target: performed.Target, scope: performed.Scope}, true + } + return actionInvocation{}, false +} + +func mergedTypedSubflowInvocation(graph *lower.ActionGraph, usage *ast.Usage) (actionInvocation, bool) { + if graph == nil { + return actionInvocation{}, false + } + typed, ok := graph.MergedTypedSubflows[usage] + if !ok || typed.Target == nil { + return actionInvocation{}, false + } + return actionInvocation{target: typed.Target, scope: typed.Scope}, true +} + // referencedInvocation reports the action a usage's reference subsetting performs. func referencedInvocation(usage *ast.Usage) (actionInvocation, bool) { for _, rel := range usage.Relationships { @@ -430,6 +462,9 @@ func actionCandidates( scope *symbols.Scope, inv actionInvocation, ) (*symbols.Symbol, []*symbols.Symbol, error) { + if inv.scope != nil { + scope = inv.scope + } target := inv.target if target == nil || len(target.Parts) == 0 { return nil, nil, fmt.Errorf("empty action reference") diff --git a/internal/exec/runtime/robustness_inherited_action_steps_test.go b/internal/exec/runtime/robustness_inherited_action_steps_test.go new file mode 100644 index 0000000000..bfc7b3389b --- /dev/null +++ b/internal/exec/runtime/robustness_inherited_action_steps_test.go @@ -0,0 +1,636 @@ +package runtime + +import ( + "errors" + "reflect" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +func TestRuntimeRobustnessInheritedActionSteps(t *testing.T) { + t.Run("body_stating_typed_usage_of_general", func(t *testing.T) { + _, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def Base { + attribute c : Integer = 0; + } + action def P :> Base { + attribute d : Integer = 0; + first start then x; + action x : Base { + assign c := c + 1; + } + then done; + } + }`, "P") + if err != nil { + t.Fatalf("ExecuteAction: %v", err) + } + }) + + t.Run("typed_usage_bodyless_inherited_pin_binding", func(t *testing.T) { + outputs, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def T { + in source : Integer = 1; + out y : Integer = source; + } + action def Bodyless { + out attribute observed : Integer = 0; + action m : T; + action read { assign observed := m.y; } + first start then m; + first m then read; + first read then done; + } + }`, "Bodyless") + if err != nil { + t.Fatalf("ExecuteAction(Bodyless): %v", err) + } + assertIntOutput(t, outputs, "observed", 1) + assertIntOutput(t, outputs, "m.y", 1) + }) + + t.Run("typed_usage_body_preserves_inherited_pin_binding", func(t *testing.T) { + outputs, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def T { + in source : Integer = 1; + out y : Integer = source; + } + action def WithBody { + out attribute observed : Integer = 0; + action m : T { assign source := 2; } + action read { assign observed := m.y; } + first start then m; + first m then read; + first read then done; + } + }`, "WithBody") + if err != nil { + t.Fatalf("ExecuteAction(WithBody): %v", err) + } + assertIntOutput(t, outputs, "m.source", 2) + assertIntOutput(t, outputs, "m.y", 2) + assertIntOutput(t, outputs, "observed", 2) + }) + + t.Run("inherited_succession_plain_restatement_is_refused", func(t *testing.T) { + _, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def Base { + out attribute c : Integer = 0; + action p; + action a[3] { assign c := c + 1; } + succession first [1] p then [3] a; + } + action def Derived :> Base { + succession first p then a; + } + }`, "Derived") + if !errors.Is(err, ErrActionStepMultiplicity) { + t.Fatalf("ExecuteAction(Derived) error = %v, want ErrActionStepMultiplicity", err) + } + var stepErr *lower.StepMultiplicityError + if !errors.As(err, &stepErr) || stepErr.Code != lower.StepOrderUnsatisfiableCode { + t.Fatalf("ExecuteAction(Derived) error = %v, want %s", err, lower.StepOrderUnsatisfiableCode) + } + }) + + t.Run("inherited_succession_matching_restatement_runs_repeated_step", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + action def Base { + out attribute c : Integer = 0; + action p; + action a[3] { assign c := c + 1; } + succession first [1] p then [3] a; + } + action def Derived :> Base { + succession first [1] p then [3] a; + } + }` + for _, action := range []string{"Base", "Derived"} { + outputs, err := executeInheritedAction(t, src, action) + if err != nil { + t.Fatalf("ExecuteAction(%s): %v", action, err) + } + assertIntOutput(t, outputs, "c", 3) + } + }) + + t.Run("composite_redefinition_keeps_inherited_statement_and_owned_subaction", func(t *testing.T) { + outputs, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def Base { + out attribute c : Integer = 0; + action a { assign c := c + 1; } + } + action def Derived :> Base { + action a :>> a { + action b { assign c := c + 10; } + } + } + }`, "Derived") + if err != nil { + t.Fatalf("ExecuteAction(Derived): %v", err) + } + assertIntOutput(t, outputs, "c", 11) + }) + + t.Run("composite_redefinition_keeps_inherited_subaction_and_owned_statement", func(t *testing.T) { + outputs, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def Base { + out attribute c : Integer = 0; + action a { + action b { assign c := c + 10; } + } + } + action def Derived :> Base { + action a :>> a { assign c := c + 1; } + } + }`, "Derived") + if err != nil { + t.Fatalf("ExecuteAction(Derived): %v", err) + } + assertIntOutput(t, outputs, "c", 11) + }) + + t.Run("specialized_body_binding_replaces_inherited_binding", func(t *testing.T) { + outputs, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def Adder { + in a : Integer; + in b : Integer; + out sum : Integer; + first step; + action step { assign sum := a + b; } + } + action def Base { + attribute x : Integer = 5; + out result : Integer = 0; + action add : Adder { in b = 2; } + bind add.a = x; + flow add.sum to fin.n; + action fin { in n : Integer; assign result := n; } + } + action def Redefined :> Base { + action add :>> add : Adder { in b = 100; } + first start then add; + succession add then fin; + then done; + } + }`, "Redefined") + if err != nil { + t.Fatalf("ExecuteAction(Redefined): %v", err) + } + assertIntOutput(t, outputs, "add.a", 5) + assertIntOutput(t, outputs, "add.b", 100) + assertIntOutput(t, outputs, "add.sum", 105) + assertIntOutput(t, outputs, "fin.n", 105) + assertIntOutput(t, outputs, "result", 105) + }) + + t.Run("terminate_usage_body_flow_is_inherited", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + action def G { + out attribute x : Integer = 0; + out attribute later : Integer = 0; + first start then stop; + action stop terminate { + first start; + then action inner { assign x := 1; } + then done; + } + then action tail { assign later := 1; } + then done; + } + action def S :> G; + }` + for _, name := range []string{"G", "S"} { + t.Run(name, func(t *testing.T) { + outputs, err := executeInheritedAction(t, src, name) + if err != nil { + t.Fatalf("ExecuteAction(%s): %v", name, err) + } + assertIntOutput(t, outputs, "x", 1) + assertIntOutput(t, outputs, "later", 0) + }) + } + }) + + t.Run("inherited_case_keeps_case_step_ordering", func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + action def G { + out attribute r : Integer = 0; + analysis nested { + return : Integer; + attribute x : Integer := 1; + action multiply { assign x := x * 10; } + action add { assign x := x + 2; } + x + } + first start then nested; + first nested then read; + action read { assign r := nested.result; } + first read then done; + } + action def S :> G; + }` + for _, name := range []string{"G", "S"} { + t.Run(name, func(t *testing.T) { + outputs, err := executeInheritedAction(t, src, name) + if err != nil { + t.Fatalf("ExecuteAction(%s): %v", name, err) + } + assertIntOutput(t, outputs, "r", 12) + }) + } + }) + + t.Run("specialization_flow_connects_inherited_steps", func(t *testing.T) { + outputs, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def G { + action a { out y : Integer; assign y := 7; } + action b { in v : Integer; } + first start then a; + first a then b; + first b then done; + } + action def S :> G { + flow f2 from a.y to b.v; + } + }`, "S") + if err != nil { + t.Fatalf("ExecuteAction(S): %v", err) + } + if got, ok := outputs["b.v"]; !ok || got.Kind != ValConst || got.Const.Int != 7 { + t.Fatalf("S b.v = %v, want 7", got) + } + }) + + t.Run("inherited_gate_resolves_same_named_flow_in_declaring_scope", func(t *testing.T) { + for _, tc := range []struct { + guard string + wantB bool + }{ + {guard: "false"}, + {guard: "true", wantB: true}, + } { + t.Run(tc.guard, func(t *testing.T) { + outputs, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def G { + attribute go : Boolean = `+tc.guard+`; + action a { out y : Integer; assign y := 7; } + action b { in v : Integer; } + first start then a; + first a if go then f; + succession flow f of Integer from a.y to b.v; + } + action def S :> G { + action c { in v : Integer; } + succession flow f of Integer from a.y to c.v; + } + }`, "S") + if err != nil { + t.Fatalf("ExecuteAction(S): %v", err) + } + if got, ok := outputs["c.v"]; !ok || got.Kind != ValConst || got.Const.Int != 7 { + t.Fatalf("S c.v = %v, want 7", got) + } + b, hasB := outputs["b.v"] + if tc.wantB { + if !hasB || b.Kind != ValConst || b.Const.Int != 7 { + t.Fatalf("S b.v = %v, want 7", b) + } + } else if hasB { + t.Fatalf("S b.v = %v, want no value from a.y", b) + } + }) + } + }) + + t.Run("inherited_gated_succession_flow", func(t *testing.T) { + for _, tc := range []struct { + guard string + value int64 + }{ + {guard: "true", value: 1}, + {guard: "false", value: 2}, + } { + t.Run(tc.guard, func(t *testing.T) { + src := `package test { + private import ScalarValues::*; + action def G { + attribute go : Boolean = ` + tc.guard + `; + first start then a; + action a { out y : Integer; assign y := 1; } + action c { out y : Integer; assign y := 2; } + action b { in v : Integer; } + first a if go then f; + first a if not go then c; + succession flow f of Integer from a.y to b.v; + succession flow of Integer from c.y to b.v; + } + action def S :> G; + }` + general, err := executeInheritedAction(t, src, "G") + if err != nil { + t.Fatalf("ExecuteAction(G): %v", err) + } + specialized, err := executeInheritedAction(t, src, "S") + if err != nil { + t.Fatalf("ExecuteAction(S): %v", err) + } + if !reflect.DeepEqual(specialized, general) { + t.Fatalf("ExecuteAction(S) outputs = %v, want G outputs %v", specialized, general) + } + got, ok := specialized["b.v"] + if !ok || got.Kind != ValConst || got.Const.Int != tc.value { + t.Fatalf("S b.v = %v, want %d", got, tc.value) + } + }) + } + }) + + t.Run("inherited_guard_survives_differently_guarded_owned_succession", func(t *testing.T) { + outputs, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def Base { + attribute x : Integer = 1; + out attribute c : Integer = 0; + action a; + action b { assign c := 1; } + first a if x < 3 then b; + } + action def S :> Base { + first a if x > 5 then b; + } + }`, "S") + if err != nil { + t.Fatalf("ExecuteAction(S): %v", err) + } + assertIntOutput(t, outputs, "c", 1) + }) + + t.Run("unsequenced_inherited_assertion_stays_unchecked", func(t *testing.T) { + src := `package test { + action def G { assert constraint check { false } } + action def S :> G; + }` + for _, name := range []string{"G", "S"} { + t.Run(name, func(t *testing.T) { + if _, err := executeInheritedAction(t, src, name); err != nil { + t.Fatalf("ExecuteAction(%s): %v, want an unsequenced assertion to remain unchecked", name, err) + } + }) + } + }) + + t.Run("inherited_assertion_sequenced_by_specialization", func(t *testing.T) { + _, err := executeInheritedAction(t, `package test { + action def G { assert constraint check { false } } + action def S :> G { + first start then check; + first check then done; + } + }`, "S") + var violation *ViolationError + if !errors.As(err, &violation) || violation.Element != "check" || !errors.Is(err, ErrViolated) { + t.Fatalf("ExecuteAction error = %v, want inherited assertion check violated", err) + } + }) + + t.Run("assertion_ordered_by_intermediate_general", func(t *testing.T) { + _, err := executeInheritedAction(t, `package test { + action def G { assert constraint check { true } } + action def M :> G { + first start then check; + first check then done; + } + action def S :> M; + }`, "S") + if err != nil { + t.Fatalf("ExecuteAction(S): %v, want the inherited assertion flow to complete", err) + } + }) + + t.Run("intermediate_general_assertion_violation", func(t *testing.T) { + src := `package test { + action def G { assert constraint check { false } } + action def M :> G { + first start then check; + first check then done; + } + action def S :> M; + }` + for _, name := range []string{"M", "S"} { + t.Run(name, func(t *testing.T) { + _, err := executeInheritedAction(t, src, name) + var violation *ViolationError + if !errors.As(err, &violation) || violation.Element != "check" || !errors.Is(err, ErrViolated) { + t.Fatalf("ExecuteAction(%s) error = %v, want assertion check violated", name, err) + } + }) + } + }) + + t.Run("inherited_sequenced_assertion_violation", func(t *testing.T) { + outputs, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def G { + attribute level : Integer = 0; + action raise { assign level := 3; } + assert constraint raised { level == 4 } + action lower { assign level := 1; } + first start then raise; + first raise then raised; + first raised then lower; + first lower then done; + } + action def S :> G; + }`, "S") + var violation *ViolationError + if !errors.As(err, &violation) || violation.Element != "raised" || + violation.Condition != "level == 4" || !errors.Is(err, ErrViolated) { + t.Fatalf("ExecuteAction error = %v, want inherited assertion raised violated", err) + } + if outputs != nil { + t.Fatalf("outputs = %v, want none from a failed run", outputs) + } + }) + + t.Run("recursive_typed_action_body", func(t *testing.T) { + _, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def A { + attribute c : Integer = 0; + first start then x; + action x : A { + assign c := c + 1; + } + then done; + } + }`, "A") + if !errors.Is(err, lower.ErrRecursiveActionTyping) { + t.Fatalf("ExecuteAction error = %v, want ErrRecursiveActionTyping", err) + } + }) + + t.Run("cyclic_specialization", func(t *testing.T) { + _, err := executeInheritedAction(t, `package test { + action def A :> B; + action def B :> A; + }`, "A") + if !errors.Is(err, lower.ErrCyclicSpecialization) { + t.Fatalf("ExecuteAction error = %v, want ErrCyclicSpecialization", err) + } + }) + + t.Run("redefinition_naming_missing_step", func(t *testing.T) { + _, err := executeInheritedAction(t, `package test { + action def Base { action a; } + action def Bad :> Base { action :>> nosuch; } + }`, "Bad") + if !errors.Is(err, lower.ErrRedefinedStepMissing) { + t.Fatalf("ExecuteAction error = %v, want ErrRedefinedStepMissing", err) + } + }) + + t.Run("inherited_succession_to_incompatible_replacement", func(t *testing.T) { + _, err := executeInheritedAction(t, `package test { + action def Base { + first start then a; + action a; + then done; + } + action def Inc :> Base { attribute :>> a : Integer; } + }`, "Inc") + if !errors.Is(err, lower.ErrIncompatibleRedefinedStep) { + t.Fatalf("ExecuteAction error = %v, want ErrIncompatibleRedefinedStep", err) + } + }) + + t.Run("entry_body_redefines_missing_performed_step", func(t *testing.T) { + err := stateRunErrorForSource(t, "Machine", `package test { + action def Bump; + state def Machine { + entry; then active; + state active { + entry action run : Bump { action :>> missing; } + } + } + }`) + if !errors.Is(err, lower.ErrRedefinedStepMissing) { + t.Fatalf("state execution error = %v, want ErrRedefinedStepMissing", err) + } + }) + + t.Run("entry_body_redefines_step_incompatibly", func(t *testing.T) { + err := stateRunErrorForSource(t, "Machine", `package test { + private import ScalarValues::*; + action def BumpBase { + first start then a; + action a; + then done; + } + action def Bump :> BumpBase; + state def Machine { + entry; then active; + state active { + entry action run : Bump { + attribute :>> a : Integer; + } + } + } + }`) + if !errors.Is(err, lower.ErrIncompatibleRedefinedStep) { + t.Fatalf("state execution error = %v, want ErrIncompatibleRedefinedStep", err) + } + }) + + t.Run("typed_entry_usage_specializes_back_to_itself", func(t *testing.T) { + err := stateRunErrorForSource(t, "Machine", `package test { + state def Machine { + attribute hits : Integer = 0; + entry; then active; + state active { + action def A :> run; + entry action run : A { assign hits := hits + 1; } + } + } + }`) + if !errors.Is(err, lower.ErrCyclicSpecialization) { + t.Fatalf("state execution error = %v, want ErrCyclicSpecialization", err) + } + }) + + t.Run("ambiguous_inherited_redefinitions", func(t *testing.T) { + _, err := executeInheritedAction(t, `package test { + action def Base { action a; } + action def Left :> Base { action :>> a; } + action def Right :> Base { action :>> a; } + action def Diamond :> Left, Right; + }`, "Diamond") + if !errors.Is(err, lower.ErrAmbiguousInheritedStep) { + t.Fatalf("ExecuteAction error = %v, want ErrAmbiguousInheritedStep", err) + } + }) + + t.Run("diamond_performs_once", func(t *testing.T) { + outputs, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def Base { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + action def Left :> Base { action l { assign c := c + 10; } } + action def Right :> Base { action r { assign c := c + 100; } } + action def Diamond :> Left, Right; + }`, "Diamond") + if err != nil { + t.Fatalf("ExecuteAction: %v", err) + } + if got := outputs["c"]; got.Kind != ValConst || got.Const.Int != 111 { + t.Fatalf("c = %v, want 111", got) + } + }) + + t.Run("narrow_zero", func(t *testing.T) { + outputs, err := executeInheritedAction(t, `package test { + private import ScalarValues::*; + action def Base { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + action def Narrow :> Base { action :>> a[0]; } + }`, "Narrow") + if err != nil { + t.Fatalf("ExecuteAction: %v", err) + } + if got := outputs["c"]; got.Kind != ValConst || got.Const.Int != 0 { + t.Fatalf("c = %v, want 0", got) + } + }) +} + +func executeInheritedAction(t *testing.T, src, name string) (map[string]Value, error) { + t.Helper() + idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, src)) + sym := findSymbolByName(idx.DocumentRoot(""), name, ast.DefAction) + if sym == nil { + t.Fatalf("action %s not found", name) + } + return ctx.ExecuteAction(sym) +} diff --git a/internal/exec/runtime/robustness_stepped_state_clock_wait_test.go b/internal/exec/runtime/robustness_stepped_state_clock_wait_test.go new file mode 100644 index 0000000000..d605f06c66 --- /dev/null +++ b/internal/exec/runtime/robustness_stepped_state_clock_wait_test.go @@ -0,0 +1,75 @@ +package runtime + +import ( + "context" + "errors" + "fmt" + "strings" + "testing" +) + +func TestRuntimeRobustnessSteppedStateBehaviorClockWait(t *testing.T) { + for _, behavior := range []string{"entry", "exit"} { + t.Run(behavior, func(t *testing.T) { + testSteppedStateBehaviorClockWait(t, behavior) + }) + } +} + +func testSteppedStateBehaviorClockWait(t *testing.T, behavior string) { + src := fmt.Sprintf(`package test { + private import SI::*; + state Machine { + entry; then active; + state active { + %s action { + first start then nap; + action nap accept after 1 [s]; + } + } + succession first active then done; + } + }`, behavior) + model := parseLibraryModel(t, src) + machine := model.state(t, "Machine") + + t.Run("explore", func(t *testing.T) { + exploration, err := Explore(context.Background(), mustPolicy(t, "explore"), model.fresh, + func(ctx *Context) (Outcome, error) { + return ctx.StateOutcomeWithEvents(machine, nil) + }) + if err != nil { + t.Fatalf("Explore: %v", err) + } + if exploration == nil || !exploration.Complete() || len(exploration.Outcomes) != 1 { + t.Fatalf("exploration = %v, want one complete run", exploration) + } + assertStateBehaviorWaitError(t, exploration.Outcomes[0].Outcome.Err) + }) + + t.Run("fixed", func(t *testing.T) { + ctx, err := model.fresh() + if err != nil { + t.Fatalf("fresh context: %v", err) + } + if err := ctx.SetSchedule(mustPolicy(t, "declared")); err != nil { + t.Fatalf("SetSchedule(declared): %v", err) + } + outcome, err := ctx.StateOutcomeWithEvents(machine, nil) + waitErr := outcome.Err + if waitErr == nil { + waitErr = err + } + assertStateBehaviorWaitError(t, waitErr) + }) +} + +func assertStateBehaviorWaitError(t *testing.T, err error) { + t.Helper() + if !errors.Is(err, ErrStateBehaviorWaits) { + t.Fatalf("state behavior wait error = %v, want ErrStateBehaviorWaits", err) + } + if errors.Is(err, errPaused) || strings.Contains(err.Error(), errPaused.Error()) { + t.Fatalf("state behavior wait leaked %q: %v", errPaused, err) + } +} diff --git a/internal/exec/runtime/robustness_test.go b/internal/exec/runtime/robustness_test.go index 57bdecfa70..180b460f71 100644 --- a/internal/exec/runtime/robustness_test.go +++ b/internal/exec/runtime/robustness_test.go @@ -355,7 +355,6 @@ func TestRuntimeRobustness(t *testing.T) { t.Run("non_terminating_do_behavior", testNonTerminatingDoBehavior) t.Run("empty_anonymous_action_body", testEmptyAnonymousActionBody) t.Run("non_terminating_anonymous_do_body", testNonTerminatingAnonymousDoBody) - t.Run("behavior_performing_an_action_and_stating_a_body", testBehaviorPerformingAnActionAndStatingABody) t.Run("qualified_assignment_target_in_a_state_effect", testQualifiedAssignmentTargetInAStateEffect) t.Run("call_of_unhandled_operation", testCallOfUnhandledOperation) t.Run("signal_no_level_of_a_composite_state_accepts", testSignalNoLevelOfACompositeStateAccepts) @@ -3972,32 +3971,6 @@ func testNonTerminatingAnonymousDoBody(t *testing.T) { } } -// testBehaviorPerformingAnActionAndStatingABody: a behavior that both performs -// an action and states a body of its own is reported rather than silently -// choosing one of the two. -func testBehaviorPerformingAnActionAndStatingABody(t *testing.T) { - err := stateRunErrorForSource(t, "Machine", `package test { - action def Bump; - state Machine { - attribute c : Integer = 0; - entry; then start; - state start; - state working { - entry action mixed : Bump { assign c := c + 1; } - } - state done; - succession first start then working; - succession first working then done; - } - }`) - if err == nil { - t.Fatal("expected a behavior stating a body and an action to be reported") - } - if !strings.Contains(err.Error(), "stating a body of its own") { - t.Errorf("expected the report to name the conflict, got: %v", err) - } -} - // testQualifiedAssignmentTargetInAStateEffect: an assignment naming more than // one segment is reported rather than writing the last segment. func testQualifiedAssignmentTargetInAStateEffect(t *testing.T) { diff --git a/internal/exec/runtime/snapshot.go b/internal/exec/runtime/snapshot.go index 4fda06c3ad..105ec4fbad 100644 --- a/internal/exec/runtime/snapshot.go +++ b/internal/exec/runtime/snapshot.go @@ -541,6 +541,7 @@ type actionCapture struct { inRun, held bool moved bool awaiting *actionFrame + occurrence *Instance outputListeners []outputListener firedBreakpoints mapState[breakpointVisit, bool] traversals []Traversal @@ -561,6 +562,7 @@ func (e *ActionExecutor) capture() actionCapture { stepCount: e.stepCount, sweep: e.sweep, sweeps: e.sweeps, pausedAt: e.pausedAt, released: e.released, pauses: e.pauses, steps: e.steps, stepsSpent: e.stepsSpent, inRun: e.inRun, held: e.held, moved: e.moved, awaiting: e.awaiting, + occurrence: e.occurrence, outputListeners: slices.Clone(e.outputListeners), firedBreakpoints: captureMap(e.firedBreakpoints), traversals: cloneTraversals(e.traversals), @@ -582,6 +584,7 @@ func (c actionCapture) restore() { e.pausedAt, e.released, e.pauses = c.pausedAt, c.released, c.pauses e.steps, e.stepsSpent, e.inRun, e.held = c.steps, c.stepsSpent, c.inRun, c.held e.moved, e.awaiting = c.moved, c.awaiting + e.occurrence, e.performances.occurrence = c.occurrence, c.occurrence e.outputListeners = slices.Clone(c.outputListeners) e.firedBreakpoints = c.firedBreakpoints.restore() e.traversals, e.traversalBase = cloneTraversals(c.traversals), c.traversalBase diff --git a/internal/exec/runtime/state_statements.go b/internal/exec/runtime/state_statements.go index 8f518d0e37..a2c48b818b 100644 --- a/internal/exec/runtime/state_statements.go +++ b/internal/exec/runtime/state_statements.go @@ -61,19 +61,38 @@ func (e *StateExecutor) executeBehavior(behavior lower.StateBehavior) (bool, err } host := e.behaviorHost(behavior, e.currentFiring()) defer e.ctx.holdClock(host.describe())() - if err := host.run(); err != nil { + if err := host.runBehavior(); err != nil { return false, err } return host.terminated, nil } +func (h *stateStmtHost) runBehavior() error { + if !h.exec.ctx.scheduling().oneMove() { + return h.run() + } + run := &bodyRun{work: h, steps: true, holdsClock: true} + for { + pause, paused := run.resume(h.exec.ctx) + if !paused { + return run.err + } + if !pause.tokenStep { + return errPaused + } + } +} + func (e *StateExecutor) validateBehaviorMultiplicity(behavior lower.StateBehavior) error { + graph := &lower.ActionGraph{ + Scope: behavior.Scope, + Multiplicities: map[ast.Node]*ast.Multiplicity{}, + Scopes: map[ast.Node]*symbols.Scope{behavior.Node: behavior.Scope}, + } if behavior.Multiplicity != nil { - graph := &lower.ActionGraph{ - Scope: behavior.Scope, - Multiplicities: map[ast.Node]*ast.Multiplicity{behavior.Node: behavior.Multiplicity}, - Scopes: map[ast.Node]*symbols.Scope{behavior.Node: behavior.Scope}, - } + graph.Multiplicities[behavior.Node] = behavior.Multiplicity + } + if graph.HasStepMultiplicity(behavior.Node, e.ctx.Semantics()) { count, err := graph.StepCount(behavior.Node, e.ctx.Semantics()) if err != nil { return fmt.Errorf("%w: %w", ErrActionStepMultiplicity, err) diff --git a/internal/exec/runtime/statements.go b/internal/exec/runtime/statements.go index 1f1d515993..f475bca2f7 100644 --- a/internal/exec/runtime/statements.go +++ b/internal/exec/runtime/statements.go @@ -1149,7 +1149,7 @@ func (e *stmtEngine) block(block lower.Block) (stmtFlow, error) { func (e *stmtEngine) runBlock(block lower.Block) (stmtFlow, error) { if e.restrictedBlockFlow(block) { for _, node := range block.Graph.Nodes { - if block.Graph.Multiplicities[node] == nil { + if !block.Graph.HasStepMultiplicity(node, e.ctx.Semantics()) { continue } count, err := block.Graph.StepCount(node, e.ctx.Semantics()) diff --git a/internal/exec/runtime/testdata/conformance/action_accept_nested_call_chain.sysml b/internal/exec/runtime/testdata/conformance/action_accept_nested_call_chain.sysml index ed163936c8..b0ca93d3c4 100644 --- a/internal/exec/runtime/testdata/conformance/action_accept_nested_call_chain.sysml +++ b/internal/exec/runtime/testdata/conformance/action_accept_nested_call_chain.sysml @@ -29,7 +29,8 @@ package test { attribute received : Integer = 0; first start; fork split; - action m : Mid2 { assign received := m.got; } + action m : Mid2; + action take { assign received := m.got; } action gate1; action gate2; action gate3; @@ -42,7 +43,8 @@ package test { succession first gate1 then gate2; succession first gate2 then gate3; succession first gate3 then sender; - succession first m then sync; + succession first m then take; + succession first take then sync; succession first sender then sync; succession first sync then done; } diff --git a/internal/exec/runtime/testdata/conformance/action_accept_nested_call_chain.trace.golden b/internal/exec/runtime/testdata/conformance/action_accept_nested_call_chain.trace.golden index e4ff563b0c..d3f9ce0e1e 100644 --- a/internal/exec/runtime/testdata/conformance/action_accept_nested_call_chain.trace.golden +++ b/internal/exec/runtime/testdata/conformance/action_accept_nested_call_chain.trace.golden @@ -29,9 +29,10 @@ stmt assign got eval chain got -> 7 step 4: token 1@done step 5: no active tokens +choice step 6: tokens 2@m, 3@sender (unordered; took 3@sender first) +step 6: token 2@take, token 3@sync stmt assign received eval chain got -> 7 -choice step 6: tokens 2@m, 3@sender (unordered; took 3@sender first) -step 6: token 2@sync, token 3@sync -step 7: token 4@done -step 8: no active tokens +step 7: token 2@sync, token 3@sync +step 8: token 4@done +step 9: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_accept_nested_chain_clock.sysml b/internal/exec/runtime/testdata/conformance/action_accept_nested_chain_clock.sysml index 8b3ed95606..b400692b3b 100644 --- a/internal/exec/runtime/testdata/conformance/action_accept_nested_chain_clock.sysml +++ b/internal/exec/runtime/testdata/conformance/action_accept_nested_chain_clock.sysml @@ -22,7 +22,8 @@ package test { attribute received : Integer = 0; attribute instant : Real = 0.0; first start; - then action m : Mid { assign received := m.got; } + then action m : Mid; + then action take { assign received := m.got; } then action record { assign instant := localClock.currentTime; } then done; } diff --git a/internal/exec/runtime/testdata/conformance/action_accept_nested_chain_clock.trace.golden b/internal/exec/runtime/testdata/conformance/action_accept_nested_chain_clock.trace.golden index 75bbfea1df..97ea2d1812 100644 --- a/internal/exec/runtime/testdata/conformance/action_accept_nested_chain_clock.trace.golden +++ b/internal/exec/runtime/testdata/conformance/action_accept_nested_chain_clock.trace.golden @@ -13,13 +13,14 @@ stmt assign got eval chain got -> 1 step 3: token 1@done step 4: no active tokens +step 2: token 1@take stmt assign received eval chain got -> 1 -step 2: token 1@record +step 3: token 1@record stmt assign instant materialize: universalClock #1 eval feature universalClock -> instance#1 eval feature localClock -> instance#1 eval chain currentTime -> 5.0 -step 3: token 1@done -step 4: no active tokens +step 4: token 1@done +step 5: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.check.expected.json b/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.check.expected.json new file mode 100644 index 0000000000..e8411ad980 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.check.expected.json @@ -0,0 +1,7 @@ +{ + "verdict": "divergent", + "divergent": { + "y": ["30", "31"], + "seen": ["30", "31"] + } +} diff --git a/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.expected.json b/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.expected.json index 6002bf1f15..43fef491c3 100644 --- a/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.expected.json +++ b/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.expected.json @@ -2,9 +2,21 @@ "type": "action", "evaluate": "test::Adjust", "libraries": true, - "outputs": { - "y": {"type": "Integer", "value": 31}, - "seen": {"type": "Integer", "value": 31}, - "scaled.y": {"type": "Integer", "value": 31} - } + "outcomes": [ + { + "outputs": { + "y": {"type": "Integer", "value": 30}, + "seen": {"type": "Integer", "value": 30}, + "scaled.y": {"type": "Integer", "value": 30} + } + }, + { + "outputs": { + "y": {"type": "Integer", "value": 31}, + "seen": {"type": "Integer", "value": 31}, + "scaled.y": {"type": "Integer", "value": 31} + } + } + ], + "admissible": "Body-stating typed usages and mixed state behaviors" } diff --git a/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.sysml b/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.sysml index fecc5056e7..84f643feb8 100644 --- a/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.sysml +++ b/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.sysml @@ -1,6 +1,5 @@ -// An invoked node's `out` values return to same-named enclosing features once -// the node completes, so what its own body makes of an output is what returns: -// `scaled` performs Scale (y = 30), its body adds one, and Adjust's `y` reads 31. +// The typed usage's own `assign y := y + 1` is unordered with Scale's inherited +// `start → scaling → done`: it leaves 31 after scaling, or scaling overwrites it with 30 first. package test { private import ScalarValues::*; diff --git a/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.trace.order b/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.trace.order new file mode 100644 index 0000000000..e97549012a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/action_invoked_node_body_writes_output.trace.order @@ -0,0 +1 @@ +scaled < check diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_classifier_perform_typed_body.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_classifier_perform_typed_body.expected.json new file mode 100644 index 0000000000..26ef027cae --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_classifier_perform_typed_body.expected.json @@ -0,0 +1,9 @@ +{ + "type": "instance", + "libraries": true, + "instantiate": "test::Machine", + "trace": true, + "slots": { + "hits": {"type": "Integer", "value": 101} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_classifier_perform_typed_body.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_classifier_perform_typed_body.sysml new file mode 100644 index 0000000000..7d19f6f97c --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_classifier_perform_typed_body.sysml @@ -0,0 +1,18 @@ +package test { + private import ScalarValues::*; + + part def Machine { + attribute hits : Integer = 0; + action def BumpBase { + inout n : Integer; + first start then a; + action a { assign n := n + 1; } + then done; + } + action def Bump :> BumpBase; + perform action p : Bump { + inout n = hits; + action b { assign n := n + 100; } + } + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_classifier_perform_typed_body.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_classifier_perform_typed_body.trace.golden new file mode 100644 index 0000000000..f343d18e6b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_classifier_perform_typed_body.trace.golden @@ -0,0 +1,21 @@ +materialize: Machine #1 +start: performed action p of #1 +materialize: p #2 +eval feature hits -> 0 +run: performed action p of #1 +eval feature hits -> 0 +enter action node: p +step 1: token 1@start, token 2@b +stmt assign n + eval feature n -> 0 + eval literal 100 -> 100 + eval operator + -> 100 +choice step 2: tokens 1@start, 2@b (unordered; took 2@b first) +step 2: token 1@a +stmt assign n + eval feature n -> 100 + eval literal 1 -> 1 + eval operator + -> 101 +step 3: token 1@done +leave action node: p +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_diamond.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_diamond.expected.json new file mode 100644 index 0000000000..99c648793b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_diamond.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::Diamond", + "trace": true, + "outputs": { + "c": {"type": "Integer", "value": 111} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_diamond.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_diamond.sysml new file mode 100644 index 0000000000..469d1ac03f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_diamond.sysml @@ -0,0 +1,14 @@ +package test { + private import ScalarValues::*; + + action def Base { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + + action def Left :> Base { action l { assign c := c + 10; } } + action def Right :> Base { action r { assign c := c + 100; } } + action def Diamond :> Left, Right; +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_diamond.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_diamond.trace.golden new file mode 100644 index 0000000000..70ae3dcdda --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_diamond.trace.golden @@ -0,0 +1,17 @@ +stmt assign c + eval feature c -> 0 + eval literal 100 -> 100 + eval operator + -> 100 +stmt assign c + eval feature c -> 100 + eval literal 10 -> 10 + eval operator + -> 110 +choice step 1: writes c := 110 by token 2, c := 100 by token 3 (unordered; c := 110 by token 2 stood) +choice step 1: tokens 1@start, 2@l, 3@r (unordered; took 3@r first) +step 1: token 1@a +stmt assign c + eval feature c -> 110 + eval literal 1 -> 1 + eval operator + -> 111 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_assign.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_assign.expected.json new file mode 100644 index 0000000000..063da13685 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_assign.expected.json @@ -0,0 +1,8 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::Direct", + "outputs": { + "c": {"type": "Integer", "value": 5} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_assign.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_assign.sysml new file mode 100644 index 0000000000..47f2a052ed --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_assign.sysml @@ -0,0 +1,13 @@ +package test { + private import ScalarValues::*; + + action def Base { + attribute c : Integer = 0; + first start then s; + action s; + then assign c := c + 5; + then done; + } + + action def Direct :> Base; +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_send.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_send.expected.json new file mode 100644 index 0000000000..e59114fb52 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_send.expected.json @@ -0,0 +1,8 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::Direct", + "outputs": { + "number": {"type": "Integer", "value": 5} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_send.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_send.sysml new file mode 100644 index 0000000000..d6a775d972 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_direct_send.sysml @@ -0,0 +1,14 @@ +package test { + private import ScalarValues::*; + + action def Base { + attribute number : Integer = 0; + first start; + then action sender { send 5 to counter; } + then action counter accept n : Integer; + then action recorder { assign number := counter.n; } + then done; + } + + action def Direct :> Base; +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_keep.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_keep.expected.json new file mode 100644 index 0000000000..c7f1926f30 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_keep.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::Keep", + "trace": true, + "outputs": { + "c": {"type": "Integer", "value": 33} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_keep.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_keep.sysml new file mode 100644 index 0000000000..c5b09483c3 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_keep.sysml @@ -0,0 +1,12 @@ +package test { + private import ScalarValues::*; + + action def Base { + attribute c : Integer = 0; + first start then a; + action a[3] { assign c := c + 1; } + then done; + } + + action def Keep :> Base { action :>> a { assign c := c + 10; } } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_keep.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_keep.trace.golden new file mode 100644 index 0000000000..f380015a1c --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_keep.trace.golden @@ -0,0 +1,30 @@ +step 1: token 1@a +step 2: token 1@a, token 2@a, token 3@a +stmt assign c + eval feature c -> 0 + eval literal 10 -> 10 + eval operator + -> 10 +stmt assign c + eval feature c -> 10 + eval literal 1 -> 1 + eval operator + -> 11 +stmt assign c + eval feature c -> 11 + eval literal 10 -> 10 + eval operator + -> 21 +stmt assign c + eval feature c -> 21 + eval literal 1 -> 1 + eval operator + -> 22 +stmt assign c + eval feature c -> 22 + eval literal 10 -> 10 + eval operator + -> 32 +stmt assign c + eval feature c -> 32 + eval literal 1 -> 1 + eval operator + -> 33 +choice step 3: writes c := 33 by token 1, c := 22 by token 2, c := 11 by token 3 (unordered; c := 33 by token 1 stood) +choice step 3: tokens 1@a, 2@a, 3@a (unordered; took 3@a first) +step 3: token 1@done +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_multi_level.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_multi_level.expected.json new file mode 100644 index 0000000000..512bcb042e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_multi_level.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::Multi", + "trace": true, + "outputs": { + "c": {"type": "Integer", "value": 11} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_multi_level.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_multi_level.sysml new file mode 100644 index 0000000000..a58d32a162 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_multi_level.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + action def B1 { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + + action def B2 :> B1 { + action b { assign c := c + 10; } + } + + action def Multi :> B2; +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_multi_level.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_multi_level.trace.golden new file mode 100644 index 0000000000..2422917989 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_multi_level.trace.golden @@ -0,0 +1,12 @@ +stmt assign c + eval feature c -> 0 + eval literal 10 -> 10 + eval operator + -> 10 +choice step 1: tokens 1@start, 2@b (unordered; took 2@b first) +step 1: token 1@a +stmt assign c + eval feature c -> 10 + eval literal 1 -> 1 + eval operator + -> 11 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_narrow.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_narrow.expected.json new file mode 100644 index 0000000000..368b07dfb6 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_narrow.expected.json @@ -0,0 +1,8 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::Narrow", + "outputs": { + "c": {"type": "Integer", "value": 2} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_narrow.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_narrow.sysml new file mode 100644 index 0000000000..1057b23527 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_narrow.sysml @@ -0,0 +1,12 @@ +package test { + private import ScalarValues::*; + + action def Base { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + + action def Narrow :> Base { action :>> a[2]; } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_nested_typed_usage_with_body.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_nested_typed_usage_with_body.expected.json new file mode 100644 index 0000000000..207fd2ea4e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_nested_typed_usage_with_body.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::Host", + "trace": true, + "outputs": { + "y.c": {"type": "Integer", "value": 101} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_nested_typed_usage_with_body.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_nested_typed_usage_with_body.sysml new file mode 100644 index 0000000000..9bd6934fba --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_nested_typed_usage_with_body.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + action def B1 { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + + action def Host { + action y : B1 { + action b { assign c := c + 100; } + } + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_nested_typed_usage_with_body.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_nested_typed_usage_with_body.trace.golden new file mode 100644 index 0000000000..0f57a0b02b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_nested_typed_usage_with_body.trace.golden @@ -0,0 +1,16 @@ +eval literal 0 -> 0 +enter action node: y +step 1: token 1@start, token 2@b +stmt assign c + eval feature c -> 0 + eval literal 100 -> 100 + eval operator + -> 100 +choice step 2: tokens 1@start, 2@b (unordered; took 2@b first) +step 2: token 1@a +stmt assign c + eval feature c -> 100 + eval literal 1 -> 1 + eval operator + -> 101 +step 3: token 1@done +leave action node: y +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_own_flow_inherited_steps.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_own_flow_inherited_steps.expected.json new file mode 100644 index 0000000000..b195d0eeb4 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_own_flow_inherited_steps.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::S", + "trace": true, + "outputs": { + "b.v": {"type": "Integer", "value": 7} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_own_flow_inherited_steps.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_own_flow_inherited_steps.sysml new file mode 100644 index 0000000000..beb0c94930 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_own_flow_inherited_steps.sysml @@ -0,0 +1,13 @@ +package test { + private import ScalarValues::*; + action def G { + action a { out y : Integer; assign y := 7; } + action b { in v : Integer; } + first start then a; + first a then b; + first b then done; + } + action def S :> G { + flow f2 from a.y to b.v; + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_own_flow_inherited_steps.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_own_flow_inherited_steps.trace.golden new file mode 100644 index 0000000000..e72c9ab83f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_own_flow_inherited_steps.trace.golden @@ -0,0 +1,6 @@ +step 1: token 1@a +stmt assign y + eval literal 7 -> 7 +step 2: token 1@b +step 3: token 1@done +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_p0.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p0.expected.json new file mode 100644 index 0000000000..2ad6053716 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p0.expected.json @@ -0,0 +1,8 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::P0", + "outputs": { + "c": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_p0.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p0.sysml new file mode 100644 index 0000000000..da8cf605c0 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p0.sysml @@ -0,0 +1,12 @@ +package test { + private import ScalarValues::*; + + action def B1 { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + + action def P0 :> B1; +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_p2.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p2.expected.json new file mode 100644 index 0000000000..5dbc87ef09 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p2.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::P2", + "outputs": { + "c": {"type": "Integer", "value": 1}, + "z": {"type": "Integer", "value": 0} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_p2.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p2.sysml new file mode 100644 index 0000000000..5d58ff3e73 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p2.sysml @@ -0,0 +1,14 @@ +package test { + private import ScalarValues::*; + + action def B1 { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + + action def P2 :> B1 { + attribute z : Integer = 0; + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_p3.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p3.expected.json new file mode 100644 index 0000000000..24109e2813 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p3.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::P3", + "trace": true, + "outputs": { + "c": {"type": "Integer", "value": 101} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_p3.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p3.sysml new file mode 100644 index 0000000000..66dc32b251 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p3.sysml @@ -0,0 +1,14 @@ +package test { + private import ScalarValues::*; + + action def B1 { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + + action def P3 :> B1 { + action b { assign c := c + 100; } + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_p3.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p3.trace.golden new file mode 100644 index 0000000000..0f1db9bb95 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_p3.trace.golden @@ -0,0 +1,12 @@ +stmt assign c + eval feature c -> 0 + eval literal 100 -> 100 + eval operator + -> 100 +choice step 1: tokens 1@start, 2@b (unordered; took 2@b first) +step 1: token 1@a +stmt assign c + eval feature c -> 100 + eval literal 1 -> 1 + eval operator + -> 101 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_perform.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_perform.expected.json new file mode 100644 index 0000000000..07094121cb --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_perform.expected.json @@ -0,0 +1,8 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::W", + "outputs": { + "p.c": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_perform.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_perform.sysml new file mode 100644 index 0000000000..53517993be --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_perform.sysml @@ -0,0 +1,10 @@ +package test { + private import ScalarValues::*; + + action def P0 { + out attribute c : Integer = 0; + assign c := c + 1; + } + + action def W { perform action p : P0; } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefine_two_levels.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefine_two_levels.expected.json new file mode 100644 index 0000000000..dd50248ec0 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefine_two_levels.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::Leaf", + "trace": true, + "outputs": { + "c": {"type": "Integer", "value": 22} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefine_two_levels.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefine_two_levels.sysml new file mode 100644 index 0000000000..532ecda402 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefine_two_levels.sysml @@ -0,0 +1,13 @@ +package test { + private import ScalarValues::*; + + action def Base { + attribute c : Integer = 0; + first start then a; + action a[2] { assign c := c + 1; } + then done; + } + + action def Mid :> Base { action :>> a[2]; } + action def Leaf :> Mid { action :>> a { assign c := c + 10; } } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefine_two_levels.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefine_two_levels.trace.golden new file mode 100644 index 0000000000..8c4feb87d1 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefine_two_levels.trace.golden @@ -0,0 +1,22 @@ +step 1: token 1@a +step 2: token 1@a, token 2@a +stmt assign c + eval feature c -> 0 + eval literal 10 -> 10 + eval operator + -> 10 +stmt assign c + eval feature c -> 10 + eval literal 1 -> 1 + eval operator + -> 11 +stmt assign c + eval feature c -> 11 + eval literal 10 -> 10 + eval operator + -> 21 +stmt assign c + eval feature c -> 21 + eval literal 1 -> 1 + eval operator + -> 22 +choice step 3: writes c := 22 by token 1, c := 11 by token 2 (unordered; c := 22 by token 1 stood) +choice step 3: tokens 1@a, 2@a (unordered; took 2@a first) +step 3: token 1@done +step 4: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefined_typed_step.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefined_typed_step.expected.json new file mode 100644 index 0000000000..a79cd107bb --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefined_typed_step.expected.json @@ -0,0 +1,8 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::Narrow2", + "outputs": { + "hits": {"type": "Integer", "value": 2} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefined_typed_step.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefined_typed_step.sysml new file mode 100644 index 0000000000..82c1087489 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_redefined_typed_step.sysml @@ -0,0 +1,17 @@ +package test { + private import ScalarValues::*; + + action def Foo { + in ref context : Base2[1]; + out attribute c : Integer = 0; + assign context.hits := context.hits + 1; + assign c := c + 1; + } + + action def Base2 { + attribute hits : Integer = 0; + action a : Foo { in ref :>> context = this; } + } + + action def Narrow2 :> Base2 { action :>> a[2]; } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_holds.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_holds.expected.json new file mode 100644 index 0000000000..5841f474ce --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_holds.expected.json @@ -0,0 +1,8 @@ +{ + "type": "action", + "evaluate": "test::S", + "trace": true, + "outputs": { + "level": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_holds.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_holds.sysml new file mode 100644 index 0000000000..cb9df040f1 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_holds.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + action def G { + attribute level : Integer = 0; + action raise { assign level := 3; } + assert constraint raised { level == 3 } + action lower { assign level := 1; } + assert not constraint high { level > 2 } + first start then raise; + first raise then raised; + first raised then lower; + first lower then high; + first high then done; + } + action def S :> G; +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_holds.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_holds.trace.golden new file mode 100644 index 0000000000..bdc6103e0a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_holds.trace.golden @@ -0,0 +1,18 @@ +step 1: token 1@raise +stmt assign level + eval literal 3 -> 3 +step 2: token 1@raised +stmt assert raised + eval feature level -> 3 + eval literal 3 -> 3 + eval operator == -> true +step 3: token 1@lower +stmt assign level + eval literal 1 -> 1 +step 4: token 1@high +stmt assert high + eval feature level -> 1 + eval literal 2 -> 2 + eval operator > -> false +step 5: token 1@done +step 6: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_violated.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_violated.expected.json new file mode 100644 index 0000000000..e7a0697c01 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_violated.expected.json @@ -0,0 +1,5 @@ +{ + "type": "action", + "evaluate": "test::S", + "error": "constraint raised: assertion evaluated to false: level == 4" +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_violated.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_violated.sysml new file mode 100644 index 0000000000..5ae1e1dc67 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_sequenced_assertion_violated.sysml @@ -0,0 +1,14 @@ +package test { + private import ScalarValues::*; + action def G { + attribute level : Integer = 0; + action raise { assign level := 3; } + assert constraint raised { level == 4 } + action lower { assign level := 1; } + first start then raise; + first raise then raised; + first raised then lower; + first lower then done; + } + action def S :> G; +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do.expected.json new file mode 100644 index 0000000000..ee02f37902 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do.expected.json @@ -0,0 +1,9 @@ +{ + "type": "state", + "evaluate": "test::Machine", + "finalState": "active", + "stateVisits": ["active"], + "outputs": { + "hits": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do.sysml new file mode 100644 index 0000000000..27daffc4e4 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + state def Machine { + attribute hits : Integer = 0; + action def Base { + inout n : Integer; + assign n := n + 1; + } + action def P0 :> Base; + entry; then active; + state active { + do action run : P0 { inout n = hits; } + } + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do_typed_body.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do_typed_body.expected.json new file mode 100644 index 0000000000..e0870115f8 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do_typed_body.expected.json @@ -0,0 +1,11 @@ +{ + "type": "state", + "evaluate": "test::Machine", + "events": [{"signal": "Go"}], + "trace": true, + "finalState": "active", + "stateVisits": ["ready", "active"], + "outputs": { + "hits": {"type": "Integer", "value": 11} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do_typed_body.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do_typed_body.sysml new file mode 100644 index 0000000000..38377f97c4 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do_typed_body.sysml @@ -0,0 +1,26 @@ +package test { + private import ScalarValues::*; + + item def Go; + + state def Machine { + attribute hits : Integer = 0; + action def BumpBase { + inout n : Integer; + first start then a; + action a { assign n := n + 1; } + then done; + } + action def Bump :> BumpBase; + + entry; then ready; + state ready; + state active { + do action mixed : Bump { + inout n = hits; + action b { assign n := n + 10; } + } + } + transition first ready accept Go then active; + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do_typed_body.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do_typed_body.trace.golden new file mode 100644 index 0000000000..ae2942748e --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_do_typed_body.trace.golden @@ -0,0 +1,22 @@ +exit: ready +enter: active +transition: ready -> active (event: accept Go) +do: active +stmt action body + eval feature hits -> 0 +enter action node: state behavior mixed + eval feature hits -> 0 +enter action node: mixed +do: active + stmt assign n + eval feature n -> 0 + eval literal 10 -> 10 + eval operator + -> 10 +choice step 2: tokens 1@start, 2@b (unordered; took 2@b first) +do: active + stmt assign n + eval feature n -> 10 + eval literal 1 -> 1 + eval operator + -> 11 +leave action node: mixed +leave action node: state behavior mixed diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry.expected.json new file mode 100644 index 0000000000..ee02f37902 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry.expected.json @@ -0,0 +1,9 @@ +{ + "type": "state", + "evaluate": "test::Machine", + "finalState": "active", + "stateVisits": ["active"], + "outputs": { + "hits": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry.sysml new file mode 100644 index 0000000000..0747ed7c5b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry.sysml @@ -0,0 +1,16 @@ +package test { + private import ScalarValues::*; + + state def Machine { + attribute hits : Integer = 0; + action def Base { + inout n : Integer; + assign n := n + 1; + } + action def P0 :> Base; + entry; then active; + state active { + entry action run : P0 { inout n = hits; } + } + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_perform_body.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_perform_body.expected.json new file mode 100644 index 0000000000..e0870115f8 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_perform_body.expected.json @@ -0,0 +1,11 @@ +{ + "type": "state", + "evaluate": "test::Machine", + "events": [{"signal": "Go"}], + "trace": true, + "finalState": "active", + "stateVisits": ["ready", "active"], + "outputs": { + "hits": {"type": "Integer", "value": 11} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_perform_body.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_perform_body.sysml new file mode 100644 index 0000000000..551289b0ac --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_perform_body.sysml @@ -0,0 +1,26 @@ +package test { + private import ScalarValues::*; + + item def Go; + + state def Machine { + attribute hits : Integer = 0; + action def BumpBase { + inout n : Integer; + first start then a; + action a { assign n := n + 1; } + then done; + } + action def Bump :> BumpBase; + + entry; then ready; + state ready; + state active { + entry perform action p : Bump { + inout n = hits; + action b { assign n := n + 10; } + } + } + transition first ready accept Go then active; + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_perform_body.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_perform_body.trace.golden new file mode 100644 index 0000000000..2e690799a2 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_perform_body.trace.golden @@ -0,0 +1,19 @@ +exit: ready +enter: active (entry action) +stmt action body + eval feature hits -> 0 +enter action node: state behavior p + eval feature hits -> 0 +enter action node: p + stmt assign n + eval feature n -> 0 + eval literal 10 -> 10 + eval operator + -> 10 +choice step 2: tokens 1@start, 2@b (unordered; took 2@b first) + stmt assign n + eval feature n -> 10 + eval literal 1 -> 1 + eval operator + -> 11 +leave action node: p +leave action node: state behavior p +transition: ready -> active (event: accept Go) diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_typed_body.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_typed_body.expected.json new file mode 100644 index 0000000000..e0870115f8 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_typed_body.expected.json @@ -0,0 +1,11 @@ +{ + "type": "state", + "evaluate": "test::Machine", + "events": [{"signal": "Go"}], + "trace": true, + "finalState": "active", + "stateVisits": ["ready", "active"], + "outputs": { + "hits": {"type": "Integer", "value": 11} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_typed_body.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_typed_body.sysml new file mode 100644 index 0000000000..e8cc6d6884 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_typed_body.sysml @@ -0,0 +1,26 @@ +package test { + private import ScalarValues::*; + + item def Go; + + state def Machine { + attribute hits : Integer = 0; + action def BumpBase { + inout n : Integer; + first start then a; + action a { assign n := n + 1; } + then done; + } + action def Bump :> BumpBase; + + entry; then ready; + state ready; + state active { + entry action mixed : Bump { + inout n = hits; + action b { assign n := n + 10; } + } + } + transition first ready accept Go then active; + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_typed_body.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_typed_body.trace.golden new file mode 100644 index 0000000000..938fe490ed --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_entry_typed_body.trace.golden @@ -0,0 +1,19 @@ +exit: ready +enter: active (entry action) +stmt action body + eval feature hits -> 0 +enter action node: state behavior mixed + eval feature hits -> 0 +enter action node: mixed + stmt assign n + eval feature n -> 0 + eval literal 10 -> 10 + eval operator + -> 10 +choice step 2: tokens 1@start, 2@b (unordered; took 2@b first) + stmt assign n + eval feature n -> 10 + eval literal 1 -> 1 + eval operator + -> 11 +leave action node: mixed +leave action node: state behavior mixed +transition: ready -> active (event: accept Go) diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit.expected.json new file mode 100644 index 0000000000..1df5b6cd66 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit.expected.json @@ -0,0 +1,10 @@ +{ + "type": "state", + "evaluate": "test::Machine", + "events": [{"signal": "Go"}], + "finalState": "completed", + "stateVisits": ["active", "completed"], + "outputs": { + "hits": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit.sysml new file mode 100644 index 0000000000..85d6a346e7 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit.sysml @@ -0,0 +1,20 @@ +package test { + private import ScalarValues::*; + + item def Go; + + state def Machine { + attribute hits : Integer = 0; + action def Base { + inout n : Integer; + assign n := n + 1; + } + action def P0 :> Base; + entry; then active; + state active { + exit action run : P0 { inout n = hits; } + } + state completed; + transition first active accept Go then completed; + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit_typed_body.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit_typed_body.expected.json new file mode 100644 index 0000000000..7ae8e8488b --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit_typed_body.expected.json @@ -0,0 +1,11 @@ +{ + "type": "state", + "evaluate": "test::Machine", + "events": [{"signal": "Go"}], + "trace": true, + "finalState": "completed", + "stateVisits": ["active", "completed"], + "outputs": { + "hits": {"type": "Integer", "value": 11} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit_typed_body.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit_typed_body.sysml new file mode 100644 index 0000000000..5290cb41f1 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit_typed_body.sysml @@ -0,0 +1,26 @@ +package test { + private import ScalarValues::*; + + item def Go; + + state def Machine { + attribute hits : Integer = 0; + action def BumpBase { + inout n : Integer; + first start then a; + action a { assign n := n + 1; } + then done; + } + action def Bump :> BumpBase; + + entry; then active; + state active { + exit action mixed : Bump { + inout n = hits; + action b { assign n := n + 10; } + } + } + state completed; + transition first active accept Go then completed; + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit_typed_body.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit_typed_body.trace.golden new file mode 100644 index 0000000000..6767438ebc --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_exit_typed_body.trace.golden @@ -0,0 +1,19 @@ +exit: active (exit action) +stmt action body + eval feature hits -> 0 +enter action node: state behavior mixed + eval feature hits -> 0 +enter action node: mixed + stmt assign n + eval feature n -> 0 + eval literal 10 -> 10 + eval operator + -> 10 +choice step 2: tokens 1@start, 2@b (unordered; took 2@b first) + stmt assign n + eval feature n -> 10 + eval literal 1 -> 1 + eval operator + -> 11 +leave action node: mixed +leave action node: state behavior mixed +enter: completed +transition: active -> completed (event: accept Go) diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_mixed_empty_bump.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_mixed_empty_bump.expected.json new file mode 100644 index 0000000000..4f96dd404d --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_mixed_empty_bump.expected.json @@ -0,0 +1,11 @@ +{ + "type": "state", + "evaluate": "test::Machine", + "events": [{"signal": "Go"}], + "trace": true, + "finalState": "active", + "stateVisits": ["ready", "active"], + "outputs": { + "c": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_mixed_empty_bump.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_mixed_empty_bump.sysml new file mode 100644 index 0000000000..95052a6f1f --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_mixed_empty_bump.sysml @@ -0,0 +1,17 @@ +package test { + private import ScalarValues::*; + + item def Go; + + state def Machine { + attribute c : Integer = 0; + action def Bump; + + entry; then ready; + state ready; + state active { + entry action mixed : Bump { assign c := c + 1; } + } + transition first ready accept Go then active; + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_mixed_empty_bump.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_mixed_empty_bump.trace.golden new file mode 100644 index 0000000000..7502dfa0c6 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_state_mixed_empty_bump.trace.golden @@ -0,0 +1,12 @@ +exit: ready +enter: active (entry action) +stmt action body +enter action node: state behavior mixed +enter action node: mixed + stmt assign c + eval feature c -> 0 + eval literal 1 -> 1 + eval operator + -> 1 +leave action node: mixed +leave action node: state behavior mixed +transition: ready -> active (event: accept Go) diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_typed_node.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_typed_node.expected.json new file mode 100644 index 0000000000..68ad781b7a --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_typed_node.expected.json @@ -0,0 +1,8 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::W", + "outputs": { + "x.c": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_typed_node.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_typed_node.sysml new file mode 100644 index 0000000000..497c635741 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_typed_node.sysml @@ -0,0 +1,12 @@ +package test { + private import ScalarValues::*; + + action def P0 { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + + action def W { action x : P0; } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed.expected.json new file mode 100644 index 0000000000..fba098bf52 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed.expected.json @@ -0,0 +1,8 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::u1", + "outputs": { + "c": {"type": "Integer", "value": 1} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed.sysml new file mode 100644 index 0000000000..e457ecddfb --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed.sysml @@ -0,0 +1,12 @@ +package test { + private import ScalarValues::*; + + action def B1 { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + + action u1 : B1; +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed_with_body.expected.json b/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed_with_body.expected.json new file mode 100644 index 0000000000..9585cbb04d --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed_with_body.expected.json @@ -0,0 +1,9 @@ +{ + "type": "action", + "libraries": true, + "evaluate": "test::u", + "trace": true, + "outputs": { + "c": {"type": "Integer", "value": 101} + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed_with_body.sysml b/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed_with_body.sysml new file mode 100644 index 0000000000..7337a4d888 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed_with_body.sysml @@ -0,0 +1,14 @@ +package test { + private import ScalarValues::*; + + action def B1 { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + + action u : B1 { + action b { assign c := c + 100; } + } +} diff --git a/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed_with_body.trace.golden b/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed_with_body.trace.golden new file mode 100644 index 0000000000..0f1db9bb95 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/inherited_action_steps_usage_typed_with_body.trace.golden @@ -0,0 +1,12 @@ +stmt assign c + eval feature c -> 0 + eval literal 100 -> 100 + eval operator + -> 100 +choice step 1: tokens 1@start, 2@b (unordered; took 2@b first) +step 1: token 1@a +stmt assign c + eval feature c -> 100 + eval literal 1 -> 1 + eval operator + -> 101 +step 2: token 1@done +step 3: no active tokens diff --git a/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node.sysml b/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node.sysml index 382cca67ce..4cfdc0cf42 100644 --- a/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node.sysml +++ b/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node.sysml @@ -1,9 +1,7 @@ -// A typed action node declared in a loop or branch of a state's entry body is a -// performance of the action it names: the `in` value it declares binds the -// callee's parameter (a loop variable reaching it), the callee's `out` comes -// back to the node, and the node's own body runs after the callee, reading that -// out and writing the machine's attributes. Arguments bind the same way, so -// total ends at 120 only if every pin, argument and body statement took effect. +// A pin-only typed node in a loop of a state's entry body is a performance of +// the action it names: its input binds from the loop variable, and `then` orders +// the read of its `out` after the node. A typed node's own statements would be +// unordered with the callee's steps (see the oracle section). package test { private import ScalarValues::*; @@ -29,9 +27,9 @@ package test { for i in 1..3 { action scaled : Scale { in x = i; - assign total := total + y; - assign runs := runs + 1; } + then assign total := total + scaled.y; + then assign runs := runs + 1; } then if total > 50 { action doubled = Scale(total, 2) { diff --git a/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node_body_writes_output.check.expected.json b/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node_body_writes_output.check.expected.json new file mode 100644 index 0000000000..e8411ad980 --- /dev/null +++ b/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node_body_writes_output.check.expected.json @@ -0,0 +1,7 @@ +{ + "verdict": "divergent", + "divergent": { + "y": ["30", "31"], + "seen": ["30", "31"] + } +} diff --git a/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node_body_writes_output.expected.json b/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node_body_writes_output.expected.json index 20e1e8aab6..6b3ceb68bb 100644 --- a/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node_body_writes_output.expected.json +++ b/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node_body_writes_output.expected.json @@ -1,10 +1,23 @@ { "type": "state", "libraries": true, - "finalState": "done", - "stateVisits": ["init", "active", "done"], - "outputs": { - "y": {"type": "Integer", "value": 31}, - "seen": {"type": "Integer", "value": 31} - } + "outcomes": [ + { + "finalState": "done", + "stateVisits": ["init", "active", "done"], + "outputs": { + "y": {"type": "Integer", "value": 30}, + "seen": {"type": "Integer", "value": 30} + } + }, + { + "finalState": "done", + "stateVisits": ["init", "active", "done"], + "outputs": { + "y": {"type": "Integer", "value": 31}, + "seen": {"type": "Integer", "value": 31} + } + } + ], + "admissible": "Body-stating typed usages and mixed state behaviors" } diff --git a/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node_body_writes_output.sysml b/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node_body_writes_output.sysml index 0244a18e59..40df7e591e 100644 --- a/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node_body_writes_output.sysml +++ b/internal/exec/runtime/testdata/conformance/state_block_flow_typed_node_body_writes_output.sysml @@ -1,6 +1,6 @@ -// A typed node in a state's body returns its `out` values once its own body has -// run: `scaled` performs Scale (y = 30), its body adds one, and the state's `y` -// reads 31. +// The typed action's own `assign y := y + 1` is unordered with Scale's inherited +// `start → scaling → done`: it leaves `y = 31` if it runs after `scaling`, and +// `y = 30` if it runs first. The following assignment observes either result. package test { private import ScalarValues::*; diff --git a/internal/exec/runtime/tool.go b/internal/exec/runtime/tool.go index 60f189ac66..908fa6e1c9 100644 --- a/internal/exec/runtime/tool.go +++ b/internal/exec/runtime/tool.go @@ -6,6 +6,7 @@ import ( "sort" "strings" + "github.com/Open-MBEE/OpenSysML/internal/ir/lower" "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" @@ -455,17 +456,24 @@ func (ctx *Context) toolPerformance(performed, callee *symbols.Symbol) (*symbols return nil, nil, nil } -// performanceBody is the action a performance of performed, of callee, holds the features -// of: the body callee states, or under a tool, which runs no body, the declaration it binds. -func (ctx *Context) performanceBody(performed, callee *symbols.Symbol) (*symbols.Symbol, *toolExecution, error) { +// performanceBody is the selected lowered flow for a performance, or a tool's action interface. +func (ctx *Context) performanceBody(performed, callee *symbols.Symbol) (*symbols.Symbol, *toolExecution, *lower.ActionGraph, error) { held, tool, err := ctx.toolPerformance(performed, callee) if err != nil { - return nil, nil, err + return nil, nil, nil, err } if tool != nil { - return held, tool, nil + graph, err := lower.ToActionInterface(held.Decl, DeclScope(held)) + if err != nil { + return nil, nil, nil, fmt.Errorf("lower action interface: %w", err) + } + return held, tool, graph, nil + } + body, graph, err := ctx.actionBodyGraph(callee) + if err != nil { + return nil, nil, nil, err } - return ctx.actionBodySymbol(callee), nil, nil + return body, nil, graph, nil } // performanceInterface is the declaration whose parameters a performance of performed, of diff --git a/internal/exec/smt/support.go b/internal/exec/smt/support.go index e1c166ce90..92b697605d 100644 --- a/internal/exec/smt/support.go +++ b/internal/exec/smt/support.go @@ -143,7 +143,7 @@ func Analyze(graph *lower.ActionGraph, model *semantics.Model, k int) (*Flow, er } } for _, node := range f.Nodes { - if frame := f.FrameOf[node]; frame != nil && frame.Graph.Multiplicities[node] != nil { + if frame := f.FrameOf[node]; frame != nil && frame.Graph.HasStepMultiplicity(node, model) { count, err := frame.Graph.StepCount(node, model) if err != nil || count != 1 { multiplicity := frame.Graph.MultiplicityText(node, model) diff --git a/internal/exec/smt/support_test.go b/internal/exec/smt/support_test.go index f94fc933a8..9ce1feab32 100644 --- a/internal/exec/smt/support_test.go +++ b/internal/exec/smt/support_test.go @@ -129,6 +129,37 @@ func TestAnalyzeRefusesRepeatedActionSteps(t *testing.T) { } } +func TestAnalyzeRefusesInheritedRepeatedActionSteps(t *testing.T) { + ctx, idx := fixture(t, "", ` + package test { + private import ScalarValues::*; + action def Base { + attribute c : Integer = 0; + first start then a; + action a[3] { assign c := c + 1; } + then done; + } + action def Keep :> Base { action :>> a; } + }`) + matches := idx.LookupQualified("test::Keep") + if len(matches) != 1 { + t.Fatalf("test::Keep matched %d symbols, want one", len(matches)) + } + graph, err := lower.ToActionGraphWith(matches[0].Decl, matches[0].Scope, resolve.New(idx)) + if err != nil { + t.Fatalf("lower Keep: %v", err) + } + lower.StartFlow(graph) + _, err = Analyze(graph, ctx.Semantics(), 10) + var unsupported *UnsupportedError + if !errors.As(err, &unsupported) || !errors.Is(err, ErrNotEncoded) { + t.Fatalf("Analyze: got %v, want a typed ErrNotEncoded refusal", err) + } + if unsupported.Node != "a" || unsupported.Construct != "action step multiplicity [3]" { + t.Errorf("refusal names %q/%q, want node a, multiplicity [3]", unsupported.Node, unsupported.Construct) + } +} + func TestAnalyzeRefusesUnaddressableStepMultiplicityWithCause(t *testing.T) { ctx, idx := fixture(t, "", ` package test { diff --git a/internal/ir/lower/action_graph.go b/internal/ir/lower/action_graph.go index b7202c24e4..5ec7837ef8 100644 --- a/internal/ir/lower/action_graph.go +++ b/internal/ir/lower/action_graph.go @@ -11,6 +11,7 @@ import ( "sync" "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" "github.com/Open-MBEE/OpenSysML/internal/syntax/source" @@ -90,6 +91,8 @@ type ActionGraph struct { // names, else its one node no succession leads to; nil where neither exists. Initial ast.Node + doneMarker ast.Node + // Concurrent are the composite subactions no succession leads to, other than // Initial, in declaration order: each starts when a performance of the flow // does, unordered against Initial and against each other (StartFlow). @@ -126,14 +129,35 @@ type ActionGraph struct { // it gives a meaning to (Probability) from any other; nil reads none. resolver *resolve.Resolver + // lowering holds the declarations whose action content is being lowered on + // this path. + lowering []ast.Node + // inherited are the actions the action specializes, nearest general first. inherited []Inherited + // Performs records the inherited action type a redefining node performs + // when the node has no own invocation. + Performs map[ast.Node]PerformedType + + // MergedTypedSubflows records typed action nodes whose body was merged into + // their subflow, so runtime does not perform the typing a second time. + MergedTypedSubflows map[ast.Node]PerformedType + + replacedByIncompatible map[ast.Node]ast.Node + // declaredIn: inherited node, flow or binding declaration → the scope of the // general's body it was written in, which says which document declares it. declaredIn map[ast.Node]*symbols.Scope } +// PerformedType is an action invocation inherited through action-node +// redefinition, resolved where the redefined node was declared. +type PerformedType struct { + Target *ast.QualifiedName + Scope *symbols.Scope +} + // Incoming returns the successions into node, in the declaration order of the // nodes they leave. The result is shared: callers must not modify it. func (g *ActionGraph) Incoming(node ast.Node) []ActionEdge { @@ -798,11 +822,25 @@ func ToActionGraph(actionDecl ast.Node, scope *symbols.Scope) (*ActionGraph, err // ToActionGraphWith is ToActionGraph reading the metadata the resolver identifies: // a succession's `@Probability { p = ...; }` becomes its edge's weight. func ToActionGraphWith(actionDecl ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) (*ActionGraph, error) { + typing := true + if usage, ok := actionDecl.(*ast.Usage); ok && usage.Kind == ast.UsageAction { + typing, _ = mergedTypedActionBody(usage, scope) + } + return toActionGraphWithTyping(actionDecl, scope, resolver, typing) +} + +func toActionGraphWithTyping(actionDecl ast.Node, scope *symbols.Scope, resolver *resolve.Resolver, typing bool) (*ActionGraph, error) { + return toActionGraphWithTypingAndAncestors(actionDecl, scope, resolver, typing, nil) +} + +func toActionGraphWithTypingAndAncestors( + actionDecl ast.Node, scope *symbols.Scope, resolver *resolve.Resolver, typing bool, ancestors []ast.Node, +) (*ActionGraph, error) { members, err := actionMembers(actionDecl) if err != nil { return nil, err } - graph, err := lowerActionFlow(members, scope, resolver) + graph, err := lowerActionFlowWithTypingAndAncestors(members, scope, resolver, typing, ancestors) if err != nil { return nil, err } @@ -810,11 +848,27 @@ func ToActionGraphWith(actionDecl ast.Node, scope *symbols.Scope, resolver *reso } func lowerActionFlow(members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) (*ActionGraph, error) { - graph, err := collectActionNodes(members, scope, resolver) + return lowerActionFlowWithTyping(members, scope, resolver, false) +} + +func lowerActionFlowWithTyping(members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver, typing bool) (*ActionGraph, error) { + return lowerActionFlowWithTypingAndAncestors(members, scope, resolver, typing, nil) +} + +func lowerActionFlowWithTypingAndAncestors( + members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver, typing bool, ancestors []ast.Node, +) (*ActionGraph, error) { + generals, cyclic := resolve.ActionGeneralization(scope, typing) + if cyclic { + graph := newActionGraph(scope) + graph.resolver = resolver + graph.lowering = actionLoweringAncestors(scope, ancestors) + return graph, fmt.Errorf("%w: %s", ErrCyclicSpecialization, actionDescription(scope)) + } + graph, err := collectActionNodesWithAncestors(members, scope, resolver, ancestors) if err != nil { return graph, err } - lowerValueBindings(graph) // The initial node is optional at graph construction time; the executor's // initialize() reports its absence. edges := &actionEdgeLowerer{graph: graph, scope: scope, weights: &probabilityReader{resolver: resolver, scope: scope}} @@ -823,9 +877,15 @@ func lowerActionFlow(members []ast.Node, scope *symbols.Scope, resolver *resolve return graph, err } } - if err := lowerInheritedPinConnections(graph, scope); err != nil { + inheritedGates, err := mergeInheritedActionContent(graph, generals) + if err != nil { + return graph, err + } + edges.gates = append(edges.gates, inheritedGates...) + if err := lowerInheritedPinConnections(graph, generals); err != nil { return graph, err } + lowerValueBindings(graph) if err := edges.resolveGates(); err != nil { return graph, err } @@ -837,12 +897,33 @@ func lowerActionFlow(members []ast.Node, scope *symbols.Scope, resolver *resolve return graph, nil } +func actionLoweringAncestors(scope *symbols.Scope, ancestors []ast.Node) []ast.Node { + out := slices.Clone(ancestors) + add := func(node ast.Node) { + if node == nil { + return + } + for _, existing := range out { + if existing == node { + return + } + } + out = append(out, node) + } + if scope != nil { + add(scope.Node()) + } + return out +} + // actionEdgeLowerer lowers the members of an action body that connect its nodes, // once the nodes themselves are collected. type actionEdgeLowerer struct { - graph *ActionGraph - scope *symbols.Scope - weights *probabilityReader + graph *ActionGraph + scope *symbols.Scope + weights *probabilityReader + nodes nodeLookup + incompatible map[ast.Node]ast.Node // gates are the successions leading to a name no node answers to, resolved // against the succession flows once every flow is lowered. gates []pendingGate @@ -853,6 +934,7 @@ type actionEdgeLowerer struct { type pendingGate struct { edge ActionEdge target ast.Node + scope *symbols.Scope } func (l *actionEdgeLowerer) member(member ast.Node) error { @@ -893,8 +975,14 @@ func (l *actionEdgeLowerer) initial(n *ast.InitialNode) error { } func (l *actionEdgeLowerer) successionEdge(n *ast.SuccessionEdge) error { - sourceNode := resolveActionEndpointForEdge(l.graph, n.Source, n.SourceMember, true) - targetNode := resolveActionEndpointForEdge(l.graph, n.Target, n.TargetMember, false) + sourceNode, err := l.endpoint(n.Source, n.SourceMember, true) + if err != nil { + return err + } + targetNode, err := l.endpoint(n.Target, n.TargetMember, false) + if err != nil { + return err + } if sourceNode == nil { return fmt.Errorf("succession edge references undefined source node %s", edgeEnd(n.Source, n.SourceMember)) } @@ -905,7 +993,7 @@ func (l *actionEdgeLowerer) successionEdge(n *ast.SuccessionEdge) error { if err != nil { return err } - l.graph.Edges[sourceNode] = append(l.graph.Edges[sourceNode], ActionEdge{ + l.addEdge(ActionEdge{ Source: sourceNode, Target: targetNode, Decl: n, @@ -917,15 +1005,21 @@ func (l *actionEdgeLowerer) successionEdge(n *ast.SuccessionEdge) error { } func (l *actionEdgeLowerer) controlFlowEdge(n *ast.ControlFlowEdge) error { - sourceNode := resolveActionEndpointForEdge(l.graph, n.Source, n.SourceMember, true) - targetNode := resolveActionEndpointForEdge(l.graph, n.Target, n.TargetMember, false) + sourceNode, err := l.endpoint(n.Source, n.SourceMember, true) + if err != nil { + return err + } + targetNode, err := l.endpoint(n.Target, n.TargetMember, false) + if err != nil { + return err + } if sourceNode == nil { return fmt.Errorf("control flow edge references undefined source %s", edgeEnd(n.Source, n.SourceMember)) } if targetNode == nil { return fmt.Errorf("control flow edge references undefined target %s", edgeEnd(n.Target, n.TargetMember)) } - l.graph.Edges[sourceNode] = append(l.graph.Edges[sourceNode], ActionEdge{ + l.addEdge(ActionEdge{ Source: sourceNode, Target: targetNode, Guard: n.Guard, @@ -935,7 +1029,14 @@ func (l *actionEdgeLowerer) controlFlowEdge(n *ast.ControlFlowEdge) error { } func (l *actionEdgeLowerer) transition(n *ast.TransitionMember) error { - sourceNode := resolveActionEndpoint(l.graph, n.Source, true) + sourceNode, err := l.endpoint(n.Source, nil, true) + if err != nil { + return err + } + targetNode, err := l.endpoint(n.Target, nil, false) + if err != nil { + return err + } if sourceNode == nil { return fmt.Errorf("succession references undefined source node %s", edgeEndName(n.Source)) } @@ -943,23 +1044,189 @@ func (l *actionEdgeLowerer) transition(n *ast.TransitionMember) error { if err != nil { return err } - edge := ActionEdge{Source: sourceNode, Guard: n.Guard, Decl: n, Probability: weight, Name: n.Name} - targetNode := resolveActionEndpoint(l.graph, n.Target, false) + edge := ActionEdge{ + Source: sourceNode, + Guard: n.Guard, + Decl: n, + Probability: weight, + Name: n.Name, + } if targetNode == nil { if n.Target != nil && ast.SimpleName(n.Target) != "" { - l.gates = append(l.gates, pendingGate{edge: edge, target: n.Target}) + l.gates = append(l.gates, pendingGate{edge: edge, target: n.Target, scope: l.scope}) return nil } return fmt.Errorf("succession references undefined target node %s", edgeEndName(n.Target)) } edge.Target = targetNode - l.graph.Edges[sourceNode] = append(l.graph.Edges[sourceNode], edge) + l.addEdge(edge) + return nil +} + +func (l *actionEdgeLowerer) addEdge(edge ActionEdge) { + if l.nodes != nil { + for _, existing := range l.graph.Edges[edge.Source] { + if sameUnconditionalActionEdge(l.graph, existing, edge) && l.graph.declaredIn[existing.Decl] == nil { + return + } + } + } + l.graph.Edges[edge.Source] = append(l.graph.Edges[edge.Source], edge) +} + +func sameUnconditionalActionEdge(graph *ActionGraph, existing, edge ActionEdge) bool { + return existing.Source == edge.Source && + existing.Target == edge.Target && + existing.Guard == nil && edge.Guard == nil && + existing.Probability == nil && edge.Probability == nil && + existing.Name == "" && edge.Name == "" && + existing.Gate == nil && edge.Gate == nil && + !existing.Carries && !edge.Carries && + sameActionEdgeMultiplicities(graph, existing, edge) +} + +func sameActionEdgeMultiplicities(graph *ActionGraph, existing, edge ActionEdge) bool { + model := semantics.NewModel(graph.resolver) + return sameActionEdgeEndMultiplicity(graph, model, + existing.SourceMultiplicity, existing.Decl, existing.Target, + edge.SourceMultiplicity, edge.Decl, edge.Target) && + sameActionEdgeEndMultiplicity(graph, model, + existing.TargetMultiplicity, existing.Decl, existing.Target, + edge.TargetMultiplicity, edge.Decl, edge.Target) +} + +func sameActionEdgeEndMultiplicity( + graph *ActionGraph, + model *semantics.Model, + existing *ast.Multiplicity, + existingDecl ast.Node, + existingStep ast.Node, + edge *ast.Multiplicity, + edgeDecl ast.Node, + edgeStep ast.Node, +) bool { + if existing == nil || edge == nil { + return existing == nil && edge == nil + } + existingBounds, err := graph.crossingRange(existingStep, existing, existingDecl, model) + if err != nil { + return false + } + edgeBounds, err := graph.crossingRange(edgeStep, edge, edgeDecl, model) + return err == nil && existingBounds == edgeBounds +} + +func (l *actionEdgeLowerer) endpoint(ref ast.Node, member ast.Node, source bool) (ast.Node, error) { + if l.nodes == nil { + if member != nil { + return resolveActionEndpointForEdge(l.graph, ref, member, source), nil + } + return resolveActionEndpoint(l.graph, ref, source), nil + } + if member != nil { + for _, node := range l.graph.Nodes { + if node == member { + return node, nil + } + } + matches := inheritedNodeMatches(l.graph, l.scope, member) + if len(matches) > 1 { + return nil, fmt.Errorf("%w: %s", ErrAmbiguousInheritedStep, getNodeName(member)) + } + if len(matches) == 1 { + return matches[0], nil + } + if replacement := l.incompatible[member]; replacement != nil { + return nil, fmt.Errorf("%w: succession end %q is replaced by %s, which is not an action step", + ErrIncompatibleRedefinedStep, getNodeName(member), ast.SimpleName(replacement)) + } + if final, ok := member.(*ast.FinalNode); ok { + return ensureInheritedDone(l.graph, final, l.scope), nil + } + if name := getNodeName(member); name != "" { + if node := l.nodes(name); node != nil { + return node, nil + } + if name == "start" && source { + if l.graph.Initial != nil { + return l.graph.Initial, nil + } + return resolveActionEndpoint(l.graph, ref, true), nil + } + } + } + qn := actionEndpointQualifiedName(ref) + if qn != nil && len(qn.Parts) > 0 { + name := qn.Parts[len(qn.Parts)-1].Text + if node := l.nodes(name); node != nil { + return node, nil + } + if name == "start" && source { + return ensureImpliedStart(l.graph, ref), nil + } + if name == "done" && !source { + return ensureInheritedDone(l.graph, &ast.FinalNode{NodeBase: ast.NodeBase{NodeSpan: ref.Span()}}, l.scope), nil + } + decl, _, found, _ := resolve.ActionNodeInScope(l.scope, qn) + if !found { + decl, found = resolve.ActionNodeOfBody(l.scope, name) + } + if found { + matches := inheritedNodeMatches(l.graph, l.scope, decl) + if len(matches) > 1 { + return nil, fmt.Errorf("%w: %s", ErrAmbiguousInheritedStep, name) + } + if len(matches) == 1 { + return matches[0], nil + } + if replacement := l.incompatible[decl]; replacement != nil { + return nil, fmt.Errorf("%w: succession end %q is replaced by %s, which is not an action step", + ErrIncompatibleRedefinedStep, name, ast.SimpleName(replacement)) + } + return nil, nil + } + for target, replacement := range l.incompatible { + if getNodeName(target) == name { + return nil, fmt.Errorf("%w: succession end %q is replaced by %s, which is not an action step", + ErrIncompatibleRedefinedStep, name, ast.SimpleName(replacement)) + } + } + } + return nil, nil +} + +func (l *actionEdgeLowerer) succession(sourceRef, targetRef ast.Node, edge ActionEdge) error { + sourceNode, err := l.endpoint(sourceRef, nil, true) + if err != nil { + return err + } + targetNode, err := l.endpoint(targetRef, nil, false) + if err != nil { + return err + } + if sourceNode == nil { + return fmt.Errorf("action succession references undefined source node %s", successionEndText(sourceRef)) + } + if targetNode == nil { + if ast.AsQualifiedName(targetRef) != nil && ast.SimpleName(targetRef) != "" { + edge.Source = sourceNode + l.gates = append(l.gates, pendingGate{edge: edge, target: targetRef, scope: l.scope}) + return nil + } + return fmt.Errorf("action succession references undefined target node %s", successionEndText(targetRef)) + } + edge.Source, edge.Target = sourceNode, targetNode + l.addEdge(edge) return nil } func (l *actionEdgeLowerer) objectFlowEdge(n *ast.ObjectFlowEdge) error { - sourceNode, sourcePin := parsePinReference(l.graph.Nodes, n.Source) - targetNode, targetPin := parsePinReference(l.graph.Nodes, n.Target) + nodes := l.nodes + if nodes == nil { + nodes = nodesNamed(l.graph.Nodes) + } + sourceNode, sourcePin := flowEnd(nodes, n.Source) + targetNode, targetPin := flowEnd(nodes, n.Target) if sourceNode == nil { return fmt.Errorf("object flow edge references undefined source %v", n.Source) } @@ -986,7 +1253,11 @@ func (l *actionEdgeLowerer) usage(n *ast.Usage) error { case ast.UsageAction: return l.weights.refuseStrayIn(n.Prefixes, n.Members) case ast.UsageBinding: - bindings, err := lowerPinBindings(l.graph, nodesNamed(l.graph.Nodes), n, l.scope) + nodes := l.nodes + if nodes == nil { + nodes = nodesNamed(l.graph.Nodes) + } + bindings, err := lowerPinBindings(l.graph, nodes, n, l.scope) if err != nil { return err } @@ -999,7 +1270,11 @@ func (l *actionEdgeLowerer) usage(n *ast.Usage) error { if n.FlowEnds == nil { return nil } - source, flow, err := lowerFlow(nodesNamed(l.graph.Nodes), n) + nodes := l.nodes + if nodes == nil { + nodes = nodesNamed(l.graph.Nodes) + } + source, flow, err := lowerFlow(nodes, n) if err != nil { return err } @@ -1043,7 +1318,7 @@ func (l *actionEdgeLowerer) successionUsage(n *ast.Usage) error { func hasDeclaredNodeMultiplicity(graph *ActionGraph, ends []*ast.ConnectorEnd) bool { for _, end := range ends { node := resolveActionEndpoint(graph, connectorEndReference(end), false) - if graph.Multiplicities[node] != nil { + if graph.HasStepMultiplicity(node, nil) { return true } } @@ -1053,9 +1328,11 @@ func hasDeclaredNodeMultiplicity(graph *ActionGraph, ends []*ast.ConnectorEnd) b // lowerInheritedPinConnections lowers the bindings and flows the actions the // action specializes wrote at pins of its nodes, nearest general first: a node // the action inherits keeps the connections its declaring action stated at it. -func lowerInheritedPinConnections(graph *ActionGraph, scope *symbols.Scope) error { - for _, body := range resolve.ActionGeneralBodies(scope) { - graph.inherited = append(graph.inherited, Inherited{Decl: body.Node(), Body: body}) +func lowerInheritedPinConnections(graph *ActionGraph, bodies []*symbols.Scope) error { + for _, body := range bodies { + if !hasInheritedBody(graph, body) { + graph.inherited = append(graph.inherited, Inherited{Decl: body.Node(), Body: body}) + } nodes := inheritedNodeLookup(graph, body) for _, member := range ast.DeclMembers(body.Node()) { u, ok := unwrapMembership(member).(*ast.Usage) @@ -1133,19 +1410,12 @@ func inheritedNodeLookup(graph *ActionGraph, body *symbols.Scope) nodeLookup { if !ok { return nil } - for _, node := range graph.Nodes { - if node == decl { - return node - } + if graph.replacedByIncompatible[decl] != nil { + return nil } - for _, node := range graph.Nodes { - u, ok := node.(*ast.Usage) - if !ok { - continue - } - if nodeScope := graph.Scopes[node]; nodeScope != nil && resolve.RedefinesActionNode(nodeScope.Parent(), u, decl) { - return node - } + matches := inheritedNodeMatches(graph, body, decl) + if len(matches) == 1 { + return matches[0] } return nil } @@ -1181,28 +1451,6 @@ func resolveFirstNode(graph *ActionGraph) error { return nil } -// succession adds the edge a succession states between the nodes its two ends resolve -// to; edge carries everything but the resolved Source and Target. A target naming no -// node is left for resolveGates, which reads it as a succession flow. -func (l *actionEdgeLowerer) succession(sourceRef, targetRef ast.Node, edge ActionEdge) error { - sourceNode := resolveActionEndpoint(l.graph, sourceRef, true) - if sourceNode == nil { - return fmt.Errorf("action succession references undefined source node %s", successionEndText(sourceRef)) - } - edge.Source = sourceNode - targetNode := resolveActionEndpoint(l.graph, targetRef, false) - if targetNode == nil { - if ast.AsQualifiedName(targetRef) != nil && ast.SimpleName(targetRef) != "" { - l.gates = append(l.gates, pendingGate{edge: edge, target: targetRef}) - return nil - } - return fmt.Errorf("action succession references undefined target node %s", successionEndText(targetRef)) - } - edge.Target = targetNode - l.graph.Edges[sourceNode] = append(l.graph.Edges[sourceNode], edge) - return nil -} - // resolveGates reads each succession whose target named no node as leading to the // succession flow of that name out of its source (`first a if g then f;`): the flow's // own succession takes the gate's guard, name and weight, and its value moves only @@ -1210,7 +1458,25 @@ func (l *actionEdgeLowerer) succession(sourceRef, targetRef ast.Node, edge Actio func (l *actionEdgeLowerer) resolveGates() error { for _, gate := range l.gates { source, name := gate.edge.Source, ast.SimpleName(gate.target) - at := slices.IndexFunc(l.graph.DataFlows[source], func(flow ObjectFlow) bool { return flow.Name == name }) + at := -1 + lookupFound := false + if qn := ast.AsQualifiedName(gate.target); qn != nil { + segments := make([]string, len(qn.Parts)) + for i, part := range qn.Parts { + segments[i] = part.Text + } + if symbol, ok := resolve.FeatureSymbolInScope(gate.scope, segments); ok { + lookupFound = true + if symbol != nil { + at = slices.IndexFunc(l.graph.DataFlows[source], func(flow ObjectFlow) bool { + return flow.Decl == symbol.Decl + }) + } + } + } + if !lookupFound { + at = slices.IndexFunc(l.graph.DataFlows[source], func(flow ObjectFlow) bool { return flow.Name == name }) + } if at < 0 { if from := flowSourceNamed(l.graph, name); from != nil { return fmt.Errorf("action succession leads to flow %s, which leaves %s, not %s", @@ -1365,32 +1631,80 @@ func nodeAnswering(nodes []ast.Node, name string) ast.Node { // `inout` pin valued by a feature name is bound to that feature, as a feature value // binds the feature to its result, so what the node leaves in the pin writes back. func lowerFeatures(graph *ActionGraph, node *ast.Usage, scope *symbols.Scope) { + members := make([]effectiveActionMember, 0, len(node.Members)) + for _, member := range node.Members { + members = append(members, effectiveActionMember{Decl: unwrapMembership(member), Scope: scope}) + } + lowerEffectiveActionNodeFeatures(graph, node, scope, members) +} + +func lowerEffectiveActionNodeFeatures(graph *ActionGraph, node *ast.Usage, scope *symbols.Scope, members []effectiveActionMember) { if graph.Features == nil { graph.Features = make(map[ast.Node][]Feature) } recordNodeScope(graph, node, scope) var features []Feature - for _, member := range node.Members { - m, ok := unwrapMembership(member).(*ast.Usage) + for _, member := range members { + m, ok := member.Decl.(*ast.Usage) if !ok { continue } + featureScope := member.Scope + if featureScope == nil { + featureScope = scope + } + if m != node { + graph.recordDeclaredIn(m, featureScope) + } if m.IsAccept { // A message payload is the accept's output pin; `accept when/at/after` binds none. if m.Value == nil && m.Ident.Name != "" { - features = append(features, Feature{Name: m.Ident.Name, Direction: ast.DirOut, Node: m, Scope: scope}) + features = append(features, Feature{Name: m.Ident.Name, Direction: ast.DirOut, Node: m, Scope: featureScope}) } continue } - feature, ok := declaredFeature(m, scope) + feature, ok := declaredFeature(m, featureScope) if !ok { continue } features = append(features, feature) - if binding, ok := inoutValueBinding(node, m, feature.Name, scope); ok { + bindingScope := featureScope + if bindingScope != nil && bindingScope.Parent() != nil { + bindingScope = bindingScope.Parent() + } + if binding, ok := inoutValueBinding(node, m, feature.Name, bindingScope); ok { graph.Bindings = append(graph.Bindings, binding) } } + if typedBody, _ := mergedTypedActionBody(node, scope); typedBody { + generals, _ := resolve.ActionGeneralization(scope, true) + seen := make(map[string]bool, len(features)) + for _, feature := range features { + seen[feature.Name] = true + } + for _, general := range generals { + if general == nil || general.Node() == nil { + continue + } + for _, raw := range ast.DeclMembers(general.Node()) { + m, ok := unwrapMembership(raw).(*ast.Usage) + if !ok || !DeclaresNodeFeature(m) { + continue + } + name, _ := ast.EffectiveName(m) + if name == "" || seen[name] { + continue + } + feature, ok := declaredFeature(m, general) + if !ok { + continue + } + seen[name] = true + graph.recordDeclaredIn(m, general) + features = append(features, feature) + } + } + } graph.Features[node] = features } @@ -1619,23 +1933,13 @@ func DeclaresNodeFeature(m *ast.Usage) bool { return m.Direction != ast.DirNone || m.Kind == ast.UsageAttribute } -// lowerBody records a nested action node's statements and the message it waits -// for, so the executor reads them from the graph rather than walking the node's -// members again. -func lowerBody(graph *ActionGraph, node *ast.Usage, scope *symbols.Scope) { - for _, member := range BodyStatementMembers(node.Members) { - graph.Bodies[node] = append(graph.Bodies[node], lowerStatement(unwrapMembership(member), scope)) - } - lowerAccept(graph, node, scope) -} - // lowerNodeBody records the statements the body of an action node declares, so // a body the notation admits on a control node or a succession executes when a // token reaches it rather than being dropped. func lowerNodeBody(graph *ActionGraph, node ast.Node, members []ast.Node, scope *symbols.Scope) { body := childScope(scope, node) for _, member := range BodyStatementMembers(members) { - graph.Bodies[node] = append(graph.Bodies[node], lowerStatement(unwrapMembership(member), body)) + graph.Bodies[node] = append(graph.Bodies[node], lowerStatement(unwrapMembership(member), body, graph.resolver)) } } @@ -1701,7 +2005,7 @@ func BodyStatementMembers(members []ast.Node) []ast.Node { // lowerStatement lowers one executable body statement, in the scope it was // written in. Every form it recognizes is lowered losslessly; a form it does not // becomes Unsupported, so the executor reports it rather than skipping it. -func lowerStatement(member ast.Node, scope *symbols.Scope) Statement { +func lowerStatement(member ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) Statement { switch m := member.(type) { case *ast.SendStatement: return lowerSend(m, scope) @@ -1715,17 +2019,17 @@ func lowerStatement(member ast.Node, scope *symbols.Scope) Statement { Until: m.Until, Variable: variable, Collection: m.Collection, - Body: lowerBlock(m, m.Body, childScope(scope, m)), + Body: lowerBlock(m, m.Body, childScope(scope, m), resolver), Node: m, Scope: scope, } case *ast.IfActionNode: lowered := If{Condition: m.Condition, Node: m, Scope: scope} if m.Then != nil { - lowered.Then = lowerBlock(m.Then, m.Then.Body, childScope(scope, m.Then)) + lowered.Then = lowerBlock(m.Then, m.Then.Body, childScope(scope, m.Then), resolver) } if m.Else != nil { - block := lowerBlock(m.Else, m.Else.Body, childScope(scope, m.Else)) + block := lowerBlock(m.Else, m.Else.Body, childScope(scope, m.Else), resolver) lowered.Else = &block } return lowered @@ -1735,7 +2039,7 @@ func lowerStatement(member ast.Node, scope *symbols.Scope) Statement { target, terminates := terminateTarget(m, scope) return Effect{Kind: EffectTerminate, Node: m, Scope: scope, Terminates: terminates, Target: target, TargetExpr: m.Target} case *ast.Usage: - return lowerUsageStatement(m, scope) + return lowerUsageStatement(m, scope, resolver) default: return Unsupported{Description: statedFlowKeyword(member), Node: member, Scope: scope} } @@ -1824,7 +2128,7 @@ func lowerAssignment(m *ast.AssignmentActionNode, scope *symbols.Scope) Statemen // (`loop action charging { … } until charging.done`). An action usage naming // the action it performs is a performed action, which the host executes or // rejects as its own purity demands. -func lowerUsageStatement(m *ast.Usage, scope *symbols.Scope) Statement { +func lowerUsageStatement(m *ast.Usage, scope *symbols.Scope, resolver *resolve.Resolver) Statement { if m.IsTerminate { return Effect{Kind: EffectTerminate, Node: m, Scope: scope, Terminates: TerminateEnclosing} } @@ -1832,7 +2136,7 @@ func lowerUsageStatement(m *ast.Usage, scope *symbols.Scope) Statement { return stmt } if m.Kind == ast.UsageAction && m.IsBodyParameter { - return lowerBlock(m, m.Members, childScope(scope, m)) + return lowerBlock(m, m.Members, childScope(scope, m), resolver) } if m.Kind == ast.UsageAction && performsAction(m) { return performEffect(m, scope) @@ -1890,12 +2194,12 @@ func redefinedNames(u *ast.Usage) []string { // lowerBlock lowers the body of a loop or of one branch of a conditional. owner // is the node the block belongs to, which is the element that owns the block's // body-local namespace, and scope is the namespace it owns. -func lowerBlock(owner ast.Node, members []ast.Node, scope *symbols.Scope) Block { +func lowerBlock(owner ast.Node, members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) Block { if statesOwnFlow(members) { - return lowerStatedBlock(owner, members, scope) + return lowerStatedBlock(owner, members, scope, resolver) } if blockNeedsFlow(members) { - graph := lowerBlockFlow(members, scope, false) + graph := lowerBlockFlow(members, scope, false, resolver) return Block{Node: owner, Scope: scope, Graph: graph, Stated: len(graph.Accepts) > 0} } block := Block{Node: owner, Scope: scope} @@ -1904,7 +2208,7 @@ func lowerBlock(owner ast.Node, members []ast.Node, scope *symbols.Scope) Block if actual == nil || isAnnotation(actual) { continue } - block.Statements = append(block.Statements, lowerStatement(actual, scope)) + block.Statements = append(block.Statements, lowerStatement(actual, scope, resolver)) } return block } @@ -1916,6 +2220,9 @@ func isAnnotation(n ast.Node) bool { case *ast.Comment, *ast.Documentation, *ast.TextualRepresentation: return true } + if usage, ok := n.(*ast.Usage); ok && usage.Kind == ast.UsageMetadata { + return true + } return false } @@ -2079,23 +2386,33 @@ func resolveActionEndpoint(graph *ActionGraph, ref ast.Node, source bool) ast.No if node != nil { return node } + name := ast.SimpleName(ref) + if impliedMarker(name, source, graph.Initial == nil) { + if source { + return ensureImpliedStart(graph, ref) + } + return ensureImpliedDone(graph, ref) + } if node = ensureInheritedActionNode(graph, ref); node != nil { return node } + return nil +} - name := ast.SimpleName(ref) - if !impliedMarker(name, source, graph.Initial == nil) { - return nil - } - if source { - first := &ast.QualifiedName{} - first.NodeSpan = ref.Span() - first.SetSingleton(ast.NameSegment{Text: "start", Span: ref.Span()}) - initial := &ast.InitialNode{NodeBase: ast.NodeBase{NodeSpan: ref.Span()}, First: first} - graph.Initial = initial - graph.Nodes = append(graph.Nodes, initial) - return initial +func ensureImpliedStart(graph *ActionGraph, ref ast.Node) ast.Node { + if graph.Initial != nil { + return graph.Initial } + first := &ast.QualifiedName{} + first.NodeSpan = ref.Span() + first.SetSingleton(ast.NameSegment{Text: "start", Span: ref.Span()}) + initial := &ast.InitialNode{NodeBase: ast.NodeBase{NodeSpan: ref.Span()}, First: first} + graph.Initial = initial + graph.Nodes = append(graph.Nodes, initial) + return initial +} + +func ensureImpliedDone(graph *ActionGraph, ref ast.Node) ast.Node { for _, final := range graph.Finals { if getNodeName(final) == "done" { return final @@ -2397,15 +2714,6 @@ func flowEndText(end ast.Node) string { return "(nothing)" } -// parsePinReference extracts node and pin name from a qualified reference. -// Format: "nodeName.pinName" or just "nodeName" (pin = "") -func parsePinReference(nodes []ast.Node, qname *ast.QualifiedName) (ast.Node, string) { - if qname == nil { - return nil, "" - } - return flowEnd(nodesNamed(nodes), qname) -} - // statementKeyword names a body statement for a diagnostic. func statementKeyword(node ast.Node) string { switch n := node.(type) { diff --git a/internal/ir/lower/action_inherited.go b/internal/ir/lower/action_inherited.go new file mode 100644 index 0000000000..3411c2dd00 --- /dev/null +++ b/internal/ir/lower/action_inherited.go @@ -0,0 +1,307 @@ +package lower + +import ( + "fmt" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" + "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" +) + +func actionDescription(scope *symbols.Scope) string { + if scope != nil { + if name := ast.SimpleName(scope.Node()); name != "" { + return name + } + } + return "action" +} + +func mergeInheritedActionContent(graph *ActionGraph, bodies []*symbols.Scope) ([]pendingGate, error) { + if graph == nil { + return nil, nil + } + ordered := orderedAssertionsForGraph(graph, bodies) + incompatible := make(map[ast.Node]ast.Node) + allBodies := append([]*symbols.Scope{graph.Scope}, bodies...) + for _, body := range allBodies { + if body == nil || body.Node() == nil { + continue + } + for _, member := range ast.DeclMembers(body.Node()) { + decl := unwrapMembership(member) + if lowerableActionNodeInBody(decl, ordered) { + continue + } + for _, target := range resolve.ActionNodeRedefinitionTargets(body, decl, false) { + incompatible[target] = decl + } + } + } + graph.replacedByIncompatible = incompatible + + seenBodies := make(map[*symbols.Scope]bool) + for _, body := range bodies { + if body == nil || seenBodies[body] { + continue + } + seenBodies[body] = true + if !hasInheritedBody(graph, body) { + graph.inherited = append(graph.inherited, Inherited{Decl: body.Node(), Body: body}) + } + for _, member := range ast.DeclMembers(body.Node()) { + decl := unwrapMembership(member) + if !lowerableActionNodeInBody(decl, ordered) { + continue + } + if initial, marker := decl.(*ast.InitialNode); marker { + graph.recordDeclaredIn(initial, body) + continue + } + if final, ok := decl.(*ast.FinalNode); ok { + ensureInheritedDone(graph, final, body) + continue + } + matches := inheritedNodeMatches(graph, body, decl) + if len(matches) > 1 { + return nil, fmt.Errorf("%w: %s", ErrAmbiguousInheritedStep, ast.SimpleName(decl)) + } + if len(matches) == 1 { + recordInheritedPerform(graph, matches[0], decl, body) + continue + } + if replacement := incompatible[decl]; replacement != nil { + continue + } + if shadowsInheritedActionNode(graph, body, decl) { + continue + } + ensureDeclaredActionNode(graph, decl, body) + } + } + + for _, body := range bodies { + if body == nil { + continue + } + for _, member := range ast.DeclMembers(body.Node()) { + decl := unwrapMembership(member) + if !lowerableActionNodeInBody(decl, ordered) { + continue + } + if _, marker := decl.(*ast.InitialNode); marker { + continue + } + if _, marker := decl.(*ast.FinalNode); marker { + continue + } + if len(inheritedNodeMatches(graph, body, decl)) > 1 { + return nil, fmt.Errorf("%w: %s", ErrAmbiguousInheritedStep, ast.SimpleName(decl)) + } + } + } + + for _, body := range bodies { + if body == nil || graph.Initial != nil { + continue + } + for _, member := range ast.DeclMembers(body.Node()) { + initial, ok := unwrapMembership(member).(*ast.InitialNode) + if !ok || initial.Successor != nil { + continue + } + if node := inheritedNodeLookup(graph, body)(ast.SimpleName(initial.First)); node != nil { + graph.Initial = node + graph.recordDeclaredIn(initial, body) + break + } + if ast.SimpleName(initial.First) == "start" { + graph.Initial = resolveActionEndpoint(graph, initial.First, true) + graph.recordDeclaredIn(initial, body) + break + } + } + } + + seenEdges := make(map[ast.Node]bool) + var gates []pendingGate + for _, body := range bodies { + if body == nil { + continue + } + lookup := inheritedNodeLookup(graph, body) + lowerer := &actionEdgeLowerer{ + graph: graph, + scope: body, + weights: &probabilityReader{resolver: graph.resolver, scope: body}, + nodes: lookup, + incompatible: incompatible, + } + for _, member := range ast.DeclMembers(body.Node()) { + decl := unwrapMembership(member) + if !inheritedActionEdge(decl) || seenEdges[decl] { + continue + } + seenEdges[decl] = true + graph.recordDeclaredIn(decl, body) + if err := lowerer.member(decl); err != nil { + return nil, err + } + } + gates = append(gates, lowerer.gates...) + } + return gates, nil +} + +func recordInheritedPerform(graph *ActionGraph, replacement, redefined ast.Node, declaringScope *symbols.Scope) { + node, ok := redefined.(*ast.Usage) + if !ok || node.Kind != ast.UsageAction || graph == nil { + return + } + target := typingTarget(node) + if target == nil { + return + } + usage, ok := replacement.(*ast.Usage) + if !ok || usage.Kind != ast.UsageAction || typingTarget(usage) != nil { + return + } + if graph.Performs == nil { + graph.Performs = make(map[ast.Node]PerformedType) + } + if _, recorded := graph.Performs[replacement]; !recorded { + graph.Performs[replacement] = PerformedType{Target: target, Scope: declaringScope} + } +} + +func hasInheritedBody(graph *ActionGraph, body *symbols.Scope) bool { + for _, inherited := range graph.inherited { + if inherited.Body == body { + return true + } + } + return false +} + +func lowerableActionNode(decl ast.Node) bool { + switch n := decl.(type) { + case *ast.Usage: + return n.Kind == ast.UsageAction || IsCaseNode(n) + case *ast.InitialNode, *ast.FinalNode, *ast.ForkNode, *ast.JoinNode, + *ast.MergeNode, *ast.DecisionNode, *ast.ActionExecutionNode, + *ast.PerformActionNode, *ast.WhileLoopActionNode, *ast.IfActionNode, + *ast.AssignmentActionNode, *ast.SendStatement, *ast.TerminateStatement: + return true + default: + return false + } +} + +func orderedAssertionsForGraph(graph *ActionGraph, bodies []*symbols.Scope) map[*ast.Usage]bool { + members := make([]ast.Node, 0) + seen := make(map[ast.Node]bool) + appendBody := func(body *symbols.Scope) { + if body == nil || body.Node() == nil { + return + } + for _, member := range ast.DeclMembers(body.Node()) { + decl := unwrapMembership(member) + if decl == nil || seen[decl] { + continue + } + seen[decl] = true + members = append(members, decl) + } + } + appendBody(graph.Scope) + for _, body := range bodies { + appendBody(body) + } + return orderedAssertions(members) +} + +func lowerableActionNodeInBody(decl ast.Node, ordered map[*ast.Usage]bool) bool { + assertion, ok := decl.(*ast.Usage) + if !ok || !resolve.IsAssertion(assertion) { + return lowerableActionNode(decl) + } + return ordered[assertion] +} + +func inheritedActionEdge(decl ast.Node) bool { + switch n := decl.(type) { + case *ast.InitialNode: + return n.Successor != nil + case *ast.SuccessionEdge, *ast.ControlFlowEdge, *ast.TransitionMember, *ast.ObjectFlowEdge: + return true + case *ast.Usage: + return n.Kind == ast.UsageSuccession + default: + return false + } +} + +func inheritedNodeMatches(graph *ActionGraph, body *symbols.Scope, decl ast.Node) []ast.Node { + var matches []ast.Node + seen := make(map[ast.Node]bool) + for _, node := range graph.Nodes { + matchesNode := node == decl + if usage, ok := node.(*ast.Usage); ok && !matchesNode { + owner := graph.Scope + if scope := graph.Scopes[node]; scope != nil && scope.Parent() != nil { + owner = scope.Parent() + } + matchesNode = resolve.RedefinesActionNode(owner, usage, decl) + } + if matchesNode && !seen[node] { + seen[node] = true + matches = append(matches, node) + } + } + return matches +} + +func shadowsInheritedActionNode(graph *ActionGraph, body *symbols.Scope, decl ast.Node) bool { + name := getNodeName(decl) + if name == "" { + return false + } + for _, node := range graph.Nodes { + if node == decl || !nodeAnswersTo(node, name) { + continue + } + owner := graph.Scope + if scope := graph.Scopes[node]; scope != nil && scope.Parent() != nil { + owner = scope.Parent() + } + if owner == nil { + continue + } + generals, _ := resolve.ActionGeneralization(owner, true) + for _, general := range generals { + if general == body { + return true + } + } + } + return false +} + +func ensureInheritedDone(graph *ActionGraph, decl *ast.FinalNode, body *symbols.Scope) ast.Node { + graph.recordDeclaredIn(decl, body) + if graph.doneMarker != nil { + return graph.doneMarker + } + for _, final := range graph.Finals { + if getNodeName(final) == "done" { + graph.doneMarker = final + return final + } + } + final := &ast.FinalNode{NodeBase: ast.NodeBase{NodeSpan: decl.Span()}} + graph.Finals = append(graph.Finals, final) + graph.Nodes = append(graph.Nodes, final) + graph.doneMarker = final + graph.recordDeclaredIn(final, body) + return final +} diff --git a/internal/ir/lower/action_inherited_test.go b/internal/ir/lower/action_inherited_test.go index 5646da139c..5e79bd347e 100644 --- a/internal/ir/lower/action_inherited_test.go +++ b/internal/ir/lower/action_inherited_test.go @@ -1,12 +1,16 @@ package lower import ( + "errors" "testing" + "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" + "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" "github.com/Open-MBEE/OpenSysML/internal/syntax/parser" "github.com/Open-MBEE/OpenSysML/internal/syntax/source" + "github.com/Open-MBEE/OpenSysML/internal/workspace/libs" ) func TestToActionGraphInheritedActionNode(t *testing.T) { @@ -28,6 +32,118 @@ func TestToActionGraphInheritedActionNode(t *testing.T) { } } +func TestToActionGraphRedefinedInheritedActionNodeBodyBindings(t *testing.T) { + src := ` + action def Adder { + in a : Integer; + in b : Integer; + out sum : Integer; + first step; + action step { assign sum := a + b; } + } + action def Base { + attribute x : Integer = 5; + action add : Adder { in b = 2; } + bind add.a = x; + } + action def Redefined :> Base { + action add :>> add : Adder { in b = 100; } + first start then add; + } + ` + graph := scopedActionGraph(t, src, "Redefined") + add := namedNode(graph, "add") + if add == nil { + t.Fatal("redefined inherited action node was not collected") + } + var bFeatures []Feature + for _, feature := range graph.Features[add] { + if feature.Name == "b" { + bFeatures = append(bFeatures, feature) + } + } + if len(bFeatures) != 1 { + t.Fatalf("add has %d features named b, want only its specialized binding", len(bFeatures)) + } + binding, ok := bFeatures[0].Value.(*ast.LiteralInteger) + if !ok || binding.Value != "100" { + t.Fatalf("add.b value = %#v, want specialized binding 100", bFeatures[0].Value) + } +} + +func TestToActionGraphInheritsTerminateUsageBodyFlow(t *testing.T) { + src := ` + action def G { + out attribute x : Integer = 0; + out attribute later : Integer = 0; + first start then stop; + action stop terminate { + first start; + then action inner { assign x := 1; } + then done; + } + then action tail { assign later := 1; } + then done; + } + action def S :> G; + ` + derived, scope, root := inheritedActionDecl(t, src, "S") + graph, err := ToActionGraph(derived, scope) + if err != nil { + t.Fatalf("ToActionGraph: %v", err) + } + stop := actionMember(t, root, "G", "stop") + if namedNode(graph, "stop") != stop { + t.Fatalf("stop node = %p, want inherited terminate usage %p", namedNode(graph, "stop"), stop) + } + body := graph.Bodies[stop] + if len(body) != 2 { + t.Fatalf("stop body has %d statements, want stated flow and terminate effect", len(body)) + } + flow, ok := body[0].(Block) + if !ok || flow.Graph == nil { + t.Fatalf("stop body first statement = %T, want flow block", body[0]) + } + if namedNode(flow.Graph, "inner") == nil { + t.Fatal("inherited terminate body flow has no inner action step") + } + if _, ok := graph.TerminateUsage(stop); !ok { + t.Fatal("inherited terminate usage has no terminate effect") + } +} + +func TestToActionGraphInheritsCaseAsCaseStep(t *testing.T) { + src := ` + action def G { + analysis nested { + return : Integer; + attribute x : Integer := 1; + action multiply { assign x := x * 10; } + action add { assign x := x + 2; } + x + } + first start then nested; + first nested then done; + } + action def S :> G; + ` + derived, scope, root := inheritedActionDecl(t, src, "S") + graph, err := ToActionGraph(derived, scope) + if err != nil { + t.Fatalf("ToActionGraph: %v", err) + } + nested := actionMember(t, root, "G", "nested") + if namedNode(graph, "nested") != nested { + t.Fatalf("nested node = %p, want inherited case %p", namedNode(graph, "nested"), nested) + } + if scope := graph.Scopes[nested]; scope == nil || scope.Node() != nested { + t.Fatalf("inherited case scope = %v, want its declaring case scope", scope) + } + if _, owns := graph.Subflows[nested]; owns { + t.Fatal("inherited case has an action subflow; its body belongs to case lowering") + } +} + func TestToActionGraphInheritedActionNodeThroughTwoSpecializations(t *testing.T) { src := ` action def Grand { action a; } @@ -44,6 +160,35 @@ func TestToActionGraphInheritedActionNodeThroughTwoSpecializations(t *testing.T) } } +func TestToActionGraphInheritsAssertionOrderedByIntermediateGeneral(t *testing.T) { + src := ` + action def G { assert constraint check { true } } + action def M :> G { + first start then check; + first check then done; + } + action def S :> M; + ` + derived, scope, root := inheritedActionDecl(t, src, "S") + graph, err := ToActionGraph(derived, scope) + if err != nil { + t.Fatalf("ToActionGraph: %v", err) + } + assertion := actionMember(t, root, "G", "check") + if got := namedNode(graph, "check"); got != assertion { + t.Fatalf("check node = %p, want inherited assertion %p", got, assertion) + } + count := 0 + for _, node := range graph.Nodes { + if node == assertion { + count++ + } + } + if count != 1 { + t.Fatalf("inherited assertion occurs %d times in graph nodes, want once", count) + } +} + func TestToActionGraphQualifiedInheritedActionNode(t *testing.T) { src := ` action def Base { action a; } @@ -122,8 +267,8 @@ func TestToActionGraphInheritedActionNodeCycleTerminates(t *testing.T) { action def Derived :> A { first start then missing; } ` derived, scope, _ := inheritedActionDecl(t, src, "Derived") - if _, err := ToActionGraph(derived, scope); err == nil { - t.Fatal("expected an undefined endpoint error") + if _, err := ToActionGraph(derived, scope); !errors.Is(err, ErrCyclicSpecialization) { + t.Fatalf("ToActionGraph error = %v, want ErrCyclicSpecialization", err) } } @@ -193,6 +338,1019 @@ func namedNode(graph *ActionGraph, name string) ast.Node { return nil } +func TestToActionGraphInheritedActionContentAndMultiplicity(t *testing.T) { + idx, base, keep, narrow := inheritedStepModel(t) + resolver := resolve.New(idx) + graph, err := ToActionGraphWith(keep.Decl, keep.Scope, resolver) + if err != nil { + t.Fatalf("lower Keep: %v", err) + } + replacement := namedNode(graph, "a") + if replacement == nil { + t.Fatal("Keep graph has no action node answering to a") + } + var count int + for _, node := range graph.Nodes { + if nodeAnswersTo(node, "a") { + count++ + } + } + if count != 1 { + t.Fatalf("Keep graph has %d nodes answering to a, want one", count) + } + baseStep, ok := base.Scope.LookupLocal("a") + if !ok { + t.Fatal("Base has no action node a") + } + var inheritedEdge ast.Node + for _, edges := range graph.Edges { + for _, edge := range edges { + if edge.Target == replacement && edge.Decl != nil && graph.declaredIn[edge.Decl] == base.Scope { + inheritedEdge = edge.Decl + } + if edge.Target == baseStep.Decl { + t.Fatal("inherited succession still targets Base::a") + } + } + } + if inheritedEdge == nil { + t.Fatal("Keep graph has no inherited succession targeting its redefining node") + } + baseNode, ok := baseStep.Scope.Node().(*ast.Usage) + if !ok { + t.Fatalf("Base::a declaration is %T, want *ast.Usage", baseStep.Scope.Node()) + } + var inheritedStatement ast.Node + for _, member := range baseNode.Members { + if _, ok := unwrapMembership(member).(*ast.AssignmentActionNode); ok { + inheritedStatement = unwrapMembership(member) + break + } + } + if inheritedStatement == nil { + t.Fatal("Base::a has no assignment statement") + } + if got := graph.declaredIn[inheritedStatement]; got != baseStep.Scope { + t.Fatalf("inherited statement scope = %p, want Base::a scope %p", got, baseStep.Scope) + } + + model := semantics.NewModel(resolver) + multiplicity, scope := graph.StepMultiplicity(replacement, model) + if multiplicity == nil || scope != base.Scope { + t.Fatalf("Keep::a multiplicity = %v in %p, want Base's declaration in %p", multiplicity, scope, base.Scope) + } + if got, err := graph.StepCount(replacement, model); err != nil || got != 3 { + t.Fatalf("Keep::a StepCount = %d, %v; want 3", got, err) + } + + narrowGraph, err := ToActionGraphWith(narrow.Decl, narrow.Scope, resolver) + if err != nil { + t.Fatalf("lower Narrow: %v", err) + } + narrowStep := namedNode(narrowGraph, "a") + multiplicity, scope = narrowGraph.StepMultiplicity(narrowStep, model) + if multiplicity == nil || scope != narrowGraph.Scopes[narrowStep] { + t.Fatalf("Narrow::a multiplicity = %v in %p, want its own node scope %p", multiplicity, scope, narrowGraph.Scopes[narrowStep]) + } + if got, err := narrowGraph.StepCount(narrowStep, model); err != nil || got != 2 { + t.Fatalf("Narrow::a StepCount = %d, %v; want 2", got, err) + } +} + +func TestToActionGraphInheritsPerformStatements(t *testing.T) { + src := ` + action def Foo; + action def Base { perform action p : Foo; } + action def Derived :> Base; + ` + derived, scope, root := inheritedActionDecl(t, src, "Derived") + graph, err := ToActionGraph(derived, scope) + if err != nil { + t.Fatalf("lower Derived: %v", err) + } + base := actionDefinition(t, root, "Base") + baseScope := scope.Parent().ChildFor(base) + for _, node := range graph.Nodes { + usage, ok := node.(*ast.Usage) + if !ok || !IsPerformedActionUsage(usage) { + continue + } + if got := graph.declaredIn[node]; got != baseScope { + t.Fatalf("inherited perform scope = %p, want Base scope %p", got, baseScope) + } + return + } + t.Fatal("Derived graph has no inherited perform node") +} + +func TestToActionGraphInheritedFlowStartsAlongsideUnorderedLocalNode(t *testing.T) { + src := ` + action def B1 { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + action def P0 :> B1; + action def P3 :> B1 { + action b { assign c := c + 100; } + } + ` + decl, scope, _ := inheritedActionDecl(t, src, "P3") + graph, err := ToActionGraph(decl, scope) + if err != nil { + t.Fatalf("lower P3: %v", err) + } + StartFlow(graph) + if graph.Initial == nil || getNodeName(graph.Initial) != "start" { + t.Fatalf("P3 initial = %v, want the inherited start marker", graph.Initial) + } + a, b := namedNode(graph, "a"), namedNode(graph, "b") + if a == nil || b == nil { + t.Fatalf("P3 nodes: inherited a = %v, local b = %v", a, b) + } + if edges := graph.Edges[graph.Initial]; len(edges) != 1 || edges[0].Target != a { + t.Fatalf("inherited start edges = %v, want one edge to a", edges) + } + if len(graph.Concurrent) != 1 || graph.Concurrent[0] != b { + t.Fatalf("concurrent starts = %d, want only local b", len(graph.Concurrent)) + } +} + +func TestToActionGraphInheritsGatedSuccessionFlow(t *testing.T) { + src := ` + private import ScalarValues::*; + action def G { + attribute go : Boolean = true; + first start then a; + action a { out y : Integer; assign y := 1; } + action c { out y : Integer; assign y := 2; } + action b { in v : Integer; } + first a if go then f; + first a if not go then c; + succession flow f of Integer from a.y to b.v; + succession flow of Integer from c.y to b.v; + } + action def S :> G; + ` + decl, scope, _ := inheritedActionDecl(t, src, "S") + graph, err := ToActionGraph(decl, scope) + if err != nil { + t.Fatalf("lower S: %v", err) + } + + a := namedNode(graph, "a") + if a == nil { + t.Fatal("S graph has no inherited action a") + } + var flow *ObjectFlow + for i := range graph.DataFlows[a] { + if graph.DataFlows[a][i].Name == "f" { + flow = &graph.DataFlows[a][i] + break + } + } + if flow == nil || flow.Kind != FlowSuccession || flow.Gate == nil { + t.Fatalf("inherited succession flow = %+v, want gated succession flow f", flow) + } + for _, edge := range graph.Edges[a] { + if edge.Carries && edge.Decl == flow.Decl { + if edge.Guard == nil || edge.Gate != flow.Gate { + t.Fatalf("carrying edge = %+v, want the inherited gate and guard", edge) + } + return + } + } + t.Fatal("inherited succession flow f has no carrying edge") +} + +func TestToActionGraphOwnFlowConnectsInheritedActionNodes(t *testing.T) { + src := ` + private import ScalarValues::*; + action def G { + action a { out y : Integer; assign y := 7; } + action b { in v : Integer; } + first start then a; + first a then b; + first b then done; + } + action def S :> G { + flow f2 from a.y to b.v; + } + ` + decl, scope, root := inheritedActionDecl(t, src, "S") + graph, err := ToActionGraph(decl, scope) + if err != nil { + t.Fatalf("lower S: %v", err) + } + + a, b := namedNode(graph, "a"), namedNode(graph, "b") + if a == nil || b == nil { + t.Fatalf("inherited action nodes: a = %v, b = %v", a, b) + } + if edges := graph.Edges[graph.Initial]; len(edges) != 1 || edges[0].Target != a { + t.Fatalf("initial edges = %v, want one edge to a", edges) + } + if edges := graph.Edges[a]; len(edges) != 1 || edges[0].Target != b { + t.Fatalf("a edges = %v, want one edge to b", edges) + } + done := namedNode(graph, "done") + if edges := graph.Edges[b]; len(edges) != 1 || edges[0].Target != done { + t.Fatalf("b edges = %v, want one edge to done", edges) + } + flowDecl := actionMember(t, root, "S", "f2") + var ownFlow *ObjectFlow + for i := range graph.DataFlows[a] { + if graph.DataFlows[a][i].Decl == flowDecl { + ownFlow = &graph.DataFlows[a][i] + break + } + } + if ownFlow == nil || ownFlow.Target != b || ownFlow.SourcePin != "y" || ownFlow.TargetPin != "v" { + t.Fatalf("S's flow = %+v, want a.y to b.v", ownFlow) + } + for _, inherited := range []ast.Node{a, b} { + count := 0 + for _, node := range graph.Nodes { + if node == inherited { + count++ + } + } + if count != 1 { + t.Fatalf("inherited node %s occurs %d times, want once", getNodeName(inherited), count) + } + } +} + +func TestToActionGraphKeepsDifferentlyGuardedInheritedSuccession(t *testing.T) { + src := ` + private import ScalarValues::*; + action def Base { + attribute x : Integer = 1; + action a; + action b; + first a if x < 3 then b; + } + action def S :> Base { + first a if x > 5 then b; + } + ` + decl, scope, _ := inheritedActionDecl(t, src, "S") + graph, err := ToActionGraph(decl, scope) + if err != nil { + t.Fatalf("lower S: %v", err) + } + a, b := namedNode(graph, "a"), namedNode(graph, "b") + if a == nil || b == nil { + t.Fatal("S graph is missing inherited action nodes a or b") + } + var edges []ActionEdge + for _, edge := range graph.Edges[a] { + if edge.Source == a && edge.Target == b { + edges = append(edges, edge) + } + } + if len(edges) != 2 { + t.Fatalf("a to b edges = %d, want both inherited and owned successions", len(edges)) + } + if edges[0].Guard == nil || edges[1].Guard == nil || edges[0].Guard == edges[1].Guard { + t.Fatalf("a to b guards = (%v, %v), want distinct non-nil guards", edges[0].Guard, edges[1].Guard) + } +} + +func TestToActionGraphDeduplicatesRestatedUnguardedSuccession(t *testing.T) { + src := ` + action def Base { + action a; + action b; + first a then b; + } + action def S :> Base { + first a then b; + } + ` + decl, scope, _ := inheritedActionDecl(t, src, "S") + graph, err := ToActionGraph(decl, scope) + if err != nil { + t.Fatalf("lower S: %v", err) + } + a, b := namedNode(graph, "a"), namedNode(graph, "b") + if a == nil || b == nil { + t.Fatal("S graph is missing action nodes a or b") + } + var edges int + for _, edge := range graph.Edges[a] { + if edge.Source == a && edge.Target == b { + edges++ + } + } + if edges != 1 { + t.Fatalf("a to b edges = %d, want one restated unguarded succession", edges) + } +} + +func TestToActionGraphResolvesInheritedGateInDeclaringScope(t *testing.T) { + src := ` + private import ScalarValues::*; + action def G { + attribute go : Boolean = false; + action a { out y : Integer; assign y := 7; } + action b { in v : Integer; } + first start then a; + first a if go then f; + succession flow f of Integer from a.y to b.v; + } + action def S :> G { + action c { in v : Integer; } + succession flow f of Integer from a.y to c.v; + } + ` + decl, scope, root := inheritedActionDecl(t, src, "S") + graph, err := ToActionGraph(decl, scope) + if err != nil { + t.Fatalf("lower S: %v", err) + } + + a := namedNode(graph, "a") + if a == nil { + t.Fatal("S graph has no inherited action a") + } + generalFlowDecl := actionMember(t, root, "G", "f") + specializedFlowDecl := actionMember(t, root, "S", "f") + var generalFlow, specializedFlow *ObjectFlow + for i := range graph.DataFlows[a] { + flow := &graph.DataFlows[a][i] + switch flow.Decl { + case generalFlowDecl: + generalFlow = flow + case specializedFlowDecl: + specializedFlow = flow + } + } + b, c := namedNode(graph, "b"), namedNode(graph, "c") + if generalFlow == nil || generalFlow.Target != b || generalFlow.Gate == nil { + t.Fatalf("G's flow = %+v, want gated flow to b", generalFlow) + } + if specializedFlow == nil || specializedFlow.Target != c { + t.Fatalf("S's flow = %+v, want flow to c", specializedFlow) + } + if specializedFlow.Gate != nil { + t.Fatalf("S's flow gate = %v, want nil", specializedFlow.Gate) + } + + var gatedEdge, ownEdge *ActionEdge + for i := range graph.Edges[a] { + edge := &graph.Edges[a][i] + switch edge.Decl { + case generalFlowDecl: + if edge.Carries { + gatedEdge = edge + } + case specializedFlowDecl: + if edge.Carries { + ownEdge = edge + } + } + } + if gatedEdge == nil || gatedEdge.Guard == nil || gatedEdge.Gate != generalFlow.Gate { + t.Fatalf("G's carrying edge = %+v, want its gate and guard", gatedEdge) + } + if ownEdge == nil || ownEdge.Guard != nil || ownEdge.Gate != nil { + t.Fatalf("S's carrying edge = %+v, want ungated", ownEdge) + } +} + +func TestToActionGraphCompositeRedefinitionMergesInheritedContent(t *testing.T) { + tests := []struct { + name string + src string + }{ + { + name: "inherited_statement_with_owned_subaction", + src: ` + action def Base { + out attribute c : Integer = 0; + action a { assign c := c + 1; } + } + action def Derived :> Base { + action a :>> a { + action b { assign c := c + 10; } + } + } + `, + }, + { + name: "inherited_subaction_with_owned_statement", + src: ` + action def Base { + out attribute c : Integer = 0; + action a { + action b { assign c := c + 10; } + } + } + action def Derived :> Base { + action a :>> a { assign c := c + 1; } + } + `, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + graph := scopedActionGraph(t, test.src, "Derived") + a := namedNode(graph, "a") + if a == nil { + t.Fatal("Derived graph has no redefined action a") + } + subflow := graph.Subflows[a] + if subflow == nil || subflow.Graph == nil { + t.Fatal("redefined action a has no composite subflow") + } + if b := namedNode(subflow.Graph, "b"); b == nil { + t.Fatal("redefined action a has no inherited or owned subaction b") + } + + seen := make(map[*ActionGraph]bool) + var assignmentCount func(*ActionGraph) int + assignmentCount = func(current *ActionGraph) int { + if current == nil || seen[current] { + return 0 + } + seen[current] = true + count := 0 + for _, body := range current.Bodies { + for _, statement := range body { + if assign, ok := statement.(Assign); ok && assign.Target == "c" { + count++ + } + } + } + for _, nested := range current.Subflows { + if nested != nil { + count += assignmentCount(nested.Graph) + } + } + return count + } + if got := assignmentCount(subflow.Graph); got != 2 { + t.Fatalf("redefined action flow has %d assignments to c, want inherited and owned statements", got) + } + }) + } +} + +func TestToActionGraphMergesBodyStatingTypedActionNode(t *testing.T) { + src := ` + action def B1 { + attribute c : Integer = 0; + first start then a; + action a { assign c := c + 1; } + then done; + } + action def Host { + action y : B1 { action b { assign c := c + 100; } } + } + ` + host, scope, _ := inheritedActionDecl(t, src, "Host") + graph, err := ToActionGraph(host, scope) + if err != nil { + t.Fatalf("lower Host: %v", err) + } + y, ok := namedNode(graph, "y").(*ast.Usage) + if !ok { + t.Fatalf("y node = %T, want *ast.Usage", namedNode(graph, "y")) + } + subflow := graph.Subflows[y] + if subflow == nil || subflow.Graph == nil { + t.Fatalf("y subflow = %#v, want a merged typed graph", subflow) + } + if _, ok := graph.MergedTypedSubflows[y]; !ok { + t.Fatal("typed subflow was not marked as already merged") + } + a, b := namedNode(subflow.Graph, "a"), namedNode(subflow.Graph, "b") + if a == nil || b == nil { + t.Fatalf("typed subflow nodes: inherited a = %v, local b = %v", a, b) + } + if edges := subflow.Graph.Edges[subflow.Graph.Initial]; len(edges) != 1 || edges[0].Target != a { + t.Fatalf("typed start edges = %v, want one edge to inherited a", edges) + } + if len(subflow.Graph.Concurrent) != 1 || subflow.Graph.Concurrent[0] != b { + t.Fatalf("typed concurrent starts = %v, want local b", subflow.Graph.Concurrent) + } +} + +func TestToActionGraphBodyStatingTypedNodeIncludesTypedPins(t *testing.T) { + src := ` + action def Scale { + in x : Integer; + in factor : Integer = 10; + out y : Integer; + first start then scaling; + action scaling { assign y := x * factor; } + then done; + } + action def Host { + action scaled : Scale { + in x = 3; + assign y := y + 1; + } + } + ` + host, scope, _ := inheritedActionDecl(t, src, "Host") + graph, err := ToActionGraph(host, scope) + if err != nil { + t.Fatalf("lower Host: %v", err) + } + node, ok := namedNode(graph, "scaled").(*ast.Usage) + if !ok { + t.Fatalf("scaled node = %T, want *ast.Usage", namedNode(graph, "scaled")) + } + features := make(map[string]Feature, len(graph.Features[node])) + for _, feature := range graph.Features[node] { + features[feature.Name] = feature + } + for name, direction := range map[string]ast.FeatureDirection{ + "x": ast.DirIn, + "factor": ast.DirIn, + "y": ast.DirOut, + } { + feature, ok := features[name] + if !ok || feature.Direction != direction { + t.Errorf("typed feature %q = %#v, present %v; want direction %v", name, feature, ok, direction) + } + } + if features["factor"].Value == nil { + t.Error("typed default for factor was not lowered") + } +} + +func TestToActionGraphTypedBodyPreservesInheritedPinBinding(t *testing.T) { + src := ` + action def T { + in source : Integer = 1; + out y : Integer = source; + } + action def Host { + action m : T { assign source := 2; } + } + ` + host, scope, root := inheritedActionDecl(t, src, "Host") + graph, err := ToActionGraph(host, scope) + if err != nil { + t.Fatalf("lower Host: %v", err) + } + typedScope := scope.Parent().ChildFor(actionDefinition(t, root, "T")) + node, ok := namedNode(graph, "m").(*ast.Usage) + if !ok { + t.Fatalf("m node = %T, want *ast.Usage", namedNode(graph, "m")) + } + for _, feature := range graph.Features[node] { + if feature.Name == "y" { + if !feature.Binding { + t.Fatal("inherited y pin is not a binding") + } + if feature.Scope != typedScope { + t.Fatalf("inherited y pin scope = %v, want its typed action's scope %v", feature.Scope, typedScope) + } + pin, ok := feature.Node.(*ast.Usage) + if !ok || getNodeName(pin) != "y" { + t.Fatalf("inherited y pin node = %T, want T::y", feature.Node) + } + return + } + } + t.Fatal("typed action node has no inherited y pin") +} + +func TestToActionGraphPinOnlyTypedUsageKeepsInvocation(t *testing.T) { + src := ` + metadata def Marker; + action def Base { + first start then a; + action a; + then done; + } + action usage : Base { + metadata marker : Marker; + in value = 3; + } + ` + p := parser.New(source.New("test.sysml", []byte(src))) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("parse diagnostics: %v", p.Diagnostics) + } + var usage *ast.Usage + for _, member := range root.Members { + candidate, ok := unwrapMembership(member).(*ast.Usage) + if ok && candidate.Kind == ast.UsageAction && getNodeName(candidate) == "usage" { + usage = candidate + break + } + } + if usage == nil { + t.Fatal("action usage not found") + } + idx := symbols.NewIndexFromDoc("test.sysml", root) + scope := idx.DocumentRoot("test.sysml").ChildFor(usage) + if scope == nil { + t.Fatal("scope for action usage missing") + } + graph, err := ToActionGraphWith(usage, scope, resolve.New(idx)) + if err != nil { + t.Fatalf("lower action usage: %v", err) + } + if inherited := namedNode(graph, "a"); inherited != nil { + t.Fatalf("pin-only typed usage adopted type body node %v", inherited) + } + if _, merged := graph.MergedTypedSubflows[usage]; merged { + t.Fatal("pin-only typed usage with metadata was marked as a merged subflow") + } +} + +func TestToActionGraphFeatureOnlySelfTypedNodeKeepsInvocation(t *testing.T) { + src := ` + action def A { + attribute c : Integer := 0; + action x : A[0..*] { + ref occurrence r :>> self; + } + } + ` + decl, scope, _ := inheritedActionDecl(t, src, "A") + graph, err := ToActionGraph(decl, scope) + if err != nil { + t.Fatalf("lower A: %v", err) + } + node, ok := namedNode(graph, "x").(*ast.Usage) + if !ok { + t.Fatalf("x node = %T, want *ast.Usage", namedNode(graph, "x")) + } + if subflow := graph.Subflows[node]; subflow != nil { + t.Fatalf("x subflow = %#v, want no merged typed subflow", subflow) + } + if _, merged := graph.MergedTypedSubflows[node]; merged { + t.Fatal("feature-only self-typed x was marked as a merged typed subflow") + } +} + +func TestToActionGraphExecutableSelfTypedNodeRefusesRecursiveMerge(t *testing.T) { + src := ` + action def A { + attribute c : Integer := 0; + first start then x; + action x : A { + assign c := c + 1; + } + then done; + } + ` + decl, scope, _ := inheritedActionDecl(t, src, "A") + graph, err := ToActionGraph(decl, scope) + if err != nil { + t.Fatalf("lower A: %v", err) + } + node := namedNode(graph, "x") + subflow := graph.Subflows[node] + if subflow == nil || !errors.Is(subflow.Err, ErrRecursiveActionTyping) { + t.Fatalf("x subflow = %#v, want ErrRecursiveActionTyping", subflow) + } +} + +func TestToActionGraphBodyStatingTypedNodeMergesFiniteGeneral(t *testing.T) { + src := ` + action def Base { + attribute c : Integer := 0; + } + action def P :> Base { + attribute d : Integer := 0; + first start then x; + action x : Base { + assign c := c + 1; + } + then done; + } + ` + decl, scope, _ := inheritedActionDecl(t, src, "P") + graph, err := ToActionGraph(decl, scope) + if err != nil { + t.Fatalf("lower P: %v", err) + } + node := namedNode(graph, "x") + subflow := graph.Subflows[node] + if subflow == nil || subflow.Graph == nil || subflow.Err != nil { + t.Fatalf("x subflow = %#v, want a successfully merged typed subflow", subflow) + } + if _, merged := graph.MergedTypedSubflows[node]; !merged { + t.Fatal("x was not marked as a merged typed subflow") + } +} + +func TestToActionGraphInheritedTypedBodyRecursionIsRefused(t *testing.T) { + src := ` + action def Base { + attribute c : Integer := 0; + action x : P { + assign c := c + 1; + } + } + action def P :> Base; + ` + decl, scope, _ := inheritedActionDecl(t, src, "Base") + graph, err := ToActionGraph(decl, scope) + if err != nil { + t.Fatalf("lower Base: %v", err) + } + outer := namedNode(graph, "x") + outerSubflow := graph.Subflows[outer] + if outerSubflow == nil || outerSubflow.Graph == nil || outerSubflow.Err != nil { + t.Fatalf("outer x subflow = %#v, want P's graph", outerSubflow) + } + inner := namedNode(outerSubflow.Graph, "x") + innerSubflow := outerSubflow.Graph.Subflows[inner] + if innerSubflow == nil || !errors.Is(innerSubflow.Err, ErrRecursiveActionTyping) { + t.Fatalf("inner x subflow = %#v, want ErrRecursiveActionTyping", innerSubflow) + } +} + +func TestToActionGraphExecutableMutuallyTypedNodesRefuseRecursiveMerge(t *testing.T) { + src := ` + action def A { + attribute c : Integer := 0; + action y : B { + assign c := c + 1; + } + } + action def B { + attribute c : Integer := 0; + action z : A { + assign c := c + 1; + } + } + ` + decl, scope, _ := inheritedActionDecl(t, src, "A") + graph, err := ToActionGraph(decl, scope) + if err != nil { + t.Fatalf("lower A: %v", err) + } + y := namedNode(graph, "y") + subflow := graph.Subflows[y] + if subflow == nil || subflow.Graph == nil { + t.Fatalf("y subflow = %#v, want B's graph", subflow) + } + z := namedNode(subflow.Graph, "z") + recursive := subflow.Graph.Subflows[z] + if recursive == nil || !errors.Is(recursive.Err, ErrRecursiveActionTyping) { + t.Fatalf("z subflow = %#v, want ErrRecursiveActionTyping", recursive) + } +} + +func TestToActionGraphTypedUsageOverridesMatchingInheritedSuccession(t *testing.T) { + src := `package test { + action def Base { + action a; + first start then a; + } + action use : Base { + first start then a; + } + }` + p := parser.New(source.New("test.sysml", []byte(src))) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("parse diagnostics: %v", p.Diagnostics) + } + idx := symbols.NewIndexFromDoc("test.sysml", root) + usageSymbols := idx.LookupQualified("test::use") + if len(usageSymbols) != 1 { + t.Fatalf("test::use matched %d symbols, want one", len(usageSymbols)) + } + usage, ok := usageSymbols[0].Decl.(*ast.Usage) + if !ok { + t.Fatalf("test::use declaration is %T, want *ast.Usage", usageSymbols[0].Decl) + } + scope := usageSymbols[0].Scope + graph, err := ToActionGraphWith(usage, scope, resolve.New(idx)) + if err != nil { + t.Fatalf("lower typed usage: %v", err) + } + var edges int + for _, outgoing := range graph.Edges { + edges += len(outgoing) + } + if edges != 1 { + t.Fatalf("typed usage graph has %d edges, want one own edge overriding the inherited succession", edges) + } +} + +func TestToActionGraphPreservesInheritedSuccessionMultiplicitiesWhenRestated(t *testing.T) { + src := ` + action def Base { + action p; + action a[3]; + succession first [1] p then [3] a; + } + action def Plain :> Base { + succession first p then a; + } + action def Matching :> Base { + succession first [1] p then [3] a; + } + ` + for _, test := range []struct { + name string + wantEdges int + wantCheck bool + }{ + {name: "plain_restatement", wantEdges: 2, wantCheck: false}, + {name: "matching_restatement", wantEdges: 1, wantCheck: true}, + } { + t.Run(test.name, func(t *testing.T) { + graph := scopedActionGraph(t, src, map[string]string{ + "plain_restatement": "Plain", + "matching_restatement": "Matching", + }[test.name]) + p, a := namedNode(graph, "p"), namedNode(graph, "a") + var edges []ActionEdge + for _, edge := range graph.Edges[p] { + if edge.Target == a { + edges = append(edges, edge) + } + } + if len(edges) != test.wantEdges { + t.Fatalf("p to a edges = %d, want %d", len(edges), test.wantEdges) + } + if test.name == "plain_restatement" { + var inherited *ActionEdge + for i := range edges { + if graph.declaredIn[edges[i].Decl] != nil { + inherited = &edges[i] + break + } + } + if inherited == nil || inherited.SourceMultiplicity == nil || inherited.TargetMultiplicity == nil { + t.Fatal("inherited p to a edge lost its endpoint multiplicities") + } + } + err := graph.CheckStep(a, semantics.NewModel(graph.resolver)) + if (err == nil) != test.wantCheck { + t.Fatalf("CheckStep(a) = %v, want success %t", err, test.wantCheck) + } + if test.name == "plain_restatement" { + var stepErr *StepMultiplicityError + if !errors.As(err, &stepErr) || stepErr.Code != StepOrderUnsatisfiableCode { + t.Fatalf("CheckStep(a) error = %v, want %s", err, StepOrderUnsatisfiableCode) + } + } + }) + } +} + +func TestToActionGraphInheritedActionNodeFeaturesKeepDeclarationScope(t *testing.T) { + src := `package test { + action def Foo { + in ref context : Base[1]; + out c : Integer = 0; + } + action def Base { + action a : Foo { + accept payload : Integer; + in ref :>> context = this; + } + } + action def Derived :> Base { action :>> a[2]; } + }` + idx := libs.NewModelIndex() + p := parser.New(source.New("test.sysml", []byte(src))) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("parse diagnostics: %v", p.Diagnostics) + } + idx.AddDocument("test.sysml", root) + idx.ExpandWildcardImports() + derived := idx.LookupQualified("test::Derived") + if len(derived) != 1 { + t.Fatalf("Derived matched %d symbols, want one", len(derived)) + } + resolver := resolve.New(idx) + graph, err := ToActionGraphWith(derived[0].Decl, derived[0].Scope, resolver) + if err != nil { + t.Fatalf("lower Derived: %v", err) + } + node := namedNode(graph, "a") + if node == nil { + t.Fatal("Derived graph has no action node answering to a") + } + base := idx.LookupQualified("test::Base") + if len(base) != 1 { + t.Fatalf("Base matched %d symbols, want one", len(base)) + } + baseStep, ok := base[0].Scope.LookupLocal("a") + if !ok { + t.Fatal("Base::a not found") + } + performed, ok := graph.Performs[node] + if !ok || performed.Target == nil || performed.Target.Text() != "Foo" || performed.Scope != base[0].Scope { + t.Fatalf("Derived::a performed type = %+v, want Foo from Base's scope", performed) + } + subflow := graph.Subflows[node] + if subflow == nil || subflow.Graph == nil { + t.Fatal("Derived::a has no inherited accept subflow") + } + var acceptNode ast.Node + for _, candidate := range subflow.Graph.Nodes { + usage, ok := candidate.(*ast.Usage) + if ok && acceptsMessage(usage) { + acceptNode = candidate + break + } + } + accept, ok := subflow.Graph.Accepts[acceptNode] + if acceptNode == nil || !ok || accept.ParamName != "payload" || + accept.Scope != subflow.Graph.Scopes[acceptNode] { + t.Fatalf("Derived::a inherited accept = %+v on %v, want payload in the inherited accept node's scope", + accept, acceptNode) + } + var inheritedPin *ast.Usage + for _, feature := range graph.Features[node] { + if feature.Name == "context" { + inheritedPin, _ = feature.Node.(*ast.Usage) + if feature.Scope == nil || feature.Scope.Node() == nil { + t.Fatalf("inherited context pin scope = %v, want its declaration scope", feature.Scope) + } + break + } + } + if inheritedPin == nil { + t.Fatal("Derived::a has no inherited context pin") + } + if got := graph.declaredIn[inheritedPin]; got != baseStep.Scope { + t.Fatalf("inherited context pin declaring scope = %v, want Base::a's scope", got) + } +} + +func TestToActionGraphInheritsTypingAcrossTwoRedefinitions(t *testing.T) { + src := ` + action def Foo { + out c : Integer = 0; + assign c := c + 1; + } + action def Base { action a : Foo; } + action def Mid :> Base { action :>> a; } + action def Narrow :> Mid { action :>> a[2]; } + ` + narrow, scope, root := inheritedActionDecl(t, src, "Narrow") + graph, err := ToActionGraph(narrow, scope) + if err != nil { + t.Fatalf("lower Narrow: %v", err) + } + node := namedNode(graph, "a") + if node == nil { + t.Fatal("Narrow graph has no action node answering to a") + } + performed, ok := graph.Performs[node] + baseScope := scope.Parent().ChildFor(actionDefinition(t, root, "Base")) + if !ok || performed.Target == nil || performed.Target.Text() != "Foo" || performed.Scope != baseScope { + t.Fatalf("Narrow::a performed type = %s in %p, want Foo from Base's scope %p", + ast.SimpleName(performed.Target), performed.Scope, baseScope) + } +} + +func inheritedStepModel(t *testing.T) (*symbols.Index, *symbols.Symbol, *symbols.Symbol, *symbols.Symbol) { + t.Helper() + src := `package test { + private import ScalarValues::*; + action def Base { + attribute c : Integer = 0; + first start then a; + action a[3] { assign c := c + 1; } + then done; + } + action def Keep :> Base { + action :>> a { assign c := c + 10; } + } + action def Narrow :> Base { + action :>> a[2]; + } + }` + p := parser.New(source.New("test.sysml", []byte(src))) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("parse diagnostics: %v", p.Diagnostics) + } + idx := libs.NewModelIndex() + idx.AddDocument("test.sysml", root) + idx.ExpandWildcardImports() + lookup := func(name string) *symbols.Symbol { + t.Helper() + matches := idx.LookupQualified("test::" + name) + if len(matches) != 1 { + t.Fatalf("test::%s matched %d symbols, want one", name, len(matches)) + } + return matches[0] + } + return idx, lookup("Base"), lookup("Keep"), lookup("Narrow") +} + // A binding or flow a base action states at a pin of a node the derived action // inherits reaches the derived graph, in the base's scope and once even when the // base is reached along two generalization paths; one at a node the derived @@ -227,13 +1385,10 @@ func TestToActionGraphInheritedPinConnections(t *testing.T) { if err != nil { t.Fatalf("ToActionGraph: %v", err) } - add, fin, extra := namedNode(graph, "add"), namedNode(graph, "fin"), namedNode(graph, "extra") - if add == nil || fin == nil || extra == nil { + add, fin, extra, idle := namedNode(graph, "add"), namedNode(graph, "fin"), namedNode(graph, "extra"), namedNode(graph, "idle") + if add == nil || fin == nil || extra == nil || idle == nil { t.Fatal("inherited action nodes were not collected") } - if namedNode(graph, "idle") != nil { - t.Fatal("a node the derived action does not sequence became a node of its graph") - } baseBody := scope.Parent().ChildFor(actionDefinition(t, root, "Base")) midBody := scope.Parent().ChildFor(actionDefinition(t, root, "Mid")) @@ -247,6 +1402,7 @@ func TestToActionGraphInheritedPinConnections(t *testing.T) { {add, "sum", "extra.m", scope}, {extra, "m", "y", midBody}, {add, "a", "x", baseBody}, + {idle, "k", "x", baseBody}, } if len(graph.Bindings) != len(want) { t.Fatalf("lowered %d pin bindings, want %d: %+v", len(graph.Bindings), len(want), graph.Bindings) @@ -263,8 +1419,15 @@ func TestToActionGraphInheritedPinConnections(t *testing.T) { } flows := graph.DataFlows[add] - if len(flows) != 1 || flows[0].Target != fin || flows[0].SourcePin != "sum" || flows[0].TargetPin != "n" { - t.Fatalf("data flows out of add = %+v, want one flow add.sum to fin.n", flows) + if len(flows) != 2 { + t.Fatalf("data flows out of add = %+v, want inherited flows to fin and idle", flows) + } + targets := map[ast.Node]bool{} + for _, flow := range flows { + targets[flow.Target] = true + } + if !targets[fin] || !targets[idle] { + t.Fatalf("data flows out of add target %v, want fin and idle", targets) } } diff --git a/internal/ir/lower/action_nodes.go b/internal/ir/lower/action_nodes.go index 6d5d99baec..641905c668 100644 --- a/internal/ir/lower/action_nodes.go +++ b/internal/ir/lower/action_nodes.go @@ -85,8 +85,15 @@ func actionMembers(actionDecl ast.Node) ([]ast.Node, error) { } func collectActionNodes(members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) (*ActionGraph, error) { + return collectActionNodesWithAncestors(members, scope, resolver, nil) +} + +func collectActionNodesWithAncestors( + members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver, ancestors []ast.Node, +) (*ActionGraph, error) { graph := newActionGraph(scope) graph.resolver = resolver + graph.lowering = actionLoweringAncestors(scope, ancestors) asserted := orderedAssertions(members) // First pass: collect nodes. @@ -126,13 +133,16 @@ func collectActionNodes(members []ast.Node, scope *symbols.Scope, resolver *reso graph.Nodes = append(graph.Nodes, n) graph.Bodies[n] = []Statement{Assert{Node: n, Sym: scope.MemberDeclaring(n), Scope: scope}} } + case *ast.PerformActionNode: + graph.Nodes = append(graph.Nodes, n) + graph.Bodies[n] = []Statement{performEffect(n, scope)} case *ast.WhileLoopActionNode, *ast.IfActionNode, *ast.AssignmentActionNode, *ast.SendStatement, *ast.TerminateStatement: // A statement written among the action's own members is a subaction of // it, ordered by the successions that bind it and started with the // owner where none does (StartFlow). graph.Nodes = append(graph.Nodes, n) - graph.Bodies[n] = []Statement{lowerStatement(n, scope)} + graph.Bodies[n] = []Statement{lowerStatement(n, scope, graph.resolver)} } } @@ -175,7 +185,19 @@ func collectInheritedActionNodes(graph *ActionGraph, members []ast.Node) { } ensureInheritedActionNode(graph, n.Target) case *ast.Usage: - if n.Kind == ast.UsageSuccession && len(n.ConnectorEnds) == 2 { + switch { + case n.Kind == ast.UsageFlow && n.FlowEnds != nil: + for _, end := range []ast.Node{n.FlowEnds.From, n.FlowEnds.To} { + segments := endSegments(end) + if len(segments) == 0 { + continue + } + nodeName := &ast.QualifiedName{} + nodeName.NodeSpan = end.Span() + nodeName.SetSingleton(ast.NameSegment{Text: segments[0], Span: end.Span()}) + ensureInheritedActionNode(graph, nodeName) + } + case n.Kind == ast.UsageSuccession && len(n.ConnectorEnds) == 2: ensureInheritedActionNode(graph, connectorEndReference(n.ConnectorEnds[0])) ensureInheritedActionNode(graph, connectorEndReference(n.ConnectorEnds[1])) } @@ -185,13 +207,24 @@ func collectInheritedActionNodes(graph *ActionGraph, members []ast.Node) { func ensureInheritedActionNode(graph *ActionGraph, ref ast.Node) ast.Node { qn := actionEndpointQualifiedName(ref) - if qn == nil { + if qn == nil || len(qn.Parts) == 0 { + return nil + } + name := qn.Parts[len(qn.Parts)-1].Text + if name == "start" || name == "done" { return nil } decl, declaringScope, found, _ := resolve.ActionNodeInScope(graph.Scope, qn) if !found || decl == nil { return nil } + return ensureDeclaredActionNode(graph, decl, declaringScope) +} + +func ensureDeclaredActionNode(graph *ActionGraph, decl ast.Node, declaringScope *symbols.Scope) ast.Node { + if decl == nil { + return nil + } for _, node := range graph.Nodes { if node == decl { return node @@ -206,13 +239,19 @@ func ensureInheritedActionNode(graph *ActionGraph, ref ast.Node) ast.Node { break } recordNodeMultiplicity(graph, n) + if IsCaseNode(n) { + recordNodeScope(graph, n, childScope(declaringScope, n)) + break + } lowerActionNode(graph, n, childScope(declaringScope, n)) case *ast.ForkNode, *ast.JoinNode, *ast.MergeNode, *ast.DecisionNode, *ast.ActionExecutionNode: lowerNodeBody(graph, n, ast.NodeBodyMembers(n), declaringScope) lowerControlFeatures(graph, n, declaringScope) + case *ast.PerformActionNode: + graph.Bodies[n] = []Statement{performEffect(n, declaringScope)} case *ast.WhileLoopActionNode, *ast.IfActionNode, *ast.AssignmentActionNode, *ast.SendStatement, *ast.TerminateStatement: - graph.Bodies[n] = []Statement{lowerStatement(n, declaringScope)} + graph.Bodies[n] = []Statement{lowerStatement(n, declaringScope, graph.resolver)} } return decl } diff --git a/internal/ir/lower/action_subflow.go b/internal/ir/lower/action_subflow.go index 5ff6640c57..f7032788a1 100644 --- a/internal/ir/lower/action_subflow.go +++ b/internal/ir/lower/action_subflow.go @@ -1,8 +1,10 @@ package lower import ( + "fmt" "slices" + "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -71,44 +73,295 @@ func runsOwnFlow(members []ast.Node) bool { // members state (its start inferred as a whole action's is), or — where they // state none — the statements and accept of a leaf. scope is the node's own namespace. func lowerActionNode(graph *ActionGraph, node *ast.Usage, scope *symbols.Scope) { - lowerFeatures(graph, node, scope) + members, cycle, err := effectiveActionMembers(node, scope) + if err != nil { + recordInvalidSubflow(graph, node, err) + return + } + if cycle { + recordInvalidSubflow(graph, node, fmt.Errorf("%w: %s", ErrCyclicSpecialization, getNodeName(node))) + return + } + lowerEffectiveActionNodeFeatures(graph, node, scope, members) + lowerEffectiveAccept(graph, node, members) + rawMembers := make([]ast.Node, 0, len(members)) + for _, member := range members { + rawMembers = append(rawMembers, member.Decl) + } + typedBody, typedAction := mergedTypedActionBody(node, scope) + var typedTarget ast.Node + if typedBody { + typedTarget = resolveTypedActionTarget(graph.resolver, typedAction) + if typedActionTargetIsAncestor(graph, typedTarget) { + if typedBodyHasExecutableContent(rawMembers) { + recordInvalidSubflow(graph, node, fmt.Errorf("%w: %s", + ErrRecursiveActionTyping, ast.SimpleName(typedAction.Target))) + return + } + typedBody = false + } + } if node.IsTerminate { - lowerTerminateNode(graph, node, scope) + lowerTerminateNode(graph, node, scope, members, typedBody, typedTarget) return } - if !runsOwnFlow(node.Members) { - lowerBody(graph, node, scope) + if !runsOwnFlow(rawMembers) && !typedBody { + for _, member := range BodyStatementMembers(rawMembers) { + actual := unwrapMembership(member) + memberScope := effectiveMemberScope(members, actual, scope) + graph.recordDeclaredIn(actual, memberScope) + graph.Bodies[node] = append(graph.Bodies[node], lowerStatement(actual, memberScope, graph.resolver)) + } if _, _, starts := startedBehavior(node, scope); starts { graph.Bodies[node] = append(graph.Bodies[node], performEffect(node, scope)) } return } - lowerAccept(graph, node, scope) if graph.Subflows == nil { graph.Subflows = make(map[ast.Node]*Subflow) } - sub, err := ToActionGraphWith(node, scope, graph.resolver) + ancestors := graph.lowering + if typedBody { + ancestors = appendLoweringAncestor(ancestors, typedTarget) + } + sub, err := toActionGraphWithTypingAndAncestors(node, scope, graph.resolver, typedBody, ancestors) if err == nil { sub.Enclosing, sub.EnclosingNode = graph, node StartFlow(sub) } graph.Subflows[node] = &Subflow{Graph: sub, Err: err} + if typedBody && typedAction.Target != nil { + if graph.MergedTypedSubflows == nil { + graph.MergedTypedSubflows = make(map[ast.Node]PerformedType) + } + graph.MergedTypedSubflows[node] = typedAction + } +} + +func resolveTypedActionTarget(resolver *resolve.Resolver, typed PerformedType) ast.Node { + if typed.Target == nil || typed.Scope == nil { + return nil + } + if resolver != nil { + if decl, _, ok := resolver.TypeDecl(typed.Scope, typed.Target); ok { + return decl + } + } + decl, _, _ := resolve.TypeDeclInScope(typed.Scope, typed.Target) + return decl +} + +func typedActionTargetIsAncestor(graph *ActionGraph, target ast.Node) bool { + if graph == nil { + return false + } + return loweringHasAncestor(graph.lowering, target) +} + +func loweringHasAncestor(ancestors []ast.Node, target ast.Node) bool { + if target == nil { + return false + } + for _, ancestor := range ancestors { + if ancestor == target { + return true + } + } + return false +} + +func appendLoweringAncestor(ancestors []ast.Node, target ast.Node) []ast.Node { + if target == nil || loweringHasAncestor(ancestors, target) { + return ancestors + } + out := append([]ast.Node(nil), ancestors...) + return append(out, target) +} + +func typedBodyHasExecutableContent(members []ast.Node) bool { + for _, member := range members { + actual := unwrapMembership(member) + if actual == nil || statesNoStep(actual) || isAnnotation(actual) { + continue + } + switch node := actual.(type) { + case *ast.Usage: + switch node.Kind { + case ast.UsageAction, ast.UsageState, ast.UsageTransition, ast.UsageSuccession, + ast.UsageStep, ast.UsageAnalysisCase, ast.UsageVerificationCase: + return true + default: + if node.IsAccept || node.IsTerminate || node.IsActionNode { + return true + } + } + case *ast.AcceptActionUsage, *ast.EntryMember, *ast.DoMember, *ast.ExitMember, + *ast.StateNode: + return true + default: + return true + } + } + return false +} + +func mergedTypedActionBody(node *ast.Usage, scope *symbols.Scope) (bool, PerformedType) { + if node == nil || !node.HasBody || declaresOnlyFeatures(node.Members) { + return false, PerformedType{} + } + for _, rel := range node.Relationships { + if rel == nil || (rel.Kind != ast.RelTyping && rel.Kind != ast.RelReferences) { + continue + } + target, ok := rel.Target.(*ast.QualifiedName) + if !ok { + continue + } + declaringScope := scope + if declaringScope != nil && declaringScope.Parent() != nil { + declaringScope = declaringScope.Parent() + } + return true, PerformedType{Target: target, Scope: declaringScope} + } + return false, PerformedType{} +} + +type effectiveActionMember struct { + Decl ast.Node + Scope *symbols.Scope +} + +func effectiveActionMembers(node *ast.Usage, scope *symbols.Scope) ([]effectiveActionMember, bool, error) { + var collect func(*ast.Usage, *symbols.Scope, map[ast.Node]bool) ([]effectiveActionMember, bool, error) + collect = func(usage *ast.Usage, bodyScope *symbols.Scope, active map[ast.Node]bool) ([]effectiveActionMember, bool, error) { + if active[usage] { + return nil, true, nil + } + active[usage] = true + defer delete(active, usage) + declaringBody := bodyScope + if declaringBody != nil && declaringBody.Parent() != nil { + declaringBody = declaringBody.Parent() + } + if missing := resolve.MissingRedefinedActionNode(declaringBody, usage); missing != "" { + return nil, false, fmt.Errorf("%w: %s", ErrRedefinedStepMissing, missing) + } + var out []effectiveActionMember + seen := make(map[ast.Node]bool) + for _, member := range usage.Members { + decl := unwrapMembership(member) + out = append(out, effectiveActionMember{Decl: decl, Scope: bodyScope}) + seen[decl] = true + } + for _, target := range resolve.ActionNodeRedefinitionTargets(declaringBody, usage, true) { + general, ok := target.(*ast.Usage) + if !ok || general.Kind != ast.UsageAction { + continue + } + generalOwner := resolve.ActionNodeDeclaringScope(declaringBody, target) + if generalOwner == nil { + continue + } + generalScope := childScope(generalOwner, general) + inherited, cyclic, err := collect(general, generalScope, active) + if err != nil { + return out, false, err + } + if cyclic { + return out, true, nil + } + for _, member := range inherited { + replaced := false + for _, own := range usage.Members { + redefinition, ok := unwrapMembership(own).(*ast.Usage) + if !ok { + continue + } + if resolve.RedefinesActionNode(declaringBody, redefinition, member.Decl) { + replaced = true + break + } + inheritedFeature, ok := member.Decl.(*ast.Usage) + if !ok || !DeclaresNodeFeature(redefinition) || !DeclaresNodeFeature(inheritedFeature) { + continue + } + ownName, _ := ast.EffectiveName(redefinition) + inheritedName, _ := ast.EffectiveName(inheritedFeature) + if ownName != "" && ownName == inheritedName { + replaced = true + break + } + } + if !replaced && !seen[member.Decl] { + seen[member.Decl] = true + out = append(out, member) + } + } + } + return out, false, nil + } + members, cycle, err := collect(node, scope, make(map[ast.Node]bool)) + return members, cycle, err +} + +func effectiveMemberScope(members []effectiveActionMember, decl ast.Node, fallback *symbols.Scope) *symbols.Scope { + for _, member := range members { + if member.Decl == decl { + return member.Scope + } + } + return fallback +} + +func recordInvalidSubflow(graph *ActionGraph, node ast.Node, err error) { + if graph.Subflows == nil { + graph.Subflows = make(map[ast.Node]*Subflow) + } + graph.Subflows[node] = &Subflow{Err: err} } // lowerTerminateNode records what a terminate action usage runs: its body, the // statements of a leaf or the flow it states as a block, then the terminate it stands for. -func lowerTerminateNode(graph *ActionGraph, node *ast.Usage, scope *symbols.Scope) { - if statesOwnFlow(node.Members) { - // The node's parameters and attributes are its own (lowerFeatures), not the block's. +func lowerTerminateNode( + graph *ActionGraph, + node *ast.Usage, + scope *symbols.Scope, + members []effectiveActionMember, + typedBody bool, + typedTarget ast.Node, +) { + rawMembers := make([]ast.Node, 0, len(members)) + for _, member := range members { + rawMembers = append(rawMembers, member.Decl) + } + if statesOwnFlow(rawMembers) { steps := slices.DeleteFunc(slices.Clone(node.Members), func(member ast.Node) bool { m, ok := unwrapMembership(member).(*ast.Usage) return ok && DeclaresNodeFeature(m) }) - graph.Bodies[node] = []Statement{lowerStatedBlock(node, steps, scope)} + ancestors := graph.lowering + if typedBody { + ancestors = appendLoweringAncestor(ancestors, typedTarget) + } + blockGraph, err := lowerActionFlowWithTypingAndAncestors(steps, scope, graph.resolver, typedBody, ancestors) + if blockGraph == nil { + blockGraph = newActionGraph(scope) + blockGraph.resolver = graph.resolver + } + if err != nil { + blockGraph.Invalid = err + } + StartFlow(blockGraph) + graph.Bodies[node] = []Statement{Block{Node: node, Scope: scope, Graph: blockGraph, Stated: true}} } else { - lowerBody(graph, node, scope) + for _, member := range BodyStatementMembers(rawMembers) { + actual := unwrapMembership(member) + memberScope := effectiveMemberScope(members, actual, scope) + graph.recordDeclaredIn(actual, memberScope) + graph.Bodies[node] = append(graph.Bodies[node], lowerStatement(actual, memberScope, graph.resolver)) + } } - graph.Bodies[node] = append(graph.Bodies[node], lowerStatement(node, scope)) + graph.Bodies[node] = append(graph.Bodies[node], lowerStatement(node, scope, graph.resolver)) } // TerminateUsage returns the terminate a terminate action usage stands for, the last of @@ -139,14 +392,18 @@ func (g *ActionGraph) StartUsage(node ast.Node) (Effect, bool) { return last, true } -// lowerAccept records the message a nested action node waits for, which a node -// owning a flow still does before that flow starts. -func lowerAccept(graph *ActionGraph, node *ast.Usage, scope *symbols.Scope) { - for _, member := range node.Members { - m, ok := unwrapMembership(member).(*ast.Usage) +// lowerEffectiveAccept records the message a nested action node waits for, which +// a node owning a flow still does before that flow starts. +func lowerEffectiveAccept(graph *ActionGraph, node *ast.Usage, members []effectiveActionMember) { + for _, member := range members { + m, ok := member.Decl.(*ast.Usage) if !ok || !m.IsAccept { continue } + scope := member.Scope + if scope == nil { + scope = graph.nodeScope(node) + } port, viaSelf := acceptPort(node) graph.Accepts[node] = Accept{ ParamName: m.Ident.Name, diff --git a/internal/ir/lower/block_graph.go b/internal/ir/lower/block_graph.go index 57111798cb..333a214639 100644 --- a/internal/ir/lower/block_graph.go +++ b/internal/ir/lower/block_graph.go @@ -3,6 +3,7 @@ package lower import ( "fmt" + "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -107,15 +108,22 @@ func outsideBlockFlow(member ast.Node) bool { // lowerBlockFlow lowers a block to the flow its members state: a node per action // node, a node per run of plain statements, a succession in declaration order. // nodeBody says the members are a nested action's own, so a parameter is its own. -func lowerBlockFlow(members []ast.Node, scope *symbols.Scope, nodeBody bool) *ActionGraph { - return lowerBlockFlowWith(members, scope, func(graph *ActionGraph, nodes []ast.Node, member ast.Node) (Statement, bool) { +func lowerBlockFlow(members []ast.Node, scope *symbols.Scope, nodeBody bool, resolver *resolve.Resolver) *ActionGraph { + return lowerBlockFlowWith(members, scope, resolver, func(graph *ActionGraph, nodes []ast.Node, member ast.Node) (Statement, bool) { return blockStep(graph, nodes, member, scope, nodeBody) }) } +// ToActionNodeFlow lowers one action node as the sole node of a block flow. +func ToActionNodeFlow(node ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) *ActionGraph { + graph := lowerBlockFlow([]ast.Node{node}, scope, false, resolver) + StartFlow(graph) + return graph +} + // lowerStatedBlock lowers a loop or branch body stating a flow of its own. -func lowerStatedBlock(owner ast.Node, members []ast.Node, scope *symbols.Scope) Block { - graph, err := lowerActionFlow(members, scope, nil) +func lowerStatedBlock(owner ast.Node, members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) Block { + graph, err := lowerActionFlow(members, scope, resolver) if err != nil { if graph == nil { graph = newActionGraph(scope) @@ -134,9 +142,10 @@ type blockStepLowering func(graph *ActionGraph, nodes []ast.Node, member ast.Nod // lowerBlockFlowWith lowers a block to its declaration-order flow, lowering the // members between action nodes with step. -func lowerBlockFlowWith(members []ast.Node, scope *symbols.Scope, step blockStepLowering) *ActionGraph { +func lowerBlockFlowWith(members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver, step blockStepLowering) *ActionGraph { graph := &ActionGraph{ Scope: scope, + resolver: resolver, Nodes: make([]ast.Node, 0, len(members)), Edges: make(map[ast.Node][]ActionEdge), Multiplicities: make(map[ast.Node]*ast.Multiplicity), @@ -351,7 +360,7 @@ func lowerFlowNode(graph *ActionGraph, node ast.Node, scope *symbols.Scope) { } lowerNestedNode(graph, n, childScope(scope, n)) default: - graph.Bodies[node] = []Statement{lowerStatement(node, scope)} + graph.Bodies[node] = []Statement{lowerStatement(node, scope, graph.resolver)} } } @@ -370,13 +379,14 @@ func acceptsMessage(node *ast.Usage) bool { // action's flow: the features it declares, and the statements or flow its members // state — a flow of its own (`first`, a succession) as the subflow the node owns. func lowerNestedNode(graph *ActionGraph, node *ast.Usage, scope *symbols.Scope) { - if node.IsTerminate || statesOwnFlow(node.Members) { + typedBody, _ := mergedTypedActionBody(node, scope) + if node.IsTerminate || statesOwnFlow(node.Members) || PerformsLeafStatements(node.Members) || typedBody { lowerActionNode(graph, node, scope) return } lowerFeatures(graph, node, scope) if blockNeedsFlow(node.Members) { - flow := lowerBlockFlow(node.Members, scope, true) + flow := lowerBlockFlow(node.Members, scope, true, graph.resolver) graph.Bodies[node] = []Statement{Block{ Node: node, Scope: scope, @@ -390,7 +400,7 @@ func lowerNestedNode(graph *ActionGraph, node *ast.Usage, scope *symbols.Scope) if actual == nil || statesNoStep(actual) { continue } - if stmt, states := blockMemberStatement(actual, scope, true); states { + if stmt, states := blockMemberStatement(actual, scope, true, graph.resolver); states { graph.Bodies[node] = append(graph.Bodies[node], stmt) } } @@ -416,7 +426,7 @@ func blockStep(graph *ActionGraph, nodes []ast.Node, member ast.Node, scope *sym return stmt, stmt != nil } } - return blockMemberStatement(member, scope, nodeBody) + return blockMemberStatement(member, scope, nodeBody, graph.resolver) } // lowerBlockConnector lowers a binding or flow at a pin of one of the block's nodes into @@ -462,19 +472,21 @@ func unsupportedConnector(u *ast.Usage, scope *symbols.Scope, err error) Stateme // blockMemberStatement lowers a member of a block's flow that is a statement, and // reports whether it states a step: a nested action's own feature is not one. -func blockMemberStatement(member ast.Node, scope *symbols.Scope, nodeBody bool) (Statement, bool) { +func blockMemberStatement(member ast.Node, scope *symbols.Scope, nodeBody bool, resolver *resolve.Resolver) (Statement, bool) { if usage, ok := member.(*ast.Usage); ok && nodeBody && DeclaresNodeFeature(usage) { return nil, false } - return lowerStatement(member, scope), true + return lowerStatement(member, scope, resolver), true } // statesNoStep reports whether a member declares something about the block // rather than a step of it, so that reaching the block does not reach it. func statesNoStep(member ast.Node) bool { - switch member.(type) { - case *ast.Documentation, *ast.Comment, *ast.Definition, *ast.Import, *ast.Alias: + switch member := member.(type) { + case *ast.Documentation, *ast.Comment, *ast.Definition, *ast.Import, *ast.Alias, *ast.PrefixMetadata: return true + case *ast.Usage: + return member.Kind == ast.UsageMetadata default: return false } diff --git a/internal/ir/lower/block_graph_resolver_test.go b/internal/ir/lower/block_graph_resolver_test.go new file mode 100644 index 0000000000..d3d9a4fec7 --- /dev/null +++ b/internal/ir/lower/block_graph_resolver_test.go @@ -0,0 +1,175 @@ +package lower + +import ( + "errors" + "testing" + + "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" + "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" + "github.com/Open-MBEE/OpenSysML/internal/syntax/parser" + "github.com/Open-MBEE/OpenSysML/internal/syntax/source" + "github.com/Open-MBEE/OpenSysML/internal/workspace/libs" +) + +func TestToActionNodeFlowKeepsInheritedProbabilityWeights(t *testing.T) { + base := ` + action def Base { + decide d; + first start then d; + first d then fast { @Probability { p = 0.9; } } + first d then slow { @Probability { p = 0.1; } } + action fast; + action slow; + } + ` + tests := []struct { + name, owner, src string + stateEntry bool + }{ + { + name: "state entry", + owner: "m", + src: `package M { + import Stochastic::*; + ` + base + ` + state m { + attribute count : Integer := 0; + entry action prep : Base { + assign count := count + 1; + } + } + }`, + stateEntry: true, + }, + { + name: "classifier perform", + owner: "Monitor", + src: `package M { + import Stochastic::*; + ` + base + ` + part def Monitor { + attribute count : Integer := 0; + perform action prep : Base { + assign count := count + 1; + } + } + }`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + graph, node := actionNodeFlowFromSource(t, tc.src, tc.owner, "prep", tc.stateEntry) + subflow := graph.Subflows[node] + if subflow == nil || subflow.Graph == nil || subflow.Err != nil { + t.Fatalf("typed behavior subflow = %#v, want a merged action graph", subflow) + } + decision := namedActionNode(t, subflow.Graph, "d") + edges := subflow.Graph.Edges[decision] + if len(edges) != 2 { + t.Fatalf("inherited decision has %d edges, want 2", len(edges)) + } + for i, want := range []float64{0.9, 0.1} { + if edges[i].Probability == nil { + t.Fatalf("inherited edge %d has no probability", i) + } + got, ok := edges[i].Probability.Constant() + if !ok || got != want { + t.Errorf("inherited edge %d weight = %v, %v; want %v", i, got, ok, want) + } + } + }) + } +} + +func TestToActionNodeFlowReportsRecursiveTypedAction(t *testing.T) { + src := `package M { + action def A { + attribute c : Integer := 0; + first start then x; + action x : A { + assign c := c + 1; + } + then done; + } + state m { + entry action prep : A { + assign c := c + 1; + } + } + }` + graph, entry := actionNodeFlowFromSource(t, src, "m", "prep", true) + prep := graph.Subflows[entry] + if prep == nil || prep.Graph == nil || prep.Err != nil { + t.Fatalf("entry behavior subflow = %#v, want A's graph", prep) + } + x := namedNode(prep.Graph, "x") + recursive := prep.Graph.Subflows[x] + if recursive == nil || !errors.Is(recursive.Err, ErrRecursiveActionTyping) { + t.Fatalf("recursive x subflow = %#v, want ErrRecursiveActionTyping", recursive) + } +} + +func actionNodeFlowFromSource( + t *testing.T, src, ownerName, behaviorName string, stateEntry bool, +) (*ActionGraph, *ast.Usage) { + t.Helper() + p := parser.New(source.New("test.sysml", []byte(src))) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("parse diagnostics: %v", p.Diagnostics) + } + idx := libs.NewModelIndex() + idx.AddDocument("test.sysml", root) + idx.ExpandWildcardImports() + resolver := resolve.New(idx) + pkg, ok := idx.DocumentRoot("test.sysml").LookupLocal("M") + if !ok { + t.Fatal("package M not indexed") + } + owner, ok := pkg.Scope.LookupLocal(ownerName) + if !ok { + t.Fatalf("%s not indexed", ownerName) + } + var behavior *ast.Usage + if stateEntry { + state, ok := owner.Decl.(*ast.Usage) + if !ok { + t.Fatalf("%s declaration is %T, want *ast.Usage", ownerName, owner.Decl) + } + for _, member := range state.Members { + entry, ok := unwrapMembership(member).(*ast.EntryMember) + if !ok { + continue + } + for _, action := range entry.Actions { + usage, ok := unwrapMembership(action).(*ast.Usage) + if !ok { + continue + } + name, _ := ast.EffectiveName(usage) + if name == behaviorName { + behavior = usage + break + } + } + } + } else { + var members []ast.Node + switch decl := owner.Decl.(type) { + case *ast.Definition: + members = decl.Members + case *ast.Usage: + members = decl.Members + } + for _, candidate := range ClassifierBehaviorsOf(members) { + if candidate.Kind == PerformedAction && candidate.Name == behaviorName { + behavior = candidate.Decl + break + } + } + } + if behavior == nil { + t.Fatalf("behavior %q not found under %s", behaviorName, ownerName) + } + return ToActionNodeFlow(behavior, owner.Scope, resolver), behavior +} diff --git a/internal/ir/lower/calc_body.go b/internal/ir/lower/calc_body.go index 95cebabe0f..f985c67505 100644 --- a/internal/ir/lower/calc_body.go +++ b/internal/ir/lower/calc_body.go @@ -39,7 +39,7 @@ func CalcBodyWithOrder(owner ast.Node, members []ast.Node, scope *symbols.Scope, stmts := caseSteps(owner, body, scope, resolver) return stmts, nil } - stmts, order := calcBodyStatements(body, scope, !PerformsSteps(owner)) + stmts, order := calcBodyStatements(body, scope, resolver, !PerformsSteps(owner)) if PerformsSteps(owner) { return stmts, nil } @@ -49,7 +49,7 @@ func CalcBodyWithOrder(owner ast.Node, members []ast.Node, scope *symbols.Scope, return stmts, order } -func calcBodyStatements(body []ast.Node, scope *symbols.Scope, ignoreNonStatementSuccessions bool) ([]Statement, *StatementOrder) { +func calcBodyStatements(body []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver, ignoreNonStatementSuccessions bool) ([]Statement, *StatementOrder) { members := make([]ast.Node, 0, len(body)) for _, member := range body { if actual := unwrapMembership(member); actual != nil && !isAnnotation(actual) { @@ -61,7 +61,7 @@ func calcBodyStatements(body []ast.Node, scope *symbols.Scope, ignoreNonStatemen if _, ok := member.(*ast.SuccessionEdge); ok { continue } - stmt, ok := calcBodyStep(member, scope, ignoreNonStatementSuccessions) + stmt, ok := calcBodyStep(member, scope, resolver, ignoreNonStatementSuccessions) if !ok { continue } @@ -76,13 +76,13 @@ func calcBodyStatements(body []ast.Node, scope *symbols.Scope, ignoreNonStatemen return bodyStmts, order } -func calcBodyStep(member ast.Node, scope *symbols.Scope, ignoreNonStatementSuccessions bool) (Statement, bool) { +func calcBodyStep(member ast.Node, scope *symbols.Scope, resolver *resolve.Resolver, ignoreNonStatementSuccessions bool) (Statement, bool) { if ignoreNonStatementSuccessions { if usage, ok := member.(*ast.Usage); ok && (usage.IsSuccessionFlow() || usage.Kind == ast.UsageSuccession) { return nil, false } } - return calcStep(member, scope) + return calcStep(member, scope, resolver) } // CalcSteps returns the statements an invocation performs, excluding bindings @@ -106,7 +106,7 @@ func isOutputBinding(node ast.Node) bool { // calcStep lowers one member of a calculation body and reports whether it // states a step or a result; a result is a Return. -func calcStep(member ast.Node, scope *symbols.Scope) (Statement, bool) { +func calcStep(member ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) (Statement, bool) { switch m := member.(type) { case *ast.Usage: if m.Direction == ast.DirIn || m.Direction == ast.DirInOut { @@ -129,7 +129,7 @@ func calcStep(member ast.Node, scope *symbols.Scope) (Statement, bool) { if ast.IsExpression(member) { return Return{Value: member, Node: member, Scope: scope}, true } - return lowerStatement(member, scope), true + return lowerStatement(member, scope, resolver), true } } diff --git a/internal/ir/lower/case_body.go b/internal/ir/lower/case_body.go index a0ceab09e3..e0e63fe513 100644 --- a/internal/ir/lower/case_body.go +++ b/internal/ir/lower/case_body.go @@ -29,9 +29,9 @@ func PerformsSteps(decl ast.Node) bool { // control nodes is the token flow an action body is (ToActionGraph); otherwise // its action nodes and statements are unordered subactions. func caseSteps(owner ast.Node, body []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) []Statement { - trailing := trailingResults(body, scope) + trailing := trailingResults(body, scope, resolver) if !statesOwnFlow(body) { - trailing = caseTrailingResults(body, scope, trailing) + trailing = caseTrailingResults(body, scope, resolver, trailing) var locals, results []Statement var members []ast.Node statementRuns := make(map[ast.Node]Statement) @@ -47,7 +47,7 @@ func caseSteps(owner ast.Node, body []ast.Node, scope *symbols.Scope, resolver * flowStarted = true continue } - stmt, states := calcStep(member, scope) + stmt, states := calcStep(member, scope, resolver) if !states { members = append(members, member) continue @@ -110,7 +110,7 @@ func caseSteps(owner ast.Node, body []ast.Node, scope *symbols.Scope, resolver * if isFlowNode(member) || outsideBlockFlow(member) || sequenced[member] || nodes[unwrapMembership(member)] { continue } - stmt, states := calcStep(member, scope) + stmt, states := calcStep(member, scope, resolver) if !states { continue } @@ -157,7 +157,7 @@ func caseOutputBinding(node ast.Node) bool { return false } -func caseTrailingResults(body []ast.Node, scope *symbols.Scope, trailing map[ast.Node]bool) map[ast.Node]bool { +func caseTrailingResults(body []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver, trailing map[ast.Node]bool) map[ast.Node]bool { for i := len(body) - 1; i >= 0; i-- { member := body[i] if trailing[member] || statesNoStep(unwrapMembership(member)) { @@ -166,7 +166,7 @@ func caseTrailingResults(body []ast.Node, scope *symbols.Scope, trailing map[ast if isFlowNode(unwrapMembership(member)) { break } - stmt, states := calcStep(member, scope) + stmt, states := calcStep(member, scope, resolver) if !states { continue } @@ -233,7 +233,7 @@ func isReturn(stmt Statement) bool { // trailingResults marks the members ending a body with results: the statements // after its last step, each returning on some path through it (IsResult). Control // flow returning among the steps stays a step, its effects in declared order. -func trailingResults(body []ast.Node, scope *symbols.Scope) map[ast.Node]bool { +func trailingResults(body []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) map[ast.Node]bool { trailing := map[ast.Node]bool{} for i := len(body) - 1; i >= 0; i-- { member := body[i] @@ -243,7 +243,7 @@ func trailingResults(body []ast.Node, scope *symbols.Scope) map[ast.Node]bool { if isFlowNode(member) { break } - stmt, states := calcStep(member, scope) + stmt, states := calcStep(member, scope, resolver) if !states { continue } diff --git a/internal/ir/lower/constraint_body.go b/internal/ir/lower/constraint_body.go index b9325e5472..55df39bb18 100644 --- a/internal/ir/lower/constraint_body.go +++ b/internal/ir/lower/constraint_body.go @@ -1,6 +1,7 @@ package lower import ( + "github.com/Open-MBEE/OpenSysML/internal/semantic/resolve" "github.com/Open-MBEE/OpenSysML/internal/semantic/symbols" "github.com/Open-MBEE/OpenSysML/internal/syntax/ast" ) @@ -14,7 +15,11 @@ import ( // parameter redefinition writes, and members declaring nothing a step can run. // Successions between steps supply precedence; other successions and control // nodes remain unexecutable because a verdict does not run a token flow. -func ConstraintStep(member ast.Node, scope *symbols.Scope) (Statement, bool) { +func ConstraintStep(member ast.Node, scope *symbols.Scope, resolvers ...*resolve.Resolver) (Statement, bool) { + var resolver *resolve.Resolver + if len(resolvers) > 0 { + resolver = resolvers[0] + } if actual := unwrapMembership(member); actual != nil { member = actual } @@ -33,24 +38,24 @@ func ConstraintStep(member ast.Node, scope *symbols.Scope) (Statement, bool) { Until: m.Until, Variable: m.Variable.Name, Collection: m.Collection, - Body: constraintLowerBlock(m, m.Body, childScope(scope, m)), + Body: constraintLowerBlock(m, m.Body, childScope(scope, m), resolver), Node: m, Scope: scope, }, true case *ast.IfActionNode: lowered := If{Condition: m.Condition, Node: m, Scope: scope} if m.Then != nil { - block := constraintLowerBlock(m.Then, m.Then.Body, childScope(scope, m.Then)) + block := constraintLowerBlock(m.Then, m.Then.Body, childScope(scope, m.Then), resolver) lowered.Then = block } if m.Else != nil { - block := constraintLowerBlock(m.Else, m.Else.Body, childScope(scope, m.Else)) + block := constraintLowerBlock(m.Else, m.Else.Body, childScope(scope, m.Else), resolver) lowered.Else = &block } return lowered, true case *ast.Usage: if m.Kind == ast.UsageAction && m.IsBodyParameter { - return constraintLowerBlock(m, m.Members, childScope(scope, m)), true + return constraintLowerBlock(m, m.Members, childScope(scope, m), resolver), true } if m.Direction == ast.DirIn || m.Direction == ast.DirInOut { // A parameter is bound by the check, not by the body. @@ -80,18 +85,18 @@ func ConstraintStep(member ast.Node, scope *symbols.Scope) (Statement, bool) { if ast.IsExpression(member) { return nil, false } - return lowerStatement(member, scope), true + return lowerStatement(member, scope, resolver), true } } -func constraintLowerBlock(owner ast.Node, members []ast.Node, scope *symbols.Scope) Block { +func constraintLowerBlock(owner ast.Node, members []ast.Node, scope *symbols.Scope, resolver *resolve.Resolver) Block { block := Block{Node: owner, Scope: scope} for _, member := range members { actual := unwrapMembership(member) if actual == nil || isAnnotation(actual) { continue } - if stmt, ok := ConstraintStep(actual, scope); ok { + if stmt, ok := ConstraintStep(actual, scope, resolver); ok { block.Statements = append(block.Statements, stmt) } } diff --git a/internal/ir/lower/errors.go b/internal/ir/lower/errors.go new file mode 100644 index 0000000000..386208d12b --- /dev/null +++ b/internal/ir/lower/errors.go @@ -0,0 +1,11 @@ +package lower + +import "errors" + +var ( + ErrCyclicSpecialization = errors.New("action specializes itself") + ErrRecursiveActionTyping = errors.New("typed action body performs a type it is nested in") + ErrRedefinedStepMissing = errors.New("redefined action step not found") + ErrIncompatibleRedefinedStep = errors.New("redefining feature is not an action step") + ErrAmbiguousInheritedStep = errors.New("inherited succession reaches more than one redefining step") +) diff --git a/internal/ir/lower/state_behavior.go b/internal/ir/lower/state_behavior.go index a37d02fa57..be9c4ecb5e 100644 --- a/internal/ir/lower/state_behavior.go +++ b/internal/ir/lower/state_behavior.go @@ -79,15 +79,17 @@ func lowerStateBehavior(action ast.Node, block ast.Node, scope *symbols.Scope, r behavior.Body = []Statement{Block{ Node: node, Scope: scope, - Graph: lowerBlockFlow([]ast.Node{node}, scope, false), + Graph: ToActionNodeFlow(node, scope, resolver), Own: true, }} case node.Kind == ast.UsageAction && node.HasBody && performsAction(node): - // Which of the two the behavior performs would be a silent pick. - behavior.Body = []Statement{Unsupported{ - Description: "performing an action and stating a body of its own", - Node: node, - Scope: scope, + bodyScope := childScope(scope, node) + behavior.Body = []Statement{Block{ + Node: node, + Scope: bodyScope, + Graph: ToActionNodeFlow(node, scope, resolver), + Own: true, + Stated: true, }} case node.Kind == ast.UsageAction && node.HasBody: // The body is a namespace of its own, so its locals are declared in the @@ -103,7 +105,7 @@ func lowerStateBehavior(action ast.Node, block ast.Node, scope *symbols.Scope, r behavior.Name = node.Name behavior.Body = lowerActionExecution(node, scope) default: - behavior.Body = []Statement{lowerStatement(action, scope)} + behavior.Body = []Statement{lowerStatement(action, scope, resolver)} } behavior.Nodes = blockNodesOf(behavior.Body, nil) behavior.Multiplicities = make(map[ast.Node]*ast.Multiplicity) @@ -124,7 +126,7 @@ func lowerStateBehavior(action ast.Node, block ast.Node, scope *symbols.Scope, r // one stating none runs its statements in declaration order. func lowerBehaviorBody(node *ast.Usage, scope *symbols.Scope, resolver *resolve.Resolver) Statement { if !statesOwnFlow(node.Members) { - return lowerBlock(node, node.Members, scope) + return lowerBlock(node, node.Members, scope, resolver) } graph, err := ToActionGraphWith(node, scope, resolver) if err != nil { @@ -157,9 +159,14 @@ func lowerActionExecution(node *ast.ActionExecutionNode, scope *symbols.Scope) [ func declaresOnlyFeatures(members []ast.Node) bool { for _, member := range members { actual := unwrapMembership(member) - if actual == nil || statesNoStep(actual) { + if actual == nil || statesNoStep(actual) || isAnnotation(actual) { continue } + if usage, ok := actual.(*ast.Usage); ok && usage.Kind == ast.UsageAttribute && usage.Direction == ast.DirNone { + if redefinitions, _ := ast.SplitRedefinitions(usage.Relationships); len(redefinitions) > 0 { + return false + } + } if m, ok := actual.(*ast.Usage); !ok || !DeclaresNodeFeature(m) { return false } diff --git a/internal/ir/lower/step_multiplicity.go b/internal/ir/lower/step_multiplicity.go index 3be96ba778..0d7878a1b2 100644 --- a/internal/ir/lower/step_multiplicity.go +++ b/internal/ir/lower/step_multiplicity.go @@ -22,13 +22,14 @@ const ( // StepMultiplicityError describes a step multiplicity the executor cannot honor. type StepMultiplicityError struct { - Node ast.Node - Step string - Multiplicity string - Code string - Reason string - Declaration ast.Node - Err error + Node ast.Node + Step string + Multiplicity string + Code string + Reason string + Declaration ast.Node + InheritedFrom string + Err error } func (e *StepMultiplicityError) Error() string { @@ -47,7 +48,11 @@ func (e *StepMultiplicityError) Error() string { if reason == "" { reason = "the action step multiplicity is unsupported" } - return fmt.Sprintf("action step %s%s: %s", step, multiplicity, reason) + message := fmt.Sprintf("action step %s%s: %s", step, multiplicity, reason) + if e.InheritedFrom != "" { + message += " (inherited from " + e.InheritedFrom + ")" + } + return message } func (e *StepMultiplicityError) Unwrap() error { @@ -57,25 +62,40 @@ func (e *StepMultiplicityError) Unwrap() error { return e.Err } -// StepCount returns the fixed number of performances declared for node. -// An absent declaration preserves the historical single-performance behavior. +// StepCount returns the fixed number of performances an owned or inherited +// multiplicity governs; when none does, the step performs once. func (g *ActionGraph) StepCount(node ast.Node, model *semantics.Model) (int64, error) { if g == nil || node == nil { return 1, nil } - multiplicity := g.Multiplicities[node] - if multiplicity == nil { + evaluator := model + if evaluator == nil { + evaluator = semantics.NewModel(g.resolver) + } + multiplicity, scope, source, found := g.stepMultiplicity(node, evaluator) + if !found { return 1, nil } - if err := g.unaddressableBoundError(node, multiplicity, g.nodeScope(node), model, nil); err != nil { + if multiplicity == nil { + rangeIn, ok := evaluator.MultiplicityOf(source) + if !ok { + return 1, nil + } + return g.fixedStepCount(node, rangeIn, model) + } + if err := g.unaddressableBoundError(node, multiplicity, scope, evaluator, nil); err != nil { return 0, err } - evaluator := model - if evaluator == nil { - evaluator = semantics.NewModel(nil) + rangeIn, ok := evaluator.RangeIn(scope, multiplicity) + if !ok { + return 0, g.stepError(node, model, StepMultiplicityNotFixedCode, + "multiplicity is not a fixed count; the executor performs a step a fixed number of times", nil) } - rangeIn, ok := evaluator.RangeIn(g.nodeScope(node), multiplicity) - if !ok || !rangeIn.Lower.Known || !rangeIn.Upper.Known || + return g.fixedStepCount(node, rangeIn, model) +} + +func (g *ActionGraph) fixedStepCount(node ast.Node, rangeIn semantics.Range, model *semantics.Model) (int64, error) { + if !rangeIn.Lower.Known || !rangeIn.Upper.Known || rangeIn.Lower.Infinite || rangeIn.Upper.Infinite || rangeIn.Lower.Value != rangeIn.Upper.Value || rangeIn.Lower.Value < 0 { @@ -85,21 +105,39 @@ func (g *ActionGraph) StepCount(node ast.Node, model *semantics.Model) (int64, e return rangeIn.Lower.Value, nil } -// MultiplicityText returns the source text of node's declared multiplicity. +// MultiplicityText returns the source text of node's effective multiplicity. func (g *ActionGraph) MultiplicityText(node ast.Node, model *semantics.Model) string { if g == nil || node == nil { return "" } - multiplicity := g.Multiplicities[node] + evaluator := model + if evaluator == nil { + evaluator = semantics.NewModel(g.resolver) + } + multiplicity, scope, source, _ := g.stepMultiplicity(node, evaluator) if multiplicity == nil { + if source != nil { + if rangeIn, ok := evaluator.MultiplicityOf(source); ok { + return semanticsRangeText(rangeIn) + } + } return "" } + if source != nil { + if text := g.sourceTextAt(scope, multiplicity, model); text != "" { + return text + } + } return g.multiplicityText(node, multiplicity, model) } // CheckStep classifies a declared multiplicity and its incident successions. func (g *ActionGraph) CheckStep(node ast.Node, model *semantics.Model) error { - if g == nil || node == nil || g.Multiplicities[node] == nil { + if g == nil || node == nil { + return nil + } + _, _, _, found := g.stepMultiplicity(node, model) + if !found { return nil } count, err := g.StepCount(node, model) @@ -153,6 +191,91 @@ func (g *ActionGraph) CheckStep(node ast.Node, model *semantics.Model) error { return nil } +func (g *ActionGraph) StepMultiplicity(node ast.Node, model *semantics.Model) (*ast.Multiplicity, *symbols.Scope) { + if g == nil || node == nil { + return nil, nil + } + evaluator := model + if evaluator == nil { + evaluator = semantics.NewModel(g.resolver) + } + multiplicity, scope, _, _ := g.stepMultiplicity(node, evaluator) + return multiplicity, scope +} + +// HasStepMultiplicity reports whether a node has an owned or effective step +// multiplicity, including recorded library facts. +func (g *ActionGraph) HasStepMultiplicity(node ast.Node, model *semantics.Model) bool { + if g == nil || node == nil { + return false + } + evaluator := model + if evaluator == nil { + evaluator = semantics.NewModel(g.resolver) + } + _, _, _, found := g.stepMultiplicity(node, evaluator) + return found +} + +func (g *ActionGraph) stepMultiplicity(node ast.Node, model *semantics.Model) (*ast.Multiplicity, *symbols.Scope, *symbols.Symbol, bool) { + if model == nil { + model = semantics.NewModel(g.resolver) + } + if multiplicity := g.Multiplicities[node]; multiplicity != nil { + return multiplicity, g.nodeScope(node), nil, true + } + usage, ok := node.(*ast.Usage) + if !ok || (usage.Kind != ast.UsageAction && !IsCaseNode(usage)) { + return nil, nil, nil, false + } + sym := actionStepSymbol(node, g.nodeScope(node)) + if sym == nil { + return nil, nil, nil, false + } + source, ok := model.GoverningMultiplicitySource(sym) + if !ok { + return nil, nil, nil, false + } + multiplicity := semantics.UsageMultiplicityOf(source) + if multiplicity != nil { + return multiplicity, source.OwnerScope, source, true + } + if source.Recorded() { + return nil, source.OwnerScope, source, true + } + return nil, nil, nil, false +} + +func semanticsRangeText(r semantics.Range) string { + bound := func(value semantics.Bound) string { + if value.Infinite { + return "*" + } + if value.Known { + return fmt.Sprint(value.Value) + } + return "?" + } + if r.Lower.Known && r.Upper.Known && r.Lower == r.Upper { + return "[" + bound(r.Lower) + "]" + } + return "[" + bound(r.Lower) + ".." + bound(r.Upper) + "]" +} + +func actionStepSymbol(node ast.Node, scope *symbols.Scope) *symbols.Symbol { + if node == nil { + return nil + } + for current := scope; current != nil; current = current.Parent() { + for _, member := range current.AllMembers() { + if member != nil && member.Decl == node { + return member + } + } + } + return nil +} + func (g *ActionGraph) checkRepeatedEdge(node ast.Node, edge ActionEdge, count int64, model *semantics.Model) error { other := edge.Source if other == node { @@ -184,11 +307,11 @@ func (g *ActionGraph) checkRepeatedEdgeOrder(node ast.Node, edge ActionEdge, cou if err != nil { return err } - sourceRange, err := g.crossingRange(node, edge.Source, edge.SourceMultiplicity, model) + sourceRange, err := g.crossingRange(node, edge.SourceMultiplicity, edge.Decl, model) if err != nil { return err } - targetRange, err := g.crossingRange(node, edge.Target, edge.TargetMultiplicity, model) + targetRange, err := g.crossingRange(node, edge.TargetMultiplicity, edge.Decl, model) if err != nil { return err } @@ -205,6 +328,13 @@ func (g *ActionGraph) checkRepeatedEdgeOrder(node ast.Node, edge ActionEdge, cou t = defaultRange } forced := s.lower >= sourceCount || t.lower >= targetCount + if succession, ok := edge.Decl.(*ast.SuccessionEdge); ok && + succession.SourceImplied && succession.TargetImplied && + edge.SourceMultiplicity == nil && edge.TargetMultiplicity == nil { + forced = false + } else if edge.Decl == nil { + forced = s.lower >= sourceCount || t.lower >= targetCount + } admitted := s.admits(sourceCount) && t.admits(targetCount) if forced && !admitted { return g.stepError(node, model, StepOrderUnsatisfiableCode, @@ -250,21 +380,29 @@ func (r crossingRange) admits(count int64) bool { return r.lower <= count && (r.upperInfinite || count <= r.upper) } -func (g *ActionGraph) crossingRange(step, endpoint ast.Node, multiplicity *ast.Multiplicity, model *semantics.Model) (crossingRange, error) { +func (g *ActionGraph) crossingRange(step ast.Node, multiplicity *ast.Multiplicity, edgeDecl ast.Node, model *semantics.Model) (crossingRange, error) { if multiplicity == nil { return crossingRange{}, nil } - if err := g.unaddressableBoundError(step, multiplicity, g.Scope, model, multiplicity); err != nil { + scope := g.Scope + if g.declaredIn[edgeDecl] != nil { + scope = g.declaredIn[edgeDecl] + } + if err := g.unaddressableBoundError(step, multiplicity, scope, model, multiplicity); err != nil { return crossingRange{}, err } evaluator := model if evaluator == nil { - evaluator = semantics.NewModel(nil) + evaluator = semantics.NewModel(g.resolver) } - r, ok := evaluator.RangeIn(g.Scope, multiplicity) + r, ok := evaluator.RangeIn(scope, multiplicity) if !ok || !r.Lower.Known || r.Lower.Infinite || (!r.Upper.Known && !r.Upper.Infinite) { + text := g.sourceTextAt(scope, multiplicity, model) + if text == "" { + text = multiplicityBoundsText(multiplicity) + } err := g.stepError(step, model, StepMultiplicityNotFixedCode, - "succession-end multiplicity "+g.multiplicityText(step, multiplicity, model)+" cannot be evaluated", multiplicity) + "succession-end multiplicity "+text+" cannot be evaluated", multiplicity) return crossingRange{}, err } return crossingRange{ @@ -616,13 +754,42 @@ func (g *ActionGraph) stepError(node ast.Node, model *semantics.Model, code, rea if name == "" { name = fmt.Sprintf("%T", node) } + inheritedFrom := "" + if g != nil { + evaluator := model + if evaluator == nil { + evaluator = semantics.NewModel(g.resolver) + } + multiplicity, _, source, _ := g.stepMultiplicity(node, evaluator) + stepSymbol := actionStepSymbol(node, g.nodeScope(node)) + if source == nil && stepSymbol != nil { + source, _ = evaluator.GoverningMultiplicitySource(stepSymbol) + } + if source != nil && stepSymbol != nil && source != stepSymbol { + for _, redefined := range evaluator.AllRedefinedFeatures(stepSymbol) { + if redefined == source { + inheritedFrom = symbols.FQNOf(source) + break + } + } + } + if declaration == nil { + if multiplicity != nil { + declaration = multiplicity + } + if source != nil && declaration == nil { + declaration = source.Decl + } + } + } return &StepMultiplicityError{ - Node: node, - Step: name, - Multiplicity: g.MultiplicityText(node, model), - Code: code, - Reason: reason, - Declaration: declaration, + Node: node, + Step: name, + Multiplicity: g.MultiplicityText(node, model), + Code: code, + Reason: reason, + Declaration: declaration, + InheritedFrom: inheritedFrom, } } @@ -636,6 +803,10 @@ func (g *ActionGraph) multiplicityText(node ast.Node, multiplicity *ast.Multipli if text := g.sourceTextWithModel(node, multiplicity, model); text != "" { return text } + return multiplicityBoundsText(multiplicity) +} + +func multiplicityBoundsText(multiplicity *ast.Multiplicity) string { lower := boundText(multiplicity.Lower) if !multiplicity.IsRange { return "[" + lower + "]" @@ -654,6 +825,14 @@ func (g *ActionGraph) sourceTextWithModel(node, textNode ast.Node, model *semant return "" } +func (g *ActionGraph) sourceTextAt(scope *symbols.Scope, textNode ast.Node, model *semantics.Model) string { + if scope == nil || textNode == nil || model == nil || model.SourceText() == nil { + return "" + } + text := model.SourceText()(symbols.DocNameOf(scope), textNode.Span()) + return strings.TrimSpace(text) +} + func boundText(node ast.Node) string { switch value := node.(type) { case *ast.LiteralInteger: diff --git a/internal/ir/view/action_pins_test.go b/internal/ir/view/action_pins_test.go index 91bdd88ec0..ddd79e44c1 100644 --- a/internal/ir/view/action_pins_test.go +++ b/internal/ir/view/action_pins_test.go @@ -94,13 +94,17 @@ func TestActionFramePinsDeclared(t *testing.T) { } } -// A usage takes its frame pins from its type, as a nested node takes its pins -// from the action it performs, and its own nodes bind to them. +// A typed usage's inherited nodes keep their bindings to its frame pins, beside +// bindings from nodes the usage declares itself. func TestActionFramePinsInheritedByUsage(t *testing.T) { rendering := renderActionPins(t, "Pins::toaster") wantNoNotices(t, rendering) wantEqual(t, "frame pins", pinTexts(rendering.Roots[0]), []string{"in bread", "out toast"}) - wantEqual(t, "bindings", edgeTexts(rendering, EdgeBinding), []string{"Pins::toaster.bread == heat2.b"}) + wantEqual(t, "bindings", edgeTexts(rendering, EdgeBinding), []string{ + "Pins::toaster.bread == heat2.b", "Pins::toaster.bread == heat.b", "pack.boxed == Pins::toaster.toast", + }) + wantEqual(t, "flows", edgeTexts(rendering, EdgeFlow), []string{"heat.t => pack.t"}) + wantEdgeEndsDrawn(t, rendering) } // The other spellings bind the same way: a qualified value (`in b = @@ -160,13 +164,16 @@ func TestActionBindingShadowedParameterDrawsNothing(t *testing.T) { wantEqual(t, "bindings", edgeTexts(rendering, EdgeBinding), nil) } -// A specializing action definition takes its frame pins from its general, as -// a usage does from its type, and its own node binds to one. +// A specialization keeps its general's inherited node bindings to frame pins +// and adds bindings from nodes it declares itself. func TestActionFramePinsInheritedBySpecialization(t *testing.T) { rendering := renderActionPins(t, "Pins::Specialized") wantNoNotices(t, rendering) wantEqual(t, "frame pins", pinTexts(rendering.Roots[0]), []string{"in bread", "out toast"}) - wantEqual(t, "bindings", edgeTexts(rendering, EdgeBinding), []string{"Pins::Specialized.bread == heat2.b"}) + wantEqual(t, "bindings", edgeTexts(rendering, EdgeBinding), []string{ + "Pins::Specialized.bread == heat2.b", "Pins::Specialized.bread == heat.b", "pack.boxed == Pins::Specialized.toast", + }) + wantEqual(t, "flows", edgeTexts(rendering, EdgeFlow), []string{"heat.t => pack.t"}) wantEdgeEndsDrawn(t, rendering) } diff --git a/internal/semantic/resolve/transition.go b/internal/semantic/resolve/transition.go index a6deac2849..99e73b354a 100644 --- a/internal/semantic/resolve/transition.go +++ b/internal/semantic/resolve/transition.go @@ -212,6 +212,101 @@ func RedefinesActionNode(body *symbols.Scope, node *ast.Usage, decl ast.Node) bo return redefinesActionNode(body, node, decl, make(map[*ast.Usage]bool)) } +// ActionNodeRedefinitionTargets returns inherited action nodes named by a +// member's redefines and, when subsets is true, subsets relationships. +func ActionNodeRedefinitionTargets(body *symbols.Scope, decl ast.Node, subsets bool) []ast.Node { + if body == nil || decl == nil { + return nil + } + var rels []*ast.Relationship + switch node := decl.(type) { + case *ast.Usage: + rels = node.Relationships + case *ast.Definition: + rels = node.Relationships + default: + return nil + } + var targets []ast.Node + seen := make(map[ast.Node]bool) + for _, rel := range rels { + if rel == nil || (rel.Kind != ast.RelRedefines && (!subsets || rel.Kind != ast.RelSubsets)) { + continue + } + target := rel.Target + if fr, ok := target.(*ast.FeatureReference); ok { + target = fr.Name + } + qn, ok := target.(*ast.QualifiedName) + if !ok || len(qn.Parts) == 0 { + continue + } + var qualifier *symbols.Symbol + if len(qn.Parts) > 1 { + qualifier, _ = lookupScopeParts(body.Parent(), qn.Parts[:len(qn.Parts)-1]) + if qualifier != nil && qualifier.Decl == body.Node() { + qualifier = nil + } + } + node, _, found, _ := inheritedActionNode(body, qn.Parts[len(qn.Parts)-1].Text, qualifier, make(map[*symbols.Symbol]bool)) + if found && !seen[node] { + seen[node] = true + targets = append(targets, node) + } + } + return targets +} + +// ActionNodeDeclaringScope returns the action body that declares an inherited +// action node. +func ActionNodeDeclaringScope(scope *symbols.Scope, decl ast.Node) *symbols.Scope { + body := enclosingActionScope(scope) + if body == nil || decl == nil { + return nil + } + candidates := append([]*symbols.Scope{body}, ActionGeneralBodies(body)...) + for _, candidate := range candidates { + for _, member := range candidate.AllMembers() { + if member != nil && member.Decl == decl { + return candidate + } + } + } + return nil +} + +// MissingRedefinedActionNode returns the name of a target a nested action +// usage says it redefines but which is not an inherited action node. +func MissingRedefinedActionNode(body *symbols.Scope, usage *ast.Usage) string { + if body == nil || usage == nil || !isActionNode(usage) { + return "" + } + for _, rel := range usage.Relationships { + if rel == nil || rel.Kind != ast.RelRedefines { + continue + } + target := rel.Target + if fr, ok := target.(*ast.FeatureReference); ok { + target = fr.Name + } + qn, ok := target.(*ast.QualifiedName) + if !ok || len(qn.Parts) == 0 { + continue + } + var qualifier *symbols.Symbol + if len(qn.Parts) > 1 { + qualifier, _ = lookupScopeParts(body.Parent(), qn.Parts[:len(qn.Parts)-1]) + if qualifier != nil && qualifier.Decl == body.Node() { + qualifier = nil + } + } + if _, _, found, _ := inheritedActionNode(body, qn.Parts[len(qn.Parts)-1].Text, qualifier, make(map[*symbols.Symbol]bool)); !found { + return qnText(qn) + } + } + return "" +} + func redefinesActionNode(body *symbols.Scope, node *ast.Usage, decl ast.Node, seen map[*ast.Usage]bool) bool { if body == nil || node == nil || seen[node] { return false @@ -266,35 +361,52 @@ func enclosingActionScope(scope *symbols.Scope) *symbols.Scope { return nil } -// ActionGeneralBodies returns the bodies of the actions the action enclosing -// scope specializes or is typed by, nearest first and each once, found from the -// scope tree alone — where a member the action inherits was declared. -func ActionGeneralBodies(scope *symbols.Scope) []*symbols.Scope { +// ActionGeneralization returns the bodies of the actions the action enclosing +// scope specializes, subsets, redefines, or is typed by, nearest first and each +// once, found from the scope tree alone — where a member the action inherits was +// declared. cyclic reports a generalization cycle; a diamond is not a cycle. +func ActionGeneralization(scope *symbols.Scope, typing bool) (bodies []*symbols.Scope, cyclic bool) { body := enclosingActionScope(scope) if body == nil { - return nil + return nil, false } - var bodies []*symbols.Scope - seen := make(map[*symbols.Symbol]bool) + seen := make(map[*symbols.Scope]bool) + onStack := map[*symbols.Scope]bool{body: true} var collect func(body *symbols.Scope) collect = func(body *symbols.Scope) { - supers, _ := declaredGenerals(body) + supers, _ := declaredGenerals(body, typing) for _, super := range supers { - if seen[super] || super.Scope == nil { + if super == nil || super.Scope == nil { + continue + } + if onStack[super.Scope] { + cyclic = true + continue + } + if seen[super.Scope] { continue } - seen[super] = true + seen[super.Scope] = true bodies = append(bodies, super.Scope) + onStack[super.Scope] = true collect(super.Scope) + delete(onStack, super.Scope) } } collect(body) + return bodies, cyclic +} + +// ActionGeneralBodies returns the bodies of the actions the action enclosing +// scope specializes or is typed by, nearest first and each once. +func ActionGeneralBodies(scope *symbols.Scope) []*symbols.Scope { + bodies, _ := ActionGeneralization(scope, true) return bodies } // declaredGenerals returns the symbols the generalizations body's declaration // states, in declaration order, and whether one of them resolves to nothing. -func declaredGenerals(body *symbols.Scope) ([]*symbols.Symbol, bool) { +func declaredGenerals(body *symbols.Scope, typing bool) ([]*symbols.Symbol, bool) { var rels []*ast.Relationship switch n := body.Node().(type) { case *ast.Definition: @@ -307,8 +419,9 @@ func declaredGenerals(body *symbols.Scope) ([]*symbols.Symbol, bool) { var supers []*symbols.Symbol var uncertain bool for _, rel := range rels { - if rel == nil || (rel.Kind != ast.RelSpecializes && rel.Kind != ast.RelTyping && - rel.Kind != ast.RelSubsets && rel.Kind != ast.RelRedefines) { + if rel == nil || (rel.Kind != ast.RelSpecializes && rel.Kind != ast.RelSubsets && + rel.Kind != ast.RelRedefines && (!typing || + (rel.Kind != ast.RelTyping && rel.Kind != ast.RelReferences))) { continue } target := rel.Target @@ -320,6 +433,28 @@ func declaredGenerals(body *symbols.Scope) ([]*symbols.Symbol, bool) { continue } super, ok := lookupScopeQualified(body.Parent(), qn) + // A nested redefining action may answer to its own name in its owner's + // scope. Its target is the inherited action node of that name, not itself. + if ok && super != nil && super.Decl == body.Node() { + if owner := body.Parent(); owner != nil { + var qualifier *symbols.Symbol + if len(qn.Parts) > 1 { + qualifier, _ = lookupScopeParts(owner.Parent(), qn.Parts[:len(qn.Parts)-1]) + if qualifier != nil && qualifier.Decl == owner.Node() { + qualifier = nil + } + } + if inherited, inheritedScope, found, unknown := inheritedActionNode(owner, qn.Parts[len(qn.Parts)-1].Text, qualifier, make(map[*symbols.Symbol]bool)); found { + super = symbolDeclaring(inheritedScope, inherited) + if unknown { + uncertain = true + } + ok = super != nil + } else { + ok = false + } + } + } if !ok || super == nil { uncertain = true continue @@ -329,8 +464,20 @@ func declaredGenerals(body *symbols.Scope) ([]*symbols.Symbol, bool) { return supers, uncertain } +func symbolDeclaring(scope *symbols.Scope, decl ast.Node) *symbols.Symbol { + if scope == nil || decl == nil { + return nil + } + for _, member := range scope.AllMembers() { + if member.Decl == decl { + return member + } + } + return nil +} + func inheritedActionNode(body *symbols.Scope, name string, qualifier *symbols.Symbol, seen map[*symbols.Symbol]bool) (ast.Node, *symbols.Scope, bool, bool) { - supers, uncertain := declaredGenerals(body) + supers, uncertain := declaredGenerals(body, true) for _, super := range supers { if qualifier != nil && super != qualifier { continue @@ -494,10 +641,12 @@ func lookupScopePartsText(scope *symbols.Scope, parts []string) (*symbols.Symbol func isActionNode(decl ast.Node) bool { switch n := decl.(type) { case *ast.Usage: - return n.Kind == ast.UsageAction || IsAssertion(n) - case *ast.ForkNode, *ast.JoinNode, *ast.MergeNode, *ast.DecisionNode, - *ast.ActionExecutionNode, *ast.WhileLoopActionNode, *ast.IfActionNode, - *ast.AssignmentActionNode, *ast.SendStatement, *ast.TerminateStatement: + return n.Kind == ast.UsageAction || n.Kind == ast.UsageAnalysisCase || + n.Kind == ast.UsageVerificationCase || IsAssertion(n) + case *ast.InitialNode, *ast.FinalNode, *ast.ForkNode, *ast.JoinNode, *ast.MergeNode, + *ast.DecisionNode, *ast.ActionExecutionNode, *ast.PerformActionNode, + *ast.WhileLoopActionNode, *ast.IfActionNode, *ast.AssignmentActionNode, + *ast.SendStatement, *ast.TerminateStatement: return true } return false diff --git a/internal/semantic/resolve/transition_test.go b/internal/semantic/resolve/transition_test.go index abdb1647d8..7faccb5333 100644 --- a/internal/semantic/resolve/transition_test.go +++ b/internal/semantic/resolve/transition_test.go @@ -10,6 +10,96 @@ import ( "github.com/Open-MBEE/OpenSysML/internal/syntax/source" ) +func TestActionGeneralizationOrderTypingAndCycles(t *testing.T) { + scope := actionBodyScope(t, `package test { + action def Core; + action def Left :> Core; + action def Right :> Core; + action def Diamond :> Left, Right; + action def Typed; + action Base : Typed; + action def Derived :> Base; + action def A :> B; + action def B :> A; + }`) + + diamond, cyclic := ActionGeneralization(scope("Diamond"), true) + if cyclic { + t.Fatal("Diamond generalization reported a cycle") + } + if got, want := actionGeneralizationNames(diamond), []string{"Left", "Core", "Right"}; !equalNames(got, want) { + t.Fatalf("Diamond generals = %v, want %v", got, want) + } + + derived, cyclic := ActionGeneralization(scope("Derived"), false) + if cyclic { + t.Fatal("Derived specialization reported a cycle") + } + if got, want := actionGeneralizationNames(derived), []string{"Base"}; !equalNames(got, want) { + t.Fatalf("Derived non-typing generals = %v, want %v", got, want) + } + typed, cyclic := ActionGeneralization(scope("Derived"), true) + if cyclic { + t.Fatal("Derived typing-inclusive generalization reported a cycle") + } + if got, want := actionGeneralizationNames(typed), []string{"Base", "Typed"}; !equalNames(got, want) { + t.Fatalf("Derived typing-inclusive generals = %v, want %v", got, want) + } + + _, cyclic = ActionGeneralization(scope("A"), false) + if !cyclic { + t.Fatal("A -> B -> A did not report a cycle") + } +} + +func actionBodyScope(t *testing.T, src string) func(string) *symbols.Scope { + t.Helper() + const name = "generalization.sysml" + p := parser.New(source.New(name, []byte(src))) + root := p.ParseFile() + if len(p.Diagnostics) != 0 { + t.Fatalf("parse diagnostics: %v", p.Diagnostics) + } + index := symbols.NewIndexFromDoc(name, root) + doc := index.DocumentRoot(name) + pkg, ok := doc.LookupLocal("test") + if !ok { + t.Fatal("package test not found") + } + return func(action string) *symbols.Scope { + t.Helper() + sym, ok := pkg.Scope.LookupLocal(action) + if !ok || sym.Scope == nil { + t.Fatalf("action body scope %s not found", action) + } + return sym.Scope + } +} + +func actionGeneralizationNames(scopes []*symbols.Scope) []string { + names := make([]string, 0, len(scopes)) + for _, scope := range scopes { + if owner := scope.Owner(); owner != nil { + names = append(names, owner.Name) + } else { + names = append(names, "") + } + } + return names +} + +func equalNames(got, want []string) bool { + if len(got) != len(want) { + return false + } + for i := range got { + if got[i] != want[i] { + return false + } + } + return true +} + // transitionTargetIn returns the target name of the first transition declared // in root, the name node name resolution keyed its verdict by. func transitionTargetIn(t *testing.T, root *ast.RootNamespace) *ast.QualifiedName { diff --git a/internal/semantic/semantics/collection.go b/internal/semantic/semantics/collection.go index 086b5f1091..f84e7c49a8 100644 --- a/internal/semantic/semantics/collection.go +++ b/internal/semantic/semantics/collection.go @@ -103,21 +103,37 @@ func (m *Model) resultHoldsNothing(result *symbols.Symbol) bool { return ok && r.Upper.Known && !r.Upper.Infinite && r.Upper.Value == 0 } -// governingMultiplicity is the multiplicity a feature declares, or inherits from a feature it -// redefines by clause or position, read through an alias; not ok where it declares and inherits none. -func (m *Model) governingMultiplicity(sym *symbols.Symbol) (Range, bool) { - if alias, ok := m.resolver.ResolveAliasTarget(sym); ok && alias != nil { - sym = alias +// GoverningMultiplicitySource returns the alias-resolved feature whose declared +// multiplicity governs sym: sym itself when it declares one, else the first +// feature it redefines that does. Subsetting alone does not inherit multiplicity. +func (m *Model) GoverningMultiplicitySource(sym *symbols.Symbol) (*symbols.Symbol, bool) { + if m == nil || sym == nil { + return nil, false } - if declared, ok := m.MultiplicityOf(sym); ok { - return declared, true + if m.resolver != nil { + if alias, ok := m.resolver.ResolveAliasTarget(sym); ok && alias != nil { + sym = alias + } + } + if _, ok := m.MultiplicityOf(sym); ok { + return sym, true } for _, redefined := range m.AllRedefinedFeatures(sym) { - if inherited, ok := m.MultiplicityOf(redefined); ok { - return inherited, true + if _, ok := m.MultiplicityOf(redefined); ok { + return redefined, true } } - return Range{}, false + return nil, false +} + +// governingMultiplicity is the multiplicity a feature declares, or inherits from a feature it +// redefines by clause or position, read through an alias; not ok where it declares and inherits none. +func (m *Model) governingMultiplicity(sym *symbols.Symbol) (Range, bool) { + source, ok := m.GoverningMultiplicitySource(sym) + if !ok { + return Range{}, false + } + return m.MultiplicityOf(source) } // GoverningMultiplicityOf returns the multiplicity governing a feature: the one diff --git a/internal/semantic/semantics/multiplicity_test.go b/internal/semantic/semantics/multiplicity_test.go index ace5c8de18..90b40dad69 100644 --- a/internal/semantic/semantics/multiplicity_test.go +++ b/internal/semantic/semantics/multiplicity_test.go @@ -105,6 +105,31 @@ func TestMultiplicityNoneWhenAbsent(t *testing.T) { } } +func TestGoverningMultiplicitySourceUsesRedefinitionNotSubsetting(t *testing.T) { + m, root := buildModel(t, `package P { + action def Base { action a[3]; } + action def Redefined :> Base { action :>> a; } + action def Subset { action b :> Base::a; } + }`) + p := sym(t, root, "P") + base := sym(t, p.Scope, "Base") + baseStep, ok := base.Scope.LookupLocal("a") + if !ok { + t.Fatal("Base::a not found") + } + + redefined := sym(t, sym(t, p.Scope, "Redefined").Scope, "a") + source, ok := m.GoverningMultiplicitySource(redefined) + if !ok || source != baseStep { + t.Fatalf("GoverningMultiplicitySource(Redefined::a) = %v, %v; want Base::a", source, ok) + } + + subset := sym(t, sym(t, p.Scope, "Subset").Scope, "b") + if source, ok := m.GoverningMultiplicitySource(subset); ok { + t.Fatalf("GoverningMultiplicitySource(Subset::b) = %s, want no source from subsetting", source.Name) + } +} + // A feature that declares no multiplicity holds exactly one value, so the // effective multiplicity of a bare usage is the assumed 1..1. func TestEffectiveMultiplicityAssumesOne(t *testing.T) {